From b34e6fdc8aa997f0c3aef7468f33ae37ba813c98 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Sun, 29 Jan 2023 15:57:46 -0700 Subject: [PATCH] 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. --- .../oxycblt/auxio/detail/DetailViewModel.kt | 11 ++-- .../org/oxycblt/auxio/home/HomeViewModel.kt | 11 ++-- .../list/selection/SelectionViewModel.kt | 9 ++- .../{MusicStore.kt => MusicRepository.kt} | 58 +++++++++++-------- .../oxycblt/auxio/music/library/Library.kt | 3 +- .../auxio/music/picker/PickerViewModel.kt | 9 ++- .../org/oxycblt/auxio/music/system/Indexer.kt | 8 +-- .../auxio/music/system/IndexerService.kt | 12 ++-- .../auxio/playback/PlaybackViewModel.kt | 6 +- .../auxio/playback/system/PlaybackService.kt | 16 ++--- .../oxycblt/auxio/search/SearchViewModel.kt | 11 ++-- 11 files changed, 79 insertions(+), 75 deletions(-) rename app/src/main/java/org/oxycblt/auxio/music/{MusicStore.kt => MusicRepository.kt} (80%) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index 88538beab..a5c8e79a4 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -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(uid)?.also(::refreshGenreList) } - private fun requireMusic(uid: Music.UID) = musicStore.library?.find(uid) + private fun requireMusic(uid: Music.UID) = musicRepository.library?.find(uid) /** * Start a new job to load a given [Song]'s [AudioInfo]. Result is pushed to [songAudioInfo]. diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt index 63e6058bd..55be766c1 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeViewModel.kt @@ -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 = _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) } /** diff --git a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt index a607b9cd6..b827a4ee3 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt @@ -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()) /** 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) } /** diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt similarity index 80% rename from app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt rename to app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index 2e9bbab2d..ff9fb3a45 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -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() - +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() + + @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) + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/music/library/Library.kt b/app/src/main/java/org/oxycblt/auxio/music/library/Library.kt index 0ee5dc736..33dcf2aa2 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/library/Library.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/library/Library.kt @@ -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 */ diff --git a/app/src/main/java/org/oxycblt/auxio/music/picker/PickerViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/picker/PickerViewModel.kt index c92334228..7297bbfc6 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/picker/PickerViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/picker/PickerViewModel.kt @@ -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(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() } diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt b/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt index abcd89d23..783b2d738 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt @@ -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 { /** diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt index b03506337..469190607 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt @@ -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 diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt index 8a4726b8a..3b84c4b7c 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -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(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) { diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt index 51e542d3b..b9fd8fe3d 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt @@ -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 diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt index f69851cdd..05bd544f7 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt @@ -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()