diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt index 890e48e24..fad2ab790 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt @@ -33,6 +33,7 @@ import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.ui.NavigationViewModel import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.ViewBindingFragment +import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.unlikelyToBeNull @@ -42,7 +43,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull */ abstract class DetailFragment : ViewBindingFragment(), Toolbar.OnMenuItemClickListener { - protected val detailModel: DetailViewModel by activityViewModels() + protected val detailModel: DetailViewModel by androidActivityViewModels() protected val navModel: NavigationViewModel by activityViewModels() protected val playbackModel: PlaybackViewModel by activityViewModels() 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 bb3aa8c1a..d11b50d90 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt @@ -22,17 +22,17 @@ import android.text.format.Formatter import android.view.LayoutInflater import androidx.appcompat.app.AlertDialog import androidx.core.view.isGone -import androidx.fragment.app.activityViewModels import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogSongDetailBinding import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.ui.ViewBindingDialogFragment +import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.formatDuration import org.oxycblt.auxio.util.launch class SongDetailDialog : ViewBindingDialogFragment() { - private val detailModel: DetailViewModel by activityViewModels() + private val detailModel: DetailViewModel by androidActivityViewModels() override fun onCreateBinding(inflater: LayoutInflater) = DialogSongDetailBinding.inflate(inflater) diff --git a/app/src/main/java/org/oxycblt/auxio/music/Music.kt b/app/src/main/java/org/oxycblt/auxio/music/Music.kt index 3038f2263..5017bbf78 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Music.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Music.kt @@ -60,7 +60,7 @@ sealed class MusicParent : Music() { data class Song( override val rawName: String, /** The path of this song. */ - val path: Path, + val path: NeoPath, /** The URI linking to this song's file. */ val uri: Uri, /** The mime type of this song. */ diff --git a/app/src/main/java/org/oxycblt/auxio/music/FileSystemFramework.kt b/app/src/main/java/org/oxycblt/auxio/music/StorageFramework.kt similarity index 71% rename from app/src/main/java/org/oxycblt/auxio/music/FileSystemFramework.kt rename to app/src/main/java/org/oxycblt/auxio/music/StorageFramework.kt index 3381e420f..79961732c 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/FileSystemFramework.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/StorageFramework.kt @@ -17,7 +17,13 @@ package org.oxycblt.auxio.music +import android.annotation.SuppressLint import android.content.Context +import android.os.Build +import android.os.Environment +import android.os.storage.StorageManager +import android.os.storage.StorageVolume +import android.provider.MediaStore import android.webkit.MimeTypeMap import com.google.android.exoplayer2.util.MimeTypes import org.oxycblt.auxio.R @@ -28,19 +34,18 @@ import org.oxycblt.auxio.R */ data class Path(val name: String, val parent: Dir) +data class NeoPath(val name: String, val parent: NeoDir) + +data class NeoDir(val volume: StorageVolume, val relativePath: String) { + fun resolveName(context: Context) = + context.getString(R.string.fmt_path, volume.getDescriptionCompat(context), relativePath) +} + /** * Represents a directory from the android file-system. Intentionally designed to be * version-agnostic and follow modern storage recommendations. */ sealed class Dir { - /** - * An absolute path. - * - * This is only used with [Song] instances on pre-Q android versions. This should be avoided in - * most cases for [Relative]. - */ - data class Absolute(val path: String) : Dir() - /** * A directory with a volume. * @@ -58,7 +63,6 @@ sealed class Dir { fun resolveName(context: Context) = when (this) { - is Absolute -> path is Relative -> when (volume) { is Volume.Primary -> context.getString(R.string.fmt_primary_path, relativePath) @@ -137,3 +141,52 @@ data class MimeType(val fromExtension: String, val fromFormat: String?) { } } } + +val StorageManager.storageVolumesCompat: List + get() = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + storageVolumes.toList() + } else { + @Suppress("UNCHECKED_CAST") + (StorageManager::class.java.getDeclaredMethod("getVolumeList").invoke(this) + as Array) + .toList() + } + +val StorageVolume.directoryCompat: String? + @SuppressLint("NewApi") + get() = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + directory?.absolutePath + } else { + when (stateCompat) { + Environment.MEDIA_MOUNTED, + Environment.MEDIA_MOUNTED_READ_ONLY -> + StorageVolume::class.java.getDeclaredMethod("getPath").invoke(this) as String + else -> null + } + } + +@SuppressLint("NewApi") +fun StorageVolume.getDescriptionCompat(context: Context): String = getDescription(context) + +val StorageVolume.isPrimaryCompat: Boolean + @SuppressLint("NewApi") get() = isPrimary + +val StorageVolume.uuidCompat: String? + @SuppressLint("NewApi") get() = uuid + +val StorageVolume.stateCompat: String + @SuppressLint("NewApi") get() = state + +val StorageVolume.mediaStoreVolumeNameCompat: String? + get() = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + mediaStoreVolumeName + } else { + if (isPrimaryCompat) { + MediaStore.VOLUME_EXTERNAL_PRIMARY + } else { + uuid?.lowercase() + } + } 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 a1c6a6bc8..4c01d1e1f 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 @@ -21,6 +21,8 @@ import android.content.Context import android.database.Cursor import android.os.Build import android.os.Environment +import android.os.storage.StorageManager +import android.os.storage.StorageVolume import android.provider.MediaStore import androidx.annotation.RequiresApi import androidx.core.database.getIntOrNull @@ -29,17 +31,22 @@ import java.io.File import org.oxycblt.auxio.music.Dir import org.oxycblt.auxio.music.Indexer import org.oxycblt.auxio.music.MimeType -import org.oxycblt.auxio.music.Path +import org.oxycblt.auxio.music.NeoDir +import org.oxycblt.auxio.music.NeoPath import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.albumCoverUri import org.oxycblt.auxio.music.audioUri +import org.oxycblt.auxio.music.directoryCompat import org.oxycblt.auxio.music.dirs.MusicDirs import org.oxycblt.auxio.music.id3GenreName +import org.oxycblt.auxio.music.mediaStoreVolumeNameCompat import org.oxycblt.auxio.music.no import org.oxycblt.auxio.music.queryCursor +import org.oxycblt.auxio.music.storageVolumesCompat import org.oxycblt.auxio.music.useQuery import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.util.contentResolverSafe +import org.oxycblt.auxio.util.getSystemServiceSafe /* * This file acts as the base for most the black magic required to get a remotely sensible music @@ -120,8 +127,13 @@ abstract class MediaStoreBackend : Indexer.Backend { private var albumArtistIndex = -1 private var dataIndex = -1 + private val _volumes = mutableListOf() + protected val volumes = _volumes + override fun query(context: Context): Cursor { val settingsManager = SettingsManager.getInstance() + val storageManager = context.getSystemServiceSafe(StorageManager::class) + _volumes.addAll(storageManager.storageVolumesCompat) val selector = buildMusicDirsSelector(settingsManager.musicDirs) return requireNotNull( @@ -266,7 +278,7 @@ abstract class MediaStoreBackend : Indexer.Backend { var id: Long? = null, var title: String? = null, var displayName: String? = null, - var dir: Dir? = null, + var dir: NeoDir? = null, var extensionMimeType: String? = null, var formatMimeType: String? = null, var size: Long? = null, @@ -286,7 +298,7 @@ abstract class MediaStoreBackend : Indexer.Backend { // every device provides these fields, but it seems likely that they do. rawName = requireNotNull(title) { "Malformed audio: No title" }, path = - Path( + NeoPath( name = requireNotNull(displayName) { "Malformed audio: No display name" }, parent = requireNotNull(dir) { "Malformed audio: No parent directory" }), uri = requireNotNull(id) { "Malformed audio: No id" }.audioUri, @@ -421,10 +433,16 @@ open class Api21MediaStoreBackend : MediaStoreBackend() { audio.displayName = data.substringAfterLast(File.separatorChar, "").ifEmpty { null } } - audio.dir = - data.substringBeforeLast(File.separatorChar, "").let { dir -> - if (dir.isNotEmpty()) Dir.Absolute(dir) else null + val rawPath = data.substringBeforeLast(File.separatorChar) + + for (volume in volumes) { + val volumePath = volume.directoryCompat ?: continue + val strippedPath = rawPath.removePrefix(volumePath + File.separatorChar) + if (strippedPath != rawPath) { + audio.dir = NeoDir(volume, strippedPath + File.separatorChar) + break } + } } return audio @@ -488,18 +506,15 @@ open class Api29MediaStoreBackend : Api21MediaStoreBackend() { cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.RELATIVE_PATH) } - val volume = cursor.getStringOrNull(volumeIndex) + val volumeName = cursor.getStringOrNull(volumeIndex) val relativePath = cursor.getStringOrNull(relativePathIndex) - if (volume != null && relativePath != null) { - audio.dir = - Dir.Relative( - volume = - when (volume) { - MediaStore.VOLUME_EXTERNAL_PRIMARY -> Dir.Volume.Primary - else -> Dir.Volume.Secondary(volume) - }, - relativePath = relativePath) + if (volumeName != null && relativePath != null) { + val volume = volumes.find { it.mediaStoreVolumeNameCompat == volumeName } + + if (volume != null) { + audio.dir = NeoDir(volume, relativePath) + } } return audio 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 080a7c640..ebb27193c 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/SettingsListFragment.kt @@ -35,8 +35,8 @@ 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 -import org.oxycblt.auxio.settings.pref.IntListPreference -import org.oxycblt.auxio.settings.pref.IntListPreferenceDialog +import org.oxycblt.auxio.settings.ui.IntListPreference +import org.oxycblt.auxio.settings.ui.IntListPreferenceDialog import org.oxycblt.auxio.ui.accent.AccentDialog import org.oxycblt.auxio.util.getSystemBarInsetsCompat import org.oxycblt.auxio.util.hardRestart diff --git a/app/src/main/java/org/oxycblt/auxio/settings/pref/IntListPrefDialog.kt b/app/src/main/java/org/oxycblt/auxio/settings/ui/IntListPrefDialog.kt similarity index 98% rename from app/src/main/java/org/oxycblt/auxio/settings/pref/IntListPrefDialog.kt rename to app/src/main/java/org/oxycblt/auxio/settings/ui/IntListPrefDialog.kt index 80b4cd516..0d1864680 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/pref/IntListPrefDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/ui/IntListPrefDialog.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.settings.pref +package org.oxycblt.auxio.settings.ui import android.app.Dialog import android.os.Bundle diff --git a/app/src/main/java/org/oxycblt/auxio/settings/pref/IntListPreference.kt b/app/src/main/java/org/oxycblt/auxio/settings/ui/IntListPreference.kt similarity index 98% rename from app/src/main/java/org/oxycblt/auxio/settings/pref/IntListPreference.kt rename to app/src/main/java/org/oxycblt/auxio/settings/ui/IntListPreference.kt index 778c5689a..f0a0fa23d 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/pref/IntListPreference.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/ui/IntListPreference.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.settings.pref +package org.oxycblt.auxio.settings.ui import android.content.Context import android.content.res.TypedArray diff --git a/app/src/main/java/org/oxycblt/auxio/settings/pref/M3SwitchPreference.kt b/app/src/main/java/org/oxycblt/auxio/settings/ui/M3SwitchPreference.kt similarity index 98% rename from app/src/main/java/org/oxycblt/auxio/settings/pref/M3SwitchPreference.kt rename to app/src/main/java/org/oxycblt/auxio/settings/ui/M3SwitchPreference.kt index 6ddc0d22a..a6f92214f 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/pref/M3SwitchPreference.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/ui/M3SwitchPreference.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.settings.pref +package org.oxycblt.auxio.settings.ui import android.content.Context import android.os.Build diff --git a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt index 126392280..5497d5020 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt @@ -173,13 +173,10 @@ fun Fragment.launch( viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(state, block) } } -fun Fragment.androidViewModelFactory() = - ViewModelProvider.AndroidViewModelFactory(requireContext().applicationContext as Application) - inline fun Fragment.androidViewModels() = viewModels { ViewModelProvider.AndroidViewModelFactory(requireActivity().application) } -inline fun Fragment.activityAndroidViewModels() = +inline fun Fragment.androidActivityViewModels() = activityViewModels { ViewModelProvider.AndroidViewModelFactory(requireActivity().application) } diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index e67fe1771..7056ada17 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -14,6 +14,7 @@ Microsoft WAVE - Internal/%s - SDCARD/%s + %1$s:%2$s + Internal:%s + SDCARD:%s \ No newline at end of file diff --git a/app/src/main/res/xml/prefs_main.xml b/app/src/main/res/xml/prefs_main.xml index cabaf24aa..098f373db 100644 --- a/app/src/main/res/xml/prefs_main.xml +++ b/app/src/main/res/xml/prefs_main.xml @@ -4,7 +4,7 @@ app:layout="@layout/item_header" app:title="@string/set_ui"> - - - - - - - - - - - -