From 627ab9794869bc6138fa0290c7addd252f611a3d Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Sun, 13 Mar 2022 16:42:03 -0600 Subject: [PATCH] all: cleanup A bunch of small changes that have accrued over the last week due to having more or less no time to work on Auxio. --- app/build.gradle | 14 +- .../java/org/oxycblt/auxio/music/Models.kt | 13 +- .../org/oxycblt/auxio/music/MusicLoader.kt | 186 +++++++++--------- .../org/oxycblt/auxio/music/MusicStore.kt | 2 +- .../{ => music}/excluded/ExcludedDatabase.kt | 2 +- .../{ => music}/excluded/ExcludedDialog.kt | 4 +- .../excluded/ExcludedEntryAdapter.kt | 2 +- .../{ => music}/excluded/ExcludedViewModel.kt | 3 +- .../auxio/playback/PlaybackViewModel.kt | 5 +- .../playback/state/PlaybackStateManager.kt | 2 - .../auxio/playback/system/PlaybackService.kt | 4 + .../auxio/settings/SettingsListFragment.kt | 2 +- .../java/org/oxycblt/auxio/ui/DiffCallback.kt | 8 +- .../org/oxycblt/auxio/util/ContextUtil.kt | 7 +- gradle.properties | 2 +- info/ARCHITECTURE.md | 8 +- 16 files changed, 140 insertions(+), 124 deletions(-) rename app/src/main/java/org/oxycblt/auxio/{ => music}/excluded/ExcludedDatabase.kt (98%) rename app/src/main/java/org/oxycblt/auxio/{ => music}/excluded/ExcludedDialog.kt (96%) rename app/src/main/java/org/oxycblt/auxio/{ => music}/excluded/ExcludedEntryAdapter.kt (98%) rename app/src/main/java/org/oxycblt/auxio/{ => music}/excluded/ExcludedViewModel.kt (98%) diff --git a/app/build.gradle b/app/build.gradle index 6131877bc..378a4bf6e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -64,11 +64,11 @@ dependencies { // General implementation "androidx.core:core-ktx:1.7.0" implementation "androidx.activity:activity-ktx:1.4.0" - implementation 'androidx.fragment:fragment-ktx:1.4.1' + implementation "androidx.fragment:fragment-ktx:1.4.1" // UI implementation "androidx.recyclerview:recyclerview:1.2.1" - implementation 'androidx.constraintlayout:constraintlayout:2.1.3' + implementation "androidx.constraintlayout:constraintlayout:2.1.3" implementation "androidx.dynamicanimation:dynamicanimation:1.0.0" implementation "androidx.viewpager2:viewpager2:1.1.0-beta01" @@ -95,20 +95,20 @@ dependencies { // Exoplayer // WARNING: THE EXOPLAYER VERSION MUST BE KEPT IN LOCK-STEP WITH THE FLAC EXTENSION. // IF NOT, VERY UNFRIENDLY BUILD FAILURES AND CRASHES MAY ENSUE. - def exoplayerVersion = '2.17.0' - implementation("com.google.android.exoplayer:exoplayer-core:$exoplayerVersion") + def exoplayerVersion = "2.17.1" + implementation "com.google.android.exoplayer:exoplayer-core:$exoplayerVersion" implementation fileTree(dir: "libs", include: ["extension-*.aar"]) // Image loading - implementation 'io.coil-kt:coil:2.0.0-alpha09' + implementation "io.coil-kt:coil:2.0.0-rc01" // Material - implementation 'com.google.android.material:material:1.6.0-alpha03' + implementation "com.google.android.material:material:1.6.0-alpha03" // --- DEBUG --- // Lint - ktlint 'com.pinterest:ktlint:0.44.0' + ktlint "com.pinterest:ktlint:0.44.0" } task ktlint(type: JavaExec, group: "verification") { diff --git a/app/src/main/java/org/oxycblt/auxio/music/Models.kt b/app/src/main/java/org/oxycblt/auxio/music/Models.kt index ad78d4379..13504b1b3 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Models.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Models.kt @@ -37,7 +37,8 @@ sealed class Item { /** * [Item] variant that represents a music item. - * @property name + * TODO: Make name the actual display name and move raw names (including file names) to a new + * field called rawName. */ sealed class Music : Item() { /** The raw name of this item. */ @@ -116,8 +117,8 @@ data class Song( internalMediaStoreArtistName ?: album.artist.resolvedName /** Internal field. Do not use. */ - val internalGroupingId: Int get() { - var result = internalGroupingArtistName.lowercase().hashCode() + val internalAlbumGroupingId: Long get() { + var result = internalGroupingArtistName.lowercase().hashCode().toLong() result = 31 * result + internalMediaStoreAlbumName.lowercase().hashCode() return result } @@ -187,7 +188,11 @@ data class Album( artist.resolvedName /** Internal field. Do not use. */ - val internalIsMissingArtist: Boolean = mArtist != null + val internalArtistGroupingId: Long get() = + internalGroupingArtistName.lowercase().hashCode().toLong() + + /** Internal field. Do not use. */ + val internalIsMissingArtist: Boolean get() = mArtist == null /** Internal method. Do not use. */ fun internalLinkArtist(artist: Artist) { diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt index d861a3787..5f4debfa2 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt @@ -1,5 +1,6 @@ package org.oxycblt.auxio.music +import android.content.ContentResolver import android.content.ContentUris import android.content.Context import android.net.Uri @@ -8,7 +9,7 @@ import androidx.core.database.getIntOrNull import androidx.core.database.getStringOrNull import androidx.core.text.isDigitsOnly import org.oxycblt.auxio.R -import org.oxycblt.auxio.excluded.ExcludedDatabase +import org.oxycblt.auxio.music.excluded.ExcludedDatabase import org.oxycblt.auxio.util.logD /** @@ -112,27 +113,39 @@ class MusicLoader { ) } - private fun loadSongs(context: Context): List { - var songs = mutableListOf() - val blacklistDatabase = ExcludedDatabase.getInstance(context) - val paths = blacklistDatabase.readPaths() + /** + * Gets a content resolver in a way that does not mangle metadata on + * certain OEM skins. See https://github.com/OxygenCobalt/Auxio/issues/50 + * for more info. + */ + private val Context.contentResolverSafe: ContentResolver get() = + applicationContext.contentResolver + /** + * Does the initial query over the song database, including excluded directory + * checks. The songs returned by this function are **not** well-formed. The + * companion [buildAlbums], [buildArtists], and [readGenres] functions must be + * called with the returned list so that all songs are properly linked up. + */ + private fun loadSongs(context: Context): List { + val blacklistDatabase = ExcludedDatabase.getInstance(context) var selector = "${MediaStore.Audio.Media.IS_MUSIC}=1" val args = mutableListOf() - // DATA was deprecated on Android 10, but is set to be un-deprecated in Android 12L. - // The only reason we'd want to change this is to add external partitions support, but - // that's less efficient and there's no demand for that right now. + // Apply the excluded directories by filtering out specific DATA values. + // DATA was deprecated in Android 10, but it was un-deprecated in Android 12L, + // so it's probably okay to use it. The only reason we would want to use + // another method is for external partitions support, but there is no demand for that. // TODO: Determine if grokking the actual DATA value outside of SQL is more or less // efficient than the current system - for (path in paths) { + for (path in blacklistDatabase.readPaths()) { selector += " AND ${MediaStore.Audio.Media.DATA} NOT LIKE ?" args += "$path%" // Append % so that the selector properly detects children } - // TODO: Move all references to contentResolver into a single variable so we can - // avoid accidentally removing the applicationContext fix - context.applicationContext.contentResolver.query( + var songs = mutableListOf() + + context.contentResolverSafe.query( MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, arrayOf( MediaStore.Audio.AudioColumns._ID, @@ -166,7 +179,7 @@ class MusicLoader { // The TRACK field is for some reason formatted as DTTT, where D is the disk // and T is the track. This is dumb and insane and forces me to mangle track - // numbers above 1000 but there is nothing we can do that won't break the app + // numbers above 1000, but there is nothing we can do that won't break the app // below API 30. // TODO: Disk number support? val track = cursor.getIntOrNull(trackIndex)?.mod(1000) @@ -222,19 +235,22 @@ class MusicLoader { return songs } + /** + * Group songs up into their respective albums. Instead of using the unreliable album or + * artist databases, we instead group up songs by their *lowercase* artist and album name + * to create albums. This serves two purposes: + * 1. Sometimes artist names can be styled differently, e.g "Rammstein" vs. "RAMMSTEIN". + * This makes sure both of those are resolved into a single artist called "Rammstein" + * 2. Sometimes MediaStore will split album IDs up if the songs differ in format. This + * ensures that all songs are unified under a single album. + * + * This does come with some costs, it's far slower than using the album ID itself, and + * it may result in an unrelated album art being selected depending on the song chosen + * as the template, but it seems to work pretty well. + */ private fun buildAlbums(songs: List): List { - // Group up songs by their lowercase artist and album name. This serves two purposes: - // 1. Sometimes artist names can be styled differently, e.g "Rammstein" vs. "RAMMSTEIN". - // This makes sure both of those are resolved into a single artist called "Rammstein" - // 2. Sometimes MediaStore will split album IDs up if the songs differ in format. This - // ensures that all songs are unified under a single album. - // This does come with some costs, it's far slower than using the album ID itself, and it - // may result in an unrelated album art being selected depending on the song chosen as - // the template, but it seems to work pretty well. val albums = mutableListOf() - val songsByAlbum = songs.groupBy { song -> - song.internalGroupingId - } + val songsByAlbum = songs.groupBy { it.internalAlbumGroupingId } for (entry in songsByAlbum) { val albumSongs = entry.value @@ -270,43 +286,30 @@ class MusicLoader { return albums } + /** + * Group up albums into artists. This also requires a de-duplication step due to some + * edge cases where [buildAlbums] could not detect duplicates. + */ private fun buildArtists(context: Context, albums: List): List { val artists = mutableListOf() - val albumsByArtist = albums.groupBy { it.internalGroupingArtistName } + val albumsByArtist = albums.groupBy { it.internalArtistGroupingId } for (entry in albumsByArtist) { - val artistName = entry.key - val resolvedName = when (artistName) { + val templateAlbum = entry.value[0] + val artistName = templateAlbum.internalGroupingArtistName + val resolvedName = when (templateAlbum.internalGroupingArtistName) { MediaStore.UNKNOWN_STRING -> context.getString(R.string.def_artist) else -> artistName } val artistAlbums = entry.value - // Album deduplication does not eliminate every case of fragmented artists, do - // we deduplicate in the artist creation step as well. - // Note that we actually don't do this in groupBy. This is generally because using - // a template song may not result in the best possible artist name in all cases. - val previousArtistIndex = artists.indexOfFirst { artist -> - artist.name.lowercase() == artistName.lowercase() - } - - if (previousArtistIndex > -1) { - val previousArtist = artists[previousArtistIndex] - logD("Merging duplicate artist into pre-existing artist ${previousArtist.name}") - artists[previousArtistIndex] = Artist( - previousArtist.name, - previousArtist.resolvedName, - previousArtist.albums + artistAlbums + artists.add( + Artist( + artistName, + resolvedName, + artistAlbums ) - } else { - artists.add( - Artist( - artistName, - resolvedName, - artistAlbums - ) - ) - } + ) } logD("Successfully built ${artists.size} artists") @@ -314,20 +317,21 @@ class MusicLoader { return artists } + /** + * Read all genres and link them up to the given songs. This is the code that + * requires me to make dozens of useless queries just to link genres up. + */ private fun readGenres(context: Context, songs: List): List { val genres = mutableListOf() - val genreCursor = context.applicationContext.contentResolver.query( + context.contentResolverSafe.query( MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI, arrayOf( MediaStore.Audio.Genres._ID, MediaStore.Audio.Genres.NAME ), null, null, null - ) - - // And then process those into Genre objects - genreCursor?.use { cursor -> + )?.use { cursor -> val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres._ID) val nameIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME) @@ -351,7 +355,6 @@ class MusicLoader { } val songsWithoutGenres = songs.filter { it.internalIsMissingGenre } - if (songsWithoutGenres.isNotEmpty()) { // Songs that don't have a genre will be thrown into an unknown genre. val unknownGenre = Genre( @@ -368,17 +371,42 @@ class MusicLoader { return genres } + /** + * Decodes the genre name from an ID3(v2) constant. See [genreConstantTable] for the + * genre constant map that Auxio uses. + */ + private val String.genreNameCompat: String? get() { + if (isDigitsOnly()) { + // ID3v1, just parse as an integer + return genreConstantTable.getOrNull(toInt()) + } + + if (startsWith('(') && endsWith(')')) { + // ID3v2.3/ID3v2.4, parse out the parentheses and get the integer + // Any genres formatted as "(CHARS)" will be ignored. + val genreInt = substring(1 until lastIndex).toIntOrNull() + if (genreInt != null) { + return genreConstantTable.getOrNull(genreInt) + } + } + + // Current name is fine. + return null + } + + /** + * Queries the genre songs for [genreId]. Some genres are insane and don't contain songs + * for some reason, so if that's the case then this function will return null. + */ private fun queryGenreSongs(context: Context, genreId: Long, songs: List): List? { val genreSongs = mutableListOf() // Don't even bother blacklisting here as useless iterations are less expensive than IO - val songCursor = context.applicationContext.contentResolver.query( + context.contentResolverSafe.query( MediaStore.Audio.Genres.Members.getContentUri("external", genreId), arrayOf(MediaStore.Audio.Genres.Members._ID), null, null, null - ) - - songCursor?.use { cursor -> + )?.use { cursor -> val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.Members._ID) while (cursor.moveToNext()) { @@ -389,30 +417,9 @@ class MusicLoader { } } - // Some genres might be empty due to MediaStore insanity. - // If that is the case, we drop them. return genreSongs.ifEmpty { null } } - private val String.genreNameCompat: String? get() { - if (isDigitsOnly()) { - // ID3v1, just parse as an integer - return legacyGenreTable.getOrNull(toInt()) - } - - if (startsWith('(') && endsWith(')')) { - // ID3v2.3/ID3v2.4, parse out the parentheses and get the integer - // Any genres formatted as "(CHARS)" will be ignored. - val genreInt = substring(1 until lastIndex).toIntOrNull() - if (genreInt != null) { - return legacyGenreTable.getOrNull(genreInt) - } - } - - // Current name is fine. - return null - } - companion object { /** * The album_artist MediaStore field has existed since at least API 21, but until API @@ -421,13 +428,13 @@ class MusicLoader { * warning about using a possibly-unsupported constant. */ @Suppress("InlinedApi") - const val AUDIO_COLUMN_ALBUM_ARTIST = MediaStore.Audio.AudioColumns.ALBUM_ARTIST + private const val AUDIO_COLUMN_ALBUM_ARTIST = MediaStore.Audio.AudioColumns.ALBUM_ARTIST /** - * A complete array of all the hardcoded genre values for ID3(v2), contains standard genres and - * winamp extensions. + * A complete table of all the constant genre values for ID3(v2), including non-standard + * extensions. */ - private val legacyGenreTable = arrayOf( + private val genreConstantTable = arrayOf( // ID3 Standard "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap", "Reggae", "Rock", @@ -441,7 +448,7 @@ class MusicLoader { "New Wave", "Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll", "Hard Rock", - // Winamp Extensions + // Winamp extensions, more or less a de-facto standard "Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion", "Bebob", "Latin", "Revival", "Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", "Progressive Rock", "Psychedelic Rock", "Symphonic Rock", "Slow Rock", "Big Band", "Chorus", @@ -454,9 +461,8 @@ class MusicLoader { "Crossover", "Contemporary Christian", "Christian Rock", "Merengue", "Salsa", "Thrash Metal", "Anime", "JPop", "Synthpop", - // Winamp 5.6+ extensions, used by EasyTAG and friends - // The only reason I include this set is because post-rock is a based genre and - // deserves a slot. + // Winamp 5.6+ extensions, also used by EasyTAG. + // I only include this because post-rock is a based genre and deserves a slot. "Abstract", "Art Rock", "Baroque", "Bhangra", "Big Beat", "Breakbeat", "Chillout", "Downtempo", "Dub", "EBM", "Eclectic", "Electro", "Electroclash", "Emo", "Experimental", "Garage", "Global", "IDM", "Illbient", "Industro-Goth", "Jam Band", "Krautrock", diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt index d98671708..95cf81e9a 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt @@ -118,7 +118,7 @@ class MusicStore private constructor() { /** * A response that [MusicStore] returns when loading music. * And before you ask, yes, I do like rust. - * TODO: Replace this with the kotlin builtin + * TODO: Add the exception to the "FAILED" ErrorKind */ sealed class Response { class Ok(val musicStore: MusicStore) : Response() diff --git a/app/src/main/java/org/oxycblt/auxio/excluded/ExcludedDatabase.kt b/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDatabase.kt similarity index 98% rename from app/src/main/java/org/oxycblt/auxio/excluded/ExcludedDatabase.kt rename to app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDatabase.kt index b3562d563..3267b986d 100644 --- a/app/src/main/java/org/oxycblt/auxio/excluded/ExcludedDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDatabase.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.excluded +package org.oxycblt.auxio.music.excluded import android.content.ContentValues import android.content.Context diff --git a/app/src/main/java/org/oxycblt/auxio/excluded/ExcludedDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDialog.kt similarity index 96% rename from app/src/main/java/org/oxycblt/auxio/excluded/ExcludedDialog.kt rename to app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDialog.kt index c5b9be463..b45aa4ed5 100644 --- a/app/src/main/java/org/oxycblt/auxio/excluded/ExcludedDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDialog.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.excluded +package org.oxycblt.auxio.music.excluded import android.net.Uri import android.os.Bundle @@ -141,6 +141,8 @@ class ExcludedDialog : LifecycleDialog() { // Only the main drive is supported, since that's all we can get from MediaColumns.DATA // Unless I change the system to use the drive/directory system, that is. But there's no // demand for that. + // TODO: You are going to split the queries into pre-Q and post-Q versions, so perhaps + // you should try to add external partition support again. if (typeAndPath[0] == "primary") { return getRootPath() + "/" + typeAndPath.last() } diff --git a/app/src/main/java/org/oxycblt/auxio/excluded/ExcludedEntryAdapter.kt b/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedEntryAdapter.kt similarity index 98% rename from app/src/main/java/org/oxycblt/auxio/excluded/ExcludedEntryAdapter.kt rename to app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedEntryAdapter.kt index b8a3b1f7d..e5763439e 100644 --- a/app/src/main/java/org/oxycblt/auxio/excluded/ExcludedEntryAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedEntryAdapter.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.excluded +package org.oxycblt.auxio.music.excluded import android.annotation.SuppressLint import android.view.ViewGroup diff --git a/app/src/main/java/org/oxycblt/auxio/excluded/ExcludedViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedViewModel.kt similarity index 98% rename from app/src/main/java/org/oxycblt/auxio/excluded/ExcludedViewModel.kt rename to app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedViewModel.kt index 8de9ccc9b..41dda89d8 100644 --- a/app/src/main/java/org/oxycblt/auxio/excluded/ExcludedViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedViewModel.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.excluded +package org.oxycblt.auxio.music.excluded import android.content.Context import androidx.lifecycle.LiveData @@ -32,6 +32,7 @@ import org.oxycblt.auxio.util.logD /** * ViewModel that acts as a wrapper around [ExcludedDatabase], allowing for the addition/removal * of paths. Use [Factory] to instantiate this. + * TODO: Unify with MusicViewModel * @author OxygenCobalt */ class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewModel() { 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 1fcb41a35..93fd20cac 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -44,6 +44,10 @@ import org.oxycblt.auxio.util.logE * **PLEASE Use this instead of [PlaybackStateManager], UI's are extremely volatile and this provides * an interface that properly sanitizes input and abstracts functions unlike the master class.** * @author OxygenCobalt + * + * TODO: Completely rework this module to support the new music rescan system, + * proper android auto and external exposing, and so on. + * - DO NOT REWRITE IT! THAT'S BAD AND WILL PROBABLY RE-INTRODUCE A TON OF BUGS. */ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { // Playback @@ -170,7 +174,6 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { logD("Playing with uri $uri") val musicStore = MusicStore.maybeGetInstance() ?: return - musicStore.findSongForUri(uri, context.contentResolver)?.let { song -> playSong(song) } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt index 0a8e1d7c9..348bb2a81 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt @@ -40,8 +40,6 @@ import org.oxycblt.auxio.util.logE * * All access should be done with [PlaybackStateManager.getInstance]. * @author OxygenCobalt - * - * TODO: Rework this to possibly handle gapless playback and more refined queue management. */ class PlaybackStateManager private constructor() { // Playback diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt index 03cac948b..c8e0d8ccc 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt @@ -510,6 +510,8 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac * due to AudioManager.ACTION_HEADSET_PLUG always firing on startup. This is fixed, but * I fear that it may not work on OEM skins that for whatever reason don't make this * action fire. + * TODO: Figure out how players like Retro are able to get autoplay working with + * bluetooth headsets */ private fun maybeResumeFromPlug() { if (playbackManager.song != null && @@ -523,6 +525,8 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac /** * Pause from a headset plug. + * TODO: Find a way to centralize this stuff into a single BroadcastReciever instead + * of the weird disjointed arrangement between MediaSession and this. */ private fun pauseFromPlug() { if (playbackManager.song != null) { diff --git a/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt index 525d5654d..3b8e2a278 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt @@ -32,7 +32,7 @@ import androidx.recyclerview.widget.RecyclerView import coil.Coil import org.oxycblt.auxio.R import org.oxycblt.auxio.accent.AccentCustomizeDialog -import org.oxycblt.auxio.excluded.ExcludedDialog +import org.oxycblt.auxio.music.excluded.ExcludedDialog import org.oxycblt.auxio.home.tabs.TabCustomizeDialog import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.settings.pref.IntListPrefDialog diff --git a/app/src/main/java/org/oxycblt/auxio/ui/DiffCallback.kt b/app/src/main/java/org/oxycblt/auxio/ui/DiffCallback.kt index 5ed786da4..7aa7ccfda 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/DiffCallback.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/DiffCallback.kt @@ -27,14 +27,14 @@ import org.oxycblt.auxio.music.Item * @author OxygenCobalt */ class DiffCallback : DiffUtil.ItemCallback() { - override fun areContentsTheSame(oldItem: T, newItem: T): Boolean { - return oldItem.hashCode() == newItem.hashCode() - } - override fun areItemsTheSame(oldItem: T, newItem: T): Boolean { // Prevent ID collisions from occurring between datatypes. if (oldItem.javaClass != newItem.javaClass) return false return oldItem.id == newItem.id } + + override fun areContentsTheSame(oldItem: T, newItem: T): Boolean { + return oldItem.hashCode() == newItem.hashCode() + } } diff --git a/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt index 77ca4c7b2..e075552a8 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/ContextUtil.kt @@ -245,11 +245,8 @@ fun Context.hardRestart() { // Instead of having to do a ton of cleanup and horrible code changes // to restart this application non-destructively, I just restart the UI task [There is only // one, after all] and then kill the application using exitProcess. Works well enough. - val intent = Intent(applicationContext, MainActivity::class.java).setFlags( - Intent.FLAG_ACTIVITY_CLEAR_TASK - ) - + val intent = Intent(applicationContext, MainActivity::class.java) + .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) startActivity(intent) - exitProcess(0) } diff --git a/gradle.properties b/gradle.properties index f3bf8f027..a69572161 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ org.gradle.jvmargs=-Xmx2048m android.useAndroidX=true # Automatically convert third-party libraries to use AndroidX android.enableJetifier=true -# Stop ExoPlayer from mangling AAR libraries with default abstract methods +# Stop ExoPlayer AARs from being mangled android.enableDexingArtifactTransform=false # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official diff --git a/info/ARCHITECTURE.md b/info/ARCHITECTURE.md index 6ee1a3b84..1be331ff0 100644 --- a/info/ARCHITECTURE.md +++ b/info/ARCHITECTURE.md @@ -17,12 +17,12 @@ org.oxycblt.auxio # Main UIs ├──.coil # Image loading components ├──.detail # Album/Artist/Genre detail UIs │ └──.recycler # RecyclerView components for detail UIs -├──.excluded # Excluded Directories UI + Systems ├──.home # Home UI │ ├──.fastscroll # Fast scroller UI │ ├──.list # Home item lists │ └──.tabs # Home tab customization ├──.music # Music data and loading +│ └──.excluded # Excluded Directories UI + Systems ├──.playback # Playback UI + Systems │ ├──.queue # Queue UI │ ├──.state # Playback state backend @@ -86,10 +86,10 @@ should only be talking to other shared objects. All objects can use the utility #### Data objects Auxio represents data in multiple ways. -`BaseModel` 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: -- `Music` is a `BaseModel` 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 of the music (from `MediaStore`). - `MusicParent` is a type of `Music` that contains children. It adds a `resolveName` field that converts the raw `MediaStore` name to a name that can be used in UIs. - `Header` and `ActionHeader` are UI data objects that represent a header item. `Header` corresponds to a simple header with no action, @@ -301,4 +301,4 @@ an immutable version of the playback state that negates some of the problems wit a layout [e.g "Form"] depending on its current dimensions and applies the `WidgetState` object to that. **Note:** The AppWidget implementation violates UI conventions by directly interfacing with coil and `PlaybackStateManager`. -This is required due to `RemoteView` limitations. \ No newline at end of file +This is required due to `RemoteView` limitations.