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
@@ -7,7 +7,7 @@ import okhttp3.HttpUrl
class AndroidCookieJar : CookieJar {
private val manager = CookieManager.getInstance()
private val manager by lazy { CookieManager.getInstance() }
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
val urlString = url.toString()
@@ -5,7 +5,8 @@ import app.cash.quickjs.QuickJs
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.Inject
import dev.zacsweers.metro.SingleIn
import tachiyomi.core.common.util.lang.withIOContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
* Util for evaluating JavaScript in sources.
@@ -23,7 +24,7 @@ class JavaScriptEngine(context: Context) {
* @param script JavaScript to execute.
* @return Result of JavaScript code as a primitive type.
*/
suspend fun <T> evaluate(script: String): T = withIOContext {
suspend fun <T> evaluate(script: String): T = withContext(Dispatchers.Default) {
QuickJs.create().use {
it.evaluate(script) as T
}
@@ -26,7 +26,7 @@ class NetworkHelper(
val cookieJar = AndroidCookieJar()
private val clientBuilder: OkHttpClient.Builder = run {
private fun clientBuilder(): OkHttpClient.Builder = run {
val builder = OkHttpClient.Builder()
.cookieJar(cookieJar)
.connectTimeout(30.seconds)
@@ -65,18 +65,20 @@ class NetworkHelper(
}
}
val client = clientBuilder
.addInterceptor(
CloudflareInterceptor(context, cookieJar, scope, ::defaultUserAgentProvider),
)
.build()
val client: OkHttpClient by lazy {
clientBuilder()
.addInterceptor(
CloudflareInterceptor(context, cookieJar, scope, ::defaultUserAgentProvider),
)
.build()
}
/**
* @deprecated Since extension-lib 1.5
*/
@Deprecated("The regular client handles Cloudflare by default")
@Suppress("UNUSED")
val cloudflareClient: OkHttpClient = client
val cloudflareClient: OkHttpClient get() = client
fun defaultUserAgentProvider() = preferences.defaultUserAgent.get().trim()
}
@@ -11,11 +11,12 @@ 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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import okhttp3.Headers
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
import tachiyomi.core.common.util.lang.launchUI
import tachiyomi.i18n.MR
import java.util.Locale
import java.util.concurrent.CountDownLatch
@@ -59,7 +60,7 @@ abstract class WebViewInterceptor(
}
if (!WebViewUtil.supportsWebView(context)) {
scope.launchUI {
scope.launch(Dispatchers.Main) {
context.toast(MR.strings.information_webview_required, Toast.LENGTH_LONG)
}
return response
@@ -1,30 +0,0 @@
package tachiyomi.core.common.util.lang
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
fun CoroutineScope.launchUI(block: suspend CoroutineScope.() -> Unit): Job =
launch(Dispatchers.Main, block = block)
fun CoroutineScope.launchIO(block: suspend CoroutineScope.() -> Unit): Job =
launch(Dispatchers.IO, block = block)
fun CoroutineScope.launchNonCancellable(block: suspend CoroutineScope.() -> Unit): Job =
launchIO { withContext(NonCancellable, block) }
suspend fun <T> withUIContext(block: suspend CoroutineScope.() -> T) = withContext(
Dispatchers.Main,
block,
)
suspend fun <T> withIOContext(block: suspend CoroutineScope.() -> T) = withContext(
Dispatchers.IO,
block,
)
suspend fun <T> withNonCancellableContext(block: suspend CoroutineScope.() -> T) =
withContext(NonCancellable, block)