diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index 4dada7534..88538beab 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -33,11 +33,11 @@ import org.oxycblt.auxio.list.BasicHeader import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.MusicStore -import org.oxycblt.auxio.music.extractor.AudioInfo +import org.oxycblt.auxio.music.format.AudioInfo +import org.oxycblt.auxio.music.format.Disc +import org.oxycblt.auxio.music.format.ReleaseType import org.oxycblt.auxio.music.library.Library import org.oxycblt.auxio.music.library.Sort -import org.oxycblt.auxio.music.tags.Disc -import org.oxycblt.auxio.music.tags.ReleaseType import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.util.* diff --git a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt index ea663824c..45779f597 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt @@ -27,7 +27,7 @@ import androidx.navigation.fragment.navArgs import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogSongDetailBinding import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.music.extractor.AudioInfo +import org.oxycblt.auxio.music.format.AudioInfo import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.util.androidActivityViewModels diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt index 2fb514f2d..a9735e385 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt @@ -33,7 +33,7 @@ import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter import org.oxycblt.auxio.list.adapter.SimpleDiffCallback import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.music.tags.Disc +import org.oxycblt.auxio.music.format.Disc import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.getPlural diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index ba7d0aee4..3489f085e 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -31,13 +31,13 @@ import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import org.oxycblt.auxio.R import org.oxycblt.auxio.list.Item +import org.oxycblt.auxio.music.format.Date +import org.oxycblt.auxio.music.format.Disc +import org.oxycblt.auxio.music.format.ReleaseType import org.oxycblt.auxio.music.library.Sort import org.oxycblt.auxio.music.parsing.parseId3GenreNames import org.oxycblt.auxio.music.parsing.parseMultiValue import org.oxycblt.auxio.music.storage.* -import org.oxycblt.auxio.music.tags.Date -import org.oxycblt.auxio.music.tags.Disc -import org.oxycblt.auxio.music.tags.ReleaseType import org.oxycblt.auxio.util.nonZeroOrNull import org.oxycblt.auxio.util.unlikelyToBeNull diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/CacheExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/CacheExtractor.kt index 9c34e2af3..22b69e887 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/CacheExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/CacheExtractor.kt @@ -17,16 +17,21 @@ package org.oxycblt.auxio.music.extractor -import android.content.ContentValues import android.content.Context -import android.database.sqlite.SQLiteDatabase -import android.database.sqlite.SQLiteOpenHelper -import androidx.core.database.getIntOrNull -import androidx.core.database.getStringOrNull +import androidx.room.Dao +import androidx.room.Database +import androidx.room.Entity +import androidx.room.Insert +import androidx.room.PrimaryKey +import androidx.room.Query +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.TypeConverter +import androidx.room.TypeConverters import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.music.format.Date import org.oxycblt.auxio.music.parsing.correctWhitespace import org.oxycblt.auxio.music.parsing.splitEscaped -import org.oxycblt.auxio.music.tags.Date import org.oxycblt.auxio.util.* /** @@ -37,14 +42,14 @@ import org.oxycblt.auxio.util.* */ interface CacheExtractor { /** Initialize the Extractor by reading the cache data into memory. */ - fun init() + suspend fun init() /** * Finalize the Extractor by writing the newly-loaded [Song.Raw]s back into the cache, alongside * freeing up memory. * @param rawSongs The songs to write into the cache. */ - fun finalize(rawSongs: List) + suspend fun finalize(rawSongs: List) /** * Use the cache to populate the given [Song.Raw]. @@ -54,6 +59,21 @@ interface CacheExtractor { * [ExtractionResult.PARSED] is not returned. */ fun populate(rawSong: Song.Raw): ExtractionResult + + companion object { + /** + * Create an instance with optional read-capacity. + * @param context [Context] required. + * @param readable Whether the new [CacheExtractor] should be able to read cached entries. + * @return A new [CacheExtractor] with the specified configuration. + */ + fun from(context: Context, readable: Boolean): CacheExtractor = + if (readable) { + ReadWriteCacheExtractor(context) + } else { + WriteOnlyCacheExtractor(context) + } + } } /** @@ -63,15 +83,18 @@ interface CacheExtractor { * @see CacheExtractor * @author Alexander Capehart (OxygenCobalt) */ -open class WriteOnlyCacheExtractor(private val context: Context) : CacheExtractor { - override fun init() { +private open class WriteOnlyCacheExtractor(private val context: Context) : CacheExtractor { + protected val cacheDao: CacheDao by lazy { CacheDatabase.getInstance(context).cacheDao() } + + override suspend fun init() { // Nothing to do. } - override fun finalize(rawSongs: List) { + override suspend fun finalize(rawSongs: List) { try { // Still write out whatever data was extracted. - CacheDatabase.getInstance(context).write(rawSongs) + cacheDao.nukeCache() + cacheDao.insertCache(rawSongs.map(CachedSong::fromRaw)) } catch (e: Exception) { logE("Unable to save cache database.") logE(e.stackTraceToString()) @@ -89,22 +112,28 @@ open class WriteOnlyCacheExtractor(private val context: Context) : CacheExtracto * @see CacheExtractor * @author Alexander Capehart */ -class ReadWriteCacheExtractor(private val context: Context) : WriteOnlyCacheExtractor(context) { - private var cacheMap: Map? = null +private class ReadWriteCacheExtractor(private val context: Context) : + WriteOnlyCacheExtractor(context) { + private var cacheMap: Map? = null private var invalidate = false - override fun init() { + override suspend fun init() { try { // Faster to load the whole database into memory than do a query on each // populate call. - cacheMap = CacheDatabase.getInstance(context).read() + val cache = cacheDao.readCache() + cacheMap = buildMap { + for (cachedSong in cache) { + put(cachedSong.mediaStoreId, cachedSong) + } + } } catch (e: Exception) { logE("Unable to load cache database.") logE(e.stackTraceToString()) } } - override fun finalize(rawSongs: List) { + override suspend fun finalize(rawSongs: List) { cacheMap = null // Same some time by not re-writing the cache if we were able to create the entire // library from it. If there is even just one song we could not populate from the @@ -122,38 +151,11 @@ class ReadWriteCacheExtractor(private val context: Context) : WriteOnlyCacheExtr // addition and modification timestamps. Technically the addition timestamp doesn't // exist, but to safeguard against possible OEM-specific timestamp incoherence, we // check for it anyway. - val cachedRawSong = map[rawSong.mediaStoreId] - if (cachedRawSong != null && - cachedRawSong.dateAdded == rawSong.dateAdded && - cachedRawSong.dateModified == rawSong.dateModified) { - // No built-in "copy from" method for data classes, just have to assign - // the data ourselves. - rawSong.musicBrainzId = cachedRawSong.musicBrainzId - rawSong.name = cachedRawSong.name - rawSong.sortName = cachedRawSong.sortName - - rawSong.size = cachedRawSong.size - rawSong.durationMs = cachedRawSong.durationMs - - rawSong.track = cachedRawSong.track - rawSong.disc = cachedRawSong.disc - rawSong.date = cachedRawSong.date - - rawSong.albumMusicBrainzId = cachedRawSong.albumMusicBrainzId - rawSong.albumName = cachedRawSong.albumName - rawSong.albumSortName = cachedRawSong.albumSortName - rawSong.releaseTypes = cachedRawSong.releaseTypes - - rawSong.artistMusicBrainzIds = cachedRawSong.artistMusicBrainzIds - rawSong.artistNames = cachedRawSong.artistNames - rawSong.artistSortNames = cachedRawSong.artistSortNames - - rawSong.albumArtistMusicBrainzIds = cachedRawSong.albumArtistMusicBrainzIds - rawSong.albumArtistNames = cachedRawSong.albumArtistNames - rawSong.albumArtistSortNames = cachedRawSong.albumArtistSortNames - - rawSong.genreNames = cachedRawSong.genreNames - + val cachedSong = map[rawSong.mediaStoreId] + if (cachedSong != null && + cachedSong.dateAdded == rawSong.dateAdded && + cachedSong.dateModified == rawSong.dateModified) { + cachedSong.copyToRaw(rawSong) return ExtractionResult.CACHED } @@ -164,311 +166,170 @@ class ReadWriteCacheExtractor(private val context: Context) : WriteOnlyCacheExtr } } -/** - * Internal [Song.Raw] cache database. - * @author Alexander Capehart (OxygenCobalt) - * @see [CacheExtractor] - */ -private class CacheDatabase(context: Context) : - SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) { - override fun onCreate(db: SQLiteDatabase) { - // Map the cacheable raw song fields to database fields. Cache-able in this context - // means information independent of the file-system, excluding IDs and timestamps required - // to retrieve items from the cache. - db.createTable(TABLE_RAW_SONGS) { - append("${Columns.MEDIA_STORE_ID} LONG PRIMARY KEY,") - append("${Columns.DATE_ADDED} LONG NOT NULL,") - append("${Columns.DATE_MODIFIED} LONG NOT NULL,") - append("${Columns.SIZE} LONG NOT NULL,") - append("${Columns.DURATION} LONG NOT NULL,") - append("${Columns.MUSIC_BRAINZ_ID} STRING,") - append("${Columns.NAME} STRING NOT NULL,") - append("${Columns.SORT_NAME} STRING,") - append("${Columns.TRACK} INT,") - append("${Columns.DISC} INT,") - append("${Columns.SUBTITLE} STRING,") - append("${Columns.DATE} STRING,") - append("${Columns.ALBUM_MUSIC_BRAINZ_ID} STRING,") - append("${Columns.ALBUM_NAME} STRING NOT NULL,") - append("${Columns.ALBUM_SORT_NAME} STRING,") - append("${Columns.RELEASE_TYPES} STRING,") - append("${Columns.ARTIST_MUSIC_BRAINZ_IDS} STRING,") - append("${Columns.ARTIST_NAMES} STRING,") - append("${Columns.ARTIST_SORT_NAMES} STRING,") - append("${Columns.ALBUM_ARTIST_MUSIC_BRAINZ_IDS} STRING,") - append("${Columns.ALBUM_ARTIST_NAMES} STRING,") - append("${Columns.ALBUM_ARTIST_SORT_NAMES} STRING,") - append("${Columns.GENRE_NAMES} STRING") - } - } - - override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) = nuke(db) - - override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) = nuke(db) - - private fun nuke(db: SQLiteDatabase) { - // No cost to nuking this database, only causes higher loading times. - logD("Nuking database") - db.apply { - execSQL("DROP TABLE IF EXISTS $TABLE_RAW_SONGS") - onCreate(this) - } - } - - /** - * Read out this database into memory. - * @return A mapping between the MediaStore IDs of the cache entries and a [Song.Raw] containing - * the cacheable data for the entry. Note that any filesystem-dependent information (excluding - * IDs and timestamps) is not cached. - */ - fun read(): Map { - requireBackgroundThread() - val start = System.currentTimeMillis() - val map = mutableMapOf() - readableDatabase.queryAll(TABLE_RAW_SONGS) { cursor -> - if (cursor.count == 0) { - // Nothing to do. - return@queryAll - } - - val idIndex = cursor.getColumnIndexOrThrow(Columns.MEDIA_STORE_ID) - val dateAddedIndex = cursor.getColumnIndexOrThrow(Columns.DATE_ADDED) - val dateModifiedIndex = cursor.getColumnIndexOrThrow(Columns.DATE_MODIFIED) - - val sizeIndex = cursor.getColumnIndexOrThrow(Columns.SIZE) - val durationIndex = cursor.getColumnIndexOrThrow(Columns.DURATION) - - val musicBrainzIdIndex = cursor.getColumnIndexOrThrow(Columns.MUSIC_BRAINZ_ID) - val nameIndex = cursor.getColumnIndexOrThrow(Columns.NAME) - val sortNameIndex = cursor.getColumnIndexOrThrow(Columns.SORT_NAME) - - val trackIndex = cursor.getColumnIndexOrThrow(Columns.TRACK) - val discIndex = cursor.getColumnIndexOrThrow(Columns.DISC) - val subtitleIndex = cursor.getColumnIndex(Columns.SUBTITLE) - val dateIndex = cursor.getColumnIndexOrThrow(Columns.DATE) - - val albumMusicBrainzIdIndex = - cursor.getColumnIndexOrThrow(Columns.ALBUM_MUSIC_BRAINZ_ID) - val albumNameIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_NAME) - val albumSortNameIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_SORT_NAME) - val releaseTypesIndex = cursor.getColumnIndexOrThrow(Columns.RELEASE_TYPES) - - val artistMusicBrainzIdsIndex = - cursor.getColumnIndexOrThrow(Columns.ARTIST_MUSIC_BRAINZ_IDS) - val artistNamesIndex = cursor.getColumnIndexOrThrow(Columns.ARTIST_NAMES) - val artistSortNamesIndex = cursor.getColumnIndexOrThrow(Columns.ARTIST_SORT_NAMES) - - val albumArtistMusicBrainzIdsIndex = - cursor.getColumnIndexOrThrow(Columns.ALBUM_ARTIST_MUSIC_BRAINZ_IDS) - val albumArtistNamesIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_ARTIST_NAMES) - val albumArtistSortNamesIndex = - cursor.getColumnIndexOrThrow(Columns.ALBUM_ARTIST_SORT_NAMES) - - val genresIndex = cursor.getColumnIndexOrThrow(Columns.GENRE_NAMES) - - while (cursor.moveToNext()) { - val raw = Song.Raw() - val id = cursor.getLong(idIndex) - - raw.mediaStoreId = id - raw.dateAdded = cursor.getLong(dateAddedIndex) - raw.dateModified = cursor.getLong(dateModifiedIndex) - - raw.size = cursor.getLong(sizeIndex) - raw.durationMs = cursor.getLong(durationIndex) - - raw.musicBrainzId = cursor.getStringOrNull(musicBrainzIdIndex) - raw.name = cursor.getString(nameIndex) - raw.sortName = cursor.getStringOrNull(sortNameIndex) - - raw.track = cursor.getIntOrNull(trackIndex) - raw.disc = cursor.getIntOrNull(discIndex) - raw.subtitle = cursor.getStringOrNull(subtitleIndex) - raw.date = cursor.getStringOrNull(dateIndex)?.let(Date::from) - - raw.albumMusicBrainzId = cursor.getStringOrNull(albumMusicBrainzIdIndex) - raw.albumName = cursor.getString(albumNameIndex) - raw.albumSortName = cursor.getStringOrNull(albumSortNameIndex) - cursor.getStringOrNull(releaseTypesIndex)?.let { - raw.releaseTypes = it.parseSQLMultiValue() - } - - cursor.getStringOrNull(artistMusicBrainzIdsIndex)?.let { - raw.artistMusicBrainzIds = it.parseSQLMultiValue() - } - cursor.getStringOrNull(artistNamesIndex)?.let { - raw.artistNames = it.parseSQLMultiValue() - } - cursor.getStringOrNull(artistSortNamesIndex)?.let { - raw.artistSortNames = it.parseSQLMultiValue() - } - - cursor.getStringOrNull(albumArtistMusicBrainzIdsIndex)?.let { - raw.albumArtistMusicBrainzIds = it.parseSQLMultiValue() - } - cursor.getStringOrNull(albumArtistNamesIndex)?.let { - raw.albumArtistNames = it.parseSQLMultiValue() - } - cursor.getStringOrNull(albumArtistSortNamesIndex)?.let { - raw.albumArtistSortNames = it.parseSQLMultiValue() - } - - cursor.getStringOrNull(genresIndex)?.let { - raw.genreNames = it.parseSQLMultiValue() - } - - map[id] = raw - } - } - - logD("Read cache in ${System.currentTimeMillis() - start}ms") - - return map - } - - /** - * Write a new list of [Song.Raw] to this database. - * @param rawSongs The new [Song.Raw] instances to cache. Note that any filesystem-dependent - * information (excluding IDs and timestamps) is not cached. - */ - fun write(rawSongs: List) { - val start = System.currentTimeMillis() - - writableDatabase.writeList(rawSongs, TABLE_RAW_SONGS) { _, rawSong -> - ContentValues(22).apply { - put(Columns.MEDIA_STORE_ID, rawSong.mediaStoreId) - put(Columns.DATE_ADDED, rawSong.dateAdded) - put(Columns.DATE_MODIFIED, rawSong.dateModified) - - put(Columns.SIZE, rawSong.size) - put(Columns.DURATION, rawSong.durationMs) - - put(Columns.MUSIC_BRAINZ_ID, rawSong.musicBrainzId) - put(Columns.NAME, rawSong.name) - put(Columns.SORT_NAME, rawSong.sortName) - - put(Columns.TRACK, rawSong.track) - put(Columns.DISC, rawSong.disc) - put(Columns.SUBTITLE, rawSong.subtitle) - put(Columns.DATE, rawSong.date?.toString()) - - put(Columns.ALBUM_MUSIC_BRAINZ_ID, rawSong.albumMusicBrainzId) - put(Columns.ALBUM_NAME, rawSong.albumName) - put(Columns.ALBUM_SORT_NAME, rawSong.albumSortName) - put(Columns.RELEASE_TYPES, rawSong.releaseTypes.toSQLMultiValue()) - - put(Columns.ARTIST_MUSIC_BRAINZ_IDS, rawSong.artistMusicBrainzIds.toSQLMultiValue()) - put(Columns.ARTIST_NAMES, rawSong.artistNames.toSQLMultiValue()) - put(Columns.ARTIST_SORT_NAMES, rawSong.artistSortNames.toSQLMultiValue()) - - put( - Columns.ALBUM_ARTIST_MUSIC_BRAINZ_IDS, - rawSong.albumArtistMusicBrainzIds.toSQLMultiValue()) - put(Columns.ALBUM_ARTIST_NAMES, rawSong.albumArtistNames.toSQLMultiValue()) - put(Columns.ALBUM_ARTIST_SORT_NAMES, rawSong.albumArtistSortNames.toSQLMultiValue()) - - put(Columns.GENRE_NAMES, rawSong.genreNames.toSQLMultiValue()) - } - } - - logD("Wrote cache in ${System.currentTimeMillis() - start}ms") - } - - // SQLite does not natively support multiple values, so we have to serialize multi-value - // tags with separators. Not ideal, but nothing we can do. - - /** - * Transforms the multi-string list into a SQL-safe multi-string value. - * @return A single string containing all values within the multi-string list, delimited by a - * ";". Pre-existing ";" characters will be escaped. - */ - private fun List.toSQLMultiValue() = - if (isNotEmpty()) { - joinToString(";") { it.replace(";", "\\;") } - } else { - null - } - - /** - * Transforms the SQL-safe multi-string value into a multi-string list. - * @return A list of strings corresponding to the delimited values present within the original - * string. Escaped delimiters are converted back into their normal forms. - */ - private fun String.parseSQLMultiValue() = splitEscaped { it == ';' }.correctWhitespace() - - /** Defines the columns used in this database. */ - private object Columns { - /** @see Song.Raw.mediaStoreId */ - const val MEDIA_STORE_ID = "msid" - /** @see Song.Raw.dateAdded */ - const val DATE_ADDED = "date_added" - /** @see Song.Raw.dateModified */ - const val DATE_MODIFIED = "date_modified" - /** @see Song.Raw.size */ - const val SIZE = "size" - /** @see Song.Raw.durationMs */ - const val DURATION = "duration" - /** @see Song.Raw.musicBrainzId */ - const val MUSIC_BRAINZ_ID = "mbid" - /** @see Song.Raw.name */ - const val NAME = "name" - /** @see Song.Raw.sortName */ - const val SORT_NAME = "sort_name" - /** @see Song.Raw.track */ - const val TRACK = "track" - /** @see Song.Raw.disc */ - const val DISC = "disc" - /** @see Song.Raw.subtitle */ - const val SUBTITLE = "subtitle" - /** @see Song.Raw.date */ - const val DATE = "date" - /** @see Song.Raw.albumMusicBrainzId */ - const val ALBUM_MUSIC_BRAINZ_ID = "album_mbid" - /** @see Song.Raw.albumName */ - const val ALBUM_NAME = "album" - /** @see Song.Raw.albumSortName */ - const val ALBUM_SORT_NAME = "album_sort" - /** @see Song.Raw.releaseTypes */ - const val RELEASE_TYPES = "album_types" - /** @see Song.Raw.artistMusicBrainzIds */ - const val ARTIST_MUSIC_BRAINZ_IDS = "artists_mbid" - /** @see Song.Raw.artistNames */ - const val ARTIST_NAMES = "artists" - /** @see Song.Raw.artistSortNames */ - const val ARTIST_SORT_NAMES = "artists_sort" - /** @see Song.Raw.albumArtistMusicBrainzIds */ - const val ALBUM_ARTIST_MUSIC_BRAINZ_IDS = "album_artists_mbid" - /** @see Song.Raw.albumArtistNames */ - const val ALBUM_ARTIST_NAMES = "album_artists" - /** @see Song.Raw.albumArtistSortNames */ - const val ALBUM_ARTIST_SORT_NAMES = "album_artists_sort" - /** @see Song.Raw.genreNames */ - const val GENRE_NAMES = "genres" - } +@Database(entities = [CachedSong::class], version = 27, exportSchema = false) +private abstract class CacheDatabase : RoomDatabase() { + abstract fun cacheDao(): CacheDao companion object { - private const val DB_NAME = "auxio_music_cache.db" - private const val DB_VERSION = 3 - private const val TABLE_RAW_SONGS = "raw_songs" - @Volatile private var INSTANCE: CacheDatabase? = null /** - * Get a singleton instance. - * @return The (possibly newly-created) singleton instance. + * Get/create the shared instance of this database. + * @param context [Context] required. */ fun getInstance(context: Context): CacheDatabase { - val currentInstance = INSTANCE - - if (currentInstance != null) { - return currentInstance + val instance = INSTANCE + if (instance != null) { + return instance } synchronized(this) { - val newInstance = CacheDatabase(context.applicationContext) + val newInstance = + Room.databaseBuilder( + context, CacheDatabase::class.java, "auxio_metadata_cache.db") + .fallbackToDestructiveMigration() + .fallbackToDestructiveMigrationFrom(0) + .fallbackToDestructiveMigrationOnDowngrade() + .build() INSTANCE = newInstance return newInstance } } } } + +@Dao +private interface CacheDao { + @Query("SELECT * FROM ${CachedSong.TABLE_NAME}") suspend fun readCache(): List + + @Query("DELETE FROM ${CachedSong.TABLE_NAME}") suspend fun nukeCache() + + @Insert suspend fun insertCache(songs: List) +} + +@Entity(tableName = CachedSong.TABLE_NAME) +@TypeConverters(CachedSong.Converters::class) +private data class CachedSong( + /** + * The ID of the [Song]'s audio file, obtained from MediaStore. Note that this ID is highly + * unstable and should only be used for accessing the audio file. + */ + @PrimaryKey var mediaStoreId: Long, + /** @see Song.dateAdded */ + var dateAdded: Long, + /** The latest date the [Song]'s audio file was modified, as a unix epoch timestamp. */ + var dateModified: Long, + /** @see Song.size */ + var size: Long? = null, + /** @see Song.durationMs */ + var durationMs: Long, + /** @see Music.UID */ + var musicBrainzId: String? = null, + /** @see Music.rawName */ + var name: String, + /** @see Music.rawSortName */ + var sortName: String? = null, + /** @see Song.track */ + var track: Int? = null, + /** @see Disc.number */ + var disc: Int? = null, + /** @See Disc.name */ + var subtitle: String? = null, + /** @see Song.date */ + var date: Date? = null, + /** @see Album.Raw.musicBrainzId */ + var albumMusicBrainzId: String? = null, + /** @see Album.Raw.name */ + var albumName: String, + /** @see Album.Raw.sortName */ + var albumSortName: String? = null, + /** @see Album.Raw.releaseType */ + var releaseTypes: List = listOf(), + /** @see Artist.Raw.musicBrainzId */ + var artistMusicBrainzIds: List = listOf(), + /** @see Artist.Raw.name */ + var artistNames: List = listOf(), + /** @see Artist.Raw.sortName */ + var artistSortNames: List = listOf(), + /** @see Artist.Raw.musicBrainzId */ + var albumArtistMusicBrainzIds: List = listOf(), + /** @see Artist.Raw.name */ + var albumArtistNames: List = listOf(), + /** @see Artist.Raw.sortName */ + var albumArtistSortNames: List = listOf(), + /** @see Genre.Raw.name */ + var genreNames: List = listOf() +) { + fun copyToRaw(rawSong: Song.Raw): CachedSong { + rawSong.musicBrainzId = musicBrainzId + rawSong.name = name + rawSong.sortName = sortName + + rawSong.size = size + rawSong.durationMs = durationMs + + rawSong.track = track + rawSong.disc = disc + rawSong.date = date + + rawSong.albumMusicBrainzId = albumMusicBrainzId + rawSong.albumName = albumName + rawSong.albumSortName = albumSortName + rawSong.releaseTypes = releaseTypes + + rawSong.artistMusicBrainzIds = artistMusicBrainzIds + rawSong.artistNames = artistNames + rawSong.artistSortNames = artistSortNames + + rawSong.albumArtistMusicBrainzIds = albumArtistMusicBrainzIds + rawSong.albumArtistNames = albumArtistNames + rawSong.albumArtistSortNames = albumArtistSortNames + + rawSong.genreNames = genreNames + return this + } + + object Converters { + @TypeConverter + fun fromMultiValue(values: List) = + values.joinToString(";") { it.replace(";", "\\;") } + + @TypeConverter + fun toMultiValue(string: String) = string.splitEscaped { it == ';' }.correctWhitespace() + + @TypeConverter fun fromDate(date: Date?) = date?.toString() + + @TypeConverter fun toDate(string: String?) = string?.let(Date::from) + } + + companion object { + const val TABLE_NAME = "cached_songs" + + fun fromRaw(rawSong: Song.Raw) = + CachedSong( + mediaStoreId = + requireNotNull(rawSong.mediaStoreId) { "Invalid raw: No MediaStore ID" }, + dateAdded = requireNotNull(rawSong.dateAdded) { "Invalid raw: No date added" }, + dateModified = + requireNotNull(rawSong.dateModified) { "Invalid raw: No date modified" }, + musicBrainzId = rawSong.musicBrainzId, + name = requireNotNull(rawSong.name) { "Invalid raw: No name" }, + sortName = rawSong.sortName, + size = rawSong.size, + durationMs = requireNotNull(rawSong.durationMs) { "Invalid raw: No duration" }, + track = rawSong.track, + disc = rawSong.disc, + date = rawSong.date, + albumMusicBrainzId = rawSong.albumMusicBrainzId, + albumName = requireNotNull(rawSong.albumName) { "Invalid raw: No album name" }, + albumSortName = rawSong.albumSortName, + releaseTypes = rawSong.releaseTypes, + artistMusicBrainzIds = rawSong.artistMusicBrainzIds, + artistNames = rawSong.artistNames, + artistSortNames = rawSong.artistSortNames, + albumArtistMusicBrainzIds = rawSong.albumArtistMusicBrainzIds, + albumArtistNames = rawSong.albumArtistNames, + albumArtistSortNames = rawSong.albumArtistSortNames, + genreNames = rawSong.genreNames) + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreExtractor.kt index 211d20a4b..f67dc5b23 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/MediaStoreExtractor.kt @@ -29,6 +29,7 @@ import androidx.core.database.getStringOrNull import java.io.File import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.music.format.Date import org.oxycblt.auxio.music.parsing.parseId3v2PositionField import org.oxycblt.auxio.music.parsing.transformPositionField import org.oxycblt.auxio.music.storage.Directory @@ -38,7 +39,6 @@ import org.oxycblt.auxio.music.storage.mediaStoreVolumeNameCompat import org.oxycblt.auxio.music.storage.safeQuery import org.oxycblt.auxio.music.storage.storageVolumesCompat import org.oxycblt.auxio.music.storage.useQuery -import org.oxycblt.auxio.music.tags.Date import org.oxycblt.auxio.util.getSystemServiceCompat import org.oxycblt.auxio.util.logD @@ -83,7 +83,7 @@ abstract class MediaStoreExtractor( * the media database for music files. * @return A [Cursor] of the music data returned from the database. */ - open fun init(): Cursor { + open suspend fun init(): Cursor { val start = System.currentTimeMillis() cacheExtractor.init() val musicSettings = MusicSettings.from(context) @@ -195,7 +195,7 @@ abstract class MediaStoreExtractor( * freeing up memory. * @param rawSongs The songs to write into the cache. */ - fun finalize(rawSongs: List) { + open suspend fun finalize(rawSongs: List) { // Free the cursor (and it's resources) cursor?.close() cursor = null @@ -325,12 +325,27 @@ abstract class MediaStoreExtractor( genreNamesMap[raw.mediaStoreId]?.let { raw.genreNames = listOf(it) } } - private companion object { + companion object { + /** + * Create a framework-backed instance. + * @param context [Context] required. + * @param cacheExtractor [CacheExtractor] to wrap. + * @return A new [MediaStoreExtractor] that will work best on the device's API level. + */ + fun from(context: Context, cacheExtractor: CacheExtractor) = + when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> + Api30MediaStoreExtractor(context, cacheExtractor) + Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> + Api29MediaStoreExtractor(context, cacheExtractor) + else -> Api21MediaStoreExtractor(context, cacheExtractor) + } + /** * The base selector that works across all versions of android. Does not exclude * directories. */ - const val BASE_SELECTOR = "NOT ${MediaStore.Audio.Media.SIZE}=0" + private const val BASE_SELECTOR = "NOT ${MediaStore.Audio.Media.SIZE}=0" /** * The album artist of a song. This column has existed since at least API 21, but until API @@ -338,13 +353,13 @@ abstract class MediaStoreExtractor( * versions that Auxio supports. */ @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 /** * The external volume. This naming has existed since API 21, but no constant existed for it * until API 29. This will work on all versions that Auxio supports. */ - @Suppress("InlinedApi") const val VOLUME_EXTERNAL = MediaStore.VOLUME_EXTERNAL + @Suppress("InlinedApi") private const val VOLUME_EXTERNAL = MediaStore.VOLUME_EXTERNAL } } @@ -358,12 +373,12 @@ abstract class MediaStoreExtractor( * @param cacheExtractor [CacheExtractor] implementation for cache optimizations. * @author Alexander Capehart (OxygenCobalt) */ -class Api21MediaStoreExtractor(context: Context, cacheExtractor: CacheExtractor) : +private class Api21MediaStoreExtractor(context: Context, cacheExtractor: CacheExtractor) : MediaStoreExtractor(context, cacheExtractor) { private var trackIndex = -1 private var dataIndex = -1 - override fun init(): Cursor { + override suspend fun init(): Cursor { val cursor = super.init() // Set up cursor indices for later use. trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK) @@ -438,12 +453,12 @@ class Api21MediaStoreExtractor(context: Context, cacheExtractor: CacheExtractor) * @author Alexander Capehart (OxygenCobalt) */ @RequiresApi(Build.VERSION_CODES.Q) -open class BaseApi29MediaStoreExtractor(context: Context, cacheExtractor: CacheExtractor) : +private open class BaseApi29MediaStoreExtractor(context: Context, cacheExtractor: CacheExtractor) : MediaStoreExtractor(context, cacheExtractor) { private var volumeIndex = -1 private var relativePathIndex = -1 - override fun init(): Cursor { + override suspend fun init(): Cursor { val cursor = super.init() // Set up cursor indices for later use. volumeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.VOLUME_NAME) @@ -501,11 +516,11 @@ open class BaseApi29MediaStoreExtractor(context: Context, cacheExtractor: CacheE * @author Alexander Capehart (OxygenCobalt) */ @RequiresApi(Build.VERSION_CODES.Q) -open class Api29MediaStoreExtractor(context: Context, cacheExtractor: CacheExtractor) : +private open class Api29MediaStoreExtractor(context: Context, cacheExtractor: CacheExtractor) : BaseApi29MediaStoreExtractor(context, cacheExtractor) { private var trackIndex = -1 - override fun init(): Cursor { + override suspend fun init(): Cursor { val cursor = super.init() // Set up cursor indices for later use. trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK) @@ -536,12 +551,12 @@ open class Api29MediaStoreExtractor(context: Context, cacheExtractor: CacheExtra * @author Alexander Capehart (OxygenCobalt) */ @RequiresApi(Build.VERSION_CODES.R) -class Api30MediaStoreExtractor(context: Context, cacheExtractor: CacheExtractor) : +private class Api30MediaStoreExtractor(context: Context, cacheExtractor: CacheExtractor) : BaseApi29MediaStoreExtractor(context, cacheExtractor) { private var trackIndex: Int = -1 private var discIndex: Int = -1 - override fun init(): Cursor { + override suspend fun init(): Cursor { val cursor = super.init() // Set up cursor indices for later use. trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER) diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt index 91880466b..fc03351b7 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/extractor/MetadataExtractor.kt @@ -23,10 +23,11 @@ import com.google.android.exoplayer2.MediaItem import com.google.android.exoplayer2.MetadataRetriever import kotlinx.coroutines.flow.flow import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.music.format.Date +import org.oxycblt.auxio.music.format.TextTags import org.oxycblt.auxio.music.parsing.parseId3v2PositionField import org.oxycblt.auxio.music.parsing.parseVorbisPositionField import org.oxycblt.auxio.music.storage.toAudioUri -import org.oxycblt.auxio.music.tags.Date import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logW @@ -53,14 +54,14 @@ class MetadataExtractor( * relies on. * @return The amount of music that is expected to be loaded. */ - fun init() = mediaStoreExtractor.init().count + suspend fun init() = mediaStoreExtractor.init().count /** * Finalize the Extractor by writing the newly-loaded [Song.Raw]s back into the cache, alongside * freeing up memory. * @param rawSongs The songs to write into the cache. */ - fun finalize(rawSongs: List) = mediaStoreExtractor.finalize(rawSongs) + suspend fun finalize(rawSongs: List) = mediaStoreExtractor.finalize(rawSongs) /** * Returns a flow that parses all [Song.Raw] instances queued by the sub-extractors. This will diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/AudioInfo.kt b/app/src/main/java/org/oxycblt/auxio/music/format/AudioInfo.kt similarity index 99% rename from app/src/main/java/org/oxycblt/auxio/music/extractor/AudioInfo.kt rename to app/src/main/java/org/oxycblt/auxio/music/format/AudioInfo.kt index 22f38d040..380d5ff77 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/AudioInfo.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/format/AudioInfo.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.extractor +package org.oxycblt.auxio.music.format import android.content.Context import android.media.MediaExtractor diff --git a/app/src/main/java/org/oxycblt/auxio/music/tags/Date.kt b/app/src/main/java/org/oxycblt/auxio/music/format/Date.kt similarity index 99% rename from app/src/main/java/org/oxycblt/auxio/music/tags/Date.kt rename to app/src/main/java/org/oxycblt/auxio/music/format/Date.kt index d3658ce6f..9fd14f95e 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/tags/Date.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/format/Date.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.tags +package org.oxycblt.auxio.music.format import android.content.Context import java.text.ParseException diff --git a/app/src/main/java/org/oxycblt/auxio/music/tags/Disc.kt b/app/src/main/java/org/oxycblt/auxio/music/format/Disc.kt similarity index 96% rename from app/src/main/java/org/oxycblt/auxio/music/tags/Disc.kt rename to app/src/main/java/org/oxycblt/auxio/music/format/Disc.kt index f6f19f97c..2a2b18bc5 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/tags/Disc.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/format/Disc.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.tags +package org.oxycblt.auxio.music.format import org.oxycblt.auxio.list.Item diff --git a/app/src/main/java/org/oxycblt/auxio/music/tags/ReleaseType.kt b/app/src/main/java/org/oxycblt/auxio/music/format/ReleaseType.kt similarity index 99% rename from app/src/main/java/org/oxycblt/auxio/music/tags/ReleaseType.kt rename to app/src/main/java/org/oxycblt/auxio/music/format/ReleaseType.kt index 3331fda7a..4c21d66b8 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/tags/ReleaseType.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/format/ReleaseType.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.tags +package org.oxycblt.auxio.music.format import org.oxycblt.auxio.R diff --git a/app/src/main/java/org/oxycblt/auxio/music/extractor/TextTags.kt b/app/src/main/java/org/oxycblt/auxio/music/format/TextTags.kt similarity index 98% rename from app/src/main/java/org/oxycblt/auxio/music/extractor/TextTags.kt rename to app/src/main/java/org/oxycblt/auxio/music/format/TextTags.kt index 493a3421e..2365319ae 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/extractor/TextTags.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/format/TextTags.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.extractor +package org.oxycblt.auxio.music.format import com.google.android.exoplayer2.metadata.Metadata import com.google.android.exoplayer2.metadata.id3.InternalFrame diff --git a/app/src/main/java/org/oxycblt/auxio/music/library/Sort.kt b/app/src/main/java/org/oxycblt/auxio/music/library/Sort.kt index f419d3747..32c02c53d 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/library/Sort.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/library/Sort.kt @@ -22,9 +22,9 @@ import kotlin.math.max import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.R import org.oxycblt.auxio.music.* +import org.oxycblt.auxio.music.format.Date +import org.oxycblt.auxio.music.format.Disc import org.oxycblt.auxio.music.library.Sort.Mode -import org.oxycblt.auxio.music.tags.Date -import org.oxycblt.auxio.music.tags.Disc /** * A sorting method. diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt b/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt index 45215a8d3..14b604325 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt @@ -24,6 +24,7 @@ import android.os.Build import androidx.core.content.ContextCompat import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import kotlinx.coroutines.yield import org.oxycblt.auxio.BuildConfig @@ -205,20 +206,8 @@ class Indexer private constructor() { // Create the chain of extractors. Each extractor builds on the previous and // enables version-specific features in order to create the best possible music // experience. - val cacheDatabase = - if (withCache) { - ReadWriteCacheExtractor(context) - } else { - WriteOnlyCacheExtractor(context) - } - val mediaStoreExtractor = - when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> - Api30MediaStoreExtractor(context, cacheDatabase) - Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> - Api29MediaStoreExtractor(context, cacheDatabase) - else -> Api21MediaStoreExtractor(context, cacheDatabase) - } + val cacheExtractor = CacheExtractor.from(context, withCache) + val mediaStoreExtractor = MediaStoreExtractor.from(context, cacheExtractor) val metadataExtractor = MetadataExtractor(context, mediaStoreExtractor) val rawSongs = loadRawSongs(metadataExtractor).ifEmpty { throw NoMusicException() } // Build the rest of the music library from the song list. This is much more powerful diff --git a/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceConverters.kt b/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceConverters.kt index 395a22e16..d3ce62be9 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceConverters.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceConverters.kt @@ -19,7 +19,6 @@ package org.oxycblt.auxio.playback.persist import androidx.room.TypeConverter import org.oxycblt.auxio.music.Music -import org.oxycblt.auxio.playback.state.RepeatMode /** * Defines conversions used in the persistence table. @@ -29,6 +28,6 @@ object PersistenceConverters { /** @see [Music.UID.toString] */ @TypeConverter fun fromMusicUID(uid: Music.UID?) = uid?.toString() - /** @see [Music.UID.fromString]*/ + /** @see [Music.UID.fromString] */ @TypeConverter fun toMusicUid(string: String?) = string?.let(Music.UID::fromString) } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceDatabase.kt b/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceDatabase.kt index ad0aa1351..767103873 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/persist/PersistenceDatabase.kt @@ -37,7 +37,7 @@ import org.oxycblt.auxio.playback.state.RepeatMode */ @Database( entities = [PlaybackState::class, QueueHeapItem::class, QueueMappingItem::class], - version = 1, + version = 27, exportSchema = false) @TypeConverters(PersistenceConverters::class) abstract class PersistenceDatabase : RoomDatabase() { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt index 3a2a39893..ec84ef92a 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt @@ -28,7 +28,7 @@ import com.google.android.exoplayer2.util.MimeTypes import java.nio.ByteBuffer import kotlin.math.pow import org.oxycblt.auxio.music.Album -import org.oxycblt.auxio.music.extractor.TextTags +import org.oxycblt.auxio.music.format.TextTags import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.util.logD diff --git a/app/src/main/java/org/oxycblt/auxio/util/DbUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/DbUtil.kt deleted file mode 100644 index fc029f105..000000000 --- a/app/src/main/java/org/oxycblt/auxio/util/DbUtil.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (c) 2022 Auxio Project - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.oxycblt.auxio.util - -import android.content.ContentValues -import android.database.Cursor -import android.database.sqlite.SQLiteDatabase -import androidx.core.database.sqlite.transaction - -/** - * Query all columns in the given [SQLiteDatabase] table, running the block when the [Cursor] is - * loaded. The block will be called with [use], allowing for automatic cleanup of [Cursor] - * resources. - * @param tableName The name of the table to query all columns in. - * @param block The code block to run with the loaded [Cursor]. - */ -inline fun SQLiteDatabase.queryAll(tableName: String, block: (Cursor) -> R) = - query(tableName, null, null, null, null, null, null)?.use(block) - -/** - * Create a table in an [SQLiteDatabase], if it does not already exist. - * @param name The name of the table to create. - * @param schema A block that adds a comma-separated list of SQL column declarations. - */ -inline fun SQLiteDatabase.createTable(name: String, schema: StringBuilder.() -> StringBuilder) { - val command = StringBuilder().append("CREATE TABLE IF NOT EXISTS $name(").schema().append(")") - execSQL(command.toString()) -} - -/** - * Safely write a list of items to an [SQLiteDatabase]. This will clear the prior list and write as - * much of the new list as possible. - * @param list The list of items to write. - * @param tableName The name of the table to write the items to. - * @param transform Code to transform an item into a corresponding [ContentValues] to the given - * table. - */ -inline fun SQLiteDatabase.writeList( - list: List, - tableName: String, - transform: (Int, T) -> ContentValues -) { - // Clear any prior items in the table. - transaction { delete(tableName, null, null) } - - var transactionPosition = 0 - while (transactionPosition < list.size) { - // Start at the current transaction position, if a transaction failed at any point, - // this value can be used to immediately start at the next item and continue writing - // the list without error. - var i = transactionPosition - transaction { - while (i < list.size) { - val values = transform(i, list[i]) - // Increment forward now so that if this insert fails, the transaction position - // will still start at the next i. - i++ - insert(tableName, null, values) - } - } - transactionPosition = i - logD( - "Wrote batch of ${T::class.simpleName} instances. " + - "Position is now at $transactionPosition") - } -} diff --git a/app/src/test/java/org/oxycblt/auxio/music/MusicTest.kt b/app/src/test/java/org/oxycblt/auxio/music/MusicTest.kt index d1262e11f..28d78a9ed 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/MusicTest.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/MusicTest.kt @@ -21,7 +21,7 @@ import java.util.* import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test -import org.oxycblt.auxio.music.tags.Date +import org.oxycblt.auxio.music.format.Date class MusicTest { @Test diff --git a/app/src/test/java/org/oxycblt/auxio/music/extractor/TextTagsTest.kt b/app/src/test/java/org/oxycblt/auxio/music/extractor/TextTagsTest.kt index fbc6a1707..7fcd53592 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/extractor/TextTagsTest.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/extractor/TextTagsTest.kt @@ -26,6 +26,7 @@ import com.google.android.exoplayer2.metadata.vorbis.VorbisComment import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test +import org.oxycblt.auxio.music.format.TextTags class TextTagsTest { @Test diff --git a/app/src/test/java/org/oxycblt/auxio/music/tags/DateTest.kt b/app/src/test/java/org/oxycblt/auxio/music/tags/DateTest.kt index 658b37e97..fd7e6d352 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/tags/DateTest.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/tags/DateTest.kt @@ -20,6 +20,7 @@ package org.oxycblt.auxio.music.tags import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test +import org.oxycblt.auxio.music.format.Date class DateTest { @Test diff --git a/app/src/test/java/org/oxycblt/auxio/music/tags/DiscTest.kt b/app/src/test/java/org/oxycblt/auxio/music/tags/DiscTest.kt index 622216922..3e43f564f 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/tags/DiscTest.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/tags/DiscTest.kt @@ -20,6 +20,7 @@ package org.oxycblt.auxio.music.tags import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test +import org.oxycblt.auxio.music.format.Disc class DiscTest { @Test diff --git a/app/src/test/java/org/oxycblt/auxio/music/tags/ReleaseTypeTest.kt b/app/src/test/java/org/oxycblt/auxio/music/tags/ReleaseTypeTest.kt index 6187fbb0e..1e6dde974 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/tags/ReleaseTypeTest.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/tags/ReleaseTypeTest.kt @@ -19,6 +19,7 @@ package org.oxycblt.auxio.music.tags import org.junit.Assert.assertEquals import org.junit.Test +import org.oxycblt.auxio.music.format.ReleaseType class ReleaseTypeTest { @Test