mirror of
https://github.com/mihonapp/mihon.git
synced 2026-10-16 13:48:09 -07:00
Use an app scoped CoroutineScope
Replaces the eight ad hoc application lifetime scopes and the GlobalScope launchUI/launchIO/launchNow helpers with a single injected CoroutineScope(SupervisorJob() + Dispatchers.IO), qualified by @AppCoroutineScope. Every scope this replaces was already on IO, or named IO explicitly, so call sites keep their dispatcher. The two that need the main thread use scope.launchUI. Scopes with a real cancellation point keep their own: HttpPageLoader, ShizukuInstaller and the reader viewers. Only HttpPageLoader's page list cache save moves to the app scope, since it runs after recycle() has cancelled the loader's own scope. NotificationReceiver now holds the broadcast open with goAsync() while its work runs, so the process isn't torn down mid update. This reverts the revert of #3403. It was reverted for #3429, where restore() dropped every download because SourceManager.get() returned null until the extensions had loaded. That surface suspends since #3869, so the race is gone.
This commit is contained in:
@@ -13,7 +13,6 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import coil3.ImageLoader
|
||||
import coil3.SingletonImageLoader
|
||||
import coil3.memory.MemoryCache
|
||||
@@ -46,6 +45,7 @@ import eu.kanade.tachiyomi.util.system.animatorDurationScale
|
||||
import eu.kanade.tachiyomi.util.system.cancelNotification
|
||||
import eu.kanade.tachiyomi.util.system.isDebugBuildType
|
||||
import eu.kanade.tachiyomi.util.system.notify
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
@@ -54,6 +54,7 @@ import logcat.LogPriority
|
||||
import logcat.LogcatLogger
|
||||
import mihon.app.di.AppGraph
|
||||
import mihon.app.di.injekt.MetroInjektRegistrar
|
||||
import mihon.core.metro.AppCoroutineScope
|
||||
import mihon.core.metro.GraphProvider
|
||||
import mihon.core.migration.Migration
|
||||
import mihon.core.migration.Migrator
|
||||
@@ -97,6 +98,9 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
||||
|
||||
@Inject private lateinit var migrations: Set<Migration>
|
||||
|
||||
@Inject @AppCoroutineScope
|
||||
private lateinit var scope: CoroutineScope
|
||||
|
||||
private val disableIncognitoReceiver = DisableIncognitoReceiver()
|
||||
|
||||
@SuppressLint("LaunchActivityFromNotification")
|
||||
@@ -126,8 +130,6 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
|
||||
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||
|
||||
val scope = ProcessLifecycleOwner.get().lifecycleScope
|
||||
|
||||
// Show notification to disable Incognito Mode when it's enabled
|
||||
basePreferences.incognitoMode.changes()
|
||||
.onEach { enabled ->
|
||||
|
||||
@@ -11,7 +11,6 @@ import dev.zacsweers.metro.SingleIn
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
@@ -41,6 +40,7 @@ import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.protobuf.ProtoBuf
|
||||
import logcat.LogPriority
|
||||
import mihon.core.metro.AppCoroutineScope
|
||||
import tachiyomi.core.common.storage.extension
|
||||
import tachiyomi.core.common.storage.nameWithoutExtension
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
@@ -65,14 +65,13 @@ import kotlin.time.Duration.Companion.seconds
|
||||
@Inject
|
||||
@SingleIn(AppScope::class)
|
||||
class DownloadCache(
|
||||
@AppCoroutineScope private val scope: CoroutineScope,
|
||||
private val context: Context,
|
||||
private val provider: DownloadProvider,
|
||||
private val sourceManager: SourceManager,
|
||||
private val storageManager: StorageManager,
|
||||
) {
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
private val _changes: Channel<Unit> = Channel(Channel.UNLIMITED)
|
||||
val changes = _changes.receiveAsFlow()
|
||||
.onStart { emit(Unit) }
|
||||
|
||||
@@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.drop
|
||||
@@ -18,6 +19,7 @@ import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import logcat.LogPriority
|
||||
import mihon.core.metro.AppCoroutineScope
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.core.common.storage.extension
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
@@ -40,6 +42,7 @@ import tachiyomi.i18n.MR
|
||||
@Inject
|
||||
@SingleIn(AppScope::class)
|
||||
class DownloadManager(
|
||||
@AppCoroutineScope private val scope: CoroutineScope,
|
||||
private val context: Context,
|
||||
private val provider: DownloadProvider,
|
||||
private val cache: DownloadCache,
|
||||
@@ -253,7 +256,7 @@ class DownloadManager(
|
||||
* @param source the source of the chapters.
|
||||
*/
|
||||
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source) {
|
||||
launchIO {
|
||||
scope.launchIO {
|
||||
val filteredChapters = getChaptersToDelete(chapters, manga)
|
||||
if (filteredChapters.isEmpty()) {
|
||||
return@launchIO
|
||||
@@ -280,7 +283,7 @@ class DownloadManager(
|
||||
* @param removeQueued whether to also remove queued downloads.
|
||||
*/
|
||||
fun deleteManga(manga: Manga, source: Source, removeQueued: Boolean = true) {
|
||||
launchIO {
|
||||
scope.launchIO {
|
||||
if (removeQueued) {
|
||||
downloader.removeFromQueue(manga)
|
||||
}
|
||||
|
||||
@@ -24,8 +24,6 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -47,11 +45,11 @@ import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.withContext
|
||||
import logcat.LogPriority
|
||||
import mihon.core.archive.ZipWriter
|
||||
import mihon.core.metro.AppCoroutineScope
|
||||
import nl.adaptivity.xmlutil.serialization.XML
|
||||
import okhttp3.Response
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
import tachiyomi.core.common.util.lang.launchNow
|
||||
import tachiyomi.core.common.util.system.ImageUtil
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE
|
||||
@@ -76,6 +74,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||
@Inject
|
||||
@SingleIn(AppScope::class)
|
||||
class Downloader(
|
||||
@AppCoroutineScope private val scope: CoroutineScope,
|
||||
private val context: Context,
|
||||
private val provider: DownloadProvider,
|
||||
private val cache: DownloadCache,
|
||||
@@ -94,8 +93,6 @@ class Downloader(
|
||||
private val _queueState = MutableStateFlow<List<Download>>(emptyList())
|
||||
val queueState = _queueState.asStateFlow()
|
||||
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
// Kept when canceled, so the next job can wait for it to finish
|
||||
private var downloaderJob: Job? = null
|
||||
|
||||
@@ -112,9 +109,8 @@ class Downloader(
|
||||
var isPaused: Boolean = false
|
||||
|
||||
init {
|
||||
launchNow {
|
||||
val chapters = async { store.restore() }
|
||||
addAllToQueue(chapters.await())
|
||||
scope.launch {
|
||||
addAllToQueue(store.restore())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ import eu.kanade.tachiyomi.util.system.cancelNotification
|
||||
import eu.kanade.tachiyomi.util.system.getBitmapOrNull
|
||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||
import eu.kanade.tachiyomi.util.system.notify
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import mihon.core.metro.AppCoroutineScope
|
||||
import tachiyomi.core.common.Constants
|
||||
import tachiyomi.core.common.i18n.pluralStringResource
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
@@ -44,6 +46,7 @@ import mihon.icons.materialsymbols.R as MaterialSymbolsR
|
||||
|
||||
@Inject
|
||||
class LibraryUpdateNotifier(
|
||||
@AppCoroutineScope private val scope: CoroutineScope,
|
||||
private val context: Context,
|
||||
private val securityPreferences: SecurityPreferences,
|
||||
private val sourceManager: SourceManager,
|
||||
@@ -213,7 +216,7 @@ class LibraryUpdateNotifier(
|
||||
|
||||
// Per-manga notification
|
||||
if (!securityPreferences.hideNotificationContent.get()) {
|
||||
launchUI {
|
||||
scope.launchUI {
|
||||
context.notify(
|
||||
updates.map { (manga, chapters) ->
|
||||
NotificationManagerCompat.NotificationWithIdAndTag(
|
||||
|
||||
@@ -18,10 +18,12 @@ import eu.kanade.tachiyomi.util.system.notificationManager
|
||||
import eu.kanade.tachiyomi.util.system.toShareIntent
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import eu.kanade.tachiyomi.util.system.workManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import mihon.app.di.appGraph
|
||||
import mihon.core.metro.AppCoroutineScope
|
||||
import tachiyomi.core.common.Constants
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
import tachiyomi.domain.chapter.interactor.GetChapter
|
||||
import tachiyomi.domain.chapter.interactor.UpdateChapter
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
@@ -52,6 +54,9 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
|
||||
@Inject private lateinit var sourceManager: SourceManager
|
||||
|
||||
@Inject @AppCoroutineScope
|
||||
private lateinit var scope: CoroutineScope
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
context.appGraph.inject(this)
|
||||
|
||||
@@ -189,7 +194,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
* @param mangaId id of manga
|
||||
*/
|
||||
private fun markAsRead(chapterUrls: Array<String>, mangaId: Long) {
|
||||
launchIO {
|
||||
launchAsync {
|
||||
val toUpdate = chapterUrls.mapNotNull { getChapter.await(it, mangaId) }
|
||||
.map {
|
||||
if (downloadPreferences.removeAfterMarkedAsRead.get()) {
|
||||
@@ -214,13 +219,28 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
* @param mangaId id of manga
|
||||
*/
|
||||
private fun downloadChapters(chapterUrls: Array<String>, mangaId: Long) {
|
||||
launchIO {
|
||||
val manga = getManga.await(mangaId) ?: return@launchIO
|
||||
launchAsync {
|
||||
val manga = getManga.await(mangaId) ?: return@launchAsync
|
||||
val chapters = chapterUrls.mapNotNull { getChapter.await(it, mangaId) }
|
||||
downloadManager.downloadChapters(manga, chapters)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs [block] on the application scope while keeping the broadcast alive, so the process isn't torn down before
|
||||
* the work finishes.
|
||||
*/
|
||||
private fun launchAsync(block: suspend CoroutineScope.() -> Unit) {
|
||||
val pendingResult = goAsync()
|
||||
scope.launch {
|
||||
try {
|
||||
block()
|
||||
} finally {
|
||||
pendingResult.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val NAME = "NotificationReceiver"
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
@@ -33,6 +32,7 @@ import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.LogPriority
|
||||
import mihon.core.metro.AppCoroutineScope
|
||||
import mihon.domain.extension.interactor.UpdateExtensionStores
|
||||
import mihon.domain.extension.model.ExtensionStore
|
||||
import mihon.domain.extension.repository.ExtensionStoreRepository
|
||||
@@ -53,6 +53,7 @@ import java.util.Locale
|
||||
@Inject
|
||||
@SingleIn(AppScope::class)
|
||||
class ExtensionManager(
|
||||
@AppCoroutineScope private val scope: CoroutineScope,
|
||||
private val context: Context,
|
||||
private val preferences: SourcePreferences,
|
||||
private val trustExtension: TrustExtension,
|
||||
@@ -62,8 +63,6 @@ class ExtensionManager(
|
||||
private val extensionUpdateNotifier: ExtensionUpdateNotifier,
|
||||
) {
|
||||
|
||||
val scope = CoroutineScope(SupervisorJob())
|
||||
|
||||
private val initialized = CompletableDeferred<Unit>()
|
||||
|
||||
private val iconMap = mutableMapOf<String, Drawable>()
|
||||
@@ -96,7 +95,7 @@ class ExtensionManager(
|
||||
init {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
loadExtensions()
|
||||
ExtensionInstallReceiver(InstallationListener()).register(context)
|
||||
ExtensionInstallReceiver(InstallationListener(), scope).register(context)
|
||||
|
||||
// Everything the load decision rests on can change while running, so decide again
|
||||
merge(
|
||||
|
||||
@@ -9,7 +9,6 @@ import androidx.core.net.toUri
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
@@ -19,10 +18,12 @@ import tachiyomi.core.common.util.system.logcat
|
||||
* notifies the given [listener] when the package is an extension.
|
||||
*
|
||||
* @param listener The listener that should be notified of extension installation events.
|
||||
* @param scope The scope the listener is notified in.
|
||||
*/
|
||||
internal class ExtensionInstallReceiver(private val listener: Listener) : BroadcastReceiver() {
|
||||
|
||||
val scope = CoroutineScope(SupervisorJob())
|
||||
internal class ExtensionInstallReceiver(
|
||||
private val listener: Listener,
|
||||
private val scope: CoroutineScope,
|
||||
) : BroadcastReceiver() {
|
||||
|
||||
fun register(context: Context) {
|
||||
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_NOT_EXPORTED)
|
||||
|
||||
@@ -14,7 +14,6 @@ import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
import eu.kanade.tachiyomi.util.system.isPackageInstalled
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -22,6 +21,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.launch
|
||||
import logcat.LogPriority
|
||||
import mihon.core.metro.AppCoroutineScope
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
@@ -34,12 +34,12 @@ import java.io.File
|
||||
*/
|
||||
@Inject
|
||||
class ExtensionInstaller(
|
||||
@AppCoroutineScope private val scope: CoroutineScope,
|
||||
private val context: Context,
|
||||
basePreferences: BasePreferences,
|
||||
networkHelper: NetworkHelper,
|
||||
) {
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
private val activeJobs = mutableMapOf<String, Job>()
|
||||
private val activeSteps = mutableMapOf<Long, MutableStateFlow<InstallStep>>()
|
||||
private val extensionInstaller = basePreferences.extensionInstaller
|
||||
|
||||
@@ -8,8 +8,6 @@ import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
@@ -17,6 +15,7 @@ import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import mihon.core.metro.AppCoroutineScope
|
||||
import tachiyomi.domain.source.model.StubSource
|
||||
import tachiyomi.domain.source.repository.StubSourceRepository
|
||||
import tachiyomi.domain.source.service.SourceManager
|
||||
@@ -27,14 +26,13 @@ import java.util.concurrent.ConcurrentHashMap
|
||||
@SingleIn(AppScope::class)
|
||||
@ContributesBinding(AppScope::class)
|
||||
class AndroidSourceManager(
|
||||
@AppCoroutineScope private val scope: CoroutineScope,
|
||||
private val extensionManager: ExtensionManager,
|
||||
private val sourceRepository: StubSourceRepository,
|
||||
private val localSource: LocalSource,
|
||||
private val downloadManager: Lazy<DownloadManager>,
|
||||
) : SourceManager {
|
||||
|
||||
private val scope = CoroutineScope(Job() + Dispatchers.IO)
|
||||
|
||||
/**
|
||||
* Null until the extensions have loaded, so that nothing observes the empty seed value.
|
||||
*/
|
||||
|
||||
@@ -54,6 +54,7 @@ import eu.kanade.tachiyomi.util.lang.byteSize
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import eu.kanade.tachiyomi.util.storage.cacheImageDir
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -68,6 +69,7 @@ import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import logcat.LogPriority
|
||||
import mihon.core.metro.AppCoroutineScope
|
||||
import tachiyomi.core.common.preference.toggle
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
import tachiyomi.core.common.util.lang.launchNonCancellable
|
||||
@@ -98,6 +100,7 @@ import kotlin.time.Instant
|
||||
@AssistedInject
|
||||
class ReaderViewModel(
|
||||
@Assisted private val savedState: SavedStateHandle,
|
||||
@AppCoroutineScope private val appScope: CoroutineScope,
|
||||
private val context: Context,
|
||||
private val sourceManager: SourceManager,
|
||||
private val downloadManager: DownloadManager,
|
||||
@@ -332,7 +335,15 @@ class ReaderViewModel(
|
||||
mutableState.update { it.copy(manga = manga, source = source) }
|
||||
if (chapterId == -1L) chapterId = initialChapterId
|
||||
|
||||
loader = ChapterLoader(context, downloadManager, downloadProvider, chapterCache, manga, source)
|
||||
loader = ChapterLoader(
|
||||
context,
|
||||
downloadManager,
|
||||
downloadProvider,
|
||||
chapterCache,
|
||||
appScope,
|
||||
manga,
|
||||
source,
|
||||
)
|
||||
|
||||
loadChapter(loader!!, chapterList.first { chapterId == it.chapter.id })
|
||||
} catch (e: Throwable) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.data.download.DownloadProvider
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import mihon.core.archive.archiveReader
|
||||
import mihon.core.archive.epubReader
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
@@ -27,6 +28,7 @@ class ChapterLoader(
|
||||
private val downloadManager: DownloadManager,
|
||||
private val downloadProvider: DownloadProvider,
|
||||
private val chapterCache: ChapterCache,
|
||||
private val appScope: CoroutineScope,
|
||||
private val manga: Manga,
|
||||
private val source: Source,
|
||||
) {
|
||||
@@ -102,7 +104,7 @@ class ChapterLoader(
|
||||
is Format.Epub -> EpubPageLoader(format.file.epubReader(context))
|
||||
}
|
||||
}
|
||||
source is HttpSource -> HttpPageLoader(chapter, source, chapterCache)
|
||||
source is HttpSource -> HttpPageLoader(chapter, source, chapterCache, appScope)
|
||||
source is StubSource -> error(context.stringResource(MR.strings.source_not_installed, source.toString()))
|
||||
else -> error(context.stringResource(MR.strings.loader_not_implemented_error))
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ internal class HttpPageLoader(
|
||||
private val chapter: ReaderChapter,
|
||||
private val source: HttpSource,
|
||||
private val chapterCache: ChapterCache,
|
||||
private val appScope: CoroutineScope,
|
||||
) : PageLoader() {
|
||||
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
@@ -132,7 +133,7 @@ internal class HttpPageLoader(
|
||||
|
||||
// Cache current page list progress for online chapters to allow a faster reopen
|
||||
chapter.pages?.let { pages ->
|
||||
launchIO {
|
||||
appScope.launchIO {
|
||||
try {
|
||||
// Convert to pages without reader information
|
||||
val pagesToSave = pages.map { Page(it.index, it.url, it.imageUrl) }
|
||||
|
||||
@@ -4,8 +4,12 @@ import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.BindingContainer
|
||||
import dev.zacsweers.metro.Provides
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.protobuf.ProtoBuf
|
||||
import mihon.core.metro.AppCoroutineScope
|
||||
import nl.adaptivity.xmlutil.XmlDeclMode
|
||||
import nl.adaptivity.xmlutil.core.XmlVersion
|
||||
import nl.adaptivity.xmlutil.serialization.XML
|
||||
@@ -13,6 +17,11 @@ import nl.adaptivity.xmlutil.serialization.XML
|
||||
@BindingContainer
|
||||
object AppBindings {
|
||||
|
||||
@Provides
|
||||
@SingleIn(AppScope::class)
|
||||
@AppCoroutineScope
|
||||
fun providesAppCoroutineScope(): CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
|
||||
@Provides
|
||||
@SingleIn(AppScope::class)
|
||||
fun providesJson(): Json = Json {
|
||||
|
||||
@@ -7,6 +7,8 @@ import dev.zacsweers.metro.SingleIn
|
||||
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
|
||||
import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor
|
||||
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import mihon.core.metro.AppCoroutineScope
|
||||
import okhttp3.Cache
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
@@ -17,6 +19,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||
@Inject
|
||||
@SingleIn(AppScope::class)
|
||||
class NetworkHelper(
|
||||
@AppCoroutineScope private val scope: CoroutineScope,
|
||||
private val context: Context,
|
||||
private val preferences: NetworkPreferences,
|
||||
) {
|
||||
@@ -64,7 +67,7 @@ class NetworkHelper(
|
||||
|
||||
val client = clientBuilder
|
||||
.addInterceptor(
|
||||
CloudflareInterceptor(context, cookieJar, ::defaultUserAgentProvider),
|
||||
CloudflareInterceptor(context, cookieJar, scope, ::defaultUserAgentProvider),
|
||||
)
|
||||
.build()
|
||||
|
||||
|
||||
+3
-1
@@ -12,6 +12,7 @@ import androidx.core.content.ContextCompat
|
||||
import eu.kanade.tachiyomi.network.AndroidCookieJar
|
||||
import eu.kanade.tachiyomi.util.system.isOutdated
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import okhttp3.Cookie
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Interceptor
|
||||
@@ -25,8 +26,9 @@ import java.util.concurrent.CountDownLatch
|
||||
class CloudflareInterceptor(
|
||||
private val context: Context,
|
||||
private val cookieManager: AndroidCookieJar,
|
||||
scope: CoroutineScope,
|
||||
defaultUserAgentProvider: () -> String,
|
||||
) : WebViewInterceptor(context, defaultUserAgentProvider) {
|
||||
) : WebViewInterceptor(context, scope, defaultUserAgentProvider) {
|
||||
|
||||
private val executor = ContextCompat.getMainExecutor(context)
|
||||
|
||||
|
||||
+3
-1
@@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||
import eu.kanade.tachiyomi.util.system.setUserAgent
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import okhttp3.Headers
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
@@ -22,6 +23,7 @@ import java.util.concurrent.TimeUnit
|
||||
|
||||
abstract class WebViewInterceptor(
|
||||
private val context: Context,
|
||||
private val scope: CoroutineScope,
|
||||
private val defaultUserAgentProvider: () -> String,
|
||||
) : Interceptor {
|
||||
|
||||
@@ -57,7 +59,7 @@ abstract class WebViewInterceptor(
|
||||
}
|
||||
|
||||
if (!WebViewUtil.supportsWebView(context)) {
|
||||
launchUI {
|
||||
scope.launchUI {
|
||||
context.toast(MR.strings.information_webview_required, Toast.LENGTH_LONG)
|
||||
}
|
||||
return response
|
||||
|
||||
@@ -1,48 +1,12 @@
|
||||
package tachiyomi.core.common.util.lang
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Think twice before using this. This is a delicate API. It is easy to accidentally create resource or memory leaks when GlobalScope is used.
|
||||
*
|
||||
* **Possible replacements**
|
||||
* - suspend function
|
||||
* - custom scope like view or presenter scope
|
||||
*/
|
||||
@DelicateCoroutinesApi
|
||||
fun launchUI(block: suspend CoroutineScope.() -> Unit): Job =
|
||||
GlobalScope.launch(Dispatchers.Main, CoroutineStart.DEFAULT, block)
|
||||
|
||||
/**
|
||||
* Think twice before using this. This is a delicate API. It is easy to accidentally create resource or memory leaks when GlobalScope is used.
|
||||
*
|
||||
* **Possible replacements**
|
||||
* - suspend function
|
||||
* - custom scope like view or presenter scope
|
||||
*/
|
||||
@DelicateCoroutinesApi
|
||||
fun launchIO(block: suspend CoroutineScope.() -> Unit): Job =
|
||||
GlobalScope.launch(Dispatchers.IO, CoroutineStart.DEFAULT, block)
|
||||
|
||||
/**
|
||||
* Think twice before using this. This is a delicate API. It is easy to accidentally create resource or memory leaks when GlobalScope is used.
|
||||
*
|
||||
* **Possible replacements**
|
||||
* - suspend function
|
||||
* - custom scope like view or presenter scope
|
||||
*/
|
||||
@DelicateCoroutinesApi
|
||||
fun launchNow(block: suspend CoroutineScope.() -> Unit): Job =
|
||||
GlobalScope.launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED, block)
|
||||
|
||||
fun CoroutineScope.launchUI(block: suspend CoroutineScope.() -> Unit): Job =
|
||||
launch(Dispatchers.Main, block = block)
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package mihon.core.metro
|
||||
|
||||
import dev.zacsweers.metro.Qualifier
|
||||
|
||||
/**
|
||||
* Qualifies the single application wide [kotlinx.coroutines.CoroutineScope].
|
||||
*
|
||||
* It lives for as long as the process does and is backed by a [kotlinx.coroutines.SupervisorJob] on
|
||||
* [kotlinx.coroutines.Dispatchers.IO], so a failure in one child never takes the others down. Use it for work that is
|
||||
* owned by an application scoped component and must outlive any screen. Work that belongs to a screen, a view model or
|
||||
* another cancellable lifecycle keeps its own scope.
|
||||
*/
|
||||
@Qualifier
|
||||
@Target(
|
||||
AnnotationTarget.VALUE_PARAMETER,
|
||||
AnnotationTarget.FUNCTION,
|
||||
AnnotationTarget.PROPERTY,
|
||||
AnnotationTarget.PROPERTY_GETTER,
|
||||
)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class AppCoroutineScope
|
||||
@@ -19,6 +19,7 @@ kotlin {
|
||||
dependencies {
|
||||
implementation(projects.sourceApi)
|
||||
implementation(projects.core.common)
|
||||
implementation(projects.core.metro)
|
||||
|
||||
implementation(libs.metro.runtime)
|
||||
implementation(libs.bundles.kotlinx.coroutines)
|
||||
|
||||
@@ -8,7 +8,6 @@ import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
@@ -17,16 +16,16 @@ import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import mihon.core.metro.AppCoroutineScope
|
||||
|
||||
@Inject
|
||||
@SingleIn(AppScope::class)
|
||||
class StorageManager(
|
||||
@AppCoroutineScope private val scope: CoroutineScope,
|
||||
private val context: Context,
|
||||
storagePreferences: StoragePreferences,
|
||||
) {
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
private var baseDir: UniFile? = getBaseDir(storagePreferences.baseStorageDirectory.get())
|
||||
|
||||
private val _changes: Channel<Unit> = Channel(Channel.UNLIMITED)
|
||||
|
||||
@@ -2,9 +2,9 @@ package tachiyomi.presentation.widget
|
||||
|
||||
import android.content.Context
|
||||
import androidx.glance.appwidget.updateAll
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import dev.zacsweers.metro.Inject
|
||||
import eu.kanade.tachiyomi.core.security.SecurityPreferences
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
@@ -22,7 +22,7 @@ class WidgetManager(
|
||||
) {
|
||||
|
||||
context(context: Context)
|
||||
fun init(scope: LifecycleCoroutineScope) {
|
||||
fun init(scope: CoroutineScope) {
|
||||
combine(
|
||||
getUpdates.subscribe(read = false, after = BaseUpdatesGridGlanceWidget.DateLimit),
|
||||
securityPreferences.useAuthenticator.changes(),
|
||||
|
||||
Reference in New Issue
Block a user