musikr: start unwinding di use

Musikr is eventually going to be an entirely independent gradle module
with a DI-agnostic API, start removing some of the directives (but not
all since some are kinda thorny to untangle)
This commit is contained in:
Alexander Capehart 2024-12-13 08:32:30 -07:00
parent 34217696c2
commit 59df1c3d57
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
19 changed files with 73 additions and 286 deletions

View file

@ -53,18 +53,14 @@ interface MusicSettings : Settings<MusicSettings.Listener> {
}
}
class MusicSettingsImpl
@Inject
constructor(
@ApplicationContext context: Context,
private val musicLocationFactory: MusicLocation.Factory
) : Settings.Impl<MusicSettings.Listener>(context), MusicSettings {
class MusicSettingsImpl @Inject constructor(@ApplicationContext private val context: Context) :
Settings.Impl<MusicSettings.Listener>(context), MusicSettings {
override var musicLocations: List<MusicLocation>
get() {
val dirs =
sharedPreferences.getStringSet(getString(R.string.set_key_music_locations), null)
?: emptySet()
return dirs.mapNotNull { musicLocationFactory.existing(Uri.parse(it)) }
return dirs.mapNotNull { MusicLocation.existing(context, Uri.parse(it)) }
}
set(value) {
sharedPreferences.edit {

View file

@ -18,10 +18,12 @@
package org.oxycblt.auxio.music
import android.content.Context
import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@ -49,10 +51,11 @@ import timber.log.Timber as L
class MusicViewModel
@Inject
constructor(
@ApplicationContext context: Context,
private val listSettings: ListSettings,
private val musicRepository: MusicRepository,
private val externalPlaylistManager: ExternalPlaylistManager
private val musicRepository: MusicRepository
) : ViewModel(), MusicRepository.UpdateListener, MusicRepository.IndexingListener {
private val externalPlaylistManager = ExternalPlaylistManager.from(context)
private val _indexingState = MutableStateFlow<IndexingState?>(null)

View file

@ -19,8 +19,6 @@
package org.oxycblt.auxio.music.locations
import android.content.ActivityNotFoundException
import android.content.ContentResolver
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
@ -50,7 +48,6 @@ class MusicSourcesDialog :
ViewBindingMaterialDialogFragment<DialogMusicLocationsBinding>(), LocationAdapter.Listener {
private val locationAdapter = LocationAdapter(this)
private var openDocumentTreeLauncher: ActivityResultLauncher<Uri?>? = null
@Inject lateinit var musicLocationFactory: MusicLocation.Factory
@Inject lateinit var musicSettings: MusicSettings
override fun onCreateBinding(inflater: LayoutInflater) =
@ -102,7 +99,7 @@ class MusicSourcesDialog :
val locations =
savedInstanceState?.getStringArrayList(KEY_PENDING_LOCATIONS)?.mapNotNull {
musicLocationFactory.existing(Uri.parse(it))
MusicLocation.existing(requireContext(), Uri.parse(it))
} ?: musicSettings.musicLocations
locationAdapter.addAll(locations)
@ -126,8 +123,6 @@ class MusicSourcesDialog :
requireBinding().locationsEmpty.isVisible = locationAdapter.locations.isEmpty()
}
@Inject lateinit var contentResolver: ContentResolver
/**
* Add a Document Tree [Uri] chosen by the user to the current [MusicLocation]s.
*
@ -140,11 +135,7 @@ class MusicSourcesDialog :
L.d("No URI given (user closed the dialog)")
return
}
val takeFlags: Int =
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
contentResolver.takePersistableUriPermission(uri, takeFlags)
val location = musicLocationFactory.new(uri)
val location = MusicLocation.new(requireContext(), uri)
if (location != null) {
locationAdapter.add(location)

View file

@ -1,33 +0,0 @@
/*
* Copyright (c) 2023 Auxio Project
* FsModule.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.musikr.fs
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
interface FsModule {
@Binds
fun musicLocationFactory(
musicLocationFactoryImpl: MusicLocationFactoryImpl
): MusicLocation.Factory
}

View file

@ -22,8 +22,6 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import android.provider.DocumentsContract
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import org.oxycblt.musikr.fs.path.DocumentPathFactory
import org.oxycblt.musikr.fs.query.contentResolverSafe
@ -42,43 +40,34 @@ class MusicLocation internal constructor(val uri: Uri, val path: Path) {
return "$uri=${volumeId}:${path.components.unixString}"
}
interface Factory {
fun new(uri: Uri): MusicLocation?
fun existing(uri: Uri): MusicLocation?
}
}
class MusicLocationFactoryImpl
@Inject
constructor(
@ApplicationContext private val context: Context,
private val documentPathFactory: DocumentPathFactory
) : MusicLocation.Factory {
override fun new(uri: Uri): MusicLocation? {
if (!DocumentsContract.isTreeUri(uri)) return null
val path = documentPathFactory.unpackDocumentTreeUri(uri) ?: return null
val notPersisted =
context.contentResolverSafe.persistedUriPermissions.none {
it.uri == uri && it.isReadPermission && it.isWritePermission
companion object {
fun new(context: Context, uri: Uri): MusicLocation? {
if (!DocumentsContract.isTreeUri(uri)) return null
val documentPathFactory = DocumentPathFactory.from(context)
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(
uri,
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)
fun existing(context: Context, uri: Uri): MusicLocation? {
val documentPathFactory = DocumentPathFactory.from(context)
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)
}
}
}

View file

@ -24,7 +24,6 @@ import android.net.Uri
import android.provider.DocumentsContract
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.File
import javax.inject.Inject
import org.oxycblt.musikr.fs.Components
import org.oxycblt.musikr.fs.Path
import org.oxycblt.musikr.fs.Volume
@ -68,11 +67,17 @@ interface DocumentPathFactory {
* @return The [Path] instance, or null if the path could not be deserialized.
*/
fun fromDocumentId(path: String): Path?
companion object {
fun from(context: Context): DocumentPathFactory {
val volumeManager = VolumeManager.from(context)
val pathInterpreter = MediaStorePathInterpreter.Factory.from(volumeManager)
return DocumentPathFactoryImpl(context, volumeManager, pathInterpreter)
}
}
}
class DocumentPathFactoryImpl
@Inject
constructor(
private class DocumentPathFactoryImpl(
@ApplicationContext private val context: Context,
private val volumeManager: VolumeManager,
private val mediaStorePathInterpreterFactory: MediaStorePathInterpreter.Factory

View file

@ -1,49 +0,0 @@
/*
* Copyright (c) 2024 Auxio Project
* PathModule.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.musikr.fs.path
import android.content.Context
import android.os.storage.StorageManager
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import org.oxycblt.auxio.util.getSystemServiceCompat
@Module
@InstallIn(SingletonComponent::class)
class PathModule {
@Provides
fun volumeManager(@ApplicationContext context: Context): VolumeManager =
VolumeManagerImpl(context.getSystemServiceCompat(StorageManager::class))
@Provides
fun mediaStorePathInterpreterFactory(
volumeManager: VolumeManager
): MediaStorePathInterpreter.Factory = MediaStorePathInterpreter.Factory.from(volumeManager)
}
@Module
@InstallIn(SingletonComponent::class)
interface PathBindsModule {
@Binds
fun documentPathFactory(documentTreePathFactory: DocumentPathFactoryImpl): DocumentPathFactory
}

View file

@ -21,7 +21,6 @@ package org.oxycblt.musikr.fs.path
import android.content.Context
import android.os.storage.StorageManager
import android.os.storage.StorageVolume
import javax.inject.Inject
import org.oxycblt.musikr.fs.Components
import org.oxycblt.musikr.fs.Volume
@ -40,10 +39,14 @@ interface VolumeManager {
* @see StorageManager.getStorageVolumes
*/
fun getVolumes(): List<Volume>
companion object {
fun from(context: Context): VolumeManager =
VolumeManagerImpl(context.getSystemService(StorageManager::class.java))
}
}
class VolumeManagerImpl @Inject constructor(private val storageManager: StorageManager) :
VolumeManager {
private class VolumeManagerImpl(private val storageManager: StorageManager) : VolumeManager {
override fun getInternalVolume(): Volume.Internal =
InternalVolumeImpl(storageManager.primaryStorageVolume)

View file

@ -22,8 +22,6 @@ import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import android.provider.DocumentsContract
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
@ -36,6 +34,10 @@ import org.oxycblt.musikr.fs.Path
interface DeviceFiles {
fun explore(locations: Flow<MusicLocation>): Flow<DeviceFile>
companion object {
fun from(context: Context): DeviceFiles = DeviceFilesImpl(context.contentResolverSafe)
}
}
data class DeviceFile(
@ -47,10 +49,7 @@ data class DeviceFile(
)
@OptIn(ExperimentalCoroutinesApi::class)
class DeviceFilesImpl @Inject constructor(@ApplicationContext private val context: Context) :
DeviceFiles {
private val contentResolver = context.contentResolverSafe
private class DeviceFilesImpl(private val contentResolver: ContentResolver) : DeviceFiles {
override fun explore(locations: Flow<MusicLocation>): Flow<DeviceFile> =
locations.flatMapMerge { location ->
exploreImpl(

View file

@ -1,42 +0,0 @@
/*
* Copyright (c) 2024 Auxio Project
* QueryModule.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.musikr.fs.query
import android.content.ContentResolver
import android.content.Context
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
class QueryProvidesModule {
@Provides
fun contentResolver(@ApplicationContext context: Context): ContentResolver =
context.contentResolverSafe
}
@Module
@InstallIn(SingletonComponent::class)
interface QueryBindsModule {
@Binds fun deviceFiles(deviceFilesImpl: DeviceFilesImpl): DeviceFiles
}

View file

@ -1,30 +0,0 @@
/*
* Copyright (c) 2023 Auxio Project
* GraphModule.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.musikr.graph
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
interface GraphModule {
@Binds fun musicGraphFactory(interpreter: MusicGraphFactoryImpl): MusicGraph.Factory
}

View file

@ -18,7 +18,6 @@
package org.oxycblt.musikr.graph
import javax.inject.Inject
import org.oxycblt.auxio.util.unlikelyToBeNull
import org.oxycblt.musikr.Music
import org.oxycblt.musikr.tag.interpret.PreAlbum
@ -39,15 +38,11 @@ data class MusicGraph(
fun build(): MusicGraph
}
interface Factory {
fun builder(): Builder
companion object {
fun builder(): Builder = MusicGraphBuilderImpl()
}
}
class MusicGraphFactoryImpl @Inject constructor() : MusicGraph.Factory {
override fun builder(): MusicGraph.Builder = MusicGraphBuilderImpl()
}
private class MusicGraphBuilderImpl : MusicGraph.Builder {
private val songVertices = mutableMapOf<Music.UID, SongVertex>()
private val albumVertices = mutableMapOf<PreAlbum, AlbumVertex>()

View file

@ -43,7 +43,6 @@ class EvaluateStepImpl
@Inject
constructor(
private val tagInterpreter: TagInterpreter,
private val musicGraphFactory: MusicGraph.Factory,
private val libraryFactory: LibraryFactory
) : EvaluateStep {
override suspend fun evaluate(
@ -56,7 +55,7 @@ constructor(
.map { tagInterpreter.interpret(it.file, it.tags, it.cover, interpretation) }
.flowOn(Dispatchers.Main)
.buffer(Channel.UNLIMITED)
val graphBuilder = musicGraphFactory.builder()
val graphBuilder = MusicGraph.builder()
preSongs.collect { graphBuilder.add(it) }
val graph = graphBuilder.build()
return libraryFactory.create(graph)

View file

@ -18,6 +18,8 @@
package org.oxycblt.musikr.pipeline
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
@ -33,9 +35,10 @@ interface ExploreStep {
fun explore(locations: List<MusicLocation>): Flow<ExploreNode>
}
class ExploreStepImpl @Inject constructor(private val deviceFiles: DeviceFiles) : ExploreStep {
class ExploreStepImpl @Inject constructor(@ApplicationContext private val context: Context) :
ExploreStep {
override fun explore(locations: List<MusicLocation>) =
deviceFiles
DeviceFiles.from(context)
.explore(locations.asFlow())
.mapNotNull {
when {

View file

@ -27,7 +27,6 @@ import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import org.oxycblt.ktaglib.KTagLib
import org.oxycblt.musikr.Storage
import org.oxycblt.musikr.cache.CachedSong
import org.oxycblt.musikr.cover.Cover
@ -36,7 +35,6 @@ import org.oxycblt.musikr.fs.query.DeviceFile
import org.oxycblt.musikr.metadata.MetadataExtractor
import org.oxycblt.musikr.tag.parse.ParsedTags
import org.oxycblt.musikr.tag.parse.TagParser
import timber.log.Timber
interface ExtractStep {
fun extract(storage: Storage, nodes: Flow<ExploreNode>): Flow<ExtractedMusic>

View file

@ -20,8 +20,6 @@ package org.oxycblt.musikr.playlist
import android.content.Context
import android.net.Uri
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import org.oxycblt.musikr.Playlist
import org.oxycblt.musikr.fs.Components
import org.oxycblt.musikr.fs.Path
@ -56,6 +54,12 @@ interface ExternalPlaylistManager {
* @return True if the playlist was successfully exported, false otherwise.
*/
suspend fun export(playlist: Playlist, uri: Uri, config: ExportConfig): Boolean
companion object {
fun from(context: Context): ExternalPlaylistManager =
ExternalPlaylistManagerImpl(
context, DocumentPathFactory.from(context), M3U.from(context))
}
}
/**
@ -81,10 +85,8 @@ data class ImportedPlaylist(val name: String?, val paths: List<PossiblePaths>)
typealias PossiblePaths = List<Path>
class ExternalPlaylistManagerImpl
@Inject
constructor(
@ApplicationContext private val context: Context,
class ExternalPlaylistManagerImpl(
private val context: Context,
private val documentPathFactory: DocumentPathFactory,
private val m3u: M3U
) : ExternalPlaylistManager {

View file

@ -17,17 +17,3 @@
*/
package org.oxycblt.musikr.playlist
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
interface PlaylistModule {
@Binds
fun externalPlaylistManager(
externalPlaylistManager: ExternalPlaylistManagerImpl
): ExternalPlaylistManager
}

View file

@ -75,6 +75,8 @@ interface M3U {
companion object {
/** The mime type used for M3U files by the android system. */
const val MIME_TYPE = "audio/x-mpegurl"
fun from(context: Context): M3U = M3UImpl(context, VolumeManager.from(context))
}
}

View file

@ -1,30 +0,0 @@
/*
* Copyright (c) 2024 Auxio Project
* M3UModule.kt is part of Auxio.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.musikr.playlist.m3u
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
interface PlaylistModule {
@Binds fun m3u(m3u: M3UImpl): M3U
}