mirror of
https://github.com/mihonapp/mihon.git
synced 2026-10-23 17:09:49 -07:00
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:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-7
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user