music: include path with loaded saf files

This commit is contained in:
Alexander Capehart 2024-11-15 12:11:57 -07:00
parent 300f26739d
commit 5b447f7efb
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
3 changed files with 21 additions and 12 deletions

View file

@ -0,0 +1,2 @@
package org.oxycblt.auxio.music.cache

View file

@ -3,12 +3,10 @@ package org.oxycblt.auxio.music.fs
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.media.MediaFormat
import android.net.Uri import android.net.Uri
import android.os.storage.StorageManager
import android.os.storage.StorageVolume
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.webkit.MimeTypeMap import androidx.documentfile.provider.DocumentFile
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asFlow
@ -23,38 +21,44 @@ interface DeviceFiles {
} }
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class DeviceFilesImpl @Inject constructor(private val contentResolver: ContentResolver) : class DeviceFilesImpl @Inject constructor(@ApplicationContext private val context: Context) :
DeviceFiles { DeviceFiles {
private val contentResolver = context.contentResolverSafe
override fun explore(uris: Flow<Uri>): Flow<DeviceFile> = override fun explore(uris: Flow<Uri>): Flow<DeviceFile> =
uris.flatMapMerge { rootUri -> uris.flatMapMerge { rootUri ->
exploreImpl(contentResolver, rootUri) exploreImpl(contentResolver, rootUri, Components.nil())
} }
private fun exploreImpl( private fun exploreImpl(
contentResolver: ContentResolver, contentResolver: ContentResolver,
uri: Uri uri: Uri,
relativePath: Components
): Flow<DeviceFile> = ): Flow<DeviceFile> =
flow { flow {
contentResolver.useQuery(uri, PROJECTION) { cursor -> contentResolver.useQuery(uri, PROJECTION) { cursor ->
val childUriIndex = val childUriIndex =
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID) cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID)
val displayNameIndex =
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME)
val mimeTypeIndex = val mimeTypeIndex =
cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_MIME_TYPE) cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_MIME_TYPE)
// Recurse into sub-directories as another flowO // Recurse into sub-directories as another flow
val recursions = mutableListOf<Flow<DeviceFile>>() val recursions = mutableListOf<Flow<DeviceFile>>()
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
val childId = cursor.getString(childUriIndex) val childId = cursor.getString(childUriIndex)
val childUri = DocumentsContract.buildDocumentUriUsingTree(uri, childId) val childUri = DocumentsContract.buildDocumentUriUsingTree(uri, childId)
val displayName = cursor.getString(displayNameIndex)
val path = relativePath.child(displayName)
val mimeType = cursor.getString(mimeTypeIndex) val mimeType = cursor.getString(mimeTypeIndex)
if (mimeType == DocumentsContract.Document.MIME_TYPE_DIR) { if (mimeType == DocumentsContract.Document.MIME_TYPE_DIR) {
// This does NOT block the current coroutine. Instead, we will // This does NOT block the current coroutine. Instead, we will
// evaluate this flow in parallel later to maximize throughput. // evaluate this flow in parallel later to maximize throughput.
recursions.add(exploreImpl(contentResolver, childUri)) recursions.add(exploreImpl(contentResolver, childUri, path))
} else { } else {
// Immediately emit all files given that it's just an O(1) op. // 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 // This also just makes sure the outer flow has a reason to exist
// rather than just being a glorified async. // rather than just being a glorified async.
emit(DeviceFile(childUri, mimeType)) emit(DeviceFile(childUri, mimeType, path))
} }
} }
// Hypothetically, we could just emitAll as we recurse into a new directory, // Hypothetically, we could just emitAll as we recurse into a new directory,
@ -69,9 +73,10 @@ class DeviceFilesImpl @Inject constructor(private val contentResolver: ContentRe
private companion object { private companion object {
val PROJECTION = arrayOf( val PROJECTION = arrayOf(
DocumentsContract.Document.COLUMN_DOCUMENT_ID, DocumentsContract.Document.COLUMN_DOCUMENT_ID,
DocumentsContract.Document.COLUMN_MIME_TYPE DocumentsContract.Document.COLUMN_DISPLAY_NAME,
DocumentsContract.Document.COLUMN_MIME_TYPE,
) )
} }
} }
data class DeviceFile(val uri: Uri, val mimeType: String) data class DeviceFile(val uri: Uri, val mimeType: String, val path: Components)

View file

@ -161,6 +161,8 @@ value class Components private constructor(val components: List<String>) {
fun containing(other: Components) = Components(other.components.drop(components.size)) fun containing(other: Components) = Components(other.components.drop(components.size))
companion object { companion object {
fun nil() = Components(listOf())
/** /**
* Parses a path string into a [Components] instance by the unix path separator (/). * Parses a path string into a [Components] instance by the unix path separator (/).
* *