deps: update

Spotless -> 6.15.0
Core -> 1.9.0
This commit is contained in:
Alexander Capehart 2023-02-24 21:57:01 -07:00
parent d6e7b99e1f
commit 83c5b85424
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
158 changed files with 889 additions and 226 deletions

View file

@ -82,7 +82,7 @@ dependencies {
implementation "androidx.recyclerview:recyclerview:1.2.1"
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
implementation "androidx.viewpager2:viewpager2:1.1.0-beta01"
implementation 'androidx.core:core-ktx:+'
implementation 'androidx.core:core-ktx:1.9.0'
// Lifecycle
def lifecycle_version = "2.5.1"

View file

@ -30,6 +30,7 @@ import org.oxycblt.auxio.ui.UISettings
/**
* A simple, rational music player for android.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@HiltAndroidApp

View file

@ -20,6 +20,7 @@ package org.oxycblt.auxio
/**
* 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 {

View file

@ -40,17 +40,13 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
/**
* Auxio's single [AppCompatActivity].
*
* TODO: Add error screens
*
* TODO: Custom language support
*
* TODO: Use proper material attributes (Not the weird dimen attributes I currently have)
*
* TODO: Migrate to material animation system
*
* TODO: Unit testing
*
* @author Alexander Capehart (OxygenCobalt)
*
* TODO: Add error screens
* TODO: Custom language support
* TODO: Use proper material attributes (Not the weird dimen attributes I currently have)
* TODO: Migrate to material animation system
* TODO: Unit testing
*/
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@ -112,9 +108,10 @@ class MainActivity : AppCompatActivity() {
/**
* 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.
* false otherwise.
*/
private fun startIntentAction(intent: Intent?): Boolean {
if (intent == null) {

View file

@ -51,6 +51,7 @@ import org.oxycblt.auxio.util.*
/**
* A wrapper around the home fragment that shows the playback fragment and controls the more
* high-level navigation features.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint

View file

@ -47,6 +47,7 @@ import org.oxycblt.auxio.util.*
/**
* A [ListFragment] that shows information about an [Album].
*
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint

View file

@ -50,6 +50,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
/**
* A [ListFragment] that shows information about an [Artist].
*
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint

View file

@ -44,6 +44,7 @@ import org.oxycblt.auxio.util.*
/**
* [ViewModel] that manages the Song, Album, Artist, and Genre detail views. Keeps track of the
* current item they are showing, sub-data to display, and configuration.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@HiltViewModel
@ -182,6 +183,7 @@ constructor(
/**
* Set a new [currentSong] from it's [Music.UID]. If the [Music.UID] differs, [currentSong] and
* [songAudioInfo] will be updated to align with the new [Song].
*
* @param uid The UID of the [Song] to load. Must be valid.
*/
fun setSongUid(uid: Music.UID) {
@ -196,6 +198,7 @@ constructor(
/**
* Set a new [currentAlbum] from it's [Music.UID]. If the [Music.UID] differs, [currentAlbum]
* and [albumList] will be updated to align with the new [Album].
*
* @param uid The [Music.UID] of the [Album] to update [currentAlbum] to. Must be valid.
*/
fun setAlbumUid(uid: Music.UID) {
@ -210,6 +213,7 @@ constructor(
/**
* Set a new [currentArtist] from it's [Music.UID]. If the [Music.UID] differs, [currentArtist]
* and [artistList] will be updated to align with the new [Artist].
*
* @param uid The [Music.UID] of the [Artist] to update [currentArtist] to. Must be valid.
*/
fun setArtistUid(uid: Music.UID) {
@ -224,6 +228,7 @@ constructor(
/**
* Set a new [currentGenre] from it's [Music.UID]. If the [Music.UID] differs, [currentGenre]
* and [genreList] will be updated to align with the new album.
*
* @param uid The [Music.UID] of the [Genre] to update [currentGenre] to. Must be valid.
*/
fun setGenreUid(uid: Music.UID) {
@ -239,6 +244,7 @@ constructor(
/**
* Start a new job to load a given [Song]'s [AudioInfo]. Result is pushed to [songAudioInfo].
*
* @param song The song to load.
*/
private fun refreshAudioInfo(song: Song) {
@ -333,8 +339,9 @@ constructor(
/**
* A simpler mapping of [ReleaseType] used for grouping and sorting songs.
*
* @param headerTitleRes The title string resource to use for a header created out of an
* instance of this enum.
* instance of this enum.
*/
private enum class AlbumGrouping(@StringRes val headerTitleRes: Int) {
ALBUMS(R.string.lbl_albums),

View file

@ -51,6 +51,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
/**
* A [ListFragment] that shows information for a particular [Genre].
*
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint

View file

@ -42,6 +42,7 @@ import org.oxycblt.auxio.util.concatLocalized
/**
* A [ViewBindingDialogFragment] that shows information about a Song.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint

View file

@ -43,6 +43,7 @@ import org.oxycblt.auxio.util.inflater
/**
* An [DetailAdapter] implementing the header and sub-items for the [Album] detail view.
*
* @param listener A [Listener] to bind interactions to.
* @author Alexander Capehart (OxygenCobalt)
*/
@ -118,6 +119,7 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene
/**
* A [RecyclerView.ViewHolder] that displays the [Album] header in the detail view. Use [from] to
* create an instance.
*
* @author Alexander Capehart (OxygenCobalt)
*/
private class AlbumDetailViewHolder private constructor(private val binding: ItemDetailBinding) :
@ -125,6 +127,7 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
/**
* Bind new data to this instance.
*
* @param album The new [Album] to bind.
* @param listener A [AlbumDetailAdapter.Listener] to bind interactions to.
*/
@ -164,6 +167,7 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
/**
* Create a new instance.
*
* @param parent The parent to inflate this instance from.
* @return A new instance.
*/
@ -187,12 +191,14 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
/**
* A [RecyclerView.ViewHolder] that displays a [Disc] to delimit different disc groups. Use [from]
* to create an instance.
*
* @author Alexander Capehart (OxygenCobalt)
*/
private class DiscViewHolder(private val binding: ItemDiscHeaderBinding) :
RecyclerView.ViewHolder(binding.root) {
/**
* Bind new data to this instance.
*
* @param disc The new [disc] to bind.
*/
fun bind(disc: Disc) {
@ -209,6 +215,7 @@ private class DiscViewHolder(private val binding: ItemDiscHeaderBinding) :
/**
* Create a new instance.
*
* @param parent The parent to inflate this instance from.
* @return A new instance.
*/
@ -227,12 +234,14 @@ private class DiscViewHolder(private val binding: ItemDiscHeaderBinding) :
/**
* A [RecyclerView.ViewHolder] that displays a [Song] in the context of an [Album]. Use [from] to
* create an instance.
*
* @author Alexander Capehart (OxygenCobalt)
*/
private class AlbumSongViewHolder private constructor(private val binding: ItemAlbumSongBinding) :
SelectionIndicatorAdapter.ViewHolder(binding.root) {
/**
* Bind new data to this instance.
*
* @param song The new [Song] to bind.
* @param listener A [SelectableListListener] to bind interactions to.
*/
@ -276,6 +285,7 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA
/**
* Create a new instance.
*
* @param parent The parent to inflate this instance from.
* @return A new instance.
*/

View file

@ -37,6 +37,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] to bind interactions to.
* @author Alexander Capehart (OxygenCobalt)
*/
@ -100,6 +101,7 @@ class ArtistDetailAdapter(private val listener: Listener<Music>) :
/**
* A [RecyclerView.ViewHolder] that displays the [Artist] header in the detail view. Use [from] to
* create an instance.
*
* @author Alexander Capehart (OxygenCobalt)
*/
private class ArtistDetailViewHolder private constructor(private val binding: ItemDetailBinding) :
@ -107,6 +109,7 @@ private class ArtistDetailViewHolder private constructor(private val binding: It
/**
* Bind new data to this instance.
*
* @param artist The new [Artist] to bind.
* @param listener A [DetailAdapter.Listener] to bind interactions to.
*/
@ -154,6 +157,7 @@ private class ArtistDetailViewHolder private constructor(private val binding: It
/**
* Create a new instance.
*
* @param parent The parent to inflate this instance from.
* @return A new instance.
*/
@ -175,12 +179,14 @@ private class ArtistDetailViewHolder private constructor(private val binding: It
/**
* A [RecyclerView.ViewHolder] that displays an [Album] in the context of an [Artist]. Use [from] to
* create an instance.
*
* @author Alexander Capehart (OxygenCobalt)
*/
private class ArtistAlbumViewHolder private constructor(private val binding: ItemParentBinding) :
SelectionIndicatorAdapter.ViewHolder(binding.root) {
/**
* Bind new data to this instance.
*
* @param album The new [Album] to bind.
* @param listener An [SelectableListListener] to bind interactions to.
*/
@ -209,6 +215,7 @@ private class ArtistAlbumViewHolder private constructor(private val binding: Ite
/**
* Create a new instance.
*
* @param parent The parent to inflate this instance from.
* @return A new instance.
*/
@ -227,12 +234,14 @@ private class ArtistAlbumViewHolder private constructor(private val binding: Ite
/**
* A [RecyclerView.ViewHolder] that displays a [Song] in the context of an [Artist]. Use [from] to
* create an instance.
*
* @author Alexander Capehart (OxygenCobalt)
*/
private class ArtistSongViewHolder private constructor(private val binding: ItemSongBinding) :
SelectionIndicatorAdapter.ViewHolder(binding.root) {
/**
* Bind new data to this instance.
*
* @param song The new [Song] to bind.
* @param listener An [SelectableListListener] to bind interactions to.
*/
@ -258,6 +267,7 @@ private class ArtistSongViewHolder private constructor(private val binding: Item
/**
* Create a new instance.
*
* @param parent The parent to inflate this instance from.
* @return A new instance.
*/

View file

@ -37,9 +37,10 @@ import org.oxycblt.auxio.util.inflater
/**
* A [RecyclerView.Adapter] that implements behavior shared across each detail view's adapters.
*
* @param listener A [Listener] to bind interactions to.
* @param diffCallback A [DiffUtil.ItemCallback] to use for item comparison when diffing the
* internal list.
* internal list.
* @author Alexander Capehart (OxygenCobalt)
*/
abstract class DetailAdapter(
@ -119,6 +120,7 @@ abstract class DetailAdapter(
/**
* A header variation that displays a button to open a sort menu.
*
* @param titleRes The string resource to use as the header title
* @author Alexander Capehart (OxygenCobalt)
*/
@ -127,12 +129,14 @@ data class SortHeader(@StringRes override val titleRes: Int) : Header
/**
* A [RecyclerView.ViewHolder] that displays a [SortHeader], a variation on [BasicHeader] that adds
* a button opening a menu for sorting. Use [from] to create an instance.
*
* @author Alexander Capehart (OxygenCobalt)
*/
private class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) :
RecyclerView.ViewHolder(binding.root) {
/**
* Bind new data to this instance.
*
* @param sortHeader The new [SortHeader] to bind.
* @param listener An [DetailAdapter.Listener] to bind interactions to.
*/
@ -152,6 +156,7 @@ private class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) :
/**
* Create a new instance.
*
* @param parent The parent to inflate this instance from.
* @return A new instance.
*/

View file

@ -38,6 +38,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] to bind interactions to.
* @author Alexander Capehart (OxygenCobalt)
*/
@ -100,12 +101,14 @@ class GenreDetailAdapter(private val listener: Listener<Music>) :
/**
* A [RecyclerView.ViewHolder] that displays the [Genre] header in the detail view. Use [from] to
* create an instance.
*
* @author Alexander Capehart (OxygenCobalt)
*/
private class GenreDetailViewHolder private constructor(private val binding: ItemDetailBinding) :
RecyclerView.ViewHolder(binding.root) {
/**
* Bind new data to this instance.
*
* @param genre The new [Song] to bind.
* @param listener A [DetailAdapter.Listener] to bind interactions to.
*/
@ -131,6 +134,7 @@ private class GenreDetailViewHolder private constructor(private val binding: Ite
/**
* Create a new instance.
*
* @param parent The parent to inflate this instance from.
* @return A new instance.
*/

View file

@ -33,6 +33,7 @@ import org.oxycblt.auxio.util.inflater
/**
* An adapter for [SongProperty] instances.
*
* @author Alexander Capehart (OxygenCobalt)
*/
class SongPropertyAdapter :
@ -48,6 +49,7 @@ class SongPropertyAdapter :
/**
* A property entry for use in [SongPropertyAdapter].
*
* @param name The contextual title to use for the property.
* @param value The value of the property.
* @author Alexander Capehart (OxygenCobalt)
@ -56,6 +58,7 @@ data class SongProperty(@StringRes val name: Int, val value: String) : Item
/**
* A [RecyclerView.ViewHolder] that displays a [SongProperty]. Use [from] to create an instance.
*
* @author Alexander Capehart (OxygenCobalt)
*/
class SongPropertyViewHolder private constructor(private val binding: ItemSongPropertyBinding) :
@ -69,6 +72,7 @@ class SongPropertyViewHolder private constructor(private val binding: ItemSongPr
companion object {
/**
* Create a new instance.
*
* @param parent The parent to inflate this instance from.
* @return A new instance.
*/

View file

@ -27,6 +27,7 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
/**
* A [FrameLayout] that automatically applies bottom insets.
*
* @author Alexander Capehart (OxygenCobalt)
*/
class EdgeFrameLayout

View file

@ -64,6 +64,7 @@ import org.oxycblt.auxio.util.*
/**
* The starting [SelectionFragment] of Auxio. Shows the user's music library and enables navigation
* to other views.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint
@ -483,10 +484,11 @@ class HomeFragment :
/**
* [FragmentStateAdapter] implementation for the [HomeFragment]'s [ViewPager2] instance.
*
* @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].
* [FragmentStateAdapter].
*/
private class HomePagerAdapter(
private val tabs: List<MusicMode>,

View file

@ -28,6 +28,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
/**
* User configuration specific to the home UI.
*
* @author Alexander Capehart (OxygenCobalt)
*/
interface HomeSettings : Settings<HomeSettings.Listener> {

View file

@ -31,6 +31,7 @@ import org.oxycblt.auxio.util.logD
/**
* The ViewModel for managing the tab data and lists of the home view.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@HiltViewModel
@ -138,6 +139,7 @@ constructor(
/**
* Get the preferred [Sort] for a given [Tab].
*
* @param tabMode The [MusicMode] of the [Tab] desired.
* @return The [Sort] preferred for that [Tab]
*/
@ -151,6 +153,7 @@ constructor(
/**
* Update the preferred [Sort] for the current [Tab]. Will update corresponding list.
*
* @param sort The new [Sort] to apply. Assumed to be an allowed sort for the current [Tab].
*/
fun setSortForCurrentTab(sort: Sort) {
@ -178,6 +181,7 @@ constructor(
/**
* Update [currentTabMode] to reflect a new ViewPager2 position
*
* @param pagerPos The new position of the ViewPager2 instance.
*/
fun synchronizeTabPosition(pagerPos: Int) {
@ -187,6 +191,7 @@ constructor(
/**
* Mark the recreation process as complete.
*
* @see shouldRecreate
*/
fun finishRecreate() {
@ -195,6 +200,7 @@ constructor(
/**
* Update whether the user is fast scrolling or not in the home view.
*
* @param isFastScrolling true if the user is currently fast scrolling, false otherwise.
*/
fun setFastScrolling(isFastScrolling: Boolean) {
@ -204,8 +210,9 @@ constructor(
/**
* 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.
* the same way as the configuration.
*/
private fun makeTabModes() =
homeSettings.homeTabs.filterIsInstance<Tab.Visible>().map { it.mode }

View file

@ -40,6 +40,7 @@ import org.oxycblt.auxio.util.isRtl
/**
* A [MaterialTextView] that displays the popup indicator used in FastScrollRecyclerView
*
* @author Alexander Capehart (OxygenCobalt), Hai Zhang
*/
class FastScrollPopupView

View file

@ -49,7 +49,7 @@ import org.oxycblt.auxio.util.*
*
* !!! MODIFICATIONS !!!:
* - Scroller will no longer show itself on startup or relayouts, which looked unpleasant with
* multiple views
* multiple views
* - DefaultAnimationHelper and RecyclerViewHelper were merged into the class
* - FastScroller overlay was merged into RecyclerView instance
* - Removed FastScrollerBuilder
@ -61,11 +61,10 @@ import org.oxycblt.auxio.util.*
* - Added drag listener
* - Added documentation
*
* TODO: Add vibration when popup changes
*
* TODO: Improve support for variably sized items (Re-back with library fast scroller?)
*
* @author Hai Zhang, Alexander Capehart (OxygenCobalt)
*
* TODO: Add vibration when popup changes
* TODO: Improve support for variably sized items (Re-back with library fast scroller?)
*/
class FastScrollRecyclerView
@JvmOverloads
@ -508,9 +507,10 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
interface PopupProvider {
/**
* Get text to use in the popup at the specified position.
*
* @param pos The position in the list.
* @return A [String] to use in the popup. Null if there is no applicable text for the popup
* at [pos].
* at [pos].
*/
fun getPopup(pos: Int): String?
}
@ -519,6 +519,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
interface Listener {
/**
* Called when the fast scrolling state changes.
*
* @param isFastScrolling true if the user is currently fast scrolling, false otherwise.
*/
fun onFastScrollingChanged(isFastScrolling: Boolean)

View file

@ -46,6 +46,7 @@ import org.oxycblt.auxio.util.collectImmediately
/**
* A [ListFragment] that shows a list of [Album]s.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint
@ -154,6 +155,7 @@ class AlbumListFragment :
/**
* A [SelectionIndicatorAdapter] that shows a list of [Album]s using [AlbumViewHolder].
*
* @param listener An [SelectableListListener] to bind interactions to.
*/
private class AlbumAdapter(private val listener: SelectableListListener<Album>) :

View file

@ -47,6 +47,7 @@ import org.oxycblt.auxio.util.nonZeroOrNull
/**
* A [ListFragment] that shows a list of [Artist]s.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint
@ -132,6 +133,7 @@ class ArtistListFragment :
/**
* A [SelectionIndicatorAdapter] that shows a list of [Artist]s using [ArtistViewHolder].
*
* @param listener An [SelectableListListener] to bind interactions to.
*/
private class ArtistAdapter(private val listener: SelectableListListener<Artist>) :

View file

@ -46,6 +46,7 @@ import org.oxycblt.auxio.util.collectImmediately
/**
* A [ListFragment] that shows a list of [Genre]s.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint
@ -131,6 +132,7 @@ class GenreListFragment :
/**
* A [SelectionIndicatorAdapter] that shows a list of [Genre]s using [GenreViewHolder].
*
* @param listener An [SelectableListListener] to bind interactions to.
*/
private class GenreAdapter(private val listener: SelectableListListener<Genre>) :

View file

@ -49,6 +49,7 @@ import org.oxycblt.auxio.util.collectImmediately
/**
* A [ListFragment] that shows a list of [Song]s.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint
@ -165,6 +166,7 @@ class SongListFragment :
/**
* A [SelectionIndicatorAdapter] that shows a list of [Song]s using [SongViewHolder].
*
* @param listener An [SelectableListListener] to bind interactions to.
*/
private class SongAdapter(private val listener: SelectableListListener<Song>) :

View file

@ -27,6 +27,7 @@ import org.oxycblt.auxio.util.logD
/**
* A [TabLayoutMediator.TabConfigurationStrategy] that uses larger/smaller tab configurations
* depending on the screen configuration.
*
* @param context [Context] required to obtain window information
* @param tabs Current tab configuration from settings
* @author Alexander Capehart (OxygenCobalt)

View file

@ -22,18 +22,21 @@ import org.oxycblt.auxio.util.logE
/**
* A representation of a library tab suitable for configuration.
*
* @param mode The type of list in the home view this instance corresponds to.
* @author Alexander Capehart (OxygenCobalt)
*/
sealed class Tab(open val mode: MusicMode) {
/**
* A visible tab. This will be visible in the home and tab configuration views.
*
* @param mode The type of list in the home view this instance corresponds to.
*/
data class Visible(override val mode: MusicMode) : Tab(mode)
/**
* A visible tab. This will be visible in the tab configuration view, but not in the home view.
*
* @param mode The type of list in the home view this instance corresponds to.
*/
data class Invisible(override val mode: MusicMode) : Tab(mode)
@ -68,6 +71,7 @@ sealed class Tab(open val mode: MusicMode) {
/**
* Convert an array of [Tab]s into it's integer representation.
*
* @param tabs The array of [Tab]s to convert
* @return An integer representation of the [Tab] array
*/
@ -93,6 +97,7 @@ sealed class Tab(open val mode: MusicMode) {
/**
* Convert a [Tab] integer representation into it's corresponding array of [Tab]s.
*
* @param intCode The integer representation of the [Tab]s.
* @return An array of [Tab]s corresponding to the sequence.
*/

View file

@ -30,6 +30,7 @@ import org.oxycblt.auxio.util.inflater
/**
* A [RecyclerView.Adapter] that displays an array of [Tab]s open for configuration.
*
* @param listener A [EditableListListener] for tab interactions.
*/
class TabAdapter(private val listener: EditableListListener<Tab>) :
@ -46,6 +47,7 @@ class TabAdapter(private val listener: EditableListListener<Tab>) :
/**
* Immediately update the tab array. This should be used when initializing the list.
*
* @param newTabs The new array of tabs to show.
*/
fun submitTabs(newTabs: Array<Tab>) {
@ -55,6 +57,7 @@ class TabAdapter(private val listener: EditableListListener<Tab>) :
/**
* Update a specific tab to the given value.
*
* @param at The position of the tab to update.
* @param tab The new tab.
*/
@ -66,6 +69,7 @@ class TabAdapter(private val listener: EditableListListener<Tab>) :
/**
* 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.
*/
@ -83,12 +87,14 @@ class TabAdapter(private val listener: EditableListListener<Tab>) :
/**
* A [RecyclerView.ViewHolder] that displays a [Tab]. Use [from] to create an instance.
*
* @author Alexander Capehart (OxygenCobalt)
*/
class TabViewHolder private constructor(private val binding: ItemTabBinding) :
DialogRecyclerView.ViewHolder(binding.root) {
/**
* Bind new data to this instance.
*
* @param tab The new [Tab] to bind.
* @param listener A [EditableListListener] to bind interactions to.
*/
@ -114,6 +120,7 @@ class TabViewHolder private constructor(private val binding: ItemTabBinding) :
companion object {
/**
* Create a new instance.
*
* @param parent The parent to inflate this instance from.
* @return A new instance.
*/

View file

@ -34,6 +34,7 @@ import org.oxycblt.auxio.util.logD
/**
* A [ViewBindingDialogFragment] that allows the user to modify the home [Tab] configuration.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint

View file

@ -23,6 +23,7 @@ import androidx.recyclerview.widget.RecyclerView
/**
* An [ItemTouchHelper.Callback] that implements dragging in the [TabAdapter].
*
* @author Alexander Capehart (OxygenCobalt)
*/
class TabDragCallback(private val adapter: TabAdapter) : ItemTouchHelper.Callback() {

View file

@ -55,14 +55,16 @@ constructor(
interface Target {
/**
* Configure the [ImageRequest.Builder] to enable [Target]-specific configuration.
*
* @param builder The [ImageRequest.Builder] that will be used to request the desired
* [Bitmap].
* [Bitmap].
* @return The same [ImageRequest.Builder] in order to easily chain configuration methods.
*/
fun onConfigRequest(builder: ImageRequest.Builder): ImageRequest.Builder = builder
/**
* Called when the loading process is completed.
*
* @param bitmap The loaded bitmap, or null if the bitmap could not be loaded.
*/
fun onCompleted(bitmap: Bitmap?)
@ -77,6 +79,7 @@ constructor(
/**
* Load the Album cover [Bitmap] from a [Song].
*
* @param song The song to load a [Bitmap] of it's album cover from.
* @param target The [Target] to deliver the [Bitmap] to asynchronously.
*/

View file

@ -21,6 +21,7 @@ import org.oxycblt.auxio.IntegerTable
/**
* Represents the options available for album cover loading.
*
* @author Alexander Capehart (OxygenCobalt)
*/
enum class CoverMode {
@ -33,6 +34,7 @@ enum class CoverMode {
/**
* The integer representation of this instance.
*
* @see fromIntCode
*/
val intCode: Int
@ -46,6 +48,7 @@ enum class CoverMode {
companion object {
/**
* Convert a [CoverMode] integer representation into an instance.
*
* @param intCode An integer representation of a [CoverMode]
* @return The corresponding [CoverMode], or null if the [CoverMode] is invalid.
* @see CoverMode.intCode

View file

@ -48,9 +48,9 @@ import org.oxycblt.auxio.util.getInteger
* This class is primarily intended for list items. For other uses, [StyledImageView] is more
* suitable.
*
* TODO: Rework content descriptions here
*
* @author Alexander Capehart (OxygenCobalt)
*
* TODO: Rework content descriptions here
*/
class ImageGroup
@JvmOverloads
@ -146,6 +146,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
/**
* Bind a [Song] to the internal [StyledImageView].
*
* @param song The [Song] to bind to the view.
* @see StyledImageView.bind
*/
@ -153,6 +154,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
/**
* Bind a [Album] to the internal [StyledImageView].
*
* @param album The [Album] to bind to the view.
* @see StyledImageView.bind
*/
@ -160,6 +162,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
/**
* Bind a [Genre] to the internal [StyledImageView].
*
* @param artist The [Artist] to bind to the view.
* @see StyledImageView.bind
*/
@ -167,6 +170,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
/**
* Bind a [Genre] to the internal [StyledImageView].
*
* @param genre The [Genre] to bind to the view.
* @see StyledImageView.bind
*/

View file

@ -27,6 +27,7 @@ import org.oxycblt.auxio.util.logD
/**
* User configuration specific to image loading.
*
* @author Alexander Capehart (OxygenCobalt)
*/
interface ImageSettings : Settings<ImageSettings.Listener> {

View file

@ -48,11 +48,10 @@ import org.oxycblt.auxio.util.getDrawableCompat
/**
* An [AppCompatImageView] with some additional styling, including:
*
* - Tonal background
* - Rounded corners based on user preferences
* - Built-in support for binding image data or using a static icon with the same styling as
* placeholder drawables.
* placeholder drawables.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@ -97,34 +96,39 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
/**
* Bind a [Song]'s album cover to this view, also updating the content description.
*
* @param song The [Song] to bind.
*/
fun bind(song: Song) = bindImpl(song, R.drawable.ic_song_24, R.string.desc_album_cover)
/**
* Bind an [Album]'s cover to this view, also updating the content description.
*
* @param album the [Album] to bind.
*/
fun bind(album: Album) = bindImpl(album, R.drawable.ic_album_24, R.string.desc_album_cover)
/**
* Bind an [Artist]'s image to this view, also updating the content description.
*
* @param artist the [Artist] to bind.
*/
fun bind(artist: Artist) = bindImpl(artist, R.drawable.ic_artist_24, R.string.desc_artist_image)
/**
* Bind an [Genre]'s image to this view, also updating the content description.
*
* @param genre the [Genre] to bind.
*/
fun bind(genre: Genre) = bindImpl(genre, R.drawable.ic_genre_24, R.string.desc_genre_image)
/**
* Internally bind a [Music]'s image to this view.
*
* @param music The music to find.
* @param errorRes The error drawable resource to use if the music cannot be loaded.
* @param descRes The content description string resource to use. The resource must have one
* field for the name of the [Music].
* field for the name of the [Music].
*/
private fun bindImpl(music: Music, @DrawableRes errorRes: Int, @StringRes descRes: Int) {
val request =
@ -144,6 +148,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
/**
* A [Drawable] wrapper that re-styles the drawable to better align with the style of
* [StyledImageView].
*
* @param context [Context] required for initialization.
* @param inner The [Drawable] to wrap.
*/

View file

@ -41,6 +41,7 @@ import org.oxycblt.auxio.music.Song
/**
* A [Keyer] implementation for [Music] data.
*
* @author Alexander Capehart (OxygenCobalt)
*/
class MusicKeyer : Keyer<Music> {
@ -56,6 +57,7 @@ class MusicKeyer : Keyer<Music> {
/**
* Generic [Fetcher] for [Album] covers. Works with both [Album] and [Song]. Use [SongFactory] or
* [AlbumFactory] for instantiation.
*
* @author Alexander Capehart (OxygenCobalt)
*/
class AlbumCoverFetcher
@ -89,6 +91,7 @@ private constructor(
/**
* [Fetcher] for [Artist] images. Use [Factory] for instantiation.
*
* @author Alexander Capehart (OxygenCobalt)
*/
class ArtistImageFetcher
@ -116,6 +119,7 @@ private constructor(
/**
* [Fetcher] for [Genre] images. Use [Factory] for instantiation.
*
* @author Alexander Capehart (OxygenCobalt)
*/
class GenreImageFetcher
@ -141,9 +145,10 @@ private constructor(
/**
* Map at most N [T] items a collection into a collection of [R], ignoring [T] that cannot be
* transformed into [R].
*
* @param n The maximum amount of items to map.
* @param transform The function that transforms data [T] from the original list into data [R] in
* the new list. Can return null if the [T] cannot be transformed into an [R].
* the new list. Can return null if the [T] cannot be transformed into an [R].
* @return A new list of at most N non-null [R] items.
*/
private inline fun <T : Any, R : Any> Collection<T>.mapAtMostNotNull(

View file

@ -38,16 +38,18 @@ import org.oxycblt.auxio.util.logW
/**
* Internal utilities for loading album covers.
*
* @author Alexander Capehart (OxygenCobalt).
*/
object Covers {
/**
* Fetch an album cover, respecting the current cover configuration.
*
* @param context [Context] required to load the image.
* @param imageSettings [ImageSettings] required to obtain configuration information.
* @param album [Album] to load the cover from.
* @return An [InputStream] of image data if the cover loading was successful, null if the cover
* loading failed or should not occur.
* loading failed or should not occur.
*/
suspend fun fetch(context: Context, imageSettings: ImageSettings, album: Album): InputStream? {
return try {
@ -67,7 +69,7 @@ object Covers {
* order:
* - [MediaMetadataRetriever], as it has the best support and speed.
* - ExoPlayer's [MetadataRetriever], as some devices (notably Samsung) can have broken
* [MediaMetadataRetriever] implementations.
* [MediaMetadataRetriever] implementations.
* - MediaStore, as a last-ditch fallback if the format is really obscure.
*
* @param context [Context] required to load the image.
@ -80,6 +82,7 @@ object Covers {
/**
* Loads an album cover with [MediaMetadataRetriever].
*
* @param context [Context] required to load the image.
* @param album [Album] to load the cover from.
* @return An [InputStream] of image data if the cover loading was successful, null otherwise.
@ -99,6 +102,7 @@ object Covers {
/**
* Loads an [Album] cover with ExoPlayer's [MetadataRetriever].
*
* @param context [Context] required to load the image.
* @param album [Album] to load the cover from.
* @return An [InputStream] of image data if the cover loading was successful, null otherwise.
@ -173,6 +177,7 @@ object Covers {
/**
* Loads an [Album] cover from MediaStore.
*
* @param context [Context] required to load the image.
* @param album [Album] to load the cover from.
* @return An [InputStream] of image data if the cover loading was successful, null otherwise.

View file

@ -27,6 +27,7 @@ import coil.transition.TransitionTarget
/**
* A copy of [CrossfadeTransition.Factory] that also applies a transition to error results.
*
* @author Coil Team, Alexander Capehart (OxygenCobalt)
*/
class ErrorCrossfadeTransitionFactory : Transition.Factory {

View file

@ -37,12 +37,14 @@ import okio.source
/**
* Utilities for constructing Artist and Genre images.
*
* @author Alexander Capehart (OxygenCobalt), Karim Abou Zeid
*/
object Images {
/**
* Create a mosaic image from the given image [InputStream]s. Derived from phonograph:
* https://github.com/kabouzeid/Phonograph
*
* @param context [Context] required to generate the mosaic.
* @param streams [InputStream]s of image data to create the mosaic out of.
* @param size [Size] of the Mosaic to generate.
@ -104,8 +106,9 @@ object Images {
/**
* 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.
* allowing it to be sub-divided.
*/
private fun Dimension.mosaicSize(): Int {
val size = pxOrElse { 512 }

View file

@ -26,6 +26,7 @@ import kotlin.math.min
/**
* A transformation that performs a center crop-style transformation on an image. Allowing this
* behavior to be intrinsic without any view configuration.
*
* @author Alexander Capehart (OxygenCobalt)
*/
class SquareFrameTransform : Transformation {

View file

@ -24,6 +24,7 @@ interface Item
/**
* A "header" used for delimiting groups of data.
*
* @author Alexander Capehart (OxygenCobalt)
*/
interface Header : Item {
@ -33,6 +34,7 @@ interface Header : Item {
/**
* A basic header with no additional actions.
*
* @param titleRes The string resource used for the header's title.
* @author Alexander Capehart (OxygenCobalt)
*/

View file

@ -36,6 +36,7 @@ import org.oxycblt.auxio.util.showToast
/**
* A Fragment containing a selectable list.
*
* @author Alexander Capehart (OxygenCobalt)
*/
abstract class ListFragment<in T : Music, VB : ViewBinding> :
@ -52,6 +53,7 @@ abstract class ListFragment<in T : Music, VB : ViewBinding> :
/**
* Called when [onClick] is called, but does not result in the item being selected. This more or
* less corresponds to an [onClick] implementation in a non-[ListFragment].
*
* @param item The [T] data of the item that was clicked.
*/
abstract fun onRealClick(item: T)
@ -73,6 +75,7 @@ abstract class ListFragment<in T : Music, VB : ViewBinding> :
/**
* Opens a menu in the context of a [Song]. This menu will be managed by the Fragment and 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 [Song] to create the menu for.
@ -111,6 +114,7 @@ abstract class ListFragment<in T : Music, VB : ViewBinding> :
/**
* Opens a menu in the context of a [Album]. This menu will be managed by the Fragment and
* 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 album The [Album] to create the menu for.
@ -147,6 +151,7 @@ abstract class ListFragment<in T : Music, VB : ViewBinding> :
/**
* Opens a menu in the context of a [Artist]. This menu will be managed by the Fragment and
* 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 artist The [Artist] to create the menu for.
@ -180,6 +185,7 @@ abstract class ListFragment<in T : Music, VB : ViewBinding> :
/**
* Opens a menu in the context of a [Genre]. This menu will be managed by the Fragment and
* 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 genre The [Genre] to create the menu for.
@ -226,6 +232,7 @@ abstract class ListFragment<in T : Music, VB : ViewBinding> :
/**
* Open a menu. This menu will be managed by the Fragment and 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 block A block that is ran within [PopupMenu] that allows further configuration.

View file

@ -23,11 +23,13 @@ import androidx.recyclerview.widget.RecyclerView
/**
* A basic listener for list interactions.
*
* @author Alexander Capehart (OxygenCobalt)
*/
interface ClickableListListener<in T> {
/**
* Called when an item in the list is clicked.
*
* @param item The [T] item that was clicked.
* @param viewHolder The [RecyclerView.ViewHolder] of the item that was clicked.
*/
@ -35,10 +37,11 @@ interface ClickableListListener<in T> {
/**
* Binds this instance to a list item.
*
* @param item The [T] to bind this item to.
* @param viewHolder The [RecyclerView.ViewHolder] of the item that was clicked.
* @param bodyView The [View] containing the main body of the list item. Any click events on
* this [View] are routed to the listener. Defaults to the root view.
* this [View] are routed to the listener. Defaults to the root view.
*/
fun bind(item: T, viewHolder: RecyclerView.ViewHolder, bodyView: View = viewHolder.itemView) {
bodyView.setOnClickListener { onClick(item, viewHolder) }
@ -47,21 +50,24 @@ interface ClickableListListener<in T> {
/**
* An extension of [ClickableListListener] that enables list editing functionality.
*
* @author Alexander Capehart (OxygenCobalt)
*/
interface EditableListListener<in T> : ClickableListListener<T> {
/**
* Called when a [RecyclerView.ViewHolder] requests that it should be dragged.
*
* @param viewHolder The [RecyclerView.ViewHolder] that should start being dragged.
*/
fun onPickUp(viewHolder: RecyclerView.ViewHolder)
/**
* Binds this instance to a list item.
*
* @param item The [T] to bind this item to.
* @param viewHolder The [RecyclerView.ViewHolder] to bind.
* @param bodyView The [View] containing the main body of the list item. Any click events on
* this [View] are routed to the listener. Defaults to the root view.
* this [View] are routed to the listener. Defaults to the root view.
* @param dragHandle A touchable [View]. Any drag on this view will start a drag event.
*/
fun bind(
@ -83,11 +89,13 @@ interface EditableListListener<in T> : ClickableListListener<T> {
/**
* An extension of [ClickableListListener] that enables menu and selection functionality.
*
* @author Alexander Capehart (OxygenCobalt)
*/
interface SelectableListListener<in T> : ClickableListListener<T> {
/**
* Called when an item in the list requests that a menu related to it should be opened.
*
* @param item The [T] item to open a menu for.
* @param anchor The [View] to anchor the menu to.
*/
@ -95,16 +103,18 @@ interface SelectableListListener<in T> : ClickableListListener<T> {
/**
* Called when an item in the list requests that it be selected.
*
* @param item The [T] item to select.
*/
fun onSelect(item: T)
/**
* Binds this instance to a list item.
*
* @param item The [T] to bind this item to.
* @param viewHolder The [RecyclerView.ViewHolder] to bind.
* @param bodyView The [View] containing the main body of the list item. Any click events on
* this [View] are routed to the listener. Defaults to the root view.
* this [View] are routed to the listener. Defaults to the root view.
* @param menuButton A clickable [View]. Any click events on this [View] will open a menu.
*/
fun bind(

View file

@ -38,6 +38,7 @@ import org.oxycblt.auxio.music.metadata.Disc
data class Sort(val mode: Mode, val direction: Direction) {
/**
* Create a new [Sort] with the same [mode], but a different [Direction].
*
* @param direction The new [Direction] to sort in.
* @return A new sort with the same mode, but with the new [Direction] value applied.
*/
@ -45,6 +46,7 @@ data class Sort(val mode: Mode, val direction: Direction) {
/**
* Create a new [Sort] with the same [direction] value, but different [mode] value.
*
* @param mode Tbe new mode to use for the Sort.
* @return A new sort with the same [direction] value, but with the new [mode] applied.
*/
@ -52,6 +54,7 @@ data class Sort(val mode: Mode, val direction: Direction) {
/**
* Sort a list of [Song]s.
*
* @param songs The list of [Song]s.
* @return A new list of [Song]s sorted by this [Sort]'s configuration.
*/
@ -63,6 +66,7 @@ data class Sort(val mode: Mode, val direction: Direction) {
/**
* Sort a list of [Album]s.
*
* @param albums The list of [Album]s.
* @return A new list of [Album]s sorted by this [Sort]'s configuration.
*/
@ -74,6 +78,7 @@ data class Sort(val mode: Mode, val direction: Direction) {
/**
* Sort a list of [Artist]s.
*
* @param artists The list of [Artist]s.
* @return A new list of [Artist]s sorted by this [Sort]'s configuration.
*/
@ -85,6 +90,7 @@ data class Sort(val mode: Mode, val direction: Direction) {
/**
* Sort a list of [Genre]s.
*
* @param genres The list of [Genre]s.
* @return A new list of [Genre]s sorted by this [Sort]'s configuration.
*/
@ -96,6 +102,7 @@ data class Sort(val mode: Mode, val direction: Direction) {
/**
* Sort a *mutable* list of [Song]s in-place using this [Sort]'s configuration.
*
* @param songs The [Song]s to sort.
*/
private fun songsInPlace(songs: MutableList<out Song>) {
@ -104,6 +111,7 @@ data class Sort(val mode: Mode, val direction: Direction) {
/**
* Sort a *mutable* list of [Album]s in-place using this [Sort]'s configuration.
*
* @param albums The [Album]s to sort.
*/
private fun albumsInPlace(albums: MutableList<out Album>) {
@ -112,6 +120,7 @@ data class Sort(val mode: Mode, val direction: Direction) {
/**
* Sort a *mutable* list of [Artist]s in-place using this [Sort]'s configuration.
*
* @param artists The [Album]s to sort.
*/
private fun artistsInPlace(artists: MutableList<out Artist>) {
@ -120,6 +129,7 @@ data class Sort(val mode: Mode, val direction: Direction) {
/**
* Sort a *mutable* list of [Genre]s in-place using this [Sort]'s configuration.
*
* @param genres The [Genre]s to sort.
*/
private fun genresInPlace(genres: MutableList<out Genre>) {
@ -128,6 +138,7 @@ data class Sort(val mode: Mode, val direction: Direction) {
/**
* The integer representation of this instance.
*
* @see fromIntCode
*/
val intCode: Int
@ -150,6 +161,7 @@ data class Sort(val mode: Mode, val direction: Direction) {
/**
* Get a [Comparator] that sorts [Song]s according to this [Mode].
*
* @param direction The direction to sort in.
* @return A [Comparator] that can be used to sort a [Song] list according to this [Mode].
*/
@ -159,6 +171,7 @@ data class Sort(val mode: Mode, val direction: Direction) {
/**
* Get a [Comparator] that sorts [Album]s according to this [Mode].
*
* @param direction The direction to sort in.
* @return A [Comparator] that can be used to sort a [Album] list according to this [Mode].
*/
@ -168,6 +181,7 @@ data class Sort(val mode: Mode, val direction: Direction) {
/**
* Return a [Comparator] that sorts [Artist]s according to this [Mode].
*
* @param direction The direction to sort in.
* @return A [Comparator] that can be used to sort a [Artist] list according to this [Mode].
*/
@ -177,6 +191,7 @@ data class Sort(val mode: Mode, val direction: Direction) {
/**
* Return a [Comparator] that sorts [Genre]s according to this [Mode].
*
* @param direction The direction to sort in.
* @return A [Comparator] that can be used to sort a [Genre] list according to this [Mode].
*/
@ -186,6 +201,7 @@ data class Sort(val mode: Mode, val direction: Direction) {
/**
* Sort by the item's name.
*
* @see Music.collationKey
*/
object ByName : Mode() {
@ -210,6 +226,7 @@ data class Sort(val mode: Mode, val direction: Direction) {
/**
* Sort by the [Album] of an item. Only available for [Song]s.
*
* @see Album.collationKey
*/
object ByAlbum : Mode() {
@ -229,6 +246,7 @@ data class Sort(val mode: Mode, val direction: Direction) {
/**
* Sort by the [Artist] name of an item. Only available for [Song] and [Album].
*
* @see Artist.collationKey
*/
object ByArtist : Mode() {
@ -256,6 +274,7 @@ data class Sort(val mode: Mode, val direction: Direction) {
/**
* Sort by the [Date] of an item. Only available for [Song] and [Album].
*
* @see Song.date
* @see Album.dates
*/
@ -308,6 +327,7 @@ data class Sort(val mode: Mode, val direction: Direction) {
/**
* Sort by the amount of songs an item contains. Only available for [MusicParent]s.
*
* @see MusicParent.songs
*/
object ByCount : Mode() {
@ -333,6 +353,7 @@ data class Sort(val mode: Mode, val direction: Direction) {
/**
* Sort by the disc number of an item. Only available for [Song]s.
*
* @see Song.disc
*/
object ByDisc : Mode() {
@ -351,6 +372,7 @@ data class Sort(val mode: Mode, val direction: Direction) {
/**
* Sort by the track number of an item. Only available for [Song]s.
*
* @see Song.track
*/
object ByTrack : Mode() {
@ -369,6 +391,7 @@ data class Sort(val mode: Mode, val direction: Direction) {
/**
* Sort by the date an item was added. Only supported by [Song]s and [Album]s.
*
* @see Song.dateAdded
* @see Album.dates
*/
@ -391,6 +414,7 @@ data class Sort(val mode: Mode, val direction: Direction) {
/**
* Utility function to create a [Comparator] in a dynamic way determined by [direction].
*
* @param direction The [Direction] to sort in.
* @see compareBy
* @see compareByDescending
@ -406,6 +430,7 @@ data class Sort(val mode: Mode, val direction: Direction) {
/**
* Utility function to create a [Comparator] in a dynamic way determined by [direction]
*
* @param direction The [Direction] to sort in.
* @param comparator A [Comparator] to wrap.
* @return A new [Comparator] with the specified configuration.
@ -419,6 +444,7 @@ data class Sort(val mode: Mode, val direction: Direction) {
/**
* Utility function to create a [Comparator] a dynamic way determined by [direction]
*
* @param direction The [Direction] to sort in.
* @param comparator A [Comparator] to wrap.
* @param selector Called to obtain a specific attribute to sort by.
@ -439,6 +465,7 @@ data class Sort(val mode: Mode, val direction: Direction) {
/**
* Utility function to create a [Comparator] that sorts in ascending order based on the
* given [Comparator], with a selector based on the item itself.
*
* @param comparator The [Comparator] to wrap.
* @return A new [Comparator] with the specified configuration.
* @see compareBy
@ -448,8 +475,9 @@ data class Sort(val mode: Mode, val direction: Direction) {
/**
* A [Comparator] that chains several other [Comparator]s together to form one comparison.
*
* @param comparators The [Comparator]s to chain. These will be iterated through in order
* during a comparison, with the first non-equal result becoming the result.
* during a comparison, with the first non-equal result becoming the result.
*/
private class MultiComparator<T>(vararg comparators: Comparator<T>) : Comparator<T> {
private val _comparators = comparators
@ -468,6 +496,7 @@ data class Sort(val mode: Mode, val direction: Direction) {
/**
* Wraps a [Comparator], extending it to compare two lists.
*
* @param inner The [Comparator] to use.
*/
private class ListComparator<T>(private val inner: Comparator<T>) : Comparator<List<T>> {
@ -500,6 +529,7 @@ data class Sort(val mode: Mode, val direction: Direction) {
/**
* A [Comparator] that compares abstract [Music] values. Internally, this is similar to
* [NullableComparator], however comparing [Music.collationKey] instead of [Comparable].
*
* @see NullableComparator
* @see Music.collationKey
*/
@ -555,6 +585,7 @@ data class Sort(val mode: Mode, val direction: Direction) {
companion object {
/**
* Convert a [Mode] integer representation into an instance.
*
* @param intCode An integer representation of a [Mode]
* @return The corresponding [Mode], or null if the [Mode] is invalid.
* @see intCode
@ -575,6 +606,7 @@ data class Sort(val mode: Mode, val direction: Direction) {
/**
* Convert a menu item ID into a [Mode].
*
* @param itemId The menu resource ID to convert
* @return A [Mode] corresponding to the given ID, or null if the ID is invalid.
* @see itemId
@ -604,6 +636,7 @@ data class Sort(val mode: Mode, val direction: Direction) {
companion object {
/**
* Convert a [Sort] integer representation into an instance.
*
* @param intCode An integer representation of a [Sort]
* @return The corresponding [Sort], or null if the [Sort] is invalid.
* @see intCode

View file

@ -21,6 +21,7 @@ import androidx.recyclerview.widget.RecyclerView
/**
* A [RecyclerView.Adapter] with [ListDiffer] integration.
*
* @param differFactory The [ListDiffer.Factory] that defines the type of [ListDiffer] to use.
*/
abstract class DiffAdapter<T, I, VH : RecyclerView.ViewHolder>(
@ -36,6 +37,7 @@ abstract class DiffAdapter<T, I, VH : RecyclerView.ViewHolder>(
/**
* Get a [T] item at the given position.
*
* @param at The position to get the item at.
* @throws IndexOutOfBoundsException If the index is not in the list bounds/
*/
@ -43,6 +45,7 @@ abstract class DiffAdapter<T, I, VH : RecyclerView.ViewHolder>(
/**
* Dynamically determine how to update the list based on the given instructions.
*
* @param newList The new list of [T] items to show.
* @param instructions The instructions specifying how to update the list.
* @param onDone Called when the update process is completed. Defaults to a no-op.

View file

@ -28,6 +28,7 @@ import androidx.recyclerview.widget.RecyclerView
/**
* List differ wrapper that provides more flexibility regarding the way lists are updated.
*
* @author Alexander Capehart (OxygenCobalt)
*/
interface ListDiffer<T, I> {
@ -36,6 +37,7 @@ interface ListDiffer<T, I> {
/**
* Dynamically determine how to update the list based on the given instructions.
*
* @param newList The new list of [T] items to show.
* @param instructions The [BasicListInstructions] specifying how to update the list.
* @param onDone Called when the update process is completed.
@ -49,6 +51,7 @@ interface ListDiffer<T, I> {
abstract class Factory<T, I> {
/**
* Create a new [ListDiffer] bound to the given [RecyclerView.Adapter].
*
* @param adapter The [RecyclerView.Adapter] to bind to.
*/
abstract fun new(adapter: RecyclerView.Adapter<*>): ListDiffer<T, I>
@ -57,8 +60,9 @@ interface ListDiffer<T, I> {
/**
* Update lists on another thread. This is useful when large diffs are likely to occur in this
* list that would be exceedingly slow with [Blocking].
*
* @param diffCallback A [DiffUtil.ItemCallback] to use for item comparison when diffing the
* internal list.
* internal list.
*/
class Async<T>(private val diffCallback: DiffUtil.ItemCallback<T>) :
Factory<T, BasicListInstructions>() {
@ -69,8 +73,9 @@ interface ListDiffer<T, I> {
/**
* Update lists on the main thread. This is useful when many small, discrete list diffs are
* likely to occur that would cause [Async] to suffer from race conditions.
*
* @param diffCallback A [DiffUtil.ItemCallback] to use for item comparison when diffing the
* internal list.
* internal list.
*/
class Blocking<T>(private val diffCallback: DiffUtil.ItemCallback<T>) :
Factory<T, BasicListInstructions>() {
@ -81,6 +86,7 @@ interface ListDiffer<T, I> {
/**
* Represents the specific way to update a list of items.
*
* @author Alexander Capehart (OxygenCobalt)
*/
enum class BasicListInstructions {

View file

@ -23,6 +23,7 @@ import org.oxycblt.auxio.util.logD
/**
* A [RecyclerView.Adapter] that supports indicating the playback status of a particular item.
*
* @param differFactory The [ListDiffer.Factory] that defines the type of [ListDiffer] to use.
* @author Alexander Capehart (OxygenCobalt)
*/
@ -50,6 +51,7 @@ abstract class PlayingIndicatorAdapter<T, I, VH : RecyclerView.ViewHolder>(
}
/**
* Update the currently playing item in the list.
*
* @param item The [T] currently being played, or null if it is not being played.
* @param isPlaying Whether playback is ongoing or paused.
*/
@ -103,9 +105,10 @@ abstract class PlayingIndicatorAdapter<T, I, VH : RecyclerView.ViewHolder>(
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.
* [isActive] will also be true.
*/
abstract fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean)
}

View file

@ -24,6 +24,7 @@ import org.oxycblt.auxio.music.Music
/**
* A [PlayingIndicatorAdapter] that also supports indicating the selection status of a group of
* items.
*
* @param differFactory The [ListDiffer.Factory] that defines the type of [ListDiffer] to use.
* @author Alexander Capehart (OxygenCobalt)
*/
@ -41,6 +42,7 @@ abstract class SelectionIndicatorAdapter<T, I, VH : RecyclerView.ViewHolder>(
/**
* Update the list of selected items.
*
* @param items A set of selected [T] items.
*/
fun setSelected(items: Set<T>) {
@ -74,6 +76,7 @@ abstract class SelectionIndicatorAdapter<T, I, VH : RecyclerView.ViewHolder>(
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)

View file

@ -23,6 +23,7 @@ import org.oxycblt.auxio.list.Item
/**
* A [DiffUtil.ItemCallback] that automatically implements the [areItemsTheSame] method. Use this
* whenever creating [DiffUtil.ItemCallback] implementations with an [Item] subclass.
*
* @author Alexander Capehart (OxygenCobalt)
*/
abstract class SimpleDiffCallback<T : Item> : DiffUtil.ItemCallback<T>() {

View file

@ -31,6 +31,7 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
* - Automatic edge-to-edge support
* - Adapter-based [SpanSizeLookup] implementation
* - Automatic [setHasFixedSize] setup
*
* @author Alexander Capehart (OxygenCobalt)
*/
open class AuxioRecyclerView
@ -89,6 +90,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
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.
*/

View file

@ -34,6 +34,7 @@ import org.oxycblt.auxio.util.getDimenPixels
* A [RecyclerView] intended for use in Dialogs, adding features such as:
* - NestedScrollView scrollIndicators behavior emulation
* - Dialog-specific [ViewHolder] that automatically resolves certain issues.
*
* @author Alexander Capehart (OxygenCobalt)
*/
class DialogRecyclerView

View file

@ -29,6 +29,7 @@ import org.oxycblt.auxio.list.adapter.DiffAdapter
/**
* A [BackportMaterialDividerItemDecoration] that sets up the divider configuration to correctly
* separate content with headers.
*
* @author Alexander Capehart (OxygenCobalt)
*/
class HeaderItemDecoration

View file

@ -36,12 +36,14 @@ import org.oxycblt.auxio.util.logD
/**
* A [RecyclerView.ViewHolder] that displays a [Song]. Use [from] to create an instance.
*
* @author Alexander Capehart (OxygenCobalt)
*/
class SongViewHolder private constructor(private val binding: ItemSongBinding) :
SelectionIndicatorAdapter.ViewHolder(binding.root) {
/**
* Bind new data to this instance.
*
* @param song The new [Song] to bind.
* @param listener An [SelectableListListener] to bind interactions to.
*/
@ -67,6 +69,7 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
/**
* Create a new instance.
*
* @param parent The parent to inflate this instance from.
* @return A new instance.
*/
@ -84,12 +87,14 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
/**
* A [RecyclerView.ViewHolder] that displays a [Album]. Use [from] to create an instance.
*
* @author Alexander Capehart (OxygenCobalt)
*/
class AlbumViewHolder private constructor(private val binding: ItemParentBinding) :
SelectionIndicatorAdapter.ViewHolder(binding.root) {
/**
* Bind new data to this instance.
*
* @param album The new [Album] to bind.
* @param listener An [SelectableListListener] to bind interactions to.
*/
@ -115,6 +120,7 @@ class AlbumViewHolder private constructor(private val binding: ItemParentBinding
/**
* Create a new instance.
*
* @param parent The parent to inflate this instance from.
* @return A new instance.
*/
@ -133,12 +139,14 @@ class AlbumViewHolder private constructor(private val binding: ItemParentBinding
/**
* A [RecyclerView.ViewHolder] that displays a [Artist]. Use [from] to create an instance.
*
* @author Alexander Capehart (OxygenCobalt)
*/
class ArtistViewHolder private constructor(private val binding: ItemParentBinding) :
SelectionIndicatorAdapter.ViewHolder(binding.root) {
/**
* Bind new data to this instance.
*
* @param artist The new [Artist] to bind.
* @param listener An [SelectableListListener] to bind interactions to.
*/
@ -173,6 +181,7 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin
/**
* Create a new instance.
*
* @param parent The parent to inflate this instance from.
* @return A new instance.
*/
@ -192,12 +201,14 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin
/**
* A [RecyclerView.ViewHolder] that displays a [Genre]. Use [from] to create an instance.
*
* @author Alexander Capehart (OxygenCobalt)
*/
class GenreViewHolder private constructor(private val binding: ItemParentBinding) :
SelectionIndicatorAdapter.ViewHolder(binding.root) {
/**
* Bind new data to this instance.
*
* @param genre The new [Genre] to bind.
* @param listener An [SelectableListListener] to bind interactions to.
*/
@ -227,6 +238,7 @@ class GenreViewHolder private constructor(private val binding: ItemParentBinding
/**
* Create a new instance.
*
* @param parent The parent to inflate this instance from.
* @return A new instance.
*/
@ -243,12 +255,14 @@ class GenreViewHolder private constructor(private val binding: ItemParentBinding
/**
* A [RecyclerView.ViewHolder] that displays a [BasicHeader]. Use [from] to create an instance.
*
* @author Alexander Capehart (OxygenCobalt)
*/
class BasicHeaderViewHolder private constructor(private val binding: ItemHeaderBinding) :
RecyclerView.ViewHolder(binding.root) {
/**
* Bind new data to this instance.
*
* @param basicHeader The new [BasicHeader] to bind.
*/
fun bind(basicHeader: BasicHeader) {
@ -262,6 +276,7 @@ class BasicHeaderViewHolder private constructor(private val binding: ItemHeaderB
/**
* Create a new instance.
*
* @param parent The parent to inflate this instance from.
* @return A new instance.
*/

View file

@ -28,6 +28,7 @@ import org.oxycblt.auxio.util.showToast
/**
* A subset of ListFragment that implements aspects of the selection UI.
*
* @author Alexander Capehart (OxygenCobalt)
*/
abstract class SelectionFragment<VB : ViewBinding> :
@ -38,8 +39,9 @@ abstract class SelectionFragment<VB : ViewBinding> :
/**
* Get the [SelectionToolbarOverlay] of the concrete Fragment to be automatically managed by
* [SelectionFragment].
*
* @return The [SelectionToolbarOverlay] of the concrete [SelectionFragment]'s [VB], or null if
* there is not one.
* there is not one.
*/
open fun getSelectionToolbar(binding: VB): SelectionToolbarOverlay? = null

View file

@ -32,6 +32,7 @@ import org.oxycblt.auxio.util.logD
/**
* A wrapper around a [MaterialToolbar] that adds an additional [MaterialToolbar] showing the
* current selection state.
*
* @author Alexander Capehart (OxygenCobalt)
*/
class SelectionToolbarOverlay
@ -65,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
*/
@ -75,6 +77,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
*/
@ -84,6 +87,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
/**
* Update the selection [MaterialToolbar] to reflect the current selection amount.
*
* @param amount The amount of items that are currently selected.
* @return true if the selection [MaterialToolbar] changes, false otherwise.
*/
@ -101,6 +105,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
/**
* Animate the visibility of the inner and selection [MaterialToolbar]s to the given state.
*
* @param selectionVisible Whether the selection [MaterialToolbar] should be visible or not.
* @return true if the toolbars have changed, false otherwise.
*/
@ -152,8 +157,9 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
/**
* Update the alpha of the inner and selection [MaterialToolbar]s.
*
* @param innerAlpha The opacity of the inner [MaterialToolbar]. This will map to the inverse
* opacity of the selection [MaterialToolbar].
* opacity of the selection [MaterialToolbar].
*/
private fun setToolbarsAlpha(innerAlpha: Float) {
innerToolbar.apply {

View file

@ -27,6 +27,7 @@ import org.oxycblt.auxio.music.model.Library
/**
* A [ViewModel] that manages the current selection.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@HiltViewModel
@ -67,6 +68,7 @@ class SelectionViewModel @Inject constructor(private val musicRepository: MusicR
/**
* Select a new [Music] item. If this item is already within the selected items, the item will
* be removed. Otherwise, it will be added.
*
* @param music The [Music] item to select.
*/
fun select(music: Music) {
@ -79,6 +81,7 @@ class SelectionViewModel @Inject constructor(private val musicRepository: MusicR
/**
* Consume the current selection. This will clear any items that were selected prior.
*
* @return The list of selected items before it was cleared.
*/
fun consume() = _selected.value.also { _selected.value = listOf() }

View file

@ -28,6 +28,7 @@ import com.google.android.exoplayer2.extractor.wav.WavExtractor
/**
* A [ExtractorsFactory] that only provides audio containers to save APK space.
*
* @author Alexander Capehart (OxygenCobalt)
*/
object AudioOnlyExtractors : ExtractorsFactory {

View file

@ -38,11 +38,13 @@ import org.oxycblt.auxio.util.toUuidOrNull
/**
* Abstract music data. This contains universal information about all concrete music
* implementations, such as identification information and names.
*
* @author Alexander Capehart (OxygenCobalt)
*/
sealed interface Music : Item {
/**
* A unique identifier for this music item.
*
* @see UID
*/
val uid: UID
@ -56,9 +58,10 @@ sealed interface Music : Item {
/**
* Returns a name suitable for use in the app UI. This should be favored over [rawName] in
* nearly all cases.
*
* @param context [Context] required to obtain placeholder text or formatting information.
* @return A human-readable string representing the name of this music. In the case that the
* item does not have a name, an analogous "Unknown X" name is returned.
* item does not have a name, an analogous "Unknown X" name is returned.
*/
fun resolveName(context: Context): String
@ -76,7 +79,7 @@ sealed interface Music : Item {
* The key will have the following attributes:
* - If [rawSortName] is present, this key will be derived from it. Otherwise [rawName] is used.
* - If the string begins with an article, such as "the", it will be stripped, as is usually
* convention for sorting media. This is not internationalized.
* convention for sorting media. This is not internationalized.
*/
val collationKey: CollationKey?
@ -86,15 +89,14 @@ sealed interface Music : Item {
* [UID] enables a much cheaper and more reliable form of differentiating music, derived from
* either a hash of meaningful metadata or the MusicBrainz ID spec. Using this enables several
* improvements to music management in this app, including:
*
* - Proper differentiation of identical music. It's common for large, well-tagged libraries to
* have functionally duplicate items that are differentiated with MusicBrainz IDs, and so [UID]
* allows us to properly differentiate between these in the app.
* have functionally duplicate items that are differentiated with MusicBrainz IDs, and so
* [UID] allows us to properly differentiate between these in the app.
* - Better music persistence between restarts. Whereas directly storing song names would be
* prone to collisions, and storing MediaStore IDs would drift rapidly as the music library
* changes, [UID] enables a much stronger form of persistence given it's unique link to a
* specific files metadata configuration, which is unlikely to collide with another item or
* drift as the music library changes.
* prone to collisions, and storing MediaStore IDs would drift rapidly as the music library
* changes, [UID] enables a much stronger form of persistence given it's unique link to a
* specific files metadata configuration, which is unlikely to collide with another item or
* drift as the music library changes.
*
* Note: Generally try to use [UID] as a black box that can only be read, written, and compared.
* It will not be fun if you try to manipulate it in any other manner.
@ -125,6 +127,7 @@ sealed interface Music : Item {
/**
* 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) {
@ -139,10 +142,11 @@ sealed interface Music : Item {
/**
* Creates an Auxio-style [UID] with a [UUID] composed of a hash of the non-subjective,
* unlikely-to-change metadata of the music.
*
* @param mode The analogous [MusicMode] of the item that created this [UID].
* @param updates Block to update the [MessageDigest] hash with the metadata of the
* item. Make sure the metadata hashed semantically aligns with the format
* specification.
* item. Make sure the metadata hashed semantically aligns with the format
* specification.
* @return A new auxio-style [UID].
*/
fun auxio(mode: MusicMode, updates: MessageDigest.() -> Unit): UID {
@ -181,19 +185,21 @@ sealed interface Music : Item {
/**
* Creates a MusicBrainz-style [UID] with a [UUID] derived from the MusicBrainz ID
* extracted from a file.
*
* @param mode The analogous [MusicMode] of the item that created this [UID].
* @param mbid The analogous MusicBrainz ID for this item that was extracted from a
* file.
* file.
* @return A new MusicBrainz-style [UID].
*/
fun musicBrainz(mode: MusicMode, mbid: UUID): UID = UID(Format.MUSICBRAINZ, mode, mbid)
/**
* Convert a [UID]'s string representation back into a concrete [UID] instance.
*
* @param uid The [UID]'s string representation, formatted as
* `format_namespace:music_mode_int-uuid`.
* `format_namespace:music_mode_int-uuid`.
* @return A [UID] converted from the string representation, or null if the string
* representation was invalid.
* representation was invalid.
*/
fun fromString(uid: String): UID? {
val split = uid.split(':', limit = 2)
@ -224,6 +230,7 @@ sealed interface Music : Item {
/**
* An abstract grouping of [Song]s and other [Music] data.
*
* @author Alexander Capehart (OxygenCobalt)
*/
sealed interface MusicParent : Music {
@ -233,6 +240,7 @@ sealed interface MusicParent : Music {
/**
* A song.
*
* @author Alexander Capehart (OxygenCobalt)
*/
interface Song : Music {
@ -281,6 +289,7 @@ interface Song : Music {
/**
* An abstract release group. While it may be called an album, it encompasses other types of
* releases like singles, EPs, and compilations.
*
* @author Alexander Capehart (OxygenCobalt)
*/
interface Album : MusicParent {
@ -311,6 +320,7 @@ interface Album : MusicParent {
/**
* An abstract artist. These are actually a combination of the artist and album artist tags from
* within the library, derived from [Song]s and [Album]s respectively.
*
* @author Alexander Capehart (OxygenCobalt)
*/
interface Artist : MusicParent {
@ -336,6 +346,7 @@ interface Artist : MusicParent {
/**
* A genre.
*
* @author Alexander Capehart (OxygenCobalt)
*/
interface Genre : MusicParent {
@ -350,6 +361,7 @@ interface Genre : MusicParent {
/**
* Run [Music.resolveName] on each instance in the given list and concatenate them into a [String]
* in a localized manner.
*
* @param context [Context] required
* @return A concatenated string.
*/
@ -359,6 +371,7 @@ fun <T : Music> List<T>.resolveNames(context: Context) =
/**
* Returns if [Music.rawName] matches for each item in a list. Useful for scenarios where the
* display information of an item must be compared without a context.
*
* @param other The list of items to compare to.
* @return True if they are the same (by [Music.rawName]), false otherwise.
*/

View file

@ -21,6 +21,7 @@ import org.oxycblt.auxio.IntegerTable
/**
* Represents a data configuration corresponding to a specific type of [Music],
*
* @author Alexander Capehart (OxygenCobalt)
*/
enum class MusicMode {
@ -35,6 +36,7 @@ enum class MusicMode {
/**
* The integer representation of this instance.
*
* @see fromIntCode
*/
val intCode: Int
@ -49,6 +51,7 @@ enum class MusicMode {
companion object {
/**
* Convert a [MusicMode] integer representation into an instance.
*
* @param intCode An integer representation of a [MusicMode]
* @return The corresponding [MusicMode], or null if the [MusicMode] is invalid.
* @see MusicMode.intCode

View file

@ -40,6 +40,7 @@ interface MusicRepository {
/**
* Add a [Listener] to this instance. This can be used to receive changes in the music library.
* Will invoke all [Listener] methods to initialize the instance with the current state.
*
* @param listener The [Listener] to add.
* @see Listener
*/
@ -47,8 +48,9 @@ interface MusicRepository {
/**
* Remove a [Listener] from this instance, preventing it from receiving any further updates.
*
* @param listener The [Listener] to remove. Does nothing if the [Listener] was never added in
* the first place.
* the first place.
* @see Listener
*/
fun removeListener(listener: Listener)
@ -57,6 +59,7 @@ interface MusicRepository {
interface Listener {
/**
* Called when the current [Library] has changed.
*
* @param library The new [Library], or null if no [Library] has been loaded yet.
*/
fun onLibraryChanged(library: Library?)

View file

@ -31,6 +31,7 @@ import org.oxycblt.auxio.util.getSystemServiceCompat
/**
* User configuration specific to music system.
*
* @author Alexander Capehart (OxygenCobalt)
*/
interface MusicSettings : Settings<MusicSettings.Listener> {

View file

@ -26,6 +26,7 @@ import org.oxycblt.auxio.music.system.Indexer
/**
* A [ViewModel] providing data specific to the music loading process.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@HiltViewModel
@ -76,6 +77,7 @@ class MusicViewModel @Inject constructor(private val indexer: Indexer) :
/**
* Non-manipulated statistics bound the last successful music load.
*
* @param songs The amount of [Song]s that were loaded.
* @param albums The amount of [Album]s that were created.
* @param artists The amount of [Artist]s that were created.

View file

@ -23,17 +23,20 @@ import org.oxycblt.auxio.util.*
/**
* A repository allowing access to cached metadata obtained in prior music loading operations.
*
* @author Alexander Capehart (OxygenCobalt)
*/
interface CacheRepository {
/**
* Read the current [Cache], if it exists.
*
* @return The stored [Cache], or null if it could not be obtained.
*/
suspend fun readCache(): Cache?
/**
* Write the list of newly-loaded [RawSong]s to the cache, replacing the prior data.
*
* @param rawSongs The [rawSongs] to write to the cache.
*/
suspend fun writeCache(rawSongs: List<RawSong>)
@ -67,6 +70,7 @@ class CacheRepositoryImpl @Inject constructor(private val cachedSongsDao: Cached
/**
* A cache of music metadata obtained in prior music loading operations. Obtain an instance with
* [CacheRepository].
*
* @author Alexander Capehart (OxygenCobalt)
*/
interface Cache {
@ -75,6 +79,7 @@ interface Cache {
/**
* Populate a [RawSong] from a cache entry, if it exists.
*
* @param rawSong The [RawSong] to populate.
* @return true if a cache entry could be applied to [rawSong], false otherwise.
*/

View file

@ -30,6 +30,7 @@ import org.oxycblt.auxio.util.logW
/**
* The properties of a [Song]'s file.
*
* @param bitrateKbps The bit rate, in kilobytes-per-second. Null if it could not be parsed.
* @param sampleRateHz The sample rate, in hertz.
* @param resolvedMimeType The known mime type of the [Song] after it's file format was determined.
@ -44,6 +45,7 @@ data class AudioInfo(
interface Provider {
/**
* Extract the [AudioInfo] of a given [Song].
*
* @param song The [Song] to read.
* @return The [AudioInfo] of the [Song], if possible to obtain.
*/
@ -53,6 +55,7 @@ data class AudioInfo(
/**
* A framework-backed implementation of [AudioInfo.Provider].
*
* @param context [Context] required to read audio files.
*/
class AudioInfoProviderImpl @Inject constructor(@ApplicationContext private val context: Context) :

View file

@ -44,10 +44,11 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
/**
* Resolve this instance into a human-readable date.
*
* @param context [Context] required to get human-readable names.
* @return If the [Date] has a valid month and year value, a more fine-grained date (ex. "Jan
* 2020") will be returned. Otherwise, a plain year value (ex. "2020") is returned. Dates will
* be properly localized.
* 2020") will be returned. Otherwise, a plain year value (ex. "2020") is returned. Dates will
* be properly localized.
*/
fun resolveDate(context: Context): String {
if (month != null) {
@ -115,6 +116,7 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
* A range of [Date]s. This is used in contexts where the [Date] of an item is derived from
* several sub-items and thus can have a "range" of release dates. Use [from] to create an
* instance.
*
* @author Alexander Capehart
*/
class Range
@ -127,10 +129,11 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
/**
* Resolve this instance into a human-readable date range.
*
* @param context [Context] required to get human-readable names.
* @return If the date has a maximum value, then a `min - max` formatted string will be
* returned with the formatted [Date]s of the minimum and maximum dates respectively.
* Otherwise, the formatted name of the minimum [Date] will be returned.
* returned with the formatted [Date]s of the minimum and maximum dates respectively.
* Otherwise, the formatted name of the minimum [Date] will be returned.
*/
fun resolveDate(context: Context) =
if (min != max) {
@ -149,9 +152,10 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
companion object {
/**
* Create a [Range] from the given list of [Date]s.
*
* @param dates The [Date]s to use.
* @return A [Range] based on the minimum and maximum [Date]s. If there are no [Date]s,
* null is returned.
* null is returned.
*/
fun from(dates: List<Date>): Range? {
if (dates.isEmpty()) {
@ -186,6 +190,7 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
/**
* Create a [Date] from a year component.
*
* @param year The year component.
* @return A new [Date] of the given component, or null if the component is invalid.
*/
@ -204,38 +209,41 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
/**
* Create a [Date] from a date component.
*
* @param year The year component.
* @param month The month component.
* @param day The day component.
* @return A new [Date] consisting of the given components. May have reduced precision if
* the components were partially invalid, and will be null if all components are invalid.
* the components were partially invalid, and will be null if all components are invalid.
*/
fun from(year: Int, month: Int, day: Int) = fromTokens(listOf(year, month, day))
/**
* Create [Date] from a datetime component.
*
* @param year The year component.
* @param month The month component.
* @param day The day component.
* @param hour The hour component
* @return A new [Date] consisting of the given components. May have reduced precision if
* the components were partially invalid, and will be null if all components are invalid.
* the components were partially invalid, and will be null if all components are invalid.
*/
fun from(year: Int, month: Int, day: Int, hour: Int, minute: Int) =
fromTokens(listOf(year, month, day, hour, minute))
/**
* Create a [Date] from a [String] timestamp.
*
* @param timestamp The ISO-8601 timestamp to parse. Can have reduced precision.
* @return A new [Date] consisting of the given components. May have reduced precision if
* the components were partially invalid, and will be null if all components are invalid or
* if the timestamp is invalid.
* the components were partially invalid, and will be null if all components are invalid
* or if the timestamp is invalid.
*/
fun from(timestamp: String): Date? {
val tokens =
// Match the input with the timestamp regex. If there is no match, see if we can
// fall back to some kind of year value.
(ISO8601_REGEX.matchEntire(timestamp)
// Match the input with the timestamp regex. If there is no match, see if we can
// fall back to some kind of year value.
(ISO8601_REGEX.matchEntire(timestamp)
?: return timestamp.toIntOrNull()?.let(Companion::from))
.groupValues
// Filter to the specific tokens we want and convert them to integer tokens.
@ -245,9 +253,10 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
/**
* Create a [Date] from the given non-validated tokens.
*
* @param tokens The tokens to use for each date component, in order of precision.
* @return A new [Date] consisting of the given components. May have reduced precision if
* the components were partially invalid, and will be null if all components are invalid.
* the components were partially invalid, and will be null if all components are invalid.
*/
private fun fromTokens(tokens: List<Int>): Date? {
val validated = mutableListOf<Int>()
@ -262,6 +271,7 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
/**
* Validate a list of tokens provided by [src], and add the valid ones to [dst]. Will stop
* as soon as an invalid token is found.
*
* @param src The input tokens to validate.
* @param dst The destination list to add valid tokens to.
*/

View file

@ -21,6 +21,7 @@ import org.oxycblt.auxio.list.Item
/**
* A disc identifier for a song.
*
* @param number The disc number.
* @param name The name of the disc group, if any. Null if not present.
*/

View file

@ -24,6 +24,7 @@ import org.oxycblt.auxio.R
*
* This class is derived from the MusicBrainz Release Group Type specification. It can be found at:
* https://musicbrainz.org/doc/Release_Group/Type
*
* @author Alexander Capehart (OxygenCobalt)
*/
sealed class ReleaseType {
@ -38,8 +39,9 @@ sealed class ReleaseType {
/**
* A plain album.
*
* @param refinement A specification of what kind of performance this release is. If null, the
* release is considered "Plain".
* release is considered "Plain".
*/
data class Album(override val refinement: Refinement?) : ReleaseType() {
override val stringRes: Int
@ -54,8 +56,9 @@ sealed class ReleaseType {
/**
* A "Extended Play", or EP. Usually a smaller release consisting of 4-5 songs.
*
* @param refinement A specification of what kind of performance this release is. If null, the
* release is considered "Plain".
* release is considered "Plain".
*/
data class EP(override val refinement: Refinement?) : ReleaseType() {
override val stringRes: Int
@ -70,8 +73,9 @@ sealed class ReleaseType {
/**
* A single. Usually a release consisting of 1-2 songs.
*
* @param refinement A specification of what kind of performance this release is. If null, the
* release is considered "Plain".
* release is considered "Plain".
*/
data class Single(override val refinement: Refinement?) : ReleaseType() {
override val stringRes: Int
@ -86,8 +90,9 @@ sealed class ReleaseType {
/**
* A compilation. Usually consists of many songs from a variety of artists.
*
* @param refinement A specification of what kind of performance this release is. If null, the
* release is considered "Plain".
* release is considered "Plain".
*/
data class Compilation(override val refinement: Refinement?) : ReleaseType() {
override val stringRes: Int
@ -149,9 +154,10 @@ sealed class ReleaseType {
/**
* Parse a [ReleaseType] from a string formatted with the MusicBrainz Release Group Type
* specification.
*
* @param types A list of values consisting of valid release type values.
* @return A [ReleaseType] consisting of the given types, or null if the types were not
* valid.
* valid.
*/
fun parse(types: List<String>): ReleaseType? {
val primary = types.getOrNull(0) ?: return null
@ -170,10 +176,11 @@ sealed class ReleaseType {
/**
* Parse "secondary" types (i.e not [Album], [EP], or [Single]) from a string formatted with
* the MusicBrainz Release Group Type specification.
*
* @param index The index of the release type to parse.
* @param convertRefinement Code to convert a [Refinement] into a [ReleaseType]
* corresponding to the callee's context. This is used in order to handle secondary times
* that are actually [Refinement]s.
* corresponding to the callee's context. This is used in order to handle secondary times
* that are actually [Refinement]s.
* @return A [ReleaseType] corresponding to the secondary type found at that index.
*/
private inline fun List<String>.parseSecondaryTypes(
@ -194,10 +201,11 @@ sealed class ReleaseType {
/**
* Parse "secondary" types (i.e not [Album], [EP], [Single]) that do not correspond to any
* child values.
*
* @param type The release type value to parse.
* @param convertRefinement Code to convert a [Refinement] into a [ReleaseType]
* corresponding to the callee's context. This is used in order to handle secondary times
* that are actually [Refinement]s.
* corresponding to the callee's context. This is used in order to handle secondary times
* that are actually [Refinement]s.
*/
private inline fun parseSecondaryTypeImpl(
type: String?,

View file

@ -19,6 +19,7 @@ package org.oxycblt.auxio.music.metadata
/**
* Defines the allowed separator characters that can be used to delimit multi-value tags.
*
* @author Alexander Capehart (OxygenCobalt)
*/
object Separators {

View file

@ -33,6 +33,7 @@ import org.oxycblt.auxio.ui.ViewBindingDialogFragment
/**
* A [ViewBindingDialogFragment] that allows the user to configure the separator characters used to
* split tags with multiple values.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint

View file

@ -43,6 +43,7 @@ interface TagExtractor {
/**
* Extract the metadata of songs from [incompleteSongs] and send them to [completeSongs]. Will
* terminate as soon as [incompleteSongs] is closed.
*
* @param incompleteSongs A [Channel] of incomplete songs to process.
* @param completeSongs A [Channel] to send completed songs to.
*/
@ -105,6 +106,7 @@ class TagExtractorImpl @Inject constructor(@ApplicationContext private val conte
/**
* Wraps a [TagExtractor] future and processes it into a [RawSong] when completed.
*
* @param context [Context] required to open the audio file.
* @param rawSong [RawSong] to process.
* @author Alexander Capehart (OxygenCobalt)
@ -121,6 +123,7 @@ private class Task(context: Context, private val rawSong: RawSong) {
/**
* Try to get a completed song from this [Task], if it has finished processing.
*
* @return A [RawSong] instance if processing has completed, null otherwise.
*/
fun get(): RawSong? {
@ -156,8 +159,9 @@ private class Task(context: Context, private val rawSong: RawSong) {
/**
* Complete this instance's [RawSong] with ID3v2 Text Identification Frames.
*
* @param textFrames A mapping between ID3v2 Text Identification Frame IDs and one or more
* values.
* values.
*/
private fun populateWithId3v2(textFrames: Map<String, List<String>>) {
// Song
@ -220,11 +224,12 @@ private class Task(context: Context, private val rawSong: RawSong) {
/**
* Parses the ID3v2.3 timestamp specification into a [Date] from the given Text Identification
* Frames.
*
* @param textFrames A mapping between ID3v2 Text Identification Frame IDs and one or more
* values.
* values.
* @return A [Date] of a year value from TORY/TYER, a month and day value from TDAT, and a
* hour/minute value from TIME. No second value is included. The latter two fields may not be
* included in they cannot be parsed. Will be null if a year value could not be parsed.
* hour/minute value from TIME. No second value is included. The latter two fields may not be
* included in they cannot be parsed. Will be null if a year value could not be parsed.
*/
private fun parseId3v23Date(textFrames: Map<String, List<String>>): Date? {
// Assume that TDAT/TIME can refer to TYER or TORY depending on if TORY
@ -261,6 +266,7 @@ private class Task(context: Context, private val rawSong: RawSong) {
/**
* Complete this instance's [RawSong] with Vorbis comments.
*
* @param comments A mapping between vorbis comment names and one or more vorbis comment values.
*/
private fun populateWithVorbis(comments: Map<String, List<String>>) {

View file

@ -26,6 +26,7 @@ import org.oxycblt.auxio.util.nonZeroOrNull
* Parse a multi-value tag based on the user configuration. If the value is already composed of more
* than one value, nothing is done. Otherwise, this function will attempt to split it based on the
* user's separator preferences.
*
* @param settings [MusicSettings] required to obtain user separator configuration.
* @return A new list of one or more [String]s.
*/
@ -40,6 +41,7 @@ fun List<String>.parseMultiValue(settings: MusicSettings) =
/**
* Split a [String] by the given selector, automatically handling escaped characters that satisfy
* the selector.
*
* @param selector A block that determines if the string should be split at a given character.
* @return One or more [String]s split by the selector.
*/
@ -83,19 +85,22 @@ inline fun String.splitEscaped(selector: (Char) -> Boolean): List<String> {
/**
* Fix trailing whitespace or blank contents in a [String].
*
* @return A string with trailing whitespace remove,d or null if the [String] was all whitespace or
* empty.
* empty.
*/
fun String.correctWhitespace() = trim().ifBlank { null }
/**
* Fix trailing whitespace or blank contents within a list of [String]s.
*
* @return A list of non-blank strings with trailing whitespace removed.
*/
fun List<String>.correctWhitespace() = mapNotNull { it.correctWhitespace() }
/**
* Attempt to parse a string by the user's separator preferences.
*
* @param settings [MusicSettings] required to obtain user separator configuration.
* @return A list of one or more [String]s that were split up by the user-defined separators.
*/
@ -109,9 +114,11 @@ private fun String.maybeParseBySeparators(settings: MusicSettings): List<String>
/**
* Parse an ID3v2-style position + total [String] field. These fields consist of a number and an
* (optional) total value delimited by a /.
*
* @return The position value extracted from the string field, or null if:
* - The position could not be parsed
* - The position was zeroed AND the total value was not present/zeroed
*
* @see transformPositionField
*/
fun String.parseId3v2PositionField() =
@ -122,11 +129,13 @@ fun String.parseId3v2PositionField() =
/**
* Parse a vorbis-style position + total field. These fields consist of two fields for the position
* and total numbers.
*
* @param pos The position value, or null if not present.
* @param total The total value, if not present.
* @return The position value extracted from the field, or null if:
* - The position could not be parsed
* - The position was zeroed AND the total value was not present/zeroed
*
* @see transformPositionField
*/
fun parseVorbisPositionField(pos: String?, total: String?) =
@ -134,6 +143,7 @@ fun parseVorbisPositionField(pos: String?, total: String?) =
/**
* Transform a raw position + total field into a position a way that tolerates placeholder values.
*
* @param pos The position value, or null if not present.
* @param total The total value, if not present.
* @return The position value extracted from the field, or null if:
@ -151,6 +161,7 @@ fun transformPositionField(pos: Int?, total: Int?) =
* Parse a multi-value genre name using ID3 rules. This will convert any ID3v1 integer
* representations of genre fields into their named counterparts, and split up singular ID3v2-style
* integer genre fields into one or more genres.
*
* @param settings [MusicSettings] required to obtain user separator configuration.
* @return A list of one or more genre names..
*/
@ -164,6 +175,7 @@ fun List<String>.parseId3GenreNames(settings: MusicSettings) =
/**
* Parse a single ID3v1/ID3v2 integer genre field into their named representations.
*
* @param settings [MusicSettings] required to obtain user separator configuration.
* @return A list of one or more genre names.
*/
@ -172,8 +184,9 @@ private fun String.parseId3MultiValueGenre(settings: MusicSettings) =
/**
* Parse an ID3v1 integer genre field.
*
* @return A named genre if the field is a valid integer, "Cover" or "Remix" if the field is
* "CR"/"RX" respectively, and nothing if the field is not a valid ID3v1 integer genre.
* "CR"/"RX" respectively, and nothing if the field is not a valid ID3v1 integer genre.
*/
private fun String.parseId3v1Genre(): String? {
// ID3v1 genres are a plain integer value without formatting, so in that case
@ -200,6 +213,7 @@ private val ID3V2_GENRE_RE = Regex("((?:\\((\\d+|RX|CR)\\))*)(.+)?")
/**
* Parse an ID3v2 integer genre field, which has support for multiple genre values and combined
* named/integer genres.
*
* @return A list of one or more genres, or null if the field is not a valid ID3v2 integer genre.
*/
private fun String.parseId3v2Genre(): List<String>? {

View file

@ -24,6 +24,7 @@ import com.google.android.exoplayer2.metadata.vorbis.VorbisComment
/**
* Processing wrapper for [Metadata] that allows organized access to text-based audio tags.
*
* @param metadata The [Metadata] to wrap.
* @author Alexander Capehart (OxygenCobalt)
*/
@ -79,8 +80,9 @@ class TextTags(metadata: Metadata) {
/**
* Copies and sanitizes a possibly invalid string outputted from ExoPlayer.
*
* @return A new string allocated in a memory-safe manner with any UTF-8 errors replaced with
* the Unicode replacement byte sequence.
* the Unicode replacement byte sequence.
*/
private fun String.sanitize() = String(encodeToByteArray())
}

View file

@ -47,14 +47,16 @@ interface Library {
/**
* Finds a [Music] item [T] in the library by it's [Music.UID].
*
* @param uid The [Music.UID] to search for.
* @return The [T] corresponding to the given [Music.UID], or null if nothing could be found or
* the [Music.UID] did not correspond to a [T].
* the [Music.UID] did not correspond to a [T].
*/
fun <T : Music> find(uid: Music.UID): T?
/**
* Convert a [Song] from an another library into a [Song] in this [Library].
*
* @param song The [Song] to convert.
* @return The analogous [Song] in this [Library], or null if it does not exist.
*/
@ -62,6 +64,7 @@ interface Library {
/**
* Convert a [MusicParent] from an another library into a [MusicParent] in this [Library].
*
* @param parent The [MusicParent] to convert.
* @return The analogous [Album] in this [Library], or null if it does not exist.
*/
@ -69,6 +72,7 @@ interface Library {
/**
* Find a [Song] instance corresponding to the given Intent.ACTION_VIEW [Uri].
*
* @param context [Context] required to analyze the [Uri].
* @param uri [Uri] to search for.
* @return A [Song] corresponding to the given [Uri], or null if one could not be found.
@ -78,6 +82,7 @@ interface Library {
companion object {
/**
* Create an instance of [Library].
*
* @param rawSongs [RawSong]s to create the library out of.
* @param settings [MusicSettings] required.
*/
@ -117,9 +122,10 @@ private class LibraryImpl(rawSongs: List<RawSong>, settings: MusicSettings) : Li
/**
* Finds a [Music] item [T] in the library by it's [Music.UID].
*
* @param uid The [Music.UID] to search for.
* @return The [T] corresponding to the given [Music.UID], or null if nothing could be found or
* the [Music.UID] did not correspond to a [T].
* the [Music.UID] did not correspond to a [T].
*/
@Suppress("UNCHECKED_CAST") override fun <T : Music> find(uid: Music.UID) = uidMap[uid] as? T
@ -130,21 +136,22 @@ private class LibraryImpl(rawSongs: List<RawSong>, settings: MusicSettings) : Li
override fun findSongForUri(context: Context, uri: Uri) =
context.contentResolverSafe.useQuery(
uri, arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE)) { cursor ->
cursor.moveToFirst()
// We are weirdly limited to DISPLAY_NAME and SIZE when trying to locate a
// song. Do what we can to hopefully find the song the user wanted to open.
val displayName =
cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
val size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE))
songs.find { it.path.name == displayName && it.size == size }
}
cursor.moveToFirst()
// We are weirdly limited to DISPLAY_NAME and SIZE when trying to locate a
// song. Do what we can to hopefully find the song the user wanted to open.
val displayName =
cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
val size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE))
songs.find { it.path.name == displayName && it.size == size }
}
/**
* Build a list [SongImpl]s from the given [RawSong].
*
* @param rawSongs The [RawSong]s to build the [SongImpl]s from.
* @param settings [MusicSettings] to obtain user parsing configuration.
* @return A sorted list of [SongImpl]s derived from the [RawSong] that should be suitable for
* grouping.
* grouping.
*/
private fun buildSongs(rawSongs: List<RawSong>, settings: MusicSettings) =
Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING)
@ -152,11 +159,12 @@ private class LibraryImpl(rawSongs: List<RawSong>, settings: MusicSettings) : Li
/**
* Build a list of [Album]s from the given [Song]s.
*
* @param songs The [Song]s to build [Album]s from. These will be linked with their respective
* [Album]s when created.
* [Album]s when created.
* @param settings [MusicSettings] to obtain user parsing configuration.
* @return A non-empty list of [Album]s. These [Album]s will be incomplete and must be linked
* with parent [Artist] instances in order to be usable.
* with parent [Artist] instances in order to be usable.
*/
private fun buildAlbums(songs: List<SongImpl>, settings: MusicSettings): List<AlbumImpl> {
// Group songs by their singular raw album, then map the raw instances and their
@ -171,15 +179,16 @@ private class LibraryImpl(rawSongs: List<RawSong>, settings: MusicSettings) : Li
* Group up [Song]s and [Album]s into [Artist] instances. Both of these items are required as
* they group into [Artist] instances much differently, with [Song]s being grouped primarily by
* artist names, and [Album]s being grouped primarily by album artist names.
*
* @param songs The [Song]s to build [Artist]s from. One [Song] can result in the creation of
* one or more [Artist] instances. These will be linked with their respective [Artist]s when
* created.
* one or more [Artist] instances. These will be linked with their respective [Artist]s when
* created.
* @param albums The [Album]s to build [Artist]s from. One [Album] can result in the creation of
* one or more [Artist] instances. These will be linked with their respective [Artist]s when
* created.
* one or more [Artist] instances. These will be linked with their respective [Artist]s when
* created.
* @param settings [MusicSettings] to obtain user parsing configuration.
* @return A non-empty list of [Artist]s. These [Artist]s will consist of the combined groupings
* of [Song]s and [Album]s.
* of [Song]s and [Album]s.
*/
private fun buildArtists(
songs: List<SongImpl>,
@ -210,9 +219,10 @@ private class LibraryImpl(rawSongs: List<RawSong>, settings: MusicSettings) : Li
/**
* Group up [Song]s into [Genre] instances.
*
* @param [songs] The [Song]s to build [Genre]s from. One [Song] can result in the creation of
* one or more [Genre] instances. These will be linked with their respective [Genre]s when
* created.
* one or more [Genre] instances. These will be linked with their respective [Genre]s when
* created.
* @param settings [MusicSettings] to obtain user parsing configuration.
* @return A non-empty list of [Genre]s.
*/

View file

@ -46,14 +46,15 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
/**
* Library-backed implementation of [Song].
*
* @param rawSong The [RawSong] to derive the member data from.
* @param musicSettings [MusicSettings] to for user parsing configuration.
* @author Alexander Capehart (OxygenCobalt)
*/
class SongImpl(rawSong: RawSong, musicSettings: MusicSettings) : Song {
override val uid =
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
rawSong.musicBrainzId?.toUuidOrNull()?.let { Music.UID.musicBrainz(MusicMode.SONGS, it) }
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
rawSong.musicBrainzId?.toUuidOrNull()?.let { Music.UID.musicBrainz(MusicMode.SONGS, it) }
?: Music.UID.auxio(MusicMode.SONGS) {
// Song UIDs are based on the raw data without parsing so that they remain
// consistent across music setting changes. Parents are not held up to the
@ -164,6 +165,7 @@ class SongImpl(rawSong: RawSong, musicSettings: MusicSettings) : Song {
/**
* Links this [Song] with a parent [Album].
*
* @param album The parent [Album] to link to.
*/
fun link(album: AlbumImpl) {
@ -172,6 +174,7 @@ class SongImpl(rawSong: RawSong, musicSettings: MusicSettings) : Song {
/**
* Links this [Song] with a parent [Artist].
*
* @param artist The parent [Artist] to link to.
*/
fun link(artist: ArtistImpl) {
@ -180,6 +183,7 @@ class SongImpl(rawSong: RawSong, musicSettings: MusicSettings) : Song {
/**
* Links this [Song] with a parent [Genre].
*
* @param genre The parent [Genre] to link to.
*/
fun link(genre: GenreImpl) {
@ -188,6 +192,7 @@ class SongImpl(rawSong: RawSong, musicSettings: MusicSettings) : Song {
/**
* Perform final validation and organization on this instance.
*
* @return This instance upcasted to [Song].
*/
fun finalize(): Song {
@ -218,10 +223,11 @@ class SongImpl(rawSong: RawSong, musicSettings: MusicSettings) : Song {
/**
* Library-backed implementation of [Album].
*
* @param rawAlbum The [RawAlbum] to derive the member data from.
* @param musicSettings [MusicSettings] to for user parsing configuration.
* @param songs The [Song]s that are a part of this [Album]. These items will be linked to this
* [Album].
* [Album].
* @author Alexander Capehart (OxygenCobalt)
*/
class AlbumImpl(
@ -230,8 +236,8 @@ class AlbumImpl(
override val songs: List<SongImpl>
) : Album {
override val uid =
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
rawAlbum.musicBrainzId?.let { Music.UID.musicBrainz(MusicMode.ALBUMS, it) }
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
rawAlbum.musicBrainzId?.let { Music.UID.musicBrainz(MusicMode.ALBUMS, it) }
?: Music.UID.auxio(MusicMode.ALBUMS) {
// Hash based on only names despite the presence of a date to increase stability.
// I don't know if there is any situation where an artist will have two albums with
@ -286,6 +292,7 @@ class AlbumImpl(
/**
* Links this [Album] with a parent [Artist].
*
* @param artist The parent [Artist] to link to.
*/
fun link(artist: ArtistImpl) {
@ -294,6 +301,7 @@ class AlbumImpl(
/**
* Perform final validation and organization on this instance.
*
* @return This instance upcasted to [Album].
*/
fun finalize(): Album {
@ -313,11 +321,12 @@ class AlbumImpl(
/**
* Library-backed implementation of [Artist].
*
* @param rawArtist The [RawArtist] to derive the member data from.
* @param musicSettings [MusicSettings] to for user parsing configuration.
* @param songAlbums A list of the [Song]s and [Album]s that are a part of this [Artist] , either
* through artist or album artist tags. Providing [Song]s to the artist is optional. These instances
* will be linked to this [Artist].
* through artist or album artist tags. Providing [Song]s to the artist is optional. These
* instances will be linked to this [Artist].
* @author Alexander Capehart (OxygenCobalt)
*/
class ArtistImpl(
@ -326,8 +335,8 @@ class ArtistImpl(
songAlbums: List<Music>
) : Artist {
override val uid =
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
rawArtist.musicBrainzId?.let { Music.UID.musicBrainz(MusicMode.ARTISTS, it) }
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
rawArtist.musicBrainzId?.let { Music.UID.musicBrainz(MusicMode.ARTISTS, it) }
?: Music.UID.auxio(MusicMode.ARTISTS) { update(rawArtist.name) }
override val rawName = rawArtist.name
override val rawSortName = rawArtist.sortName
@ -379,14 +388,16 @@ class ArtistImpl(
* Returns the original position of this [Artist]'s [RawArtist] within the given [RawArtist]
* list. This can be used to create a consistent ordering within child [Artist] lists based on
* the original tag order.
*
* @param rawArtists The [RawArtist] instances to check. It is assumed that this [Artist]'s
* [RawArtist] will be within the list.
* [RawArtist] will be within the list.
* @return The index of the [Artist]'s [RawArtist] within the list.
*/
fun getOriginalPositionIn(rawArtists: List<RawArtist>) = rawArtists.indexOf(rawArtist)
/**
* Perform final validation and organization on this instance.
*
* @return This instance upcasted to [Artist].
*/
fun finalize(): Artist {
@ -400,6 +411,7 @@ class ArtistImpl(
}
/**
* Library-backed implementation of [Genre].
*
* @param rawGenre [RawGenre] to derive the member data from.
* @param musicSettings [MusicSettings] to for user parsing configuration.
* @param songs Child [SongImpl]s of this instance.
@ -450,14 +462,16 @@ class GenreImpl(
* Returns the original position of this [Genre]'s [RawGenre] within the given [RawGenre] list.
* This can be used to create a consistent ordering within child [Genre] lists based on the
* original tag order.
*
* @param rawGenres The [RawGenre] instances to check. It is assumed that this [Genre] 's
* [RawGenre] will be within the list.
* [RawGenre] will be within the list.
* @return The index of the [Genre]'s [RawGenre] within the list.
*/
fun getOriginalPositionIn(rawGenres: List<RawGenre>) = rawGenres.indexOf(rawGenre)
/**
* Perform final validation and organization on this instance.
*
* @return This instance upcasted to [Genre].
*/
fun finalize(): Music {
@ -468,6 +482,7 @@ class GenreImpl(
/**
* Update a [MessageDigest] with a lowercase [String].
*
* @param string The [String] to hash. If null, it will not be hashed.
*/
@VisibleForTesting
@ -481,6 +496,7 @@ fun MessageDigest.update(string: String?) {
/**
* Update a [MessageDigest] with the string representation of a [Date].
*
* @param date The [Date] to hash. If null, nothing will be done.
*/
@VisibleForTesting
@ -494,6 +510,7 @@ fun MessageDigest.update(date: Date?) {
/**
* Update a [MessageDigest] with the lowercase versions of all of the input [String]s.
*
* @param strings The [String]s to hash. If a [String] is null, it will not be hashed.
*/
@VisibleForTesting
@ -503,6 +520,7 @@ fun MessageDigest.update(strings: List<String?>) {
/**
* Update a [MessageDigest] with the little-endian bytes of a [Int].
*
* @param n The [Int] to write. If null, nothing will be done.
*/
@VisibleForTesting
@ -520,6 +538,7 @@ private val COLLATOR: Collator = Collator.getInstance().apply { strength = Colla
/**
* Provided implementation to create a [CollationKey] in the way described by [Music.collationKey].
* This should be used in all overrides of all [CollationKey].
*
* @param musicSettings [MusicSettings] required for user parsing configuration.
* @return A [CollationKey] that follows the specification described by [Music.collationKey].
*/

View file

@ -24,6 +24,7 @@ import org.oxycblt.auxio.music.storage.Directory
/**
* Raw information about a [SongImpl] obtained from the filesystem/Extractor instances.
*
* @author Alexander Capehart (OxygenCobalt)
*/
class RawSong(
@ -88,6 +89,7 @@ class RawSong(
/**
* Raw information about an [AlbumImpl] obtained from the component [SongImpl] instances.
*
* @author Alexander Capehart (OxygenCobalt)
*/
class RawAlbum(
@ -134,6 +136,7 @@ class RawAlbum(
/**
* Raw information about an [ArtistImpl] obtained from the component [SongImpl] and [AlbumImpl]
* instances.
*
* @author Alexander Capehart (OxygenCobalt)
*/
class RawArtist(
@ -175,6 +178,7 @@ class RawArtist(
/**
* Raw information about a [GenreImpl] obtained from the component [SongImpl] instances.
*
* @author Alexander Capehart (OxygenCobalt)
*/
class RawGenre(

View file

@ -27,6 +27,7 @@ import org.oxycblt.auxio.util.inflater
/**
* [RecyclerView.Adapter] that manages a list of [Directory] instances.
*
* @param listener A [DirectoryAdapter.Listener] to bind interactions to.
* @author Alexander Capehart (OxygenCobalt)
*/
@ -48,6 +49,7 @@ class DirectoryAdapter(private val listener: Listener) :
/**
* Add a [Directory] to the end of the list.
*
* @param dir The [Directory] to add.
*/
fun add(dir: Directory) {
@ -61,6 +63,7 @@ class DirectoryAdapter(private val listener: Listener) :
/**
* Add a list of [Directory] instances to the end of the list.
*
* @param dirs The [Directory instances to add.
*/
fun addAll(dirs: List<Directory>) {
@ -71,6 +74,7 @@ class DirectoryAdapter(private val listener: Listener) :
/**
* Remove a [Directory] from the list.
*
* @param dir The [Directory] to remove. Must exist in the list.
*/
fun remove(dir: Directory) {
@ -87,12 +91,14 @@ class DirectoryAdapter(private val listener: Listener) :
/**
* A [RecyclerView.Recycler] that displays a [Directory]. Use [from] to create an instance.
*
* @author Alexander Capehart (OxygenCobalt)
*/
class MusicDirViewHolder private constructor(private val binding: ItemMusicDirBinding) :
DialogRecyclerView.ViewHolder(binding.root) {
/**
* Bind new data to this instance.
*
* @param dir The new [Directory] to bind.
* @param listener A [DirectoryAdapter.Listener] to bind interactions to.
*/
@ -104,6 +110,7 @@ class MusicDirViewHolder private constructor(private val binding: ItemMusicDirBi
companion object {
/**
* Create a new instance.
*
* @param parent The parent to inflate this instance from.
* @return A new instance.
*/

View file

@ -28,6 +28,7 @@ import org.oxycblt.auxio.R
/**
* A full absolute path to a file. Only intended for display purposes. For accessing files, URIs are
* preferred in all cases due to scoped storage limitations.
*
* @param name The name of the file.
* @param parent The parent [Directory] of the file.
* @author Alexander Capehart (OxygenCobalt)
@ -36,6 +37,7 @@ data class Path(val name: String, val parent: Directory)
/**
* A volume-aware relative path to a directory.
*
* @param volume The [StorageVolume] that the [Directory] is contained in.
* @param relativePath The relative path from within the [StorageVolume] to the [Directory].
* @author Alexander Capehart (OxygenCobalt)
@ -43,6 +45,7 @@ data class Path(val name: String, val parent: Directory)
class Directory private constructor(val volume: StorageVolume, val relativePath: String) {
/**
* Resolve the [Directory] instance into a human-readable path name.
*
* @param context [Context] required to obtain volume descriptions.
* @return A human-readable path.
* @see StorageVolume.getDescription
@ -55,8 +58,9 @@ class Directory private constructor(val volume: StorageVolume, val relativePath:
* violation of the document tree URI contract, but it's also the only one can sensibly work
* with these uris in the UI, and it doesn't exactly matter since we never write or read to
* directory.
*
* @return A URI [String] abiding by the document tree specification, or null if the [Directory]
* is not valid.
* is not valid.
*/
fun toDocumentTreeUri() =
// Document tree URIs consist of a prefixed volume name followed by a relative path.
@ -84,9 +88,10 @@ class Directory private constructor(val volume: StorageVolume, val relativePath:
/**
* Create a new directory instance from the given components.
*
* @param volume The [StorageVolume] that the [Directory] is contained in.
* @param relativePath The relative path from within the [StorageVolume] to the [Directory].
* Will be stripped of any trailing separators for a consistent internal representation.
* Will be stripped of any trailing separators for a consistent internal representation.
* @return A new [Directory] created from the components.
*/
fun from(volume: StorageVolume, relativePath: String) =
@ -97,8 +102,9 @@ class Directory private constructor(val volume: StorageVolume, val relativePath:
* Create a new directory from a document tree URI. This is a huge violation of the document
* tree URI contract, but it's also the only one can sensibly work with these uris in the
* UI, and it doesn't exactly matter since we never write or read directory.
*
* @param storageManager [StorageManager] in order to obtain the [StorageVolume] specified
* in the given URI.
* in the given URI.
* @param uri The URI string to parse into a [Directory].
* @return A new [Directory] parsed from the URI, or null if the URI is not valid.
*/
@ -123,26 +129,29 @@ class Directory private constructor(val volume: StorageVolume, val relativePath:
/**
* Represents the configuration for specific directories to filter to/from when loading music.
*
* @param dirs A list of [Directory] instances. How these are interpreted depends on [shouldInclude]
* @param shouldInclude True if the library should only load from the [Directory] instances, false
* if the library should not load from the [Directory] instances.
* if the library should not load from the [Directory] instances.
* @author Alexander Capehart (OxygenCobalt)
*/
data class MusicDirectories(val dirs: List<Directory>, val shouldInclude: Boolean)
/**
* A mime type of a file. Only intended for display.
*
* @param fromExtension The mime type obtained by analyzing the file extension.
* @param fromFormat The mime type obtained by analyzing the file format. Null if could not be
* obtained.
* obtained.
* @author Alexander Capehart (OxygenCobalt)
*/
data class MimeType(val fromExtension: String, val fromFormat: String?) {
/**
* Resolve the mime type into a human-readable format name, such as "Ogg Vorbis".
*
* @param context [Context] required to obtain human-readable strings.
* @return A human-readable name for this mime type. Will first try [fromFormat], then falling
* back to [fromExtension], and then null if that fails.
* back to [fromExtension], and then null if that fails.
*/
fun resolveName(context: Context): String? {
// We try our best to produce a more readable name for the common audio formats.

View file

@ -42,22 +42,25 @@ import org.oxycblt.auxio.util.logD
* music extraction process and primarily intended for redundancy for files not natively supported
* by other extractors. Solely relying on this is not recommended, as it often produces bad
* metadata.
*
* @author Alexander Capehart (OxygenCobalt)
*/
interface MediaStoreExtractor {
/**
* Query the media database.
*
* @return A new [Query] returned from the media database.
*/
suspend fun query(): Query
/**
* Consume the [Cursor] loaded after [query].
*
* @param query The [Query] to consume.
* @param cache A [Cache] used to avoid extracting metadata for cached songs, or null if no
* [Cache] was available.
* [Cache] was available.
* @param incompleteSongs A channel where songs that could not be retrieved from the [Cache]
* should be sent to.
* should be sent to.
* @param completeSongs A channel where completed songs should be sent to.
*/
suspend fun consume(
@ -79,6 +82,7 @@ interface MediaStoreExtractor {
companion object {
/**
* Create a framework-backed instance.
*
* @param context [Context] required.
* @param musicSettings [MusicSettings] required.
* @return A new [MediaStoreExtractor] that will work best on the device's API level.
@ -158,27 +162,28 @@ private abstract class BaseMediaStoreExtractor(
context.contentResolverSafe.useQuery(
MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI,
arrayOf(MediaStore.Audio.Genres._ID, MediaStore.Audio.Genres.NAME)) { genreCursor ->
val idIndex = genreCursor.getColumnIndexOrThrow(MediaStore.Audio.Genres._ID)
val nameIndex = genreCursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME)
val idIndex = genreCursor.getColumnIndexOrThrow(MediaStore.Audio.Genres._ID)
val nameIndex = genreCursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME)
while (genreCursor.moveToNext()) {
val id = genreCursor.getLong(idIndex)
val name = genreCursor.getStringOrNull(nameIndex) ?: continue
while (genreCursor.moveToNext()) {
val id = genreCursor.getLong(idIndex)
val name = genreCursor.getStringOrNull(nameIndex) ?: continue
context.contentResolverSafe.useQuery(
MediaStore.Audio.Genres.Members.getContentUri(VOLUME_EXTERNAL, id),
arrayOf(MediaStore.Audio.Genres.Members._ID)) { cursor ->
val songIdIndex =
cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.Members._ID)
context.contentResolverSafe.useQuery(
MediaStore.Audio.Genres.Members.getContentUri(VOLUME_EXTERNAL, id),
arrayOf(MediaStore.Audio.Genres.Members._ID)) { cursor ->
val songIdIndex =
cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.Members._ID)
while (cursor.moveToNext()) {
// Assume that a song can't inhabit multiple genre entries, as I doubt
// MediaStore is actually aware that songs can have multiple genres.
genreNamesMap[cursor.getLong(songIdIndex)] = name
}
while (cursor.moveToNext()) {
// Assume that a song can't inhabit multiple genre entries, as I
// doubt
// MediaStore is actually aware that songs can have multiple genres.
genreNamesMap[cursor.getLong(songIdIndex)] = name
}
}
}
}
}
logD("Finished initialization in ${System.currentTimeMillis() - start}ms")
return wrapQuery(cursor, genreNamesMap)
@ -232,15 +237,17 @@ private abstract class BaseMediaStoreExtractor(
/**
* The companion template to add to the projection's selector whenever arguments are added by
* [addDirToSelector].
*
* @see addDirToSelector
*/
protected abstract val dirSelectorTemplate: String
/**
* Add a [Directory] to the given list of projection selector arguments.
*
* @param dir The [Directory] to add.
* @param args The destination list to append selector arguments to that are analogous to the
* given [Directory].
* given [Directory].
* @return true if the [Directory] was added, false otherwise.
* @see dirSelectorTemplate
*/
@ -431,6 +438,7 @@ private class Api21MediaStoreExtractor(context: Context, musicSettings: MusicSet
/**
* A [BaseMediaStoreExtractor] that implements common behavior supported from API 29 onwards.
*
* @param context [Context] required to query the media database.
* @author Alexander Capehart (OxygenCobalt)
*/
@ -494,8 +502,8 @@ private abstract class BaseApi29MediaStoreExtractor(
/**
* A [BaseMediaStoreExtractor] that completes the music loading process in a way compatible with at
* API
* 29.
* API 29.
*
* @param context [Context] required to query the media database.
* @author Alexander Capehart (OxygenCobalt)
*/
@ -535,6 +543,7 @@ private class Api29MediaStoreExtractor(context: Context, musicSettings: MusicSet
/**
* A [BaseMediaStoreExtractor] that completes the music loading process in a way compatible from API
* 30 onwards.
*
* @param context [Context] required to query the media database.
* @author Alexander Capehart (OxygenCobalt)
*/
@ -584,8 +593,9 @@ private class Api30MediaStoreExtractor(context: Context, musicSettings: MusicSet
* Unpack the track number from a combined track + disc [Int] field. These fields appear within
* MediaStore's TRACK column, and combine the track and disc value into a single field where the
* disc number is the 4th+ digit.
*
* @return The track number extracted from the combined integer value, or null if the value was
* zero.
* zero.
*/
private fun Int.unpackTrackNo() = transformPositionField(mod(1000), null)
@ -593,6 +603,7 @@ private fun Int.unpackTrackNo() = transformPositionField(mod(1000), null)
* Unpack the disc number from a combined track + disc [Int] field. These fields appear within
* MediaStore's TRACK column, and combine the track and disc value into a single field where the
* disc number is the 4th+ digit.
*
* @return The disc number extracted from the combined integer field, or null if the value was zero.
*/
private fun Int.unpackDiscNo() = transformPositionField(div(1000), null)

View file

@ -41,6 +41,7 @@ import org.oxycblt.auxio.util.showToast
/**
* Dialog that manages the music dirs setting.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint
@ -149,8 +150,9 @@ class MusicDirsDialog :
/**
* 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.
* is null or not valid.
*/
private fun addDocumentTreeUriToDirs(uri: Uri?) {
if (uri == null) {

View file

@ -42,10 +42,11 @@ val Context.contentResolverSafe: ContentResolver
/**
* A shortcut for querying the [ContentResolver] database.
*
* @param uri The [Uri] of content to retrieve.
* @param projection A list of SQL columns to query from the database.
* @param selector A SQL selection statement to filter results. Spaces where arguments should be
* filled in are represented with a "?".
* 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 return the queried [Cursor].
@ -61,13 +62,14 @@ fun ContentResolver.safeQuery(
/**
* A shortcut for [safeQuery] with [use] applied, automatically cleaning up the [Cursor]'s resources
* when no longer used.
*
* @param uri The [Uri] of content to retrieve.
* @param projection A list of SQL columns to query from the database.
* @param selector A SQL selection statement to filter results. Spaces where arguments should be
* filled in are represented with a "?".
* filled in are represented with a "?".
* @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.
* is empty.
* @throws IllegalStateException If the [ContentResolver] did not return the queried [Cursor].
* @see ContentResolver.query
*/
@ -84,6 +86,7 @@ private val EXTERNAL_COVERS_URI = Uri.parse("content://media/external/audio/albu
/**
* Convert a [MediaStore] Song ID into a [Uri] to it's audio file.
*
* @return An external storage audio file [Uri]. May not exist.
* @see ContentUris.withAppendedId
* @see MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
@ -94,6 +97,7 @@ fun Long.toAudioUri() =
/**
* Convert a [MediaStore] Album ID into a [Uri] to it's system-provided album cover. This cover will
* be fast to load, but will be lower quality.
*
* @return An external storage image [Uri]. May not exist.
* @see ContentUris.withAppendedId
*/
@ -105,6 +109,7 @@ fun Long.toCoverUri() = ContentUris.withAppendedId(EXTERNAL_COVERS_URI, this)
/**
* Provides the analogous method to [StorageManager.getStorageVolumes] method that is usable from
* API 21 to API 23, in which the [StorageManager] API was hidden and differed greatly.
*
* @see StorageManager.getStorageVolumes
*/
@Suppress("NewApi")
@ -114,6 +119,7 @@ private val SM_API21_GET_VOLUME_LIST_METHOD: Method by
/**
* Provides the analogous method to [StorageVolume.getDirectory] method that is usable from API 21
* to API 23, in which the [StorageVolume] API was hidden and differed greatly.
*
* @see StorageVolume.getDirectory
*/
@Suppress("NewApi")
@ -122,6 +128,7 @@ private val SV_API21_GET_PATH_METHOD: Method by lazyReflectedMethod(StorageVolum
/**
* The [StorageVolume] considered the "primary" volume by the system, obtained in a
* version-compatible manner.
*
* @see StorageManager.getPrimaryStorageVolume
* @see StorageVolume.isPrimary
*/
@ -131,6 +138,7 @@ val StorageManager.primaryStorageVolumeCompat: StorageVolume
/**
* The list of [StorageVolume]s currently recognized by [StorageManager], in a version-compatible
* manner.
*
* @see StorageManager.getStorageVolumes
*/
val StorageManager.storageVolumesCompat: List<StorageVolume>
@ -145,6 +153,7 @@ val StorageManager.storageVolumesCompat: List<StorageVolume>
/**
* The the absolute path to this [StorageVolume]'s directory within the file-system, in a
* version-compatible manner. Will be null if the [StorageVolume] cannot be read.
*
* @see StorageVolume.getDirectory
*/
val StorageVolume.directoryCompat: String?
@ -164,6 +173,7 @@ val StorageVolume.directoryCompat: String?
/**
* Get the human-readable description of this volume, such as "Internal Shared Storage".
*
* @param context [Context] required to obtain human-readable string resources.
* @return A human-readable name for this volume.
*/
@ -173,6 +183,7 @@ fun StorageVolume.getDescriptionCompat(context: Context): String = getDescriptio
/**
* If this [StorageVolume] is considered the "Primary" volume where the Android System is kept. May
* still be a removable volume.
*
* @see StorageVolume.isPrimary
*/
val StorageVolume.isPrimaryCompat: Boolean
@ -181,6 +192,7 @@ val StorageVolume.isPrimaryCompat: Boolean
/**
* If this storage is "emulated", i.e intrinsic to the device, obtained in a version compatible
* manner.
*
* @see StorageVolume.isEmulated
*/
val StorageVolume.isEmulatedCompat: Boolean
@ -198,6 +210,7 @@ val StorageVolume.isInternalCompat: Boolean
/**
* The unique identifier for this [StorageVolume], obtained in a version compatible manner. Can be
* null.
*
* @see StorageVolume.getUuid
*/
val StorageVolume.uuidCompat: String?
@ -206,6 +219,7 @@ val StorageVolume.uuidCompat: String?
/**
* The current state of this [StorageVolume], such as "mounted" or "read-only", obtained in a
* version compatible manner.
*
* @see StorageVolume.getState
*/
val StorageVolume.stateCompat: String
@ -214,6 +228,7 @@ val StorageVolume.stateCompat: String
/**
* Returns the name of this volume that can be used to interact with [MediaStore], in a version
* compatible manner. Will be null if the volume is not scanned by [MediaStore].
*
* @see StorageVolume.getMediaStoreVolumeName
*/
val StorageVolume.mediaStoreVolumeNameCompat: String?

View file

@ -68,6 +68,7 @@ interface Indexer {
* Register a [Controller] for this instance. This instance will handle any commands to start
* the music loading process. There can be only one [Controller] at a time. Will invoke all
* [Listener] methods to initialize the instance with the current state.
*
* @param controller The [Controller] to register. Will do nothing if already registered.
*/
fun registerController(controller: Controller)
@ -75,8 +76,9 @@ interface Indexer {
/**
* Unregister the [Controller] from this instance, prevent it from recieving any further
* commands.
*
* @param controller The [Controller] to unregister. Must be the current [Controller]. Does
* nothing if invoked by another [Controller] implementation.
* nothing if invoked by another [Controller] implementation.
*/
fun unregisterController(controller: Controller)
@ -84,14 +86,16 @@ interface Indexer {
* Register the [Listener] for this instance. This can be used to receive rapid-fire updates to
* the current music loading state. There can be only one [Listener] at a time. Will invoke all
* [Listener] methods to initialize the instance with the current state.
*
* @param listener The [Listener] to add.
*/
fun registerListener(listener: Listener)
/**
* Unregister a [Listener] from this instance, preventing it from recieving any further updates.
*
* @param listener The [Listener] to unregister. Must be the current [Listener]. Does nothing if
* invoked by another [Listener] implementation.
* invoked by another [Listener] implementation.
* @see Listener
*/
fun unregisterListener(listener: Listener)
@ -99,9 +103,10 @@ interface Indexer {
/**
* Start the indexing process. This should be done from in the background from [Controller]'s
* context after a command has been received to start the process.
*
* @param context [Context] required to load music.
* @param withCache Whether to use the cache or not when loading. If false, the cache will still
* be written, but no cache entries will be loaded into the new library.
* be written, but no cache entries will be loaded into the new library.
* @param scope The [CoroutineScope] to run the indexing job in.
* @return The [Job] stacking the indexing status.
*/
@ -111,8 +116,9 @@ interface Indexer {
* Request that the music library should be reloaded. This should be used by components that do
* not manage the indexing process in order to signal that the [Indexer.Controller] should call
* [index] eventually.
*
* @param withCache Whether to use the cache when loading music. Does nothing if there is no
* [Indexer.Controller].
* [Indexer.Controller].
*/
fun requestReindex(withCache: Boolean)
@ -126,6 +132,7 @@ interface Indexer {
sealed class State {
/**
* Music loading is ongoing.
*
* @param indexing The current music loading progress..
* @see Indexer.Indexing
*/
@ -133,6 +140,7 @@ interface Indexer {
/**
* Music loading has completed.
*
* @param result The outcome of the music loading process.
*/
data class Complete(val result: Result<Library>) : State()
@ -140,6 +148,7 @@ interface Indexer {
/**
* Represents the current progress of the music loader. Usually encapsulated in a [State].
*
* @see State.Indexing
*/
sealed class Indexing {
@ -150,6 +159,7 @@ interface Indexer {
/**
* Music loading has a definite progress.
*
* @param current The current amount of songs that have been loaded.
* @param total The projected total amount of songs that will be loaded.
*/
@ -182,7 +192,7 @@ interface Indexer {
* Notes:
* - Null means that no loading is going on, but no load has completed either.
* - [State.Complete] may represent a previous load, if the current loading process was
* canceled for one reason or another.
* canceled for one reason or another.
*/
fun onIndexerStateChanged(state: State?)
}
@ -195,8 +205,9 @@ interface Indexer {
/**
* Called when a new music loading process was requested. Implementations should forward
* this to [index].
*
* @param withCache Whether to use the cache or not when loading. If false, the cache should
* still be written, but no cache entries will be loaded into the new library.
* still be written, but no cache entries will be loaded into the new library.
* @see index
*/
fun onStartIndexing(withCache: Boolean)
@ -390,8 +401,9 @@ constructor(
* Emit a new [Indexer.State.Indexing] state. This can be used to signal the current state of
* the music loading process to external code. Assumes that the callee has already checked if
* they have not been canceled and thus have the ability to emit a new state.
*
* @param indexing The new [Indexer.Indexing] state to emit, or null if no loading process is
* occurring.
* occurring.
*/
@Synchronized
private fun emitIndexing(indexing: Indexer.Indexing?) {
@ -409,8 +421,9 @@ constructor(
* Emit a new [Indexer.State.Complete] state. This can be used to signal the completion of the
* music loading process to external code. Will check if the callee has not been canceled and
* thus has the ability to emit a new state
*
* @param result The new [Result] to emit, representing the outcome of the music loading
* process.
* process.
*/
private suspend fun emitCompletion(result: Result<Library>) {
yield()

View file

@ -29,6 +29,7 @@ import org.oxycblt.auxio.util.newMainPendingIntent
/**
* A dynamic [ForegroundServiceNotification] that shows the current music loading state.
*
* @param context [Context] required to create the notification.
* @author Alexander Capehart (OxygenCobalt)
*/
@ -53,6 +54,7 @@ class IndexingNotification(private val context: Context) :
/**
* Update this notification with the new music loading state.
*
* @param indexing The new music loading state to display in the notification.
* @return true if the notification updated, false otherwise
*/
@ -90,6 +92,7 @@ class IndexingNotification(private val context: Context) :
/**
* A static [ForegroundServiceNotification] that signals to the user that the app is currently
* monitoring the music library for changes.
*
* @author Alexander Capehart (OxygenCobalt)
*/
class ObservingNotification(context: Context) :

View file

@ -50,9 +50,9 @@ import org.oxycblt.auxio.util.logD
* This [Service] also handles automatic rescanning, as that is a similarly long-running background
* operation that would be unsuitable elsewhere in the app.
*
* TODO: Unify with PlaybackService as part of the service independence project
*
* @author Alexander Capehart (OxygenCobalt)
*
* TODO: Unify with PlaybackService as part of the service independence project
*/
@AndroidEntryPoint
class IndexerService : Service(), Indexer.Controller, MusicSettings.Listener {
@ -176,6 +176,7 @@ class IndexerService : Service(), Indexer.Controller, MusicSettings.Listener {
/**
* 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) {

View file

@ -29,6 +29,7 @@ import org.oxycblt.auxio.util.inflater
/**
* An [RecyclerView.Adapter] that displays a list of [Artist] choices.
*
* @param listener A [ClickableListListener] to bind interactions to.
* @author OxygenCobalt.
*/
@ -46,6 +47,7 @@ class ArtistChoiceAdapter(private val listener: ClickableListListener<Artist>) :
/**
* Immediately update the [Artist] choices.
*
* @param newArtists The new [Artist]s to show.
*/
fun submitList(newArtists: List<Artist>) {
@ -64,6 +66,7 @@ class ArtistChoiceViewHolder(private val binding: ItemPickerChoiceBinding) :
DialogRecyclerView.ViewHolder(binding.root) {
/**
* Bind new data to this instance.
*
* @param artist The new [Artist] to bind.
* @param listener A [ClickableListListener] to bind interactions to.
*/
@ -76,6 +79,7 @@ class ArtistChoiceViewHolder(private val binding: ItemPickerChoiceBinding) :
companion object {
/**
* Create a new instance.
*
* @param parent The parent to inflate this instance from.
* @return A new instance.
*/

View file

@ -28,6 +28,7 @@ import org.oxycblt.auxio.ui.NavigationViewModel
/**
* An [ArtistPickerDialog] intended for when [Artist] navigation is ambiguous.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint

View file

@ -35,6 +35,7 @@ import org.oxycblt.auxio.util.collectImmediately
* The base class for dialogs that implements common behavior across all [Artist] pickers. These are
* shown whenever what to do with an item's [Artist] is ambiguous, as there are multiple [Artist]'s
* to choose from.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint

View file

@ -31,6 +31,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
/**
* An [ArtistPickerDialog] intended for when [Artist] playback is ambiguous.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint

View file

@ -29,6 +29,7 @@ import org.oxycblt.auxio.util.inflater
/**
* An [RecyclerView.Adapter] that displays a list of [Genre] choices.
*
* @param listener A [ClickableListListener] to bind interactions to.
* @author OxygenCobalt.
*/
@ -46,6 +47,7 @@ class GenreChoiceAdapter(private val listener: ClickableListListener<Genre>) :
/**
* Immediately update the [Genre] choices.
*
* @param newGenres The new [Genre]s to show.
*/
fun submitList(newGenres: List<Genre>) {
@ -64,6 +66,7 @@ class GenreChoiceViewHolder(private val binding: ItemPickerChoiceBinding) :
DialogRecyclerView.ViewHolder(binding.root) {
/**
* Bind new data to this instance.
*
* @param genre The new [Genre] to bind.
* @param listener A [ClickableListListener] to bind interactions to.
*/
@ -76,6 +79,7 @@ class GenreChoiceViewHolder(private val binding: ItemPickerChoiceBinding) :
companion object {
/**
* Create a new instance.
*
* @param parent The parent to inflate this instance from.
* @return A new instance.
*/

View file

@ -39,6 +39,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
/**
* A picker [ViewBindingDialogFragment] intended for when [Genre] playback is ambiguous.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint

View file

@ -29,6 +29,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
/**
* a [ViewModel] that manages the current music picker state. Make it so that the dialogs just
* contain the music themselves and then exit if the library changes.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@HiltViewModel
@ -62,6 +63,7 @@ class PickerViewModel @Inject constructor(private val musicRepository: MusicRepo
/**
* Set a new [currentItem] from it's [Music.UID].
*
* @param uid The [Music.UID] of the [Song] to update to.
*/
fun setItemUid(uid: Music.UID) {

View file

@ -22,6 +22,7 @@ import org.oxycblt.auxio.IntegerTable
/**
* Represents a configuration option for what kind of "secondary" action to show in a particular UI
* context.
*
* @author Alexander Capehart (OxygenCobalt)
*/
enum class ActionMode {
@ -34,6 +35,7 @@ enum class ActionMode {
/**
* The integer representation of this instance.
*
* @see fromIntCode
*/
val intCode: Int
@ -47,6 +49,7 @@ enum class ActionMode {
companion object {
/**
* Convert a [ActionMode] integer representation into an instance.
*
* @param intCode An integer representation of a [ActionMode]
* @return The corresponding [ActionMode], or null if the [ActionMode] is invalid.
* @see ActionMode.intCode

View file

@ -35,6 +35,7 @@ import org.oxycblt.auxio.util.getColorCompat
/**
* A [ViewBindingFragment] that shows the current playback state in a compact manner.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint

View file

@ -31,6 +31,7 @@ import org.oxycblt.auxio.util.getDimen
/**
* The [BaseBottomSheetBehavior] for the playback bottom sheet. This bottom sheet
*
* @author Alexander Capehart (OxygenCobalt)
*/
class PlaybackBottomSheetBehavior<V : View>(context: Context, attributeSet: AttributeSet?) :

View file

@ -47,6 +47,7 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
/**
* A [ViewBindingFragment] more information about the currently playing song, alongside all
* available controls.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@AndroidEntryPoint

View file

@ -31,6 +31,7 @@ import org.oxycblt.auxio.util.logD
/**
* User configuration specific to the playback system.
*
* @author Alexander Capehart (OxygenCobalt)
*/
interface PlaybackSettings : Settings<PlaybackSettings.Listener> {

View file

@ -21,52 +21,60 @@ import android.text.format.DateUtils
/**
* Convert milliseconds into deci-seconds (1/10th of a second).
*
* @return A converted deci-second value.
*/
fun Long.msToDs() = floorDiv(100)
/**
* Convert milliseconds into seconds.
*
* @return A converted second value.
*/
fun Long.msToSecs() = floorDiv(1000)
/**
* Convert deci-seconds (1/10th of a second) into milliseconds.
*
* @return A converted millisecond value.
*/
fun Long.dsToMs() = times(100)
/**
* Convert deci-seconds (1/10th of a second) into seconds.
*
* @return A converted second value.
*/
fun Long.dsToSecs() = floorDiv(10)
/**
* Convert seconds into milliseconds.
*
* @return A converted millisecond value.
*/
fun Long.secsToMs() = times(1000)
/**
* Convert a millisecond value into a string duration.
*
* @param isElapsed Whether this duration is represents elapsed time. If this is false, then --:--
* will be returned if the second value is 0.
* will be returned if the second value is 0.
*/
fun Long.formatDurationMs(isElapsed: Boolean) = msToSecs().formatDurationSecs(isElapsed)
/**
* // * Format a deci-second value (1/10th of a second) into a string duration.
*
* @param isElapsed Whether this duration is represents elapsed time. If this is false, then --:--
* will be returned if the second value is 0.
* will be returned if the second value is 0.
*/
fun Long.formatDurationDs(isElapsed: Boolean) = dsToSecs().formatDurationSecs(isElapsed)
/**
* Convert a second value into a string duration.
*
* @param isElapsed Whether this duration is represents elapsed time. If this is false, then --:--
* will be returned if the second value is 0.
* will be returned if the second value is 0.
*/
fun Long.formatDurationSecs(isElapsed: Boolean): String {
if (!isElapsed && this == 0L) {

View file

@ -33,6 +33,7 @@ import org.oxycblt.auxio.playback.state.*
/**
* An [ViewModel] that provides a safe UI frontend for the current playback state.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@HiltViewModel
@ -76,6 +77,7 @@ constructor(
/**
* Flag signaling to open a picker dialog in order to resolve an ambiguous choice when playing a
* [Song] from one of it's [Artist]s.
*
* @see playFromArtist
*/
val artistPickerSong: StateFlow<Song?>
@ -163,6 +165,7 @@ constructor(
* - If [MusicMode.ALBUMS], the [Song] is played from it's [Album].
* - If [MusicMode.ARTISTS], the [Song] is played from one of it's [Artist]s.
* - If [MusicMode.GENRES], the [Song] is played from one of it's [Genre]s.
*
* @param song The [Song] to play.
* @param playbackMode The [MusicMode] to play from.
*/
@ -177,9 +180,10 @@ constructor(
/**
* Play a [Song] from one of it's [Artist]s.
*
* @param song The [Song] to play.
* @param artist The [Artist] to play from. Must be linked to the [Song]. If null, the user will
* be prompted on what artist to play. Defaults to null.
* be prompted on what artist to play. Defaults to null.
*/
fun playFromArtist(song: Song, artist: Artist? = null) {
if (artist != null) {
@ -194,6 +198,7 @@ constructor(
/**
* Mark the [Artist] playback choice process as complete. This should occur when the [Artist]
* choice dialog is opened after this flag is detected.
*
* @see playFromArtist
*/
fun finishPlaybackArtistPicker() {
@ -202,9 +207,10 @@ constructor(
/**
* PLay a [Song] from one of it's [Genre]s.
*
* @param song The [Song] to play.
* @param genre The [Genre] to play from. Must be linked to the [Song]. If null, the user will
* be prompted on what artist to play. Defaults to null.
* be prompted on what artist to play. Defaults to null.
*/
fun playFromGenre(song: Song, genre: Genre? = null) {
if (genre != null) {
@ -219,6 +225,7 @@ constructor(
/**
* Mark the [Genre] playback choice process as complete. This should occur when the [Genre]
* choice dialog is opened after this flag is detected.
*
* @see playFromGenre
*/
fun finishPlaybackGenrePicker() {
@ -227,24 +234,28 @@ constructor(
/**
* Play an [Album].
*
* @param album The [Album] to play.
*/
fun play(album: Album) = playImpl(null, album, false)
/**
* Play an [Artist].
*
* @param artist The [Artist] to play.
*/
fun play(artist: Artist) = playImpl(null, artist, false)
/**
* Play a [Genre].
*
* @param genre The [Genre] to play.
*/
fun play(genre: Genre) = playImpl(null, genre, false)
/**
* Play a [Music] selection.
*
* @param selection The selection to play.
*/
fun play(selection: List<Music>) =
@ -252,24 +263,28 @@ constructor(
/**
* Shuffle an [Album].
*
* @param album The [Album] to shuffle.
*/
fun shuffle(album: Album) = playImpl(null, album, true)
/**
* Shuffle an [Artist].
*
* @param artist The [Artist] to shuffle.
*/
fun shuffle(artist: Artist) = playImpl(null, artist, true)
/**
* Shuffle an [Genre].
*
* @param genre The [Genre] to shuffle.
*/
fun shuffle(genre: Genre) = playImpl(null, genre, true)
/**
* Shuffle a [Music] selection.
*
* @param selection The selection to shuffle.
*/
fun shuffle(selection: List<Music>) =
@ -298,6 +313,7 @@ constructor(
/**
* Start the given [InternalPlayer.Action] to be completed eventually. This can be used to
* enqueue a playback action at startup to then occur when the music library is fully loaded.
*
* @param action The [InternalPlayer.Action] to perform eventually.
*/
fun startAction(action: InternalPlayer.Action) {
@ -308,6 +324,7 @@ constructor(
/**
* Seek to the given position in the currently playing [Song].
*
* @param positionDs The position to seek to, in deci-seconds (1/10th of a second).
*/
fun seekTo(positionDs: Long) {
@ -328,6 +345,7 @@ constructor(
/**
* Add a [Song] to the top of the queue.
*
* @param song The [Song] to add.
*/
fun playNext(song: Song) {
@ -336,6 +354,7 @@ constructor(
/**
* Add a [Album] to the top of the queue.
*
* @param album The [Album] to add.
*/
fun playNext(album: Album) {
@ -344,6 +363,7 @@ constructor(
/**
* Add a [Artist] to the top of the queue.
*
* @param artist The [Artist] to add.
*/
fun playNext(artist: Artist) {
@ -352,6 +372,7 @@ constructor(
/**
* Add a [Genre] to the top of the queue.
*
* @param genre The [Genre] to add.
*/
fun playNext(genre: Genre) {
@ -360,6 +381,7 @@ constructor(
/**
* Add a selection to the top of the queue.
*
* @param selection The [Music] selection to add.
*/
fun playNext(selection: List<Music>) {
@ -368,6 +390,7 @@ constructor(
/**
* Add a [Song] to the end of the queue.
*
* @param song The [Song] to add.
*/
fun addToQueue(song: Song) {
@ -376,6 +399,7 @@ constructor(
/**
* Add a [Album] to the end of the queue.
*
* @param album The [Album] to add.
*/
fun addToQueue(album: Album) {
@ -384,6 +408,7 @@ constructor(
/**
* Add a [Artist] to the end of the queue.
*
* @param artist The [Artist] to add.
*/
fun addToQueue(artist: Artist) {
@ -392,6 +417,7 @@ constructor(
/**
* Add a [Genre] to the end of the queue.
*
* @param genre The [Genre] to add.
*/
fun addToQueue(genre: Genre) {
@ -400,6 +426,7 @@ constructor(
/**
* Add a selection to the end of the queue.
*
* @param selection The [Music] selection to add.
*/
fun addToQueue(selection: List<Music>) {
@ -420,6 +447,7 @@ constructor(
/**
* Toggle [repeatMode] (ex. from [RepeatMode.NONE] to [RepeatMode.TRACK])
*
* @see RepeatMode.increment
*/
fun toggleRepeatMode() {
@ -430,6 +458,7 @@ constructor(
/**
* Force-save the current playback state.
*
* @param onDone Called when the save is completed with true if successful, and false otherwise.
*/
fun savePlaybackState(onDone: (Boolean) -> Unit) {
@ -440,6 +469,7 @@ constructor(
/**
* Clear the current playback state.
*
* @param onDone Called when the wipe is completed with true if successful, and false otherwise.
*/
fun wipePlaybackState(onDone: (Boolean) -> Unit) {
@ -448,8 +478,9 @@ constructor(
/**
* Force-restore the current playback state.
*
* @param onDone Called when the restoration is completed with true if successful, and false
* otherwise.
* otherwise.
*/
fun tryRestorePlaybackState(onDone: (Boolean) -> Unit) {
viewModelScope.launch {
@ -468,9 +499,10 @@ constructor(
/**
* Convert the given selection to a list of [Song]s.
*
* @param selection The selection of [Music] to convert.
* @return A [Song] list containing the child items of any [MusicParent] instances in the list
* alongside the unchanged [Song]s or the original selection.
* alongside the unchanged [Song]s or the original selection.
*/
private fun selectionToSongs(selection: List<Music>): List<Song> {
return selection.flatMap {

View file

@ -32,6 +32,7 @@ import org.oxycblt.auxio.playback.state.RepeatMode
/**
* Provides raw access to the database storing the persisted playback state.
*
* @author Alexander Capehart
*/
@Database(
@ -42,12 +43,14 @@ import org.oxycblt.auxio.playback.state.RepeatMode
abstract class PersistenceDatabase : RoomDatabase() {
/**
* Get the current [PlaybackStateDao].
*
* @return A [PlaybackStateDao] providing control of the database's playback state tables.
*/
abstract fun playbackStateDao(): PlaybackStateDao
/**
* Get the current [QueueDao].
*
* @return A [QueueDao] providing control of the database's queue tables.
*/
abstract fun queueDao(): QueueDao
@ -63,12 +66,14 @@ abstract class PersistenceDatabase : RoomDatabase() {
/**
* Provides control of the persisted playback state table.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@Dao
interface PlaybackStateDao {
/**
* Get the previously persisted [PlaybackState].
*
* @return The previously persisted [PlaybackState], or null if one was not present.
*/
@Query("SELECT * FROM ${PlaybackState.TABLE_NAME} WHERE id = 0")
@ -79,6 +84,7 @@ interface PlaybackStateDao {
/**
* Insert a new [PlaybackState] into the database.
*
* @param state The [PlaybackState] to insert.
*/
@Insert(onConflict = OnConflictStrategy.ABORT) suspend fun insertState(state: PlaybackState)
@ -86,18 +92,21 @@ interface PlaybackStateDao {
/**
* Provides control of the persisted queue state tables.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@Dao
interface QueueDao {
/**
* Get the previously persisted queue heap.
*
* @return A list of persisted [QueueHeapItem]s wrapping each heap item.
*/
@Query("SELECT * FROM ${QueueHeapItem.TABLE_NAME}") suspend fun getHeap(): List<QueueHeapItem>
/**
* Get the previously persisted queue mapping.
*
* @return A list of persisted [QueueMappingItem]s wrapping each heap item.
*/
@Query("SELECT * FROM ${QueueMappingItem.TABLE_NAME}")
@ -111,12 +120,14 @@ interface QueueDao {
/**
* Insert new heap entries into the database.
*
* @param heap The list of wrapped [QueueHeapItem]s to insert.
*/
@Insert(onConflict = OnConflictStrategy.ABORT) suspend fun insertHeap(heap: List<QueueHeapItem>)
/**
* Insert new mapping entries into the database.
*
* @param mapping The list of wrapped [QueueMappingItem] to insert.
*/
@Insert(onConflict = OnConflictStrategy.ABORT)

View file

@ -27,17 +27,20 @@ import org.oxycblt.auxio.util.logE
/**
* Manages the persisted playback state in a structured manner.
*
* @author Alexander Capehart (OxygenCobalt)
*/
interface PersistenceRepository {
/**
* Read the previously persisted [PlaybackStateManager.SavedState].
*
* @param library The [Library] required to de-serialize the [PlaybackStateManager.SavedState].
*/
suspend fun readState(library: Library): PlaybackStateManager.SavedState?
/**
* Persist a new [PlaybackStateManager.SavedState].
*
* @param state The [PlaybackStateManager.SavedState] to persist.
*/
suspend fun saveState(state: PlaybackStateManager.SavedState?): Boolean

View file

@ -45,6 +45,7 @@ interface Queue {
val isShuffled: Boolean
/**
* Resolve this queue into a more conventional list of [Song]s.
*
* @return A list of [Song] corresponding to the current queue mapping.
*/
fun resolve(): List<Song>
@ -67,9 +68,10 @@ interface Queue {
/**
* An immutable representation of the queue state.
*
* @param heap The heap of [Song]s that are/were used in the queue. This can be modified with
* null values to represent [Song]s that were "lost" from the heap without having to change
* other values.
* null values to represent [Song]s that were "lost" from the heap without having to change
* other values.
* @param orderedMapping The mapping of the [heap] to an ordered queue.
* @param shuffledMapping The mapping of the [heap] to a shuffled queue.
* @param index The index of the currently playing [Song] at the time of serialization.
@ -85,9 +87,10 @@ interface Queue {
/**
* Remaps the [heap] of this instance based on the given mapping function and copies it into
* a new [SavedState].
*
* @param transform Code to remap the existing [Song] heap into a new [Song] heap. This
* **MUST** be the same size as the original heap. [Song] instances that could not be
* converted should be replaced with null in the new heap.
* **MUST** be the same size as the original heap. [Song] instances that could not be
* converted should be replaced with null in the new heap.
* @throws IllegalStateException If the invariant specified by [transform] is violated.
*/
inline fun remap(transform: (Song?) -> Song?) =
@ -121,6 +124,7 @@ class EditableQueue : Queue {
/**
* Go to a particular index in the queue.
*
* @param to The index of the [Song] to start playing, in the current queue mapping.
* @return true if the queue jumped to that position, false otherwise.
*/
@ -134,11 +138,12 @@ class EditableQueue : Queue {
/**
* Start a new queue configuration.
*
* @param play The [Song] to play, or null to start from a random position.
* @param queue The queue of [Song]s to play. Must contain [play]. This list will become the
* heap internally.
* heap internally.
* @param shuffled Whether to shuffle the queue or not. This changes the interpretation of
* [queue].
* [queue].
*/
fun start(play: Song?, queue: List<Song>, shuffled: Boolean) {
heap = queue.toMutableList()
@ -152,6 +157,7 @@ class EditableQueue : Queue {
/**
* Re-order the queue.
*
* @param shuffled Whether the queue should be shuffled or not.
*/
fun reorder(shuffled: Boolean) {
@ -185,10 +191,11 @@ class EditableQueue : Queue {
/**
* Add [Song]s to the top of the queue. Will start playback if nothing is playing.
*
* @param songs The [Song]s to add.
* @return [Queue.ChangeResult.MAPPING] if added to an existing queue, or
* [Queue.ChangeResult.SONG] if there was no prior playback and these enqueued [Song]s start new
* playback.
* [Queue.ChangeResult.SONG] if there was no prior playback and these enqueued [Song]s start
* new playback.
*/
fun playNext(songs: List<Song>): Queue.ChangeResult {
if (orderedMapping.isEmpty()) {
@ -214,10 +221,11 @@ class EditableQueue : Queue {
/**
* Add [Song]s to the end of the queue. Will start playback if nothing is playing.
*
* @param songs The [Song]s to add.
* @return [Queue.ChangeResult.MAPPING] if added to an existing queue, or
* [Queue.ChangeResult.SONG] if there was no prior playback and these enqueued [Song]s start new
* playback.
* [Queue.ChangeResult.SONG] if there was no prior playback and these enqueued [Song]s start
* new playback.
*/
fun addToQueue(songs: List<Song>): Queue.ChangeResult {
if (orderedMapping.isEmpty()) {
@ -238,11 +246,12 @@ class EditableQueue : Queue {
/**
* Move a [Song] at the given position to a new position.
*
* @param src The position of the [Song] to move.
* @param dst The destination position of the [Song].
* @return [Queue.ChangeResult.MAPPING] if the move occurred after the current index,
* [Queue.ChangeResult.INDEX] if the move occurred before or at the current index, requiring it
* to be mutated.
* [Queue.ChangeResult.INDEX] if the move occurred before or at the current index, requiring
* it to be mutated.
*/
fun move(src: Int, dst: Int): Queue.ChangeResult {
if (shuffledMapping.isNotEmpty()) {
@ -273,10 +282,11 @@ class EditableQueue : Queue {
/**
* Remove a [Song] at the given position.
*
* @param at The position of the [Song] to remove.
* @return [Queue.ChangeResult.MAPPING] if the removed [Song] was after the current index,
* [Queue.ChangeResult.INDEX] if the removed [Song] was before the current index, and
* [Queue.ChangeResult.SONG] if the currently playing [Song] was removed.
* [Queue.ChangeResult.INDEX] if the removed [Song] was before the current index, and
* [Queue.ChangeResult.SONG] if the currently playing [Song] was removed.
*/
fun remove(at: Int): Queue.ChangeResult {
if (shuffledMapping.isNotEmpty()) {
@ -311,6 +321,7 @@ class EditableQueue : Queue {
/**
* Convert the current state of this instance into a [Queue.SavedState].
*
* @return A new [Queue.SavedState] reflecting the exact state of the queue when called.
*/
fun toSavedState() =
@ -321,6 +332,7 @@ class EditableQueue : Queue {
/**
* Update this instance from the given [Queue.SavedState].
*
* @param savedState A [Queue.SavedState] with a valid queue representation.
*/
fun applySavedState(savedState: Queue.SavedState) {

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