musikr: improve music location creation
This commit is contained in:
parent
2a38d1ae8d
commit
c270759dec
3 changed files with 55 additions and 51 deletions
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,12 +15,11 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.locations
|
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)
|
||||||
|
|
|
@ -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
|
||||||
context.contentResolverSafe.takePersistableUriPermission(
|
val notPersisted = context.contentResolverSafe.persistedUriPermissions
|
||||||
uri,
|
.none { it.uri == uri && it.isReadPermission && it.isWritePermission }
|
||||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
if (notPersisted) {
|
||||||
)
|
context.contentResolverSafe.takePersistableUriPermission(
|
||||||
|
uri,
|
||||||
|
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
|
)
|
||||||
|
}
|
||||||
return MusicLocation(uri, path)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val VOLUME_INTERNAL = "internal"
|
Loading…
Reference in a new issue