diff --git a/README.md b/README.md index e95d5198f..682f0d40b 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ I primarily built Auxio for myself, but you can use it too, I guess. - Opinionated UX that prioritizes ease of use over edge cases - Customizable behavior - Advanced media indexer that prioritizes correct metadata +- SD Card-aware folder management - Reliable playback state persistence - Full ReplayGain support (On MP3, MP4, FLAC, OGG, and OPUS) - Edge-to-edge diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt index e1785b6a3..250eea1c4 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -176,7 +176,7 @@ class HomeFragment : ViewBindingFragment(), Toolbar.OnMenuI homeModel.updateCurrentSort( unlikelyToBeNull( homeModel - .getSortForDisplay(unlikelyToBeNull(homeModel.currentTab.value)) + .getSortForDisplay(homeModel.currentTab.value) .ascending(item.isChecked))) } else -> { @@ -185,7 +185,7 @@ class HomeFragment : ViewBindingFragment(), Toolbar.OnMenuI homeModel.updateCurrentSort( unlikelyToBeNull( homeModel - .getSortForDisplay(unlikelyToBeNull(homeModel.currentTab.value)) + .getSortForDisplay(homeModel.currentTab.value) .assignId(item.itemId))) } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt index f0a38e559..65a5dc0b7 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt @@ -33,7 +33,6 @@ import org.oxycblt.auxio.ui.SyncBackingData import org.oxycblt.auxio.util.formatDuration import org.oxycblt.auxio.util.launch import org.oxycblt.auxio.util.logEOrThrow -import org.oxycblt.auxio.util.unlikelyToBeNull /** * A [HomeListFragment] for showing a list of [Album]s. @@ -54,7 +53,7 @@ class AlbumListFragment : HomeListFragment() { } override fun getPopup(pos: Int): String? { - val album = unlikelyToBeNull(homeModel.albums.value)[pos] + val album = homeModel.albums.value[pos] // Change how we display the popup depending on the mode. return when (homeModel.getSortForDisplay(DisplayMode.SHOW_ALBUMS)) { diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt index b4a8a928f..a2acc0d74 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt @@ -33,7 +33,6 @@ import org.oxycblt.auxio.ui.SyncBackingData import org.oxycblt.auxio.util.formatDuration import org.oxycblt.auxio.util.launch import org.oxycblt.auxio.util.logEOrThrow -import org.oxycblt.auxio.util.unlikelyToBeNull /** * A [HomeListFragment] for showing a list of [Artist]s. @@ -54,7 +53,7 @@ class ArtistListFragment : HomeListFragment() { } override fun getPopup(pos: Int): String? { - val artist = unlikelyToBeNull(homeModel.artists.value)[pos] + val artist = homeModel.artists.value[pos] // Change how we display the popup depending on the mode. return when (homeModel.getSortForDisplay(DisplayMode.SHOW_ARTISTS)) { diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt index 8078d8b09..a07efd60a 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt @@ -33,7 +33,6 @@ import org.oxycblt.auxio.ui.SyncBackingData import org.oxycblt.auxio.util.formatDuration import org.oxycblt.auxio.util.launch import org.oxycblt.auxio.util.logEOrThrow -import org.oxycblt.auxio.util.unlikelyToBeNull /** * A [HomeListFragment] for showing a list of [Genre]s. @@ -54,7 +53,7 @@ class GenreListFragment : HomeListFragment() { } override fun getPopup(pos: Int): String? { - val genre = unlikelyToBeNull(homeModel.genres.value)[pos] + val genre = homeModel.genres.value[pos] // Change how we display the popup depending on the mode. return when (homeModel.getSortForDisplay(DisplayMode.SHOW_GENRES)) { diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt index 7da89ebd8..6f10c9c0a 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt @@ -32,7 +32,6 @@ import org.oxycblt.auxio.ui.SyncBackingData import org.oxycblt.auxio.util.formatDuration import org.oxycblt.auxio.util.launch import org.oxycblt.auxio.util.logEOrThrow -import org.oxycblt.auxio.util.unlikelyToBeNull /** * A [HomeListFragment] for showing a list of [Song]s. @@ -53,7 +52,7 @@ class SongListFragment : HomeListFragment() { } override fun getPopup(pos: Int): String? { - val song = unlikelyToBeNull(homeModel.songs.value)[pos] + val song = homeModel.songs.value[pos] // Change how we display the popup depending on the mode. // Note: We don't use the more correct individual artist name here, as sorts are largely diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt index 43cfe4da2..5fbe42849 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -35,7 +35,6 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.util.logE -import org.oxycblt.auxio.util.unlikelyToBeNull /** * The ViewModel that provides a UI frontend for [PlaybackStateManager]. @@ -194,15 +193,14 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback, MusicStore /** Remove a queue item using it's recyclerview adapter index. */ fun removeQueueDataItem(adapterIndex: Int) { - val index = - adapterIndex + (playbackManager.queue.size - unlikelyToBeNull(_nextUp.value).size) + val index = adapterIndex + (playbackManager.queue.size - _nextUp.value.size) if (index in playbackManager.queue.indices) { playbackManager.removeQueueItem(index) } } /** Move queue items using their recyclerview adapter indices. */ fun moveQueueDataItems(adapterFrom: Int, adapterTo: Int): Boolean { - val delta = (playbackManager.queue.size - unlikelyToBeNull(_nextUp.value).size) + val delta = (playbackManager.queue.size - _nextUp.value.size) val from = adapterFrom + delta val to = adapterTo + delta if (from in playbackManager.queue.indices && to in playbackManager.queue.indices) { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt index 872bc0b0f..02b8bcb62 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt @@ -33,7 +33,6 @@ import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.unlikelyToBeNull /** * The component managing the [MediaSessionCompat] instance. @@ -122,8 +121,7 @@ class MediaSessionComponent(private val context: Context, private val player: Pl } if (song.album.year != null) { - metadata.putString( - MediaMetadataCompat.METADATA_KEY_DATE, unlikelyToBeNull(song.album.year).toString()) + metadata.putString(MediaMetadataCompat.METADATA_KEY_DATE, song.album.year.toString()) } // Normally, android expects one to provide a URI to the metadata instance instead of diff --git a/app/src/main/java/org/oxycblt/auxio/ui/Sort.kt b/app/src/main/java/org/oxycblt/auxio/ui/Sort.kt index e6d9314cc..35bb14d8c 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/Sort.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/Sort.kt @@ -44,6 +44,8 @@ import org.oxycblt.auxio.util.logEOrThrow * @author OxygenCobalt * * TODO: Make comparators static instances + * + * TODO: Separate sort mode and ascending state */ sealed class Sort(open val isAscending: Boolean) { protected abstract val sortIntCode: Int diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index 8c89559b0..90d31b5b1 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -3,14 +3,17 @@ Auxio is a local music player with a fast, reliable UI/UX without the many usele Features - ExoPlayer based playback -- Customizable UI & Behavior +- Snappy UI derived from the latest Material Design guidelines +- Opinionated UX that prioritizes ease of use over edge cases +- Customizable behavior - Advanced media indexer that prioritizes correct metadata +- SD Card-aware folder management - Reliable playback state persistence -- ReplayGain support (On MP3, MP4, FLAC, OGG, and OPUS) -- Material You (Android 12+ only) +- Full ReplayGain support (On MP3, MP4, FLAC, OGG, and OPUS) - Edge-to-edge - Embedded covers support - Search Functionality -- Audio/Headset focus +- Headset autoplay +- Stylish widgets that automatically adapt to their size - Completely private and offline -- No rounded album covers (Unless you want them. Then you can.) \ No newline at end of file +- No rounded album covers (Unless you want them. Then you can.) diff --git a/info/ARCHITECTURE.md b/info/ARCHITECTURE.md index acddae5e7..a7a4113d7 100644 --- a/info/ARCHITECTURE.md +++ b/info/ARCHITECTURE.md @@ -5,12 +5,16 @@ This document is designed to provide an overview of Auxio's architecture and des Auxio has a couple of core systems or concepts that should be understood when working with the codebase. #### Package Structure -Auxio's package structure is strictly feature-oriented. For example, playback code is exclusively in the `playback` package, -and detail code is exclusively in the `detail` package. Sub-packages can be related to the code it contains, such as `detail.recycler` -for the detail UI adapters, or they can be related to a sub-feature, like `playback.queue` for the queue UI. +Auxio is deliberately structured in a way that I call "Anti-CLEAN Architecture". There is one gradle project, with sub-packages that +are strictly feature-oriented. For example, playback code is exclusively in the `playback` package, and detail code is exclusively in +the `detail` package. Sub-packages can be related to the code it contains, such as `detail.recycler` for the detail UI adapters, or +hey can be related to a sub-feature, like `playback.queue` for the queue UI. The outliers here are `.ui` and `.util`, which are generic utility or component packages. +Sticking to a single project reduces compile times, makes it easier to add new features, and simply makes Auxio's code easier to +reason about. + A full run-down of Auxio's current package structure as of the latest version is shown below. ``` @@ -23,7 +27,8 @@ org.oxycblt.auxio # Main UIs │ └──.tabs # Home tab customization ├──.image # Image loading components ├──.music # Music data and loading -│ └──.excluded # Excluded Directories UI + Systems +│ ├──.backend # System-side music loading +│ └──.dirs # Music Folders UI + Systems ├──.playback # Playback UI + Systems │ ├──.queue # Queue UI │ ├──.replaygain # ReplayGain System + UIs @@ -31,7 +36,7 @@ org.oxycblt.auxio # Main UIs │ └──.system # System-side playback [Services, ExoPlayer] ├──.search # Search UI ├──.settings # Settings UI + Systems -│ └──.pref # Int preference add-on +│ └──.ui # Preference extensions ├──.ui # Shared views and models │ └──.accent # Color Scheme UI + Systems ├──.util # Shared utilities @@ -50,9 +55,12 @@ should be added as a new `Fragment` implementation and added to one of the two n Fragments themselves are based off a super class called `ViewBindingFragment` that takes a view-binding and then leverages it within the fragment lifecycle. -- Create variables [Bindings, Adapters, etc] -- Set up the UI -- Set up ViewModel instances and LiveData observers +Generally: +- Most variables are kept as member variables, and cleared out when the view is destroyed. +- Observing data is done through the `Fragment.launch` extension, and always points to another function +in order to reduce possible memory leaks. +- When possible (and readable), `Fragment` implementations inherit any listener interfaces they need, +and simply clear them out when done. `findViewById` is to **only** be used when interfacing with non-Auxio views. Otherwise, view-binding should be used in all cases. Code that involves retrieving the binding should be isolated into its own function, with @@ -72,7 +80,7 @@ Auxio's codebase is mostly centered around 4 different types of code that commun - UIs: Fragments, RecyclerView items, and Activities are part of this class. All of them should have little data logic in them and should primarily focus on displaying information in their UIs. - ViewModels: These usually contain data and values that a UI can display, along with doing data processing. The data -often takes the form of `MutableLiveData` or `LiveData`, which can be observed. +often takes the form of `MutableStateFlow` or `StateFlow`, which can be observed. - Shared Objects: These are the fundamental building blocks of Auxio, and exist at the process level. These are usually retrieved using `getInstance` or a similar function. Shared Objects should be avoided in UIs, as their volatility can cause problems. Its better to use a ViewModel and their exposed data instead. @@ -172,9 +180,6 @@ with a data list similar to this: Each adapter instance also handles the highlighting of the currently playing item in the detail menu. -`DetailViewModel` acts as the holder for the currently displaying items, along with having the `navToItem` LiveData that coordinates menu/playback -navigation [Such as when a user presses "Go to artist"] - #### `.home` This package contains the components for the "home" UI in Auxio, or the UI that the user first sees when they open the app. @@ -192,17 +197,42 @@ This should not be used for UIs. - `BaseFetcher`, which is effectively Auxio's image loading routine. Most changes to image loading should be done there, and not it's sub-classes like `AlbumArtFetcher`. -#### `.music` -This package contains all `Music` implementations, the music loading implementation, and the excluded directory system. +This package also contains the two UI components used for all covers in Auxio: +- `StyledImageView`, which adds extensions for dynamically loading covers, handles rounded corners, and a stable icon style. +- `ImageGroup`, an extension of `StyledImageView` that all of the previous features, alongside a playing indicator and one custom view. -Key classes in this package include: -- `MusicStore`, which is the primary access point for music data. -- `Indexer`, which implements all of the `MediaStore` hacks to create a good metadata indexer for Auxio. +#### `.music` +This package contains all `Music` implementations, the music loading implementation, and the music folder system. This is the second +most complicated package in the app, as loading music in a sane way is horribly difficult. + +The major classes are: +- `MusicStore`, which is the container for a `Library` instance. Any code wanting to access the library should use this +- `Indexer`, which manages how music is loaded. This is only used by code that must reflect the music loading state. + +Internally, there are several other major systems: +- `IndexerService`, which does the indexer work in the background. +- `Indexer.Backend` implementations, which actually talk to the media database and load music. As it stands, there +are two classes of backend: + - Version-specific `MediaStoreBackend` implementations, which transform the (often insane) music data from Android + into a usable `Song`. + - `ExoPlayerBackend`, which mutates audio with extracted ID3v2 and Vorbis tags. This enables some extra features + and side-steps unfixable issues with `MediaStore` +- `StorageFramework`, which is a group of utilities that allows Auxio to be volume-aware and to work with both +extension-based and format-based mime types. + +The music loading process is roughly as follows: +1. Something triggers `IndexerService` to start indexing, either by the UI or by the service itself starting. +2. `Indexer` picks an appropriate `Backend`, and begins loading music. `Indexer` may periodically update it's state +during this time with the current progress. +3. In the case that `IndexerService` is killed, `Indexer` falls back to a previous state (or null if there isn't one) +4. If the music loading process completes, `Indexer` will push a `Response`. `IndexerService` will read this, and in +the case that the new `Library` differs, it will push it to `MusicStore` +5. `MusicStore` updates any `Callback` instances with the new `Library`. #### `.playback` This module not only contains the playback system described above, but also multiple other components: -- `queue` contains the Queue UI and it's fancy item UIs. +- `queue` contains the Queue UI and it's fancy item system. - `replaygain` contains the ReplayGain implementation and the UIs related to it. Auxio's ReplayGain implementation is somewhat different compared to other apps, as it leverages ExoPlayer's metadata and audio processing systems to not only parse ReplayGain tags, but also allow volume amplification above 100%. @@ -228,14 +258,13 @@ a normal choice preference to be backed by the integer representations that Auxi #### `.ui` Shared views and view configuration models. This contains: -- Customized views such as `EdgeAppBarLayout`, `StyledImageView`, and others, which provide extra styling and behavior -not provided by default. +- Important `Fragment` superclasses like `ViewBindingFragment` and `MenuFragment` +- Customized views such as `EdgeAppBarLayout`, and others, which just fix existing shortcomings with the views. - Configuration models like `DisplayMode` and `Sort`, which are used in many places but aren't tied to a specific feature. -- `newMenu` and `ActionMenu`, which automates menu creation for most data types. - The `RecyclerView` adapter framework described previously. -- The view-binding super classes described previously. - `BottomSheetLayout`, a highly important layout that underpins Auxio's UI flow. - Standard `ViewHolder` implementations that can be used for common datatypes. +- `NavigationViewModel`, which acts as an interface to control navigation to a particular item and navigation within `MainFragment` #### `.util` Shared utilities. This is primarily for QoL when developing Auxio. Documentation is provided on each method.