Use app scoped CoroutineScope (#3403)

This commit is contained in:
AntsyLich
2026-06-12 17:14:29 +06:00
committed by GitHub
parent 63a71fa447
commit 509eee5dfb
17 changed files with 91 additions and 80 deletions
@@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
@@ -53,6 +54,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File import java.io.File
import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
/** /**
@@ -63,17 +65,16 @@ import kotlin.time.Duration.Companion.seconds
*/ */
class DownloadCache( class DownloadCache(
private val context: Context, private val context: Context,
private val scope: CoroutineScope,
private val provider: DownloadProvider = Injekt.get(), private val provider: DownloadProvider = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
private val extensionManager: ExtensionManager = Injekt.get(), private val extensionManager: ExtensionManager = Injekt.get(),
private val storageManager: StorageManager = Injekt.get(), private val storageManager: StorageManager = Injekt.get(),
) { ) {
private val scope = CoroutineScope(Dispatchers.IO)
private val _changes: Channel<Unit> = Channel(Channel.UNLIMITED) private val _changes: Channel<Unit> = Channel(Channel.UNLIMITED)
val changes = _changes.receiveAsFlow() val changes = _changes.receiveAsFlow()
.onStart { emit(Unit) } .onStart { emit(Unit) }
.flowOn(Dispatchers.IO)
.shareIn(scope, SharingStarted.Lazily, 1) .shareIn(scope, SharingStarted.Lazily, 1)
/** /**
@@ -101,7 +102,7 @@ class DownloadCache(
init { init {
// Attempt to read cache file // Attempt to read cache file
scope.launch { scope.launchIO {
rootDownloadsDirMutex.withLock { rootDownloadsDirMutex.withLock {
try { try {
if (diskCacheFile.exists()) { if (diskCacheFile.exists()) {
@@ -4,6 +4,7 @@ import android.content.Context
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
@@ -35,6 +36,7 @@ import uy.kohesive.injekt.api.get
*/ */
class DownloadManager( class DownloadManager(
private val context: Context, private val context: Context,
private val scope: CoroutineScope,
private val provider: DownloadProvider = Injekt.get(), private val provider: DownloadProvider = Injekt.get(),
private val cache: DownloadCache = Injekt.get(), private val cache: DownloadCache = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(), private val getCategories: GetCategories = Injekt.get(),
@@ -45,7 +47,7 @@ class DownloadManager(
/** /**
* Downloader whose only task is to download chapters. * Downloader whose only task is to download chapters.
*/ */
private val downloader = Downloader(context, provider, cache) private val downloader = Downloader(context, provider, cache, scope)
val isRunning: Boolean val isRunning: Boolean
get() = downloader.isRunning get() = downloader.isRunning
@@ -221,7 +223,7 @@ class DownloadManager(
* @param source the source of the chapters. * @param source the source of the chapters.
*/ */
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source) { fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source) {
launchIO { scope.launchIO {
val filteredChapters = getChaptersToDelete(chapters, manga) val filteredChapters = getChaptersToDelete(chapters, manga)
if (filteredChapters.isEmpty()) { if (filteredChapters.isEmpty()) {
return@launchIO return@launchIO
@@ -248,7 +250,7 @@ class DownloadManager(
* @param removeQueued whether to also remove queued downloads. * @param removeQueued whether to also remove queued downloads.
*/ */
fun deleteManga(manga: Manga, source: Source, removeQueued: Boolean = true) { fun deleteManga(manga: Manga, source: Source, removeQueued: Boolean = true) {
launchIO { scope.launchIO {
if (removeQueued) { if (removeQueued) {
downloader.removeFromQueue(manga) downloader.removeFromQueue(manga)
} }
@@ -44,7 +44,6 @@ import okhttp3.Response
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.storage.extension import tachiyomi.core.common.storage.extension
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.lang.launchNow
import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.system.ImageUtil import tachiyomi.core.common.util.system.ImageUtil
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
@@ -71,6 +70,7 @@ class Downloader(
private val context: Context, private val context: Context,
private val provider: DownloadProvider, private val provider: DownloadProvider,
private val cache: DownloadCache, private val cache: DownloadCache,
private val scope: CoroutineScope,
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
private val chapterCache: ChapterCache = Injekt.get(), private val chapterCache: ChapterCache = Injekt.get(),
private val downloadPreferences: DownloadPreferences = Injekt.get(), private val downloadPreferences: DownloadPreferences = Injekt.get(),
@@ -95,7 +95,6 @@ class Downloader(
*/ */
private val notifier by lazy { DownloadNotifier(context) } private val notifier by lazy { DownloadNotifier(context) }
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private var downloaderJob: Job? = null private var downloaderJob: Job? = null
/** /**
@@ -111,7 +110,7 @@ class Downloader(
var isPaused: Boolean = false var isPaused: Boolean = false
init { init {
launchNow { scope.launch {
val chapters = async { store.restore() } val chapters = async { store.restore() }
addAllToQueue(chapters.await()) addAllToQueue(chapters.await())
} }
@@ -190,7 +189,7 @@ class Downloader(
private fun launchDownloaderJob() { private fun launchDownloaderJob() {
if (isRunning) return if (isRunning) return
downloaderJob = scope.launch { downloaderJob = scope.launchIO {
val activeDownloadsFlow = combine( val activeDownloadsFlow = combine(
queueState, queueState,
downloadPreferences.parallelSourceLimit.changes(), downloadPreferences.parallelSourceLimit.changes(),
@@ -28,6 +28,8 @@ import eu.kanade.tachiyomi.util.system.cancelNotification
import eu.kanade.tachiyomi.util.system.getBitmapOrNull import eu.kanade.tachiyomi.util.system.getBitmapOrNull
import eu.kanade.tachiyomi.util.system.notificationBuilder import eu.kanade.tachiyomi.util.system.notificationBuilder
import eu.kanade.tachiyomi.util.system.notify import eu.kanade.tachiyomi.util.system.notify
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import tachiyomi.core.common.Constants import tachiyomi.core.common.Constants
import tachiyomi.core.common.i18n.pluralStringResource import tachiyomi.core.common.i18n.pluralStringResource
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
@@ -44,7 +46,7 @@ import java.text.NumberFormat
class LibraryUpdateNotifier( class LibraryUpdateNotifier(
private val context: Context, private val context: Context,
private val scope: CoroutineScope = Injekt.get(),
private val securityPreferences: SecurityPreferences = Injekt.get(), private val securityPreferences: SecurityPreferences = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
) { ) {
@@ -209,7 +211,7 @@ class LibraryUpdateNotifier(
// Per-manga notification // Per-manga notification
if (!securityPreferences.hideNotificationContent.get()) { if (!securityPreferences.hideNotificationContent.get()) {
launchUI { scope.launch {
context.notify( context.notify(
updates.map { (manga, chapters) -> updates.map { (manga, chapters) ->
NotificationManagerCompat.NotificationWithIdAndTag( NotificationManagerCompat.NotificationWithIdAndTag(
@@ -6,6 +6,8 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
@@ -17,6 +19,10 @@ import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat
import eu.kanade.tachiyomi.util.system.notificationManager import eu.kanade.tachiyomi.util.system.notificationManager
import eu.kanade.tachiyomi.util.system.toShareIntent import eu.kanade.tachiyomi.util.system.toShareIntent
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import tachiyomi.core.common.Constants import tachiyomi.core.common.Constants
import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchIO
@@ -32,6 +38,8 @@ import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
/** /**
@@ -45,6 +53,7 @@ class NotificationReceiver : BroadcastReceiver() {
private val getChapter: GetChapter by injectLazy() private val getChapter: GetChapter by injectLazy()
private val updateChapter: UpdateChapter by injectLazy() private val updateChapter: UpdateChapter by injectLazy()
private val downloadManager: DownloadManager by injectLazy() private val downloadManager: DownloadManager by injectLazy()
private val scope: CoroutineScope by injectLazy()
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
when (intent.action) { when (intent.action) {
@@ -197,7 +206,7 @@ class NotificationReceiver : BroadcastReceiver() {
val downloadPreferences: DownloadPreferences = Injekt.get() val downloadPreferences: DownloadPreferences = Injekt.get()
val sourceManager: SourceManager = Injekt.get() val sourceManager: SourceManager = Injekt.get()
launchIO { async(scope, Dispatchers.IO) {
val toUpdate = chapterUrls.mapNotNull { getChapter.await(it, mangaId) } val toUpdate = chapterUrls.mapNotNull { getChapter.await(it, mangaId) }
.map { .map {
val chapter = it.copy(read = true) val chapter = it.copy(read = true)
@@ -223,13 +232,27 @@ class NotificationReceiver : BroadcastReceiver() {
* @param mangaId id of manga * @param mangaId id of manga
*/ */
private fun downloadChapters(chapterUrls: Array<String>, mangaId: Long) { private fun downloadChapters(chapterUrls: Array<String>, mangaId: Long) {
launchIO { async(scope, Dispatchers.IO) {
val manga = getManga.await(mangaId) ?: return@launchIO val manga = getManga.await(mangaId) ?: return@async
val chapters = chapterUrls.mapNotNull { getChapter.await(it, mangaId) } val chapters = chapterUrls.mapNotNull { getChapter.await(it, mangaId) }
downloadManager.downloadChapters(manga, chapters) downloadManager.downloadChapters(manga, chapters)
} }
} }
private fun BroadcastReceiver.async(
scope: CoroutineScope,
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit,
) {
val result = goAsync()
try {
scope.launch(context, start, block)
} finally {
result.finish()
}
}
companion object { companion object {
private const val NAME = "NotificationReceiver" private const val NAME = "NotificationReceiver"
@@ -2,6 +2,8 @@ package eu.kanade.tachiyomi.di
import android.app.Application import android.app.Application
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.sqlite.driver.bundled.BundledSQLiteDriver import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.db.SqlDriver
import com.eygraber.sqldelight.androidx.driver.AndroidxSqliteConfiguration import com.eygraber.sqldelight.androidx.driver.AndroidxSqliteConfiguration
@@ -20,6 +22,9 @@ import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.network.JavaScriptEngine import eu.kanade.tachiyomi.network.JavaScriptEngine
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.AndroidSourceManager import eu.kanade.tachiyomi.source.AndroidSourceManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.plus
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.protobuf.ProtoBuf import kotlinx.serialization.protobuf.ProtoBuf
import nl.adaptivity.xmlutil.XmlDeclMode import nl.adaptivity.xmlutil.XmlDeclMode
@@ -101,18 +106,20 @@ class AppModule(val app: Application) : InjektModule {
ProtoBuf ProtoBuf
} }
addSingletonFactory<CoroutineScope> { ProcessLifecycleOwner.get().lifecycleScope + SupervisorJob() }
addSingletonFactory { ChapterCache(app, get()) } addSingletonFactory { ChapterCache(app, get()) }
addSingletonFactory { CoverCache(app) } addSingletonFactory { CoverCache(app) }
addSingletonFactory { NetworkHelper(app, get()) } addSingletonFactory { NetworkHelper(app, get(), get()) }
addSingletonFactory { JavaScriptEngine(app) } addSingletonFactory { JavaScriptEngine(app) }
addSingletonFactory<SourceManager> { AndroidSourceManager(app, get(), get()) } addSingletonFactory<SourceManager> { AndroidSourceManager(app, get(), get(), get()) }
addSingletonFactory { ExtensionManager(app) } addSingletonFactory { ExtensionManager(app, get()) }
addSingletonFactory { DownloadProvider(app) } addSingletonFactory { DownloadProvider(app) }
addSingletonFactory { DownloadManager(app) } addSingletonFactory { DownloadManager(app, get()) }
addSingletonFactory { DownloadCache(app) } addSingletonFactory { DownloadCache(app, get()) }
addSingletonFactory { TrackerManager() } addSingletonFactory { TrackerManager() }
addSingletonFactory { DelayedTrackingStore(app) } addSingletonFactory { DelayedTrackingStore(app) }
@@ -122,7 +129,7 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory { AndroidStorageFolderProvider(app) } addSingletonFactory { AndroidStorageFolderProvider(app) }
addSingletonFactory { LocalSourceFileSystem(get()) } addSingletonFactory { LocalSourceFileSystem(get()) }
addSingletonFactory { LocalCoverManager(app, get()) } addSingletonFactory { LocalCoverManager(app, get()) }
addSingletonFactory { StorageManager(app, get()) } addSingletonFactory { StorageManager(app, get(), get()) }
// Asynchronously init expensive components for a faster cold start // Asynchronously init expensive components for a faster cold start
ContextCompat.getMainExecutor(app).execute { ContextCompat.getMainExecutor(app).execute {
@@ -41,12 +41,10 @@ import java.util.Locale
*/ */
class ExtensionManager( class ExtensionManager(
private val context: Context, private val context: Context,
private val scope: CoroutineScope,
private val preferences: SourcePreferences = Injekt.get(), private val preferences: SourcePreferences = Injekt.get(),
private val trustExtension: TrustExtension = Injekt.get(), private val trustExtension: TrustExtension = Injekt.get(),
) { ) {
val scope = CoroutineScope(SupervisorJob())
private val _isInitialized = MutableStateFlow(false) private val _isInitialized = MutableStateFlow(false)
val isInitialized: StateFlow<Boolean> = _isInitialized.asStateFlow() val isInitialized: StateFlow<Boolean> = _isInitialized.asStateFlow()
@@ -58,7 +56,7 @@ class ExtensionManager(
/** /**
* The installer which installs, updates and uninstalls the extensions. * The installer which installs, updates and uninstalls the extensions.
*/ */
private val installer by lazy { ExtensionInstaller(context) } private val installer by lazy { ExtensionInstaller(context, scope) }
private val iconMap = mutableMapOf<String, Drawable>() private val iconMap = mutableMapOf<String, Drawable>()
@@ -22,6 +22,7 @@ import kotlinx.coroutines.launch
import logcat.LogPriority import logcat.LogPriority
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.system.logcat import tachiyomi.core.common.util.system.logcat
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@@ -34,9 +35,8 @@ import java.io.File
*/ */
internal class ExtensionInstaller( internal class ExtensionInstaller(
private val context: Context, private val context: Context,
private val scope: CoroutineScope,
) { ) {
private val scope = CoroutineScope(Dispatchers.IO)
private val activeJobs = mutableMapOf<String, Job>() private val activeJobs = mutableMapOf<String, Job>()
private val activeSteps = mutableMapOf<Long, MutableStateFlow<InstallStep>>() private val activeSteps = mutableMapOf<Long, MutableStateFlow<InstallStep>>()
private val extensionInstaller = Injekt.get<BasePreferences>().extensionInstaller private val extensionInstaller = Injekt.get<BasePreferences>().extensionInstaller
@@ -57,7 +57,7 @@ internal class ExtensionInstaller(
val step = MutableStateFlow(InstallStep.Pending) val step = MutableStateFlow(InstallStep.Pending)
activeSteps[downloadId] = step activeSteps[downloadId] = step
val job = scope.launch { val job = scope.launchIO {
val tmpFile = File(context.cacheDir, "extension_${extension.pkgName}.apk") val tmpFile = File(context.cacheDir, "extension_${extension.pkgName}.apk")
try { try {
step.value = InstallStep.Downloading step.value = InstallStep.Downloading
@@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.domain.source.model.StubSource import tachiyomi.domain.source.model.StubSource
import tachiyomi.domain.source.repository.StubSourceRepository import tachiyomi.domain.source.repository.StubSourceRepository
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
@@ -26,6 +27,7 @@ import java.util.concurrent.ConcurrentHashMap
class AndroidSourceManager( class AndroidSourceManager(
private val context: Context, private val context: Context,
private val scope: CoroutineScope,
private val extensionManager: ExtensionManager, private val extensionManager: ExtensionManager,
private val sourceRepository: StubSourceRepository, private val sourceRepository: StubSourceRepository,
) : SourceManager { ) : SourceManager {
@@ -35,8 +37,6 @@ class AndroidSourceManager(
private val downloadManager: DownloadManager by injectLazy() private val downloadManager: DownloadManager by injectLazy()
private val scope = CoroutineScope(Job() + Dispatchers.IO)
private val sourcesMapFlow = MutableStateFlow(ConcurrentHashMap<Long, Source>()) private val sourcesMapFlow = MutableStateFlow(ConcurrentHashMap<Long, Source>())
private val stubSourcesMap = ConcurrentHashMap<Long, StubSource>() private val stubSourcesMap = ConcurrentHashMap<Long, StubSource>()
@@ -46,7 +46,7 @@ class AndroidSourceManager(
} }
init { init {
scope.launch { scope.launchIO {
extensionManager.installedExtensionsFlow extensionManager.installedExtensionsFlow
.collectLatest { extensions -> .collectLatest { extensions ->
val mutableMap = ConcurrentHashMap<Long, Source>( val mutableMap = ConcurrentHashMap<Long, Source>(
@@ -69,7 +69,7 @@ class AndroidSourceManager(
} }
} }
scope.launch { scope.launchIO {
sourceRepository.subscribeAll() sourceRepository.subscribeAll()
.collectLatest { sources -> .collectLatest { sources ->
val mutableMap = stubSourcesMap.toMutableMap() val mutableMap = stubSourcesMap.toMutableMap()
@@ -100,9 +100,9 @@ class AndroidSourceManager(
} }
private fun registerStubSource(source: StubSource) { private fun registerStubSource(source: StubSource) {
scope.launch { scope.launchIO {
val dbSource = sourceRepository.getStubSource(source.id) val dbSource = sourceRepository.getStubSource(source.id)
if (dbSource == source) return@launch if (dbSource == source) return@launchIO
sourceRepository.upsertStubSource(source.id, source.lang, source.name) sourceRepository.upsertStubSource(source.id, source.lang, source.name)
if (dbSource != null) { if (dbSource != null) {
downloadManager.renameSource(dbSource, source) downloadManager.renameSource(dbSource, source)
@@ -286,7 +286,7 @@ class ReaderViewModel @JvmOverloads constructor(
val context = Injekt.get<Application>() val context = Injekt.get<Application>()
val source = sourceManager.getOrStub(manga.source) val source = sourceManager.getOrStub(manga.source)
loader = ChapterLoader(context, downloadManager, downloadProvider, manga, source) loader = ChapterLoader(context, viewModelScope, downloadManager, downloadProvider, manga, source)
loadChapter(loader!!, chapterList.first { chapterId == it.chapter.id }) loadChapter(loader!!, chapterList.first { chapterId == it.chapter.id })
Result.success(true) Result.success(true)
@@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.data.download.DownloadProvider
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import kotlinx.coroutines.CoroutineScope
import mihon.core.archive.archiveReader import mihon.core.archive.archiveReader
import mihon.core.archive.epubReader import mihon.core.archive.epubReader
import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.i18n.stringResource
@@ -22,6 +23,7 @@ import tachiyomi.source.local.io.Format
*/ */
class ChapterLoader( class ChapterLoader(
private val context: Context, private val context: Context,
private val scope: CoroutineScope,
private val downloadManager: DownloadManager, private val downloadManager: DownloadManager,
private val downloadProvider: DownloadProvider, private val downloadProvider: DownloadProvider,
private val manga: Manga, private val manga: Manga,
@@ -100,7 +102,7 @@ class ChapterLoader(
is Format.Epub -> EpubPageLoader(format.file.epubReader(context)) is Format.Epub -> EpubPageLoader(format.file.epubReader(context))
} }
} }
source is HttpSource -> HttpPageLoader(chapter, source) source is HttpSource -> HttpPageLoader(chapter, source, scope)
source is StubSource -> error(context.stringResource(MR.strings.source_not_installed, source.toString())) source is StubSource -> error(context.stringResource(MR.strings.source_not_installed, source.toString()))
else -> error(context.stringResource(MR.strings.loader_not_implemented_error)) else -> error(context.stringResource(MR.strings.loader_not_implemented_error))
} }
@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.ui.reader.loader package eu.kanade.tachiyomi.ui.reader.loader
import android.provider.Settings
import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.database.models.toDomainChapter import eu.kanade.tachiyomi.data.database.models.toDomainChapter
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
@@ -8,7 +9,9 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
@@ -31,11 +34,10 @@ import kotlin.math.min
internal class HttpPageLoader( internal class HttpPageLoader(
private val chapter: ReaderChapter, private val chapter: ReaderChapter,
private val source: HttpSource, private val source: HttpSource,
scope: CoroutineScope,
private val chapterCache: ChapterCache = Injekt.get(), private val chapterCache: ChapterCache = Injekt.get(),
) : PageLoader() { ) : PageLoader() {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
/** /**
* A queue used to manage requests one by one while allowing priorities. * A queue used to manage requests one by one while allowing priorities.
*/ */
@@ -124,14 +126,14 @@ internal class HttpPageLoader(
queue.offer(PriorityPage(page, PriorityPage.RETRY)) queue.offer(PriorityPage(page, PriorityPage.RETRY))
} }
@OptIn(DelicateCoroutinesApi::class)
override fun recycle() { override fun recycle() {
super.recycle() super.recycle()
scope.cancel()
queue.clear() queue.clear()
// Cache current page list progress for online chapters to allow a faster reopen // Cache current page list progress for online chapters to allow a faster reopen
chapter.pages?.let { pages -> chapter.pages?.let { pages ->
launchIO { GlobalScope.launchIO {
try { try {
// Convert to pages without reader information // Convert to pages without reader information
val pagesToSave = pages.map { Page(it.index, it.url, it.imageUrl) } val pagesToSave = pages.map { Page(it.index, it.url, it.imageUrl) }
@@ -5,6 +5,7 @@ import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
import eu.kanade.tachiyomi.network.interceptor.IgnoreGzipInterceptor import eu.kanade.tachiyomi.network.interceptor.IgnoreGzipInterceptor
import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
import kotlinx.coroutines.CoroutineScope
import okhttp3.Cache import okhttp3.Cache
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.brotli.BrotliInterceptor import okhttp3.brotli.BrotliInterceptor
@@ -15,6 +16,7 @@ import java.util.concurrent.TimeUnit
class NetworkHelper( class NetworkHelper(
private val context: Context, private val context: Context,
private val preferences: NetworkPreferences, private val preferences: NetworkPreferences,
scope: CoroutineScope,
) { ) {
val cookieJar = AndroidCookieJar() val cookieJar = AndroidCookieJar()
@@ -64,7 +66,7 @@ class NetworkHelper(
val client = clientBuilder val client = clientBuilder
.addInterceptor( .addInterceptor(
CloudflareInterceptor(context, cookieJar, ::defaultUserAgentProvider), CloudflareInterceptor(context, cookieJar, scope, ::defaultUserAgentProvider),
) )
.build() .build()
@@ -11,6 +11,7 @@ import androidx.core.content.ContextCompat
import eu.kanade.tachiyomi.network.AndroidCookieJar import eu.kanade.tachiyomi.network.AndroidCookieJar
import eu.kanade.tachiyomi.util.system.isOutdated import eu.kanade.tachiyomi.util.system.isOutdated
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.CoroutineScope
import okhttp3.Cookie import okhttp3.Cookie
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor import okhttp3.Interceptor
@@ -25,8 +26,9 @@ import java.util.concurrent.CountDownLatch
class CloudflareInterceptor( class CloudflareInterceptor(
private val context: Context, private val context: Context,
private val cookieManager: AndroidCookieJar, private val cookieManager: AndroidCookieJar,
scope: CoroutineScope,
defaultUserAgentProvider: () -> String, defaultUserAgentProvider: () -> String,
) : WebViewInterceptor(context, defaultUserAgentProvider) { ) : WebViewInterceptor(context, scope, defaultUserAgentProvider) {
private val executor = ContextCompat.getMainExecutor(context) private val executor = ContextCompat.getMainExecutor(context)
@@ -9,6 +9,8 @@ import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.WebViewUtil import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.setDefaultSettings import eu.kanade.tachiyomi.util.system.setDefaultSettings
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import okhttp3.Headers import okhttp3.Headers
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Request import okhttp3.Request
@@ -21,6 +23,7 @@ import java.util.concurrent.TimeUnit
abstract class WebViewInterceptor( abstract class WebViewInterceptor(
private val context: Context, private val context: Context,
private val scope: CoroutineScope,
private val defaultUserAgentProvider: () -> String, private val defaultUserAgentProvider: () -> String,
) : Interceptor { ) : Interceptor {
@@ -56,7 +59,7 @@ abstract class WebViewInterceptor(
} }
if (!WebViewUtil.supportsWebView(context)) { if (!WebViewUtil.supportsWebView(context)) {
launchUI { scope.launch {
context.toast(MR.strings.information_webview_required, Toast.LENGTH_LONG) context.toast(MR.strings.information_webview_required, Toast.LENGTH_LONG)
} }
return response return response
@@ -10,39 +10,6 @@ import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext 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 = fun CoroutineScope.launchUI(block: suspend CoroutineScope.() -> Unit): Job =
launch(Dispatchers.Main, block = block) launch(Dispatchers.Main, block = block)
@@ -10,6 +10,7 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
@@ -17,15 +18,14 @@ import kotlinx.coroutines.flow.shareIn
class StorageManager( class StorageManager(
private val context: Context, private val context: Context,
scope: CoroutineScope,
storagePreferences: StoragePreferences, storagePreferences: StoragePreferences,
) { ) {
private val scope = CoroutineScope(Dispatchers.IO)
private var baseDir: UniFile? = getBaseDir(storagePreferences.baseStorageDirectory.get()) private var baseDir: UniFile? = getBaseDir(storagePreferences.baseStorageDirectory.get())
private val _changes: Channel<Unit> = Channel(Channel.UNLIMITED) private val _changes: Channel<Unit> = Channel(Channel.UNLIMITED)
val changes = _changes.receiveAsFlow() val changes = _changes.receiveAsFlow()
.flowOn(Dispatchers.IO)
.shareIn(scope, SharingStarted.Lazily, 1) .shareIn(scope, SharingStarted.Lazily, 1)
init { init {
@@ -43,6 +43,7 @@ class StorageManager(
} }
_changes.send(Unit) _changes.send(Unit)
} }
.flowOn(Dispatchers.IO)
.launchIn(scope) .launchIn(scope)
} }