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 org.oxycblt.auxio.music.MusicRepository
|
||||
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
|
||||
|
||||
/**
|
||||
|
|
|
@ -31,6 +31,8 @@ import kotlinx.coroutines.flow.emitAll
|
|||
import kotlinx.coroutines.flow.flatMapMerge
|
||||
import kotlinx.coroutines.flow.flattenMerge
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import org.oxycblt.musikr.fs.util.contentResolverSafe
|
||||
import org.oxycblt.musikr.fs.util.useQuery
|
||||
|
||||
interface DeviceFiles {
|
||||
fun explore(locations: Flow<MusicLocation>): Flow<DeviceFile>
|
||||
|
|
|
@ -26,6 +26,7 @@ import dagger.Provides
|
|||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import org.oxycblt.musikr.fs.util.contentResolverSafe
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
|
|
|
@ -25,6 +25,7 @@ import android.provider.DocumentsContract
|
|||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
import org.oxycblt.musikr.fs.path.DocumentPathFactory
|
||||
import org.oxycblt.musikr.fs.util.contentResolverSafe
|
||||
|
||||
class MusicLocation internal constructor(val uri: Uri, val path: Path) {
|
||||
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.Path
|
||||
import org.oxycblt.musikr.fs.Volume
|
||||
import org.oxycblt.musikr.fs.contentResolverSafe
|
||||
import org.oxycblt.musikr.fs.useQuery
|
||||
import org.oxycblt.musikr.fs.util.contentResolverSafe
|
||||
import org.oxycblt.musikr.fs.util.useQuery
|
||||
|
||||
/**
|
||||
* 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.fs.Components
|
||||
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.playlist.m3u.M3U
|
||||
import timber.log.Timber as L
|
||||
|
|
Loading…
Reference in a new issue