From 9b13b4c94eef7ca48831c908eb048758d6fc1b02 Mon Sep 17 00:00:00 2001 From: OxygenCobalt Date: Sun, 12 Jun 2022 15:51:05 -0600 Subject: [PATCH] music: revamp excluded into music dirs Completely rework the excluded directory system into a new "Music Folders" system. This is implemented alongside a new "Include" mode. This mode allows the user to restrict music indexing to a parsicular folder. I've been reluctant to add this feature, as having two separate options seemed bad. This resolves it by effectively packing whether to include/exclude directories into a single option. Resolves #154. --- CHANGELOG.md | 10 +- ...athFramework.kt => FileSystemFramework.kt} | 21 +++- .../auxio/music/backend/ExoPlayerBackend.kt | 2 + .../auxio/music/backend/MediaStoreBackend.kt | 36 ++++-- .../MusicDirAdapter.kt} | 28 ++--- .../org/oxycblt/auxio/music/dirs/MusicDirs.kt | 65 +++++++++++ .../MusicDirsDialog.kt} | 104 ++++++++++++------ .../music/excluded/ExcludedDirectories.kt | 64 ----------- .../replaygain/ReplayGainAudioProcessor.kt | 6 +- .../playback/system/MediaSessionComponent.kt | 6 + .../auxio/settings/SettingsListFragment.kt | 6 +- .../oxycblt/auxio/settings/SettingsManager.kt | 31 ++++-- app/src/main/res/layout/dialog_excluded.xml | 34 ------ app/src/main/res/layout/dialog_music_dirs.xml | 93 ++++++++++++++++ ...em_excluded_dir.xml => item_music_dir.xml} | 8 +- app/src/main/res/values-ar-rIQ/strings.xml | 4 +- app/src/main/res/values-cs/strings.xml | 4 +- app/src/main/res/values-de/strings.xml | 4 +- app/src/main/res/values-es/strings.xml | 4 +- app/src/main/res/values-it/strings.xml | 4 +- app/src/main/res/values-ko/strings.xml | 4 +- app/src/main/res/values-nl/strings.xml | 4 +- app/src/main/res/values-ru/strings.xml | 4 +- app/src/main/res/values-zh-rCN/strings.xml | 4 +- app/src/main/res/values/strings.xml | 22 ++-- app/src/main/res/values/styles_ui.xml | 1 + app/src/main/res/xml/prefs_main.xml | 6 +- build.gradle | 2 +- 28 files changed, 351 insertions(+), 230 deletions(-) rename app/src/main/java/org/oxycblt/auxio/music/{PathFramework.kt => FileSystemFramework.kt} (88%) rename app/src/main/java/org/oxycblt/auxio/music/{excluded/ExcludedAdapter.kt => dirs/MusicDirAdapter.kt} (73%) create mode 100644 app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirs.kt rename app/src/main/java/org/oxycblt/auxio/music/{excluded/ExcludedDialog.kt => dirs/MusicDirsDialog.kt} (59%) delete mode 100644 app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDirectories.kt delete mode 100644 app/src/main/res/layout/dialog_excluded.xml create mode 100644 app/src/main/res/layout/dialog_music_dirs.xml rename app/src/main/res/layout/{item_excluded_dir.xml => item_music_dir.xml} (88%) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4f49155f..ffa6d36dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,12 @@ ## dev [v2.3.2, v2.4.0, or v3.0.0] #### What's New +- Excluded directories has been revampled into "Music folders" + - Folders on external drives can now be excluded on Android Q+ [#134] + - Added new "Include" option to restrict indexing to a particular folder [#154] - Added a new view for song properties (Such as Bitrate) -- Folders on external drives can now be excluded on Android Q+ [#134] -- The playback bar now has a new design, with an improved progress -indicator and a skip action +- The playback bar now has a new design, with an improved progress indicator and a + skip action - When playing, covers now shows an animated indicator #### What's Improved @@ -14,6 +16,7 @@ indicator and a skip action - The toolbar layout is now consistent with Material Design 3 - Genre parsing now handles multiple integer values and cover/remix indicators (May wipe playback state) - "Rounded album covers" option is no longer dependent on "Show album covers" option +- Added song actions to the playback panel #### What's Fixed - Playback bar now picks the larger inset in case that gesture inset is missing [#149] @@ -25,6 +28,7 @@ indicator and a skip action - Moved music loading to a foreground service - Phased out `ImageButton` for `MaterialButton` - Unified icon sizing +- Added original date support to ExoPlayer parser (Not exposed in app) ## v2.3.1 diff --git a/app/src/main/java/org/oxycblt/auxio/music/PathFramework.kt b/app/src/main/java/org/oxycblt/auxio/music/FileSystemFramework.kt similarity index 88% rename from app/src/main/java/org/oxycblt/auxio/music/PathFramework.kt rename to app/src/main/java/org/oxycblt/auxio/music/FileSystemFramework.kt index f9aa1270c..743bdcf55 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/PathFramework.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/FileSystemFramework.kt @@ -70,6 +70,7 @@ sealed class Dir { /** * Represents a mime type as it is loaded by Auxio. [fromExtension] is based on the file extension * should always exist, while [fromFormat] is based on the file itself and may not be available. + * @author OxygenCobalt */ data class MimeType(val fromExtension: String, val fromFormat: String?) { fun resolveName(context: Context): String { @@ -86,18 +87,26 @@ data class MimeType(val fromExtension: String, val fromFormat: String?) { // We have special names for the most common formats. val readableStringRes = when (readableMime) { - // Classic formats + // MPEG formats + // While MP4 is AAC, it's considered separate given how common it is. "audio/mpeg", "audio/mp3" -> R.string.cdc_mp3 + "audio/mp4", + "audio/mp4a-latm", + "audio/mpeg4-generic" -> R.string.cdc_mp4 + + // Free formats + // Generic Ogg is included here as it's actually formatted as "Ogg", not "OGG" + "audio/ogg", + "application/ogg" -> R.string.cdc_ogg "audio/vorbis" -> R.string.cdc_ogg_vorbis "audio/opus" -> R.string.cdc_ogg_opus "audio/flac" -> R.string.cdc_flac - // MP4, 3GPP, M4A, etc. are all based on AAC - "audio/mp4", - "audio/mp4a-latm", - "audio/mpeg4-generic", + // The other AAC containers have a generic name "audio/aac", + "audio/aacp", + "audio/aac-adts", "audio/3gpp", "audio/3gpp2", -> R.string.cdc_aac @@ -107,6 +116,8 @@ data class MimeType(val fromExtension: String, val fromFormat: String?) { "audio/wave", "audio/vnd.wave" -> R.string.cdc_wav "audio/x-ms-wma" -> R.string.cdc_wma + + // Don't know else -> -1 } diff --git a/app/src/main/java/org/oxycblt/auxio/music/backend/ExoPlayerBackend.kt b/app/src/main/java/org/oxycblt/auxio/music/backend/ExoPlayerBackend.kt index aee092549..f5ed0e372 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/backend/ExoPlayerBackend.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/backend/ExoPlayerBackend.kt @@ -203,6 +203,8 @@ class Task(context: Context, private val audio: MediaStoreBackend.Audio) { } } + // TODO: Release types + private fun populateId3v2(tags: Map) { // Title tags["TIT2"]?.let { audio.title = it } diff --git a/app/src/main/java/org/oxycblt/auxio/music/backend/MediaStoreBackend.kt b/app/src/main/java/org/oxycblt/auxio/music/backend/MediaStoreBackend.kt index cb9c58f24..73e06d63e 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/backend/MediaStoreBackend.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/backend/MediaStoreBackend.kt @@ -33,6 +33,7 @@ import org.oxycblt.auxio.music.Path import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.albumCoverUri import org.oxycblt.auxio.music.audioUri +import org.oxycblt.auxio.music.dirs.MusicDirs import org.oxycblt.auxio.music.id3GenreName import org.oxycblt.auxio.music.no import org.oxycblt.auxio.music.queryCursor @@ -122,7 +123,7 @@ abstract class MediaStoreBackend : Indexer.Backend { override fun query(context: Context): Cursor { val settingsManager = SettingsManager.getInstance() - val selector = buildExcludedSelector(settingsManager.excludedDirs) + val selector = buildMusicDirsSelector(settingsManager.musicDirs) return requireNotNull( context.contentResolverSafe.queryCursor( @@ -187,7 +188,7 @@ abstract class MediaStoreBackend : Indexer.Backend { open val projection: Array get() = BASE_PROJECTION - abstract fun buildExcludedSelector(dirs: List): Selector + abstract fun buildMusicDirsSelector(dirs: MusicDirs): Selector /** * Build an [Audio] based on the current cursor values. Each implementation should try to obtain @@ -367,19 +368,25 @@ open class Api21MediaStoreBackend : MediaStoreBackend() { super.projection + arrayOf(MediaStore.Audio.AudioColumns.TRACK, MediaStore.Audio.AudioColumns.DATA) - override fun buildExcludedSelector(dirs: List): Selector { + override fun buildMusicDirsSelector(dirs: MusicDirs): Selector { val base = Environment.getExternalStorageDirectory().absolutePath var selector = BASE_SELECTOR val args = mutableListOf() - // Apply the excluded directories by filtering out specific DATA values. - for (dir in dirs) { + // Apply directories by filtering out specific DATA values. + for (dir in dirs.dirs) { if (dir.volume is Dir.Volume.Secondary) { - logW("Cannot exclude directories on secondary drives") - continue + // Should never happen. + throw IllegalStateException() } - selector += " AND ${MediaStore.Audio.Media.DATA} NOT LIKE ?" + selector += + if (dirs.shouldInclude) { + " AND ${MediaStore.Audio.Media.DATA} LIKE ?" + } else { + " AND ${MediaStore.Audio.Media.DATA} NOT LIKE ?" + } + args += "${base}/${dir.relativePath}%" } @@ -442,17 +449,22 @@ open class Api29MediaStoreBackend : Api21MediaStoreBackend() { MediaStore.Audio.AudioColumns.VOLUME_NAME, MediaStore.Audio.AudioColumns.RELATIVE_PATH) - override fun buildExcludedSelector(dirs: List): Selector { + override fun buildMusicDirsSelector(dirs: MusicDirs): Selector { var selector = BASE_SELECTOR val args = mutableListOf() // Starting in Android Q, we finally have access to the volume name. This allows // use to properly exclude folders on secondary devices such as SD cards. - for (dir in dirs) { + for (dir in dirs.dirs) { selector += - " AND NOT (${MediaStore.Audio.AudioColumns.VOLUME_NAME} LIKE ? " + - "AND ${MediaStore.Audio.AudioColumns.RELATIVE_PATH} LIKE ?)" + if (dirs.shouldInclude) { + " AND (${MediaStore.Audio.AudioColumns.VOLUME_NAME} LIKE ? " + + "AND ${MediaStore.Audio.AudioColumns.RELATIVE_PATH} LIKE ?)" + } else { + " AND NOT (${MediaStore.Audio.AudioColumns.VOLUME_NAME} LIKE ? " + + "AND ${MediaStore.Audio.AudioColumns.RELATIVE_PATH} LIKE ?)" + } // Assume that volume names are always lowercase counterparts to the volume // name stored in-app. I have no idea how well this holds up on other devices. diff --git a/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedAdapter.kt b/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirAdapter.kt similarity index 73% rename from app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedAdapter.kt rename to app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirAdapter.kt index 4a07f485b..4a0e2f186 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirAdapter.kt @@ -15,10 +15,10 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.excluded +package org.oxycblt.auxio.music.dirs import android.content.Context -import org.oxycblt.auxio.databinding.ItemExcludedDirBinding +import org.oxycblt.auxio.databinding.ItemMusicDirBinding import org.oxycblt.auxio.music.Dir import org.oxycblt.auxio.ui.BackingData import org.oxycblt.auxio.ui.BindingViewHolder @@ -31,16 +31,16 @@ import org.oxycblt.auxio.util.textSafe * Adapter that shows the excluded directories and their "Clear" button. * @author OxygenCobalt */ -class ExcludedAdapter(listener: Listener) : - MonoAdapter(listener) { +class MusicDirAdapter(listener: Listener) : + MonoAdapter(listener) { override val data = ExcludedBackingData(this) - override val creator = ExcludedViewHolder.CREATOR + override val creator = MusicDirViewHolder.CREATOR interface Listener { fun onRemoveDirectory(dir: Dir.Relative) } - class ExcludedBackingData(private val adapter: ExcludedAdapter) : BackingData() { + class ExcludedBackingData(private val adapter: MusicDirAdapter) : BackingData() { private val _currentList = mutableListOf() val currentList: List = _currentList @@ -70,22 +70,22 @@ class ExcludedAdapter(listener: Listener) : } } -/** The viewholder for [ExcludedAdapter]. Not intended for use in other adapters. */ -class ExcludedViewHolder private constructor(private val binding: ItemExcludedDirBinding) : - BindingViewHolder(binding.root) { - override fun bind(item: Dir.Relative, listener: ExcludedAdapter.Listener) { - binding.excludedPath.textSafe = item.resolveName(binding.context) - binding.excludedClear.setOnClickListener { listener.onRemoveDirectory(item) } +/** The viewholder for [MusicDirAdapter]. Not intended for use in other adapters. */ +class MusicDirViewHolder private constructor(private val binding: ItemMusicDirBinding) : + BindingViewHolder(binding.root) { + override fun bind(item: Dir.Relative, listener: MusicDirAdapter.Listener) { + binding.dirPath.textSafe = item.resolveName(binding.context) + binding.dirDelete.setOnClickListener { listener.onRemoveDirectory(item) } } companion object { val CREATOR = - object : Creator { + object : Creator { override val viewType: Int get() = throw UnsupportedOperationException() override fun create(context: Context) = - ExcludedViewHolder(ItemExcludedDirBinding.inflate(context.inflater)) + MusicDirViewHolder(ItemMusicDirBinding.inflate(context.inflater)) } } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirs.kt b/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirs.kt new file mode 100644 index 000000000..694749148 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirs.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2022 Auxio Project + * + * 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.os.Build +import java.io.File +import org.oxycblt.auxio.music.Dir +import org.oxycblt.auxio.util.logD +import org.oxycblt.auxio.util.logW + +data class MusicDirs(val dirs: List, val shouldInclude: Boolean) { + companion object { + private const val VOLUME_PRIMARY_NAME = "primary" + + fun parseDir(dir: String): Dir.Relative? { + logD("Parse from string $dir") + + val split = dir.split(File.pathSeparator, limit = 2) + + val volume = + when (split[0]) { + VOLUME_PRIMARY_NAME -> Dir.Volume.Primary + else -> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + Dir.Volume.Secondary(split[0]) + } else { + // While Android Q provides a stable way of accessing volumes, we can't + // trust that DATA provides a stable volume scheme on older versions, so + // external volumes are not supported. + logW("Cannot use secondary volumes below Android 10") + return null + } + } + + val relativePath = split.getOrNull(1) ?: return null + + return Dir.Relative(volume, relativePath) + } + + fun toDir(dir: Dir.Relative): String { + val volume = + when (dir.volume) { + is Dir.Volume.Primary -> VOLUME_PRIMARY_NAME + is Dir.Volume.Secondary -> dir.volume.name + } + + return "${volume}:${dir.relativePath}" + } + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDialog.kt b/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirsDialog.kt similarity index 59% rename from app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDialog.kt rename to app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirsDialog.kt index 0aff6cfa9..22fc275b7 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/dirs/MusicDirsDialog.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.music.excluded +package org.oxycblt.auxio.music.dirs import android.net.Uri import android.os.Bundle @@ -25,42 +25,41 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible import androidx.fragment.app.activityViewModels -import kotlinx.coroutines.delay import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.R -import org.oxycblt.auxio.databinding.DialogExcludedBinding +import org.oxycblt.auxio.databinding.DialogMusicDirsBinding import org.oxycblt.auxio.music.Dir import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.util.hardRestart -import org.oxycblt.auxio.util.launch import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.showToast /** - * Dialog that manages the currently excluded directories. + * Dialog that manages the music dirs setting. * @author OxygenCobalt */ -class ExcludedDialog : - ViewBindingDialogFragment(), ExcludedAdapter.Listener { +class MusicDirsDialog : + ViewBindingDialogFragment(), MusicDirAdapter.Listener { private val settingsManager = SettingsManager.getInstance() private val playbackModel: PlaybackViewModel by activityViewModels() - private val excludedAdapter = ExcludedAdapter(this) + private val dirAdapter = MusicDirAdapter(this) - override fun onCreateBinding(inflater: LayoutInflater) = DialogExcludedBinding.inflate(inflater) + override fun onCreateBinding(inflater: LayoutInflater) = + DialogMusicDirsBinding.inflate(inflater) override fun onConfigDialog(builder: AlertDialog.Builder) { // Don't set the click listener here, we do some custom magic in onCreateView instead. builder - .setTitle(R.string.set_excluded) + .setTitle(R.string.set_dirs) .setNeutralButton(R.string.lbl_add, null) .setPositiveButton(R.string.lbl_save, null) .setNegativeButton(R.string.lbl_cancel, null) } - override fun onBindingCreated(binding: DialogExcludedBinding, savedInstanceState: Bundle?) { + override fun onBindingCreated(binding: DialogMusicDirsBinding, savedInstanceState: Bundle?) { val launcher = registerForActivityResult(ActivityResultContracts.OpenDocumentTree(), ::addDocTreePath) @@ -77,7 +76,10 @@ class ExcludedDialog : } dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setOnClickListener { - if (settingsManager.excludedDirs != excludedAdapter.data.currentList) { + val dirs = settingsManager.musicDirs + + if (dirs.dirs != dirAdapter.data.currentList || + dirs.shouldInclude != isInclude(requireBinding())) { logD("Committing changes") saveAndRestart() } else { @@ -87,34 +89,55 @@ class ExcludedDialog : } } - binding.excludedRecycler.apply { - adapter = excludedAdapter + binding.dirsRecycler.apply { + adapter = dirAdapter itemAnimator = null } - val dirs = - savedInstanceState - ?.getStringArrayList(KEY_PENDING_DIRS) - ?.mapNotNull(ExcludedDirectories::fromString) - ?: settingsManager.excludedDirs + var dirs = settingsManager.musicDirs - excludedAdapter.data.addAll(dirs) - requireBinding().excludedEmpty.isVisible = dirs.isEmpty() + if (savedInstanceState != null) { + val pendingDirs = savedInstanceState.getStringArrayList(KEY_PENDING_DIRS) + + if (pendingDirs != null) { + dirs = + MusicDirs( + pendingDirs.mapNotNull(MusicDirs::parseDir), + savedInstanceState.getBoolean(KEY_PENDING_MODE)) + } + } + + dirAdapter.data.addAll(dirs.dirs) + requireBinding().dirsEmpty.isVisible = dirs.dirs.isEmpty() + + binding.folderModeGroup.apply { + check( + if (dirs.shouldInclude) { + R.id.dirs_mode_include + } else { + R.id.dirs_mode_exclude + }) + + updateMode() + addOnButtonCheckedListener { _, _, _ -> updateMode() } + } } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putStringArrayList( - KEY_PENDING_DIRS, ArrayList(excludedAdapter.data.currentList.map { it.toString() })) + KEY_PENDING_DIRS, ArrayList(dirAdapter.data.currentList.map { it.toString() })) + outState.putBoolean(KEY_PENDING_MODE, isInclude(requireBinding())) } - override fun onDestroyBinding(binding: DialogExcludedBinding) { + override fun onDestroyBinding(binding: DialogMusicDirsBinding) { super.onDestroyBinding(binding) - binding.excludedRecycler.adapter = null + binding.dirsRecycler.adapter = null } override fun onRemoveDirectory(dir: Dir.Relative) { - excludedAdapter.data.remove(dir) + dirAdapter.data.remove(dir) + requireBinding().dirsEmpty.isVisible = dirAdapter.data.currentList.isEmpty() } private fun addDocTreePath(uri: Uri?) { @@ -126,8 +149,8 @@ class ExcludedDialog : val dir = parseExcludedUri(uri) if (dir != null) { - excludedAdapter.data.add(dir) - requireBinding().excludedEmpty.isVisible = false + dirAdapter.data.add(dir) + requireBinding().dirsEmpty.isVisible = false } else { requireContext().showToast(R.string.err_bad_dir) } @@ -143,22 +166,31 @@ class ExcludedDialog : val treeUri = DocumentsContract.getTreeDocumentId(docUri) // Parsing handles the rest - return ExcludedDirectories.fromString(treeUri) + return MusicDirs.parseDir(treeUri) } - private fun saveAndRestart() { - settingsManager.excludedDirs = excludedAdapter.data.currentList - - // TODO: Dumb stopgap measure until automatic rescanning, REMOVE THIS BEFORE - // MAKING ANY RELEASE!!!!!! - launch { - delay(1000) - playbackModel.savePlaybackState(requireContext()) { requireContext().hardRestart() } + private fun updateMode() { + val binding = requireBinding() + if (isInclude(binding)) { + binding.dirsModeDesc.setText(R.string.set_dirs_mode_include_desc) + } else { + binding.dirsModeDesc.setText(R.string.set_dirs_mode_exclude_desc) } } + private fun isInclude(binding: DialogMusicDirsBinding) = + binding.folderModeGroup.checkedButtonId == R.id.dirs_mode_include + + private fun saveAndRestart() { + settingsManager.musicDirs = + MusicDirs(dirAdapter.data.currentList, isInclude(requireBinding())) + + playbackModel.savePlaybackState(requireContext()) { requireContext().hardRestart() } + } + companion object { const val TAG = BuildConfig.APPLICATION_ID + ".tag.EXCLUDED" const val KEY_PENDING_DIRS = BuildConfig.APPLICATION_ID + ".key.PENDING_DIRS" + const val KEY_PENDING_MODE = BuildConfig.APPLICATION_ID + ".key.SHOULD_INCLUDE" } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDirectories.kt b/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDirectories.kt deleted file mode 100644 index 64ab4c323..000000000 --- a/app/src/main/java/org/oxycblt/auxio/music/excluded/ExcludedDirectories.kt +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (c) 2022 Auxio Project - * - * 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.excluded - -import android.os.Build -import java.io.File -import org.oxycblt.auxio.music.Dir -import org.oxycblt.auxio.util.logD -import org.oxycblt.auxio.util.logW - -object ExcludedDirectories { - private const val VOLUME_PRIMARY_NAME = "primary" - - fun fromString(dir: String): Dir.Relative? { - logD("Parse from string $dir") - - val split = dir.split(File.pathSeparator, limit = 2) - - val volume = - when (split[0]) { - VOLUME_PRIMARY_NAME -> Dir.Volume.Primary - else -> - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - Dir.Volume.Secondary(split[0]) - } else { - // While Android Q provides a stable way of accessing volumes, we can't - // trust - // that DATA provides a stable volume scheme on older versions, so external - // volumes are not supported. - logW("Cannot use secondary volumes below Android 10") - return null - } - } - - val relativePath = split.getOrNull(1) ?: return null - - return Dir.Relative(volume, relativePath) - } - - fun toString(dir: Dir.Relative): String { - val volume = - when (dir.volume) { - is Dir.Volume.Primary -> VOLUME_PRIMARY_NAME - is Dir.Volume.Secondary -> dir.volume.name - } - - return "${volume}:${dir.relativePath}" - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt index 5e48f7fb8..de76c1de6 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/replaygain/ReplayGainAudioProcessor.kt @@ -23,7 +23,6 @@ import com.google.android.exoplayer2.audio.BaseAudioProcessor import com.google.android.exoplayer2.metadata.Metadata import com.google.android.exoplayer2.metadata.id3.TextInformationFrame import com.google.android.exoplayer2.metadata.vorbis.VorbisComment -import java.lang.UnsupportedOperationException import java.nio.ByteBuffer import kotlin.math.pow import org.oxycblt.auxio.music.Album @@ -79,7 +78,7 @@ class ReplayGainAudioProcessor : BaseAudioProcessor() { // ReplayGain is configurable, so determine what to do based off of the mode. val useAlbumGain = when (settingsManager.replayGainMode) { - ReplayGainMode.OFF -> throw UnsupportedOperationException() + ReplayGainMode.OFF -> throw IllegalStateException() // User wants track gain to be preferred. Default to album gain only if // there is no track gain. @@ -226,8 +225,7 @@ class ReplayGainAudioProcessor : BaseAudioProcessor() { val buffer = replaceOutputBuffer(size) if (volume == 1f) { - // No need to apply ReplayGain, do a mem move using put instead of - // a for loop (the latter is not efficient) + // No need to apply ReplayGain. buffer.put(inputBuffer.slice()) } else { for (i in position until limit step 2) { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt index 94f5632ed..872bc0b0f 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/MediaSessionComponent.kt @@ -36,6 +36,12 @@ import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.unlikelyToBeNull /** + * The component managing the [MediaSessionCompat] instance. + * + * I really don't like how I have to do this, but until I can feasibly work with the ExoPlayer queue + * system using something like MediaSessionConnector is more or less impossible. + * + * @author OxygenCobalt */ class MediaSessionComponent(private val context: Context, private val player: Player) : Player.Listener, diff --git a/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt b/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt index 9e9bb30c9..080a7c640 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt @@ -31,7 +31,7 @@ import androidx.recyclerview.widget.RecyclerView import coil.Coil import org.oxycblt.auxio.R import org.oxycblt.auxio.home.tabs.TabCustomizeDialog -import org.oxycblt.auxio.music.excluded.ExcludedDialog +import org.oxycblt.auxio.music.dirs.MusicDirsDialog import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.replaygain.PreAmpCustomizeDialog import org.oxycblt.auxio.playback.replaygain.ReplayGainMode @@ -193,10 +193,10 @@ class SettingsListFragment : PreferenceFragmentCompat() { true } } - SettingsManager.KEY_EXCLUDED -> { + SettingsManager.KEY_MUSIC_DIRS -> { onPreferenceClickListener = Preference.OnPreferenceClickListener { - ExcludedDialog().show(childFragmentManager, ExcludedDialog.TAG) + MusicDirsDialog().show(childFragmentManager, MusicDirsDialog.TAG) true } } diff --git a/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt b/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt index ecf77469b..92f04240f 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt @@ -23,8 +23,7 @@ import androidx.appcompat.app.AppCompatDelegate import androidx.core.content.edit import androidx.preference.PreferenceManager import org.oxycblt.auxio.home.tabs.Tab -import org.oxycblt.auxio.music.Dir -import org.oxycblt.auxio.music.excluded.ExcludedDirectories +import org.oxycblt.auxio.music.dirs.MusicDirs import org.oxycblt.auxio.playback.replaygain.ReplayGainMode import org.oxycblt.auxio.playback.replaygain.ReplayGainPreAmp import org.oxycblt.auxio.playback.state.PlaybackMode @@ -138,15 +137,22 @@ class SettingsManager private constructor(context: Context) : val pauseOnRepeat: Boolean get() = inner.getBoolean(KEY_PAUSE_ON_REPEAT, false) - /** The list of directories excluded from indexing. */ - var excludedDirs: List - get() = - (inner.getStringSet(KEY_EXCLUDED, null) ?: emptySet()).mapNotNull( - ExcludedDirectories::fromString) + /** The list of directories that music should be hidden/loaded from. */ + var musicDirs: MusicDirs + get() { + val dirs = + (inner.getStringSet(KEY_MUSIC_DIRS, null) ?: emptySet()).mapNotNull( + MusicDirs::parseDir) + + return MusicDirs(dirs, inner.getBoolean(KEY_SHOULD_INCLUDE, false)) + } set(value) { inner.edit { - putStringSet(KEY_EXCLUDED, value.map(ExcludedDirectories::toString).toSet()) - apply() + putStringSet(KEY_MUSIC_DIRS, value.dirs.map(MusicDirs::toDir).toSet()) + putBoolean(KEY_SHOULD_INCLUDE, value.shouldInclude) + + // TODO: This is a stopgap measure before automatic rescanning, remove + commit() } } @@ -252,12 +258,12 @@ class SettingsManager private constructor(context: Context) : init { inner.registerOnSharedPreferenceChangeListener(this) - if (!inner.contains(KEY_EXCLUDED)) { + if (!inner.contains(KEY_MUSIC_DIRS)) { logD("Attempting to migrate excluded directories") // We need to migrate this setting now while we have a context. Note that while // this does do IO work, the old excluded directory database is so small as to make // it negligible. - excludedDirs = handleExcludedCompat(context) + musicDirs = MusicDirs(handleExcludedCompat(context), false) } } @@ -325,7 +331,8 @@ class SettingsManager private constructor(context: Context) : const val KEY_SAVE_STATE = "auxio_save_state" const val KEY_REINDEX = "auxio_reindex" - const val KEY_EXCLUDED = "auxio_excluded_dirs" + const val KEY_MUSIC_DIRS = "auxio_music_dirs" + const val KEY_SHOULD_INCLUDE = "auxio_include_dirs" const val KEY_SEARCH_FILTER_MODE = "KEY_SEARCH_FILTER" diff --git a/app/src/main/res/layout/dialog_excluded.xml b/app/src/main/res/layout/dialog_excluded.xml deleted file mode 100644 index bc03c8687..000000000 --- a/app/src/main/res/layout/dialog_excluded.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_music_dirs.xml b/app/src/main/res/layout/dialog_music_dirs.xml new file mode 100644 index 000000000..98e306bd3 --- /dev/null +++ b/app/src/main/res/layout/dialog_music_dirs.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + +