Move blocking work off the main thread and fix threading races (#4068)

* Defer startup work until first use

* Move cover work off the main thread

* Make tracker calls main-safe and token refresh thread-safe

Kitsu, Shikimori and MangaBaka refresh their token under a lock like
MAL, so parallel calls no longer refresh it twice.

* Fix extension threading and make installs cancellable

The store service read response bodies on Main, which failed with
NetworkOnMainThreadException for http stores.

* Move reader work off the main thread and save progress in order

* Serialize the downloader and fix download threading

* Stop blocking on WorkManager status queries

Also removes NotificationReceiver's open chapter action: nothing sends
it, and its handler used runBlocking.

* Start migration jobs on the migrator's dispatcher

They started undispatched, so each ran on the caller's thread until its
first suspension.

* Run screen and view model work on fitting dispatchers

* Launch work that outlives a screen on the app scope

A launch on another dispatcher is dropped if the screen closes before it
starts, which dialogs that dismiss right away often do. Only the UI
follow-up stays on viewModelScope.

* Remove the coroutine helper extensions

They hid which dispatcher and scope a call used, and the GlobalScope
ones hid that it was global.
This commit is contained in:
AntsyLich
2026-10-10 17:08:49 +00:00
committed by GitHub
parent f9c5fa9011
commit c46317f099
131 changed files with 1612 additions and 1274 deletions
@@ -15,9 +15,11 @@ import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.SMangaUpdate
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.withContext
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.atStartOfDayIn
@@ -31,7 +33,6 @@ import nl.adaptivity.xmlutil.serialization.XML
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.storage.extension
import tachiyomi.core.common.storage.nameWithoutExtension
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.system.ImageUtil
import tachiyomi.core.common.util.system.logcat
import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE
@@ -86,7 +87,9 @@ class LocalSource(
override suspend fun getLatestUpdates(page: Int) = getSearchManga(page, "", LatestFilters)
override suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage = withIOContext {
override suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage = withContext(
Dispatchers.IO,
) {
val lastModifiedLimit = if (filters === LatestFilters) {
System.currentTimeMillis() - LATEST_THRESHOLD
} else {
@@ -160,7 +163,7 @@ class LocalSource(
}
// Manga details related
private suspend fun getMangaDetails(manga: SManga): SManga = withIOContext {
private suspend fun getMangaDetails(manga: SManga): SManga = withContext(Dispatchers.IO) {
coverManager.find(manga.url)?.let {
manga.thumbnail_url = it.uri.toString()
}
@@ -224,7 +227,7 @@ class LocalSource(
logcat(LogPriority.ERROR, e) { "Error setting manga details from local metadata for ${manga.title}" }
}
return@withIOContext manga
return@withContext manga
}
private fun <T> getComicInfoForChapter(chapter: UniFile, block: (InputStream) -> T): T? {
@@ -287,7 +290,7 @@ class LocalSource(
}
// Chapters
private suspend fun getChapterList(manga: SManga): List<SChapter> = withIOContext {
private suspend fun getChapterList(manga: SManga): List<SChapter> = withContext(Dispatchers.IO) {
val chapters = fileSystem.getFilesInMangaDirectory(manga.url)
// Only keep supported formats
.filterNot { it.name.orEmpty().startsWith('.') }