diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt b/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt index 25a4a398e..9bfc1f7c3 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/RawMusic.kt @@ -22,6 +22,7 @@ import java.util.UUID import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.music.fs.DeviceFile import org.oxycblt.auxio.music.fs.Path import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.info.ReleaseType @@ -32,23 +33,9 @@ import org.oxycblt.auxio.music.info.ReleaseType * @author Alexander Capehart (OxygenCobalt) */ data class RawSong( - /** - * The ID of the [SongImpl]'s audio file, obtained from MediaStore. Note that this ID is highly - * unstable and should only be used for accessing the audio file. - */ - var mediaStoreId: Long? = null, - /** @see Song.dateAdded */ - var dateAdded: Long? = null, - /** The latest date the [SongImpl]'s audio file was modified, as a unix epoch timestamp. */ - var dateModified: Long? = null, - /** @see Song.path */ - var path: Path? = null, - /** @see Song.size */ - var size: Long? = null, + val file: DeviceFile, /** @see Song.durationMs */ var durationMs: Long? = null, - /** @see Song.mimeType */ - var extensionMimeType: String? = null, /** @see Song.replayGainAdjustment */ var replayGainTrackAdjustment: Float? = null, /** @see Song.replayGainAdjustment */ diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/DeviceFiles.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/DeviceFiles.kt index 3a61160ce..b1bfc1045 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/DeviceFiles.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/fs/DeviceFiles.kt @@ -1,12 +1,29 @@ +/* + * Copyright (c) 2024 Auxio Project + * DeviceFiles.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 . + */ + package org.oxycblt.auxio.music.fs - import android.content.ContentResolver import android.content.Context import android.net.Uri import android.provider.DocumentsContract -import androidx.documentfile.provider.DocumentFile import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asFlow @@ -14,7 +31,6 @@ 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): Flow @@ -23,60 +39,66 @@ interface DeviceFiles { @OptIn(ExperimentalCoroutinesApi::class) class DeviceFilesImpl @Inject constructor(@ApplicationContext private val context: Context) : DeviceFiles { - private val contentResolver = context.contentResolverSafe + private val contentResolver = context.contentResolverSafe + override fun explore(uris: Flow): Flow = - uris.flatMapMerge { rootUri -> - exploreImpl(contentResolver, rootUri, Components.nil()) - } + uris.flatMapMerge { rootUri -> exploreImpl(contentResolver, rootUri, Components.nil()) } private fun exploreImpl( contentResolver: ContentResolver, uri: Uri, relativePath: Components - ): Flow = - flow { - contentResolver.useQuery(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) - // Recurse into sub-directories as another flow - val recursions = mutableListOf>() - while (cursor.moveToNext()) { - val childId = cursor.getString(childUriIndex) - val childUri = DocumentsContract.buildDocumentUriUsingTree(uri, childId) - val displayName = cursor.getString(displayNameIndex) - val path = relativePath.child(displayName) - 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, path)) - } 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, path)) - } + ): Flow = flow { + contentResolver.useQuery(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) + // Recurse into sub-directories as another flow + val recursions = mutableListOf>() + while (cursor.moveToNext()) { + val childId = cursor.getString(childUriIndex) + val childUri = DocumentsContract.buildDocumentUriUsingTree(uri, childId) + val displayName = cursor.getString(displayNameIndex) + val path = relativePath.child(displayName) + 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, path)) + } 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. + val lastModified = cursor.getLong(lastModifiedIndex) + val size = cursor.getLong(sizeIndex) + emit(DeviceFile(childUri, mimeType, path, lastModified)) } - // 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()) } + // 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_DISPLAY_NAME, - DocumentsContract.Document.COLUMN_MIME_TYPE, - ) + val PROJECTION = + arrayOf( + DocumentsContract.Document.COLUMN_DOCUMENT_ID, + DocumentsContract.Document.COLUMN_DISPLAY_NAME, + DocumentsContract.Document.COLUMN_MIME_TYPE, + DocumentsContract.Document.COLUMN_SIZE, + DocumentsContract.Document.COLUMN_LAST_MODIFIED, + ) } } -data class DeviceFile(val uri: Uri, val mimeType: String, val path: Components) \ No newline at end of file +data class DeviceFile(val uri: Uri, val mimeType: String, val path: Components, val size: Long, val lastModified: Long) diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/StorageUtil.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/StorageUtil.kt index f07065f2b..eceab8b14 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/StorageUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/fs/StorageUtil.kt @@ -88,13 +88,14 @@ inline fun ContentResolver.mapQuery( selector: String? = null, args: Array? = null, crossinline transform: Cursor.() -> R -) = useQuery(uri, projection, selector, args) { - sequence { - while (it.moveToNext()) { - yield(it.transform()) +) = + useQuery(uri, projection, selector, args) { + sequence { + 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")