music: use storage volume

Leverage StorageVolume when working with file paths.

StorageVolume is android's navive API for handling external volumes.
Ideally, we would want to replace our built-in volume class with this
new API, however doing so is somewhat complicated as some methods only
exist on newer API levels. This is only the first step until we are
able to migrate the excluded directory system to this as well.
This commit is contained in:
OxygenCobalt 2022-06-13 14:34:08 -06:00
parent 107f7bee27
commit ab1ff416e1
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
12 changed files with 119 additions and 52 deletions

View file

@ -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<FragmentDetailBinding>(), 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()

View file

@ -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<DialogSongDetailBinding>() {
private val detailModel: DetailViewModel by activityViewModels()
private val detailModel: DetailViewModel by androidActivityViewModels()
override fun onCreateBinding(inflater: LayoutInflater) =
DialogSongDetailBinding.inflate(inflater)

View file

@ -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. */

View file

@ -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<StorageVolume>
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<StorageVolume>)
.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()
}
}

View file

@ -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<StorageVolume>()
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

View file

@ -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

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.settings.pref
package org.oxycblt.auxio.settings.ui
import android.app.Dialog
import android.os.Bundle

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.settings.pref
package org.oxycblt.auxio.settings.ui
import android.content.Context
import android.content.res.TypedArray

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.settings.pref
package org.oxycblt.auxio.settings.ui
import android.content.Context
import android.os.Build

View file

@ -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 <reified T : AndroidViewModel> Fragment.androidViewModels() =
viewModels<T> { ViewModelProvider.AndroidViewModelFactory(requireActivity().application) }
inline fun <reified T : AndroidViewModel> Fragment.activityAndroidViewModels() =
inline fun <reified T : AndroidViewModel> Fragment.androidActivityViewModels() =
activityViewModels<T> {
ViewModelProvider.AndroidViewModelFactory(requireActivity().application)
}

View file

@ -14,6 +14,7 @@
<string name="cdc_wav">Microsoft WAVE</string>
<!-- Note: These are stopgap measures until we make the path code rely on components! -->
<string name="fmt_primary_path">Internal/%s</string>
<string name="fmt_secondary_path">SDCARD/%s</string>
<string name="fmt_path">%1$s:%2$s</string>
<string name="fmt_primary_path">Internal:%s</string>
<string name="fmt_secondary_path">SDCARD:%s</string>
</resources>

View file

@ -4,7 +4,7 @@
app:layout="@layout/item_header"
app:title="@string/set_ui">
<org.oxycblt.auxio.settings.pref.IntListPreference
<org.oxycblt.auxio.settings.ui.IntListPreference
app:defaultValue="@integer/theme_auto"
app:entries="@array/entries_theme"
app:entryValues="@array/values_theme"
@ -19,7 +19,7 @@
app:key="auxio_accent2"
app:title="@string/set_accent" />
<org.oxycblt.auxio.settings.pref.M3SwitchPreference
<org.oxycblt.auxio.settings.ui.M3SwitchPreference
app:allowDividerBelow="false"
app:defaultValue="false"
app:iconSpaceReserved="false"
@ -39,14 +39,14 @@
app:summary="@string/set_lib_tabs_desc"
app:title="@string/set_lib_tabs" />
<org.oxycblt.auxio.settings.pref.M3SwitchPreference
<org.oxycblt.auxio.settings.ui.M3SwitchPreference
app:defaultValue="true"
app:iconSpaceReserved="false"
app:key="KEY_SHOW_COVERS"
app:summary="@string/set_show_covers_desc"
app:title="@string/set_show_covers" />
<org.oxycblt.auxio.settings.pref.M3SwitchPreference
<org.oxycblt.auxio.settings.ui.M3SwitchPreference
app:defaultValue="false"
app:dependency="KEY_SHOW_COVERS"
app:iconSpaceReserved="false"
@ -54,14 +54,14 @@
app:summary="@string/set_quality_covers_desc"
app:title="@string/set_quality_covers" />
<org.oxycblt.auxio.settings.pref.M3SwitchPreference
<org.oxycblt.auxio.settings.ui.M3SwitchPreference
app:defaultValue="false"
app:iconSpaceReserved="false"
app:key="auxio_round_covers"
app:summary="@string/set_round_covers_desc"
app:title="@string/set_round_covers" />
<org.oxycblt.auxio.settings.pref.M3SwitchPreference
<org.oxycblt.auxio.settings.ui.M3SwitchPreference
app:allowDividerBelow="false"
app:defaultValue="false"
app:iconSpaceReserved="false"
@ -76,14 +76,14 @@
app:layout="@layout/item_header"
app:title="@string/set_audio">
<org.oxycblt.auxio.settings.pref.M3SwitchPreference
<org.oxycblt.auxio.settings.ui.M3SwitchPreference
app:defaultValue="false"
app:iconSpaceReserved="false"
app:key="auxio_headset_autoplay"
app:summary="@string/set_headset_autoplay_desc"
app:title="@string/set_headset_autoplay" />
<org.oxycblt.auxio.settings.pref.IntListPreference
<org.oxycblt.auxio.settings.ui.IntListPreference
app:allowDividerBelow="false"
app:defaultValue="@integer/replay_gain_off"
app:entries="@array/entries_replay_gain"
@ -106,7 +106,7 @@
app:layout="@layout/item_header"
app:title="@string/set_behavior">
<org.oxycblt.auxio.settings.pref.IntListPreference
<org.oxycblt.auxio.settings.ui.IntListPreference
app:defaultValue="@integer/play_mode_songs"
app:entries="@array/entries_song_playback_mode"
app:entryValues="@array/values_song_playback_mode"
@ -115,14 +115,14 @@
app:title="@string/set_song_mode"
app:useSimpleSummaryProvider="true" />
<org.oxycblt.auxio.settings.pref.M3SwitchPreference
<org.oxycblt.auxio.settings.ui.M3SwitchPreference
app:defaultValue="true"
app:iconSpaceReserved="false"
app:key="KEY_KEEP_SHUFFLE"
app:summary="@string/set_keep_shuffle_desc"
app:title="@string/set_keep_shuffle" />
<org.oxycblt.auxio.settings.pref.M3SwitchPreference
<org.oxycblt.auxio.settings.ui.M3SwitchPreference
app:allowDividerBelow="false"
app:defaultValue="true"
app:iconSpaceReserved="false"
@ -130,7 +130,7 @@
app:summary="@string/set_rewind_prev_desc"
app:title="@string/set_rewind_prev" />
<org.oxycblt.auxio.settings.pref.M3SwitchPreference
<org.oxycblt.auxio.settings.ui.M3SwitchPreference
app:allowDividerBelow="false"
app:defaultValue="false"
app:iconSpaceReserved="false"