musikr: break apart storageutil
This commit is contained in:
parent
ced2adb2c6
commit
efceefc221
9 changed files with 206 additions and 246 deletions
|
@ -27,7 +27,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import org.oxycblt.auxio.music.MusicRepository
|
import org.oxycblt.auxio.music.MusicRepository
|
||||||
import org.oxycblt.auxio.music.MusicSettings
|
import org.oxycblt.auxio.music.MusicSettings
|
||||||
import org.oxycblt.musikr.fs.contentResolverSafe
|
import org.oxycblt.musikr.fs.util.contentResolverSafe
|
||||||
import timber.log.Timber as L
|
import timber.log.Timber as L
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -31,6 +31,8 @@ import kotlinx.coroutines.flow.emitAll
|
||||||
import kotlinx.coroutines.flow.flatMapMerge
|
import kotlinx.coroutines.flow.flatMapMerge
|
||||||
import kotlinx.coroutines.flow.flattenMerge
|
import kotlinx.coroutines.flow.flattenMerge
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import org.oxycblt.musikr.fs.util.contentResolverSafe
|
||||||
|
import org.oxycblt.musikr.fs.util.useQuery
|
||||||
|
|
||||||
interface DeviceFiles {
|
interface DeviceFiles {
|
||||||
fun explore(locations: Flow<MusicLocation>): Flow<DeviceFile>
|
fun explore(locations: Flow<MusicLocation>): Flow<DeviceFile>
|
||||||
|
|
|
@ -26,6 +26,7 @@ import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import org.oxycblt.musikr.fs.util.contentResolverSafe
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
|
|
|
@ -25,6 +25,7 @@ import android.provider.DocumentsContract
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import org.oxycblt.musikr.fs.path.DocumentPathFactory
|
import org.oxycblt.musikr.fs.path.DocumentPathFactory
|
||||||
|
import org.oxycblt.musikr.fs.util.contentResolverSafe
|
||||||
|
|
||||||
class MusicLocation internal constructor(val uri: Uri, val path: Path) {
|
class MusicLocation internal constructor(val uri: Uri, val path: Path) {
|
||||||
override fun equals(other: Any?) =
|
override fun equals(other: Any?) =
|
||||||
|
|
|
@ -1,242 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2022 Auxio Project
|
|
||||||
* StorageUtil.kt is part of Auxio.
|
|
||||||
*
|
|
||||||
* 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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.oxycblt.musikr.fs
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.ContentResolver
|
|
||||||
import android.content.ContentUris
|
|
||||||
import android.content.Context
|
|
||||||
import android.database.Cursor
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Environment
|
|
||||||
import android.os.storage.StorageManager
|
|
||||||
import android.os.storage.StorageVolume
|
|
||||||
import android.provider.MediaStore
|
|
||||||
import java.lang.reflect.Method
|
|
||||||
import org.oxycblt.auxio.util.lazyReflectedMethod
|
|
||||||
|
|
||||||
// --- MEDIASTORE UTILITIES ---
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a content resolver that will not mangle MediaStore queries on certain devices. See
|
|
||||||
* https://github.com/OxygenCobalt/Auxio/issues/50 for more info.
|
|
||||||
*/
|
|
||||||
val Context.contentResolverSafe: ContentResolver
|
|
||||||
get() = applicationContext.contentResolver
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A shortcut for querying the [ContentResolver] database.
|
|
||||||
*
|
|
||||||
* @param uri The [Uri] of content to retrieve.
|
|
||||||
* @param projection A list of SQL columns to query from the database.
|
|
||||||
* @param selector A SQL selection statement to filter results. Spaces where arguments should be
|
|
||||||
* filled in are represented with a "?".
|
|
||||||
* @param args The arguments used for the selector.
|
|
||||||
* @return A [Cursor] of the queried values, organized by the column projection.
|
|
||||||
* @throws IllegalStateException If the [ContentResolver] did not return the queried [Cursor].
|
|
||||||
* @see ContentResolver.query
|
|
||||||
*/
|
|
||||||
fun ContentResolver.safeQuery(
|
|
||||||
uri: Uri,
|
|
||||||
projection: Array<out String>,
|
|
||||||
selector: String? = null,
|
|
||||||
args: Array<String>? = null
|
|
||||||
) = requireNotNull(query(uri, projection, selector, args, null)) { "ContentResolver query failed" }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A shortcut for [safeQuery] with [use] applied, automatically cleaning up the [Cursor]'s resources
|
|
||||||
* when no longer used.
|
|
||||||
*
|
|
||||||
* @param uri The [Uri] of content to retrieve.
|
|
||||||
* @param projection A list of SQL columns to query from the database.
|
|
||||||
* @param selector A SQL selection statement to filter results. Spaces where arguments should be
|
|
||||||
* filled in are represented with a "?".
|
|
||||||
* @param args The arguments used for the selector.
|
|
||||||
* @param block The block of code to run with the queried [Cursor]. Will not be ran if the [Cursor]
|
|
||||||
* is empty.
|
|
||||||
* @throws IllegalStateException If the [ContentResolver] did not return the queried [Cursor].
|
|
||||||
* @see ContentResolver.query
|
|
||||||
*/
|
|
||||||
inline fun <reified R> ContentResolver.useQuery(
|
|
||||||
uri: Uri,
|
|
||||||
projection: Array<out String>,
|
|
||||||
selector: String? = null,
|
|
||||||
args: Array<String>? = null,
|
|
||||||
block: (Cursor) -> R
|
|
||||||
) = safeQuery(uri, projection, selector, args).use(block)
|
|
||||||
|
|
||||||
inline fun <reified R> ContentResolver.mapQuery(
|
|
||||||
uri: Uri,
|
|
||||||
projection: Array<out String>,
|
|
||||||
selector: String? = null,
|
|
||||||
args: Array<String>? = null,
|
|
||||||
crossinline transform: Cursor.() -> R
|
|
||||||
) =
|
|
||||||
useQuery(uri, projection, selector, args) {
|
|
||||||
sequence<R> {
|
|
||||||
while (it.moveToNext()) {
|
|
||||||
yield(it.transform())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Album art [MediaStore] database is not a built-in constant, have to define it ourselves. */
|
|
||||||
private val externalCoversUri = Uri.parse("content://media/external/audio/albumart")
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a [MediaStore] Song ID into a [Uri] to it's audio file.
|
|
||||||
*
|
|
||||||
* @return An external storage audio file [Uri]. May not exist.
|
|
||||||
* @see ContentUris.withAppendedId
|
|
||||||
* @see MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
|
|
||||||
*/
|
|
||||||
fun Long.toAudioUri() =
|
|
||||||
ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, this)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a [MediaStore] Album ID into a [Uri] to it's system-provided album cover. This cover will
|
|
||||||
* be fast to load, but will be lower quality.
|
|
||||||
*
|
|
||||||
* @return An external storage image [Uri]. May not exist.
|
|
||||||
* @see ContentUris.withAppendedId
|
|
||||||
*/
|
|
||||||
fun Long.toSongCoverUri(): Uri =
|
|
||||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.buildUpon().run {
|
|
||||||
appendPath(this@toSongCoverUri.toString())
|
|
||||||
appendPath("albumart")
|
|
||||||
build()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Long.toAlbumCoverUri(): Uri = ContentUris.withAppendedId(externalCoversUri, this)
|
|
||||||
|
|
||||||
// --- STORAGEMANAGER UTILITIES ---
|
|
||||||
// Largely derived from Material Files: https://github.com/zhanghai/MaterialFiles
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides the analogous method to [StorageVolume.getDirectory] method that is usable from API 21
|
|
||||||
* to API 23, in which the [StorageVolume] API was hidden and differed greatly.
|
|
||||||
*
|
|
||||||
* @see StorageVolume.getDirectory
|
|
||||||
*/
|
|
||||||
@Suppress("NewApi")
|
|
||||||
private val svApi21GetPathMethod: Method by lazyReflectedMethod(StorageVolume::class, "getPath")
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The list of [StorageVolume]s currently recognized by [StorageManager], in a version-compatible
|
|
||||||
* manner.
|
|
||||||
*
|
|
||||||
* @see StorageManager.getStorageVolumes
|
|
||||||
*/
|
|
||||||
val StorageManager.storageVolumesCompat: List<StorageVolume>
|
|
||||||
get() = storageVolumes.toList()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The the absolute path to this [StorageVolume]'s directory within the file-system, in a
|
|
||||||
* version-compatible manner. Will be null if the [StorageVolume] cannot be read.
|
|
||||||
*
|
|
||||||
* @see StorageVolume.getDirectory
|
|
||||||
*/
|
|
||||||
val StorageVolume.directoryCompat: String?
|
|
||||||
@SuppressLint("NewApi")
|
|
||||||
get() =
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
directory?.absolutePath
|
|
||||||
} else {
|
|
||||||
// Replicate API: Analogous method if mounted, null if not
|
|
||||||
when (stateCompat) {
|
|
||||||
Environment.MEDIA_MOUNTED,
|
|
||||||
Environment.MEDIA_MOUNTED_READ_ONLY -> svApi21GetPathMethod.invoke(this) as String
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the human-readable description of this volume, such as "Internal Shared Storage".
|
|
||||||
*
|
|
||||||
* @param context [Context] required to obtain human-readable string resources.
|
|
||||||
* @return A human-readable name for this volume.
|
|
||||||
*/
|
|
||||||
@SuppressLint("NewApi")
|
|
||||||
fun StorageVolume.getDescriptionCompat(context: Context): String = getDescription(context)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If this [StorageVolume] is considered the "Primary" volume where the Android System is kept. May
|
|
||||||
* still be a removable volume.
|
|
||||||
*
|
|
||||||
* @see StorageVolume.isPrimary
|
|
||||||
*/
|
|
||||||
val StorageVolume.isPrimaryCompat: Boolean
|
|
||||||
@SuppressLint("NewApi") get() = isPrimary
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If this storage is "emulated", i.e intrinsic to the device, obtained in a version compatible
|
|
||||||
* manner.
|
|
||||||
*
|
|
||||||
* @see StorageVolume.isEmulated
|
|
||||||
*/
|
|
||||||
val StorageVolume.isEmulatedCompat: Boolean
|
|
||||||
@SuppressLint("NewApi") get() = isEmulated
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If this [StorageVolume] represents the "Internal Shared Storage" volume, also known as "primary"
|
|
||||||
* to [MediaStore] and Document [Uri]s, obtained in a version compatible manner.
|
|
||||||
*/
|
|
||||||
val StorageVolume.isInternalCompat: Boolean
|
|
||||||
// Must contain the android system AND be an emulated drive, as non-emulated system
|
|
||||||
// volumes use their UUID instead of primary in MediaStore/Document URIs.
|
|
||||||
get() = isPrimaryCompat && isEmulatedCompat
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The unique identifier for this [StorageVolume], obtained in a version compatible manner. Can be
|
|
||||||
* null.
|
|
||||||
*
|
|
||||||
* @see StorageVolume.getUuid
|
|
||||||
*/
|
|
||||||
val StorageVolume.uuidCompat: String?
|
|
||||||
@SuppressLint("NewApi") get() = uuid
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The current state of this [StorageVolume], such as "mounted" or "read-only", obtained in a
|
|
||||||
* version compatible manner.
|
|
||||||
*
|
|
||||||
* @see StorageVolume.getState
|
|
||||||
*/
|
|
||||||
val StorageVolume.stateCompat: String
|
|
||||||
@SuppressLint("NewApi") get() = state
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the name of this volume that can be used to interact with [MediaStore], in a version
|
|
||||||
* compatible manner. Will be null if the volume is not scanned by [MediaStore].
|
|
||||||
*
|
|
||||||
* @see StorageVolume.getMediaStoreVolumeName
|
|
||||||
*/
|
|
||||||
val StorageVolume.mediaStoreVolumeNameCompat: String?
|
|
||||||
get() =
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
mediaStoreVolumeName
|
|
||||||
} else {
|
|
||||||
// Replicate API: primary_external if primary storage, lowercase uuid otherwise
|
|
||||||
if (isPrimaryCompat) {
|
|
||||||
// "primary_external" is used in all versions that Auxio supports, is safe to use.
|
|
||||||
@Suppress("NewApi") MediaStore.VOLUME_EXTERNAL_PRIMARY
|
|
||||||
} else {
|
|
||||||
uuidCompat?.lowercase()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -28,8 +28,8 @@ import javax.inject.Inject
|
||||||
import org.oxycblt.musikr.fs.Components
|
import org.oxycblt.musikr.fs.Components
|
||||||
import org.oxycblt.musikr.fs.Path
|
import org.oxycblt.musikr.fs.Path
|
||||||
import org.oxycblt.musikr.fs.Volume
|
import org.oxycblt.musikr.fs.Volume
|
||||||
import org.oxycblt.musikr.fs.contentResolverSafe
|
import org.oxycblt.musikr.fs.util.contentResolverSafe
|
||||||
import org.oxycblt.musikr.fs.useQuery
|
import org.oxycblt.musikr.fs.util.useQuery
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A factory for parsing the reverse-engineered format of the URIs obtained from document picker.
|
* A factory for parsing the reverse-engineered format of the URIs obtained from document picker.
|
||||||
|
|
126
app/src/main/java/org/oxycblt/musikr/fs/path/VolumeCompat.kt
Normal file
126
app/src/main/java/org/oxycblt/musikr/fs/path/VolumeCompat.kt
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
package org.oxycblt.musikr.fs.path
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Environment
|
||||||
|
import android.os.storage.StorageManager
|
||||||
|
import android.os.storage.StorageVolume
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import org.oxycblt.auxio.util.lazyReflectedMethod
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
|
||||||
|
// Largely derived from Material Files: https://github.com/zhanghai/MaterialFiles
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides the analogous method to [StorageVolume.getDirectory] method that is usable from API 21
|
||||||
|
* to API 23, in which the [StorageVolume] API was hidden and differed greatly.
|
||||||
|
*
|
||||||
|
* @see StorageVolume.getDirectory
|
||||||
|
*/
|
||||||
|
@Suppress("NewApi")
|
||||||
|
private val svApi21GetPathMethod: Method by lazyReflectedMethod(StorageVolume::class, "getPath")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of [StorageVolume]s currently recognized by [StorageManager], in a version-compatible
|
||||||
|
* manner.
|
||||||
|
*
|
||||||
|
* @see StorageManager.getStorageVolumes
|
||||||
|
*/
|
||||||
|
val StorageManager.storageVolumesCompat: List<StorageVolume>
|
||||||
|
get() = storageVolumes.toList()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The the absolute path to this [StorageVolume]'s directory within the file-system, in a
|
||||||
|
* version-compatible manner. Will be null if the [StorageVolume] cannot be read.
|
||||||
|
*
|
||||||
|
* @see StorageVolume.getDirectory
|
||||||
|
*/
|
||||||
|
val StorageVolume.directoryCompat: String?
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
get() =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
directory?.absolutePath
|
||||||
|
} else {
|
||||||
|
// Replicate API: Analogous method if mounted, null if not
|
||||||
|
when (stateCompat) {
|
||||||
|
Environment.MEDIA_MOUNTED,
|
||||||
|
Environment.MEDIA_MOUNTED_READ_ONLY -> svApi21GetPathMethod.invoke(this) as String
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the human-readable description of this volume, such as "Internal Shared Storage".
|
||||||
|
*
|
||||||
|
* @param context [Context] required to obtain human-readable string resources.
|
||||||
|
* @return A human-readable name for this volume.
|
||||||
|
*/
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
fun StorageVolume.getDescriptionCompat(context: Context): String = getDescription(context)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this [StorageVolume] is considered the "Primary" volume where the Android System is kept. May
|
||||||
|
* still be a removable volume.
|
||||||
|
*
|
||||||
|
* @see StorageVolume.isPrimary
|
||||||
|
*/
|
||||||
|
val StorageVolume.isPrimaryCompat: Boolean
|
||||||
|
@SuppressLint("NewApi") get() = isPrimary
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this storage is "emulated", i.e intrinsic to the device, obtained in a version compatible
|
||||||
|
* manner.
|
||||||
|
*
|
||||||
|
* @see StorageVolume.isEmulated
|
||||||
|
*/
|
||||||
|
val StorageVolume.isEmulatedCompat: Boolean
|
||||||
|
@SuppressLint("NewApi") get() = isEmulated
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this [StorageVolume] represents the "Internal Shared Storage" volume, also known as "primary"
|
||||||
|
* to [MediaStore] and Document [Uri]s, obtained in a version compatible manner.
|
||||||
|
*/
|
||||||
|
val StorageVolume.isInternalCompat: Boolean
|
||||||
|
// Must contain the android system AND be an emulated drive, as non-emulated system
|
||||||
|
// volumes use their UUID instead of primary in MediaStore/Document URIs.
|
||||||
|
get() = isPrimaryCompat && isEmulatedCompat
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The unique identifier for this [StorageVolume], obtained in a version compatible manner. Can be
|
||||||
|
* null.
|
||||||
|
*
|
||||||
|
* @see StorageVolume.getUuid
|
||||||
|
*/
|
||||||
|
val StorageVolume.uuidCompat: String?
|
||||||
|
@SuppressLint("NewApi") get() = uuid
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current state of this [StorageVolume], such as "mounted" or "read-only", obtained in a
|
||||||
|
* version compatible manner.
|
||||||
|
*
|
||||||
|
* @see StorageVolume.getState
|
||||||
|
*/
|
||||||
|
val StorageVolume.stateCompat: String
|
||||||
|
@SuppressLint("NewApi") get() = state
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the name of this volume that can be used to interact with [MediaStore], in a version
|
||||||
|
* compatible manner. Will be null if the volume is not scanned by [MediaStore].
|
||||||
|
*
|
||||||
|
* @see StorageVolume.getMediaStoreVolumeName
|
||||||
|
*/
|
||||||
|
val StorageVolume.mediaStoreVolumeNameCompat: String?
|
||||||
|
get() =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
mediaStoreVolumeName
|
||||||
|
} else {
|
||||||
|
// Replicate API: primary_external if primary storage, lowercase uuid otherwise
|
||||||
|
if (isPrimaryCompat) {
|
||||||
|
// "primary_external" is used in all versions that Auxio supports, is safe to use.
|
||||||
|
@Suppress("NewApi") MediaStore.VOLUME_EXTERNAL_PRIMARY
|
||||||
|
} else {
|
||||||
|
uuidCompat?.lowercase()
|
||||||
|
}
|
||||||
|
}
|
72
app/src/main/java/org/oxycblt/musikr/fs/util/QueryUtil.kt
Normal file
72
app/src/main/java/org/oxycblt/musikr/fs/util/QueryUtil.kt
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2022 Auxio Project
|
||||||
|
* QueryUtil.kt is part of Auxio.
|
||||||
|
*
|
||||||
|
* 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.oxycblt.musikr.fs.util
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.Context
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.net.Uri
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a content resolver that will not mangle MediaStore queries on certain devices. See
|
||||||
|
* https://github.com/OxygenCobalt/Auxio/issues/50 for more info.
|
||||||
|
*/
|
||||||
|
val Context.contentResolverSafe: ContentResolver
|
||||||
|
get() = applicationContext.contentResolver
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A shortcut for querying the [ContentResolver] database.
|
||||||
|
*
|
||||||
|
* @param uri The [Uri] of content to retrieve.
|
||||||
|
* @param projection A list of SQL columns to query from the database.
|
||||||
|
* @param selector A SQL selection statement to filter results. Spaces where arguments should be
|
||||||
|
* filled in are represented with a "?".
|
||||||
|
* @param args The arguments used for the selector.
|
||||||
|
* @return A [Cursor] of the queried values, organized by the column projection.
|
||||||
|
* @throws IllegalStateException If the [ContentResolver] did not return the queried [Cursor].
|
||||||
|
* @see ContentResolver.query
|
||||||
|
*/
|
||||||
|
fun ContentResolver.safeQuery(
|
||||||
|
uri: Uri,
|
||||||
|
projection: Array<out String>,
|
||||||
|
selector: String? = null,
|
||||||
|
args: Array<String>? = null
|
||||||
|
) = requireNotNull(query(uri, projection, selector, args, null)) { "ContentResolver query failed" }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A shortcut for [safeQuery] with [use] applied, automatically cleaning up the [Cursor]'s resources
|
||||||
|
* when no longer used.
|
||||||
|
*
|
||||||
|
* @param uri The [Uri] of content to retrieve.
|
||||||
|
* @param projection A list of SQL columns to query from the database.
|
||||||
|
* @param selector A SQL selection statement to filter results. Spaces where arguments should be
|
||||||
|
* filled in are represented with a "?".
|
||||||
|
* @param args The arguments used for the selector.
|
||||||
|
* @param block The block of code to run with the queried [Cursor]. Will not be ran if the [Cursor]
|
||||||
|
* is empty.
|
||||||
|
* @throws IllegalStateException If the [ContentResolver] did not return the queried [Cursor].
|
||||||
|
* @see ContentResolver.query
|
||||||
|
*/
|
||||||
|
inline fun <reified R> ContentResolver.useQuery(
|
||||||
|
uri: Uri,
|
||||||
|
projection: Array<out String>,
|
||||||
|
selector: String? = null,
|
||||||
|
args: Array<String>? = null,
|
||||||
|
block: (Cursor) -> R
|
||||||
|
) = safeQuery(uri, projection, selector, args).use(block)
|
|
@ -25,7 +25,7 @@ import javax.inject.Inject
|
||||||
import org.oxycblt.musikr.Playlist
|
import org.oxycblt.musikr.Playlist
|
||||||
import org.oxycblt.musikr.fs.Components
|
import org.oxycblt.musikr.fs.Components
|
||||||
import org.oxycblt.musikr.fs.Path
|
import org.oxycblt.musikr.fs.Path
|
||||||
import org.oxycblt.musikr.fs.contentResolverSafe
|
import org.oxycblt.musikr.fs.util.contentResolverSafe
|
||||||
import org.oxycblt.musikr.fs.path.DocumentPathFactory
|
import org.oxycblt.musikr.fs.path.DocumentPathFactory
|
||||||
import org.oxycblt.musikr.playlist.m3u.M3U
|
import org.oxycblt.musikr.playlist.m3u.M3U
|
||||||
import timber.log.Timber as L
|
import timber.log.Timber as L
|
||||||
|
|
Loading…
Reference in a new issue