diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3e47e546c..06d462b82 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,6 +15,7 @@ android:icon="@mipmap/ic_launcher" android:label="@string/info_app_name" android:roundIcon="@mipmap/ic_launcher_round" + android:exported="true" android:supportsRtl="true" android:theme="@style/Theme.Base"> () var albums = mutableListOf() + var artists = mutableListOf() var songs = mutableListOf() - private val resolver = app.contentResolver + private val resolver = context.contentResolver /** - * Begin the loading process. Resulting models are pushed to [genres], [albums], and [songs]. + * Begin the loading process. + * Resulting models are pushed to [genres], [artists], [albums], and [songs]. */ - fun loadMusic() { + fun load() { loadGenres() loadAlbums() loadSongs() + + linkAlbums() + buildArtists() + linkGenres() } private fun loadGenres() { @@ -60,14 +61,11 @@ class MusicLoader(private val app: Application) { genres.add(Genre(id, name)) } - - cursor.close() } logD("Genre search finished with ${genres.size} genres found.") } - @SuppressLint("InlinedApi") private fun loadAlbums() { logD("Starting album search...") @@ -83,8 +81,8 @@ class MusicLoader(private val app: Application) { Albums.DEFAULT_SORT_ORDER ) - val albumPlaceholder = app.getString(R.string.placeholder_album) - val artistPlaceholder = app.getString(R.string.placeholder_artist) + val albumPlaceholder = context.getString(R.string.placeholder_album) + val artistPlaceholder = context.getString(R.string.placeholder_artist) albumCursor?.use { cursor -> val idIndex = cursor.getColumnIndexOrThrow(Albums._ID) @@ -106,8 +104,6 @@ class MusicLoader(private val app: Application) { albums.add(Album(id, name, artistName, coverUri, year)) } - - cursor.close() } albums = albums.distinctBy { @@ -129,32 +125,30 @@ class MusicLoader(private val app: Application) { Media.TITLE, // 2 Media.ALBUM_ID, // 3 Media.TRACK, // 4 - Media.DURATION, // 5 + Media.DURATION // 5 ), - Media.IS_MUSIC + "=1", null, + "${Media.IS_MUSIC}=1", null, Media.DEFAULT_SORT_ORDER ) songCursor?.use { cursor -> val idIndex = cursor.getColumnIndexOrThrow(Media._ID) - val fileIndex = cursor.getColumnIndexOrThrow(Media.DISPLAY_NAME) val titleIndex = cursor.getColumnIndexOrThrow(Media.TITLE) + val fileIndex = cursor.getColumnIndexOrThrow(Media.DISPLAY_NAME) val albumIndex = cursor.getColumnIndexOrThrow(Media.ALBUM_ID) val trackIndex = cursor.getColumnIndexOrThrow(Media.TRACK) val durationIndex = cursor.getColumnIndexOrThrow(Media.DURATION) while (cursor.moveToNext()) { val id = cursor.getLong(idIndex) - val title = cursor.getString(titleIndex) val fileName = cursor.getString(fileIndex) + val title = cursor.getString(titleIndex) ?: fileName val albumId = cursor.getLong(albumIndex) val track = cursor.getInt(trackIndex) val duration = cursor.getLong(durationIndex) - songs.add(Song(id, title ?: fileName, fileName, albumId, track, duration)) + songs.add(Song(id, title, fileName, albumId, track, duration)) } - - cursor.close() } songs = songs.distinctBy { @@ -163,4 +157,97 @@ class MusicLoader(private val app: Application) { logD("Song search finished with ${songs.size} found") } + + private fun linkAlbums() { + logD("Linking albums") + + // Group up songs by their album ids and then link them with their albums + val songsByAlbum = songs.groupBy { it.albumId } + val unknownAlbum = Album( + name = context.getString(R.string.placeholder_album), + artistName = context.getString(R.string.placeholder_artist) + ) + + songsByAlbum.forEach { entry -> + (albums.find { it.id == entry.key } ?: unknownAlbum).linkSongs(entry.value) + } + + albums.removeAll { it.songs.isEmpty() } + + // If something goes horribly wrong and somehow songs are still not linked up by the + // album id, just throw them into an unknown album. + if (unknownAlbum.songs.isNotEmpty()) { + albums.add(unknownAlbum) + } + } + + private fun buildArtists() { + logD("Linking artists") + + // Group albums up by their artist name, should not result in any null-artist issues + val albumsByArtist = albums.groupBy { it.artistName } + + albumsByArtist.forEach { entry -> + artists.add( + // IDs are incremented from the minimum int value so that they remain unique. + Artist( + id = (artists.size + Int.MIN_VALUE).toLong(), + name = entry.key, albums = entry.value + ) + ) + } + + logD("Albums successfully linked into ${artists.size} artists") + } + + private fun linkGenres() { + logD("Linking genres") + + /* + * Okay, I'm going to go on a bit of a tangent here because this bit of code infuriates me. + * + * In an ideal world I should just be able to write MediaStore.Media.Audio.GENRE + * in the original song projection and then have it fetch the genre from the database, but + * no, why would ANYONE do that? Instead, I have to manually iterate through each genre, get + * A LIST OF SONGS FROM THEM, and then waste CPU cycles REPEATEDLY ITERATING through the + * songs list to LINK EACH SONG WITH THEIR GENRE. This is the bottleneck in my loader, + * without this code the load times drop from ~130ms to ~60ms, but of course I have to do + * this if I want an sensible genre system. Why is it this way? Nobody knows! Now this + * quirk is immortalized and has to be replicated in all future iterations of this API! Yay! + * + * I hate this platform so much. + */ + genres.forEach { genre -> + val songCursor = resolver.query( + Genres.Members.getContentUri("external", genre.id), + arrayOf(Genres.Members._ID), + null, null, null + ) + + songCursor?.use { cursor -> + val idIndex = cursor.getColumnIndexOrThrow(Genres.Members._ID) + + while (cursor.moveToNext()) { + val id = cursor.getLong(idIndex) + + songs.find { it.id == id }?.let { song -> + genre.linkSong(song) + } + } + } + } + + // Any songs without genres will be thrown into an unknown genre + val songsWithoutGenres = songs.filter { it.genre == null } + + if (songsWithoutGenres.isNotEmpty()) { + val unknownGenre = Genre(name = context.getString(R.string.placeholder_genre)) + + songsWithoutGenres.forEach { song -> + unknownGenre.linkSong(song) + } + + genres.add(unknownGenre) + } + } } 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 972edc5b6..c889cdb7e 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt @@ -8,8 +8,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.oxycblt.auxio.logD import org.oxycblt.auxio.logE -import org.oxycblt.auxio.music.processing.MusicLinker -import org.oxycblt.auxio.music.processing.MusicLoader import java.lang.Exception /** @@ -54,19 +52,16 @@ class MusicStore private constructor() { try { val loader = MusicLoader(app) - loader.loadMusic() + loader.load() if (loader.songs.isEmpty()) { return@withContext Response.NO_MUSIC } - val linker = MusicLinker(app, loader.songs, loader.albums, loader.genres) - linker.link() - - mSongs = linker.songs.toList() - mAlbums = linker.albums.toList() - mArtists = linker.artists.toList() - mGenres = linker.genres.toList() + mSongs = loader.songs + mAlbums = loader.albums + mArtists = loader.artists + mGenres = loader.genres this@MusicStore.logD( "Music load completed successfully in ${System.currentTimeMillis() - start}ms." diff --git a/app/src/main/java/org/oxycblt/auxio/music/processing/MusicLinker.kt b/app/src/main/java/org/oxycblt/auxio/music/processing/MusicLinker.kt deleted file mode 100644 index 869c91266..000000000 --- a/app/src/main/java/org/oxycblt/auxio/music/processing/MusicLinker.kt +++ /dev/null @@ -1,123 +0,0 @@ -package org.oxycblt.auxio.music.processing - -import android.content.Context -import android.provider.MediaStore.Audio.Genres -import org.oxycblt.auxio.R -import org.oxycblt.auxio.logD -import org.oxycblt.auxio.music.Album -import org.oxycblt.auxio.music.Artist -import org.oxycblt.auxio.music.Genre -import org.oxycblt.auxio.music.Song - -/** - * Object that links music data, such as grouping songs into their albums & genres and creating - * artists out of the albums. - * @author OxygenCobalt - */ -class MusicLinker( - private val context: Context, - val songs: MutableList, - val albums: MutableList, - val genres: MutableList -) { - private val resolver = context.contentResolver - val artists = mutableListOf() - - /** - * Begin the linking process. - * Modified models are pushed to [songs], [albums], [artists], and [genres] - */ - fun link() { - linkAlbums() - linkArtists() - linkGenres() - } - - private fun linkAlbums() { - logD("Linking albums") - - // Group up songs by their album ids and then link them with their albums - val songsByAlbum = songs.groupBy { it.albumId } - val unknownAlbum = Album( - name = context.getString(R.string.placeholder_album), - artistName = context.getString(R.string.placeholder_artist) - ) - - songsByAlbum.forEach { entry -> - (albums.find { it.id == entry.key } ?: unknownAlbum).linkSongs(entry.value) - } - - // If something goes horribly wrong and somehow songs are still not linked up by the - // album id, just throw them into an unknown album. - if (unknownAlbum.songs.isNotEmpty()) { - albums.add(unknownAlbum) - } - } - - private fun linkArtists() { - logD("Linking artists") - - // Group albums up by their artist name, should not result in any null-artist issues - val albumsByArtist = albums.groupBy { it.artistName } - - albumsByArtist.forEach { entry -> - artists.add( - Artist( - id = (artists.size + Int.MIN_VALUE).toLong(), - name = entry.key, albums = entry.value - ) - ) - } - - logD("Albums successfully linked into ${artists.size} artists") - } - - private fun linkGenres() { - logD("Linking genres") - - /* - * Okay, I'm going to go on a bit of a tangent here because this bit of code infuriates me. - * - * In an ideal world I should just be able to write MediaStore.Media.Audio.GENRE - * in the original song projection and then have it fetch the genre from the database, but - * no, why would ANYONE do that? Instead, I have to manually iterate through each genre, get - * A LIST OF SONGS FROM THEM, and then waste CPU cycles REPEATEDLY ITERATING through the - * songs list to LINK EACH SONG WITH THEIR GENRE. Why is it this way? Nobody knows! Now this - * quirk is immortalized and has to be replicated in all future iterations of this API! Yay! - * - * I hate this platform so much. - */ - genres.forEach { genre -> - val songCursor = resolver.query( - Genres.Members.getContentUri("external", genre.id), - arrayOf(Genres.Members._ID), - null, null, null - ) - - songCursor?.use { cursor -> - val idIndex = cursor.getColumnIndexOrThrow(Genres.Members._ID) - - while (cursor.moveToNext()) { - val id = cursor.getLong(idIndex) - - songs.find { it.id == id }?.let { song -> - genre.linkSong(song) - } - } - } - } - - // Any songs without genres will be thrown into an unknown genre - val songsWithoutGenres = songs.filter { it.genre == null } - - if (songsWithoutGenres.isNotEmpty()) { - val unknownGenre = Genre(name = context.getString(R.string.placeholder_genre)) - - songsWithoutGenres.forEach { song -> - unknownGenre.linkSong(song) - } - - genres.add(unknownGenre) - } - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/songs/CobaltScrollThumb.kt b/app/src/main/java/org/oxycblt/auxio/songs/CobaltScrollThumb.kt index 7121e4093..d33b9a801 100644 --- a/app/src/main/java/org/oxycblt/auxio/songs/CobaltScrollThumb.kt +++ b/app/src/main/java/org/oxycblt/auxio/songs/CobaltScrollThumb.kt @@ -5,6 +5,7 @@ import android.graphics.drawable.GradientDrawable import android.os.Build import android.util.AttributeSet import android.view.MotionEvent +import android.view.View import android.view.ViewGroup import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout @@ -66,11 +67,11 @@ class CobaltScrollThumb @JvmOverloads constructor( } } - isVisible = false + visibility = View.INVISIBLE isActivated = false post { - isVisible = true + visibility = View.VISIBLE } }