info: update ARCHITECTURE

Update the ARCHITECTURE document to reflect the architectural changes
made in 2.5.0.
This commit is contained in:
OxygenCobalt 2022-07-13 09:21:02 -06:00
parent ce7dd48f02
commit 08423d5e93
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
7 changed files with 240 additions and 183 deletions

View file

@ -75,12 +75,12 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
private val playbackModel: PlaybackViewModel by androidActivityViewModels() private val playbackModel: PlaybackViewModel by androidActivityViewModels()
private val navModel: NavigationViewModel by activityViewModels() private val navModel: NavigationViewModel by activityViewModels()
private val homeModel: HomeViewModel by androidActivityViewModels() private val homeModel: HomeViewModel by androidActivityViewModels()
private val indexerModel: MusicViewModel by activityViewModels() private val musicModel: MusicViewModel by activityViewModels()
// lifecycleObject builds this in the creation step, so doing this is okay. // lifecycleObject builds this in the creation step, so doing this is okay.
private val storagePermissionLauncher: ActivityResultLauncher<String> by lifecycleObject { private val storagePermissionLauncher: ActivityResultLauncher<String> by lifecycleObject {
registerForActivityResult(ActivityResultContracts.RequestPermission()) { registerForActivityResult(ActivityResultContracts.RequestPermission()) {
indexerModel.reindex() musicModel.reindex()
} }
} }
@ -142,8 +142,8 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
collect(homeModel.recreateTabs, ::handleRecreateTabs) collect(homeModel.recreateTabs, ::handleRecreateTabs)
collectImmediately(homeModel.currentTab, ::updateCurrentTab) collectImmediately(homeModel.currentTab, ::updateCurrentTab)
collectImmediately(indexerModel.libraryExists, homeModel.isFastScrolling, ::updateFab) collectImmediately(musicModel.libraryExists, homeModel.isFastScrolling, ::updateFab)
collectImmediately(indexerModel.indexerState, ::handleIndexerState) collectImmediately(musicModel.indexerState, ::handleIndexerState)
collect(navModel.exploreNavigationItem, ::handleNavigation) collect(navModel.exploreNavigationItem, ::handleNavigation)
} }
@ -283,7 +283,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
binding.homeIndexingAction.apply { binding.homeIndexingAction.apply {
visibility = View.VISIBLE visibility = View.VISIBLE
text = getString(R.string.lbl_retry) text = getString(R.string.lbl_retry)
setOnClickListener { indexerModel.reindex() } setOnClickListener { musicModel.reindex() }
} }
} }
is Indexer.Response.NoMusic -> { is Indexer.Response.NoMusic -> {
@ -292,7 +292,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
binding.homeIndexingAction.apply { binding.homeIndexingAction.apply {
visibility = View.VISIBLE visibility = View.VISIBLE
text = getString(R.string.lbl_retry) text = getString(R.string.lbl_retry)
setOnClickListener { indexerModel.reindex() } setOnClickListener { musicModel.reindex() }
} }
} }
is Indexer.Response.NoPerms -> { is Indexer.Response.NoPerms -> {

View file

@ -46,6 +46,8 @@ import org.oxycblt.auxio.util.logD
* You could probably do the same using WorkManager and the GooberQueue library or whatever, but the * You could probably do the same using WorkManager and the GooberQueue library or whatever, but the
* boilerplate you skip is not worth the insanity of androidx. * boilerplate you skip is not worth the insanity of androidx.
* *
* TODO: Add a wake-lock to the music loading process
*
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class IndexerService : Service(), Indexer.Controller, Settings.Callback { class IndexerService : Service(), Indexer.Controller, Settings.Callback {

View file

@ -29,13 +29,12 @@ enum class RepeatMode {
TRACK; TRACK;
/** Increment the mode, e.g from [NONE] to [ALL] */ /** Increment the mode, e.g from [NONE] to [ALL] */
fun increment(): RepeatMode { fun increment() =
return when (this) { when (this) {
NONE -> ALL NONE -> ALL
ALL -> TRACK ALL -> TRACK
TRACK -> NONE TRACK -> NONE
} }
}
/** The integer code representing this particular mode. */ /** The integer code representing this particular mode. */
val intCode: Int val intCode: Int
@ -48,13 +47,12 @@ enum class RepeatMode {
companion object { companion object {
/** Convert an int [code] into an instance, or null if it isn't valid. */ /** Convert an int [code] into an instance, or null if it isn't valid. */
fun fromIntCode(code: Int): RepeatMode? { fun fromIntCode(code: Int) =
return when (code) { when (code) {
IntegerTable.REPEAT_MODE_NONE -> NONE IntegerTable.REPEAT_MODE_NONE -> NONE
IntegerTable.REPEAT_MODE_ALL -> ALL IntegerTable.REPEAT_MODE_ALL -> ALL
IntegerTable.REPEAT_MODE_TRACK -> TRACK IntegerTable.REPEAT_MODE_TRACK -> TRACK
else -> null else -> null
} }
}
} }
} }

View file

@ -55,7 +55,7 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
@Suppress("UNUSED") @Suppress("UNUSED")
class SettingsListFragment : PreferenceFragmentCompat() { class SettingsListFragment : PreferenceFragmentCompat() {
private val playbackModel: PlaybackViewModel by androidActivityViewModels() private val playbackModel: PlaybackViewModel by androidActivityViewModels()
private val indexerModel: MusicViewModel by activityViewModels() private val musicModel: MusicViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -129,7 +129,7 @@ class SettingsListFragment : PreferenceFragmentCompat() {
} }
} }
getString(R.string.set_key_reindex) -> { getString(R.string.set_key_reindex) -> {
indexerModel.reindex() musicModel.reindex()
} }
else -> return super.onPreferenceTreeClick(preference) else -> return super.onPreferenceTreeClick(preference)
} }

View file

@ -2,6 +2,25 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<!-- ANDROID COMPONENT-SPECIFIC STYLES.--> <!-- ANDROID COMPONENT-SPECIFIC STYLES.-->
<!--
Hacks around the incorrectly-sized navigation icon in the Toolbar, changing it from
56dp to 48dp.
-->
<style name="Widget.Auxio.Toolbar.Navigation" parent="Widget.AppCompat.Toolbar.Button.Navigation">
<item name="android:minWidth">@dimen/size_btn</item>
</style>
<!--
Hacks around the old overflow button that was deliberately downsized to 36dp
(presumably for compat with older devices)
-->
<style name="Widget.Auxio.Button.Overflow" parent="Widget.AppCompat.ActionButton.Overflow">
<item name="android:minWidth">@dimen/size_btn</item>
<item name="android:minHeight">@dimen/size_btn</item>
<item name="android:paddingStart">0dp</item>
<item name="android:paddingEnd">0dp</item>
</style>
<style name="Theme.Auxio.Dialog" parent="ThemeOverlay.Material3.MaterialAlertDialog"> <style name="Theme.Auxio.Dialog" parent="ThemeOverlay.Material3.MaterialAlertDialog">
<item name="android:checkedTextViewStyle">@style/Widget.Auxio.Dialog.CheckedTextView</item> <item name="android:checkedTextViewStyle">@style/Widget.Auxio.Dialog.CheckedTextView</item>
</style> </style>
@ -20,6 +39,7 @@
<item name="android:scrollIndicators" tools:ignore="NewApi">top|bottom</item> <item name="android:scrollIndicators" tools:ignore="NewApi">top|bottom</item>
</style> </style>
<!-- Widget TextView that mimics the main Auxio Primary/Secondary TextViews. --> <!-- Widget TextView that mimics the main Auxio Primary/Secondary TextViews. -->
<style name="Widget.Auxio.TextView.AppWidget" parent="Widget.Auxio.TextView.Base"> <style name="Widget.Auxio.TextView.AppWidget" parent="Widget.Auxio.TextView.Base">
<item name="android:singleLine">true</item> <item name="android:singleLine">true</item>
@ -57,23 +77,4 @@
<item name="android:padding">@dimen/spacing_medium</item> <item name="android:padding">@dimen/spacing_medium</item>
<item name="android:orientation">vertical</item> <item name="android:orientation">vertical</item>
</style> </style>
<!--
Hacks around the incorrectly-sized navigation icon in the Toolbar, changing it from
56dp to 48dp.
-->
<style name="Widget.Auxio.Toolbar.Navigation" parent="Widget.AppCompat.Toolbar.Button.Navigation">
<item name="android:minWidth">@dimen/size_btn</item>
</style>
<!--
Hacks around the old overflow button that was deliberately downsized to 36dp
(presumably for compat with older devices)
-->
<style name="Widget.Auxio.Button.Overflow" parent="Widget.AppCompat.ActionButton.Overflow">
<item name="android:minWidth">@dimen/size_btn</item>
<item name="android:minHeight">@dimen/size_btn</item>
<item name="android:paddingStart">0dp</item>
<item name="android:paddingEnd">0dp</item>
</style>
</resources> </resources>

View file

@ -58,7 +58,6 @@
<item name="textAppearanceBodySmall">@style/TextAppearance.Auxio.BodySmall</item> <item name="textAppearanceBodySmall">@style/TextAppearance.Auxio.BodySmall</item>
</style> </style>
<!-- <!--
Theming widgets is technically possible below Android 12, but I *really* don't care enough Theming widgets is technically possible below Android 12, but I *really* don't care enough
to bother with it. to bother with it.

View file

@ -1,20 +1,24 @@
# Architecture # Architecture
This document is designed to provide an overview of Auxio's architecture and design decisions. It will be updated as Auxio changes, This document is designed to provide an overview of Auxio's architecture and design decisions. It
however it may not completely line up as parts of the codebase will change rapidly at times. will be updated as Auxio changes, however it may not completely line up as parts of the codebase
will change rapidly at times.
## Core Facets ## Core Facets
Auxio has a couple of core systems or concepts that should be understood when working with the codebase. Auxio has a couple of core systems or concepts that should be understood when working with the
codebase.
#### Package Structure #### Package Structure
Auxio is deliberately structured in a way that I call "Anti-CLEAN Architecture". There is one gradle project, with sub-packages that Auxio is deliberately structured in a way that I call "Anti-CLEAN Architecture". There is one
are strictly feature-oriented. For example, playback code is exclusively in the `playback` package, and detail code is exclusively in gradle project, with sub-packages that are strictly feature-oriented. For example, playback code is
the `detail` package. Sub-packages can be related to the code it contains, such as `detail.recycler` for the detail UI adapters, or exclusively in the `playback` package, and detail code is exclusively in the `detail` package.
hey can be related to a sub-feature, like `playback.queue` for the queue UI. 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. 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 Sticking to a single project reduces compile times, makes it easier to add new features, and simply
reason about. makes Auxio's code better to reason about since you don't have to jump across disparately related
gradle projects.
A full run-down of Auxio's current package structure as of the latest version is shown below. A full run-down of Auxio's current package structure as of the latest version is shown below.
@ -28,8 +32,8 @@ org.oxycblt.auxio # Main UIs
│ └──.tabs # Home tab customization │ └──.tabs # Home tab customization
├──.image # Image loading components ├──.image # Image loading components
├──.music # Music data and loading ├──.music # Music data and loading
│ ├──.backend # System-side music loading │ ├──.dirs # Music folders UI + systems
│ └──.dirs # Music Folders UI + Systems │ └──.system # Internal music loading
├──.playback # Playback UI + Systems ├──.playback # Playback UI + Systems
│ ├──.queue # Queue UI │ ├──.queue # Queue UI
│ ├──.replaygain # ReplayGain System + UIs │ ├──.replaygain # ReplayGain System + UIs
@ -38,8 +42,12 @@ org.oxycblt.auxio # Main UIs
├──.search # Search UI ├──.search # Search UI
├──.settings # Settings UI + Systems ├──.settings # Settings UI + Systems
│ └──.ui # Preference extensions │ └──.ui # Preference extensions
├──.ui # Shared views and models ├──.ui # Custom android components
│ └──.accent # Color Scheme UI + Systems │ ├──.accent # Color Scheme UI + Systems
│ ├──.coordinator # CoordinatorLayout components
│ ├──.fragment # Fragment extensions
│ ├──.recycler # RecyclerView extensions
│ └──.system # System-side components
├──.util # Shared utilities ├──.util # Shared utilities
└──.widgets # AppWidgets └──.widgets # AppWidgets
``` ```
@ -47,49 +55,56 @@ org.oxycblt.auxio # Main UIs
Each package is gone over in more detail later on. Each package is gone over in more detail later on.
#### UI Structure #### UI Structure
Auxio only has one activity, `MainActivity`. Do not try to add more activities to the codebase. Instead, a new UI Auxio only has one activity, `MainActivity`. Do not try to add more activities to the codebase.
should be added as a new `Fragment` implementation and added to one of the two navigation graphs: Instead, a new UI should be added as a new `Fragment` implementation and added to one of the two
navigation graphs:
- `nav_main`: Navigation *from* `MainFragment` - `nav_main`: Navigation *from* `MainFragment`
- `nav_explore`: Navigation *in* `MainFragment` - `nav_explore`: Navigation *in* `MainFragment`
Fragments themselves are based off a super class called `ViewBindingFragment` that takes a view-binding and then Fragments themselves are based off several extensions that enable extra functionality, such as
leverages it within the fragment lifecycle. ViewBinding or safe `PopupMenu` creation.
Generally: Generally:
- Most variables are kept as member variables, and cleared out when the view is destroyed. - 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 - Observing data is done through the `Fragment.launch` extension, and always points to another
in order to reduce possible memory leaks. function in order to reduce possible memory leaks.
- When possible (and readable), `Fragment` implementations inherit any listener interfaces they need, - When possible (and readable), `Fragment` implementations inherit any listener interfaces they
and simply clear them out when done. 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 `findViewById` is to **only** be used when interfacing with non-Auxio views. Otherwise, view-binding
used in all cases. Code that involves retrieving the binding should be isolated into its own function, with should be used in all cases. Code that involves retrieving the binding should be isolated into its
the binding being obtained by calling `requireBinding`. own function, with the binding being obtained by calling `requireBinding`.
At times it may be more appropriate to use a `View` instead of a full blown fragment. This is okay as long as At times it may be more appropriate to use a `View` instead of a fragment. This is okay as long as
view-binding is still used. view-binding is still used.
Auxio uses `RecyclerView` for all list information. Due to the complexities of Auxio, the way one defines an Auxio uses `RecyclerView` for all list information. Due to the complexities of Auxio, the way one
adapter differs quite heavily from the normal library. Generally, start with `MonoAdapter` for a list with one defines an adapter differs quite heavily from the normal library. Generally, start with
type of data and `MultiAdapter` for lists with many types of data, then follow the documentation to see how `MonoAdapter` for a list with one type of data and `MultiAdapter` for lists with many types of data,
to fully implement the class. then follow the documentation to see how to fully implement the class.
#### Object communication #### Object communication
Auxio's codebase is mostly centered around 4 different types of code that communicates with each-other. Auxio's codebase is mostly centered around 4 different types of code that communicates with
each-other.
- UIs: Fragments, RecyclerView items, and Activities are part of this class. All of them should have little data logic - UIs: Fragments, RecyclerView items, and Activities are part of this class. All of them should have
in them and should primarily focus on displaying information in their UIs. 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 - ViewModels: These usually contain data and values that a UI can display, along with doing data
often takes the form of `MutableStateFlow` or `StateFlow`, which can be observed. processing. The data ten 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 - Services: Auxio's services are intended to perform some kind of long-running task, while driving
retrieved using `getInstance` or a similar function. Shared Objects should be avoided in UIs, as their volatility can the Shared Object related to the work that they are doing.
cause problems. Its better to use a ViewModel and their exposed data instead. - Shared Objects: These are the fundamental building blocks of Auxio, and exist at the process
- Utilities: These are largely found in the `.util` package, taking the form of standalone or extension functions level. These are usually retrieved using `getInstance` or a similar function. Usually, methods in
that can be used anywhere. these are also Synchronized to prevent issues with global/mutable/concurrent state. Shared Objects
should be avoided in UIs, as their volatility can cause problems. Its better to use a ViewModel
and their exposed data instead.
- Utilities: These are largely found in the `.util` package, taking the form of standalone or
extension functions that can be used anywhere.
Ideally, UIs should only be talking to ViewModels, ViewModels should only be talking to the Shared Objects, and Shared Objects Ideally, UIs should only be talking to ViewModels, ViewModels and Services should only be talking
should only be talking to other shared objects. All objects can use the utility functions where appropriate. to the Shared Objects, and Shared Objects should only be talking to other shared objects. All
objects can use the utility functions where appropriate.
#### Data objects #### Data objects
Auxio represents data in multiple ways. Auxio represents data in multiple ways.
@ -97,179 +112,218 @@ Auxio represents data in multiple ways.
`Item` is the base class for most music and UI data in Auxio, with a single ID field meant to mark it as unique. `Item` is the base class for most music and UI data in Auxio, with a single ID field meant to mark it as unique.
It has the following implementations: It has the following implementations:
- `Music` is a `Item` that represents music. It adds a `name` field that represents the raw name of the music (from `MediaStore`), - `Music` is a `Item` that represents music. It adds a `name` field that represents the raw name
and a `resolveName` method meant to resolve the name in context of the UI. of the music (from `MediaStore`), and a `resolveName` method meant to resolve the name in context
of the UI.
- `MusicParent` is a type of `Music` that contains children. - `MusicParent` is a type of `Music` that contains children.
- `Header` corresponds to a simple header with a title and no interaction functionality. There are also the detail-specific - `Header` corresponds to a simple header with a title and no interaction functionality. There are
`DiscHeader` and `SortHeader`, however these are largely unrelated to `Header`. also the `detail`-specific `DiscHeader` and `SortHeader`, however these are largely unrelated to
`Header`.
Other data types represent a specific UI configuration or state: Other data types represent a specific UI configuration or state:
- Sealed classes like `Sort` contain an ascending state that can be modified immutably. - Data structures like `Sort` contain an ascending state that can be modified immutably.
- Enums like `DisplayMode` and `RepeatMode` only contain static data, such as a string resource. - Enums like `DisplayMode` and `RepeatMode` only contain static data, such as a string resource.
Things to keep in mind while working with music data: Things to keep in mind while working with music data:
- `id` is not derived from the `MediaStore` ID of the music data. It is actually a hash of the unique fields of the music data. - `id` is not derived from the `MediaStore` ID of the music data. It is actually a hash of the
Attempting to use it as a `MediaStore` ID will result in errors. unique, non-subjective fields of the music data. Attempting to use it as a `MediaStore` ID will
- Any field or method beginning with `internal` is off-limits. These fields are meant for use within `MusicLoader` and generally result in errors.
provide poor UX to the user. The only reason they are public is to make the loading process not have to rely on separate "Raw" - Any field or method beginning with `_` is off-limits. These fields are meant for use within
objects. `MusicLoader` and generally provide poor UX to the user. The only reason they are public is to
simplify the loading process, as there is no reason to remove internal fields given that it won't
free memory.
- `rawName` is used when doing internal work, such as saving music data or diffing items - `rawName` is used when doing internal work, such as saving music data or diffing items
- `sortName` is used in the fast scroller indicators and sorting. Avoid it wherever else. - `sortName` is used in the fast scroller indicators and sorting. Avoid it wherever else.
- `resolveName()` should be used when displaying any kind of music data to the user. - `resolveName()` should be used when displaying any kind of music data to the user.
- For songs, `individualArtistRawName` and `resolveIndividualArtistName` should always be used when displaying the artist of - For songs, `individualArtistRawName` and `resolveIndividualArtistName` should always be used when
a song, as it will always show collaborator information first before defaulting to the album artist. displaying the artist of a song, as it will always show collaborator information first before
defaulting to the album artist.
#### Music Access #### Music Access
All music on a system is asynchronously loaded into the shared object `MusicStore`. More specifically, it is accessible within Whereas other apps load music from `MediaStore`, Auxio does not do that, as it prevents any kind of
the `Library` construct. By the nature of music loading, **`Library` may not be available at all times.** reasonable metadata functionality (ex. Album Artists). Instead, Auxio loads all music into an
in-memory relational construct called `Library`.
- ViewModels should try to await or gracefully exit the called method if `Library` is not available `Library` is obtained from `MusicStore`, however since Auxio's music loading is asynchronous and
- In the case that a ViewModel needs a `Library` instance to function, it can be asserted with `requireNotNull`. This should be done sparingly. can occur several times over the runtime of the app, it is highly recommended for code to rely
- Other shared objects that rely on `MusicStore` [like `PlaybackStateManager`] will no-op if music is not available. on `MusicStore.Callback`. In the case that a piece of code would only be ran if there was a library,
`requireNotNull` can be used, however this is highly risky.
If the loading status needs to be shown in a UI, `MusicViewModel` can be used to observe the current music loader response. Since Shared Objects should not be attached to the Callback system of another Shared Object, it
is highly recommended to check for the existence of a `Library` if required, and no-op if it was
not available.
Monitoring the loading progress (Internally called the indexing state) should be done sparingly,
and is best done with `MusicViewModel`.
#### Playback System #### Playback System
Auxio's playback system is somewhat unorthodox, as it avoids much of the android-provided APIs in favor of a more controllable and sensible system. The android/androidx media state APIs are terrible, and are often the cause of the strange queue
behavior and jank you see in other apps. So, Auxio does not use it, instead implementing it's own
playback engine that is more controllable and sensible and simply mirroring it to the android APIs.
The diagram below highlights the overall structure and connections: The diagram below highlights the overall structure and connections:
``` ```
┌──────────────────── PlaybackService ────────────────┐ ┌──────────────────── PlaybackService
│ │ │ │
PlaybackStateManager [Communicates with] │ PlaybackStateManager [Communicates with] │
│ │ [Contains] │ [Communicates with] │ │ [Controls]
│ │ │ │
│ ├ WidgetComponent │ ├ WidgetComponent
│ ├ NotificationComponent │ ├ NotificationComponent
│ ├ MediaSessionComponent │ ├ MediaSessionComponent
│ └ Player │ └ Player
└──────────────────── PlaybackViewModel ───────────────────── UIs └──────────────────── PlaybackViewModel ───────────────────── UIs
[Communicates with] [Controls]
``` ```
`PlaybackStateManager` is the shared object that contains the master copy of the playback state, doing all operations on it. This object should `PlaybackStateManager` is the shared object that contains the master copy of the playback state,
***NEVER*** be used in a UI, as it does not sanitize input and can cause major problems if a Volatile UI interacts with it. It's callback system doing all operations on it. This object should ***NEVER*** be used in a UI, as it does not sanitize
is also prone to memory leaks if not cleared when done. `PlaybackViewModel` should be used instead, as it exposes stable data and safe functions input and can cause major problems if a Volatile UI interacts with it. It's callback system is also
that UIs can use to interact with the playback state. prone to memory leaks if not cleared when done.
`PlaybackService`'s job is to use the playback state to manage the ExoPlayer instance, the notification, the media session, the widget, and For UIs, `PlaybackViewModel` exists instead. It provides safe, observable data and abstractions to
also modify the state depending on system events, such as when a button is pressed on a headset. It should **never** be bound to, mostly because make managing the playback state simple from the UI.
`PlaybackService`'s job is to use the playback state to manage the ExoPlayer instance, the
notification, the media session, the widget, and also modify the state depending on system events,
such as when a button is pressed on a headset. It should **never** be bound to, mostly because
there is no need given that `PlaybackViewModel` exposes the same data in a much safer fashion. there is no need given that `PlaybackViewModel` exposes the same data in a much safer fashion.
#### Data Integers #### Data Integers
Integer representations of data/UI elements are used heavily in Auxio, primarily for efficiency. To prevent any strange bugs, all integer Integer representations of data/UI elements are used heavily in Auxio, primarily for efficiency. To
representations must be unique. To see a table of all current integers, see the `C` class within the project. prevent any strange bugs, all integer representations must be unique. To see a table of all current
integers, see the `IntegerTable` class within the project.
Some datatypes [like `Tab` and `Sort`] have even more fine-grained integer representations for other data. More information can be found in Some datatypes [like `Tab` and `Sort`] have even more fine-grained integer representations for other
the documentation for those datatypes. data. More information can be found in the documentation for those datatypes.
## Package-by-package rundown ## Package-by-package rundown
#### `org.oxycblt.auxio` #### `org.oxycblt.auxio`
This is the root package and contains the application instance and the landing UIs. This should be kept sparse with most other code being placed This is the root package and contains the application instance and the landing UIs. This should be
into a package. kept sparse with most other code being placed into a package.
#### `.detail` #### `.detail`
Contains all the detail UIs for some data types in Auxio. All detail user interfaces share the same base layout (A Single RecyclerView) and Contains all the detail UIs for some data types in Auxio. All detail user interfaces share the same
only change the adapter/data being used. The adapters display both the header with information and the child items of the item itself, usually base layout (A Single RecyclerView) and only change the adapter/data being used. The adapters
with a data list similar to this: display both the header with information and the child items of the item itself, usually with a data
list similar to this:
`Item being displayed | Header Item | Child Item | Child Item | Child Item...` `Item being displayed | Header Item | Child Item | Child Item | Child Item...`
Each adapter instance also handles the highlighting of the currently playing item in the detail menu. Note that the actual dataset used is more complex once sorting and disc numbers is taken into
account. Item highlighting and certain shared ViewHolders are already managed by the `DetailAdapter`
super-class, which should be implemented by all adapters in the detail UI.
#### `.home` #### `.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. This package contains the components for the "home" UI in Auxio, or the UI that the user first sees
when they open the app.
- The base package contains the top-level components that manage the FloatingActionButton, AppBar, and ViewPager instances. - The base package contains the top-level components that manage the FloatingActionButton, AppBar,
and ViewPager instances.
- The `fastscroll` package contains the fast scroll component used in each list of music - The `fastscroll` package contains the fast scroll component used in each list of music
- The `list` package contains the individual fragments for each list of music. These are all placed in the top-level ViewPager instance. - The `list` package contains the individual fragments for each list of music. These are all placed
- The `tabs` package contains the data representation of an individual library tab and the UIs for editing them. in the top-level ViewPager instance.
- The `tabs` package contains the data representation of an individual library tab and the UIs for
editing them.
#### `.image` #### `.image`
[Coil](https://github.com/coil-kt/coil) is the image loader used by Auxio. This package contains the components Auxio leverages to load images [Coil](https://github.com/coil-kt/coil) is the image loader used by Auxio. This package contains the
in a stable manner. Usually, you do not need to import this package elsewhere, but there are some important components: components Auxio leverages to load images in a stable manner. Usually, you do not need to import
this package elsewhere, but there are some important components:
- `BitmapProvider`, which allows external components (Such as in PlaybackService) to load a `Bitmap` in a way not prone to race conditions. - `BitmapProvider`, which allows external components (Such as in PlaybackService) to load a `Bitmap`
This should not be used for UIs. in a way not prone to race conditions. 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 - `BaseFetcher`, which is effectively Auxio's image loading routine. Most changes to image loading
sub-classes like `AlbumArtFetcher`. should be done there, and not it's sub-classes like `AlbumArtFetcher`.
This package also contains the two UI components used for all covers in Auxio: 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. - `StyledImageView`, which adds extensions for dynamically loading covers, handles rounded corners,
- `ImageGroup`, an extension of `StyledImageView` that all of the previous features, alongside a playing indicator and one custom view. and a stable icon style.
- `ImageGroup`, an extension of `StyledImageView` that all of the previous features, alongside a
playing indicator and one custom view.
#### `.music` #### `.music`
This package contains all `Music` implementations, the music loading implementation, and the music folder system. This is the second This package contains all `Music` implementations, the music loading implementation, and the music
most complicated package in the app, as loading music in a sane way is horribly difficult. folder system. This is the second most complicated package in the app, as loading music in a sane
way is horribly difficult.
Unlike other apps, Auxio does not load music from `MediaStore` as it is shown in the UI. That is dumb and stupid and prevents
any advanced features like Album Artists. Instead, we have a single loading process that constructs an entire in-memory music
library, which does increase memory usage, but allows for very high-quality metadata.
The major classes are: The major classes are:
- `MusicStore`, which is the container for a `Library` instance. Any code wanting to access the library should use this. - `MusicStore`, which is the container for a `Library` instance. Any code wanting to access the
- `Indexer`, which manages how music is loaded. This is only used by code that must reflect the music loading state. library should use this.
- `Indexer`, which manages how music is loaded. This is only used by code that must manage or
mirror the music loading state.
Internally, there are several other major systems: Internally, there are several other major systems:
- `IndexerService`, which does the indexer work in the background. - `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 - `Indexer.Backend` implementations, which actually talk to the media database and load music.
are two classes of backend: As it stands, there are two classes of backend:
- Version-specific `MediaStoreBackend` implementations, which transform the (often insane) music data from Android - Version-specific `MediaStoreBackend` implementations, which transform the (often insane)
into a usable `Song`. music data from Android into a usable `Song`.
- `ExoPlayerBackend`, which mutates audio with extracted ID3v2 and Vorbis tags. This enables some extra features - `ExoPlayerBackend`, which mutates audio with extracted ID3v2 and Vorbis tags. This enables
and side-steps unfixable issues with `MediaStore` 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 - `StorageFramework`, which is a group of utilities that allows Auxio to be volume-aware and to
extension-based and format-based mime types. work with both extension-based and format-based mime types.
The music loading process is roughly as follows: 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. 1. Something triggers `IndexerService` to start indexing, either by the UI or by the service itself
2. `Indexer` picks an appropriate `Backend`, and begins loading music. `Indexer` may periodically update it's state starting.
during this time with the current progress. 2. `Indexer` picks an appropriate `Backend`, and begins loading music. `Indexer` may periodically
3. In the case that `IndexerService` is killed, `Indexer` falls back to a previous state (or null if there isn't one) update it's state during this time with the current progress.
4. If the music loading process completes, `Indexer` will push a `Response`. `IndexerService` will read this, and in 3. In the case that `IndexerService` is killed, `Indexer` falls back to a previous state (or null
the case that the new `Library` differs, it will push it to `MusicStore` 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`. 5. `MusicStore` updates any `Callback` instances with the new `Library`.
#### `.playback` #### `.playback`
This module not only contains the playback system described above, but also multiple other components: 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 system. - `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 - `replaygain` contains the ReplayGain implementation and the UIs related to it. Auxio's ReplayGain
somewhat different compared to other apps, as it leverages ExoPlayer's metadata and audio processing systems to not only implementation is somewhat different compared to other apps, as it leverages ExoPlayer's metadata
parse ReplayGain tags, but also allow volume amplification above 100%. and audio processing systems to not only parse ReplayGain tags, but also allow volume amplification
above 100%.
- `state` contains the core playback state and persistence system. - `state` contains the core playback state and persistence system.
- `system` contains the system-facing playback system, i.e `PlaybackService` - `system` contains the system-facing playback system, i.e `PlaybackService`.
The base package contains the user-facing UIs representing the playback state, specifically the playback bar and the The base package contains the user-facing UIs representing the playback state, specifically the
playback panel that it expands into. Note that while the playback UI does rely on `BottomSheetLayout`, the layout is playback bar and the playback panel that it expands into. Note that while the playback UI does rely
designed to be at least somewhat re-usable, so it is in the generic `.ui` class. on `BottomSheetLayout`, the layout is designed to be at least somewhat re-usable, so it is in the
generic `.ui` class.
#### `.search` #### `.search`
Package for Auxio's search functionality, `SearchViewHolder` handles the data results and filtering while `SearchFragment`/`SearchAdapter` handles the Package for Auxio's search functionality, `SearchViewHolder` handles the data results and filtering
display of the results and user input. while `SearchFragment`/`SearchAdapter` handles the display of the results and user input.
#### `.settings` #### `.settings`
The settings system is primarily based off of `SettingsManager`, a wrapper around `SharedPreferences`. This allows settings to be read/written in a The settings system is primarily based off of `Settings`, a type-safe wrapper around
much simpler/safer manner and without a context being needed. The Settings UI is largely contained in `SettingsListFragment`, which is a standard `SharedPreferences`. `Settings` is not a shared object, but actually a utility instantiated
`PreferenceFragment` implementation wrapped by the more general `SettingsFragment`. with a `Context`. Thus, the way to leverage them differs depending on if the code is in a UI,
ViewModel, Shared Object, or Service.
Internally, the settings package also leverages a couple custom preference implementations, notably `IntListPreference`, which enables Internally, the settings package also leverages a couple custom preference implementations,
a normal choice preference to be backed by the integer representations that Auxio uses. notably `IntListPreference`, which enables a normal choice preference to be backed by the
integer data that Auxio uses.
#### `.ui` #### `.ui`
Shared views and view configuration models. This contains: Shared views and view configuration models. This contains:
- Important `Fragment` superclasses like `ViewBindingFragment` and `MenuFragment` - Important `Fragment` superclasses like `ViewBindingFragment` and `MenuFragment`
- Customized views such as `EdgeAppBarLayout`, and others, which just fix existing shortcomings with the views. - Customized views such as `EdgeAppBarLayout`, and others, which fix shortcomings with the
- Configuration models like `DisplayMode` and `Sort`, which are used in many places but aren't tied to a specific feature. default implementations.
- Configuration models like `DisplayMode` and `Sort`, which are used in many places but aren't tied
to a specific feature.
- The `RecyclerView` adapter framework described previously. - The `RecyclerView` adapter framework described previously.
- `BottomSheetLayout`, a highly important layout that underpins Auxio's UI flow. - `BottomSheetLayout`, which implements a bottom sheet in a way that is not completely broken and
insane.
- Standard `ViewHolder` implementations that can be used for common datatypes. - 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` - `NavigationViewModel`, which acts as an interface to control navigation to a particular item and
navigation within `MainFragment`
#### `.util` #### `.util`
Shared utilities. This is primarily for QoL when developing Auxio. Documentation is provided on each method. Shared utilities. This is primarily for QoL when developing Auxio. Documentation is provided on each method.
@ -282,16 +336,19 @@ Utilities are separated into a few groups:
bloated and over-engineered libraries like Timber. bloated and over-engineered libraries like Timber.
#### `.widgets` #### `.widgets`
This package contains Auxio's AppWidget implementation, which deviates from other AppWidget implementations by packing multiple This package contains Auxio's AppWidget implementation, which deviates from other AppWidget
different layouts into a single widget and then switching between them depending on the widget size. Note that since `RemoteViews` implementations by packing multiple different layouts into a single widget and then switching
and the AppWidget API in general is incredibly outdated and limited, this package deviates from much of Auxio's normal UI between them depending on the widget size. Note that since `RemoteViews` and the AppWidget API
in general is incredibly outdated and limited, this package deviates from much of Auxio's normal UI
conventions. conventions.
The playback service owns `WidgetComponent`, which listens to `PlaybackStateManager` for updates. During an update, it reloads PlaybackService owns `WidgetComponent`, which listens to `PlaybackStateManager` for updates. During
all song metadata and playback state into a `WidgetState`, which is an immutable version of the playback state that negates some an update, it reloads all song metadata and playback state into a `WidgetState`, which is an
of the problems with using a volatile shared object. immutable version of the playback state that negates some of the problems with using a volatile
shared object.
`WidgetProvider` is the widget "implementation" exposed in the manifest. When `WidgetComponent` updates it, the class will create `WidgetProvider` is the widget "implementation" exposed in the manifest. When `WidgetComponent`
a series of layouts [e.g "Forms"] for a variety of "size buckets" that would adequately contain the widget. This is then used as updates it, the class will create a series of layouts [e.g "Forms"] for a variety of "size buckets"
the widget views, either with the native responsive behavior on Android 12 and above, or with the responsive behavior back-ported that would adequately contain the widget. This is then used as the widget views, either with the
native responsive behavior on Android 12 and above, or with the responsive behavior back-ported
to older devices. to older devices.