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:
parent
82247775ac
commit
627ab97948
16 changed files with 140 additions and 124 deletions
|
@ -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") {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<Song> {
|
||||
var songs = mutableListOf<Song>()
|
||||
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<Song> {
|
||||
val blacklistDatabase = ExcludedDatabase.getInstance(context)
|
||||
var selector = "${MediaStore.Audio.Media.IS_MUSIC}=1"
|
||||
val args = mutableListOf<String>()
|
||||
|
||||
// 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<Song>()
|
||||
|
||||
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<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 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<Album>): List<Artist> {
|
||||
val artists = mutableListOf<Artist>()
|
||||
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<Song>): List<Genre> {
|
||||
val genres = mutableListOf<Genre>()
|
||||
|
||||
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<Song>): List<Song>? {
|
||||
val genreSongs = mutableListOf<Song>()
|
||||
|
||||
// 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",
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* 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.Context
|
|
@ -16,7 +16,7 @@
|
|||
* 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.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()
|
||||
}
|
|
@ -16,7 +16,7 @@
|
|||
* 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.view.ViewGroup
|
|
@ -16,7 +16,7 @@
|
|||
* 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 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() {
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -27,14 +27,14 @@ import org.oxycblt.auxio.music.Item
|
|||
* @author OxygenCobalt
|
||||
*/
|
||||
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 {
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
This is required due to `RemoteView` limitations.
|
||||
|
|
Loading…
Reference in a new issue