diff --git a/app/src/main/java/org/oxycblt/musikr/MimeType.kt b/app/src/main/java/org/oxycblt/musikr/MimeType.kt
new file mode 100644
index 000000000..8e6dd0672
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/musikr/MimeType.kt
@@ -0,0 +1,84 @@
+package org.oxycblt.musikr
+
+import android.content.Context
+import android.media.MediaFormat
+import android.webkit.MimeTypeMap
+import org.oxycblt.auxio.R
+
+/**
+ * A mime type of a file. Only intended for display.
+ *
+ * @param fromExtension The mime type obtained by analyzing the file extension.
+ * @param fromFormat The mime type obtained by analyzing the file format. Null if could not be
+ * obtained.
+ * @author Alexander Capehart (OxygenCobalt)
+ *
+ * TODO: Get around to simplifying this
+ */
+data class MimeType(val fromExtension: String, val fromFormat: String?) {
+ /**
+ * Resolve the mime type into a human-readable format name, such as "Ogg Vorbis".
+ *
+ * @param context [Context] required to obtain human-readable strings.
+ * @return A human-readable name for this mime type. Will first try [fromFormat], then falling
+ * back to [fromExtension], and then null if that fails.
+ */
+ fun resolveName(context: Context): String? {
+ // We try our best to produce a more readable name for the common audio formats.
+ val formatName =
+ when (fromFormat) {
+ // We start with the extracted mime types, as they are more consistent. Note that
+ // we do not include container formats at all with these names. It is only the
+ // inner codec that we bother with.
+ MediaFormat.MIMETYPE_AUDIO_MPEG -> R.string.cdc_mp3
+ MediaFormat.MIMETYPE_AUDIO_AAC -> R.string.cdc_aac
+ MediaFormat.MIMETYPE_AUDIO_VORBIS -> R.string.cdc_vorbis
+ MediaFormat.MIMETYPE_AUDIO_OPUS -> R.string.cdc_opus
+ MediaFormat.MIMETYPE_AUDIO_FLAC -> R.string.cdc_flac
+ // TODO: Add ALAC to this as soon as I can stop using MediaFormat for
+ // extracting metadata and just use ExoPlayer.
+ // We don't give a name to more unpopular formats.
+ else -> -1
+ }
+
+ if (formatName > -1) {
+ return context.getString(formatName)
+ }
+
+ // Fall back to the file extension in the case that we have no mime type or
+ // a useless "audio/raw" mime type. Here:
+ // - We return names for container formats instead of the inner format, as we
+ // cannot parse the file.
+ // - We are at the mercy of the Android OS, hence we check for every possible mime
+ // type for a particular format according to Wikipedia.
+ val extensionName =
+ when (fromExtension) {
+ "audio/mpeg",
+ "audio/mp3" -> R.string.cdc_mp3
+ "audio/mp4",
+ "audio/mp4a-latm",
+ "audio/mpeg4-generic" -> R.string.cdc_mp4
+ "audio/aac",
+ "audio/aacp",
+ "audio/3gpp",
+ "audio/3gpp2" -> R.string.cdc_aac
+ "audio/ogg",
+ "application/ogg",
+ "application/x-ogg" -> R.string.cdc_ogg
+ "audio/flac" -> R.string.cdc_flac
+ "audio/wav",
+ "audio/x-wav",
+ "audio/wave",
+ "audio/vnd.wave" -> R.string.cdc_wav
+ "audio/x-matroska" -> R.string.cdc_mka
+ else -> -1
+ }
+
+ return if (extensionName > -1) {
+ context.getString(extensionName)
+ } else {
+ // Fall back to the extension if we can't find a special name for this format.
+ MimeTypeMap.getSingleton().getExtensionFromMimeType(fromExtension)?.uppercase()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/oxycblt/musikr/Music.kt b/app/src/main/java/org/oxycblt/musikr/Music.kt
index 6e3f8fcbe..effa83116 100644
--- a/app/src/main/java/org/oxycblt/musikr/Music.kt
+++ b/app/src/main/java/org/oxycblt/musikr/Music.kt
@@ -30,7 +30,6 @@ import kotlinx.parcelize.Parcelize
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.music.MusicType
import org.oxycblt.musikr.cover.Cover
-import org.oxycblt.musikr.fs.MimeType
import org.oxycblt.musikr.fs.Path
import org.oxycblt.musikr.tag.Date
import org.oxycblt.musikr.tag.Disc
diff --git a/app/src/main/java/org/oxycblt/musikr/fs/Fs.kt b/app/src/main/java/org/oxycblt/musikr/fs/Fs.kt
deleted file mode 100644
index 245999190..000000000
--- a/app/src/main/java/org/oxycblt/musikr/fs/Fs.kt
+++ /dev/null
@@ -1,319 +0,0 @@
-/*
- * Copyright (c) 2022 Auxio Project
- * Fs.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.musikr.fs
-
-import android.content.Context
-import android.media.MediaFormat
-import android.os.storage.StorageManager
-import android.os.storage.StorageVolume
-import android.webkit.MimeTypeMap
-import java.io.File
-import javax.inject.Inject
-import org.oxycblt.auxio.R
-
-/**
- * An abstraction of an android file system path, including the volume and relative path.
- *
- * @param volume The volume that the path is on.
- * @param components The components of the path of the file, relative to the root of the volume.
- */
-data class Path(
- val volume: Volume,
- val components: Components,
-) {
- /** The name of the file/directory. */
- val name: String?
- get() = components.name
-
- /** The parent directory of the path, or itself if it's the root path. */
- val directory: Path
- get() = Path(volume, components.parent())
-
- /**
- * Transforms this [Path] into a "file" of the given name that's within the "directory"
- * represented by the current path. Ex. "/storage/emulated/0/Music" ->
- * "/storage/emulated/0/Music/file.mp3"
- *
- * @param fileName The name of the file to append to the path.
- * @return The new [Path] instance.
- */
- fun file(fileName: String) = Path(volume, components.child(fileName))
-
- /**
- * Resolves the [Path] in a human-readable format.
- *
- * @param context [Context] required to obtain human-readable strings.
- */
- fun resolve(context: Context) = "${volume.resolveName(context)}/$components"
-}
-
-sealed interface Volume {
- /** The name of the volume as it appears in MediaStore. */
- val mediaStoreName: String?
-
- /**
- * The components of the path to the volume, relative from the system root. Should not be used
- * except for compatibility purposes.
- */
- val components: Components?
-
- /** Resolves the name of the volume in a human-readable format. */
- fun resolveName(context: Context): String
-
- /** A volume representing the device's internal storage. */
- interface Internal : Volume
-
- /** A volume representing an external storage device, identified by a UUID. */
- interface External : Volume {
- /** The UUID of the volume. */
- val id: String?
- }
-}
-
-/**
- * The components of a path. This allows the path to be manipulated without having tp handle
- * separator parsing.
- *
- * @param components The components of the path.
- */
-@JvmInline
-value class Components private constructor(val components: List) {
- /** The name of the file/directory. */
- val name: String?
- get() = components.lastOrNull()
-
- override fun toString() = unixString
-
- /** Formats these components using the unix file separator (/) */
- val unixString: String
- get() = components.joinToString(File.separator)
-
- /** Formats these components using the windows file separator (\). */
- val windowsString: String
- get() = components.joinToString("\\")
-
- /**
- * Returns a new [Components] instance with the last element of the path removed as a "parent"
- * element of the original instance.
- *
- * @return The new [Components] instance, or the original instance if it's the root path.
- */
- fun parent() = Components(components.dropLast(1))
-
- /**
- * Returns a new [Components] instance with the given name appended to the end of the path as a
- * "child" element of the original instance.
- *
- * @param name The name of the file/directory to append to the path.
- */
- fun child(name: String) =
- if (name.isNotEmpty()) {
- Components(components + name.trimSlashes())
- } else {
- this
- }
-
- /**
- * Removes the first [n] elements of the path, effectively resulting in a path that is n levels
- * deep.
- *
- * @param n The number of elements to remove.
- * @return The new [Components] instance.
- */
- fun depth(n: Int) = Components(components.drop(n))
-
- /**
- * Concatenates this [Components] instance with another.
- *
- * @param other The [Components] instance to concatenate with.
- * @return The new [Components] instance.
- */
- fun child(other: Components) = Components(components + other.components)
-
- /**
- * Returns the given [Components] has a prefix equal to this [Components] instance. Effectively,
- * as if the given [Components] instance was a child of this [Components] instance.
- */
- fun contains(other: Components): Boolean {
- if (other.components.size < components.size) {
- return false
- }
-
- return components == other.components.take(components.size)
- }
-
- fun containing(other: Components) = Components(other.components.drop(components.size))
-
- companion object {
- fun nil() = Components(listOf())
-
- /**
- * Parses a path string into a [Components] instance by the unix path separator (/).
- *
- * @param path The path string to parse.
- * @return The [Components] instance.
- */
- fun parseUnix(path: String) =
- Components(path.trimSlashes().split(File.separatorChar).filter { it.isNotEmpty() })
-
- /**
- * Parses a path string into a [Components] instance by the windows path separator.
- *
- * @param path The path string to parse.
- * @return The [Components] instance.
- */
- fun parseWindows(path: String) =
- Components(path.trimSlashes().split('\\').filter { it.isNotEmpty() })
-
- private fun String.trimSlashes() = trimStart(File.separatorChar).trimEnd(File.separatorChar)
- }
-}
-
-/** A wrapper around [StorageManager] that provides instances of the [Volume] interface. */
-interface VolumeManager {
- /**
- * The internal storage volume of the device.
- *
- * @see StorageManager.getPrimaryStorageVolume
- */
- fun getInternalVolume(): Volume.Internal
-
- /**
- * The list of [Volume]s currently recognized by [StorageManager].
- *
- * @see StorageManager.getStorageVolumes
- */
- fun getVolumes(): List
-}
-
-class VolumeManagerImpl @Inject constructor(private val storageManager: StorageManager) :
- VolumeManager {
- override fun getInternalVolume(): Volume.Internal =
- InternalVolumeImpl(storageManager.primaryStorageVolume)
-
- override fun getVolumes() =
- storageManager.storageVolumesCompat.map {
- if (it.isInternalCompat) {
- InternalVolumeImpl(it)
- } else {
- ExternalVolumeImpl(it)
- }
- }
-
- private data class InternalVolumeImpl(val storageVolume: StorageVolume) : Volume.Internal {
- override val mediaStoreName
- get() = storageVolume.mediaStoreVolumeNameCompat
-
- override val components
- get() = storageVolume.directoryCompat?.let(Components.Companion::parseUnix)
-
- override fun resolveName(context: Context) = storageVolume.getDescriptionCompat(context)
- }
-
- private data class ExternalVolumeImpl(val storageVolume: StorageVolume) : Volume.External {
- override val id
- get() = storageVolume.uuidCompat
-
- override val mediaStoreName
- get() = storageVolume.mediaStoreVolumeNameCompat
-
- override val components
- get() = storageVolume.directoryCompat?.let(Components.Companion::parseUnix)
-
- override fun resolveName(context: Context) = storageVolume.getDescriptionCompat(context)
- }
-}
-
-/**
- * A mime type of a file. Only intended for display.
- *
- * @param fromExtension The mime type obtained by analyzing the file extension.
- * @param fromFormat The mime type obtained by analyzing the file format. Null if could not be
- * obtained.
- * @author Alexander Capehart (OxygenCobalt)
- *
- * TODO: Get around to simplifying this
- */
-data class MimeType(val fromExtension: String, val fromFormat: String?) {
- /**
- * Resolve the mime type into a human-readable format name, such as "Ogg Vorbis".
- *
- * @param context [Context] required to obtain human-readable strings.
- * @return A human-readable name for this mime type. Will first try [fromFormat], then falling
- * back to [fromExtension], and then null if that fails.
- */
- fun resolveName(context: Context): String? {
- // We try our best to produce a more readable name for the common audio formats.
- val formatName =
- when (fromFormat) {
- // We start with the extracted mime types, as they are more consistent. Note that
- // we do not include container formats at all with these names. It is only the
- // inner codec that we bother with.
- MediaFormat.MIMETYPE_AUDIO_MPEG -> R.string.cdc_mp3
- MediaFormat.MIMETYPE_AUDIO_AAC -> R.string.cdc_aac
- MediaFormat.MIMETYPE_AUDIO_VORBIS -> R.string.cdc_vorbis
- MediaFormat.MIMETYPE_AUDIO_OPUS -> R.string.cdc_opus
- MediaFormat.MIMETYPE_AUDIO_FLAC -> R.string.cdc_flac
- // TODO: Add ALAC to this as soon as I can stop using MediaFormat for
- // extracting metadata and just use ExoPlayer.
- // We don't give a name to more unpopular formats.
- else -> -1
- }
-
- if (formatName > -1) {
- return context.getString(formatName)
- }
-
- // Fall back to the file extension in the case that we have no mime type or
- // a useless "audio/raw" mime type. Here:
- // - We return names for container formats instead of the inner format, as we
- // cannot parse the file.
- // - We are at the mercy of the Android OS, hence we check for every possible mime
- // type for a particular format according to Wikipedia.
- val extensionName =
- when (fromExtension) {
- "audio/mpeg",
- "audio/mp3" -> R.string.cdc_mp3
- "audio/mp4",
- "audio/mp4a-latm",
- "audio/mpeg4-generic" -> R.string.cdc_mp4
- "audio/aac",
- "audio/aacp",
- "audio/3gpp",
- "audio/3gpp2" -> R.string.cdc_aac
- "audio/ogg",
- "application/ogg",
- "application/x-ogg" -> R.string.cdc_ogg
- "audio/flac" -> R.string.cdc_flac
- "audio/wav",
- "audio/x-wav",
- "audio/wave",
- "audio/vnd.wave" -> R.string.cdc_wav
- "audio/x-matroska" -> R.string.cdc_mka
- else -> -1
- }
-
- return if (extensionName > -1) {
- context.getString(extensionName)
- } else {
- // Fall back to the extension if we can't find a special name for this format.
- MimeTypeMap.getSingleton().getExtensionFromMimeType(fromExtension)?.uppercase()
- }
- }
-}
diff --git a/app/src/main/java/org/oxycblt/musikr/fs/FsModule.kt b/app/src/main/java/org/oxycblt/musikr/fs/FsModule.kt
index ff7ea4841..5169cc847 100644
--- a/app/src/main/java/org/oxycblt/musikr/fs/FsModule.kt
+++ b/app/src/main/java/org/oxycblt/musikr/fs/FsModule.kt
@@ -31,19 +31,12 @@ import org.oxycblt.musikr.fs.path.DocumentPathFactory
import org.oxycblt.musikr.fs.path.DocumentPathFactoryImpl
import org.oxycblt.musikr.fs.path.MediaStorePathInterpreter
import org.oxycblt.auxio.util.getSystemServiceCompat
+import org.oxycblt.musikr.fs.path.VolumeManager
+import org.oxycblt.musikr.fs.path.VolumeManagerImpl
@Module
@InstallIn(SingletonComponent::class)
-class FsModule {
- @Provides
- fun volumeManager(@ApplicationContext context: Context): VolumeManager =
- VolumeManagerImpl(context.getSystemServiceCompat(StorageManager::class))
-
- @Provides
- fun mediaStorePathInterpreterFactory(
- volumeManager: VolumeManager
- ): MediaStorePathInterpreter.Factory = MediaStorePathInterpreter.Factory.from(volumeManager)
-
+class FsProvidesModule {
@Provides
fun contentResolver(@ApplicationContext context: Context): ContentResolver =
context.contentResolverSafe
@@ -52,9 +45,6 @@ class FsModule {
@Module
@InstallIn(SingletonComponent::class)
interface FsBindsModule {
- @Binds
- fun documentPathFactory(documentTreePathFactory: DocumentPathFactoryImpl): DocumentPathFactory
-
@Binds fun deviceFiles(deviceFilesImpl: DeviceFilesImpl): DeviceFiles
@Binds fun musicLocationFactory(musicLocationFactoryImpl: MusicLocationFactoryImpl): MusicLocation.Factory
diff --git a/app/src/main/java/org/oxycblt/musikr/fs/Path.kt b/app/src/main/java/org/oxycblt/musikr/fs/Path.kt
new file mode 100644
index 000000000..9ad2fcf19
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/musikr/fs/Path.kt
@@ -0,0 +1,162 @@
+package org.oxycblt.musikr.fs
+
+import android.content.Context
+import java.io.File
+
+/**
+ * An abstraction of an android file system path, including the volume and relative path.
+ *
+ * @param volume The volume that the path is on.
+ * @param components The components of the path of the file, relative to the root of the volume.
+ */
+data class Path(
+ val volume: Volume,
+ val components: Components,
+) {
+ /** The name of the file/directory. */
+ val name: String?
+ get() = components.name
+
+ /** The parent directory of the path, or itself if it's the root path. */
+ val directory: Path
+ get() = Path(volume, components.parent())
+
+ /**
+ * Transforms this [Path] into a "file" of the given name that's within the "directory"
+ * represented by the current path. Ex. "/storage/emulated/0/Music" ->
+ * "/storage/emulated/0/Music/file.mp3"
+ *
+ * @param fileName The name of the file to append to the path.
+ * @return The new [Path] instance.
+ */
+ fun file(fileName: String) = Path(volume, components.child(fileName))
+
+ /**
+ * Resolves the [Path] in a human-readable format.
+ *
+ * @param context [Context] required to obtain human-readable strings.
+ */
+ fun resolve(context: Context) = "${volume.resolveName(context)}/$components"
+}
+
+sealed interface Volume {
+ /** The name of the volume as it appears in MediaStore. */
+ val mediaStoreName: String?
+
+ /**
+ * The components of the path to the volume, relative from the system root. Should not be used
+ * except for compatibility purposes.
+ */
+ val components: Components?
+
+ /** Resolves the name of the volume in a human-readable format. */
+ fun resolveName(context: Context): String
+
+ /** A volume representing the device's internal storage. */
+ interface Internal : Volume
+
+ /** A volume representing an external storage device, identified by a UUID. */
+ interface External : Volume {
+ /** The UUID of the volume. */
+ val id: String?
+ }
+}
+
+/**
+ * The components of a path. This allows the path to be manipulated without having tp handle
+ * separator parsing.
+ *
+ * @param components The components of the path.
+ */
+@JvmInline
+value class Components private constructor(val components: List) {
+ /** The name of the file/directory. */
+ val name: String?
+ get() = components.lastOrNull()
+
+ override fun toString() = unixString
+
+ /** Formats these components using the unix file separator (/) */
+ val unixString: String
+ get() = components.joinToString(File.separator)
+
+ /** Formats these components using the windows file separator (\). */
+ val windowsString: String
+ get() = components.joinToString("\\")
+
+ /**
+ * Returns a new [Components] instance with the last element of the path removed as a "parent"
+ * element of the original instance.
+ *
+ * @return The new [Components] instance, or the original instance if it's the root path.
+ */
+ fun parent() = Components(components.dropLast(1))
+
+ /**
+ * Returns a new [Components] instance with the given name appended to the end of the path as a
+ * "child" element of the original instance.
+ *
+ * @param name The name of the file/directory to append to the path.
+ */
+ fun child(name: String) =
+ if (name.isNotEmpty()) {
+ Components(components + name.trimSlashes())
+ } else {
+ this
+ }
+
+ /**
+ * Removes the first [n] elements of the path, effectively resulting in a path that is n levels
+ * deep.
+ *
+ * @param n The number of elements to remove.
+ * @return The new [Components] instance.
+ */
+ fun depth(n: Int) = Components(components.drop(n))
+
+ /**
+ * Concatenates this [Components] instance with another.
+ *
+ * @param other The [Components] instance to concatenate with.
+ * @return The new [Components] instance.
+ */
+ fun child(other: Components) = Components(components + other.components)
+
+ /**
+ * Returns the given [Components] has a prefix equal to this [Components] instance. Effectively,
+ * as if the given [Components] instance was a child of this [Components] instance.
+ */
+ fun contains(other: Components): Boolean {
+ if (other.components.size < components.size) {
+ return false
+ }
+
+ return components == other.components.take(components.size)
+ }
+
+ fun containing(other: Components) = Components(other.components.drop(components.size))
+
+ companion object {
+ fun nil() = Components(listOf())
+
+ /**
+ * Parses a path string into a [Components] instance by the unix path separator (/).
+ *
+ * @param path The path string to parse.
+ * @return The [Components] instance.
+ */
+ fun parseUnix(path: String) =
+ Components(path.trimSlashes().split(File.separatorChar).filter { it.isNotEmpty() })
+
+ /**
+ * Parses a path string into a [Components] instance by the windows path separator.
+ *
+ * @param path The path string to parse.
+ * @return The [Components] instance.
+ */
+ fun parseWindows(path: String) =
+ Components(path.trimSlashes().split('\\').filter { it.isNotEmpty() })
+
+ private fun String.trimSlashes() = trimStart(File.separatorChar).trimEnd(File.separatorChar)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/oxycblt/musikr/fs/path/DocumentPathFactory.kt b/app/src/main/java/org/oxycblt/musikr/fs/path/DocumentPathFactory.kt
index 5820e7af3..c85bbdbee 100644
--- a/app/src/main/java/org/oxycblt/musikr/fs/path/DocumentPathFactory.kt
+++ b/app/src/main/java/org/oxycblt/musikr/fs/path/DocumentPathFactory.kt
@@ -28,7 +28,6 @@ import javax.inject.Inject
import org.oxycblt.musikr.fs.Components
import org.oxycblt.musikr.fs.Path
import org.oxycblt.musikr.fs.Volume
-import org.oxycblt.musikr.fs.VolumeManager
import org.oxycblt.musikr.fs.contentResolverSafe
import org.oxycblt.musikr.fs.useQuery
diff --git a/app/src/main/java/org/oxycblt/musikr/fs/path/MediaStorePathInterpreter.kt b/app/src/main/java/org/oxycblt/musikr/fs/path/MediaStorePathInterpreter.kt
index d93fef53e..5c505f2eb 100644
--- a/app/src/main/java/org/oxycblt/musikr/fs/path/MediaStorePathInterpreter.kt
+++ b/app/src/main/java/org/oxycblt/musikr/fs/path/MediaStorePathInterpreter.kt
@@ -23,7 +23,6 @@ import android.os.Build
import android.provider.MediaStore
import org.oxycblt.musikr.fs.Components
import org.oxycblt.musikr.fs.Path
-import org.oxycblt.musikr.fs.VolumeManager
import timber.log.Timber as L
/**
diff --git a/app/src/main/java/org/oxycblt/musikr/fs/path/PathModule.kt b/app/src/main/java/org/oxycblt/musikr/fs/path/PathModule.kt
new file mode 100644
index 000000000..7e75dc81a
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/musikr/fs/path/PathModule.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2024 Auxio Project
+ * FsModule.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.musikr.fs.path
+
+import android.content.ContentResolver
+import android.content.Context
+import android.os.storage.StorageManager
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import org.oxycblt.musikr.fs.path.DocumentPathFactory
+import org.oxycblt.musikr.fs.path.DocumentPathFactoryImpl
+import org.oxycblt.musikr.fs.path.MediaStorePathInterpreter
+import org.oxycblt.auxio.util.getSystemServiceCompat
+import org.oxycblt.musikr.fs.path.VolumeManager
+import org.oxycblt.musikr.fs.path.VolumeManagerImpl
+
+@Module
+@InstallIn(SingletonComponent::class)
+class PathModule {
+ @Provides
+ fun volumeManager(@ApplicationContext context: Context): VolumeManager =
+ VolumeManagerImpl(context.getSystemServiceCompat(StorageManager::class))
+
+ @Provides
+ fun mediaStorePathInterpreterFactory(
+ volumeManager: VolumeManager
+ ): MediaStorePathInterpreter.Factory = MediaStorePathInterpreter.Factory.from(volumeManager)
+}
+
+@Module
+@InstallIn(SingletonComponent::class)
+interface PathBindsModule {
+ @Binds
+ fun documentPathFactory(documentTreePathFactory: DocumentPathFactoryImpl): DocumentPathFactory
+}
diff --git a/app/src/main/java/org/oxycblt/musikr/fs/path/VolumeManager.kt b/app/src/main/java/org/oxycblt/musikr/fs/path/VolumeManager.kt
new file mode 100644
index 000000000..72c56bde7
--- /dev/null
+++ b/app/src/main/java/org/oxycblt/musikr/fs/path/VolumeManager.kt
@@ -0,0 +1,69 @@
+package org.oxycblt.musikr.fs.path
+
+import android.content.Context
+import android.os.storage.StorageManager
+import android.os.storage.StorageVolume
+import org.oxycblt.musikr.fs.Components
+import org.oxycblt.musikr.fs.Volume
+import org.oxycblt.musikr.fs.directoryCompat
+import org.oxycblt.musikr.fs.getDescriptionCompat
+import org.oxycblt.musikr.fs.isInternalCompat
+import org.oxycblt.musikr.fs.mediaStoreVolumeNameCompat
+import org.oxycblt.musikr.fs.storageVolumesCompat
+import org.oxycblt.musikr.fs.uuidCompat
+import javax.inject.Inject
+
+/** A wrapper around [StorageManager] that provides instances of the [Volume] interface. */
+interface VolumeManager {
+ /**
+ * The internal storage volume of the device.
+ *
+ * @see StorageManager.getPrimaryStorageVolume
+ */
+ fun getInternalVolume(): Volume.Internal
+
+ /**
+ * The list of [Volume]s currently recognized by [StorageManager].
+ *
+ * @see StorageManager.getStorageVolumes
+ */
+ fun getVolumes(): List
+}
+
+class VolumeManagerImpl @Inject constructor(private val storageManager: StorageManager) :
+ VolumeManager {
+ override fun getInternalVolume(): Volume.Internal =
+ InternalVolumeImpl(storageManager.primaryStorageVolume)
+
+ override fun getVolumes() =
+ storageManager.storageVolumesCompat.map {
+ if (it.isInternalCompat) {
+ InternalVolumeImpl(it)
+ } else {
+ ExternalVolumeImpl(it)
+ }
+ }
+
+ private data class InternalVolumeImpl(val storageVolume: StorageVolume) : Volume.Internal {
+ override val mediaStoreName
+ get() = storageVolume.mediaStoreVolumeNameCompat
+
+ override val components
+ get() = storageVolume.directoryCompat?.let(Components.Companion::parseUnix)
+
+ override fun resolveName(context: Context) = storageVolume.getDescriptionCompat(context)
+ }
+
+ private data class ExternalVolumeImpl(val storageVolume: StorageVolume) : Volume.External {
+ override val id
+ get() = storageVolume.uuidCompat
+
+ override val mediaStoreName
+ get() = storageVolume.mediaStoreVolumeNameCompat
+
+ override val components
+ get() = storageVolume.directoryCompat?.let(Components.Companion::parseUnix)
+
+ override fun resolveName(context: Context) = storageVolume.getDescriptionCompat(context)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/oxycblt/musikr/metadata/AudioProperties.kt b/app/src/main/java/org/oxycblt/musikr/metadata/AudioProperties.kt
index 38df57b45..628800a9b 100644
--- a/app/src/main/java/org/oxycblt/musikr/metadata/AudioProperties.kt
+++ b/app/src/main/java/org/oxycblt/musikr/metadata/AudioProperties.kt
@@ -24,7 +24,7 @@ import android.media.MediaFormat
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import org.oxycblt.musikr.Song
-import org.oxycblt.musikr.fs.MimeType
+import org.oxycblt.musikr.MimeType
import timber.log.Timber as L
/**
diff --git a/app/src/main/java/org/oxycblt/musikr/playlist/m3u/M3U.kt b/app/src/main/java/org/oxycblt/musikr/playlist/m3u/M3U.kt
index 79c3c9dee..c8cd12e42 100644
--- a/app/src/main/java/org/oxycblt/musikr/playlist/m3u/M3U.kt
+++ b/app/src/main/java/org/oxycblt/musikr/playlist/m3u/M3U.kt
@@ -31,7 +31,7 @@ import org.oxycblt.musikr.resolveNames
import org.oxycblt.musikr.fs.Components
import org.oxycblt.musikr.fs.Path
import org.oxycblt.musikr.fs.Volume
-import org.oxycblt.musikr.fs.VolumeManager
+import org.oxycblt.musikr.fs.path.VolumeManager
import org.oxycblt.musikr.playlist.ExportConfig
import org.oxycblt.musikr.playlist.ImportedPlaylist
import org.oxycblt.musikr.playlist.PossiblePaths
diff --git a/app/src/main/java/org/oxycblt/musikr/tag/interpret/PreMusic.kt b/app/src/main/java/org/oxycblt/musikr/tag/interpret/PreMusic.kt
index ce2526f05..ee4ae1a0a 100644
--- a/app/src/main/java/org/oxycblt/musikr/tag/interpret/PreMusic.kt
+++ b/app/src/main/java/org/oxycblt/musikr/tag/interpret/PreMusic.kt
@@ -22,7 +22,7 @@ import android.net.Uri
import java.util.UUID
import org.oxycblt.musikr.Music
import org.oxycblt.auxio.music.MusicType
-import org.oxycblt.musikr.fs.MimeType
+import org.oxycblt.musikr.MimeType
import org.oxycblt.musikr.fs.Path
import org.oxycblt.musikr.playlist.PlaylistHandle
import org.oxycblt.musikr.tag.Date
diff --git a/app/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt b/app/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt
index 21534c592..309488ece 100644
--- a/app/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt
+++ b/app/src/main/java/org/oxycblt/musikr/tag/interpret/TagInterpreter.kt
@@ -21,7 +21,7 @@ package org.oxycblt.musikr.tag.interpret
import javax.inject.Inject
import org.oxycblt.auxio.R
import org.oxycblt.musikr.fs.DeviceFile
-import org.oxycblt.musikr.fs.MimeType
+import org.oxycblt.musikr.MimeType
import org.oxycblt.musikr.tag.Disc
import org.oxycblt.musikr.tag.Interpretation
import org.oxycblt.musikr.tag.Name