music: introduce saf explorer
No functionality right now
This commit is contained in:
parent
4d27c444de
commit
300f26739d
2 changed files with 91 additions and 0 deletions
77
app/src/main/java/org/oxycblt/auxio/music/fs/DeviceFiles.kt
Normal file
77
app/src/main/java/org/oxycblt/auxio/music/fs/DeviceFiles.kt
Normal file
|
@ -0,0 +1,77 @@
|
|||
package org.oxycblt.auxio.music.fs
|
||||
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.media.MediaFormat
|
||||
import android.net.Uri
|
||||
import android.os.storage.StorageManager
|
||||
import android.os.storage.StorageVolume
|
||||
import android.provider.DocumentsContract
|
||||
import android.webkit.MimeTypeMap
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flatMapMerge
|
||||
import kotlinx.coroutines.flow.flattenMerge
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import javax.inject.Inject
|
||||
|
||||
interface DeviceFiles {
|
||||
fun explore(uris: Flow<Uri>): Flow<DeviceFile>
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class DeviceFilesImpl @Inject constructor(private val contentResolver: ContentResolver) :
|
||||
DeviceFiles {
|
||||
override fun explore(uris: Flow<Uri>): Flow<DeviceFile> =
|
||||
uris.flatMapMerge { rootUri ->
|
||||
exploreImpl(contentResolver, rootUri)
|
||||
}
|
||||
|
||||
private fun exploreImpl(
|
||||
contentResolver: ContentResolver,
|
||||
uri: Uri
|
||||
): Flow<DeviceFile> =
|
||||
flow {
|
||||
contentResolver.useQuery(uri, PROJECTION) { cursor ->
|
||||
val childUriIndex =
|
||||
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID)
|
||||
val mimeTypeIndex =
|
||||
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_MIME_TYPE)
|
||||
// Recurse into sub-directories as another flowO
|
||||
val recursions = mutableListOf<Flow<DeviceFile>>()
|
||||
while (cursor.moveToNext()) {
|
||||
val childId = cursor.getString(childUriIndex)
|
||||
val childUri = DocumentsContract.buildDocumentUriUsingTree(uri, childId)
|
||||
val mimeType = cursor.getString(mimeTypeIndex)
|
||||
if (mimeType == DocumentsContract.Document.MIME_TYPE_DIR) {
|
||||
// This does NOT block the current coroutine. Instead, we will
|
||||
// evaluate this flow in parallel later to maximize throughput.
|
||||
recursions.add(exploreImpl(contentResolver, childUri))
|
||||
} else {
|
||||
// Immediately emit all files given that it's just an O(1) op.
|
||||
// This also just makes sure the outer flow has a reason to exist
|
||||
// rather than just being a glorified async.
|
||||
emit(DeviceFile(childUri, mimeType))
|
||||
}
|
||||
}
|
||||
// Hypothetically, we could just emitAll as we recurse into a new directory,
|
||||
// but this will block the flow and force the tree search to be sequential.
|
||||
// Instead, try to leverage flow parallelism and do all recursive calls in parallel.
|
||||
// Kotlin coroutines can handle doing possibly thousands of parallel calls, it'll
|
||||
// be fine. I hope.
|
||||
emitAll(recursions.asFlow().flattenMerge())
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val PROJECTION = arrayOf(
|
||||
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
|
||||
DocumentsContract.Document.COLUMN_MIME_TYPE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class DeviceFile(val uri: Uri, val mimeType: String)
|
|
@ -82,6 +82,20 @@ inline fun <reified R> ContentResolver.useQuery(
|
|||
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")
|
||||
|
||||
|
|
Loading…
Reference in a new issue