musikr: improve music location creation

This commit is contained in:
Alexander Capehart 2024-12-07 17:19:30 -07:00
parent 2a38d1ae8d
commit c270759dec
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
3 changed files with 55 additions and 51 deletions

View file

@ -24,8 +24,8 @@ import androidx.core.content.edit
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject import javax.inject.Inject
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.musikr.fs.path.DocumentPathFactory
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.musikr.fs.MusicLocation
import timber.log.Timber as L import timber.log.Timber as L
/** /**
@ -35,7 +35,7 @@ import timber.log.Timber as L
*/ */
interface MusicSettings : Settings<MusicSettings.Listener> { interface MusicSettings : Settings<MusicSettings.Listener> {
/** The locations of music to load. */ /** The locations of music to load. */
var musicLocations: List<Uri> var musicLocations: List<MusicLocation>
/** Whether to exclude non-music audio files from the music library. */ /** Whether to exclude non-music audio files from the music library. */
val excludeNonMusic: Boolean val excludeNonMusic: Boolean
/** Whether to be actively watching for changes in the music library. */ /** Whether to be actively watching for changes in the music library. */
@ -57,41 +57,19 @@ class MusicSettingsImpl
@Inject @Inject
constructor( constructor(
@ApplicationContext context: Context, @ApplicationContext context: Context,
private val documentPathFactory: DocumentPathFactory private val musicLocationFactory: MusicLocation.Factory
) : Settings.Impl<MusicSettings.Listener>(context), MusicSettings { ) : Settings.Impl<MusicSettings.Listener>(context), MusicSettings {
// override var musicDirs: MusicDirectories override var musicLocations: List<MusicLocation>
// get() {
// val dirs =
// (sharedPreferences.getStringSet(getString(R.string.set_key_music_dirs), null)
// ?: emptySet())
// .mapNotNull(documentPathFactory::fromDocumentId)
// return MusicDirectories(
// dirs,
// sharedPreferences.getBoolean(getString(R.string.set_key_music_dirs_include),
// false))
// }
// set(value) {
// sharedPreferences.edit {
// putStringSet(
// getString(R.string.set_key_music_dirs),
// value.dirs.map(documentPathFactory::toDocumentId).toSet())
// putBoolean(getString(R.string.set_key_music_dirs_include),
// value.shouldInclude)
// apply()
// }
// }
override var musicLocations: List<Uri>
get() { get() {
val dirs = val dirs =
sharedPreferences.getStringSet(getString(R.string.set_key_music_locations), null) sharedPreferences.getStringSet(getString(R.string.set_key_music_locations), null)
?: emptySet() ?: emptySet()
return dirs.map { Uri.parse(it) } return dirs.mapNotNull { musicLocationFactory.existing(Uri.parse(it)) }
} }
set(value) { set(value) {
sharedPreferences.edit { sharedPreferences.edit {
putStringSet( putStringSet(
getString(R.string.set_key_music_locations), value.map(Uri::toString).toSet()) getString(R.string.set_key_music_locations), value.map { it.toString() }.toSet())
apply() apply()
} }
} }

View file

@ -20,7 +20,6 @@ package org.oxycblt.auxio.music.locations
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
@ -51,8 +50,10 @@ class MusicSourcesDialog :
ViewBindingMaterialDialogFragment<DialogMusicLocationsBinding>(), LocationAdapter.Listener { ViewBindingMaterialDialogFragment<DialogMusicLocationsBinding>(), LocationAdapter.Listener {
private val locationAdapter = LocationAdapter(this) private val locationAdapter = LocationAdapter(this)
private var openDocumentTreeLauncher: ActivityResultLauncher<Uri?>? = null private var openDocumentTreeLauncher: ActivityResultLauncher<Uri?>? = null
@Inject lateinit var musicLocationFactory: MusicLocation.Factory @Inject
@Inject lateinit var musicSettings: MusicSettings lateinit var musicLocationFactory: MusicLocation.Factory
@Inject
lateinit var musicSettings: MusicSettings
override fun onCreateBinding(inflater: LayoutInflater) = override fun onCreateBinding(inflater: LayoutInflater) =
DialogMusicLocationsBinding.inflate(inflater) DialogMusicLocationsBinding.inflate(inflater)
@ -62,7 +63,7 @@ class MusicSourcesDialog :
.setTitle(R.string.set_locations) .setTitle(R.string.set_locations)
.setNegativeButton(R.string.lbl_cancel, null) .setNegativeButton(R.string.lbl_cancel, null)
.setPositiveButton(R.string.lbl_save) { _, _ -> .setPositiveButton(R.string.lbl_save) { _, _ ->
val newDirs = locationAdapter.locations.map { it.uri } val newDirs = locationAdapter.locations
if (musicSettings.musicLocations != newDirs) { if (musicSettings.musicLocations != newDirs) {
L.d("Committing changes") L.d("Committing changes")
musicSettings.musicLocations = newDirs musicSettings.musicLocations = newDirs
@ -77,7 +78,8 @@ class MusicSourcesDialog :
openDocumentTreeLauncher = openDocumentTreeLauncher =
registerForActivityResult( registerForActivityResult(
ActivityResultContracts.OpenDocumentTree(), ActivityResultContracts.OpenDocumentTree(),
::addDocumentTreeUriToDirs) ::addDocumentTreeUriToDirs
)
binding.locationsAdd.apply { binding.locationsAdd.apply {
ViewCompat.setTooltipText(this, contentDescription) ViewCompat.setTooltipText(this, contentDescription)
@ -102,13 +104,11 @@ class MusicSourcesDialog :
itemAnimator = null itemAnimator = null
} }
val locationUris =
savedInstanceState?.getStringArrayList(KEY_PENDING_LOCATIONS)?.map { Uri.parse(it) }
?: musicSettings.musicLocations
val locations = val locations =
locationUris.mapNotNull { savedInstanceState?.getStringArrayList(KEY_PENDING_LOCATIONS)?.mapNotNull {
musicLocationFactory.create(it) musicLocationFactory.existing(Uri.parse(it))
} }
?: musicSettings.musicLocations
locationAdapter.addAll(locations) locationAdapter.addAll(locations)
requireBinding().locationsEmpty.isVisible = locations.isEmpty() requireBinding().locationsEmpty.isVisible = locations.isEmpty()
@ -117,7 +117,8 @@ class MusicSourcesDialog :
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
outState.putStringArrayList( outState.putStringArrayList(
KEY_PENDING_LOCATIONS, ArrayList(locationAdapter.locations.map { it.uri.toString() })) KEY_PENDING_LOCATIONS, ArrayList(locationAdapter.locations.map { it.uri.toString() })
)
} }
override fun onDestroyBinding(binding: DialogMusicLocationsBinding) { override fun onDestroyBinding(binding: DialogMusicLocationsBinding) {
@ -131,7 +132,8 @@ class MusicSourcesDialog :
requireBinding().locationsEmpty.isVisible = locationAdapter.locations.isEmpty() requireBinding().locationsEmpty.isVisible = locationAdapter.locations.isEmpty()
} }
@Inject lateinit var contentResolver: ContentResolver @Inject
lateinit var contentResolver: ContentResolver
/** /**
* Add a Document Tree [Uri] chosen by the user to the current [MusicLocation]s. * Add a Document Tree [Uri] chosen by the user to the current [MusicLocation]s.
@ -149,7 +151,7 @@ class MusicSourcesDialog :
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
contentResolver.takePersistableUriPermission(uri, takeFlags) contentResolver.takePersistableUriPermission(uri, takeFlags)
val location = musicLocationFactory.create(uri) val location = musicLocationFactory.new(uri)
if (location != null) { if (location != null) {
locationAdapter.add(location) locationAdapter.add(location)

View file

@ -6,6 +6,7 @@ import android.net.Uri
import android.provider.DocumentsContract import android.provider.DocumentsContract
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import org.oxycblt.musikr.fs.path.DocumentPathFactory import org.oxycblt.musikr.fs.path.DocumentPathFactory
import org.oxycblt.musikr.fs.path.VolumeManager
import javax.inject.Inject import javax.inject.Inject
class MusicLocation internal constructor(val uri: Uri, val path: Path) { class MusicLocation internal constructor(val uri: Uri, val path: Path) {
@ -13,10 +14,18 @@ class MusicLocation internal constructor(val uri: Uri, val path: Path) {
other is MusicLocation && uri == other.uri && path == other.path other is MusicLocation && uri == other.uri && path == other.path
override fun hashCode() = 31 * uri.hashCode() + path.hashCode() override fun hashCode() = 31 * uri.hashCode() + path.hashCode()
override fun toString() = "src:$uri=$path" override fun toString(): String {
val volumeId = when (path.volume) {
is Volume.Internal -> VOLUME_INTERNAL
is Volume.External -> path.volume.id
}
return "$uri=${volumeId}:${path.components.unixString}"
}
interface Factory { interface Factory {
fun create(uri: Uri): MusicLocation? fun new(uri: Uri): MusicLocation?
fun existing(uri: Uri): MusicLocation?
} }
} }
@ -24,13 +33,28 @@ class MusicLocationFactoryImpl @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val documentPathFactory: DocumentPathFactory private val documentPathFactory: DocumentPathFactory
) : MusicLocation.Factory { ) : MusicLocation.Factory {
override fun create(uri: Uri): MusicLocation? { override fun new(uri: Uri): MusicLocation? {
check(DocumentsContract.isTreeUri(uri)) { "URI $uri is not a document tree URI" } if (!DocumentsContract.isTreeUri(uri)) return null
val path = documentPathFactory.unpackDocumentTreeUri(uri) ?: return null val path = documentPathFactory.unpackDocumentTreeUri(uri) ?: return null
val notPersisted = context.contentResolverSafe.persistedUriPermissions
.none { it.uri == uri && it.isReadPermission && it.isWritePermission }
if (notPersisted) {
context.contentResolverSafe.takePersistableUriPermission( context.contentResolverSafe.takePersistableUriPermission(
uri, uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
) )
}
return MusicLocation(uri, path)
}
override fun existing(uri: Uri): MusicLocation? {
if (!DocumentsContract.isTreeUri(uri)) return null
val notPersisted = context.contentResolverSafe.persistedUriPermissions
.none { it.uri == uri && it.isReadPermission && it.isWritePermission }
if (notPersisted) return null
val path = documentPathFactory.unpackDocumentTreeUri(uri) ?: return null
return MusicLocation(uri, path) return MusicLocation(uri, path)
} }
} }
private const val VOLUME_INTERNAL = "internal"