From d62c85f8a563ddb515cd579036c3b8056f3987dd Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Tue, 4 Mar 2025 15:48:40 -0700 Subject: [PATCH] musikr: avoid redundant dir child queries Make parents async rather than children --- .../org/oxycblt/musikr/cover/FolderCovers.kt | 2 +- .../oxycblt/musikr/fs/device/DeviceFile.kt | 7 +- .../oxycblt/musikr/fs/device/DeviceFiles.kt | 143 +++++++++--------- .../oxycblt/musikr/pipeline/ExploreStep.kt | 18 +-- 4 files changed, 78 insertions(+), 92 deletions(-) diff --git a/musikr/src/main/java/org/oxycblt/musikr/cover/FolderCovers.kt b/musikr/src/main/java/org/oxycblt/musikr/cover/FolderCovers.kt index 929de0074..46db555e6 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/cover/FolderCovers.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/cover/FolderCovers.kt @@ -63,7 +63,7 @@ open class FolderCovers(private val context: Context) : Covers { class MutableFolderCovers(private val context: Context) : FolderCovers(context), MutableCovers { override suspend fun create(file: DeviceFile, metadata: Metadata): CoverResult { - val parent = file.parent + val parent = file.parent.await() val coverFile = findCoverInDirectory(parent) ?: return CoverResult.Miss() return CoverResult.Hit(FolderCoverImpl(context, coverFile.uri)) } diff --git a/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFile.kt b/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFile.kt index 6590491a6..e2ce0e210 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFile.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFile.kt @@ -19,6 +19,7 @@ package org.oxycblt.musikr.fs.device import android.net.Uri +import kotlinx.coroutines.Deferred import kotlinx.coroutines.flow.Flow import org.oxycblt.musikr.fs.Path @@ -30,8 +31,8 @@ sealed interface DeviceNode { data class DeviceDirectory( override val uri: Uri, override val path: Path, - val parent: DeviceDirectory?, - var children: Flow + val parent: Deferred?, + val children: List ) : DeviceNode data class DeviceFile( @@ -40,5 +41,5 @@ data class DeviceFile( val modifiedMs: Long, val mimeType: String, val size: Long, - val parent: DeviceDirectory + val parent: Deferred ) : DeviceNode diff --git a/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFiles.kt b/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFiles.kt index 239de9c30..82737558c 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFiles.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/fs/device/DeviceFiles.kt @@ -15,23 +15,28 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + package org.oxycblt.musikr.fs.device import android.content.ContentResolver import android.content.Context import android.net.Uri import android.provider.DocumentsContract +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Deferred import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flatMapMerge +import kotlinx.coroutines.flow.flattenMerge import kotlinx.coroutines.flow.flow import org.oxycblt.musikr.fs.MusicLocation import org.oxycblt.musikr.fs.Path internal interface DeviceFiles { - fun explore(locations: Flow): Flow + fun explore(locations: Flow): Flow companion object { fun from(context: Context, ignoreHidden: Boolean): DeviceFiles = @@ -44,91 +49,82 @@ private class DeviceFilesImpl( private val contentResolver: ContentResolver, private val ignoreHidden: Boolean ) : DeviceFiles { - override fun explore(locations: Flow): Flow = + override fun explore(locations: Flow): Flow = locations.flatMapMerge { location -> - // Create a root directory for each location - val rootDirectory = - DeviceDirectory( - uri = location.uri, path = location.path, parent = null, children = emptyFlow()) - // Set up the children flow for the root directory - rootDirectory.children = - exploreDirectoryImpl( - contentResolver, - location.uri, - DocumentsContract.getTreeDocumentId(location.uri), - location.path, - rootDirectory, - ignoreHidden) - - // Return a flow that emits the root directory - flow { emit(rootDirectory) } + exploreDirectoryImpl( + location.uri, + DocumentsContract.getTreeDocumentId(location.uri), + location.path, + null + ) } private fun exploreDirectoryImpl( - contentResolver: ContentResolver, rootUri: Uri, treeDocumentId: String, relativePath: Path, - parent: DeviceDirectory, - ignoreHidden: Boolean - ): Flow = flow { + parent: Deferred? + ): Flow = flow { + // Make a kotlin future + val uri = + DocumentsContract.buildChildDocumentsUriUsingTree(rootUri, treeDocumentId) + val directoryDeferred = CompletableDeferred() + val recursive = mutableListOf>() + val children = mutableListOf() contentResolver.useQuery( - DocumentsContract.buildChildDocumentsUriUsingTree(rootUri, treeDocumentId), - PROJECTION) { cursor -> - val childUriIndex = - cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID) - val displayNameIndex = - cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME) - val mimeTypeIndex = - cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_MIME_TYPE) - val sizeIndex = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_SIZE) - val lastModifiedIndex = - cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_LAST_MODIFIED) + uri, PROJECTION + ) { cursor -> + val childUriIndex = + cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID) + val displayNameIndex = + cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME) + val mimeTypeIndex = + cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_MIME_TYPE) + val sizeIndex = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_SIZE) + val lastModifiedIndex = + cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_LAST_MODIFIED) - while (cursor.moveToNext()) { - val childId = cursor.getString(childUriIndex) - val displayName = cursor.getString(displayNameIndex) + while (cursor.moveToNext()) { + val childId = cursor.getString(childUriIndex) + val displayName = cursor.getString(displayNameIndex) - // Skip hidden files/directories if ignoreHidden is true - if (ignoreHidden && displayName.startsWith(".")) { - continue - } + // Skip hidden files/directories if ignoreHidden is true + if (ignoreHidden && displayName.startsWith(".")) { + continue + } - val newPath = relativePath.file(displayName) - val mimeType = cursor.getString(mimeTypeIndex) - val lastModified = cursor.getLong(lastModifiedIndex) + val newPath = relativePath.file(displayName) + val mimeType = cursor.getString(mimeTypeIndex) + val lastModified = cursor.getLong(lastModifiedIndex) + + if (mimeType == DocumentsContract.Document.MIME_TYPE_DIR) { + recursive.add( + exploreDirectoryImpl( + rootUri, + childId, + newPath, + directoryDeferred + ) + ) + } else { + val size = cursor.getLong(sizeIndex) val childUri = DocumentsContract.buildDocumentUriUsingTree(rootUri, childId) - - if (mimeType == DocumentsContract.Document.MIME_TYPE_DIR) { - // Create a directory node with empty children flow initially - val directory = - DeviceDirectory( - uri = childUri, - path = newPath, - parent = parent, - children = emptyFlow()) - - // Set up the children flow for this directory - directory.children = - exploreDirectoryImpl( - contentResolver, rootUri, childId, newPath, directory, ignoreHidden) - - // Emit the directory node - emit(directory) - } else { - val size = cursor.getLong(sizeIndex) - emit( - DeviceFile( - uri = childUri, - mimeType = mimeType, - path = newPath, - size = size, - modifiedMs = lastModified, - parent = parent)) - } + emit( + DeviceFile( + uri = childUri, + mimeType = mimeType, + path = newPath, + size = size, + modifiedMs = lastModified, + parent = directoryDeferred + ) + ) } } + } + directoryDeferred.complete(DeviceDirectory(uri, relativePath, parent, children)) + emitAll(recursive.asFlow().flattenMerge()) } private companion object { @@ -138,6 +134,7 @@ private class DeviceFilesImpl( DocumentsContract.Document.COLUMN_DISPLAY_NAME, DocumentsContract.Document.COLUMN_MIME_TYPE, DocumentsContract.Document.COLUMN_SIZE, - DocumentsContract.Document.COLUMN_LAST_MODIFIED) + DocumentsContract.Document.COLUMN_LAST_MODIFIED + ) } } diff --git a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt index e39c3edd9..62c67cc96 100644 --- a/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt +++ b/musikr/src/main/java/org/oxycblt/musikr/pipeline/ExploreStep.kt @@ -25,6 +25,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flattenMerge import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn @@ -59,7 +60,8 @@ private class ExploreStepImpl( val audios = deviceFiles .explore(locations.asFlow()) - .flattenFilter { it.mimeType.startsWith("audio/") || it.mimeType == M3U.MIME_TYPE } + .filter { it.mimeType.startsWith("audio/") && it.mimeType != M3U.MIME_TYPE } + .map { ExploreNode.Audio(it) } .flowOn(Dispatchers.IO) .buffer() val playlists = @@ -69,20 +71,6 @@ private class ExploreStepImpl( .buffer() return merge(audios, playlists) } - - @OptIn(ExperimentalCoroutinesApi::class) - private fun Flow.flattenFilter(block: (DeviceFile) -> Boolean): Flow = - flow { - collect { - val recurse = mutableListOf>() - when { - it is DeviceFile && block(it) -> emit(ExploreNode.Audio(it)) - it is DeviceDirectory -> recurse.add(it.children.flattenFilter(block)) - else -> {} - } - emitAll(recurse.asFlow().flattenMerge()) - } - } } internal sealed interface ExploreNode {