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:
parent
107f7bee27
commit
ab1ff416e1
12 changed files with 119 additions and 52 deletions
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue