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")