diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index b74c6e70a..4e4ef4728 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/App.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt @@ -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 + @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 -> diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt index 9ce4ecd3e..ae4dc458a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt @@ -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 = Channel(Channel.UNLIMITED) val changes = _changes.receiveAsFlow() .onStart { emit(Unit) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt index 24acb2458..3563e40de 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt @@ -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, 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) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt index 94de0860e..5a34f4806 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt @@ -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>(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()) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt index 287250e09..0e8940311 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateNotifier.kt @@ -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( diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt index 6ce865e30..552b2d47f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt @@ -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, 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, 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" diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt index 5b126a5e0..d8b699e2d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt @@ -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() private val iconMap = mutableMapOf() @@ -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( diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt index 5a6604c01..7c3d27c14 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt @@ -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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt index 453fd2223..ef1a819ff 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt @@ -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() private val activeSteps = mutableMapOf>() private val extensionInstaller = basePreferences.extensionInstaller diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt b/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt index c1aaaf273..bf2d3b08d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/AndroidSourceManager.kt @@ -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, ) : SourceManager { - private val scope = CoroutineScope(Job() + Dispatchers.IO) - /** * Null until the extensions have loaded, so that nothing observes the empty seed value. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt index 95f5e39e0..34802f989 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt @@ -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) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt index 5fd1cc519..519a9d3da 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt @@ -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)) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt index 1dfdaa0d1..9df554339 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/HttpPageLoader.kt @@ -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) } diff --git a/app/src/main/java/mihon/app/di/AppBindings.kt b/app/src/main/java/mihon/app/di/AppBindings.kt index 9274a6580..f6a9751b1 100644 --- a/app/src/main/java/mihon/app/di/AppBindings.kt +++ b/app/src/main/java/mihon/app/di/AppBindings.kt @@ -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 { diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt index 21357b1cc..8eea836e7 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt @@ -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() diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt index bc43fb52e..7cc97fb01 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt @@ -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) diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt index 547459d72..3187025a5 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/WebViewInterceptor.kt @@ -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 diff --git a/core/common/src/main/kotlin/tachiyomi/core/common/util/lang/CoroutinesExtensions.kt b/core/common/src/main/kotlin/tachiyomi/core/common/util/lang/CoroutinesExtensions.kt index a76bff2cc..07421fc25 100644 --- a/core/common/src/main/kotlin/tachiyomi/core/common/util/lang/CoroutinesExtensions.kt +++ b/core/common/src/main/kotlin/tachiyomi/core/common/util/lang/CoroutinesExtensions.kt @@ -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) diff --git a/core/metro/src/main/kotlin/mihon/core/metro/AppCoroutineScope.kt b/core/metro/src/main/kotlin/mihon/core/metro/AppCoroutineScope.kt new file mode 100644 index 000000000..678e56919 --- /dev/null +++ b/core/metro/src/main/kotlin/mihon/core/metro/AppCoroutineScope.kt @@ -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 diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts index 005fe14b0..4bb1f3047 100644 --- a/domain/build.gradle.kts +++ b/domain/build.gradle.kts @@ -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) diff --git a/domain/src/main/java/tachiyomi/domain/storage/service/StorageManager.kt b/domain/src/main/java/tachiyomi/domain/storage/service/StorageManager.kt index 457013e1f..a7b2d97e7 100644 --- a/domain/src/main/java/tachiyomi/domain/storage/service/StorageManager.kt +++ b/domain/src/main/java/tachiyomi/domain/storage/service/StorageManager.kt @@ -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 = Channel(Channel.UNLIMITED) diff --git a/presentation-widget/src/main/java/tachiyomi/presentation/widget/WidgetManager.kt b/presentation-widget/src/main/java/tachiyomi/presentation/widget/WidgetManager.kt index 442d92497..e9aebcb2b 100644 --- a/presentation-widget/src/main/java/tachiyomi/presentation/widget/WidgetManager.kt +++ b/presentation-widget/src/main/java/tachiyomi/presentation/widget/WidgetManager.kt @@ -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(),