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.
This commit is contained in:
OxygenCobalt 2022-03-13 16:42:03 -06:00
parent 82247775ac
commit 627ab97948
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
16 changed files with 140 additions and 124 deletions

View file

@ -64,11 +64,11 @@ dependencies {
// General // General
implementation "androidx.core:core-ktx:1.7.0" implementation "androidx.core:core-ktx:1.7.0"
implementation "androidx.activity:activity-ktx:1.4.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 // UI
implementation "androidx.recyclerview:recyclerview:1.2.1" 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.dynamicanimation:dynamicanimation:1.0.0"
implementation "androidx.viewpager2:viewpager2:1.1.0-beta01" implementation "androidx.viewpager2:viewpager2:1.1.0-beta01"
@ -95,20 +95,20 @@ dependencies {
// Exoplayer // Exoplayer
// WARNING: THE EXOPLAYER VERSION MUST BE KEPT IN LOCK-STEP WITH THE FLAC EXTENSION. // WARNING: THE EXOPLAYER VERSION MUST BE KEPT IN LOCK-STEP WITH THE FLAC EXTENSION.
// IF NOT, VERY UNFRIENDLY BUILD FAILURES AND CRASHES MAY ENSUE. // IF NOT, VERY UNFRIENDLY BUILD FAILURES AND CRASHES MAY ENSUE.
def exoplayerVersion = '2.17.0' def exoplayerVersion = "2.17.1"
implementation("com.google.android.exoplayer:exoplayer-core:$exoplayerVersion") implementation "com.google.android.exoplayer:exoplayer-core:$exoplayerVersion"
implementation fileTree(dir: "libs", include: ["extension-*.aar"]) implementation fileTree(dir: "libs", include: ["extension-*.aar"])
// Image loading // Image loading
implementation 'io.coil-kt:coil:2.0.0-alpha09' implementation "io.coil-kt:coil:2.0.0-rc01"
// Material // Material
implementation 'com.google.android.material:material:1.6.0-alpha03' implementation "com.google.android.material:material:1.6.0-alpha03"
// --- DEBUG --- // --- DEBUG ---
// Lint // Lint
ktlint 'com.pinterest:ktlint:0.44.0' ktlint "com.pinterest:ktlint:0.44.0"
} }
task ktlint(type: JavaExec, group: "verification") { task ktlint(type: JavaExec, group: "verification") {

View file

@ -37,7 +37,8 @@ sealed class Item {
/** /**
* [Item] variant that represents a music 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() { sealed class Music : Item() {
/** The raw name of this item. */ /** The raw name of this item. */
@ -116,8 +117,8 @@ data class Song(
internalMediaStoreArtistName ?: album.artist.resolvedName internalMediaStoreArtistName ?: album.artist.resolvedName
/** Internal field. Do not use. */ /** Internal field. Do not use. */
val internalGroupingId: Int get() { val internalAlbumGroupingId: Long get() {
var result = internalGroupingArtistName.lowercase().hashCode() var result = internalGroupingArtistName.lowercase().hashCode().toLong()
result = 31 * result + internalMediaStoreAlbumName.lowercase().hashCode() result = 31 * result + internalMediaStoreAlbumName.lowercase().hashCode()
return result return result
} }
@ -187,7 +188,11 @@ data class Album(
artist.resolvedName artist.resolvedName
/** Internal field. Do not use. */ /** 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. */ /** Internal method. Do not use. */
fun internalLinkArtist(artist: Artist) { fun internalLinkArtist(artist: Artist) {

View file

@ -1,5 +1,6 @@
package org.oxycblt.auxio.music package org.oxycblt.auxio.music
import android.content.ContentResolver
import android.content.ContentUris import android.content.ContentUris
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
@ -8,7 +9,7 @@ import androidx.core.database.getIntOrNull
import androidx.core.database.getStringOrNull import androidx.core.database.getStringOrNull
import androidx.core.text.isDigitsOnly import androidx.core.text.isDigitsOnly
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.excluded.ExcludedDatabase import org.oxycblt.auxio.music.excluded.ExcludedDatabase
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
/** /**
@ -112,27 +113,39 @@ class MusicLoader {
) )
} }
private fun loadSongs(context: Context): List<Song> { /**
var songs = mutableListOf<Song>() * Gets a content resolver in a way that does not mangle metadata on
val blacklistDatabase = ExcludedDatabase.getInstance(context) * certain OEM skins. See https://github.com/OxygenCobalt/Auxio/issues/50
val paths = blacklistDatabase.readPaths() * 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<Song> {
val blacklistDatabase = ExcludedDatabase.getInstance(context)
var selector = "${MediaStore.Audio.Media.IS_MUSIC}=1" var selector = "${MediaStore.Audio.Media.IS_MUSIC}=1"
val args = mutableListOf<String>() val args = mutableListOf<String>()
// DATA was deprecated on Android 10, but is set to be un-deprecated in Android 12L. // Apply the excluded directories by filtering out specific DATA values.
// The only reason we'd want to change this is to add external partitions support, but // DATA was deprecated in Android 10, but it was un-deprecated in Android 12L,
// that's less efficient and there's no demand for that right now. // 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 // TODO: Determine if grokking the actual DATA value outside of SQL is more or less
// efficient than the current system // efficient than the current system
for (path in paths) { for (path in blacklistDatabase.readPaths()) {
selector += " AND ${MediaStore.Audio.Media.DATA} NOT LIKE ?" selector += " AND ${MediaStore.Audio.Media.DATA} NOT LIKE ?"
args += "$path%" // Append % so that the selector properly detects children args += "$path%" // Append % so that the selector properly detects children
} }
// TODO: Move all references to contentResolver into a single variable so we can var songs = mutableListOf<Song>()
// avoid accidentally removing the applicationContext fix
context.applicationContext.contentResolver.query( context.contentResolverSafe.query(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
arrayOf( arrayOf(
MediaStore.Audio.AudioColumns._ID, 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 // 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 // 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. // below API 30.
// TODO: Disk number support? // TODO: Disk number support?
val track = cursor.getIntOrNull(trackIndex)?.mod(1000) val track = cursor.getIntOrNull(trackIndex)?.mod(1000)
@ -222,19 +235,22 @@ class MusicLoader {
return songs 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<Song>): List<Album> { private fun buildAlbums(songs: List<Song>): List<Album> {
// 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<Album>() val albums = mutableListOf<Album>()
val songsByAlbum = songs.groupBy { song -> val songsByAlbum = songs.groupBy { it.internalAlbumGroupingId }
song.internalGroupingId
}
for (entry in songsByAlbum) { for (entry in songsByAlbum) {
val albumSongs = entry.value val albumSongs = entry.value
@ -270,35 +286,23 @@ class MusicLoader {
return albums 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<Album>): List<Artist> { private fun buildArtists(context: Context, albums: List<Album>): List<Artist> {
val artists = mutableListOf<Artist>() val artists = mutableListOf<Artist>()
val albumsByArtist = albums.groupBy { it.internalGroupingArtistName } val albumsByArtist = albums.groupBy { it.internalArtistGroupingId }
for (entry in albumsByArtist) { for (entry in albumsByArtist) {
val artistName = entry.key val templateAlbum = entry.value[0]
val resolvedName = when (artistName) { val artistName = templateAlbum.internalGroupingArtistName
val resolvedName = when (templateAlbum.internalGroupingArtistName) {
MediaStore.UNKNOWN_STRING -> context.getString(R.string.def_artist) MediaStore.UNKNOWN_STRING -> context.getString(R.string.def_artist)
else -> artistName else -> artistName
} }
val artistAlbums = entry.value 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
)
} else {
artists.add( artists.add(
Artist( Artist(
artistName, artistName,
@ -307,27 +311,27 @@ class MusicLoader {
) )
) )
} }
}
logD("Successfully built ${artists.size} artists") logD("Successfully built ${artists.size} artists")
return artists 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<Song>): List<Genre> { private fun readGenres(context: Context, songs: List<Song>): List<Genre> {
val genres = mutableListOf<Genre>() val genres = mutableListOf<Genre>()
val genreCursor = context.applicationContext.contentResolver.query( context.contentResolverSafe.query(
MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI, MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI,
arrayOf( arrayOf(
MediaStore.Audio.Genres._ID, MediaStore.Audio.Genres._ID,
MediaStore.Audio.Genres.NAME MediaStore.Audio.Genres.NAME
), ),
null, null, null null, null, null
) )?.use { cursor ->
// And then process those into Genre objects
genreCursor?.use { cursor ->
val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres._ID) val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres._ID)
val nameIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME) val nameIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME)
@ -351,7 +355,6 @@ class MusicLoader {
} }
val songsWithoutGenres = songs.filter { it.internalIsMissingGenre } val songsWithoutGenres = songs.filter { it.internalIsMissingGenre }
if (songsWithoutGenres.isNotEmpty()) { if (songsWithoutGenres.isNotEmpty()) {
// Songs that don't have a genre will be thrown into an unknown genre. // Songs that don't have a genre will be thrown into an unknown genre.
val unknownGenre = Genre( val unknownGenre = Genre(
@ -368,17 +371,42 @@ class MusicLoader {
return genres 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<Song>): List<Song>? { private fun queryGenreSongs(context: Context, genreId: Long, songs: List<Song>): List<Song>? {
val genreSongs = mutableListOf<Song>() val genreSongs = mutableListOf<Song>()
// Don't even bother blacklisting here as useless iterations are less expensive than IO // 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), MediaStore.Audio.Genres.Members.getContentUri("external", genreId),
arrayOf(MediaStore.Audio.Genres.Members._ID), arrayOf(MediaStore.Audio.Genres.Members._ID),
null, null, null null, null, null
) )?.use { cursor ->
songCursor?.use { cursor ->
val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.Members._ID) val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.Members._ID)
while (cursor.moveToNext()) { 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 } 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 { companion object {
/** /**
* The album_artist MediaStore field has existed since at least API 21, but until API * 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. * warning about using a possibly-unsupported constant.
*/ */
@Suppress("InlinedApi") @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 * A complete table of all the constant genre values for ID3(v2), including non-standard
* winamp extensions. * extensions.
*/ */
private val legacyGenreTable = arrayOf( private val genreConstantTable = arrayOf(
// ID3 Standard // ID3 Standard
"Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", "Hip-Hop", "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", "Hip-Hop",
"Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap", "Reggae", "Rock", "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", "New Wave", "Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi", "Tribal",
"Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll", "Hard Rock", "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", "Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion", "Bebob", "Latin",
"Revival", "Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", "Progressive Rock", "Revival", "Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", "Progressive Rock",
"Psychedelic Rock", "Symphonic Rock", "Slow Rock", "Big Band", "Chorus", "Psychedelic Rock", "Symphonic Rock", "Slow Rock", "Big Band", "Chorus",
@ -454,9 +461,8 @@ class MusicLoader {
"Crossover", "Contemporary Christian", "Christian Rock", "Merengue", "Salsa", "Crossover", "Contemporary Christian", "Christian Rock", "Merengue", "Salsa",
"Thrash Metal", "Anime", "JPop", "Synthpop", "Thrash Metal", "Anime", "JPop", "Synthpop",
// Winamp 5.6+ extensions, used by EasyTAG and friends // Winamp 5.6+ extensions, also used by EasyTAG.
// The only reason I include this set is because post-rock is a based genre and // I only include this because post-rock is a based genre and deserves a slot.
// deserves a slot.
"Abstract", "Art Rock", "Baroque", "Bhangra", "Big Beat", "Breakbeat", "Chillout", "Abstract", "Art Rock", "Baroque", "Bhangra", "Big Beat", "Breakbeat", "Chillout",
"Downtempo", "Dub", "EBM", "Eclectic", "Electro", "Electroclash", "Emo", "Experimental", "Downtempo", "Dub", "EBM", "Eclectic", "Electro", "Electroclash", "Emo", "Experimental",
"Garage", "Global", "IDM", "Illbient", "Industro-Goth", "Jam Band", "Krautrock", "Garage", "Global", "IDM", "Illbient", "Industro-Goth", "Jam Band", "Krautrock",

View file

@ -118,7 +118,7 @@ class MusicStore private constructor() {
/** /**
* A response that [MusicStore] returns when loading music. * A response that [MusicStore] returns when loading music.
* And before you ask, yes, I do like rust. * 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 { sealed class Response {
class Ok(val musicStore: MusicStore) : Response() class Ok(val musicStore: MusicStore) : Response()

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.excluded package org.oxycblt.auxio.music.excluded
import android.content.ContentValues import android.content.ContentValues
import android.content.Context import android.content.Context

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.excluded package org.oxycblt.auxio.music.excluded
import android.net.Uri import android.net.Uri
import android.os.Bundle 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 // 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 // Unless I change the system to use the drive/directory system, that is. But there's no
// demand for that. // 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") { if (typeAndPath[0] == "primary") {
return getRootPath() + "/" + typeAndPath.last() return getRootPath() + "/" + typeAndPath.last()
} }

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.excluded package org.oxycblt.auxio.music.excluded
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.view.ViewGroup import android.view.ViewGroup

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.excluded package org.oxycblt.auxio.music.excluded
import android.content.Context import android.content.Context
import androidx.lifecycle.LiveData 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 * ViewModel that acts as a wrapper around [ExcludedDatabase], allowing for the addition/removal
* of paths. Use [Factory] to instantiate this. * of paths. Use [Factory] to instantiate this.
* TODO: Unify with MusicViewModel
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewModel() { class ExcludedViewModel(private val excludedDatabase: ExcludedDatabase) : ViewModel() {

View file

@ -44,6 +44,10 @@ import org.oxycblt.auxio.util.logE
* **PLEASE Use this instead of [PlaybackStateManager], UI's are extremely volatile and this provides * **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.** * an interface that properly sanitizes input and abstracts functions unlike the master class.**
* @author OxygenCobalt * @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 { class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
// Playback // Playback
@ -170,7 +174,6 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
logD("Playing with uri $uri") logD("Playing with uri $uri")
val musicStore = MusicStore.maybeGetInstance() ?: return val musicStore = MusicStore.maybeGetInstance() ?: return
musicStore.findSongForUri(uri, context.contentResolver)?.let { song -> musicStore.findSongForUri(uri, context.contentResolver)?.let { song ->
playSong(song) playSong(song)
} }

View file

@ -40,8 +40,6 @@ import org.oxycblt.auxio.util.logE
* *
* All access should be done with [PlaybackStateManager.getInstance]. * All access should be done with [PlaybackStateManager.getInstance].
* @author OxygenCobalt * @author OxygenCobalt
*
* TODO: Rework this to possibly handle gapless playback and more refined queue management.
*/ */
class PlaybackStateManager private constructor() { class PlaybackStateManager private constructor() {
// Playback // Playback

View file

@ -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 * 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 * I fear that it may not work on OEM skins that for whatever reason don't make this
* action fire. * action fire.
* TODO: Figure out how players like Retro are able to get autoplay working with
* bluetooth headsets
*/ */
private fun maybeResumeFromPlug() { private fun maybeResumeFromPlug() {
if (playbackManager.song != null && if (playbackManager.song != null &&
@ -523,6 +525,8 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
/** /**
* Pause from a headset plug. * 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() { private fun pauseFromPlug() {
if (playbackManager.song != null) { if (playbackManager.song != null) {

View file

@ -32,7 +32,7 @@ import androidx.recyclerview.widget.RecyclerView
import coil.Coil import coil.Coil
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.accent.AccentCustomizeDialog 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.home.tabs.TabCustomizeDialog
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.settings.pref.IntListPrefDialog import org.oxycblt.auxio.settings.pref.IntListPrefDialog

View file

@ -27,14 +27,14 @@ import org.oxycblt.auxio.music.Item
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class DiffCallback<T : Item> : DiffUtil.ItemCallback<T>() { class DiffCallback<T : Item> : DiffUtil.ItemCallback<T>() {
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean {
return oldItem.hashCode() == newItem.hashCode()
}
override fun areItemsTheSame(oldItem: T, newItem: T): Boolean { override fun areItemsTheSame(oldItem: T, newItem: T): Boolean {
// Prevent ID collisions from occurring between datatypes. // Prevent ID collisions from occurring between datatypes.
if (oldItem.javaClass != newItem.javaClass) return false if (oldItem.javaClass != newItem.javaClass) return false
return oldItem.id == newItem.id return oldItem.id == newItem.id
} }
override fun areContentsTheSame(oldItem: T, newItem: T): Boolean {
return oldItem.hashCode() == newItem.hashCode()
}
} }

View file

@ -245,11 +245,8 @@ fun Context.hardRestart() {
// Instead of having to do a ton of cleanup and horrible code changes // 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 // 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. // one, after all] and then kill the application using exitProcess. Works well enough.
val intent = Intent(applicationContext, MainActivity::class.java).setFlags( val intent = Intent(applicationContext, MainActivity::class.java)
Intent.FLAG_ACTIVITY_CLEAR_TASK .setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
)
startActivity(intent) startActivity(intent)
exitProcess(0) exitProcess(0)
} }

View file

@ -17,7 +17,7 @@ org.gradle.jvmargs=-Xmx2048m
android.useAndroidX=true android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX # Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true android.enableJetifier=true
# Stop ExoPlayer from mangling AAR libraries with default abstract methods # Stop ExoPlayer AARs from being mangled
android.enableDexingArtifactTransform=false android.enableDexingArtifactTransform=false
# Kotlin code style for this project: "official" or "obsolete": # Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official kotlin.code.style=official

View file

@ -17,12 +17,12 @@ org.oxycblt.auxio # Main UIs
├──.coil # Image loading components ├──.coil # Image loading components
├──.detail # Album/Artist/Genre detail UIs ├──.detail # Album/Artist/Genre detail UIs
│ └──.recycler # RecyclerView components for detail UIs │ └──.recycler # RecyclerView components for detail UIs
├──.excluded # Excluded Directories UI + Systems
├──.home # Home UI ├──.home # Home UI
│ ├──.fastscroll # Fast scroller UI │ ├──.fastscroll # Fast scroller UI
│ ├──.list # Home item lists │ ├──.list # Home item lists
│ └──.tabs # Home tab customization │ └──.tabs # Home tab customization
├──.music # Music data and loading ├──.music # Music data and loading
│ └──.excluded # Excluded Directories UI + Systems
├──.playback # Playback UI + Systems ├──.playback # Playback UI + Systems
│ ├──.queue # Queue UI │ ├──.queue # Queue UI
│ ├──.state # Playback state backend │ ├──.state # Playback state backend
@ -86,10 +86,10 @@ should only be talking to other shared objects. All objects can use the utility
#### Data objects #### Data objects
Auxio represents data in multiple ways. 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: 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 - `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. 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, - `Header` and `ActionHeader` are UI data objects that represent a header item. `Header` corresponds to a simple header with no action,