music: clean implementation

Clean up the music implementation heavily, removing redundant code and
splitting off some code into utilities.
This commit is contained in:
OxygenCobalt 2022-05-28 14:49:18 -06:00
parent 2576fb26ba
commit 87bdf50d39
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
8 changed files with 181 additions and 123 deletions

View file

@ -19,7 +19,6 @@
package org.oxycblt.auxio.music package org.oxycblt.auxio.music
import android.content.ContentUris
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.provider.MediaStore import android.provider.MediaStore
@ -63,6 +62,8 @@ data class Song(
override val rawName: String, override val rawName: String,
/** The file name of this song, excluding the full path. */ /** The file name of this song, excluding the full path. */
val fileName: String, val fileName: String,
/** The URI linking to this song's file. */
val uri: Uri,
/** The total duration of this song, in millis. */ /** The total duration of this song, in millis. */
val durationMs: Long, val durationMs: Long,
/** The track number of this song, null if there isn't any. */ /** The track number of this song, null if there isn't any. */
@ -70,19 +71,17 @@ data class Song(
/** The disc number of this song, null if there isn't any. */ /** The disc number of this song, null if there isn't any. */
val disc: Int?, val disc: Int?,
/** Internal field. Do not use. */ /** Internal field. Do not use. */
val _mediaStoreId: Long, val _year: Int?,
/** Internal field. Do not use. */ /** Internal field. Do not use. */
val _mediaStoreYear: Int?, val _albumName: String,
/** Internal field. Do not use. */ /** Internal field. Do not use. */
val _mediaStoreAlbumName: String, val _albumCoverUri: Uri,
/** Internal field. Do not use. */ /** Internal field. Do not use. */
val _mediaStoreAlbumId: Long, val _artistName: String?,
/** Internal field. Do not use. */ /** Internal field. Do not use. */
val _mediaStoreArtistName: String?, val _albumArtistName: String?,
/** Internal field. Do not use. */ /** Internal field. Do not use. */
val _mediaStoreAlbumArtistName: String?, val _genreName: String?
/** Internal field. Do not use. */
val _mediaStoreGenreName: String?
) : Music() { ) : Music() {
override val id: Long override val id: Long
get() { get() {
@ -100,11 +99,6 @@ data class Song(
override fun resolveName(context: Context) = rawName override fun resolveName(context: Context) = rawName
/** The URI for this song. */
val uri: Uri
get() =
ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, _mediaStoreId)
/** The duration of this song, in seconds (rounded down) */ /** The duration of this song, in seconds (rounded down) */
val durationSecs: Long val durationSecs: Long
get() = durationMs / 1000 get() = durationMs / 1000
@ -124,26 +118,27 @@ data class Song(
* back to the album artist tag (i.e parent artist name). Null if name is unknown. * back to the album artist tag (i.e parent artist name). Null if name is unknown.
*/ */
val individualRawArtistName: String? val individualRawArtistName: String?
get() = _mediaStoreArtistName ?: album.artist.rawName get() = _artistName ?: album.artist.rawName
/** /**
* Resolve the artist name for this song in particular. First uses the artist tag, and then * Resolve the artist name for this song in particular. First uses the artist tag, and then
* falls back to the album artist tag (i.e parent artist name) * falls back to the album artist tag (i.e parent artist name)
*/ */
fun resolveIndividualArtistName(context: Context) = fun resolveIndividualArtistName(context: Context) =
_mediaStoreArtistName ?: album.artist.resolveName(context) _artistName ?: album.artist.resolveName(context)
/** Internal field. Do not use. */ /** Internal field. Do not use. */
val _albumGroupingId: Long val _albumGroupingId: Long
get() { get() {
var result = _artistGroupingName.lowercase().hashCode().toLong() var result =
result = 31 * result + _mediaStoreAlbumName.lowercase().hashCode() (_artistGroupingName?.lowercase() ?: MediaStore.UNKNOWN_STRING).hashCode().toLong()
result = 31 * result + _albumName.lowercase().hashCode()
return result return result
} }
/** Internal field. Do not use. */ /** Internal field. Do not use. */
val _artistGroupingName: String val _artistGroupingName: String?
get() = _mediaStoreAlbumArtistName ?: _mediaStoreArtistName ?: MediaStore.UNKNOWN_STRING get() = _albumArtistName ?: _artistName
/** Internal field. Do not use. */ /** Internal field. Do not use. */
val _isMissingAlbum: Boolean val _isMissingAlbum: Boolean
@ -176,7 +171,7 @@ data class Album(
/** The songs of this album. */ /** The songs of this album. */
override val songs: List<Song>, override val songs: List<Song>,
/** Internal field. Do not use. */ /** Internal field. Do not use. */
val _artistGroupingName: String, val _artistGroupingName: String?,
) : MusicParent() { ) : MusicParent() {
init { init {
for (song in songs) { for (song in songs) {
@ -204,7 +199,7 @@ data class Album(
/** Internal field. Do not use. */ /** Internal field. Do not use. */
val _artistGroupingId: Long val _artistGroupingId: Long
get() = _artistGroupingName.lowercase().hashCode().toLong() get() = (_artistGroupingName?.lowercase() ?: MediaStore.UNKNOWN_STRING).hashCode().toLong()
/** Internal field. Do not use. */ /** Internal field. Do not use. */
val _isMissingArtist: Boolean val _isMissingArtist: Boolean

View file

@ -30,6 +30,7 @@ import org.oxycblt.auxio.music.indexer.Indexer
import org.oxycblt.auxio.util.contentResolverSafe import org.oxycblt.auxio.util.contentResolverSafe
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logE
import org.oxycblt.auxio.util.useQuery
/** /**
* The main storage for music items. Getting an instance of this object is more complicated as it * The main storage for music items. Getting an instance of this object is more complicated as it
@ -117,17 +118,15 @@ class MusicStore private constructor() {
* @return The corresponding [Song] for this [uri], null if there isn't one. * @return The corresponding [Song] for this [uri], null if there isn't one.
*/ */
fun findSongForUri(context: Context, uri: Uri): Song? { fun findSongForUri(context: Context, uri: Uri): Song? {
context.contentResolverSafe return context.contentResolverSafe.useQuery(
.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null) uri, arrayOf(OpenableColumns.DISPLAY_NAME)) { cursor ->
?.use { cursor ->
cursor.moveToFirst() cursor.moveToFirst()
val fileName =
val displayName =
cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)) cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
return songs.find { it.fileName == fileName } songs.find { it.fileName == displayName }
} }
return null
} }
} }

View file

@ -25,6 +25,9 @@ import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
/**
* A [ViewModel] that represents the current music indexing state.
*/
class MusicViewModel : ViewModel(), MusicStore.Callback { class MusicViewModel : ViewModel(), MusicStore.Callback {
private val musicStore = MusicStore.getInstance() private val musicStore = MusicStore.getInstance()

View file

@ -17,10 +17,8 @@
package org.oxycblt.auxio.music.indexer package org.oxycblt.auxio.music.indexer
import android.content.ContentUris
import android.content.Context import android.content.Context
import android.database.Cursor import android.database.Cursor
import android.net.Uri
import android.os.Build import android.os.Build
import android.provider.MediaStore import android.provider.MediaStore
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
@ -62,7 +60,7 @@ object Indexer {
} }
/** /**
* Does the initial query over the song database, including excluded directory checks. The songs * Does the initial query over the song database using [backend]. The songs
* returned by this function are **not** well-formed. The companion [buildAlbums], * returned by this function are **not** well-formed. The companion [buildAlbums],
* [buildArtists], and [buildGenres] functions must be called with the returned list so that all * [buildArtists], and [buildGenres] functions must be called with the returned list so that all
* songs are properly linked up. * songs are properly linked up.
@ -74,10 +72,10 @@ object Indexer {
songs = songs =
songs.distinctBy { songs.distinctBy {
it.rawName to it.rawName to
it._mediaStoreAlbumName to it._albumName to
it._mediaStoreArtistName to it._artistName to
it._mediaStoreAlbumArtistName to it._albumArtistName to
it._mediaStoreGenreName to it._genreName to
it.track to it.track to
it.disc to it.disc to
it.durationMs it.durationMs
@ -122,22 +120,13 @@ object Indexer {
} }
} }
val albumName = templateSong._mediaStoreAlbumName
val albumYear = templateSong._mediaStoreYear
val albumCoverUri =
ContentUris.withAppendedId(
Uri.parse("content://media/external/audio/albumart"),
templateSong._mediaStoreAlbumId)
val artistName = templateSong._artistGroupingName
albums.add( albums.add(
Album( Album(
albumName, rawName = templateSong._albumName,
albumYear, year = templateSong._year,
albumCoverUri, albumCoverUri = templateSong._albumCoverUri,
albumSongs, _artistGroupingName = templateSong._artistGroupingName,
artistName, songs = entry.value))
))
} }
logD("Successfully built ${albums.size} albums") logD("Successfully built ${albums.size} albums")
@ -154,16 +143,13 @@ object Indexer {
val albumsByArtist = albums.groupBy { it._artistGroupingId } val albumsByArtist = albums.groupBy { it._artistGroupingId }
for (entry in albumsByArtist) { for (entry in albumsByArtist) {
// The first album will suffice for template metadata.
val templateAlbum = entry.value[0] val templateAlbum = entry.value[0]
val artistName =
if (templateAlbum._artistGroupingName != MediaStore.UNKNOWN_STRING) {
templateAlbum._artistGroupingName
} else {
null
}
val artistAlbums = entry.value
artists.add(Artist(artistName, artistAlbums)) artists.add(Artist(
rawName = templateAlbum._artistGroupingName,
albums = entry.value
))
} }
logD("Successfully built ${artists.size} artists") logD("Successfully built ${artists.size} artists")
@ -172,16 +158,15 @@ object Indexer {
} }
/** /**
* Read all genres and link them up to the given songs. This is the code that requires me to * Build genres and link them to their particular songs.
* make dozens of useless queries just to link genres up.
*/ */
private fun buildGenres(songs: List<Song>): List<Genre> { private fun buildGenres(songs: List<Song>): List<Genre> {
val genres = mutableListOf<Genre>() val genres = mutableListOf<Genre>()
val songsByGenre = songs.groupBy { it._mediaStoreGenreName?.hashCode() } val songsByGenre = songs.groupBy { it._genreName?.hashCode() }
for (entry in songsByGenre) { for (entry in songsByGenre) {
val templateSong = entry.value[0] val templateSong = entry.value[0]
genres.add(Genre(templateSong._mediaStoreGenreName, entry.value)) genres.add(Genre(rawName = templateSong._genreName, songs = entry.value))
} }
logD("Successfully built ${genres.size} genres") logD("Successfully built ${genres.size} genres")
@ -190,7 +175,10 @@ object Indexer {
} }
interface Backend { interface Backend {
/** Query the media database for an initial cursor. */
fun query(context: Context): Cursor fun query(context: Context): Cursor
/** Create a list of songs from the [Cursor] queried in [query]. */
fun loadSongs(context: Context, cursor: Cursor): Collection<Song> fun loadSongs(context: Context, cursor: Cursor): Collection<Song>
} }
} }

View file

@ -17,8 +17,10 @@
package org.oxycblt.auxio.music.indexer package org.oxycblt.auxio.music.indexer
import android.content.ContentUris
import android.content.Context import android.content.Context
import android.database.Cursor import android.database.Cursor
import android.net.Uri
import android.os.Build import android.os.Build
import android.provider.MediaStore import android.provider.MediaStore
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
@ -27,10 +29,12 @@ import androidx.core.database.getStringOrNull
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.excluded.ExcludedDatabase import org.oxycblt.auxio.music.excluded.ExcludedDatabase
import org.oxycblt.auxio.util.contentResolverSafe import org.oxycblt.auxio.util.contentResolverSafe
import org.oxycblt.auxio.util.queryCursor
import org.oxycblt.auxio.util.useQuery
/* /*
* This class acts as the base for most the black magic required to get a remotely sensible music * This file acts as the base for most the black magic required to get a remotely sensible music
* indexing system while still optimizing for time. I would recommend you leave this module now * indexing system while still optimizing for time. I would recommend you leave this file now
* before you lose your sanity trying to understand the hoops I had to jump through for this system, * before you lose your sanity trying to understand the hoops I had to jump through for this system,
* but if you really want to stay, here's a debrief on why this code is so awful. * but if you really want to stay, here's a debrief on why this code is so awful.
* *
@ -88,6 +92,11 @@ import org.oxycblt.auxio.util.contentResolverSafe
* I wish I was born in the neolithic. * I wish I was born in the neolithic.
*/ */
/**
* Represents a [Indexer.Backend] that loads music from the media database ([MediaStore]). This is
* not a fully-featured class by itself, and it's API-specific derivatives should be used instead.
* @author OxygenCobalt
*/
abstract class MediaStoreBackend : Indexer.Backend { abstract class MediaStoreBackend : Indexer.Backend {
private var idIndex = -1 private var idIndex = -1
private var titleIndex = -1 private var titleIndex = -1
@ -115,12 +124,11 @@ abstract class MediaStoreBackend : Indexer.Backend {
} }
return requireNotNull( return requireNotNull(
context.contentResolverSafe.query( context.contentResolverSafe.queryCursor(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
projection, projection,
selector, selector,
args.toTypedArray(), args.toTypedArray())) { "Content resolver failure: No Cursor returned" }
null)) { "Content resolver failure: No Cursor returned" }
} }
override fun loadSongs(context: Context, cursor: Cursor): Collection<Song> { override fun loadSongs(context: Context, cursor: Cursor): Collection<Song> {
@ -129,14 +137,13 @@ abstract class MediaStoreBackend : Indexer.Backend {
audios.add(buildAudio(context, cursor)) audios.add(buildAudio(context, cursor))
} }
context.contentResolverSafe // The audio is not actually complete at this point, as we cannot obtain a genre
.query( // through a song query. Instead, we have to do the hack where we iterate through
// every genre and assign it's name to each component song.
context.contentResolverSafe.useQuery(
MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI, MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI,
arrayOf(MediaStore.Audio.Genres._ID, MediaStore.Audio.Genres.NAME), arrayOf(MediaStore.Audio.Genres._ID, MediaStore.Audio.Genres.NAME)) { genreCursor ->
null,
null,
null)
?.use { genreCursor ->
val idIndex = genreCursor.getColumnIndexOrThrow(MediaStore.Audio.Genres._ID) val idIndex = genreCursor.getColumnIndexOrThrow(MediaStore.Audio.Genres._ID)
val nameIndex = genreCursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME) val nameIndex = genreCursor.getColumnIndexOrThrow(MediaStore.Audio.Genres.NAME)
@ -153,20 +160,19 @@ abstract class MediaStoreBackend : Indexer.Backend {
return audios.map { it.toSong() } return audios.map { it.toSong() }
} }
/**
* Links up the given genre data ([genreId] and [genreName]) to the child audios connected to
* [genreId].
*/
private fun linkGenreAudios( private fun linkGenreAudios(
context: Context, context: Context,
genreId: Long, genreId: Long,
genreName: String, genreName: String,
audios: List<Audio> audios: List<Audio>
) { ) {
context.contentResolverSafe context.contentResolverSafe.useQuery(
.query( MediaStore.Audio.Genres.Members.getContentUri(VOLUME_EXTERNAL, genreId),
MediaStore.Audio.Genres.Members.getContentUri("external", genreId), arrayOf(MediaStore.Audio.Genres.Members._ID)) { cursor ->
arrayOf(MediaStore.Audio.Genres.Members._ID),
null,
null,
null)
?.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()) {
@ -176,9 +182,18 @@ abstract class MediaStoreBackend : Indexer.Backend {
} }
} }
/**
* The projection to use when querying media. Add version-specific columns here in our
* implementation.
*/
open val projection: Array<String> open val projection: Array<String>
get() = PROJECTION get() = BASE_PROJECTION
/**
* Build an [Audio] based on the current cursor values. Each implementation should try to obtain
* an upstream [Audio] first, and then populate it with version-specific fields outlined in
* [projection].
*/
open fun buildAudio(context: Context, cursor: Cursor): Audio { open fun buildAudio(context: Context, cursor: Cursor): Audio {
// Initialize our cursor indices if we haven't already. // Initialize our cursor indices if we haven't already.
if (idIndex == -1) { if (idIndex == -1) {
@ -254,18 +269,22 @@ abstract class MediaStoreBackend : Indexer.Backend {
) { ) {
fun toSong(): Song = fun toSong(): Song =
Song( Song(
requireNotNull(title) { "Malformed song: No title" }, rawName = requireNotNull(title) { "Malformed audio: No title" },
requireNotNull(displayName) { "Malformed song: No file name" }, fileName = requireNotNull(displayName) { "Malformed audio: No file name" },
requireNotNull(duration) { "Malformed song: No duration" }, uri = ContentUris.withAppendedId(
track, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
disc, requireNotNull(id) { "Malformed audio: No song id" }),
requireNotNull(id) { "Malformed song: No song id" }, durationMs = requireNotNull(duration) { "Malformed audio: No duration" },
year, track = track,
requireNotNull(album) { "Malformed song: No album name" }, disc = disc,
requireNotNull(albumId) { "Malformed song: No album id" }, _year = year,
artist, _albumName = requireNotNull(album) { "Malformed song: No album name" },
albumArtist, _albumCoverUri = ContentUris.withAppendedId(
genre) EXTERNAL_ALBUM_ART_URI,
requireNotNull(albumId) { "Malformed song: No album id" }),
_artistName = artist,
_albumArtistName = albumArtist,
_genreName = genre)
} }
companion object { companion object {
@ -278,7 +297,23 @@ abstract class MediaStoreBackend : Indexer.Backend {
@Suppress("InlinedApi") @Suppress("InlinedApi")
private const val AUDIO_COLUMN_ALBUM_ARTIST = MediaStore.Audio.AudioColumns.ALBUM_ARTIST private const val AUDIO_COLUMN_ALBUM_ARTIST = MediaStore.Audio.AudioColumns.ALBUM_ARTIST
private val PROJECTION = /**
* External has existed since at least API 21, but no constant existed for it until API 29.
* This constant is safe to use.
*/
@Suppress("InlinedApi") private const val VOLUME_EXTERNAL = MediaStore.VOLUME_EXTERNAL
/**
* For some reason the album art URI namespace does not have a member in [MediaStore], but
* it still works since at least API 21.
*/
private val EXTERNAL_ALBUM_ART_URI = Uri.parse("content://media/external/audio/albumart")
/**
* The basic projection that works across all versions of android. Is incomplete, hence why
* sub-implementations should be used instead.
*/
private val BASE_PROJECTION =
arrayOf( arrayOf(
MediaStore.Audio.AudioColumns._ID, MediaStore.Audio.AudioColumns._ID,
MediaStore.Audio.AudioColumns.TITLE, MediaStore.Audio.AudioColumns.TITLE,
@ -293,6 +328,10 @@ abstract class MediaStoreBackend : Indexer.Backend {
} }
} }
/**
* A [MediaStoreBackend] that completes the music loading process in a way compatible from
* @author OxygenCobalt
*/
class Api21MediaStoreBackend : MediaStoreBackend() { class Api21MediaStoreBackend : MediaStoreBackend() {
private var trackIndex = -1 private var trackIndex = -1
@ -325,6 +364,11 @@ class Api21MediaStoreBackend : MediaStoreBackend() {
} }
} }
/**
* A [MediaStoreBackend] that completes the music loading process in a way compatible with at least
* API 30.
* @author OxygenCobalt
*/
@RequiresApi(Build.VERSION_CODES.R) @RequiresApi(Build.VERSION_CODES.R)
class Api30MediaStoreBackend : MediaStoreBackend() { class Api30MediaStoreBackend : MediaStoreBackend() {
private var trackIndex: Int = -1 private var trackIndex: Int = -1
@ -350,16 +394,16 @@ class Api30MediaStoreBackend : MediaStoreBackend() {
// N is the number and T is the total. Parse the number while leaving out the // N is the number and T is the total. Parse the number while leaving out the
// total, as we have no use for it. // total, as we have no use for it.
cursor cursor.getStringOrNull(trackIndex)?.no?.let { audio.track = it }
.getStringOrNull(trackIndex) cursor.getStringOrNull(discIndex)?.no?.let { audio.disc = it }
?.split('/', limit = 2)
?.getOrNull(0)
?.toIntOrNull()
?.let { audio.track = it }
cursor.getStringOrNull(discIndex)?.split('/', limit = 2)?.getOrNull(0)?.toIntOrNull()?.let {
audio.disc = it
}
return audio return audio
} }
/**
* Parse out the number field from an NN/TT string that is typically found in DISC_NUMBER and
* CD_TRACK_NUMBER.
*/
private val String.no: Int?
get() = split('/', limit = 2).getOrNull(0)?.toIntOrNull()
} }

View file

@ -73,6 +73,10 @@ constructor(
// to jump around). // to jump around).
if (value <= durationSecs && !isActivated) { if (value <= durationSecs && !isActivated) {
binding.seekBarSlider.value = value.toFloat() binding.seekBarSlider.value = value.toFloat()
// We would want to keep this in the callback, but the callback only fires when
// a value changes completely, and sometimes that does not happen with this view.
binding.seekBarPosition.textSafe = value.formatDuration(true)
} }
} }

View file

@ -133,10 +133,15 @@ class ReplayGainAudioProcessor : BaseAudioProcessor() {
val value: String val value: String
when (entry) { when (entry) {
// ID3v2 text information frame, usually these are formatted in lowercase
// (like "replaygain_track_gain"), but can also be uppercase. Make sure that
// capitalization is consistent before continuing.
is TextInformationFrame -> { is TextInformationFrame -> {
key = entry.description?.uppercase() key = entry.description?.uppercase()
value = entry.value value = entry.value
} }
// Vorbis comment. These are nearly always uppercase, so a check for such is
// skipped.
is VorbisComment -> { is VorbisComment -> {
key = entry.key key = entry.key
value = entry.value value = entry.value
@ -183,6 +188,7 @@ class ReplayGainAudioProcessor : BaseAudioProcessor() {
albumGain += tag.value / 256f albumGain += tag.value / 256f
found = true found = true
} }
return if (found) { return if (found) {
Gain(trackGain, albumGain) Gain(trackGain, albumGain)
} else { } else {

View file

@ -17,6 +17,7 @@
package org.oxycblt.auxio.util package org.oxycblt.auxio.util
import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.database.Cursor import android.database.Cursor
@ -24,6 +25,7 @@ import android.database.sqlite.SQLiteDatabase
import android.graphics.Insets import android.graphics.Insets
import android.graphics.Rect import android.graphics.Rect
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build import android.os.Build
import android.view.View import android.view.View
import android.view.WindowInsets import android.view.WindowInsets
@ -154,6 +156,23 @@ fun Fragment.requireAttached() = check(!isDetached) { "Fragment is detached from
fun <R> SQLiteDatabase.queryAll(tableName: String, block: (Cursor) -> R) = fun <R> SQLiteDatabase.queryAll(tableName: String, block: (Cursor) -> R) =
query(tableName, null, null, null, null, null, null)?.use(block) query(tableName, null, null, null, null, null, null)?.use(block)
/** Shortcut for making a [ContentResolver] query with less superfluous arguments. */
fun ContentResolver.queryCursor(
uri: Uri,
projection: Array<out String>,
selector: String? = null,
args: Array<String>? = null
) = query(uri, projection, selector, args, null)
/** Shortcut for making a [ContentResolver] query and using the particular cursor with [use]. */
fun <R> ContentResolver.useQuery(
uri: Uri,
projection: Array<out String>,
selector: String? = null,
args: Array<String>? = null,
block: (Cursor) -> R
): R? = queryCursor(uri, projection, selector, args)?.use(block)
/** /**
* Resolve system bar insets in a version-aware manner. This can be used to apply padding to a view * Resolve system bar insets in a version-aware manner. This can be used to apply padding to a view
* that properly follows all the frustrating changes that were made between Android 8-11. * that properly follows all the frustrating changes that were made between Android 8-11.