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:
AntsyLich
2026-10-10 17:37:45 +06:00
parent e21414b229
commit f921ec07a6
22 changed files with 118 additions and 82 deletions
+5 -3
View File
@@ -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()
@@ -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)
@@ -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
+1
View File
@@ -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(),