From fbe409b47c1642305667e39e4ffeefcb37be7fb0 Mon Sep 17 00:00:00 2001 From: birdbird <6892457-tzugen@users.noreply.gitlab.com> Date: Tue, 29 Sep 2026 12:34:21 +0000 Subject: [PATCH] di: replace KoinComponent service-locator sprawl with constructor injection Move ~20 classes from KoinComponent/'by inject()' lookup to constructor injection, registered in the Koin modules; the dependency graph is now visible in constructors and these classes are testable without Koin. - util: CacheCleaner, CommunicationError, DownloadUtil, ContextMenuUtil are now plain classes provided as singles. - adapters: Utils object becomes PopupMenuFactory; TrackViewBinder, HeaderViewBinder, ArtistRowBinder, TextRowBinder, ShareRowBinder, AlbumRowDelegate/AlbumGridDelegate receive their dependencies (incl. PopupMenuFactory) via constructor; FolderSelectorBinder drops a dead KoinComponent. - service: MusicServiceFactory and ImageLoaderProvider receive the Koin instance explicitly (they must resolve from the dynamically reloaded music service module); PlaybackStateSerializer, DownloadTask, MediaLibrarySessionCallback, CachedMusicService and OfflineMusicService take their dependencies via constructor. - subsonic: VideoPlayer becomes an injected class; AvatarFetcher and CoverArtFetcher take the SubsonicAPIClient, their Coil factories accept a lazy clientProvider so the client stays current across server switches. - models: EditServerModel, GenericListModel and all subclasses get constructor injection and are wired in the viewModel registrations. - imageloader: ArtworkBitmapLoader takes the ImageLoaderProvider. - view: ChatAdapter takes activeServerProvider/imageLoaderProvider. Service-locator access is confined to Android framework entry points (UApp, DownloadService, PlaybackService, UltrasonicIntentReceiver, AlbumArtContentProvider) and the shared Fragment error-handling extensions; redundant KoinComponent/KoinScopeComponent declarations were removed from fragments. Tests: CommunicationErrorReportingTest, AvatarFetcherTest, CoverArtFetcherTest and OfflineMusicServiceCatalogTest inject mocks directly instead of booting Koin. --- .../ultrasonic/adapters/AlbumRowDelegate.kt | 10 ++-- .../ultrasonic/adapters/ArtistRowBinder.kt | 7 +-- .../moire/ultrasonic/adapters/BaseAdapter.kt | 2 +- .../adapters/FolderSelectorBinder.kt | 4 +- .../ultrasonic/adapters/HeaderViewBinder.kt | 8 +--- .../ultrasonic/adapters/ShareRowBinder.kt | 5 +- .../ultrasonic/adapters/TextRowBinder.kt | 3 +- .../ultrasonic/adapters/TrackViewBinder.kt | 15 ++---- .../org/moire/ultrasonic/adapters/Utils.kt | 17 ++++--- .../kotlin/org/moire/ultrasonic/app/UApp.kt | 18 ++++--- .../di/AppPermanentStorageModule.kt | 19 ++++---- .../moire/ultrasonic/di/ApplicationModule.kt | 22 +++++++-- .../moire/ultrasonic/di/MediaPlayerModule.kt | 4 +- .../moire/ultrasonic/di/MusicServiceModule.kt | 4 +- .../ultrasonic/fragment/AlbumListFragment.kt | 10 +++- .../ultrasonic/fragment/ArtistListFragment.kt | 7 ++- .../moire/ultrasonic/fragment/ChatFragment.kt | 13 +++-- .../ultrasonic/fragment/DownloadsFragment.kt | 21 ++++++++- .../ultrasonic/fragment/EditServerFragment.kt | 5 +- .../ultrasonic/fragment/EntryListFragment.kt | 12 ++--- .../moire/ultrasonic/fragment/MainFragment.kt | 5 +- .../ultrasonic/fragment/PlayerFragment.kt | 16 +++++-- .../ultrasonic/fragment/PlaylistsFragment.kt | 10 +++- .../ultrasonic/fragment/PodcastFragment.kt | 7 ++- .../ultrasonic/fragment/SearchFragment.kt | 32 ++++++++----- .../fragment/SelectGenreFragment.kt | 7 ++- .../ultrasonic/fragment/SettingsFragment.kt | 4 +- .../ultrasonic/fragment/SharesFragment.kt | 18 ++++--- .../fragment/TrackCollectionFragment.kt | 34 +++++++++----- .../imageloader/ArtworkBitmapLoader.kt | 8 +--- .../ultrasonic/imageloader/AvatarFetcher.kt | 21 +++++---- .../ultrasonic/imageloader/CoverArtFetcher.kt | 21 +++++---- .../moire/ultrasonic/model/AlbumListModel.kt | 8 +++- .../moire/ultrasonic/model/ArtistListModel.kt | 9 +++- .../moire/ultrasonic/model/EditServerModel.kt | 9 +--- .../ultrasonic/model/GenericListModel.kt | 16 +++---- .../moire/ultrasonic/model/GenreListModel.kt | 10 +++- .../ultrasonic/model/PlaylistListModel.kt | 13 +++-- .../ultrasonic/model/PodcastListModel.kt | 10 +++- .../moire/ultrasonic/model/SearchListModel.kt | 9 +++- .../moire/ultrasonic/model/ShareListModel.kt | 10 +++- .../ultrasonic/model/TrackCollectionModel.kt | 8 +++- .../ultrasonic/service/CachedMusicService.kt | 10 ++-- .../ultrasonic/service/DownloadService.kt | 36 +++++++++----- .../moire/ultrasonic/service/DownloadTask.kt | 12 ++--- .../service/MediaLibrarySessionCallback.kt | 14 ++---- .../ultrasonic/service/MusicServiceFactory.kt | 22 +++++---- .../ultrasonic/service/OfflineMusicService.kt | 8 +--- .../ultrasonic/service/PlaybackService.kt | 9 +++- .../service/PlaybackStateSerializer.kt | 6 +-- .../subsonic/ImageLoaderProvider.kt | 18 +++---- .../moire/ultrasonic/subsonic/VideoPlayer.kt | 47 ++++++++----------- .../org/moire/ultrasonic/util/CacheCleaner.kt | 19 ++------ .../ultrasonic/util/CommunicationError.kt | 7 +-- .../moire/ultrasonic/util/ContextMenuUtil.kt | 24 +++++----- .../ultrasonic/util/CoroutinePatterns.kt | 11 ++++- .../org/moire/ultrasonic/util/DownloadUtil.kt | 12 ++--- .../org/moire/ultrasonic/view/ChatAdapter.kt | 22 ++++----- .../imageloader/AvatarFetcherTest.kt | 28 ++--------- .../imageloader/CoverArtFetcherTest.kt | 32 ++----------- .../service/OfflineMusicServiceCatalogTest.kt | 11 +---- .../util/CommunicationErrorReportingTest.kt | 18 ++----- 62 files changed, 462 insertions(+), 395 deletions(-) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowDelegate.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowDelegate.kt index 647d5371..bd5f5fbd 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowDelegate.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/AlbumRowDelegate.kt @@ -31,7 +31,8 @@ import org.moire.ultrasonic.util.LayoutType open class AlbumRowDelegate( open val onItemClick: (Album) -> Unit, open val onContextMenuClick: (MenuItem, Album) -> Boolean, - private val imageLoaderProvider: ImageLoaderProvider + private val imageLoaderProvider: ImageLoaderProvider, + private val popupMenuFactory: PopupMenuFactory ) : ItemViewDelegate() { private val starDrawable: Int = R.drawable.rating_star_full @@ -45,7 +46,7 @@ open class AlbumRowDelegate( holder.details.setOnClickListener { onItemClick(item) } holder.coverArt.setOnClickListener { onItemClick(item) } holder.details.setOnLongClickListener { - val popup = Utils.createPopupMenu(holder.itemView) + val popup = popupMenuFactory.createPopupMenu(holder.itemView) popup.setOnMenuItemClickListener { menuItem -> onContextMenuClick(menuItem, item) @@ -135,7 +136,8 @@ open class AlbumRowDelegate( class AlbumGridDelegate( onItemClick: (Album) -> Unit, onContextMenuClick: (MenuItem, Album) -> Boolean, - imageLoaderProvider: ImageLoaderProvider -) : AlbumRowDelegate(onItemClick, onContextMenuClick, imageLoaderProvider) { + imageLoaderProvider: ImageLoaderProvider, + popupMenuFactory: PopupMenuFactory +) : AlbumRowDelegate(onItemClick, onContextMenuClick, imageLoaderProvider, popupMenuFactory) { override var layoutType = LayoutType.COVER } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt index 563d6cb6..f628d9ea 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ArtistRowBinder.kt @@ -34,9 +34,10 @@ class ArtistRowBinder( val onContextMenuClick: (MenuItem, ArtistOrIndex) -> Boolean, private val enableSections: Boolean = true, private val imageLoaderProvider: ImageLoaderProvider, - private val activeServerProvider: ActiveServerProvider + private val activeServerProvider: ActiveServerProvider, + private val popupMenuFactory: PopupMenuFactory ) : ItemViewBinder(), - Utils.SectionedBinder { + SectionedBinder { val layout = R.layout.list_item_artist val contextMenuLayout = R.menu.context_menu_artist @@ -47,7 +48,7 @@ class ArtistRowBinder( holder.section.isVisible = enableSections holder.layout.setOnClickListener { onItemClick(item) } holder.layout.setOnLongClickListener { - val popup = Utils.createPopupMenu(holder.itemView, contextMenuLayout) + val popup = popupMenuFactory.createPopupMenu(holder.itemView, contextMenuLayout) popup.setOnMenuItemClickListener { menuItem -> onContextMenuClick(menuItem, item) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt index 1bde0b09..3b7e1796 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/BaseAdapter.kt @@ -222,7 +222,7 @@ class BaseAdapter(allowDuplicateEntries: Boolean = false) : val type = getItemViewType(position) val binder = types.getType(type).delegate - if (binder is Utils.SectionedBinder) { + if (binder is SectionedBinder) { return binder.getSectionName(items[position] as Identifiable) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/FolderSelectorBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/FolderSelectorBinder.kt index a57bb28e..61d09fd0 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/FolderSelectorBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/FolderSelectorBinder.kt @@ -11,7 +11,6 @@ import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.drakeet.multitype.ItemViewBinder import java.lang.ref.WeakReference -import org.koin.core.component.KoinComponent import org.moire.ultrasonic.R import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.MusicFolder @@ -23,8 +22,7 @@ import org.moire.ultrasonic.service.UltrasonicBus * select one. The intended usage is to supply a filter to lists of artists, albums, etc */ class FolderSelectorBinder(context: Context) : - ItemViewBinder(), - KoinComponent { + ItemViewBinder() { private val weakContext: WeakReference = WeakReference(context) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt index d6a6db91..b47ccd51 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/HeaderViewBinder.kt @@ -11,8 +11,6 @@ import androidx.recyclerview.widget.RecyclerView import com.drakeet.multitype.ItemViewBinder import java.lang.ref.WeakReference import java.util.Random -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject import org.moire.ultrasonic.R import org.moire.ultrasonic.subsonic.ImageLoaderProvider import org.moire.ultrasonic.util.Util @@ -20,13 +18,11 @@ import org.moire.ultrasonic.util.Util /** * This Binder can bind a list of entries into a Header */ -class HeaderViewBinder(context: Context) : - ItemViewBinder(), - KoinComponent { +class HeaderViewBinder(context: Context, private val imageLoaderProvider: ImageLoaderProvider) : + ItemViewBinder() { private val weakContext: WeakReference = WeakReference(context) private val random: Random = Random() - private val imageLoaderProvider: ImageLoaderProvider by inject() // Set our layout files val layout = R.layout.list_header_album diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ShareRowBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ShareRowBinder.kt index f6b06211..f5b45370 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ShareRowBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/ShareRowBinder.kt @@ -22,7 +22,8 @@ import org.moire.ultrasonic.domain.Share */ class ShareRowBinder( private val onItemClick: (Share) -> Unit, - private val onContextMenuItemClick: (MenuItem, Share) -> Boolean + private val onContextMenuItemClick: (MenuItem, Share) -> Boolean, + private val popupMenuFactory: PopupMenuFactory ) : ItemViewBinder() { override fun onBindViewHolder(holder: ViewHolder, item: Share) { @@ -30,7 +31,7 @@ class ShareRowBinder( holder.description.text = item.description holder.itemView.setOnClickListener { onItemClick(item) } holder.itemView.setOnLongClickListener { view -> - val popup = Utils.createPopupMenu(view, R.menu.select_share_context) + val popup = popupMenuFactory.createPopupMenu(view, R.menu.select_share_context) popup.setOnMenuItemClickListener { menuItem -> onContextMenuItemClick(menuItem, item) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TextRowBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TextRowBinder.kt index 7590e62c..6bec7539 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TextRowBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TextRowBinder.kt @@ -25,6 +25,7 @@ class TextRowBinder( private val textResolver: (T) -> String?, private val onItemClick: (T) -> Unit, private val onContextMenuItemClick: (MenuItem, T) -> Boolean, + private val popupMenuFactory: PopupMenuFactory, private val contextMenuLayout: (T) -> Int? = { null } ) : ItemViewBinder() { @@ -34,7 +35,7 @@ class TextRowBinder( holder.itemView.setOnLongClickListener { view -> val menuLayout = contextMenuLayout(item) ?: return@setOnLongClickListener false - val popup = Utils.createPopupMenu(view, menuLayout) + val popup = popupMenuFactory.createPopupMenu(view, menuLayout) popup.setOnMenuItemClickListener { menuItem -> onContextMenuItemClick(menuItem, item) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt index 2cc6b810..94333be1 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewBinder.kt @@ -9,8 +9,6 @@ import android.view.ViewGroup import android.widget.PopupMenu import androidx.lifecycle.LifecycleOwner import com.drakeet.multitype.ItemViewBinder -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject import org.moire.ultrasonic.R import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.domain.Identifiable @@ -24,23 +22,20 @@ class TrackViewBinder( val checkable: Boolean, val draggable: Boolean, val lifecycleOwner: LifecycleOwner, + val popupMenuFactory: PopupMenuFactory, + val playback: PlaybackRepository, + val activeServerProvider: ActiveServerProvider, val showPlayingInPlayOrder: Boolean = false, val createContextMenu: (View, Track) -> PopupMenu = { view, _ -> - Utils.createPopupMenu( + popupMenuFactory.createPopupMenu( view, R.menu.context_menu_track ) } -) : ItemViewBinder(), - KoinComponent { +) : ItemViewBinder() { var startDrag: ((TrackViewHolder) -> Unit)? = null - // Resolved once per binder (which lives as long as the fragment) instead of - // once per recycled ViewHolder. - private val playback: PlaybackRepository by inject() - private val activeServerProvider: ActiveServerProvider by inject() - // Set our layout files val layout = R.layout.list_item_track diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/Utils.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/Utils.kt index 027444dd..478c140f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/Utils.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/Utils.kt @@ -3,17 +3,16 @@ package org.moire.ultrasonic.adapters import android.view.MenuInflater import android.view.View import android.widget.PopupMenu -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject import org.moire.ultrasonic.R import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.domain.Identifiable -object Utils : KoinComponent { +/** + * Creates the context menus shown on long-press in the list views. Hides the + * download and share actions while the app is in offline mode. + */ +class PopupMenuFactory(private val activeServerProvider: ActiveServerProvider) { - private val activeServerProvider: ActiveServerProvider by inject() - - @JvmStatic fun createPopupMenu(view: View, layout: Int = R.menu.context_menu_artist): PopupMenu { val popup = PopupMenu(view.context, view) val inflater: MenuInflater = popup.menuInflater @@ -31,8 +30,8 @@ object Utils : KoinComponent { popup.show() return popup } +} - interface SectionedBinder { - fun getSectionName(item: Identifiable): String - } +interface SectionedBinder { + fun getSectionName(item: Identifiable): String } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt index 7d87b431..6022056a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt @@ -16,6 +16,8 @@ import coil3.request.crossfade import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.koin.android.ext.koin.androidContext +import org.koin.core.component.KoinComponent +import org.koin.core.component.get import org.koin.core.context.startKoin import org.koin.core.context.stopKoin import org.moire.ultrasonic.BuildConfig @@ -44,7 +46,8 @@ import timber.log.Timber.DebugTree class UApp : Application(), - SingletonImageLoader.Factory { + SingletonImageLoader.Factory, + KoinComponent { private var ioScope = CoroutinePatterns.loggingScope(Dispatchers.IO) @@ -90,9 +93,11 @@ class UApp : override fun newImageLoader(context: Context): ImageLoader = ImageLoader.Builder(context) .components { - add(CoverArtFetcher.Factory()) + // The client is resolved lazily when a fetcher is created: the SubsonicAPIClient + // is regenerated whenever the active server changes. + add(CoverArtFetcher.Factory { get() }) add(CoverArtKeyer()) - add(AvatarFetcher.Factory()) + add(AvatarFetcher.Factory { get() }) add(AvatarKeyer()) } .memoryCache( @@ -147,10 +152,9 @@ class UApp : companion object { var instance: UApp? = null - fun applicationContext(): Context = - requireNotNull(instance) { - "UApp.instance accessed before Application.onCreate" - }.applicationContext + fun applicationContext(): Context = requireNotNull(instance) { + "UApp.instance accessed before Application.onCreate" + }.applicationContext } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/AppPermanentStorageModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/AppPermanentStorageModule.kt index 155442e7..e7bc222c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/AppPermanentStorageModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/AppPermanentStorageModule.kt @@ -28,6 +28,7 @@ import org.moire.ultrasonic.model.SearchListModel import org.moire.ultrasonic.model.ServerSettingsModel import org.moire.ultrasonic.model.ShareListModel import org.moire.ultrasonic.model.TrackCollectionModel +import org.moire.ultrasonic.util.CommunicationError import org.moire.ultrasonic.util.Settings const val SP_NAME = "Default_SP" @@ -64,13 +65,13 @@ val appPermanentStorage = module { viewModel { EditServerModel(get()) } viewModel { ChatViewModel() } - viewModel { AlbumListModel(get()) } - viewModel { ArtistListModel(get()) } - viewModel { GenreListModel(get()) } - viewModel { PlaylistListModel(get()) } - viewModel { PodcastListModel(get()) } - viewModel { SearchListModel(get()) } - viewModel { ShareListModel(get()) } - viewModel { TrackCollectionModel(get()) } - viewModel { DownloadListModel(get()) } + viewModel { AlbumListModel(get(), get(), get(), get()) } + viewModel { ArtistListModel(get(), get(), get(), get()) } + viewModel { GenreListModel(get(), get(), get(), get()) } + viewModel { PlaylistListModel(get(), get(), get(), get(), get()) } + viewModel { PodcastListModel(get(), get(), get(), get()) } + viewModel { SearchListModel(get(), get(), get(), get()) } + viewModel { ShareListModel(get(), get(), get(), get()) } + viewModel { TrackCollectionModel(get(), get(), get(), get()) } + viewModel { DownloadListModel(get(), get(), get(), get()) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/ApplicationModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/ApplicationModule.kt index d62d62f4..2ac372f8 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/ApplicationModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/ApplicationModule.kt @@ -1,10 +1,16 @@ package org.moire.ultrasonic.di +import org.koin.core.Koin import org.koin.dsl.module +import org.moire.ultrasonic.adapters.PopupMenuFactory import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.subsonic.ImageLoaderProvider +import org.moire.ultrasonic.subsonic.VideoPlayer import org.moire.ultrasonic.util.CacheCleaner +import org.moire.ultrasonic.util.CommunicationError +import org.moire.ultrasonic.util.ContextMenuUtil +import org.moire.ultrasonic.util.DownloadUtil /** * This Koin module contains the registration of general classes needed for Ultrasonic @@ -14,8 +20,18 @@ val applicationModule = module { // Must NOT live in musicServiceModule: that module is unloaded/reloaded by the // factory itself whenever the library access state changes. - single { MusicServiceFactory(get()) } + single { MusicServiceFactory(get(), getKoin()) } - single { ImageLoaderProvider() } - single { CacheCleaner() } + // The ImageLoaderProvider resolves the SubsonicAPIClient and the ServerID from the + // music service module at request time, because that module is regenerated whenever + // the active server changes. It therefore receives the Koin instance itself instead + // of a fixed snapshot of its definitions. + single { ImageLoaderProvider(getKoin()) } + + single { PopupMenuFactory(get()) } + single { CacheCleaner(get(), get()) } + single { CommunicationError(get()) } + single { DownloadUtil(get(), get(), get()) } + single { ContextMenuUtil(get(), get(), get()) } + single { VideoPlayer(get()) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MediaPlayerModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MediaPlayerModule.kt index 21830864..2d4fdcc8 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MediaPlayerModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MediaPlayerModule.kt @@ -1,5 +1,7 @@ package org.moire.ultrasonic.di +import android.content.Context +import org.koin.android.ext.koin.androidContext import org.koin.core.module.dsl.viewModel import org.koin.dsl.module import org.moire.ultrasonic.model.NowPlayingViewModel @@ -21,7 +23,7 @@ import org.moire.ultrasonic.subsonic.ShareHandler val mediaPlayerModule = module { // These are dependency-free - single { PlaybackStateSerializer() } + single { PlaybackStateSerializer(androidContext()) } single { ExternalStorageMonitor() } single { NetworkAndStorageChecker(get()) } single { ShareHandler(get()) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt index be0afc0c..cfeed242 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt @@ -59,10 +59,10 @@ val musicServiceModule = module { single { SubsonicAPIClient(get(), get()) } single(named(ONLINE_MUSIC_SERVICE)) { - CachedMusicService(RESTMusicService(get(), get())) + CachedMusicService(RESTMusicService(get(), get()), get()) } single(named(OFFLINE_MUSIC_SERVICE)) { - OfflineMusicService() + OfflineMusicService(get()) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt index 48248a6a..a59ab04c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/AlbumListFragment.kt @@ -19,11 +19,13 @@ import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import kotlinx.coroutines.launch +import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.R import org.moire.ultrasonic.adapters.AlbumGridDelegate import org.moire.ultrasonic.adapters.AlbumRowDelegate +import org.moire.ultrasonic.adapters.PopupMenuFactory import org.moire.ultrasonic.api.subsonic.models.AlbumListType import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.domain.Album @@ -44,6 +46,8 @@ class AlbumListFragment( ) : EntryListFragment(), FilterableFragment { + private val popupMenuFactory: PopupMenuFactory by inject() + private var filterButtonBar: FilterButtonBar? = null /** @@ -283,12 +287,14 @@ class AlbumListFragment( AlbumRowDelegate( ::onItemClick, ::onContextMenuItemSelected, - imageLoaderProvider = imageLoaderProvider + imageLoaderProvider = imageLoaderProvider, + popupMenuFactory = popupMenuFactory ), AlbumGridDelegate( ::onItemClick, ::onContextMenuItemSelected, - imageLoaderProvider + imageLoaderProvider, + popupMenuFactory ) ).withKotlinClassLinker { _, _ -> when (layoutType) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt index 320f9a2b..4552c70e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt @@ -12,10 +12,12 @@ import android.view.View import androidx.lifecycle.LiveData import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.R import org.moire.ultrasonic.adapters.ArtistRowBinder +import org.moire.ultrasonic.adapters.PopupMenuFactory import org.moire.ultrasonic.api.subsonic.models.AlbumListType import org.moire.ultrasonic.domain.ArtistOrIndex import org.moire.ultrasonic.domain.Index @@ -26,6 +28,8 @@ import org.moire.ultrasonic.model.ArtistListModel */ class ArtistListFragment : EntryListFragment() { + private val popupMenuFactory: PopupMenuFactory by inject() + /** * The ViewModel to use to get the data */ @@ -53,7 +57,8 @@ class ArtistListFragment : EntryListFragment() { { entry -> onItemClick(entry) }, { menuItem, entry -> onContextMenuItemSelected(menuItem, entry) }, imageLoaderProvider = imageLoaderProvider, - activeServerProvider = activeServerProvider + activeServerProvider = activeServerProvider, + popupMenuFactory = popupMenuFactory ) ) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ChatFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ChatFragment.kt index 950e5381..adc2dac0 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ChatFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ChatFragment.kt @@ -35,14 +35,14 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject import org.moire.ultrasonic.R import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.fragment.FragmentTitle.setTitle import org.moire.ultrasonic.model.ChatViewModel import org.moire.ultrasonic.service.MusicServiceFactory +import org.moire.ultrasonic.subsonic.ImageLoaderProvider import org.moire.ultrasonic.util.RefreshableFragment import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util.applyTheme @@ -52,7 +52,6 @@ import org.moire.ultrasonic.view.ChatAdapter class ChatFragment : Fragment(), - KoinComponent, RefreshableFragment { private lateinit var chatListView: ListView private lateinit var messageEditText: EditText @@ -60,6 +59,7 @@ class ChatFragment : override var swipeRefresh: SwipeRefreshLayout? = null private val activeServerProvider: ActiveServerProvider by inject() private val musicServiceFactory: MusicServiceFactory by inject() + private val imageLoaderProvider: ImageLoaderProvider by inject() private val chatViewModel: ChatViewModel by viewModel() @@ -134,7 +134,12 @@ class ChatFragment : private fun observeChatMessages() { chatViewModel.chatMessages.observe(viewLifecycleOwner) { messages -> if (!messages.isNullOrEmpty()) { - chatListView.adapter = ChatAdapter(requireContext(), messages) + chatListView.adapter = ChatAdapter( + requireContext(), + messages, + activeServerProvider, + imageLoaderProvider + ) } } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt index ad6ebb1a..efd5fdef 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/DownloadsFragment.kt @@ -13,13 +13,19 @@ import android.view.MenuItem import android.view.View import androidx.core.view.isVisible import androidx.lifecycle.LiveData +import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.moire.ultrasonic.R +import org.moire.ultrasonic.adapters.PopupMenuFactory import org.moire.ultrasonic.adapters.TrackViewBinder import org.moire.ultrasonic.app.UApp +import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.model.GenericListModel import org.moire.ultrasonic.service.DownloadService +import org.moire.ultrasonic.service.MusicServiceFactory +import org.moire.ultrasonic.service.PlaybackRepository +import org.moire.ultrasonic.util.CommunicationError import org.moire.ultrasonic.util.Util /** @@ -33,6 +39,9 @@ import org.moire.ultrasonic.util.Util */ class DownloadsFragment : MultiListFragment() { + private val playback: PlaybackRepository by inject() + private val popupMenuFactory: PopupMenuFactory by inject() + /** * The ViewModel to use to get the data */ @@ -57,7 +66,10 @@ class DownloadsFragment : MultiListFragment() { { _, _ -> true }, checkable = false, draggable = false, - lifecycleOwner = viewLifecycleOwner + lifecycleOwner = viewLifecycleOwner, + popupMenuFactory = popupMenuFactory, + playback = playback, + activeServerProvider = activeServerProvider ) ) @@ -79,6 +91,11 @@ class DownloadsFragment : MultiListFragment() { } } -class DownloadListModel(application: Application) : GenericListModel(application) { +class DownloadListModel( + application: Application, + activeServerProvider: ActiveServerProvider, + musicServiceFactory: MusicServiceFactory, + communicationError: CommunicationError +) : GenericListModel(application, activeServerProvider, musicServiceFactory, communicationError) { fun getList(): LiveData> = DownloadService.observableDownloads } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt index 488b7999..053d8d33 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt @@ -42,7 +42,7 @@ import org.moire.ultrasonic.model.EditServerModel import org.moire.ultrasonic.model.ServerSettingsModel import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.service.UltrasonicBus -import org.moire.ultrasonic.util.CommunicationError.getErrorMessage +import org.moire.ultrasonic.util.CommunicationError import org.moire.ultrasonic.util.ErrorDialog import org.moire.ultrasonic.util.InfoDialog import org.moire.ultrasonic.util.ServerColor @@ -59,6 +59,7 @@ class EditServerFragment : Fragment() { private val serverSettingsModel: ServerSettingsModel by viewModel() private val activeServerProvider: ActiveServerProvider by inject() private val musicServiceFactory: MusicServiceFactory by inject() + private val communicationError: CommunicationError by inject() private var currentServerSetting: ServerSetting? = null @@ -432,7 +433,7 @@ class EditServerFragment : Fragment() { Timber.w(exception) ErrorDialog.Builder(requireContext()) .setTitle(R.string.error_label) - .setMessage(getErrorMessage(exception)) + .setMessage(communicationError.getErrorMessage(exception)) .show() } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt index 2f1f43dd..ab8e4f80 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EntryListFragment.kt @@ -14,23 +14,23 @@ import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import kotlinx.coroutines.launch -import org.koin.core.component.KoinScopeComponent +import org.koin.android.ext.android.inject import org.moire.ultrasonic.adapters.FolderSelectorBinder import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.GenericEntry import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.service.UltrasonicBus -import org.moire.ultrasonic.util.ContextMenuUtil.handleContextMenu +import org.moire.ultrasonic.util.ContextMenuUtil /** * An extension of the MultiListFragment, with a few helper functions geared * towards the display of MusicDirectory.Entries. * @param T: The type of data which will be used (must extend GenericEntry) */ -abstract class EntryListFragment : - MultiListFragment(), - KoinScopeComponent { +abstract class EntryListFragment : MultiListFragment() { + internal val contextMenuUtil: ContextMenuUtil by inject() + /** * Whether to show the folder selector */ @@ -41,7 +41,7 @@ abstract class EntryListFragment : override fun onContextMenuItemSelected(menuItem: MenuItem, item: T): Boolean { val isArtist = (item is Artist) - return handleContextMenu(menuItem, item, isArtist, this) + return contextMenuUtil.handleContextMenu(menuItem, item, isArtist, this) } override fun onItemClick(item: T) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MainFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MainFragment.kt index 6359e1c0..6acfe38f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MainFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/MainFragment.kt @@ -26,7 +26,6 @@ import kotlin.collections.hashMapOf import kotlin.collections.set import kotlinx.coroutines.launch import org.koin.androidx.scope.ScopeFragment -import org.koin.core.component.KoinScopeComponent import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.R import org.moire.ultrasonic.api.subsonic.models.AlbumListType @@ -39,9 +38,7 @@ import org.moire.ultrasonic.view.SortOrder import org.moire.ultrasonic.view.ViewCapabilities import timber.log.Timber -class MainFragment : - ScopeFragment(), - KoinScopeComponent { +class MainFragment : ScopeFragment() { private var filterButtonBar: FilterButtonBar? = null private var layoutType: LayoutType = LayoutType.COVER diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index f6febd94..b50f4d23 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -75,9 +75,9 @@ import kotlinx.coroutines.withContext import org.koin.android.ext.android.inject import org.koin.androidx.scope.ScopeFragment import org.koin.androidx.viewmodel.ext.android.viewModel -import org.koin.core.component.KoinScopeComponent import org.moire.ultrasonic.R import org.moire.ultrasonic.adapters.BaseAdapter +import org.moire.ultrasonic.adapters.PopupMenuFactory import org.moire.ultrasonic.adapters.TrackViewBinder import org.moire.ultrasonic.api.subsonic.models.AlbumListType import org.moire.ultrasonic.app.UApp @@ -94,13 +94,14 @@ import org.moire.ultrasonic.model.PlayerViewModel import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.service.PlaybackBackend import org.moire.ultrasonic.service.PlaybackProgress +import org.moire.ultrasonic.service.PlaybackRepository import org.moire.ultrasonic.service.UltrasonicBus import org.moire.ultrasonic.subsonic.ImageLoaderProvider import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker import org.moire.ultrasonic.subsonic.ShareHandler import org.moire.ultrasonic.util.CommunicationError -import org.moire.ultrasonic.util.CoroutinePatterns import org.moire.ultrasonic.util.ConfirmationDialog +import org.moire.ultrasonic.util.CoroutinePatterns import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util.themeColor @@ -117,7 +118,6 @@ import timber.log.Timber class PlayerFragment : ScopeFragment(), GestureDetector.OnGestureListener, - KoinScopeComponent, CoroutineScope by CoroutinePatterns.loggingScope(Dispatchers.Main) { // Settings @@ -137,6 +137,9 @@ class PlayerFragment : private val playerViewModel: PlayerViewModel by viewModel() private val shareHandler: ShareHandler by inject() private val imageLoaderProvider: ImageLoaderProvider by inject() + private val playback: PlaybackRepository by inject() + private val popupMenuFactory: PopupMenuFactory by inject() + private val communicationError: CommunicationError by inject() private var currentSong: Track? = null private lateinit var viewManager: LinearLayoutManager private var progressJob: Job? = null @@ -440,7 +443,7 @@ class PlayerFragment : } // Query the Jukebox state in an IO Context - ioScope.launch(CommunicationError.getHandler(context)) { + ioScope.launch(communicationError.getHandler(context)) { try { jukeboxAvailable = musicServiceFactory.getMusicService().isJukeboxAvailable() } catch (all: Exception) { @@ -855,7 +858,7 @@ class PlayerFragment : toast(R.string.download_playlist_done) } else { Timber.e(it, "Exception has occurred in savePlaylistInBackground") - CommunicationError.handleError( + communicationError.handleError( it, context, showToast = true, @@ -905,6 +908,9 @@ class PlayerFragment : checkable = false, draggable = true, lifecycleOwner = viewLifecycleOwner, + popupMenuFactory = popupMenuFactory, + playback = playback, + activeServerProvider = activeServerProvider, showPlayingInPlayOrder = true ) { view, track -> onCreateContextMenu(view, track) }.apply { this.startDrag = { holder -> diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlaylistsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlaylistsFragment.kt index 307b0889..6f183ae2 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlaylistsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlaylistsFragment.kt @@ -26,9 +26,11 @@ import androidx.navigation.fragment.findNavController import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.R +import org.moire.ultrasonic.adapters.PopupMenuFactory import org.moire.ultrasonic.adapters.TextRowBinder import org.moire.ultrasonic.domain.Playlist import org.moire.ultrasonic.fragment.FragmentTitle.setTitle @@ -45,6 +47,9 @@ import org.moire.ultrasonic.util.toastingExceptionHandler */ class PlaylistsFragment : MultiListFragment() { + private val popupMenuFactory: PopupMenuFactory by inject() + private val downloadUtil: DownloadUtil by inject() + /** * The ViewModel to use to get the data */ @@ -81,7 +86,8 @@ class PlaylistsFragment : MultiListFragment() { } else { R.menu.select_playlist_context } - } + }, + popupMenuFactory = popupMenuFactory ) ) } @@ -138,7 +144,7 @@ class PlaylistsFragment : MultiListFragment() { R.id.playlist_menu_unpin -> DownloadAction.UNPIN else -> DownloadAction.DOWNLOAD } - DownloadUtil.justDownload( + downloadUtil.justDownload( action = action, fragment = this, id = playlist.id, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PodcastFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PodcastFragment.kt index 4e01b8d9..2b1eb026 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PodcastFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PodcastFragment.kt @@ -12,9 +12,11 @@ import android.view.MenuItem import android.view.View import androidx.lifecycle.LiveData import androidx.navigation.fragment.findNavController +import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.R +import org.moire.ultrasonic.adapters.PopupMenuFactory import org.moire.ultrasonic.adapters.TextRowBinder import org.moire.ultrasonic.domain.PodcastsChannel import org.moire.ultrasonic.fragment.FragmentTitle.setTitle @@ -25,6 +27,8 @@ import org.moire.ultrasonic.model.PodcastListModel */ class PodcastFragment : MultiListFragment() { + private val popupMenuFactory: PopupMenuFactory by inject() + /** * The ViewModel to use to get the data */ @@ -52,7 +56,8 @@ class PodcastFragment : MultiListFragment() { TextRowBinder( textResolver = { it.title }, onItemClick = { onItemClick(it) }, - onContextMenuItemClick = { _, _ -> false } + onContextMenuItemClick = { _, _ -> false }, + popupMenuFactory = popupMenuFactory ) ) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt index 97023319..758dc880 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SearchFragment.kt @@ -16,15 +16,15 @@ import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import kotlinx.coroutines.launch +import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel -import org.koin.core.component.KoinScopeComponent -import org.koin.core.component.inject import org.moire.ultrasonic.R import org.moire.ultrasonic.adapters.AlbumRowDelegate import org.moire.ultrasonic.adapters.ArtistRowBinder import org.moire.ultrasonic.adapters.DividerBinder import org.moire.ultrasonic.adapters.MoreButtonBinder import org.moire.ultrasonic.adapters.MoreButtonBinder.MoreButton +import org.moire.ultrasonic.adapters.PopupMenuFactory import org.moire.ultrasonic.adapters.TrackViewBinder import org.moire.ultrasonic.api.subsonic.models.AlbumListType import org.moire.ultrasonic.domain.Album @@ -37,9 +37,9 @@ import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.fragment.FragmentTitle.setTitle import org.moire.ultrasonic.model.SearchListModel import org.moire.ultrasonic.service.PlaybackQueueActions -import org.moire.ultrasonic.subsonic.VideoPlayer.Companion.playVideo -import org.moire.ultrasonic.util.ContextMenuUtil.handleContextMenu -import org.moire.ultrasonic.util.ContextMenuUtil.handleContextMenuTracks +import org.moire.ultrasonic.service.PlaybackRepository +import org.moire.ultrasonic.subsonic.VideoPlayer +import org.moire.ultrasonic.util.ContextMenuUtil import org.moire.ultrasonic.util.RefreshableFragment import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util.toast @@ -51,11 +51,14 @@ import org.moire.ultrasonic.util.toastingExceptionHandler */ class SearchFragment : MultiListFragment(), - KoinScopeComponent, RefreshableFragment { private var searchResult: SearchResult? = null override var swipeRefresh: SwipeRefreshLayout? = null private val playbackQueue: PlaybackQueueActions by inject() + private val playback: PlaybackRepository by inject() + private val popupMenuFactory: PopupMenuFactory by inject() + private val contextMenuUtil: ContextMenuUtil by inject() + private val videoPlayer: VideoPlayer by inject() private val navArgs by navArgs() override val listModel: SearchListModel by viewModel() override val mainLayout: Int = R.layout.search @@ -88,7 +91,8 @@ class SearchFragment : onContextMenuClick = ::onContextMenuItemSelected, enableSections = false, imageLoaderProvider = imageLoaderProvider, - activeServerProvider = activeServerProvider + activeServerProvider = activeServerProvider, + popupMenuFactory = popupMenuFactory ) ) @@ -96,7 +100,8 @@ class SearchFragment : AlbumRowDelegate( onItemClick = ::onItemClick, onContextMenuClick = ::onContextMenuItemSelected, - imageLoaderProvider = imageLoaderProvider + imageLoaderProvider = imageLoaderProvider, + popupMenuFactory = popupMenuFactory ) ) @@ -106,7 +111,10 @@ class SearchFragment : onContextMenuClick = ::onContextMenuItemSelected, checkable = false, draggable = false, - lifecycleOwner = viewLifecycleOwner + lifecycleOwner = viewLifecycleOwner, + popupMenuFactory = popupMenuFactory, + playback = playback, + activeServerProvider = activeServerProvider ) ) @@ -231,7 +239,7 @@ class SearchFragment : } private fun onVideoSelected(track: Track) { - playVideo(requireContext(), track) + videoPlayer.playVideo(requireContext(), track) } private fun autoplay() { @@ -266,13 +274,13 @@ class SearchFragment : override fun onContextMenuItemSelected(menuItem: MenuItem, item: Identifiable): Boolean { // Here the Item could be a track or an album or an artist if (item is Track) { - return handleContextMenuTracks( + return contextMenuUtil.handleContextMenuTracks( menuItem = menuItem, tracks = listOf(item), fragment = this ) } else { - return handleContextMenu( + return contextMenuUtil.handleContextMenu( menuItem = menuItem, item = item, isArtist = item is Artist, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SelectGenreFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SelectGenreFragment.kt index ba88e831..ce57b5e2 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SelectGenreFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SelectGenreFragment.kt @@ -12,9 +12,11 @@ import android.view.MenuItem import android.view.View import androidx.lifecycle.LiveData import androidx.navigation.fragment.findNavController +import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.R +import org.moire.ultrasonic.adapters.PopupMenuFactory import org.moire.ultrasonic.adapters.TextRowBinder import org.moire.ultrasonic.domain.Genre import org.moire.ultrasonic.fragment.FragmentTitle.setTitle @@ -26,6 +28,8 @@ import org.moire.ultrasonic.util.Settings.maxSongs */ class SelectGenreFragment : MultiListFragment() { + private val popupMenuFactory: PopupMenuFactory by inject() + /** * The ViewModel to use to get the data */ @@ -53,7 +57,8 @@ class SelectGenreFragment : MultiListFragment() { TextRowBinder( textResolver = { it.name }, onItemClick = { onItemClick(it) }, - onContextMenuItemClick = { _, _ -> false } + onContextMenuItemClick = { _, _ -> false }, + popupMenuFactory = popupMenuFactory ) ) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt index 58c2ef56..870b158b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt @@ -23,7 +23,6 @@ import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import java.io.File import kotlin.math.ceil -import org.koin.core.component.KoinComponent import org.moire.ultrasonic.R import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.fragment.FragmentTitle.setTitle @@ -59,8 +58,7 @@ import timber.log.Timber @Suppress("TooManyFunctions") class SettingsFragment : PreferenceFragmentCompat(), - OnSharedPreferenceChangeListener, - KoinComponent { + OnSharedPreferenceChangeListener { private var theme: ListPreference? = null private var cacheLocation: Preference? = null private var showArtistPicture: CheckBoxPreference? = null diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SharesFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SharesFragment.kt index 130daa7a..d6775f14 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SharesFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SharesFragment.kt @@ -29,10 +29,11 @@ import java.util.concurrent.TimeUnit import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel -import org.koin.core.component.KoinScopeComponent import org.moire.ultrasonic.NavigationGraphDirections import org.moire.ultrasonic.R +import org.moire.ultrasonic.adapters.PopupMenuFactory import org.moire.ultrasonic.adapters.ShareRowBinder import org.moire.ultrasonic.domain.Share import org.moire.ultrasonic.fragment.FragmentTitle.setTitle @@ -50,15 +51,17 @@ import org.moire.ultrasonic.util.toastingExceptionHandler /** * Displays the shares in the media library */ -class SharesFragment : - MultiListFragment(), - KoinScopeComponent { +class SharesFragment : MultiListFragment() { /** * The ViewModel to use to get the data */ override val listModel: ShareListModel by viewModel() + private val popupMenuFactory: PopupMenuFactory by inject() + private val contextMenuUtil: ContextMenuUtil by inject() + private val downloadUtil: DownloadUtil by inject() + /** * The list is cached locally, no need to refresh it on creation */ @@ -81,7 +84,8 @@ class SharesFragment : onItemClick = { onItemClick(it) }, onContextMenuItemClick = { menuItem, item -> onContextMenuItemSelected(menuItem, item) - } + }, + popupMenuFactory = popupMenuFactory ) ) } @@ -104,7 +108,7 @@ class SharesFragment : R.id.share_menu_play_now, R.id.share_menu_play_shuffled -> { - ContextMenuUtil.playTracksAndToast( + contextMenuUtil.playTracksAndToast( this, insertionMode = QueueInsertionMode.CLEAR, id = item.id, @@ -137,7 +141,7 @@ class SharesFragment : R.id.share_menu_unpin -> DownloadAction.UNPIN else -> DownloadAction.DOWNLOAD } - DownloadUtil.justDownload( + downloadUtil.justDownload( action = action, fragment = this, id = share.id, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index 6227f885..90f70bb0 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -34,8 +34,8 @@ import org.moire.ultrasonic.R import org.moire.ultrasonic.adapters.AlbumHeader import org.moire.ultrasonic.adapters.AlbumRowDelegate import org.moire.ultrasonic.adapters.HeaderViewBinder +import org.moire.ultrasonic.adapters.PopupMenuFactory import org.moire.ultrasonic.adapters.TrackViewBinder -import org.moire.ultrasonic.adapters.Utils import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.MusicDirectory @@ -43,6 +43,7 @@ import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.fragment.FragmentTitle.setTitle import org.moire.ultrasonic.model.TrackCollectionModel import org.moire.ultrasonic.service.PlaybackQueueActions +import org.moire.ultrasonic.service.PlaybackRepository import org.moire.ultrasonic.service.QueueInsertionMode import org.moire.ultrasonic.service.UltrasonicBus import org.moire.ultrasonic.subsonic.ShareHandler @@ -88,6 +89,11 @@ open class TrackCollectionFragment(initialOrder: SortOrder? = null) : internal val playbackQueue: PlaybackQueueActions by inject() private val shareHandler: ShareHandler by inject() + private val playback: PlaybackRepository by inject() + private val popupMenuFactory: PopupMenuFactory by inject() + internal val contextMenuUtil: ContextMenuUtil by inject() + private val downloadUtil: DownloadUtil by inject() + private val videoPlayer: VideoPlayer by inject() override val listModel: TrackCollectionModel by viewModel() @@ -106,6 +112,7 @@ open class TrackCollectionFragment(initialOrder: SortOrder? = null) : sortOrderName?.let { sortOrder = SortOrder.valueOf(it) } } + @Suppress("LongMethod") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -140,7 +147,8 @@ open class TrackCollectionFragment(initialOrder: SortOrder? = null) : viewAdapter.register( HeaderViewBinder( - context = requireContext() + context = requireContext(), + imageLoaderProvider = imageLoaderProvider ) ) @@ -151,8 +159,11 @@ open class TrackCollectionFragment(initialOrder: SortOrder? = null) : checkable = true, draggable = false, lifecycleOwner = viewLifecycleOwner, + popupMenuFactory = popupMenuFactory, + playback = playback, + activeServerProvider = activeServerProvider, createContextMenu = { view, _ -> - Utils.createPopupMenu(view, R.menu.context_menu_track_collection) + popupMenuFactory.createPopupMenu(view, R.menu.context_menu_track_collection) } ) ) @@ -161,7 +172,8 @@ open class TrackCollectionFragment(initialOrder: SortOrder? = null) : AlbumRowDelegate( { entry -> onItemClick(entry) }, { menuItem, entry -> onContextMenuItemSelected(menuItem, entry) }, - imageLoaderProvider = imageLoaderProvider + imageLoaderProvider = imageLoaderProvider, + popupMenuFactory = popupMenuFactory ) ) @@ -347,7 +359,7 @@ open class TrackCollectionFragment(initialOrder: SortOrder? = null) : // Need a valid id to recurse sub directories stuff if (hasSubFolders && navArgs.id != null) { - ContextMenuUtil.playTracksAndToast( + contextMenuUtil.playTracksAndToast( fragment = this, insertionMode = insertionMode, id = navArgs.id!!, @@ -367,7 +379,7 @@ open class TrackCollectionFragment(initialOrder: SortOrder? = null) : } } private fun unpinSelectedTracks() { - DownloadUtil.justDownload( + downloadUtil.justDownload( action = DownloadAction.UNPIN, fragment = this, tracks = getSelectedTracks() @@ -375,7 +387,7 @@ open class TrackCollectionFragment(initialOrder: SortOrder? = null) : } private fun downloadSelectedOrAllTracks(save: Boolean) { - DownloadUtil.justDownload( + downloadUtil.justDownload( action = if (save) DownloadAction.PIN else DownloadAction.DOWNLOAD, fragment = this, tracks = getSelectedOrAllTracks() @@ -383,7 +395,7 @@ open class TrackCollectionFragment(initialOrder: SortOrder? = null) : } private fun playSelectedOrAllTracks(insertionMode: QueueInsertionMode) { - ContextMenuUtil.playTracksAndToast( + contextMenuUtil.playTracksAndToast( fragment = this, insertionMode = insertionMode, tracks = getSelectedOrAllTracks() @@ -391,7 +403,7 @@ open class TrackCollectionFragment(initialOrder: SortOrder? = null) : } private fun deleteSelectedTracks() { - DownloadUtil.justDownload( + downloadUtil.justDownload( action = DownloadAction.DELETE, fragment = this, tracks = getSelectedTracks() @@ -612,7 +624,7 @@ open class TrackCollectionFragment(initialOrder: SortOrder? = null) : val tracks = getClickedSong(item) - return ContextMenuUtil.handleContextMenuTracks( + return contextMenuUtil.handleContextMenuTracks( menuItem = menuItem, tracks = tracks, fragment = this @@ -660,7 +672,7 @@ open class TrackCollectionFragment(initialOrder: SortOrder? = null) : } item is Track && item.isVideo -> { - VideoPlayer.playVideo(requireContext(), item) + videoPlayer.playVideo(requireContext(), item) } else -> { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ArtworkBitmapLoader.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ArtworkBitmapLoader.kt index e5f753f0..566e555b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ArtworkBitmapLoader.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/ArtworkBitmapLoader.kt @@ -17,16 +17,10 @@ import com.google.common.util.concurrent.ListeningExecutorService import com.google.common.util.concurrent.MoreExecutors import java.io.IOException import java.util.concurrent.Executors -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject import org.moire.ultrasonic.subsonic.ImageLoaderProvider @SuppressLint("UnsafeOptInUsageError") -class ArtworkBitmapLoader : - BitmapLoader, - KoinComponent { - - private val imageLoaderProvider: ImageLoaderProvider by inject() +class ArtworkBitmapLoader(private val imageLoaderProvider: ImageLoaderProvider) : BitmapLoader { private val executorService: ListeningExecutorService by lazy { MoreExecutors.listeningDecorator( diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/AvatarFetcher.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/AvatarFetcher.kt index 61db08bc..64dcffc8 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/AvatarFetcher.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/AvatarFetcher.kt @@ -17,8 +17,6 @@ import coil3.request.Options import java.io.IOException import okio.buffer import okio.source -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient data class AvatarRequest(val username: String) @@ -27,11 +25,11 @@ class AvatarKeyer : Keyer { override fun key(data: AvatarRequest, options: Options): String = data.username } -class AvatarFetcher(private val avatarRequest: AvatarRequest, private val options: Options) : - Fetcher, - KoinComponent { - private val client: SubsonicAPIClient by inject() - +class AvatarFetcher( + private val avatarRequest: AvatarRequest, + private val options: Options, + private val client: SubsonicAPIClient +) : Fetcher { override suspend fun fetch(): FetchResult { // Inverted call order, because Mockito has problems with chained calls. val response = client.toStreamResponse( @@ -48,11 +46,16 @@ class AvatarFetcher(private val avatarRequest: AvatarRequest, private val option ) } - class Factory : Fetcher.Factory { + /** + * The client is resolved lazily through [clientProvider] at request time, because the + * SubsonicAPIClient is regenerated whenever the active server changes. + */ + class Factory(private val clientProvider: () -> SubsonicAPIClient) : + Fetcher.Factory { override fun create( data: AvatarRequest, options: Options, imageLoader: coil3.ImageLoader - ): Fetcher = AvatarFetcher(data, options) + ): Fetcher = AvatarFetcher(data, options, clientProvider()) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/CoverArtFetcher.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/CoverArtFetcher.kt index 164d9635..c690523e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/CoverArtFetcher.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/imageloader/CoverArtFetcher.kt @@ -22,8 +22,6 @@ import java.io.File import java.io.IOException import okio.buffer import okio.source -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.FileUtil.SUFFIX_LARGE @@ -37,11 +35,11 @@ class CoverArtKeyer : Keyer { override fun key(data: CoverArtRequest, options: Options): String? = data.cacheKey } -class CoverArtFetcher(private val coverArtRequest: CoverArtRequest, private val options: Options) : - Fetcher, - KoinComponent { - private val client: SubsonicAPIClient by inject() - +class CoverArtFetcher( + private val coverArtRequest: CoverArtRequest, + private val options: Options, + private val client: SubsonicAPIClient +) : Fetcher { override suspend fun fetch(): FetchResult { // Check the cache before sending out a network request val result = getCachedImage() @@ -128,11 +126,16 @@ class CoverArtFetcher(private val coverArtRequest: CoverArtRequest, private val return bitmap1 } - class Factory : Fetcher.Factory { + /** + * The client is resolved lazily through [clientProvider] at request time, because the + * SubsonicAPIClient is regenerated whenever the active server changes. + */ + class Factory(private val clientProvider: () -> SubsonicAPIClient) : + Fetcher.Factory { override fun create( data: CoverArtRequest, options: Options, imageLoader: coil3.ImageLoader - ): Fetcher = CoverArtFetcher(data, options) + ): Fetcher = CoverArtFetcher(data, options, clientProvider()) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt index 9e1e5c12..c5cdc2d9 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/AlbumListModel.kt @@ -18,8 +18,14 @@ import org.moire.ultrasonic.domain.Genre import org.moire.ultrasonic.service.CachedMusicService import org.moire.ultrasonic.service.MusicService import org.moire.ultrasonic.service.MusicServiceFactory +import org.moire.ultrasonic.util.CommunicationError -class AlbumListModel(application: Application) : GenericListModel(application) { +class AlbumListModel( + application: Application, + activeServerProvider: ActiveServerProvider, + musicServiceFactory: MusicServiceFactory, + communicationError: CommunicationError +) : GenericListModel(application, activeServerProvider, musicServiceFactory, communicationError) { val list: MutableLiveData> = MutableLiveData() private var lastType: AlbumListType? = null diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ArtistListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ArtistListModel.kt index 49b52cba..4f3b8834 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ArtistListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ArtistListModel.kt @@ -13,11 +13,18 @@ import java.text.Collator import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.domain.ArtistOrIndex import org.moire.ultrasonic.service.MusicService +import org.moire.ultrasonic.service.MusicServiceFactory +import org.moire.ultrasonic.util.CommunicationError /** * Provides ViewModel which contains the list of available Artists */ -class ArtistListModel(application: Application) : GenericListModel(application) { +class ArtistListModel( + application: Application, + activeServerProvider: ActiveServerProvider, + musicServiceFactory: MusicServiceFactory, + communicationError: CommunicationError +) : GenericListModel(application, activeServerProvider, musicServiceFactory, communicationError) { private val artists: MutableLiveData> = MutableLiveData() /** diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/EditServerModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/EditServerModel.kt index 2c0307bb..17739049 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/EditServerModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/EditServerModel.kt @@ -16,8 +16,6 @@ import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.flatMapMerge import kotlinx.coroutines.flow.flow import okhttp3.OkHttpClient -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject import org.moire.ultrasonic.BuildConfig import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient import org.moire.ultrasonic.api.subsonic.SubsonicAPIDefinition @@ -26,18 +24,13 @@ import org.moire.ultrasonic.api.subsonic.SubsonicClientConfiguration import org.moire.ultrasonic.api.subsonic.SubsonicRESTException import org.moire.ultrasonic.api.subsonic.allowSelfSignedCertificates import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse -import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ServerSetting import org.moire.ultrasonic.util.Constants import retrofit2.HttpException import timber.log.Timber @Suppress("MagicNumber") -class EditServerModel(val app: Application) : - AndroidViewModel(app), - KoinComponent { - - val activeServerProvider: ActiveServerProvider by inject() +class EditServerModel(val app: Application) : AndroidViewModel(app) { private suspend fun serverFunctionAvailable( type: ServerFeature, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt index d001419d..37568717 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenericListModel.kt @@ -18,8 +18,6 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ServerSetting import org.moire.ultrasonic.domain.MusicFolder @@ -30,12 +28,12 @@ import org.moire.ultrasonic.util.CommunicationError /** * An abstract Model, which can be extended to retrieve a list of items from the API */ -open class GenericListModel(application: Application) : - AndroidViewModel(application), - KoinComponent { - - val activeServerProvider: ActiveServerProvider by inject() - val musicServiceFactory: MusicServiceFactory by inject() +open class GenericListModel( + application: Application, + val activeServerProvider: ActiveServerProvider, + val musicServiceFactory: MusicServiceFactory, + private val communicationError: CommunicationError +) : AndroidViewModel(application) { val activeServer: ServerSetting get() = activeServerProvider.getActiveServer() @@ -118,7 +116,7 @@ open class GenericListModel(application: Application) : // CommunicationError reports transport failures to the provider, which switches // subsequent loads to the local catalog, and shows a unified message. API and // authentication failures are deliberately left fully visible to the user. - CommunicationError.handleError(all, context) + communicationError.handleError(all, context) } } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenreListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenreListModel.kt index 49d1aef5..0e387603 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenreListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/GenreListModel.kt @@ -9,13 +9,21 @@ package org.moire.ultrasonic.model import android.app.Application import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.domain.Genre import org.moire.ultrasonic.service.MusicService +import org.moire.ultrasonic.service.MusicServiceFactory +import org.moire.ultrasonic.util.CommunicationError /** * Provides a ViewModel which contains the list of available Genres */ -class GenreListModel(application: Application) : GenericListModel(application) { +class GenreListModel( + application: Application, + activeServerProvider: ActiveServerProvider, + musicServiceFactory: MusicServiceFactory, + communicationError: CommunicationError +) : GenericListModel(application, activeServerProvider, musicServiceFactory, communicationError) { private val genres: MutableLiveData> = MutableLiveData() /** diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/PlaylistListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/PlaylistListModel.kt index 9361e22c..880a72cb 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/PlaylistListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/PlaylistListModel.kt @@ -9,16 +9,23 @@ package org.moire.ultrasonic.model import android.app.Application import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import org.koin.core.component.inject +import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.domain.Playlist import org.moire.ultrasonic.service.MusicService +import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.util.CacheCleaner +import org.moire.ultrasonic.util.CommunicationError /** * Provides a ViewModel which contains the list of available Playlists */ -class PlaylistListModel(application: Application) : GenericListModel(application) { - private val cacheCleaner: CacheCleaner by inject() +class PlaylistListModel( + application: Application, + activeServerProvider: ActiveServerProvider, + musicServiceFactory: MusicServiceFactory, + communicationError: CommunicationError, + private val cacheCleaner: CacheCleaner +) : GenericListModel(application, activeServerProvider, musicServiceFactory, communicationError) { private val playlists: MutableLiveData> = MutableLiveData() /** diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/PodcastListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/PodcastListModel.kt index 60bf81f3..93ea6d1e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/PodcastListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/PodcastListModel.kt @@ -9,13 +9,21 @@ package org.moire.ultrasonic.model import android.app.Application import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.domain.PodcastsChannel import org.moire.ultrasonic.service.MusicService +import org.moire.ultrasonic.service.MusicServiceFactory +import org.moire.ultrasonic.util.CommunicationError /** * Provides a ViewModel which contains the list of available Podcast channels */ -class PodcastListModel(application: Application) : GenericListModel(application) { +class PodcastListModel( + application: Application, + activeServerProvider: ActiveServerProvider, + musicServiceFactory: MusicServiceFactory, + communicationError: CommunicationError +) : GenericListModel(application, activeServerProvider, musicServiceFactory, communicationError) { private val channels: MutableLiveData> = MutableLiveData() /** diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt index 67699722..f45e055b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/SearchListModel.kt @@ -11,12 +11,19 @@ import android.app.Application import androidx.lifecycle.MutableLiveData import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.domain.SearchCriteria import org.moire.ultrasonic.domain.SearchResult import org.moire.ultrasonic.service.MusicServiceFactory +import org.moire.ultrasonic.util.CommunicationError import org.moire.ultrasonic.util.Settings -class SearchListModel(application: Application) : GenericListModel(application) { +class SearchListModel( + application: Application, + activeServerProvider: ActiveServerProvider, + musicServiceFactory: MusicServiceFactory, + communicationError: CommunicationError +) : GenericListModel(application, activeServerProvider, musicServiceFactory, communicationError) { var searchResult: MutableLiveData = MutableLiveData() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ShareListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ShareListModel.kt index a3155391..9ef4b36e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ShareListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/ShareListModel.kt @@ -9,13 +9,21 @@ package org.moire.ultrasonic.model import android.app.Application import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.domain.Share import org.moire.ultrasonic.service.MusicService +import org.moire.ultrasonic.service.MusicServiceFactory +import org.moire.ultrasonic.util.CommunicationError /** * Provides a ViewModel which contains the list of available Shares */ -class ShareListModel(application: Application) : GenericListModel(application) { +class ShareListModel( + application: Application, + activeServerProvider: ActiveServerProvider, + musicServiceFactory: MusicServiceFactory, + communicationError: CommunicationError +) : GenericListModel(application, activeServerProvider, musicServiceFactory, communicationError) { private val shares: MutableLiveData> = MutableLiveData() /** diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt index 86a1f08d..58f60f24 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/TrackCollectionModel.kt @@ -19,12 +19,18 @@ import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.service.DownloadService import org.moire.ultrasonic.service.DownloadState import org.moire.ultrasonic.service.MusicServiceFactory +import org.moire.ultrasonic.util.CommunicationError import org.moire.ultrasonic.util.Util /* * Model for retrieving different collections of tracks from the API */ -class TrackCollectionModel(application: Application) : GenericListModel(application) { +class TrackCollectionModel( + application: Application, + activeServerProvider: ActiveServerProvider, + musicServiceFactory: MusicServiceFactory, + communicationError: CommunicationError +) : GenericListModel(application, activeServerProvider, musicServiceFactory, communicationError) { val currentList: MutableLiveData> = MutableLiveData() private var loadedUntil: Int = 0 diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt index 5ad0a3c5..83baa4c2 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt @@ -9,8 +9,6 @@ package org.moire.ultrasonic.service import java.io.InputStream import java.util.concurrent.TimeUnit import kotlin.Pair -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject import org.moire.ultrasonic.api.subsonic.models.AlbumListType import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.MetaDatabase @@ -38,10 +36,10 @@ import org.moire.ultrasonic.util.TimeLimitedCache import org.moire.ultrasonic.util.Util @Suppress("TooManyFunctions") -class CachedMusicService(private val musicService: MusicService) : - MusicService, - KoinComponent { - private val activeServerProvider: ActiveServerProvider by inject() +class CachedMusicService( + private val musicService: MusicService, + private val activeServerProvider: ActiveServerProvider +) : MusicService { private var metaDatabase: MetaDatabase = activeServerProvider.getActiveMetaDatabase() // Old style TimeLimitedCache diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt index 47e141c2..39876244 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadService.kt @@ -31,7 +31,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.koin.core.component.KoinComponent import org.koin.core.component.inject -import org.koin.java.KoinJavaComponent.inject import org.moire.ultrasonic.R import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.data.ActiveServerProvider @@ -39,6 +38,7 @@ import org.moire.ultrasonic.data.toLocalMediaCatalogEntry import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.service.DownloadState.Companion.isFinalState import org.moire.ultrasonic.service.UltrasonicBus +import org.moire.ultrasonic.subsonic.ImageLoaderProvider import org.moire.ultrasonic.util.CacheCleaner import org.moire.ultrasonic.util.FileUtil.getCompleteFile import org.moire.ultrasonic.util.FileUtil.getPartialFile @@ -68,6 +68,9 @@ class DownloadService : private var scope: CoroutineScope? = null private val storageMonitor: ExternalStorageMonitor by inject() private val cacheCleaner: CacheCleaner by inject() + private val musicServiceFactory: MusicServiceFactory by inject() + private val imageLoaderProvider: ImageLoaderProvider by inject() + private val activeServerProvider: ActiveServerProvider by inject() private val binder: IBinder = SimpleServiceBinder(this) private var isInForeground = false @@ -151,7 +154,14 @@ class DownloadService : // Use poll() instead of remove() which throws an Exception if there is no element. val track: DownloadableTrack = downloadQueue.poll() ?: continue - val downloadTask = DownloadTask(track, scope!!, ::downloadStateChangedCallback) + val downloadTask = DownloadTask( + track, + scope!!, + ::downloadStateChangedCallback, + musicServiceFactory, + imageLoaderProvider, + activeServerProvider + ) activeDownloads[track.id] = downloadTask downloadTask.start() @@ -245,11 +255,13 @@ class DownloadService : } @Suppress("MagicNumber", "NestedBlockDepth", "TooManyFunctions") - companion object { + companion object : KoinComponent { - // Access the instance lazily; the companion may be touched before Koin started. - private fun activeServerProvider(): ActiveServerProvider = - inject(ActiveServerProvider::class.java).value + // The companion is the static bridge to the running Android Service, and may be + // touched before Koin started. The lazy delegates therefore defer resolution until + // the first actual call. + private val activeServerProvider: ActiveServerProvider by inject() + private val cacheCleaner: CacheCleaner by inject() private var startFuture: SettableFuture? = null @@ -424,12 +436,11 @@ class DownloadService : Storage.delete(track.getPartialFile()) // A legacy filename can be shared by catalog rows from different servers. // Delete the catalog record first and only remove a completed file when it has no owners. - val catalog = inject(ActiveServerProvider::class.java) - .value.offlineMetaDatabase.localMediaCatalogDao() + val catalog = activeServerProvider.offlineMetaDatabase.localMediaCatalogDao() val sourceServerId = if (track.serverId > 0) { track.serverId } else { - activeServerProvider().getSelectedServerId() + activeServerProvider.getSelectedServerId() } val catalogEntry = catalog.get(sourceServerId, track.id) val pathsToDelete = if (catalogEntry != null) { @@ -443,7 +454,6 @@ class DownloadService : if (catalog.referencesToPath(path) == 0) Storage.delete(path) } postState(track, DownloadState.IDLE) - val cacheCleaner: CacheCleaner by inject(CacheCleaner::class.java) cacheCleaner.cleanDatabaseSelective(track) pathsToDelete.forEach(Util::scanMedia) } @@ -500,10 +510,10 @@ class DownloadService : val sourceServerId = if (track.serverId > 0) { track.serverId } else { - activeServerProvider().getSelectedServerId() + activeServerProvider.getSelectedServerId() } - inject(ActiveServerProvider::class.java) - .value.offlineMetaDatabase.localMediaCatalogDao() + activeServerProvider + .offlineMetaDatabase.localMediaCatalogDao() .upsert( track.toLocalMediaCatalogEntry( sourceServerId = sourceServerId, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadTask.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadTask.kt index 29de3436..7e9f0ff3 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadTask.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/DownloadTask.kt @@ -16,8 +16,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.MetaDatabase import org.moire.ultrasonic.data.toLocalMediaCatalogEntry @@ -42,12 +40,12 @@ private const val REFRESH_INTERVAL = 50 class DownloadTask( val downloadTrack: DownloadableTrack, private val scope: CoroutineScope, - private val stateChangedCallback: (DownloadableTrack, DownloadState, progress: Int?) -> Unit -) : KoinComponent { - private val musicServiceFactory: MusicServiceFactory by inject() + private val stateChangedCallback: (DownloadableTrack, DownloadState, progress: Int?) -> Unit, + musicServiceFactory: MusicServiceFactory, + private val imageLoaderProvider: ImageLoaderProvider, + private val activeServerProvider: ActiveServerProvider +) { private val musicService = musicServiceFactory.getMusicService() - private val imageLoaderProvider: ImageLoaderProvider by inject() - private val activeServerProvider: ActiveServerProvider by inject() private var job: Job? = null private var inputStream: InputStream? = null diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaLibrarySessionCallback.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaLibrarySessionCallback.kt index 25f8c1de..9da1afff 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaLibrarySessionCallback.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaLibrarySessionCallback.kt @@ -43,8 +43,6 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.guava.await import kotlinx.coroutines.guava.future import kotlinx.coroutines.launch -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject import org.moire.ultrasonic.R import org.moire.ultrasonic.api.subsonic.models.AlbumListType import org.moire.ultrasonic.app.UApp @@ -103,21 +101,19 @@ const val PLAY_COMMAND = "play " /** * MediaBrowserService implementation for e.g. Android Auto */ -@Suppress("TooManyFunctions", "LargeClass", "UnusedPrivateMember") +@Suppress("TooManyFunctions", "LargeClass", "UnusedPrivateMember", "LongParameterList") @OptIn(UnstableApi::class) class MediaLibrarySessionCallback( + private val activeServerProvider: ActiveServerProvider, + private val musicServiceFactory: MusicServiceFactory, + private val ratingManager: RatingManager, private val moveInPlayOrder: (from: Int, to: Int) -> Unit, private val shuffleFromStart: () -> Unit, private val setBackend: (PlaybackBackend) -> Unit, private val shutdown: () -> Unit, private val playbackResumption: () -> ListenableFuture -) : MediaLibraryService.MediaLibrarySession.Callback, - KoinComponent { - - private val activeServerProvider: ActiveServerProvider by inject() - private val musicServiceFactory: MusicServiceFactory by inject() - private val ratingManager: RatingManager by inject() +) : MediaLibraryService.MediaLibrarySession.Callback { private val serviceJob = SupervisorJob() private val serviceScope = CoroutineScope(Dispatchers.IO + serviceJob) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicServiceFactory.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicServiceFactory.kt index 50df8b17..7f296576 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicServiceFactory.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicServiceFactory.kt @@ -18,10 +18,7 @@ */ package org.moire.ultrasonic.service -import org.koin.core.component.KoinComponent -import org.koin.core.component.get -import org.koin.core.context.loadKoinModules -import org.koin.core.context.unloadKoinModules +import org.koin.core.Koin import org.koin.core.qualifier.named import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.LibraryAccessState @@ -44,8 +41,13 @@ import timber.log.Timber * external reset call, [getMusicService] checks for itself whether the module is still * valid for the current state, and regenerates it if needed. */ -class MusicServiceFactory(private val activeServerProvider: ActiveServerProvider) : - KoinComponent { +class MusicServiceFactory( + private val activeServerProvider: ActiveServerProvider, + // The Koin instance is injected instead of using the KoinComponent service locator, + // because this factory is the one component that must resolve from a Koin module + // that is regenerated at runtime (see below). + private val koin: Koin +) { // Identifies the access state the module was last loaded for. Only the transition // dimensions are compared: server property edits (invalidateCache) intentionally do @@ -66,9 +68,9 @@ class MusicServiceFactory(private val activeServerProvider: ActiveServerProvider fun getMusicService(): MusicService { ensureMusicServiceUpToDate() return if (activeServerProvider.isOffline()) { - get(named(OFFLINE_MUSIC_SERVICE)) + koin.get(named(OFFLINE_MUSIC_SERVICE)) } else { - get(named(ONLINE_MUSIC_SERVICE)) + koin.get(named(ONLINE_MUSIC_SERVICE)) } } @@ -80,8 +82,8 @@ class MusicServiceFactory(private val activeServerProvider: ActiveServerProvider @Synchronized fun resetMusicService() { Timber.i("Regenerating Koin Music Service Module") - unloadKoinModules(musicServiceModule) - loadKoinModules(musicServiceModule) + koin.unloadModules(listOf(musicServiceModule)) + koin.loadModules(listOf(musicServiceModule)) loadedStateKey = accessStateKey } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt index fd548bc9..fd34caff 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt @@ -20,8 +20,6 @@ import java.util.LinkedList import java.util.Locale import java.util.concurrent.TimeUnit import java.util.regex.Pattern -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject import org.moire.ultrasonic.api.subsonic.models.AlbumListType import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.LocalMediaCatalogEntry @@ -57,10 +55,8 @@ import org.moire.ultrasonic.util.Util.safeClose import timber.log.Timber @Suppress("TooManyFunctions") -class OfflineMusicService : - MusicService, - KoinComponent { - private val activeServerProvider: ActiveServerProvider by inject() +class OfflineMusicService(private val activeServerProvider: ActiveServerProvider) : + MusicService { private var metaDatabase: MetaDatabase = activeServerProvider.getActiveMetaDatabase() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackService.kt index 81dd815c..ec75eafd 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackService.kt @@ -58,7 +58,9 @@ import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.imageloader.ArtworkBitmapLoader import org.moire.ultrasonic.provider.UltrasonicAppWidgetProvider import org.moire.ultrasonic.service.MusicServiceFactory +import org.moire.ultrasonic.service.RatingManager import org.moire.ultrasonic.service.UltrasonicBus +import org.moire.ultrasonic.subsonic.ImageLoaderProvider import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.CoroutinePatterns import org.moire.ultrasonic.util.Settings @@ -85,6 +87,8 @@ class PlaybackService : private val activeServerProvider: ActiveServerProvider by inject() private val musicServiceFactory: MusicServiceFactory by inject() private val playbackStateSerializer: PlaybackStateSerializer by inject() + private val ratingManager: RatingManager by inject() + private val imageLoaderProvider: ImageLoaderProvider by inject() private lateinit var librarySessionCallback: MediaLibrarySessionCallback private lateinit var playbackStateStore: PlaybackStateStore @@ -191,6 +195,9 @@ class PlaybackService : // Create browser interface librarySessionCallback = MediaLibrarySessionCallback( + activeServerProvider, + musicServiceFactory, + ratingManager, ::moveInPlayOrder, { (player as? QueueAwarePlayer)?.shuffleFromStart() }, ::updateBackendFromController, @@ -201,7 +208,7 @@ class PlaybackService : // This will need to use the AutoCalls mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback) .setSessionActivity(getPendingIntentForContent()) - .setBitmapLoader(ArtworkBitmapLoader()) + .setBitmapLoader(ArtworkBitmapLoader(imageLoaderProvider)) .build() // Send custom layout to legacy session. diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt index 7d64647c..4928c60e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/PlaybackStateSerializer.kt @@ -12,8 +12,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.FileUtil import timber.log.Timber @@ -47,9 +45,7 @@ internal class PendingPlaybackStateLoads { * * TODO: Should use: MediaItemsWithStartPosition */ -class PlaybackStateSerializer : KoinComponent { - - private val context by inject() +class PlaybackStateSerializer(private val context: Context) { private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ImageLoaderProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ImageLoaderProvider.kt index 8c7e4cef..e28eab8d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ImageLoaderProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ImageLoaderProvider.kt @@ -12,8 +12,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.koin.core.component.KoinComponent -import org.koin.core.component.get +import org.koin.core.Koin import org.koin.core.qualifier.named import org.moire.ultrasonic.R import org.moire.ultrasonic.app.UApp @@ -27,11 +26,14 @@ import timber.log.Timber /** * Handles the lifetime of the Image Loader */ -class ImageLoaderProvider : - KoinComponent, - CoroutineScope by CoroutinePatterns.loggingScope(Dispatchers.IO) { +class ImageLoaderProvider( + // The Koin instance is injected instead of using the KoinComponent service locator: + // the SubsonicAPIClient and the ServerID live in the music service module which is + // regenerated on every server switch, so they must be re-resolved lazily. + private val koin: Koin +) : CoroutineScope by CoroutinePatterns.loggingScope(Dispatchers.IO) { private var imageLoader: ImageLoader? = null - private var serverID: String = get(named("ServerID")) + private var serverID: String = koin.get(named("ServerID")) @Synchronized fun clearImageLoader() { @@ -49,9 +51,9 @@ class ImageLoaderProvider : @Synchronized fun getImageLoader(): ImageLoader { // We need to generate a new ImageLoader if the server has changed... - val currentID = get(named("ServerID")) + val currentID = koin.get(named("ServerID")) if (imageLoader == null || currentID != serverID) { - imageLoader = ImageLoader(UApp.applicationContext(), get(), config) + imageLoader = ImageLoader(UApp.applicationContext(), koin.get(), config) serverID = currentID launch { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/VideoPlayer.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/VideoPlayer.kt index c24b5eef..b6626926 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/VideoPlayer.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/VideoPlayer.kt @@ -3,8 +3,6 @@ package org.moire.ultrasonic.subsonic import android.content.Context import android.content.Intent import androidx.core.net.toUri -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject import org.moire.ultrasonic.R import org.moire.ultrasonic.domain.Track import org.moire.ultrasonic.service.MusicServiceFactory @@ -13,32 +11,27 @@ import org.moire.ultrasonic.util.Util /** * This utility class helps starting video playback */ -@Suppress("UtilityClassWithPublicConstructor") -class VideoPlayer { +class VideoPlayer(private val musicServiceFactory: MusicServiceFactory) { - companion object : KoinComponent { - private val musicServiceFactory: MusicServiceFactory by inject() - - fun playVideo(context: Context, track: Track?) { - if (!Util.hasUsableNetwork() || track == null) { - Util.toast(R.string.select_album_no_network, true, context) - return - } - try { - val intent = Intent(Intent.ACTION_VIEW) - val url = musicServiceFactory.getMusicService().getStreamUrl( - track.id, - maxBitRate = null, - format = "raw" - ) - intent.setDataAndType( - url?.toUri(), - "video/*" - ) - context.startActivity(intent) - } catch (all: Exception) { - Util.toast(all.toString(), false, context) - } + fun playVideo(context: Context, track: Track?) { + if (!Util.hasUsableNetwork() || track == null) { + Util.toast(R.string.select_album_no_network, true, context) + return + } + try { + val intent = Intent(Intent.ACTION_VIEW) + val url = musicServiceFactory.getMusicService().getStreamUrl( + track.id, + maxBitRate = null, + format = "raw" + ) + intent.setDataAndType( + url?.toUri(), + "video/*" + ) + context.startActivity(intent) + } catch (all: Exception) { + Util.toast(all.toString(), false, context) } } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt index 21826d6a..efb0a017 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CacheCleaner.kt @@ -12,9 +12,6 @@ import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject -import org.koin.java.KoinJavaComponent.inject import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.TrackRef import org.moire.ultrasonic.domain.Playlist @@ -36,12 +33,10 @@ import timber.log.Timber /** * Responsible for cleaning up files from the offline download cache on the filesystem. */ -class CacheCleaner : - CoroutineScope by CoroutinePatterns.loggingScope(Dispatchers.IO), - KoinComponent { - - private val activeServerProvider by inject() - private val playback by inject() +class CacheCleaner( + private val activeServerProvider: ActiveServerProvider, + private val playback: PlaybackRepository +) : CoroutineScope by CoroutinePatterns.loggingScope(Dispatchers.IO) { private fun exceptionHandler(tag: String): CoroutineExceptionHandler = CoroutineExceptionHandler { @@ -249,11 +244,7 @@ class CacheCleaner : private fun backgroundPlaylistsCleanup(vararg params: List) { try { - val activeServerProvider = inject( - ActiveServerProvider::class.java - ) - - val server = activeServerProvider.value.getActiveServer().name + val server = activeServerProvider.getActiveServer().name val playlistFiles = listFiles(getPlaylistDirectory(server)) val playlists = params[0] diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CommunicationError.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CommunicationError.kt index 9058f5fb..6bba83dc 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CommunicationError.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CommunicationError.kt @@ -17,8 +17,6 @@ import java.security.cert.CertificateException import javax.net.ssl.SSLException import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineExceptionHandler -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject import org.moire.ultrasonic.R import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException import org.moire.ultrasonic.api.subsonic.SubsonicRESTException @@ -31,8 +29,7 @@ import timber.log.Timber * Contains helper functions to handle the exceptions * thrown during the communication with a Subsonic server */ -object CommunicationError : KoinComponent { - private val activeServerProvider: ActiveServerProvider by inject() +class CommunicationError(private val activeServerProvider: ActiveServerProvider) { fun getHandler( context: Context?, @@ -52,7 +49,6 @@ object CommunicationError : KoinComponent { * @param showToast if true, the message is shown in a Toast; otherwise in an ErrorDialog * @param messagePrefix optional operation-specific prefix, e.g. "Failed to delete playlist" */ - @JvmStatic @JvmOverloads fun handleError( error: Throwable, @@ -88,7 +84,6 @@ object CommunicationError : KoinComponent { } } - @JvmStatic @Suppress("ReturnCount") fun getErrorMessage(error: Throwable): String { val context = UApp.applicationContext() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ContextMenuUtil.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ContextMenuUtil.kt index 64f0a957..7385cb1a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ContextMenuUtil.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ContextMenuUtil.kt @@ -9,8 +9,6 @@ package org.moire.ultrasonic.util import android.view.MenuItem import androidx.fragment.app.Fragment -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject import org.moire.ultrasonic.R import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.Track @@ -19,9 +17,11 @@ import org.moire.ultrasonic.service.QueueInsertionMode import org.moire.ultrasonic.subsonic.ShareHandler import org.moire.ultrasonic.util.Util.navigateToCurrent -object ContextMenuUtil : KoinComponent { - private val playbackQueue: PlaybackQueueActions by inject() - private val shareHandler: ShareHandler by inject() +class ContextMenuUtil( + private val playbackQueue: PlaybackQueueActions, + private val shareHandler: ShareHandler, + private val downloadUtil: DownloadUtil +) { /* * Callback for menu items of collections (albums, artists etc) @@ -58,7 +58,7 @@ object ContextMenuUtil : KoinComponent { ) R.id.menu_pin -> - DownloadUtil.justDownload( + downloadUtil.justDownload( action = DownloadAction.PIN, fragment = fragment, id = item.id, @@ -66,7 +66,7 @@ object ContextMenuUtil : KoinComponent { ) R.id.menu_unpin -> - DownloadUtil.justDownload( + downloadUtil.justDownload( action = DownloadAction.UNPIN, fragment = fragment, id = item.id, @@ -74,7 +74,7 @@ object ContextMenuUtil : KoinComponent { ) R.id.menu_download -> - DownloadUtil.justDownload( + downloadUtil.justDownload( action = DownloadAction.DOWNLOAD, fragment = fragment, id = item.id, @@ -117,7 +117,7 @@ object ContextMenuUtil : KoinComponent { } R.id.song_menu_pin -> { - DownloadUtil.justDownload( + downloadUtil.justDownload( action = DownloadAction.PIN, fragment = fragment, tracks = tracks @@ -125,7 +125,7 @@ object ContextMenuUtil : KoinComponent { } R.id.song_menu_unpin -> { - DownloadUtil.justDownload( + downloadUtil.justDownload( action = DownloadAction.UNPIN, fragment = fragment, tracks = tracks @@ -133,7 +133,7 @@ object ContextMenuUtil : KoinComponent { } R.id.song_menu_download -> { - DownloadUtil.justDownload( + downloadUtil.justDownload( action = DownloadAction.DOWNLOAD, fragment = fragment, tracks = tracks @@ -165,7 +165,7 @@ object ContextMenuUtil : KoinComponent { ) { fragment.launchWithToast { val list = tracks.ifEmpty { - DownloadUtil.getTracksFromServerAsync( + downloadUtil.getTracksFromServerAsync( isArtist, requireNotNull(id), isDirectory, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CoroutinePatterns.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CoroutinePatterns.kt index 006e3463..eb609f8d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CoroutinePatterns.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/CoroutinePatterns.kt @@ -19,6 +19,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.async import kotlinx.coroutines.launch +import org.koin.core.context.GlobalContext import org.moire.ultrasonic.util.Util.toast import timber.log.Timber @@ -42,6 +43,12 @@ object CoroutinePatterns { CoroutineScope(SupervisorJob() + dispatcher + loggingExceptionHandler) } +/** + * Resolves the CommunicationError singleton at the Fragment boundary (the extension + * functions below are only ever called on Fragments, which are Android entry points). + */ +private fun communicationError(): CommunicationError = GlobalContext.get().get() + fun Fragment.toastingExceptionHandler(prefix: String = ""): CoroutineExceptionHandler = CoroutineExceptionHandler { _, @@ -51,7 +58,7 @@ fun Fragment.toastingExceptionHandler(prefix: String = ""): CoroutineExceptionHa if (this is RefreshableFragment) { this.swipeRefresh?.isRefreshing = false } - CommunicationError.handleError( + communicationError().handleError( exception, context, showToast = true, @@ -74,7 +81,7 @@ fun Fragment.launchWithToast(block: suspend CoroutineScope.() -> String?) { // Setup a handler when the job is done deferred.invokeOnCompletion { if (it != null && it !is CancellationException) { - CommunicationError.handleError(it, context, showToast = true) + communicationError().handleError(it, context, showToast = true) } else { scope.launch(Dispatchers.Main) { val successString = deferred.await() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DownloadUtil.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DownloadUtil.kt index d7c56737..6562dcf3 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DownloadUtil.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/DownloadUtil.kt @@ -11,8 +11,6 @@ import androidx.fragment.app.Fragment import java.util.LinkedList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject import org.moire.ultrasonic.R import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.domain.MusicDirectory @@ -24,11 +22,11 @@ import org.moire.ultrasonic.service.MusicServiceFactory * Retrieves a list of songs and adds them to the now playing list */ @Suppress("LongParameterList") -object DownloadUtil : KoinComponent { - - private val activeServerProvider: ActiveServerProvider by inject() - private val cacheCleaner: CacheCleaner by inject() - private val musicServiceFactory: MusicServiceFactory by inject() +class DownloadUtil( + private val activeServerProvider: ActiveServerProvider, + private val cacheCleaner: CacheCleaner, + private val musicServiceFactory: MusicServiceFactory +) { fun justDownload( action: DownloadAction, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/ChatAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/ChatAdapter.kt index bd84b060..4a81e45a 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/ChatAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/view/ChatAdapter.kt @@ -17,23 +17,21 @@ import android.view.ViewGroup import android.widget.ArrayAdapter import android.widget.ImageView import android.widget.TextView -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject import org.moire.ultrasonic.R import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.domain.ChatMessage import org.moire.ultrasonic.subsonic.ImageLoaderProvider -class ChatAdapter(private val context: Context, private val messages: List) : - ArrayAdapter( - context, - R.layout.chat_item, - messages - ), - KoinComponent { - - private val activeServerProvider: ActiveServerProvider by inject() - private val imageLoaderProvider: ImageLoaderProvider by inject() +class ChatAdapter( + private val context: Context, + private val messages: List, + private val activeServerProvider: ActiveServerProvider, + private val imageLoaderProvider: ImageLoaderProvider +) : ArrayAdapter( + context, + R.layout.chat_item, + messages +) { override fun areAllItemsEnabled(): Boolean = true diff --git a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/AvatarFetcherTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/AvatarFetcherTest.kt index 82e396b8..f31b3ace 100644 --- a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/AvatarFetcherTest.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/AvatarFetcherTest.kt @@ -19,14 +19,8 @@ import kotlinx.coroutines.test.runTest import okhttp3.ResponseBody import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldNotBe -import org.junit.After -import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.koin.core.context.startKoin -import org.koin.core.context.stopKoin -import org.koin.dsl.module -import org.koin.test.KoinTest import org.mockito.Answers import org.mockito.Mockito.`when` import org.mockito.kotlin.any @@ -40,31 +34,15 @@ import retrofit2.Response import timber.log.Timber @RunWith(RobolectricTestRunner::class) -class AvatarFetcherTest : KoinTest { +class AvatarFetcherTest { private val mockApiClient: SubsonicAPIClient = mock(defaultAnswer = Answers.RETURNS_DEEP_STUBS) private val context: Context = ApplicationProvider.getApplicationContext() private val imageLoader = coil3.ImageLoader.Builder(context).build() - @Before - fun setUp() { - startKoin { - modules( - module { - single { mockApiClient } - } - ) - } - } - - @After - fun tearDown() { - stopKoin() - } - @Test fun `Should throw IOException when request to api failed`() = runTest { // Create the fetcher under test. - val fetcher = AvatarFetcher.Factory().create( + val fetcher = AvatarFetcher.Factory { mockApiClient }.create( AvatarRequest("user"), Options(context), imageLoader @@ -95,7 +73,7 @@ class AvatarFetcherTest : KoinTest { fun `Should load bitmap from network`() = runTest { // Create the fetcher under test. val user = "user" - val fetcher = AvatarFetcher.Factory().create( + val fetcher = AvatarFetcher.Factory { mockApiClient }.create( AvatarRequest(user), Options(context), imageLoader diff --git a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/CoverArtFetcherTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/CoverArtFetcherTest.kt index 1674a7ea..57f11608 100644 --- a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/CoverArtFetcherTest.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/imageloader/CoverArtFetcherTest.kt @@ -20,14 +20,8 @@ import kotlinx.coroutines.test.runTest import okhttp3.ResponseBody import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldNotBe -import org.junit.After -import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.koin.core.context.startKoin -import org.koin.core.context.stopKoin -import org.koin.dsl.module -import org.koin.test.KoinTest import org.mockito.Answers import org.mockito.Mockito.`when` import org.mockito.kotlin.any @@ -41,31 +35,15 @@ import retrofit2.Response import timber.log.Timber @RunWith(RobolectricTestRunner::class) -class CoverArtFetcherTest : KoinTest { +class CoverArtFetcherTest { private val mockApiClient: SubsonicAPIClient = mock(defaultAnswer = Answers.RETURNS_DEEP_STUBS) private val context: Context = ApplicationProvider.getApplicationContext() private val imageLoader = coil3.ImageLoader.Builder(context).build() - @Before - fun setUp() { - startKoin { - modules( - module { - single { mockApiClient } - } - ) - } - } - - @After - fun tearDown() { - stopKoin() - } - @Test fun `Should throw IOException when request to api failed`() = runTest { // Create the fetcher under test. - val fetcher = CoverArtFetcher.Factory().create( + val fetcher = CoverArtFetcher.Factory { mockApiClient }.create( CoverArtRequest("some", "-1", 0), Options(context), imageLoader @@ -97,7 +75,7 @@ class CoverArtFetcherTest : KoinTest { // Create the fetcher under test. val id = "id" val size = 0 - val fetcher = CoverArtFetcher.Factory().create( + val fetcher = CoverArtFetcher.Factory { mockApiClient }.create( CoverArtRequest(id, "someCacheKey.jpg", size), Options(context), imageLoader @@ -137,7 +115,7 @@ class CoverArtFetcherTest : KoinTest { val id = "id" val size = 0 val cacheKey = "Big_Buck_Bunny.jpeg" - val fetcher = CoverArtFetcher.Factory().create( + val fetcher = CoverArtFetcher.Factory { mockApiClient }.create( CoverArtRequest(id, cacheKey, size), Options(context), imageLoader @@ -165,7 +143,7 @@ class CoverArtFetcherTest : KoinTest { val size = 0 val cacheKeySmall = "Big_Buck_Bunny.jpeg-small" val cacheKeyLarge = "Big_Buck_Bunny.jpeg" - val fetcher = CoverArtFetcher.Factory().create( + val fetcher = CoverArtFetcher.Factory { mockApiClient }.create( CoverArtRequest(id, cacheKeySmall, size), Options(context), imageLoader diff --git a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/service/OfflineMusicServiceCatalogTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/service/OfflineMusicServiceCatalogTest.kt index ef88ffbe..1c420cb1 100644 --- a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/service/OfflineMusicServiceCatalogTest.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/service/OfflineMusicServiceCatalogTest.kt @@ -8,9 +8,6 @@ import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.koin.core.context.startKoin -import org.koin.core.context.stopKoin -import org.koin.dsl.module import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -26,6 +23,7 @@ import org.robolectric.RobolectricTestRunner class OfflineMusicServiceCatalogTest { private lateinit var database: MetaDatabase private lateinit var service: OfflineMusicService + private val activeServerProvider: ActiveServerProvider = mock() @Before fun setUp() { @@ -44,18 +42,13 @@ class OfflineMusicServiceCatalogTest { context, MetaDatabase::class.java ).allowMainThreadQueries().build() - val activeServerProvider: ActiveServerProvider = mock() whenever(activeServerProvider.getActiveMetaDatabase()).thenReturn(database) - startKoin { - modules(module { single { activeServerProvider } }) - } - service = OfflineMusicService() + service = OfflineMusicService(activeServerProvider) } @After fun tearDown() { - stopKoin() database.close() UApp.instance = null } diff --git a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/util/CommunicationErrorReportingTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/util/CommunicationErrorReportingTest.kt index 3e11f319..3bb25430 100644 --- a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/util/CommunicationErrorReportingTest.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/util/CommunicationErrorReportingTest.kt @@ -14,9 +14,6 @@ import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.koin.core.context.startKoin -import org.koin.core.context.stopKoin -import org.koin.dsl.module import org.mockito.Mockito.reset import org.mockito.kotlin.any import org.mockito.kotlin.mock @@ -40,10 +37,9 @@ import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class CommunicationErrorReportingTest { - // CommunicationError is a Kotlin object and caches the injected provider on first use, - // so a single mock instance is deliberately shared by all tests of this class. It must - // be created lazily after UApp.instance is set, because mocking ActiveServerProvider - // triggers its companion object initialization, which reads Android settings. + // The mock must be created lazily after UApp.instance is set, because mocking + // ActiveServerProvider triggers its companion object initialization, which reads + // Android settings. companion object { private val activeServerProvider: ActiveServerProvider by lazy { mock() } } @@ -63,14 +59,10 @@ class CommunicationErrorReportingTest { UApp.instance = app reset(activeServerProvider) - startKoin { - modules(module { single { activeServerProvider } }) - } } @After fun tearDown() { - stopKoin() UApp.instance = null } @@ -78,7 +70,7 @@ class CommunicationErrorReportingTest { fun `handleError reports the error to the provider even without a UI context`() { val error = IOException("Connection reset by peer") - CommunicationError.handleError(error, null) + CommunicationError(activeServerProvider).handleError(error, null) verify(activeServerProvider).reportNetworkFailure(error, false) } @@ -87,7 +79,7 @@ class CommunicationErrorReportingTest { fun `handleError reports local file IO errors to the provider untouched`() { val error = IOException("No space left on device") - CommunicationError.handleError(error, null) + CommunicationError(activeServerProvider).handleError(error, null) verify(activeServerProvider).reportNetworkFailure(error, false) }