musikr: break apart Fs.kt
This commit is contained in:
parent
3eaa96ffda
commit
2a38d1ae8d
13 changed files with 377 additions and 339 deletions
84
app/src/main/java/org/oxycblt/musikr/MimeType.kt
Normal file
84
app/src/main/java/org/oxycblt/musikr/MimeType.kt
Normal file
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -30,7 +30,6 @@ import kotlinx.parcelize.Parcelize
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.music.MusicType
|
import org.oxycblt.auxio.music.MusicType
|
||||||
import org.oxycblt.musikr.cover.Cover
|
import org.oxycblt.musikr.cover.Cover
|
||||||
import org.oxycblt.musikr.fs.MimeType
|
|
||||||
import org.oxycblt.musikr.fs.Path
|
import org.oxycblt.musikr.fs.Path
|
||||||
import org.oxycblt.musikr.tag.Date
|
import org.oxycblt.musikr.tag.Date
|
||||||
import org.oxycblt.musikr.tag.Disc
|
import org.oxycblt.musikr.tag.Disc
|
||||||
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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<String>) {
|
|
||||||
/** 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<Volume>
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -31,19 +31,12 @@ import org.oxycblt.musikr.fs.path.DocumentPathFactory
|
||||||
import org.oxycblt.musikr.fs.path.DocumentPathFactoryImpl
|
import org.oxycblt.musikr.fs.path.DocumentPathFactoryImpl
|
||||||
import org.oxycblt.musikr.fs.path.MediaStorePathInterpreter
|
import org.oxycblt.musikr.fs.path.MediaStorePathInterpreter
|
||||||
import org.oxycblt.auxio.util.getSystemServiceCompat
|
import org.oxycblt.auxio.util.getSystemServiceCompat
|
||||||
|
import org.oxycblt.musikr.fs.path.VolumeManager
|
||||||
|
import org.oxycblt.musikr.fs.path.VolumeManagerImpl
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
class FsModule {
|
class FsProvidesModule {
|
||||||
@Provides
|
|
||||||
fun volumeManager(@ApplicationContext context: Context): VolumeManager =
|
|
||||||
VolumeManagerImpl(context.getSystemServiceCompat(StorageManager::class))
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
fun mediaStorePathInterpreterFactory(
|
|
||||||
volumeManager: VolumeManager
|
|
||||||
): MediaStorePathInterpreter.Factory = MediaStorePathInterpreter.Factory.from(volumeManager)
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
fun contentResolver(@ApplicationContext context: Context): ContentResolver =
|
fun contentResolver(@ApplicationContext context: Context): ContentResolver =
|
||||||
context.contentResolverSafe
|
context.contentResolverSafe
|
||||||
|
@ -52,9 +45,6 @@ class FsModule {
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
interface FsBindsModule {
|
interface FsBindsModule {
|
||||||
@Binds
|
|
||||||
fun documentPathFactory(documentTreePathFactory: DocumentPathFactoryImpl): DocumentPathFactory
|
|
||||||
|
|
||||||
@Binds fun deviceFiles(deviceFilesImpl: DeviceFilesImpl): DeviceFiles
|
@Binds fun deviceFiles(deviceFilesImpl: DeviceFilesImpl): DeviceFiles
|
||||||
|
|
||||||
@Binds fun musicLocationFactory(musicLocationFactoryImpl: MusicLocationFactoryImpl): MusicLocation.Factory
|
@Binds fun musicLocationFactory(musicLocationFactoryImpl: MusicLocationFactoryImpl): MusicLocation.Factory
|
||||||
|
|
162
app/src/main/java/org/oxycblt/musikr/fs/Path.kt
Normal file
162
app/src/main/java/org/oxycblt/musikr/fs/Path.kt
Normal file
|
@ -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<String>) {
|
||||||
|
/** 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,7 +28,6 @@ import javax.inject.Inject
|
||||||
import org.oxycblt.musikr.fs.Components
|
import org.oxycblt.musikr.fs.Components
|
||||||
import org.oxycblt.musikr.fs.Path
|
import org.oxycblt.musikr.fs.Path
|
||||||
import org.oxycblt.musikr.fs.Volume
|
import org.oxycblt.musikr.fs.Volume
|
||||||
import org.oxycblt.musikr.fs.VolumeManager
|
|
||||||
import org.oxycblt.musikr.fs.contentResolverSafe
|
import org.oxycblt.musikr.fs.contentResolverSafe
|
||||||
import org.oxycblt.musikr.fs.useQuery
|
import org.oxycblt.musikr.fs.useQuery
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,6 @@ import android.os.Build
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import org.oxycblt.musikr.fs.Components
|
import org.oxycblt.musikr.fs.Components
|
||||||
import org.oxycblt.musikr.fs.Path
|
import org.oxycblt.musikr.fs.Path
|
||||||
import org.oxycblt.musikr.fs.VolumeManager
|
|
||||||
import timber.log.Timber as L
|
import timber.log.Timber as L
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
55
app/src/main/java/org/oxycblt/musikr/fs/path/PathModule.kt
Normal file
55
app/src/main/java/org/oxycblt/musikr/fs/path/PathModule.kt
Normal file
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
|
@ -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<Volume>
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,7 +24,7 @@ import android.media.MediaFormat
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import org.oxycblt.musikr.Song
|
import org.oxycblt.musikr.Song
|
||||||
import org.oxycblt.musikr.fs.MimeType
|
import org.oxycblt.musikr.MimeType
|
||||||
import timber.log.Timber as L
|
import timber.log.Timber as L
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -31,7 +31,7 @@ import org.oxycblt.musikr.resolveNames
|
||||||
import org.oxycblt.musikr.fs.Components
|
import org.oxycblt.musikr.fs.Components
|
||||||
import org.oxycblt.musikr.fs.Path
|
import org.oxycblt.musikr.fs.Path
|
||||||
import org.oxycblt.musikr.fs.Volume
|
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.ExportConfig
|
||||||
import org.oxycblt.musikr.playlist.ImportedPlaylist
|
import org.oxycblt.musikr.playlist.ImportedPlaylist
|
||||||
import org.oxycblt.musikr.playlist.PossiblePaths
|
import org.oxycblt.musikr.playlist.PossiblePaths
|
||||||
|
|
|
@ -22,7 +22,7 @@ import android.net.Uri
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import org.oxycblt.musikr.Music
|
import org.oxycblt.musikr.Music
|
||||||
import org.oxycblt.auxio.music.MusicType
|
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.fs.Path
|
||||||
import org.oxycblt.musikr.playlist.PlaylistHandle
|
import org.oxycblt.musikr.playlist.PlaylistHandle
|
||||||
import org.oxycblt.musikr.tag.Date
|
import org.oxycblt.musikr.tag.Date
|
||||||
|
|
|
@ -21,7 +21,7 @@ package org.oxycblt.musikr.tag.interpret
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.musikr.fs.DeviceFile
|
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.Disc
|
||||||
import org.oxycblt.musikr.tag.Interpretation
|
import org.oxycblt.musikr.tag.Interpretation
|
||||||
import org.oxycblt.musikr.tag.Name
|
import org.oxycblt.musikr.tag.Name
|
||||||
|
|
Loading…
Reference in a new issue