mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2026-10-22 23:42:14 +03:00
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.
This commit is contained in:
1 parent
caa45c2243
commit
fbe409b47c
62 files changed
+462
-395
No files matched your search
@@ -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<Album, AlbumRowDelegate.ListViewHolder>() {
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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<ArtistOrIndex, ArtistRowBinder.ViewHolder>(),
|
||||
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)
|
||||
|
||||
@@ -222,7 +222,7 @@ class BaseAdapter<T : Identifiable>(allowDuplicateEntries: Boolean = false) :
|
||||
val type = getItemViewType(position)
|
||||
val binder = types.getType<Any>(type).delegate
|
||||
|
||||
if (binder is Utils.SectionedBinder) {
|
||||
if (binder is SectionedBinder) {
|
||||
return binder.getSectionName(items[position] as Identifiable)
|
||||
}
|
||||
|
||||
|
||||
@@ -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<FolderSelectorBinder.FolderHeader, FolderSelectorBinder.ViewHolder>(),
|
||||
KoinComponent {
|
||||
ItemViewBinder<FolderSelectorBinder.FolderHeader, FolderSelectorBinder.ViewHolder>() {
|
||||
|
||||
private val weakContext: WeakReference<Context> = WeakReference(context)
|
||||
|
||||
|
||||
@@ -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<AlbumHeader, HeaderViewBinder.ViewHolder>(),
|
||||
KoinComponent {
|
||||
class HeaderViewBinder(context: Context, private val imageLoaderProvider: ImageLoaderProvider) :
|
||||
ItemViewBinder<AlbumHeader, HeaderViewBinder.ViewHolder>() {
|
||||
|
||||
private val weakContext: WeakReference<Context> = WeakReference(context)
|
||||
private val random: Random = Random()
|
||||
private val imageLoaderProvider: ImageLoaderProvider by inject()
|
||||
|
||||
// Set our layout files
|
||||
val layout = R.layout.list_header_album
|
||||
|
||||
@@ -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<Share, ShareRowBinder.ViewHolder>() {
|
||||
|
||||
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)
|
||||
|
||||
@@ -25,6 +25,7 @@ class TextRowBinder<T : GenericEntry>(
|
||||
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<T, TextRowBinder.ViewHolder>() {
|
||||
|
||||
@@ -34,7 +35,7 @@ class TextRowBinder<T : GenericEntry>(
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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<Identifiable, TrackViewHolder>(),
|
||||
KoinComponent {
|
||||
) : ItemViewBinder<Identifiable, TrackViewHolder>() {
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()) }
|
||||
}
|
||||
@@ -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()) }
|
||||
}
|
||||
@@ -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()) }
|
||||
|
||||
@@ -59,10 +59,10 @@ val musicServiceModule = module {
|
||||
single { SubsonicAPIClient(get(), get()) }
|
||||
|
||||
single<MusicService>(named(ONLINE_MUSIC_SERVICE)) {
|
||||
CachedMusicService(RESTMusicService(get(), get()))
|
||||
CachedMusicService(RESTMusicService(get(), get()), get())
|
||||
}
|
||||
|
||||
single<MusicService>(named(OFFLINE_MUSIC_SERVICE)) {
|
||||
OfflineMusicService()
|
||||
OfflineMusicService(get())
|
||||
}
|
||||
}
|
||||
@@ -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<Album>(),
|
||||
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) {
|
||||
|
||||
@@ -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<ArtistOrIndex>() {
|
||||
|
||||
private val popupMenuFactory: PopupMenuFactory by inject()
|
||||
|
||||
/**
|
||||
* The ViewModel to use to get the data
|
||||
*/
|
||||
@@ -53,7 +57,8 @@ class ArtistListFragment : EntryListFragment<ArtistOrIndex>() {
|
||||
{ entry -> onItemClick(entry) },
|
||||
{ menuItem, entry -> onContextMenuItemSelected(menuItem, entry) },
|
||||
imageLoaderProvider = imageLoaderProvider,
|
||||
activeServerProvider = activeServerProvider
|
||||
activeServerProvider = activeServerProvider,
|
||||
popupMenuFactory = popupMenuFactory
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Track>() {
|
||||
|
||||
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<Track>() {
|
||||
{ _, _ -> true },
|
||||
checkable = false,
|
||||
draggable = false,
|
||||
lifecycleOwner = viewLifecycleOwner
|
||||
lifecycleOwner = viewLifecycleOwner,
|
||||
popupMenuFactory = popupMenuFactory,
|
||||
playback = playback,
|
||||
activeServerProvider = activeServerProvider
|
||||
)
|
||||
)
|
||||
|
||||
@@ -79,6 +91,11 @@ class DownloadsFragment : MultiListFragment<Track>() {
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadListModel(application: Application) : GenericListModel(application) {
|
||||
class DownloadListModel(
|
||||
application: Application,
|
||||
activeServerProvider: ActiveServerProvider,
|
||||
musicServiceFactory: MusicServiceFactory,
|
||||
communicationError: CommunicationError
|
||||
) : GenericListModel(application, activeServerProvider, musicServiceFactory, communicationError) {
|
||||
fun getList(): LiveData<List<Track>> = DownloadService.observableDownloads
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<T : GenericEntry> :
|
||||
MultiListFragment<T>(),
|
||||
KoinScopeComponent {
|
||||
abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>() {
|
||||
internal val contextMenuUtil: ContextMenuUtil by inject()
|
||||
|
||||
/**
|
||||
* Whether to show the folder selector
|
||||
*/
|
||||
@@ -41,7 +41,7 @@ abstract class EntryListFragment<T : GenericEntry> :
|
||||
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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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<Playlist>() {
|
||||
|
||||
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<Playlist>() {
|
||||
} else {
|
||||
R.menu.select_playlist_context
|
||||
}
|
||||
}
|
||||
},
|
||||
popupMenuFactory = popupMenuFactory
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -138,7 +144,7 @@ class PlaylistsFragment : MultiListFragment<Playlist>() {
|
||||
R.id.playlist_menu_unpin -> DownloadAction.UNPIN
|
||||
else -> DownloadAction.DOWNLOAD
|
||||
}
|
||||
DownloadUtil.justDownload(
|
||||
downloadUtil.justDownload(
|
||||
action = action,
|
||||
fragment = this,
|
||||
id = playlist.id,
|
||||
|
||||
@@ -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<PodcastsChannel>() {
|
||||
|
||||
private val popupMenuFactory: PopupMenuFactory by inject()
|
||||
|
||||
/**
|
||||
* The ViewModel to use to get the data
|
||||
*/
|
||||
@@ -52,7 +56,8 @@ class PodcastFragment : MultiListFragment<PodcastsChannel>() {
|
||||
TextRowBinder(
|
||||
textResolver = { it.title },
|
||||
onItemClick = { onItemClick(it) },
|
||||
onContextMenuItemClick = { _, _ -> false }
|
||||
onContextMenuItemClick = { _, _ -> false },
|
||||
popupMenuFactory = popupMenuFactory
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<Identifiable>(),
|
||||
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<SearchFragmentArgs>()
|
||||
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,
|
||||
|
||||
@@ -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<Genre>() {
|
||||
|
||||
private val popupMenuFactory: PopupMenuFactory by inject()
|
||||
|
||||
/**
|
||||
* The ViewModel to use to get the data
|
||||
*/
|
||||
@@ -53,7 +57,8 @@ class SelectGenreFragment : MultiListFragment<Genre>() {
|
||||
TextRowBinder(
|
||||
textResolver = { it.name },
|
||||
onItemClick = { onItemClick(it) },
|
||||
onContextMenuItemClick = { _, _ -> false }
|
||||
onContextMenuItemClick = { _, _ -> false },
|
||||
popupMenuFactory = popupMenuFactory
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Share>(),
|
||||
KoinScopeComponent {
|
||||
class SharesFragment : MultiListFragment<Share>() {
|
||||
|
||||
/**
|
||||
* 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,
|
||||
|
||||
+23
-11
@@ -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 -> {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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<AvatarRequest> {
|
||||
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<AvatarRequest> {
|
||||
/**
|
||||
* 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<AvatarRequest> {
|
||||
override fun create(
|
||||
data: AvatarRequest,
|
||||
options: Options,
|
||||
imageLoader: coil3.ImageLoader
|
||||
): Fetcher = AvatarFetcher(data, options)
|
||||
): Fetcher = AvatarFetcher(data, options, clientProvider())
|
||||
}
|
||||
}
|
||||
@@ -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<CoverArtRequest> {
|
||||
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<CoverArtRequest> {
|
||||
/**
|
||||
* 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<CoverArtRequest> {
|
||||
override fun create(
|
||||
data: CoverArtRequest,
|
||||
options: Options,
|
||||
imageLoader: coil3.ImageLoader
|
||||
): Fetcher = CoverArtFetcher(data, options)
|
||||
): Fetcher = CoverArtFetcher(data, options, clientProvider())
|
||||
}
|
||||
}
|
||||
@@ -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<List<Album>> = MutableLiveData()
|
||||
private var lastType: AlbumListType? = null
|
||||
|
||||
@@ -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<List<ArtistOrIndex>> = MutableLiveData()
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<List<Genre>> = MutableLiveData()
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<List<Playlist>> = MutableLiveData()
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<List<PodcastsChannel>> = MutableLiveData()
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<SearchResult?> = MutableLiveData()
|
||||
|
||||
|
||||
@@ -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<List<Share>> = MutableLiveData()
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<List<MusicDirectory.Child>> = MutableLiveData()
|
||||
private var loadedUntil: Int = 0
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>(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<DownloadService>? = 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>(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>(ActiveServerProvider::class.java)
|
||||
.value.offlineMetaDatabase.localMediaCatalogDao()
|
||||
activeServerProvider
|
||||
.offlineMetaDatabase.localMediaCatalogDao()
|
||||
.upsert(
|
||||
track.toLocalMediaCatalogEntry(
|
||||
sourceServerId = sourceServerId,
|
||||
|
||||
@@ -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
|
||||
|
||||
+5
-9
@@ -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<MediaSession.MediaItemsWithStartPosition>
|
||||
) : 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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<Context>()
|
||||
class PlaybackStateSerializer(private val context: Context) {
|
||||
|
||||
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private val mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
||||
|
||||
@@ -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<String>(named("ServerID"))
|
||||
val currentID = koin.get<String>(named("ServerID"))
|
||||
if (imageLoader == null || currentID != serverID) {
|
||||
imageLoader = ImageLoader(UApp.applicationContext(), get(), config)
|
||||
imageLoader = ImageLoader(UApp.applicationContext(), koin.get(), config)
|
||||
serverID = currentID
|
||||
|
||||
launch {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ActiveServerProvider>()
|
||||
private val playback by inject<PlaybackRepository>()
|
||||
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<Playlist>) {
|
||||
try {
|
||||
val activeServerProvider = inject<ActiveServerProvider>(
|
||||
ActiveServerProvider::class.java
|
||||
)
|
||||
|
||||
val server = activeServerProvider.value.getActiveServer().name
|
||||
val server = activeServerProvider.getActiveServer().name
|
||||
val playlistFiles = listFiles(getPlaylistDirectory(server))
|
||||
val playlists = params[0]
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<ChatMessage>) :
|
||||
ArrayAdapter<ChatMessage>(
|
||||
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<ChatMessage>,
|
||||
private val activeServerProvider: ActiveServerProvider,
|
||||
private val imageLoaderProvider: ImageLoaderProvider
|
||||
) : ArrayAdapter<ChatMessage>(
|
||||
context,
|
||||
R.layout.chat_item,
|
||||
messages
|
||||
) {
|
||||
|
||||
override fun areAllItemsEnabled(): Boolean = true
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+2
-9
@@ -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
|
||||
}
|
||||
|
||||
+5
-13
@@ -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)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user