From 364675b252042497d30382cb61be732d700637b2 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Tue, 19 Dec 2023 22:14:59 -0700 Subject: [PATCH] music: revamp paths Revamp paths with an entirely new abstraction that should improve testability and integration with M3U playlists. --- .../oxycblt/auxio/detail/SongDetailDialog.kt | 5 +- .../org/oxycblt/auxio/music/MusicSettings.kt | 16 +- .../auxio/music/device/DeviceMusicImpl.kt | 22 +- .../oxycblt/auxio/music/device/RawMusic.kt | 4 +- .../music/{fs => dirs}/DirectoryAdapter.kt | 55 ++-- .../auxio/music/dirs/DirectoryModule.kt | 31 +++ .../music/dirs/DocumentTreePathFactory.kt | 104 ++++++++ .../auxio/music/dirs/MusicDirectories.kt | 31 +++ .../music/{fs => dirs}/MusicDirsDialog.kt | 33 +-- .../java/org/oxycblt/auxio/music/fs/Fs.kt | 239 +++++++++++------- .../org/oxycblt/auxio/music/fs/FsModule.kt | 10 +- .../auxio/music/fs/MediaStoreExtractor.kt | 76 +++--- .../org/oxycblt/auxio/search/SearchEngine.kt | 2 +- .../main/res/layout/item_song_property.xml | 2 +- app/src/main/res/navigation/outer.xml | 2 +- app/src/main/res/values-ar-rIQ/strings.xml | 2 - app/src/main/res/values-ar/strings.xml | 1 - app/src/main/res/values-be/strings.xml | 2 - app/src/main/res/values-cs/strings.xml | 2 - app/src/main/res/values-de/strings.xml | 2 - app/src/main/res/values-el/strings.xml | 1 - app/src/main/res/values-es/strings.xml | 2 - app/src/main/res/values-fi/strings.xml | 2 - app/src/main/res/values-fil/strings.xml | 1 - app/src/main/res/values-fr/strings.xml | 2 - app/src/main/res/values-gl/strings.xml | 2 - app/src/main/res/values-hi/strings.xml | 2 - app/src/main/res/values-hr/strings.xml | 2 - app/src/main/res/values-hu/strings.xml | 2 - app/src/main/res/values-in/strings.xml | 2 - app/src/main/res/values-it/strings.xml | 2 - app/src/main/res/values-iw/strings.xml | 2 - app/src/main/res/values-ja/strings.xml | 2 - app/src/main/res/values-ko/strings.xml | 2 - app/src/main/res/values-lt/strings.xml | 2 - app/src/main/res/values-ml/strings.xml | 1 - app/src/main/res/values-nb-rNO/strings.xml | 2 - app/src/main/res/values-nl/strings.xml | 2 - app/src/main/res/values-pa/strings.xml | 2 - app/src/main/res/values-pl/strings.xml | 2 - app/src/main/res/values-pt-rBR/strings.xml | 2 - app/src/main/res/values-pt-rPT/strings.xml | 2 - app/src/main/res/values-ro/strings.xml | 2 - app/src/main/res/values-ru/strings.xml | 2 - app/src/main/res/values-sl/strings.xml | 2 - app/src/main/res/values-sv/strings.xml | 2 - app/src/main/res/values-tr/strings.xml | 2 - app/src/main/res/values-uk/strings.xml | 2 - app/src/main/res/values-zh-rCN/strings.xml | 2 - app/src/main/res/values/strings.xml | 3 +- 50 files changed, 422 insertions(+), 277 deletions(-) rename app/src/main/java/org/oxycblt/auxio/music/{fs => dirs}/DirectoryAdapter.kt (70%) create mode 100644 app/src/main/java/org/oxycblt/auxio/music/dirs/DirectoryModule.kt create mode 100644 app/src/main/java/org/oxycblt/auxio/music/dirs/DocumentTreePathFactory.kt create mode 100644 app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirectories.kt rename app/src/main/java/org/oxycblt/auxio/music/{fs => dirs}/MusicDirsDialog.kt (84%) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt index f43da103c..d7c682ce6 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt @@ -102,10 +102,7 @@ class SongDetailDialog : ViewBindingMaterialDialogFragment { } } -class MusicSettingsImpl @Inject constructor(@ApplicationContext context: Context) : - Settings.Impl(context), MusicSettings { +class MusicSettingsImpl +@Inject +constructor( + @ApplicationContext context: Context, + val documentTreePathFactory: DocumentTreePathFactory +) : Settings.Impl(context), MusicSettings { private val storageManager = context.getSystemServiceCompat(StorageManager::class) override var musicDirs: MusicDirectories @@ -64,7 +68,7 @@ class MusicSettingsImpl @Inject constructor(@ApplicationContext context: Context val dirs = (sharedPreferences.getStringSet(getString(R.string.set_key_music_dirs), null) ?: emptySet()) - .mapNotNull { Directory.fromDocumentTreeUri(storageManager, it) } + .mapNotNull(documentTreePathFactory::deserializeDocumentTreePath) return MusicDirectories( dirs, sharedPreferences.getBoolean(getString(R.string.set_key_music_dirs_include), false)) @@ -73,7 +77,7 @@ class MusicSettingsImpl @Inject constructor(@ApplicationContext context: Context sharedPreferences.edit { putStringSet( getString(R.string.set_key_music_dirs), - value.dirs.map(Directory::toDocumentTreeUri).toSet()) + value.dirs.map(documentTreePathFactory::serializeDocumentTreePath).toSet()) putBoolean(getString(R.string.set_key_music_dirs_include), value.shouldInclude) apply() } diff --git a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt index ec71efbf4..e96768db9 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/device/DeviceMusicImpl.kt @@ -28,7 +28,6 @@ import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicType import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.fs.MimeType -import org.oxycblt.auxio.music.fs.Path import org.oxycblt.auxio.music.fs.toAudioUri import org.oxycblt.auxio.music.fs.toCoverUri import org.oxycblt.auxio.music.info.Date @@ -85,14 +84,10 @@ class SongImpl( requireNotNull(rawSong.mediaStoreId) { "Invalid raw ${rawSong.fileName}: No id" } .toAudioUri() override val path = - Path( - name = + requireNotNull(rawSong.directory) { "Invalid raw ${rawSong.fileName}: No parent directory" } + .file( requireNotNull(rawSong.fileName) { "Invalid raw ${rawSong.fileName}: No display name" - }, - parent = - requireNotNull(rawSong.directory) { - "Invalid raw ${rawSong.fileName}: No parent directory" }) override val mimeType = MimeType( @@ -247,11 +242,11 @@ class SongImpl( * @return This instance upcasted to [Song]. */ fun finalize(): Song { - checkNotNull(_album) { "Malformed song ${path.name}: No album" } + checkNotNull(_album) { "Malformed song ${path}: No album" } - check(_artists.isNotEmpty()) { "Malformed song ${path.name}: No artists" } + check(_artists.isNotEmpty()) { "Malformed song ${path}: No artists" } check(_artists.size == rawArtists.size) { - "Malformed song ${path.name}: Artist grouping mismatch" + "Malformed song ${path}: Artist grouping mismatch" } for (i in _artists.indices) { // Non-destructively reorder the linked artists so that they align with @@ -262,10 +257,8 @@ class SongImpl( _artists[i] = other } - check(_genres.isNotEmpty()) { "Malformed song ${path.name}: No genres" } - check(_genres.size == rawGenres.size) { - "Malformed song ${path.name}: Genre grouping mismatch" - } + check(_genres.isNotEmpty()) { "Malformed song ${path}: No genres" } + check(_genres.size == rawGenres.size) { "Malformed song ${path}: Genre grouping mismatch" } for (i in _genres.indices) { // Non-destructively reorder the linked genres so that they align with // the genre ordering within the song metadata. @@ -519,6 +512,7 @@ class ArtistImpl( return this } } + /** * Library-backed implementation of [Genre]. * 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 73fa3c753..1dea14722 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,7 +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.Directory +import org.oxycblt.auxio.music.fs.Path import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.info.ReleaseType @@ -44,7 +44,7 @@ data class RawSong( /** @see Song.path */ var fileName: String? = null, /** @see Song.path */ - var directory: Directory? = null, + var directory: Path? = null, /** @see Song.size */ var size: Long? = null, /** @see Song.durationMs */ diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/DirectoryAdapter.kt b/app/src/main/java/org/oxycblt/auxio/music/dirs/DirectoryAdapter.kt similarity index 70% rename from app/src/main/java/org/oxycblt/auxio/music/fs/DirectoryAdapter.kt rename to app/src/main/java/org/oxycblt/auxio/music/dirs/DirectoryAdapter.kt index 5e0799d72..9beedd79f 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/DirectoryAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/dirs/DirectoryAdapter.kt @@ -16,13 +16,14 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.fs +package org.oxycblt.auxio.music.dirs import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.databinding.ItemMusicDirBinding import org.oxycblt.auxio.list.recycler.DialogRecyclerView +import org.oxycblt.auxio.music.fs.Path import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.logD @@ -35,11 +36,11 @@ import org.oxycblt.auxio.util.logD */ class DirectoryAdapter(private val listener: Listener) : RecyclerView.Adapter() { - private val _dirs = mutableListOf() + private val _dirs = mutableListOf() /** - * The current list of [Directory]s, may not line up with [MusicDirectories] due to removals. + * The current list of [SystemPath]s, may not line up with [MusicDirectories] due to removals. */ - val dirs: List = _dirs + val dirs: List = _dirs override fun getItemCount() = dirs.size @@ -50,37 +51,37 @@ class DirectoryAdapter(private val listener: Listener) : holder.bind(dirs[position], listener) /** - * Add a [Directory] to the end of the list. + * Add a [Path] to the end of the list. * - * @param dir The [Directory] to add. + * @param path The [Path] to add. */ - fun add(dir: Directory) { - if (_dirs.contains(dir)) return - logD("Adding $dir") - _dirs.add(dir) + fun add(path: Path) { + if (_dirs.contains(path)) return + logD("Adding $path") + _dirs.add(path) notifyItemInserted(_dirs.lastIndex) } /** - * Add a list of [Directory] instances to the end of the list. + * Add a list of [Path] instances to the end of the list. * - * @param dirs The [Directory] instances to add. + * @param path The [Path] instances to add. */ - fun addAll(dirs: List) { - logD("Adding ${dirs.size} directories") - val oldLastIndex = dirs.lastIndex - _dirs.addAll(dirs) - notifyItemRangeInserted(oldLastIndex, dirs.size) + fun addAll(path: List) { + logD("Adding ${path.size} directories") + val oldLastIndex = path.lastIndex + _dirs.addAll(path) + notifyItemRangeInserted(oldLastIndex, path.size) } /** - * Remove a [Directory] from the list. + * Remove a [Path] from the list. * - * @param dir The [Directory] to remove. Must exist in the list. + * @param path The [Path] to remove. Must exist in the list. */ - fun remove(dir: Directory) { - logD("Removing $dir") - val idx = _dirs.indexOf(dir) + fun remove(path: Path) { + logD("Removing $path") + val idx = _dirs.indexOf(path) _dirs.removeAt(idx) notifyItemRemoved(idx) } @@ -88,7 +89,7 @@ class DirectoryAdapter(private val listener: Listener) : /** A Listener for [DirectoryAdapter] interactions. */ interface Listener { /** Called when the delete button on a directory item is clicked. */ - fun onRemoveDirectory(dir: Directory) + fun onRemoveDirectory(dir: Path) } } @@ -102,12 +103,12 @@ class MusicDirViewHolder private constructor(private val binding: ItemMusicDirBi /** * Bind new data to this instance. * - * @param dir The new [Directory] to bind. + * @param path The new [Path] to bind. * @param listener A [DirectoryAdapter.Listener] to bind interactions to. */ - fun bind(dir: Directory, listener: DirectoryAdapter.Listener) { - binding.dirPath.text = dir.resolveName(binding.context) - binding.dirDelete.setOnClickListener { listener.onRemoveDirectory(dir) } + fun bind(path: Path, listener: DirectoryAdapter.Listener) { + binding.dirPath.text = path.resolve(binding.context) + binding.dirDelete.setOnClickListener { listener.onRemoveDirectory(path) } } companion object { diff --git a/app/src/main/java/org/oxycblt/auxio/music/dirs/DirectoryModule.kt b/app/src/main/java/org/oxycblt/auxio/music/dirs/DirectoryModule.kt new file mode 100644 index 000000000..f88d71449 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/dirs/DirectoryModule.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 Auxio Project + * DirectoryModule.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.dirs + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +interface DirectoryModule { + @Binds + fun documentTreePathFactory(factory: DocumentTreePathFactoryImpl): DocumentTreePathFactory +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/dirs/DocumentTreePathFactory.kt b/app/src/main/java/org/oxycblt/auxio/music/dirs/DocumentTreePathFactory.kt new file mode 100644 index 000000000..038e90b19 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/dirs/DocumentTreePathFactory.kt @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2023 Auxio Project + * DocumentTreePathFactory.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.dirs + +import android.net.Uri +import android.provider.DocumentsContract +import java.io.File +import javax.inject.Inject +import org.oxycblt.auxio.music.fs.Components +import org.oxycblt.auxio.music.fs.Path +import org.oxycblt.auxio.music.fs.Volume +import org.oxycblt.auxio.music.fs.VolumeManager + +/** + * A factory for parsing the reverse-engineered format of the URIs obtained from the document tree + * (i.e directory) folder. + * + * @author Alexander Capehart (OxygenCobalt) + */ +interface DocumentTreePathFactory { + /** + * Unpacks a document tree URI into a [Path] instance, using [deserializeDocumentTreePath]. + * + * @param uri The document tree URI to unpack. + * @return The [Path] instance, or null if the URI could not be unpacked. + */ + fun unpackDocumentTreeUri(uri: Uri): Path? + + /** + * Serializes a [Path] instance into a document tree URI format path. + * + * @param path The [Path] instance to serialize. + * @return The serialized path. + */ + fun serializeDocumentTreePath(path: Path): String + + /** + * Deserializes a document tree URI format path into a [Path] instance. + * + * @param path The path to deserialize. + * @return The [Path] instance, or null if the path could not be deserialized. + */ + fun deserializeDocumentTreePath(path: String): Path? +} + +class DocumentTreePathFactoryImpl @Inject constructor(private val volumeManager: VolumeManager) : + DocumentTreePathFactory { + override fun unpackDocumentTreeUri(uri: Uri): Path? { + // Convert the document tree URI into it's relative path form, which can then be + // parsed into a Directory instance. + val docUri = + DocumentsContract.buildDocumentUriUsingTree( + uri, DocumentsContract.getTreeDocumentId(uri)) + val treeUri = DocumentsContract.getTreeDocumentId(docUri) + return deserializeDocumentTreePath(treeUri) + } + + override fun serializeDocumentTreePath(path: Path): String = + when (val volume = path.volume) { + // The primary storage has a volume prefix of "primary", regardless + // of if it's internal or not. + is Volume.Internal -> "$DOCUMENT_URI_PRIMARY_NAME:${path.components}" + // Document tree URIs consist of a prefixed volume name followed by a relative path. + is Volume.External -> "${volume.id}:${path.components}" + } + + override fun deserializeDocumentTreePath(path: String): Path? { + // Document tree URIs consist of a prefixed volume name followed by a relative path, + // delimited with a colon. + val split = path.split(File.pathSeparator, limit = 2) + val volume = + when (split[0]) { + // The primary storage has a volume prefix of "primary", regardless + // of if it's internal or not. + DOCUMENT_URI_PRIMARY_NAME -> volumeManager.getInternalVolume() + // Removable storage has a volume prefix of it's UUID, try to find it + // within StorageManager's volume list. + else -> + volumeManager.getVolumes().find { it is Volume.External && it.id == split[0] } + } + val relativePath = split.getOrNull(1) ?: return null + return Path(volume ?: return null, Components.parse(relativePath)) + } + + private companion object { + const val DOCUMENT_URI_PRIMARY_NAME = "primary" + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirectories.kt b/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirectories.kt new file mode 100644 index 000000000..c85b21ad5 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirectories.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 Auxio Project + * MusicDirectories.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.dirs + +import org.oxycblt.auxio.music.fs.Path + +/** + * Represents the configuration for specific directories to filter to/from when loading music. + * + * @param dirs A list of [Directory] instances. How these are interpreted depends on [shouldInclude] + * @param shouldInclude True if the library should only load from the [Directory] instances, false + * if the library should not load from the [Directory] instances. + * @author Alexander Capehart (OxygenCobalt) + */ +data class MusicDirectories(val dirs: List, val shouldInclude: Boolean) diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/MusicDirsDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirsDialog.kt similarity index 84% rename from app/src/main/java/org/oxycblt/auxio/music/fs/MusicDirsDialog.kt rename to app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirsDialog.kt index bca211f9a..1db3bb6c7 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/MusicDirsDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirsDialog.kt @@ -16,13 +16,11 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.fs +package org.oxycblt.auxio.music.dirs import android.content.ActivityNotFoundException import android.net.Uri import android.os.Bundle -import android.os.storage.StorageManager -import android.provider.DocumentsContract import android.view.LayoutInflater import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts @@ -35,8 +33,8 @@ import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogMusicDirsBinding import org.oxycblt.auxio.music.MusicSettings +import org.oxycblt.auxio.music.fs.Path import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment -import org.oxycblt.auxio.util.getSystemServiceCompat import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.showToast @@ -50,7 +48,7 @@ class MusicDirsDialog : ViewBindingMaterialDialogFragment(), DirectoryAdapter.Listener { private val dirAdapter = DirectoryAdapter(this) private var openDocumentTreeLauncher: ActivityResultLauncher? = null - private var storageManager: StorageManager? = null + @Inject lateinit var documentTreePathFactory: DocumentTreePathFactory @Inject lateinit var musicSettings: MusicSettings override fun onCreateBinding(inflater: LayoutInflater) = @@ -70,10 +68,6 @@ class MusicDirsDialog : } override fun onBindingCreated(binding: DialogMusicDirsBinding, savedInstanceState: Bundle?) { - val context = requireContext() - val storageManager = - context.getSystemServiceCompat(StorageManager::class).also { storageManager = it } - openDocumentTreeLauncher = registerForActivityResult( ActivityResultContracts.OpenDocumentTree(), ::addDocumentTreeUriToDirs) @@ -107,9 +101,8 @@ class MusicDirsDialog : if (pendingDirs != null) { dirs = MusicDirectories( - pendingDirs.mapNotNull { - Directory.fromDocumentTreeUri(storageManager, it) - }, + pendingDirs.mapNotNull( + documentTreePathFactory::deserializeDocumentTreePath), savedInstanceState.getBoolean(KEY_PENDING_MODE)) } } @@ -133,18 +126,18 @@ class MusicDirsDialog : override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putStringArrayList( - KEY_PENDING_DIRS, ArrayList(dirAdapter.dirs.map { it.toString() })) + KEY_PENDING_DIRS, + ArrayList(dirAdapter.dirs.map(documentTreePathFactory::serializeDocumentTreePath))) outState.putBoolean(KEY_PENDING_MODE, isUiModeInclude(requireBinding())) } override fun onDestroyBinding(binding: DialogMusicDirsBinding) { super.onDestroyBinding(binding) - storageManager = null openDocumentTreeLauncher = null binding.dirsRecycler.adapter = null } - override fun onRemoveDirectory(dir: Directory) { + override fun onRemoveDirectory(dir: Path) { dirAdapter.remove(dir) requireBinding().dirsEmpty.isVisible = dirAdapter.dirs.isEmpty() } @@ -162,15 +155,7 @@ class MusicDirsDialog : return } - // Convert the document tree URI into it's relative path form, which can then be - // parsed into a Directory instance. - val docUri = - DocumentsContract.buildDocumentUriUsingTree( - uri, DocumentsContract.getTreeDocumentId(uri)) - val treeUri = DocumentsContract.getTreeDocumentId(docUri) - val dir = - Directory.fromDocumentTreeUri( - requireNotNull(storageManager) { "StorageManager was not available" }, treeUri) + val dir = documentTreePathFactory.unpackDocumentTreeUri(uri) if (dir != null) { dirAdapter.add(dir) diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt index 93a777a6a..494141fb7 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/fs/Fs.kt @@ -24,119 +24,174 @@ 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 +import org.oxycblt.auxio.util.logD /** - * A full absolute path to a file. Only intended for display purposes. For accessing files, URIs are - * preferred in all cases due to scoped storage limitations. + * An abstraction of an android file system path, including the volume and relative path. * - * @param name The name of the file. - * @param parent The parent [Directory] of the file. - * @author Alexander Capehart (OxygenCobalt) + * @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 name: String, val parent: Directory) +data class Path( + val volume: Volume, + val components: Components, +) { + /** The name of the file/directory. */ + val name: String? + get() = components.name -/** - * A volume-aware relative path to a directory. - * - * @param volume The [StorageVolume] that the [Directory] is contained in. - * @param relativePath The relative path from within the [StorageVolume] to the [Directory]. - * @author Alexander Capehart (OxygenCobalt) - */ -class Directory private constructor(val volume: StorageVolume, val relativePath: String) { - /** - * Resolve the [Directory] instance into a human-readable path name. - * - * @param context [Context] required to obtain volume descriptions. - * @return A human-readable path. - * @see StorageVolume.getDescription - */ - fun resolveName(context: Context) = - context.getString(R.string.fmt_path, volume.getDescriptionCompat(context), relativePath) + /** The parent directory of the path, or itself if it's the root path. */ + val directory: Path + get() = Path(volume, components.parent()) + + override fun toString() = "Path(storageVolume=$volume, components=$components)" /** - * Converts this [Directory] instance into an opaque document tree path. This is a huge - * violation of the document tree URI contract, but it's also the only one can sensibly work - * with these uris in the UI, and it doesn't exactly matter since we never write or read to - * directory. + * 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" * - * @return A URI [String] abiding by the document tree specification, or null if the [Directory] - * is not valid. + * @param fileName The name of the file to append to the path. + * @return The new [Path] instance. */ - fun toDocumentTreeUri() = - // Document tree URIs consist of a prefixed volume name followed by a relative path. - if (volume.isInternalCompat) { - // The primary storage has a volume prefix of "primary", regardless - // of if it's internal or not. - "$DOCUMENT_URI_PRIMARY_NAME:$relativePath" - } else { - // Removable storage has a volume prefix of it's UUID. - volume.uuidCompat?.let { uuid -> "$uuid:$relativePath" } - } + fun file(fileName: String) = Path(volume, components.child(fileName)) - override fun hashCode(): Int { - var result = volume.hashCode() - result = 31 * result + relativePath.hashCode() - return result - } + /** + * 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" +} - override fun equals(other: Any?) = - other is Directory && other.volume == volume && other.relativePath == relativePath +sealed interface Volume { + /** The name of the volume as it appears in MediaStore. */ + val mediaStoreName: String? - companion object { - /** The name given to the internal volume when in a document tree URI. */ - private const val DOCUMENT_URI_PRIMARY_NAME = "primary" + /** + * The components of the path to the volume, relative from the system root. Should not be used + * except for compatibility purposes. + */ + val components: Components? - /** - * Create a new directory instance from the given components. - * - * @param volume The [StorageVolume] that the [Directory] is contained in. - * @param relativePath The relative path from within the [StorageVolume] to the [Directory]. - * Will be stripped of any trailing separators for a consistent internal representation. - * @return A new [Directory] created from the components. - */ - fun from(volume: StorageVolume, relativePath: String) = - Directory( - volume, relativePath.removePrefix(File.separator).removeSuffix(File.separator)) + /** Resolves the name of the volume in a human-readable format. */ + fun resolveName(context: Context): String - /** - * Create a new directory from a document tree URI. This is a huge violation of the document - * tree URI contract, but it's also the only one can sensibly work with these uris in the - * UI, and it doesn't exactly matter since we never write or read directory. - * - * @param storageManager [StorageManager] in order to obtain the [StorageVolume] specified - * in the given URI. - * @param uri The URI string to parse into a [Directory]. - * @return A new [Directory] parsed from the URI, or null if the URI is not valid. - */ - fun fromDocumentTreeUri(storageManager: StorageManager, uri: String): Directory? { - // Document tree URIs consist of a prefixed volume name followed by a relative path, - // delimited with a colon. - val split = uri.split(File.pathSeparator, limit = 2) - val volume = - when (split[0]) { - // The primary storage has a volume prefix of "primary", regardless - // of if it's internal or not. - DOCUMENT_URI_PRIMARY_NAME -> storageManager.primaryStorageVolumeCompat - // Removable storage has a volume prefix of it's UUID, try to find it - // within StorageManager's volume list. - else -> storageManager.storageVolumesCompat.find { it.uuidCompat == split[0] } - } - val relativePath = split.getOrNull(1) - return from(volume ?: return null, relativePath ?: return null) - } + /** 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? } } /** - * Represents the configuration for specific directories to filter to/from when loading music. + * The components of a path. This allows the path to be manipulated without having tp handle + * separator parsing. * - * @param dirs A list of [Directory] instances. How these are interpreted depends on [shouldInclude] - * @param shouldInclude True if the library should only load from the [Directory] instances, false - * if the library should not load from the [Directory] instances. - * @author Alexander Capehart (OxygenCobalt) + * @param components The components of the path. */ -data class MusicDirectories(val dirs: List, val shouldInclude: Boolean) +@JvmInline +value class Components private constructor(val components: List) { + /** The name of the file/directory. */ + val name: String? + get() = components.lastOrNull() + + override fun toString() = components.joinToString(File.separator) + + /** + * 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()).also { logD(it.components) } + } else { + this + } + + companion object { + /** + * Parses a path string into a [Components] instance by the system path separator. + * + * @param path The path string to parse. + * @return The [Components] instance. + */ + fun parse(path: String) = + Components(path.trimSlashes().split(File.separatorChar).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 class InternalVolumeImpl(val storageVolume: StorageVolume) : Volume.Internal { + override val mediaStoreName + get() = storageVolume.mediaStoreVolumeNameCompat + + override val components + get() = storageVolume.directoryCompat?.let(Components::parse) + + override fun resolveName(context: Context) = storageVolume.getDescriptionCompat(context) + } + + private 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::parse) + + override fun resolveName(context: Context) = storageVolume.getDescriptionCompat(context) + } +} /** * A mime type of a file. Only intended for display. diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/FsModule.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/FsModule.kt index 828a468da..74703e8f0 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/FsModule.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/fs/FsModule.kt @@ -19,16 +19,22 @@ package org.oxycblt.auxio.music.fs import android.content.Context +import android.os.storage.StorageManager import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import org.oxycblt.auxio.util.getSystemServiceCompat @Module @InstallIn(SingletonComponent::class) class FsModule { @Provides - fun mediaStoreExtractor(@ApplicationContext context: Context) = - MediaStoreExtractor.from(context) + fun volumeManager(@ApplicationContext context: Context): VolumeManager = + VolumeManagerImpl(context.getSystemServiceCompat(StorageManager::class)) + + @Provides + fun mediaStoreExtractor(@ApplicationContext context: Context, volumeManager: VolumeManager) = + MediaStoreExtractor.from(context, volumeManager) } diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt index b25a360a7..1c0b58a7c 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt @@ -21,7 +21,6 @@ package org.oxycblt.auxio.music.fs import android.content.Context import android.database.Cursor import android.os.Build -import android.os.storage.StorageManager import android.provider.MediaStore import androidx.annotation.RequiresApi import androidx.core.database.getIntOrNull @@ -31,10 +30,10 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.yield import org.oxycblt.auxio.music.cache.Cache import org.oxycblt.auxio.music.device.RawSong +import org.oxycblt.auxio.music.dirs.MusicDirectories import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.metadata.parseId3v2PositionField import org.oxycblt.auxio.music.metadata.transformPositionField -import org.oxycblt.auxio.util.getSystemServiceCompat import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.sendWithTimeout @@ -93,13 +92,16 @@ interface MediaStoreExtractor { * Create a framework-backed instance. * * @param context [Context] required. + * @param volumeManager [VolumeManager] required. * @return A new [MediaStoreExtractor] that will work best on the device's API level. */ - fun from(context: Context): MediaStoreExtractor = + fun from(context: Context, volumeManager: VolumeManager): MediaStoreExtractor = when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> Api30MediaStoreExtractor(context) - Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> Api29MediaStoreExtractor(context) - else -> Api21MediaStoreExtractor(context) + Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> + Api30MediaStoreExtractor(context, volumeManager) + Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> + Api29MediaStoreExtractor(context, volumeManager) + else -> Api21MediaStoreExtractor(context, volumeManager) } } } @@ -249,15 +251,15 @@ private abstract class BaseMediaStoreExtractor(protected val context: Context) : protected abstract val dirSelectorTemplate: String /** - * Add a [Directory] to the given list of projection selector arguments. + * Add a [SystemPath] to the given list of projection selector arguments. * - * @param dir The [Directory] to add. + * @param path The [SystemPath] to add. * @param args The destination list to append selector arguments to that are analogous to the - * given [Directory]. - * @return true if the [Directory] was added, false otherwise. + * given [SystemPath]. + * @return true if the [SystemPath] was added, false otherwise. * @see dirSelectorTemplate */ - protected abstract fun addDirToSelector(dir: Directory, args: MutableList): Boolean + protected abstract fun addDirToSelector(path: Path, args: MutableList): Boolean protected abstract fun wrapQuery( cursor: Cursor, @@ -362,7 +364,8 @@ private abstract class BaseMediaStoreExtractor(protected val context: Context) : // Note: The separation between version-specific backends may not be the cleanest. To preserve // speed, we only want to add redundancy on known issues, not with possible issues. -private class Api21MediaStoreExtractor(context: Context) : BaseMediaStoreExtractor(context) { +private class Api21MediaStoreExtractor(context: Context, private val volumeManager: VolumeManager) : + BaseMediaStoreExtractor(context) { override val projection: Array get() = super.projection + @@ -378,28 +381,27 @@ private class Api21MediaStoreExtractor(context: Context) : BaseMediaStoreExtract override val dirSelectorTemplate: String get() = "${MediaStore.Audio.Media.DATA} LIKE ?" - override fun addDirToSelector(dir: Directory, args: MutableList): Boolean { + override fun addDirToSelector(path: Path, args: MutableList): Boolean { // "%" signifies to accept any DATA value that begins with the Directory's path, // thus recursively filtering all files in the directory. - args.add("${dir.volume.directoryCompat ?: return false}/${dir.relativePath}%") + args.add("${path.volume.components ?: return false}${path.components}%") return true } override fun wrapQuery( cursor: Cursor, genreNamesMap: Map, - ): MediaStoreExtractor.Query = - Query(cursor, genreNamesMap, context.getSystemServiceCompat(StorageManager::class)) + ): MediaStoreExtractor.Query = Query(cursor, genreNamesMap, volumeManager) private class Query( cursor: Cursor, genreNamesMap: Map, - storageManager: StorageManager + volumeManager: VolumeManager ) : BaseMediaStoreExtractor.Query(cursor, genreNamesMap) { // Set up cursor indices for later use. private val trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK) private val dataIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATA) - private val volumes = storageManager.storageVolumesCompat + private val volumes = volumeManager.getVolumes() override fun populateFileInfo(rawSong: RawSong) { super.populateFileInfo(rawSong) @@ -417,10 +419,10 @@ private class Api21MediaStoreExtractor(context: Context) : BaseMediaStoreExtract // the Directory we will use. val rawPath = data.substringBeforeLast(File.separatorChar) for (volume in volumes) { - val volumePath = volume.directoryCompat ?: continue + val volumePath = (volume.components ?: continue).toString() val strippedPath = rawPath.removePrefix(volumePath) if (strippedPath != rawPath) { - rawSong.directory = Directory.from(volume, strippedPath) + rawSong.directory = Path(volume, Components.parse(strippedPath)) break } } @@ -466,26 +468,26 @@ private abstract class BaseApi29MediaStoreExtractor(context: Context) : "(${MediaStore.Audio.AudioColumns.VOLUME_NAME} LIKE ? " + "AND ${MediaStore.Audio.AudioColumns.RELATIVE_PATH} LIKE ?)" - override fun addDirToSelector(dir: Directory, args: MutableList): Boolean { + override fun addDirToSelector(path: Path, args: MutableList): Boolean { // MediaStore uses a different naming scheme for it's volume column convert this // directory's volume to it. - args.add(dir.volume.mediaStoreVolumeNameCompat ?: return false) + args.add(path.volume.mediaStoreName ?: return false) // "%" signifies to accept any DATA value that begins with the Directory's path, // thus recursively filtering all files in the directory. - args.add("${dir.relativePath}%") + args.add("${path.components}%") return true } abstract class Query( cursor: Cursor, genreNamesMap: Map, - storageManager: StorageManager + private val volumeManager: VolumeManager ) : BaseMediaStoreExtractor.Query(cursor, genreNamesMap) { private val volumeIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.VOLUME_NAME) private val relativePathIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.RELATIVE_PATH) - private val volumes = storageManager.storageVolumesCompat + private val volumes = volumeManager.getVolumes() final override fun populateFileInfo(rawSong: RawSong) { super.populateFileInfo(rawSong) @@ -493,9 +495,9 @@ private abstract class BaseApi29MediaStoreExtractor(context: Context) : // This is combined with the plain relative path column to create the directory. val volumeName = cursor.getString(volumeIndex) val relativePath = cursor.getString(relativePathIndex) - val volume = volumes.find { it.mediaStoreVolumeNameCompat == volumeName } + val volume = volumes.find { it.mediaStoreName == volumeName } if (volume != null) { - rawSong.directory = Directory.from(volume, relativePath) + rawSong.directory = Path(volume, Components.parse(relativePath)) } } } @@ -509,7 +511,8 @@ private abstract class BaseApi29MediaStoreExtractor(context: Context) : * @author Alexander Capehart (OxygenCobalt) */ @RequiresApi(Build.VERSION_CODES.Q) -private class Api29MediaStoreExtractor(context: Context) : BaseApi29MediaStoreExtractor(context) { +private class Api29MediaStoreExtractor(context: Context, private val volumeManager: VolumeManager) : + BaseApi29MediaStoreExtractor(context) { override val projection: Array get() = super.projection + arrayOf(MediaStore.Audio.AudioColumns.TRACK) @@ -517,14 +520,13 @@ private class Api29MediaStoreExtractor(context: Context) : BaseApi29MediaStoreEx override fun wrapQuery( cursor: Cursor, genreNamesMap: Map - ): MediaStoreExtractor.Query = - Query(cursor, genreNamesMap, context.getSystemServiceCompat(StorageManager::class)) + ): MediaStoreExtractor.Query = Query(cursor, genreNamesMap, volumeManager) private class Query( cursor: Cursor, genreNamesMap: Map, - storageManager: StorageManager - ) : BaseApi29MediaStoreExtractor.Query(cursor, genreNamesMap, storageManager) { + volumeManager: VolumeManager + ) : BaseApi29MediaStoreExtractor.Query(cursor, genreNamesMap, volumeManager) { private val trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK) override fun populateTags(rawSong: RawSong) { @@ -549,7 +551,8 @@ private class Api29MediaStoreExtractor(context: Context) : BaseApi29MediaStoreEx * @author Alexander Capehart (OxygenCobalt) */ @RequiresApi(Build.VERSION_CODES.R) -private class Api30MediaStoreExtractor(context: Context) : BaseApi29MediaStoreExtractor(context) { +private class Api30MediaStoreExtractor(context: Context, private val volumeManager: VolumeManager) : + BaseApi29MediaStoreExtractor(context) { override val projection: Array get() = super.projection + @@ -562,14 +565,13 @@ private class Api30MediaStoreExtractor(context: Context) : BaseApi29MediaStoreEx override fun wrapQuery( cursor: Cursor, genreNamesMap: Map - ): MediaStoreExtractor.Query = - Query(cursor, genreNamesMap, context.getSystemServiceCompat(StorageManager::class)) + ): MediaStoreExtractor.Query = Query(cursor, genreNamesMap, volumeManager) private class Query( cursor: Cursor, genreNamesMap: Map, - storageManager: StorageManager - ) : BaseApi29MediaStoreExtractor.Query(cursor, genreNamesMap, storageManager) { + volumeManager: VolumeManager + ) : BaseApi29MediaStoreExtractor.Query(cursor, genreNamesMap, volumeManager) { private val trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER) private val discIndex = diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt index 2c8d9158f..0e0944961 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchEngine.kt @@ -71,7 +71,7 @@ class SearchEngineImpl @Inject constructor(@ApplicationContext private val conte return SearchEngine.Items( songs = items.songs?.searchListImpl(query) { q, song -> - song.path.name.contains(q, ignoreCase = true) + song.path.name?.contains(q, ignoreCase = true) == true }, albums = items.albums?.searchListImpl(query), artists = items.artists?.searchListImpl(query), diff --git a/app/src/main/res/layout/item_song_property.xml b/app/src/main/res/layout/item_song_property.xml index 5334756bd..bc24a3a3e 100644 --- a/app/src/main/res/layout/item_song_property.xml +++ b/app/src/main/res/layout/item_song_property.xml @@ -9,7 +9,7 @@ android:paddingEnd="@dimen/spacing_medium" android:paddingTop="@dimen/spacing_small" android:paddingBottom="@dimen/spacing_small" - tools:hint="@string/lbl_file_name" + tools:hint="@string/lbl_path" app:expandedHintEnabled="false"> الغاء التنسيق الحجم - المسار إحصائيات المكتبة معدل البت - اسم الملف تجميع مباشر تجميعات خصائص الاغنية diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index ca6f6fafd..51bb12461 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -32,7 +32,6 @@ اذهب للفنان عرض والتحكم في تشغيل الموسيقى خلط - اسم الملف خلط الكل إلغاء حفظ diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index a17118fb0..97414d1ba 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -83,10 +83,8 @@ Чарга Перайсці да альбома Перайсці да выканаўцы - Імя файла Праглядзіце ўласцівасці Уласцівасці песні - Бацькоўскі шлях Фармат Перамяшаць усё Бітрэйт diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index e39a84ca3..dd85dcc80 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -185,8 +185,6 @@ Přehrát ze zobrazené položky Zobrazit vlastnosti Vlastnosti skladby - Název souboru - Nadřazená cesta Formát Velikost Přenosová rychlost diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 4c0fa5b55..02da0dab5 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -179,8 +179,6 @@ Abtastrate Eigenschaften ansehen Lied-Eigenschaften - Dateiname - Elternpfad Format Größe Bitrate diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index b5730db8e..039c864ec 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -80,7 +80,6 @@ %d kbps Πρόσθεση Ιδιότητες τραγουδιού - Όνομα αρχείου Προβολή Ιδιοτήτων Στατιστικά συλλογής Ζωντανό άλμπουμ diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 20af977ec..747a2b559 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -194,7 +194,6 @@ Mixtapes (recopilación de canciones) Mixtape (recopilación de canciones) Remezclas - Nombre de archivo Siempre empezar la reproducción cuando se conecten auriculares (puede no funcionar en todos los dispositivos) Pre-amp ReplayGain El pre-amp se aplica al ajuste existente durante la reproducción @@ -215,7 +214,6 @@ Single remix Compilaciones EP de remixes - Directorio superior Eliminar el estado de reproducción guardado previamente (si existe) Abrir la cola Género diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 262cfde7b..0bbdde9ec 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -43,8 +43,6 @@ Siirry albumiin Näytä ominaisuudet Kappaleen ominaisuudet - Tiedostonimi - Ylätason polku Muoto Koko Bittitaajuus diff --git a/app/src/main/res/values-fil/strings.xml b/app/src/main/res/values-fil/strings.xml index d7e2bdc59..bc3ad9dc4 100644 --- a/app/src/main/res/values-fil/strings.xml +++ b/app/src/main/res/values-fil/strings.xml @@ -46,7 +46,6 @@ Puntahan ang album Tignan ang katangian Katangian ng kanta - Pangalan ng file Pormat Laki Tulin ng mga bit diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 742007eb0..11cf24113 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -82,8 +82,6 @@ Débit binaire Utiliser une autre action de notification Taux d\'échantillonnage - Chemin parent - Nom du fichier Tout mélanger Annuler Enregistrer diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index faf684102..d1a992526 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -53,11 +53,9 @@ Engadir á cola Excluir o que non é música Ir ao artista - Nome do arquivo Mesturar Mesturar todo Restablecer - Directorio superior Formato Tamaño Tasa de bits diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 84be232d8..265b733e5 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -61,14 +61,12 @@ एंड्रॉयड के लिए एक सीधा साधा, विवेकशील गाने बजाने वाला ऐप। नई प्लेलिस्ट अगला चलाएं - फ़ाइल का नाम लायब्रेरी टैब्स एल्बम से चलाएं सामग्री %d चयनित प्रारूप प्लेलिस्ट में जोड़ें - मुख्य पथ बिट-रेट रद्द करें सहेजें diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 22bab4d29..737a95181 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -40,8 +40,6 @@ Popis pjesama Reproduciraj sljedeću Svojstva pjesme - Naziv datoteke - Glavni direktorij Format Veličina Brzina prijenosa diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index c92aa6abf..ad272228a 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -83,7 +83,6 @@ Keverés be/ki kapcsolása %s album borítója Visszajátszás - Szülő útvonal Mappa eltávolítása Lejátszólistához ad Formátum @@ -160,7 +159,6 @@ Inkább album, ha egyet játszik Zene frissítése Mégse - Fájl név Egyéni lejátszási sáv művelet A lejátszás mindig akkor indul el, ha a fejhallgató csatlakoztatva van (nem minden eszközön működik) Automatikus újratöltés diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 3b2486eb4..05c65a556 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -52,7 +52,6 @@ %d Album Properti lagu - Nama berkas Laju bit OK Batal @@ -65,7 +64,6 @@ Putar otomatis headset Selalu mulai memutar ketika headset tersambung (mungkin tidak berfungsi pada semua perangkat) Strategi ReplayGain - Path induk Ukuran Tingkat sampel Tab Pustaka diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 1a4bf8938..68d67a9de 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -181,8 +181,6 @@ Frequenza di campionamento Vedi proprietà Proprietà brano - Nome file - Directory superiore Formato Dimensione Bitrate diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index d74cc9319..ecdb73c14 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -131,7 +131,6 @@ רצועה תור מעבר לאומן - שם קובץ ערבוב המצב שוחזר אודות @@ -242,7 +241,6 @@ תמונת רשימת השמעה עבור %s אדום ירוק - נתיב ראשי לא ניתן לשחזר את המצב רצועה %d יצירת רשימת השמעה חדשה diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 7c011f04e..e3b8dc43c 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -57,7 +57,6 @@ 表示されたアイテムから再生 再生停止 - ファイル名 追加した日付け サンプルレート 降順 @@ -245,7 +244,6 @@ 初期 (高速読み込み) 再生中の場合はアルバムを優先 複数値セパレータ - 親パス 変更されるたびに音楽ライブラリをリロードします (永続的な通知が必要です) サウンドと再生の動作を構成する 戻る前に巻き戻す diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 0d9822a7f..97463dcae 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -163,7 +163,6 @@ 추가한 폴더에서만 음악을 불러옵니다. 곡 속성 속성 보기 - 파일 이름 샘플 속도 전송 속도 크기 @@ -195,7 +194,6 @@ 앰퍼샌드 (&) MPEG-1 오디오 추가한 날짜 - 상위 경로 맞춤형 재생 동작 버튼 반복 방식 대기열 열기 diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 94c41de55..da150c3ea 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -36,7 +36,6 @@ Maišyti Pridėtas į eilę Dainų ypatybės - Failo pavadinimas Išsaugoti Apie Pridėti @@ -214,7 +213,6 @@ DJ miksas Gyvai kompiliacija Remikso kompiliacija - Pirminis kelias Išvalyti anksčiau išsaugotą grojimo būseną (jei yra) Daugiareikšmiai separatoriai Pasvirasis brūkšnys (/) diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index f1ae5be53..b93ec52f9 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -25,7 +25,6 @@ വലിപ്പം ചേർക്കുക ശരി - ഉത്ഭവ പാത റദ്ദാക്കുക വെളിച്ചം കുറിച്ച് diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index c276bd0bd..230f1fe37 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -192,8 +192,6 @@ Format Vis egenskaper Spor-egenskaper - Filnavn - Overnevnt sti Pause ved gjentagelse Rød diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index c0ecc5905..8c760d532 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -121,9 +121,7 @@ Annuleren Bibliotheek tabbladen Jaar - Ouderpad Lied eigenschappen - Bestandsnaam Voorkeur album als er een speelt Voorkeur titel Voorkeur album diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index c33f1e852..2f029b2ab 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -55,7 +55,6 @@ ਐਲਬਮ \'ਤੇ ਜਾਓ ਵਿਸ਼ੇਸ਼ਤਾਵਾਂ ਵੇਖੋ ਗੀਤ ਦੀਆਂ ਵਿਸ਼ੇਸ਼ਤਾਵਾਂ - ਪੇਰੈਂਟ ਮਾਰਗ ਫਾਰਮੈਟ ਆਕਾਰ ਸ਼ਫਲ @@ -76,7 +75,6 @@ ਗੀਤ ਦੀ ਗਿਣਤੀ ਘਟਦੇ ਹੋਏ ਕਲਾਕਾਰ \'ਤੇ ਜਾਓ - ਫਾਈਲ ਦਾ ਨਾਮ ਬਿੱਟ ਰੇਟ ਸੈਂਪਲ ਰੇਟ ਸ਼ਾਮਿਲ ਕਰੋ diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 737838fe6..fa4993f8d 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -90,8 +90,6 @@ Autoodtwarzanie w słuchawkach Morski +%.1f dB - Nazwa pliku - Ścieżka katalogu Format Niebieskozielony Płyta %d diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 6716a415a..366aa1e2e 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -106,7 +106,6 @@ Artista Álbum Propriedades da música - Nome do arquivo Formato Tamanho Taxa de bits @@ -158,7 +157,6 @@ Ajuste em faixas sem metadados Reprodução automática em fones de ouvido Pré-amplificação da normalização de volume - Caminho principal OK Exibição Ativa cantos arredondados em elementos adicionais da interface do usuário diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index e02dbbe6c..2fa1582f3 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -115,7 +115,6 @@ Taxa de amostragem Salvar Separadores multi-valor - Nome do ficheiro Tamanho Propriedades Propriedades da música @@ -217,7 +216,6 @@ A carregar a sua biblioteca de músicas… (%1$d/%2$d) Retroceder antes de voltar Parar reprodução - Caminho principal Ativar cantos arredondados em elementos adicionais da interface do utilizador (requer que as capas dos álbuns sejam arredondadas) %d Selecionadas Misturas DJ diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index e892e2739..3ad8ebd47 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -103,10 +103,8 @@ Egalizator Bit rate Data adăugării - Calea principală Format Proprietățile cântecului - Numele fișierului Amestecare Adaugă Frecvența de eșantionare diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 4859680ad..268cd49b1 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -157,7 +157,6 @@ Внимание: Изменение предусиления на большое положительное значение может привести к появлению искажений на некоторых звуковых дорожках. Сведения Свойства трека - Путь Формат Размер Частота дискретизации @@ -165,7 +164,6 @@ Статистика библиотеки Восстановить состояние воспроизведения Продолжительность - Имя файла Мини-альбом Мини-альбомы Сингл diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 5d8da013d..1cbd74549 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -59,7 +59,6 @@ Shrani trenutno stanje predvajanja zdaj Preskoči na zadnjo pesem Ponovno naloži glasbeno knjižnico vsakič, ko se zazna sprememba (zahteva vztrajno obvestilo) - Pot do datoteke %d pesem %d pesmi @@ -79,7 +78,6 @@ Mešanice Izvajalec Pravilno razvrsti imena, ki se začnejo z številkami ali besedami, kot so \'the\' (najbolje deluje z angleško glasbo) - Ime datoteke Zelenkasto modra Vztrajnost Premešaj vse pesmi diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 16af4e8f1..2c9a072cb 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -52,7 +52,6 @@ Visa egenskaper Dela Egenskaper för låt - Överordnad mapp Format Storlek Samplingsfrekvens @@ -99,7 +98,6 @@ Disk Sortera Lägg till kö - Filnamn Lägg till Tillstånd tog bort Bithastighet diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 5e77b99ab..68997f497 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -52,10 +52,8 @@ %d albüm %d albümler - Dosya adı Özellikleri görüntüle Şarkı özellikleri - Ana yol Biçim Karıştır Hepsini karıştır diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 2164136ed..e2b1c3891 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -54,7 +54,6 @@ %d альбомів %d альбомів - Ім\'я файлу Формат Добре Скасувати @@ -79,7 +78,6 @@ Збірки Збірка Концертний альбом - Шлях до каталогу Екран Рік Обкладинки альбомів diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index f3742f1d0..e7072c57d 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -176,8 +176,6 @@ 音轨 查看属性 曲目属性 - 文件名 - 上级目录 格式 大小 比特率 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f00389de8..61ed0e770 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -135,8 +135,7 @@ Share Song properties - File name - Parent path + Path Format