music: hide musicstore impl

Hide the MusicStore implementation behind an interface, transforming it
into a new MusicRepository class.

This is in preparation for dependency injection.
This commit is contained in:
Alexander Capehart 2023-01-29 15:57:46 -07:00
parent bb2ea9df27
commit b34e6fdc8a
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
11 changed files with 79 additions and 75 deletions

View file

@ -32,7 +32,6 @@ import org.oxycblt.auxio.detail.recycler.SortHeader
import org.oxycblt.auxio.list.BasicHeader
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.format.AudioInfo
import org.oxycblt.auxio.music.format.Disc
import org.oxycblt.auxio.music.format.ReleaseType
@ -49,8 +48,8 @@ import org.oxycblt.auxio.util.*
* @author Alexander Capehart (OxygenCobalt)
*/
class DetailViewModel(application: Application) :
AndroidViewModel(application), MusicStore.Listener {
private val musicStore = MusicStore.getInstance()
AndroidViewModel(application), MusicRepository.Listener {
private val musicRepository = MusicRepository.get()
private val musicSettings = MusicSettings.from(application)
private val playbackSettings = PlaybackSettings.from(application)
private val audioInfoProvider = AudioInfo.Provider.from(application)
@ -137,11 +136,11 @@ class DetailViewModel(application: Application) :
get() = playbackSettings.inParentPlaybackMode
init {
musicStore.addListener(this)
musicRepository.addListener(this)
}
override fun onCleared() {
musicStore.removeListener(this)
musicRepository.removeListener(this)
}
override fun onLibraryChanged(library: Library?) {
@ -235,7 +234,7 @@ class DetailViewModel(application: Application) :
_currentGenre.value = requireMusic<Genre>(uid)?.also(::refreshGenreList)
}
private fun <T : Music> requireMusic(uid: Music.UID) = musicStore.library?.find<T>(uid)
private fun <T : Music> requireMusic(uid: Music.UID) = musicRepository.library?.find<T>(uid)
/**
* Start a new job to load a given [Song]'s [AudioInfo]. Result is pushed to [songAudioInfo].

View file

@ -23,7 +23,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.home.tabs.Tab
import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.library.Library
import org.oxycblt.auxio.music.library.Sort
import org.oxycblt.auxio.playback.PlaybackSettings
@ -34,8 +33,8 @@ import org.oxycblt.auxio.util.logD
* @author Alexander Capehart (OxygenCobalt)
*/
class HomeViewModel(application: Application) :
AndroidViewModel(application), MusicStore.Listener, HomeSettings.Listener {
private val musicStore = MusicStore.getInstance()
AndroidViewModel(application), MusicRepository.Listener, HomeSettings.Listener {
private val musicRepository = MusicRepository.get()
private val homeSettings = HomeSettings.from(application)
private val musicSettings = MusicSettings.from(application)
private val playbackSettings = PlaybackSettings.from(application)
@ -92,13 +91,13 @@ class HomeViewModel(application: Application) :
val isFastScrolling: StateFlow<Boolean> = _isFastScrolling
init {
musicStore.addListener(this)
musicRepository.addListener(this)
homeSettings.registerListener(this)
}
override fun onCleared() {
super.onCleared()
musicStore.removeListener(this)
musicRepository.removeListener(this)
homeSettings.unregisterListener(this)
}
@ -130,7 +129,7 @@ class HomeViewModel(application: Application) :
override fun onHideCollaboratorsChanged() {
// Changes in the hide collaborator setting will change the artist contents
// of the library, consider it a library update.
onLibraryChanged(musicStore.library)
onLibraryChanged(musicRepository.library)
}
/**

View file

@ -21,15 +21,14 @@ import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.library.Library
/**
* A [ViewModel] that manages the current selection.
* @author Alexander Capehart (OxygenCobalt)
*/
class SelectionViewModel : ViewModel(), MusicStore.Listener {
private val musicStore = MusicStore.getInstance()
class SelectionViewModel : ViewModel(), MusicRepository.Listener {
private val musicRepository = MusicRepository.get()
private val _selected = MutableStateFlow(listOf<Music>())
/** the currently selected items. These are ordered in earliest selected and latest selected. */
@ -37,7 +36,7 @@ class SelectionViewModel : ViewModel(), MusicStore.Listener {
get() = _selected
init {
musicStore.addListener(this)
musicRepository.addListener(this)
}
override fun onLibraryChanged(library: Library?) {
@ -60,7 +59,7 @@ class SelectionViewModel : ViewModel(), MusicStore.Listener {
override fun onCleared() {
super.onCleared()
musicStore.removeListener(this)
musicRepository.removeListener(this)
}
/**

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2021 Auxio Project
* Copyright (c) 2023 Auxio Project
*
* 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
@ -28,22 +28,13 @@ import org.oxycblt.auxio.music.library.Library
*
* @author Alexander Capehart (OxygenCobalt)
*/
class MusicStore private constructor() {
private val listeners = mutableListOf<Listener>()
interface MusicRepository {
/**
* The current [Library]. May be null if a [Library] has not been successfully loaded yet. This
* can change, so it's highly recommended to not access this directly and instead rely on
* [Listener].
*/
@Volatile
var library: Library? = null
set(value) {
field = value
for (callback in listeners) {
callback.onLibraryChanged(library)
}
}
var library: Library?
/**
* Add a [Listener] to this instance. This can be used to receive changes in the music library.
@ -51,11 +42,7 @@ class MusicStore private constructor() {
* @param listener The [Listener] to add.
* @see Listener
*/
@Synchronized
fun addListener(listener: Listener) {
listener.onLibraryChanged(library)
listeners.add(listener)
}
fun addListener(listener: Listener)
/**
* Remove a [Listener] from this instance, preventing it from receiving any further updates.
@ -63,12 +50,9 @@ class MusicStore private constructor() {
* the first place.
* @see Listener
*/
@Synchronized
fun removeListener(listener: Listener) {
listeners.remove(listener)
}
fun removeListener(listener: Listener)
/** A listener for changes in the music library. */
/** A listener for changes in [MusicRepository] */
interface Listener {
/**
* Called when the current [Library] has changed.
@ -78,23 +62,47 @@ class MusicStore private constructor() {
}
companion object {
@Volatile private var INSTANCE: MusicStore? = null
@Volatile private var INSTANCE: MusicRepository? = null
/**
* Get a singleton instance.
* @return The (possibly newly-created) singleton instance.
*/
fun getInstance(): MusicStore {
fun get(): MusicRepository {
val currentInstance = INSTANCE
if (currentInstance != null) {
return currentInstance
}
synchronized(this) {
val newInstance = MusicStore()
val newInstance = RealMusicRepository()
INSTANCE = newInstance
return newInstance
}
}
}
}
private class RealMusicRepository : MusicRepository {
private val listeners = mutableListOf<MusicRepository.Listener>()
@Volatile
override var library: Library? = null
set(value) {
field = value
for (callback in listeners) {
callback.onLibraryChanged(library)
}
}
@Synchronized
override fun addListener(listener: MusicRepository.Listener) {
listener.onLibraryChanged(library)
listeners.add(listener)
}
@Synchronized
override fun removeListener(listener: MusicRepository.Listener) {
listeners.remove(listener)
}
}

View file

@ -29,7 +29,8 @@ import org.oxycblt.auxio.util.logD
* Organized music library information.
*
* This class allows for the creation of a well-formed music library graph from raw song
* information. It's generally not expected to create this yourself and instead use [MusicStore].
* information. It's generally not expected to create this yourself and instead use
* [MusicRepository].
*
* @author Alexander Capehart
*/

View file

@ -21,7 +21,6 @@ import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.library.Library
import org.oxycblt.auxio.util.unlikelyToBeNull
@ -30,8 +29,8 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* contain the music themselves and then exit if the library changes.
* @author Alexander Capehart (OxygenCobalt)
*/
class PickerViewModel : ViewModel(), MusicStore.Listener {
private val musicStore = MusicStore.getInstance()
class PickerViewModel : ViewModel(), MusicRepository.Listener {
private val musicRepository = MusicRepository.get()
private val _currentItem = MutableStateFlow<Music?>(null)
/** The current item whose artists should be shown in the picker. Null if there is no item. */
@ -49,7 +48,7 @@ class PickerViewModel : ViewModel(), MusicStore.Listener {
get() = _genreChoices
override fun onCleared() {
musicStore.removeListener(this)
musicRepository.removeListener(this)
}
override fun onLibraryChanged(library: Library?) {
@ -63,7 +62,7 @@ class PickerViewModel : ViewModel(), MusicStore.Listener {
* @param uid The [Music.UID] of the [Song] to update to.
*/
fun setItemUid(uid: Music.UID) {
val library = unlikelyToBeNull(musicStore.library)
val library = unlikelyToBeNull(musicRepository.library)
_currentItem.value = library.find(uid)
refreshChoices()
}

View file

@ -39,8 +39,8 @@ import org.oxycblt.auxio.util.logW
*
* This class provides low-level access into the exact state of the music loading process. **This
* class should not be used in most cases.** It is highly volatile and provides far more information
* than is usually needed. Use [MusicStore] instead if you do not need to work with the exact music
* loading state.
* than is usually needed. Use [MusicRepository] instead if you do not need to work with the exact
* music loading state.
*
* @author Alexander Capehart (OxygenCobalt)
*/
@ -345,8 +345,8 @@ class Indexer private constructor() {
* A listener for rapid-fire changes in the music loading state.
*
* This is only useful for code that absolutely must show the current loading process.
* Otherwise, [MusicStore.Listener] is highly recommended due to it's updates only consisting of
* the [Library].
* Otherwise, [MusicRepository.Listener] is highly recommended due to it's updates only
* consisting of the [Library].
*/
interface Listener {
/**

View file

@ -31,8 +31,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.storage.contentResolverSafe
import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.service.ForegroundManager
@ -55,7 +55,7 @@ import org.oxycblt.auxio.util.logD
*/
class IndexerService : Service(), Indexer.Controller, MusicSettings.Listener {
private val indexer = Indexer.getInstance()
private val musicStore = MusicStore.getInstance()
private val musicRepository = MusicRepository.get()
private val playbackManager = PlaybackStateManager.get()
private val serviceJob = Job()
private val indexScope = CoroutineScope(serviceJob + Dispatchers.IO)
@ -85,7 +85,7 @@ class IndexerService : Service(), Indexer.Controller, MusicSettings.Listener {
indexer.registerController(this)
// An indeterminate indexer and a missing library implies we are extremely early
// in app initialization so start loading music.
if (musicStore.library == null && indexer.isIndeterminate) {
if (musicRepository.library == null && indexer.isIndeterminate) {
logD("No library present and no previous response, indexing music now")
onStartIndexing(true)
}
@ -129,11 +129,11 @@ class IndexerService : Service(), Indexer.Controller, MusicSettings.Listener {
is Indexer.State.Indexing -> updateActiveSession(state.indexing)
is Indexer.State.Complete -> {
val newLibrary = state.result.getOrNull()
if (newLibrary != null && newLibrary != musicStore.library) {
if (newLibrary != null && newLibrary != musicRepository.library) {
logD("Applying new library")
// We only care if the newly-loaded library is going to replace a previously
// loaded library.
if (musicStore.library != null) {
if (musicRepository.library != null) {
// Wipe possibly-invalidated outdated covers
imageLoader.memoryCache?.clear()
// Clear invalid models from PlaybackStateManager. This is not connected
@ -153,7 +153,7 @@ class IndexerService : Service(), Indexer.Controller, MusicSettings.Listener {
}
}
// Forward the new library to MusicStore to continue the update process.
musicStore.library = newLibrary
musicRepository.library = newLibrary
}
// On errors, while we would want to show a notification that displays the
// error, that requires the Android 13 notification permission, which is not

View file

@ -40,7 +40,7 @@ class PlaybackViewModel(application: Application) :
private val playbackSettings = PlaybackSettings.from(application)
private val playbackManager = PlaybackStateManager.get()
private val persistenceRepository = PersistenceRepository.from(application)
private val musicStore = MusicStore.getInstance()
private val musicRepository = MusicRepository.get()
private var lastPositionJob: Job? = null
private val _song = MutableStateFlow<Song?>(null)
@ -279,7 +279,7 @@ class PlaybackViewModel(application: Application) :
check(song == null || parent == null || parent.songs.contains(song)) {
"Song to play not in parent"
}
val library = musicStore.library ?: return
val library = musicRepository.library ?: return
val sort =
when (parent) {
is Genre -> musicSettings.genreSongSort
@ -449,7 +449,7 @@ class PlaybackViewModel(application: Application) :
*/
fun tryRestorePlaybackState(onDone: (Boolean) -> Unit) {
viewModelScope.launch {
val library = musicStore.library
val library = musicRepository.library
if (library != null) {
val savedState = persistenceRepository.readState(library)
if (savedState != null) {

View file

@ -43,8 +43,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.library.Library
import org.oxycblt.auxio.playback.PlaybackSettings
@ -80,7 +80,7 @@ class PlaybackService :
Player.Listener,
InternalPlayer,
MediaSessionComponent.Listener,
MusicStore.Listener {
MusicRepository.Listener {
// Player components
private lateinit var player: ExoPlayer
private lateinit var replayGainProcessor: ReplayGainAudioProcessor
@ -90,12 +90,12 @@ class PlaybackService :
private lateinit var widgetComponent: WidgetComponent
private val systemReceiver = PlaybackReceiver()
// Managers
// Shared components
private val playbackManager = PlaybackStateManager.get()
private val musicStore = MusicStore.getInstance()
private lateinit var musicSettings: MusicSettings
private lateinit var playbackSettings: PlaybackSettings
private lateinit var persistenceRepository: PersistenceRepository
private val musicRepository = MusicRepository.get()
private lateinit var musicSettings: MusicSettings
// State
private lateinit var foregroundManager: ForegroundManager
@ -153,7 +153,7 @@ class PlaybackService :
// Initialize any listener-dependent components last as we wouldn't want a listener race
// condition to cause us to load music before we were fully initialize.
playbackManager.registerInternalPlayer(this)
musicStore.addListener(this)
musicRepository.addListener(this)
widgetComponent = WidgetComponent(this)
mediaSessionComponent = MediaSessionComponent(this, this)
registerReceiver(
@ -193,7 +193,7 @@ class PlaybackService :
// Pause just in case this destruction was unexpected.
playbackManager.setPlaying(false)
playbackManager.unregisterInternalPlayer(this)
musicStore.removeListener(this)
musicRepository.removeListener(this)
unregisterReceiver(systemReceiver)
serviceJob.cancel()
@ -339,7 +339,7 @@ class PlaybackService :
override fun performAction(action: InternalPlayer.Action): Boolean {
val library =
musicStore.library
musicRepository.library
// No library, cannot do anything.
?: return false

View file

@ -30,7 +30,6 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.list.BasicHeader
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.library.Library
import org.oxycblt.auxio.music.library.Sort
import org.oxycblt.auxio.playback.PlaybackSettings
@ -41,8 +40,8 @@ import org.oxycblt.auxio.util.logD
* @author Alexander Capehart (OxygenCobalt)
*/
class SearchViewModel(application: Application) :
AndroidViewModel(application), MusicStore.Listener {
private val musicStore = MusicStore.getInstance()
AndroidViewModel(application), MusicRepository.Listener {
private val musicRepository = MusicRepository.get()
private val searchSettings = SearchSettings.from(application)
private val playbackSettings = PlaybackSettings.from(application)
private var searchEngine = SearchEngine.from(application)
@ -59,12 +58,12 @@ class SearchViewModel(application: Application) :
get() = playbackSettings.inListPlaybackMode
init {
musicStore.addListener(this)
musicRepository.addListener(this)
}
override fun onCleared() {
super.onCleared()
musicStore.removeListener(this)
musicRepository.removeListener(this)
}
override fun onLibraryChanged(library: Library?) {
@ -84,7 +83,7 @@ class SearchViewModel(application: Application) :
currentSearchJob?.cancel()
lastQuery = query
val library = musicStore.library
val library = musicRepository.library
if (query.isNullOrEmpty() || library == null) {
logD("Search query is not applicable.")
_searchResults.value = listOf()