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
@@ -2,7 +2,6 @@ package tachiyomi.domain.category.interactor
import dev.zacsweers.metro.Inject
import logcat.LogPriority
import tachiyomi.core.common.util.lang.withNonCancellableContext
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.category.model.NewCategory
import tachiyomi.domain.category.repository.CategoryRepository
@@ -20,8 +19,8 @@ class CreateCategoryWithName(
return sort.type.flag or sort.direction.flag
}
suspend fun await(name: String): Result = withNonCancellableContext {
try {
suspend fun await(name: String): Result {
return try {
categoryRepository.insert(NewCategory(name = name, flags = initialFlags))
Result.Success
} catch (e: Exception) {
@@ -2,7 +2,6 @@ package tachiyomi.domain.category.interactor
import dev.zacsweers.metro.Inject
import logcat.LogPriority
import tachiyomi.core.common.util.lang.withNonCancellableContext
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.category.model.Category
import tachiyomi.domain.category.repository.CategoryRepository
@@ -16,12 +15,12 @@ class DeleteCategory(
private val downloadPreferences: DownloadPreferences,
) {
suspend fun await(categoryId: Long) = withNonCancellableContext {
suspend fun await(categoryId: Long): Result {
try {
categoryRepository.delete(categoryId)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
return@withNonCancellableContext Result.InternalError(e)
return Result.InternalError(e)
}
val orderedIds = categoryRepository.getAll()
@@ -47,7 +46,7 @@ class DeleteCategory(
preference.set(ids.minus(categoryIdString))
}
try {
return try {
categoryRepository.updateAllOrders(orderedIds = orderedIds)
Result.Success
} catch (e: Exception) {
@@ -2,7 +2,6 @@ package tachiyomi.domain.category.interactor
import dev.zacsweers.metro.Inject
import logcat.LogPriority
import tachiyomi.core.common.util.lang.withNonCancellableContext
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.category.model.Category
import tachiyomi.domain.category.repository.CategoryRepository
@@ -12,8 +11,8 @@ class RenameCategory(
private val categoryRepository: CategoryRepository,
) {
suspend fun await(categoryId: Long, name: String) = withNonCancellableContext {
try {
suspend fun await(categoryId: Long, name: String): Result {
return try {
categoryRepository.updateName(categoryId = categoryId, name = name)
Result.Success
} catch (e: Exception) {
@@ -4,7 +4,6 @@ import dev.zacsweers.metro.Inject
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import logcat.LogPriority
import tachiyomi.core.common.util.lang.withNonCancellableContext
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.category.model.Category
import tachiyomi.domain.category.repository.CategoryRepository
@@ -15,26 +14,24 @@ class ReorderCategory(
) {
private val mutex = Mutex()
suspend fun await(category: Category, newIndex: Int) = withNonCancellableContext {
mutex.withLock {
val categories = categoryRepository.getAll()
.filterNot(Category::isSystemCategory)
.toMutableList()
suspend fun await(category: Category, newIndex: Int) = mutex.withLock {
val categories = categoryRepository.getAll()
.filterNot(Category::isSystemCategory)
.toMutableList()
val currentIndex = categories.indexOfFirst { it.id == category.id }
if (currentIndex == -1) {
return@withNonCancellableContext Result.Unchanged
}
val currentIndex = categories.indexOfFirst { it.id == category.id }
if (currentIndex == -1) {
return@withLock Result.Unchanged
}
try {
categories.add(newIndex, categories.removeAt(currentIndex))
try {
categories.add(newIndex, categories.removeAt(currentIndex))
categoryRepository.updateAllOrders(orderedIds = categories.map { it.id })
Result.Success
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
Result.InternalError(e)
}
categoryRepository.updateAllOrders(orderedIds = categories.map { it.id })
Result.Success
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
Result.InternalError(e)
}
}
@@ -1,7 +1,6 @@
package tachiyomi.domain.chapter.interactor
import dev.zacsweers.metro.Inject
import tachiyomi.core.common.util.lang.withNonCancellableContext
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.interactor.GetFavorites
import tachiyomi.domain.manga.interactor.SetMangaChapterFlags
@@ -15,15 +14,11 @@ class SetMangaDefaultChapterFlags(
) {
suspend fun await(manga: Manga) {
withNonCancellableContext {
setDefaultFlags(listOf(manga.id))
}
setDefaultFlags(listOf(manga.id))
}
suspend fun awaitAll() {
withNonCancellableContext {
setDefaultFlags(getFavorites.await().map { it.id })
}
setDefaultFlags(getFavorites.await().map { it.id })
}
private suspend fun setDefaultFlags(mangaIds: List<Long>) {
@@ -17,6 +17,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.shareIn
import mihon.core.metro.AppCoroutineScope
import java.util.concurrent.ConcurrentHashMap
@Inject
@SingleIn(AppScope::class)
@@ -26,7 +27,14 @@ class StorageManager(
storagePreferences: StoragePreferences,
) {
private var baseDir: UniFile? = getBaseDir(storagePreferences.baseStorageDirectory.get())
@Volatile
private var baseDirLazy: Lazy<UniFile?> = lazy { getBaseDir(storagePreferences.baseStorageDirectory.get()) }
private val baseDir: UniFile?
get() = baseDirLazy.value
// On SAF, createDirectory lists the whole base directory to find an existing child, so a resolved child
// is reused while it's under the current base directory and still exists; it can be deleted outside the app
private val childDirectories = ConcurrentHashMap<String, Pair<UniFile, UniFile>>()
private val _changes: Channel<Unit> = Channel(Channel.UNLIMITED)
val changes = _changes.receiveAsFlow()
@@ -37,7 +45,7 @@ class StorageManager(
.drop(1)
.distinctUntilChanged()
.onEach { uri ->
baseDir = getBaseDir(uri)
baseDirLazy = lazyOf(getBaseDir(uri))
baseDir?.let { parent ->
parent.createDirectory(AUTOMATIC_BACKUPS_PATH)
parent.createDirectory(LOCAL_SOURCE_PATH)
@@ -56,15 +64,23 @@ class StorageManager(
}
fun getAutomaticBackupsDirectory(): UniFile? {
return baseDir?.createDirectory(AUTOMATIC_BACKUPS_PATH)
return getChildDirectory(AUTOMATIC_BACKUPS_PATH)
}
fun getDownloadsDirectory(): UniFile? {
return baseDir?.createDirectory(DOWNLOADS_PATH)
return getChildDirectory(DOWNLOADS_PATH)
}
fun getLocalSourceDirectory(): UniFile? {
return baseDir?.createDirectory(LOCAL_SOURCE_PATH)
return getChildDirectory(LOCAL_SOURCE_PATH)
}
private fun getChildDirectory(name: String): UniFile? {
val parent = baseDir ?: return null
childDirectories[name]
?.takeIf { (cachedParent, directory) -> cachedParent === parent && directory.exists() }
?.let { (_, directory) -> return directory }
return parent.createDirectory(name)?.also { childDirectories[name] = parent to it }
}
}