all: general cleanup

Do some general code cleanup.
This commit is contained in:
OxygenCobalt 2022-02-06 09:01:51 -07:00
parent 4d22b99577
commit 0209e526e1
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
13 changed files with 110 additions and 122 deletions

View file

@ -42,7 +42,7 @@ I primarily built Auxio for myself, but you can use it too, I guess.
- Customizable UI & Behavior
- Genres/Artists/Albums/Songs indexing
- Reliable playback state persistence
- ReplayGain support (On MP3, FLAC, OGG, and OPUS)
- ReplayGain support (On MP3, MP4, FLAC, OGG, and OPUS)
- Material You (Android 12+ only)
- Edge-to-edge
- Embedded covers support

View file

@ -20,7 +20,7 @@ package org.oxycblt.auxio.detail
import androidx.annotation.MenuRes
import androidx.appcompat.widget.PopupMenu
import androidx.core.view.forEach
import androidx.core.view.children
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
@ -125,7 +125,7 @@ abstract class DetailFragment : Fragment() {
}
if (showItem != null) {
menu.forEach { item ->
for (item in menu.children) {
item.isVisible = showItem(item.itemId)
}
}

View file

@ -64,7 +64,7 @@ sealed class Tab(open val mode: DisplayMode) {
var sequence = 0b0100
var shift = SEQUENCE_LEN * 4
distinct.forEach { tab ->
for (tab in distinct) {
val bin = when (tab) {
is Visible -> 1.shl(3) or tab.mode.ordinal
is Invisible -> tab.mode.ordinal
@ -107,10 +107,9 @@ sealed class Tab(open val mode: DisplayMode) {
// Make sure there are no duplicate tabs
val distinct = tabs.distinctBy { it.mode }
// For safety, use the default configuration if something went wrong
// and we have an empty or larger-than-expected tab array.
// For safety, return null if we have an empty or larger-than-expected tab array.
if (distinct.isEmpty() || distinct.size < SEQUENCE_LEN) {
logE("Sequence size was ${distinct.size}, which is invalid. Using defaults instead")
logE("Sequence size was ${distinct.size}, which is invalid.")
return null
}

View file

@ -137,7 +137,7 @@ data class Album(
val _mediaStoreArtistName: String,
) : MusicParent() {
init {
songs.forEach { song ->
for (song in songs) {
song.mediaStoreLinkAlbum(this)
}
}
@ -181,7 +181,7 @@ data class Artist(
val albums: List<Album>
) : MusicParent() {
init {
albums.forEach { album ->
for (album in albums) {
album.mediaStoreLinkArtist(this)
}
}
@ -198,23 +198,19 @@ data class Artist(
data class Genre(
override val name: String,
override val resolvedName: String,
/** Internal field. Do not use. */
val _mediaStoreId: Long
val songs: List<Song>
) : MusicParent() {
init {
for (song in songs) {
song.mediaStoreLinkGenre(this)
}
}
override val id = name.hashCode().toLong()
/** The formatted total duration of this genre */
val totalDuration: String get() =
songs.sumOf { it.seconds }.toDuration(false)
private val mSongs = mutableListOf<Song>()
val songs: List<Song> get() = mSongs
/** Internal method. Do not use. */
fun linkSong(song: Song) {
mSongs.add(song)
song.mediaStoreLinkGenre(this)
}
}
/**

View file

@ -22,13 +22,14 @@ import java.lang.Exception
*
* You think that if you wanted to query a song's genre from a media database, you could just
* put "genre" in the query and it would return it, right? But not with MediaStore! No, that's
* too straightforward for this class that was dropped on it's head as a baby. So instead, you
* too straightforward for this contract that was dropped on it's head as a baby. So instead, you
* have to query for each genre, query all the songs in each genre, and then iterate through those
* songs to link every song with their genre. This is not documented anywhere, and the
* O(mom im scared) algorithm you have to run to get it working single-handedly DOUBLES Auxio's
* loading times. At no point have the devs considered that this column is absolutely insane, and
* instead focused on adding infuriat- I mean nice proprietary extensions to MediaStore for their
* own Google Play Music, and we all know how great that worked out!
* own Google Play Music, and of course every Google Play Music user knew how great that turned
* out!
*
* It's not even ergonomics that makes this API bad. It's base implementation is completely borked
* as well. Did you know that MediaStore doesn't accept dates that aren't from ID3v2.3 MP3 files?
@ -37,7 +38,7 @@ import java.lang.Exception
* DATE tag. Once again, this is because internally android uses an ancient in-house metadata
* parser to get everything indexed, and so far they have not bothered to modernize this parser
* or even switch it to something more powerful like Taglib, not even in Android 12. ID3v2.4 has
* been around for 21 years. It can drink now. All of my what.
* been around for *21 years.* *It can drink now.* All of my what.
*
* Not to mention all the other infuriating quirks. Album artists can't be accessed from the albums
* table, so we have to go for the less efficient "make a big query on all the songs lol" method
@ -46,21 +47,21 @@ import java.lang.Exception
* crippling the normal tables so that you're railroaded into their music app. The way I do
* blacklisting relies on a deprecated method, and the supposedly "modern" method is SLOWER and
* causes even more problems since I have to manage databases across version boundaries. Sometimes
* music will have a deformed clone that I can't filter out, sometimes Genres will just break for no
* reason, and sometimes tags encoded in UTF-8 will be interpreted as anything from UTF-16 to Latin-1
* to Shift JIS WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY
* music will have a deformed clone that I can't filter out, sometimes Genres will just break for
* no reason, and sometimes tags encoded in UTF-8 will be interpreted as anything from UTF-16 to
* Latin-1 to *Shift JIS* WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY WHY
*
* Is there anything we can do about it? No. Google has routinely shut down issues that begged google
* to fix glaring issues with MediaStore or to just take the API behind the woodshed and shoot it.
* Largely because they have zero incentive to improve it given how "obscure" music listening is.
* As a result, some players like Vanilla and VLC just hack their own pidgin version of MediaStore
* from their own parsers, but this is both infeasible for Auxio due to how incredibly slow it is
* to get a file handle from the android sandbox AND how much harder it is to manage a database of
* your own media that mirrors the filesystem perfectly. And even if I set aside those crippling
* issues and changed my indexer to that, it would face the even larger problem of how google keeps
* trying to kill the filesystem and force you into their ContentResolver API. In the future
* MediaStore could be the only system we have, which is also the day that greenland melts and
* birthdays stop happening forever.
* Largely because they have zero incentive to improve it given how "obscure" local music listening
* is. As a result, some players like Vanilla and VLC just hack their own pseudo-MediaStore
* implementation from their own (better) parsers, but this is both infeasible for Auxio due to how
* incredibly slow it is to get a file handle from the android sandbox AND how much harder it is to
* manage a database of your own media that mirrors the filesystem perfectly. And even if I set
* aside those crippling issues and changed my indexer to that, it would face the even larger
* problem of how google keeps trying to kill the filesystem and force you into their
* ContentResolver API. In the future MediaStore could be the only system we have, which is also the
* day that greenland melts and birthdays stop happening forever.
*
* I'm pretty sure nothing is going to happen and MediaStore will continue to be neglected and
* probably deprecated eventually for a "new" API that just coincidentally excludes music indexing.
@ -122,32 +123,32 @@ class MusicLoader {
args += "$path%" // Append % so that the selector properly detects children
}
context.contentResolver.query(
context.applicationContext.contentResolver.query(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
arrayOf(
MediaStore.Audio.Media._ID,
MediaStore.Audio.Media.TITLE,
MediaStore.Audio.Media.DISPLAY_NAME,
MediaStore.Audio.Media.ALBUM,
MediaStore.Audio.Media.ALBUM_ID,
MediaStore.Audio.Media.ARTIST,
MediaStore.Audio.Media.ALBUM_ARTIST,
MediaStore.Audio.Media.YEAR,
MediaStore.Audio.Media.TRACK,
MediaStore.Audio.Media.DURATION,
MediaStore.Audio.AudioColumns._ID,
MediaStore.Audio.AudioColumns.TITLE,
MediaStore.Audio.AudioColumns.DISPLAY_NAME,
MediaStore.Audio.AudioColumns.ALBUM,
MediaStore.Audio.AudioColumns.ALBUM_ID,
MediaStore.Audio.AudioColumns.ARTIST,
MediaStore.Audio.AudioColumns.ALBUM_ARTIST,
MediaStore.Audio.AudioColumns.YEAR,
MediaStore.Audio.AudioColumns.TRACK,
MediaStore.Audio.AudioColumns.DURATION,
),
selector, args.toTypedArray(), null
)?.use { cursor ->
val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)
val titleIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE)
val fileIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME)
val albumIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM)
val albumIdIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID)
val artistIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST)
val albumArtistIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ARTIST)
val yearIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.YEAR)
val trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TRACK)
val durationIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION)
val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns._ID)
val titleIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TITLE)
val fileIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISPLAY_NAME)
val albumIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM)
val albumIdIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM_ID)
val artistIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ARTIST)
val albumArtistIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM_ARTIST)
val yearIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.YEAR)
val trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK)
val durationIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DURATION)
while (cursor.moveToNext()) {
val id = cursor.getLong(idIndex)
@ -284,7 +285,6 @@ class MusicLoader {
private fun readGenres(context: Context, songs: List<Song>): List<Genre> {
val genres = mutableListOf<Genre>()
// First, get a cursor for every genre in the android system
val genreCursor = context.contentResolver.query(
MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI,
arrayOf(
@ -300,45 +300,46 @@ class MusicLoader {
val nameIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME)
while (cursor.moveToNext()) {
// Genre names can be a normal name, an ID3v2 constant, or null. Normal names are
// resolved as usual, but null values don't make sense and are often junk anyway,
// so we skip genres that have them.
val id = cursor.getLong(idIndex)
// No non-broken genre would be missing a name.
val name = cursor.getStringOrNull(nameIndex) ?: continue
val resolvedName = when (name) {
MediaStore.UNKNOWN_STRING -> context.getString(R.string.def_genre)
else -> name.getGenreNameCompat() ?: name
}
val resolvedName = name.getGenreNameCompat() ?: name
val genreSongs = queryGenreSongs(context, id, songs) ?: continue
val genre = Genre(name, resolvedName, id)
linkGenre(context, genre, songs)
genres.add(genre)
genres.add(
Genre(
name,
resolvedName,
genreSongs
)
)
}
}
// Songs that don't have a genre will be thrown into an unknown genre.
val unknownGenre = Genre(
name = MediaStore.UNKNOWN_STRING,
resolvedName = context.getString(R.string.def_genre),
Long.MIN_VALUE
)
val songsWithoutGenres = songs.filter { it.genre == null }
songs.forEach { song ->
if (song.genre == null) {
unknownGenre.linkSong(song)
}
}
if (songsWithoutGenres.isNotEmpty()) {
// Songs that don't have a genre will be thrown into an unknown genre.
val unknownGenre = Genre(
name = MediaStore.UNKNOWN_STRING,
resolvedName = context.getString(R.string.def_genre),
songsWithoutGenres
)
if (unknownGenre.songs.isNotEmpty()) {
genres.add(unknownGenre)
}
return genres
}
private fun linkGenre(context: Context, genre: Genre, songs: List<Song>) {
private fun queryGenreSongs(context: Context, genreId: Long, songs: List<Song>): List<Song>? {
val genreSongs = mutableListOf<Song>()
// Don't even bother blacklisting here as useless iterations are less expensive than IO
val songCursor = context.contentResolver.query(
MediaStore.Audio.Genres.Members.getContentUri("external", genre._mediaStoreId),
MediaStore.Audio.Genres.Members.getContentUri("external", genreId),
arrayOf(MediaStore.Audio.Genres.Members._ID),
null, null, null
)
@ -350,9 +351,13 @@ class MusicLoader {
val id = cursor.getLong(idIndex)
songs.find { it._mediaStoreId == id }?.let { song ->
genre.linkSong(song)
genreSongs.add(song)
}
}
}
// Some genres might be empty due to MediaStore empty.
// If that is the case, we drop them.
return genreSongs.ifEmpty { null }
}
}

View file

@ -98,6 +98,8 @@ class AudioReactor(
return
}
logD("$metadata")
// ReplayGain is configurable, so determine what to do based off of the mode.
val useAlbumGain: (Gain) -> Boolean = when (settingsManager.replayGainMode) {
ReplayGainMode.OFF -> {
@ -144,8 +146,8 @@ class AudioReactor(
// Final adjustment along the volume curve.
// Ensure this is clamped to 0 or 1 so that it can be used as a volume.
// TODO: Support positive ReplayGain values. They're more obscure but still exist.
// It will likely require moving functionality from this class to an AudioProcessor
// While positive ReplayGain values *could* be theoretically added, it's such
// a niche use-case that to be worth the effort required. Maybe if someone requests it.
volume = MathUtils.clamp((10f.pow((adjust / 20f))), 0f, 1f)
}

View file

@ -55,11 +55,8 @@ class SettingsListFragment : PreferenceFragmentCompat() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
preferenceScreen.children.forEach { pref ->
recursivelyHandleChildren(pref)
}
preferenceManager.onDisplayPreferenceDialogListener = this
preferenceScreen.children.forEach(::recursivelyHandlePreference)
view.findViewById<RecyclerView>(androidx.preference.R.id.recycler_view).apply {
clipToPadding = false
@ -87,28 +84,18 @@ class SettingsListFragment : PreferenceFragmentCompat() {
}
/**
* Recursively call [handlePreference] on a preference.
* Recursively handle a preference, doing any specific actions on it.
*/
private fun recursivelyHandleChildren(preference: Preference) {
if (!preference.isVisible) {
return
}
private fun recursivelyHandlePreference(preference: Preference) {
if (!preference.isVisible) return
if (preference is PreferenceCategory) {
// If this preference is a category of its own, handle its own children
preference.children.forEach { pref ->
recursivelyHandleChildren(pref)
for (child in preference.children) {
recursivelyHandlePreference(child)
}
} else {
handlePreference(preference)
}
}
/**
* Handle a preference, doing any specific actions on it.
*/
private fun handlePreference(pref: Preference) {
pref.apply {
preference.apply {
when (key) {
SettingsManager.KEY_THEME -> {
setIcon(AppCompatDelegate.getDefaultNightMode().toThemeIcon())

View file

@ -24,7 +24,7 @@
android:text="@{String.valueOf(song.track)}"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.Auxio.TitleMedium"
android:textSize="20sp"
android:textSize="@dimen/text_size_ext_title_mid_larger"
android:textColor="@color/sel_accented_secondary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"

View file

@ -22,6 +22,10 @@
<dimen name="size_track_number">32dp</dimen>
<dimen name="size_play_fab_icon">32dp</dimen>
<dimen name="text_size_ext_label_larger">16sp</dimen>
<dimen name="text_size_ext_title_mid_large">18sp</dimen>
<dimen name="text_size_ext_title_mid_larger">20sp</dimen>
<!-- Misc -->
<dimen name="elevation_small">2dp</dimen>
<dimen name="elevation_normal">4dp</dimen>

View file

@ -4,7 +4,7 @@
<style name="TextAppearance.Auxio.DisplayLarge" parent="TextAppearance.Material3.DisplayLarge">
<item name="fontFamily">@font/inter_semibold</item>
<item name="android:fontFamily">@font/inter_semibold</item>
<item name="android:textStyle">normal</item>
<item name="android:textStyle">bold</item>
</style>
<style name="TextAppearance.Auxio.DisplayMedium" parent="TextAppearance.Material3.DisplayMedium">
@ -22,7 +22,7 @@
<style name="TextAppearance.Auxio.HeadlineLarge" parent="TextAppearance.Material3.HeadlineLarge">
<item name="fontFamily">@font/inter_semibold</item>
<item name="android:fontFamily">@font/inter_semibold</item>
<item name="android:textStyle">normal</item>
<item name="android:textStyle">bold</item>
</style>
<style name="TextAppearance.Auxio.HeadlineMedium" parent="TextAppearance.Material3.HeadlineMedium">
@ -40,7 +40,7 @@
<style name="TextAppearance.Auxio.TitleLarge" parent="TextAppearance.Material3.TitleLarge">
<item name="fontFamily">@font/inter_semibold</item>
<item name="android:fontFamily">@font/inter_semibold</item>
<item name="android:textStyle">normal</item>
<item name="android:textStyle">bold</item>
</style>
<style name="TextAppearance.Auxio.TitleMedium" parent="TextAppearance.Material3.TitleMedium">
@ -58,21 +58,21 @@
<style name="TextAppearance.Auxio.LabelLarge" parent="TextAppearance.Material3.LabelLarge">
<item name="fontFamily">@font/inter_semibold</item>
<item name="android:fontFamily">@font/inter_semibold</item>
<item name="android:textStyle">normal</item>
<item name="android:textStyle">bold</item>
<item name="android:letterSpacing">0.01</item>
</style>
<style name="TextAppearance.Auxio.LabelMedium" parent="TextAppearance.Material3.LabelMedium">
<item name="fontFamily">@font/inter_semibold</item>
<item name="android:fontFamily">@font/inter_semibold</item>
<item name="android:textStyle">normal</item>
<item name="android:textStyle">bold</item>
<item name="android:letterSpacing">0.01</item>
</style>
<style name="TextAppearance.Auxio.LabelSmall" parent="TextAppearance.Material3.LabelSmall">
<item name="fontFamily">@font/inter_semibold</item>
<item name="android:fontFamily">@font/inter_semibold</item>
<item name="android:textStyle">normal</item>
<item name="android:textStyle">bold</item>
<item name="android:letterSpacing">0.01</item>
</style>
@ -97,19 +97,14 @@
<item name="android:letterSpacing">0.03</item>
</style>
<!--
Text extensions
Material3 TextAppearances are really inflexible, so these add some extra categories that
allow for better UX.
-->
<style name="TextAppearance.Auxio.TitleMidLarge" parent="TextAppearance.Material3.TitleMedium">
<item name="fontFamily">@font/inter_semibold</item>
<item name="android:fontFamily">@font/inter_semibold</item>
<item name="android:textStyle">normal</item>
<item name="android:textSize">18sp</item>
<item name="android:textStyle">bold</item>
<item name="android:textSize">@dimen/text_size_ext_title_mid_large</item>
</style>
<style name="TextAppearance.Auxio.LabelLarger" parent="TextAppearance.Auxio.LabelLarge">
<item name="android:textSize">16sp</item>
<item name="android:textSize">@dimen/text_size_ext_label_larger</item>
</style>
</resources>

View file

@ -9,7 +9,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:7.1.0'
classpath 'com.android.tools.build:gradle:7.1.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navigation_version"

View file

@ -29,4 +29,5 @@ Feel free to fork Auxio to add your own feature set however.
- Tag editing [#33] (Out of scope)
- Gapless Playback [#35] (Technical issues)
- Reduce leading instrument [#45] (Technical issues, Out of scope)
- Opening music through a provider [#30] (Out of scope)
- Disabling track numbers [#73] (Out of scope)
- Opening music through a provider [#30] (Out of scope)

View file

@ -38,8 +38,7 @@ I hope to make the app rescan music on the fly eventually.
#### ReplayGain isn't working on my music!
This is for a couple reason:
- Auxio doesn't extract ReplayGain tags for your format. This is the case with MP4 files since there's no
defined ReplayGain standard for those.
- Auxio doesn't extract ReplayGain tags for your format.
- Auxio doesn't recognize your ReplayGain tags. This is usually because of a non-standard tag like ID3v2's `RVAD` or
an unrecognized name.
- Your tags use a ReplayGain value higher than 0. Due to technical limitations, Auxio does not support this right now.