From 9c01119d24419f076ac4007469c91b16aa3c798c Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Thu, 26 Feb 2026 18:42:17 +0600 Subject: [PATCH] Reapply "Fix thread starvation caused by not yielding or using an inappropriate thread pool (#2955)" This reverts commit 1d7c838ae64e624d9dd0884722f0c6ae5d18e386. # Conflicts: # CHANGELOG.md # app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt # app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt # app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt # app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt --- .../settings/screen/SettingsDataScreen.kt | 5 +- .../more/settings/screen/data/StorageInfo.kt | 44 ++++++++++---- .../tachiyomi/data/cache/ChapterCache.kt | 13 ++-- .../data/download/DownloadManager.kt | 4 +- .../tachiyomi/data/download/DownloadStore.kt | 6 +- .../tachiyomi/data/download/Downloader.kt | 6 +- .../data/notification/NotificationReceiver.kt | 38 +++++++----- .../tachiyomi/extension/ExtensionManager.kt | 25 ++++---- .../extension/util/ExtensionLoader.kt | 10 ++-- .../kanade/tachiyomi/ui/main/MainActivity.kt | 15 +++-- .../tachiyomi/ui/reader/ReaderActivity.kt | 60 ++++++++++++------- .../tachiyomi/ui/reader/ReaderViewModel.kt | 48 +++++++++------ .../ui/updates/UpdatesScreenModel.kt | 2 +- .../java/mihon/core/migration/Migrator.kt | 4 +- .../java/tachiyomi/data/TransactionContext.kt | 51 ++++++++++++---- .../moko-resources/base/strings.xml | 1 + 16 files changed, 209 insertions(+), 123 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt index 3c77da500..a15c8ea7c 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt @@ -303,7 +303,10 @@ object SettingsDataScreen : SearchableSettings { val chapterCache = remember { Injekt.get() } var cacheReadableSizeSema by remember { mutableIntStateOf(0) } - val cacheReadableSize = remember(cacheReadableSizeSema) { chapterCache.readableSize } + var cacheReadableSize by remember { mutableStateOf(context.stringResource(MR.strings.calculating)) } + LaunchedEffect(cacheReadableSizeSema) { + cacheReadableSize = chapterCache.getReadableSize() + } // SY --> val pagePreviewCache = remember { Injekt.get() } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/StorageInfo.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/StorageInfo.kt index 5fed6c6ef..a8578d97d 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/StorageInfo.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/StorageInfo.kt @@ -9,12 +9,18 @@ import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import eu.kanade.tachiyomi.util.storage.DiskUtil +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource @@ -45,10 +51,24 @@ private fun StorageInfo( ) { val context = LocalContext.current - val available = remember(file) { DiskUtil.getAvailableStorageSpace(file) } - val availableText = remember(available) { Formatter.formatFileSize(context, available) } - val total = remember(file) { DiskUtil.getTotalStorageSpace(file) } - val totalText = remember(total) { Formatter.formatFileSize(context, total) } + var available by remember(file) { mutableStateOf(-1L) } + var total by remember(file) { mutableStateOf(-1L) } + + LaunchedEffect(file) { + available = withContext(Dispatchers.IO) { DiskUtil.getAvailableStorageSpace(file) } + total = withContext(Dispatchers.IO) { DiskUtil.getTotalStorageSpace(file) } + } + + val availableText = if (available == -1L) { + stringResource(MR.strings.calculating) + } else { + Formatter.formatFileSize(context, available) + } + val totalText = if (total == -1L) { + stringResource(MR.strings.calculating) + } else { + Formatter.formatFileSize(context, total) + } Column( verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall), @@ -58,13 +78,15 @@ private fun StorageInfo( style = MaterialTheme.typography.header, ) - LinearProgressIndicator( - modifier = Modifier - .clip(MaterialTheme.shapes.small) - .fillMaxWidth() - .height(12.dp), - progress = { (1 - (available / total.toFloat())) }, - ) + if (total > 0) { + LinearProgressIndicator( + modifier = Modifier + .clip(MaterialTheme.shapes.small) + .fillMaxWidth() + .height(12.dp), + progress = { (1 - (available / total.toFloat())) }, + ) + } Text( text = stringResource(MR.strings.available_disk_space_info, availableText, totalText), diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt index 08ed9197c..733b4e9ca 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/cache/ChapterCache.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import logcat.LogPriority import okhttp3.Response @@ -63,17 +64,13 @@ class ChapterCache( */ private val cacheDir: File = diskCache.directory - /** - * Returns real size of directory. - */ - private val realSize: Long - get() = DiskUtil.getDirectorySize(cacheDir) - /** * Returns real size of directory in human readable format. */ - val readableSize: String - get() = Formatter.formatFileSize(context, realSize) + suspend fun getReadableSize(): String = withContext(Dispatchers.IO) { + val size = DiskUtil.getDirectorySize(cacheDir) + Formatter.formatFileSize(context, size) + } // --> EH // Cache size is in MB diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt index 806a38f42..633ba2776 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt @@ -109,10 +109,10 @@ class DownloadManager( return queueState.value.find { it.chapter.id == chapterId } } - fun startDownloadNow(chapterId: Long) { + suspend fun startDownloadNow(chapterId: Long) { val existingDownload = getQueuedDownloadOrNull(chapterId) // If not in queue try to start a new download - val toAdd = existingDownload ?: runBlocking { Download.fromChapterId(chapterId) } ?: return + val toAdd = existingDownload ?: Download.fromChapterId(chapterId) ?: return queueState.value.toMutableList().apply { existingDownload?.let { remove(it) } add(0, toAdd) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadStore.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadStore.kt index cd4d81a88..56c2bd075 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadStore.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadStore.kt @@ -89,7 +89,7 @@ class DownloadStore( /** * Returns the list of downloads to restore. It should be called in a background thread. */ - fun restore(): List { + suspend fun restore(): List { val objs = preferences.all .mapNotNull { it.value as? String } .mapNotNull { deserialize(it) } @@ -100,10 +100,10 @@ class DownloadStore( val cachedManga = mutableMapOf() for ((mangaId, chapterId) in objs) { val manga = cachedManga.getOrPut(mangaId) { - runBlocking { getManga.await(mangaId) } + getManga.await(mangaId) } ?: continue val source = sourceManager.get(manga.source) as? HttpSource ?: continue - val chapter = runBlocking { getChapter.await(chapterId) } ?: continue + val chapter = getChapter.await(chapterId) ?: continue downloads.add(Download(source, manga, chapter)) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt index 44b5a4f60..efa9ee80d 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt @@ -121,9 +121,9 @@ class Downloader( var isPaused: Boolean = false init { - launchNow { - val chapters = async { store.restore() } - addAllToQueue(chapters.await()) + scope.launch { + val chapters = store.restore() + addAllToQueue(chapters) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt index 1007e716d..87fceaa66 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt @@ -23,6 +23,7 @@ import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.runBlocking import tachiyomi.core.common.Constants import tachiyomi.core.common.util.lang.launchIO +import tachiyomi.core.common.util.lang.withUIContext import tachiyomi.domain.chapter.interactor.GetChapter import tachiyomi.domain.chapter.interactor.UpdateChapter import tachiyomi.domain.chapter.model.Chapter @@ -84,11 +85,18 @@ class NotificationReceiver : BroadcastReceiver() { ACTION_CANCEL_APP_UPDATE_DOWNLOAD -> cancelDownloadAppUpdate(context) // Open reader activity ACTION_OPEN_CHAPTER -> { - openChapter( - context, - intent.getLongExtra(EXTRA_MANGA_ID, -1), - intent.getLongExtra(EXTRA_CHAPTER_ID, -1), - ) + val pendingResult = goAsync() + launchIO { + try { + openChapter( + context, + intent.getLongExtra(EXTRA_MANGA_ID, -1), + intent.getLongExtra(EXTRA_CHAPTER_ID, -1), + ) + } finally { + pendingResult.finish() + } + } } // Mark updated manga chapters as read ACTION_MARK_AS_READ -> { @@ -153,16 +161,18 @@ class NotificationReceiver : BroadcastReceiver() { * @param mangaId id of manga * @param chapterId id of chapter */ - private fun openChapter(context: Context, mangaId: Long, chapterId: Long) { - val manga = runBlocking { getManga.await(mangaId) } - val chapter = runBlocking { getChapter.await(chapterId) } - if (manga != null && chapter != null) { - val intent = ReaderActivity.newIntent(context, manga.id, chapter.id).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + private suspend fun openChapter(context: Context, mangaId: Long, chapterId: Long) { + val manga = getManga.await(mangaId) + val chapter = getChapter.await(chapterId) + withUIContext { + if (manga != null && chapter != null) { + val intent = ReaderActivity.newIntent(context, manga.id, chapter.id).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + } + context.startActivity(intent) + } else { + context.toast(MR.strings.chapter_error) } - context.startActivity(intent) - } else { - context.toast(MR.strings.chapter_error) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt index bf6012f50..9d20bc6bd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt @@ -30,6 +30,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch import logcat.LogPriority import tachiyomi.core.common.util.lang.withUIContext import tachiyomi.core.common.util.system.logcat @@ -140,20 +141,22 @@ class ExtensionManager( * Loads and registers the installed extensions. */ private fun initExtensions() { - val extensions = ExtensionLoader.loadExtensions(context) + scope.launch { + val extensions = ExtensionLoader.loadExtensions(context) - installedExtensionMapFlow.value = extensions - .filterIsInstance() - .associate { it.extension.pkgName to it.extension } + installedExtensionMapFlow.value = extensions + .filterIsInstance() + .associate { it.extension.pkgName to it.extension } - untrustedExtensionMapFlow.value = extensions - .filterIsInstance() - .associate { it.extension.pkgName to it.extension } - // SY --> - .filterNotBlacklisted() - // SY <-- + untrustedExtensionMapFlow.value = extensions + .filterIsInstance() + .associate { it.extension.pkgName to it.extension } + // SY --> + .filterNotBlacklisted() + // SY <-- - _isInitialized.value = true + _isInitialized.value = true + } } // EXH --> diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt index 739ce15cc..a55dd22a6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt @@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.util.storage.copyAndSetReadOnlyTo import eu.kanade.tachiyomi.util.system.ChildFirstPathClassLoader import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.runBlocking import logcat.LogPriority import tachiyomi.core.common.util.system.logcat @@ -114,7 +115,7 @@ internal object ExtensionLoader { * * @param context The application context. */ - fun loadExtensions(context: Context): List { + suspend fun loadExtensions(context: Context): List { val pkgManager = context.packageManager val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @@ -160,11 +161,10 @@ internal object ExtensionLoader { if (extPkgs.isEmpty()) return emptyList() // Load each extension concurrently and wait for completion - return runBlocking { - val deferred = extPkgs.map { + return coroutineScope { + extPkgs.map { async { loadExtension(context, it) } - } - deferred.awaitAll() + }.awaitAll() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index 3f6e1e3be..f21b6c091 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -163,13 +163,6 @@ class MainActivity : BaseActivity() { super.onCreate(savedInstanceState) - val didMigration = if (isLaunch) { - addAnalytics() - Migrator.awaitAndRelease() - } else { - false - } - // Do not let the launcher create a new activity http://stackoverflow.com/questions/16283079 if (!isTaskRoot) { finish() @@ -182,6 +175,12 @@ class MainActivity : BaseActivity() { // SY <-- setComposeContent { + var didMigration by remember { mutableStateOf(null) } + LaunchedEffect(Unit) { + addAnalytics() + didMigration = Migrator.awaitAndRelease() + } + val context = LocalContext.current var incognito by remember { mutableStateOf(getIncognitoState.await(null)) } @@ -309,7 +308,7 @@ class MainActivity : BaseActivity() { } // SY <-- - var showChangelog by remember { mutableStateOf(didMigration && !BuildConfig.DEBUG) } + var showChangelog by remember { mutableStateOf(didMigration == true && !BuildConfig.DEBUG) } if (showChangelog) { // SY --> WhatsNewDialog(onDismissRequest = { showChangelog = false }) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index aa210ea18..71fc59b35 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -32,6 +32,7 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -78,6 +79,7 @@ import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.reader.ReaderViewModel.SetAsCoverResult.AddToLibraryFirst import eu.kanade.tachiyomi.ui.reader.ReaderViewModel.SetAsCoverResult.Error import eu.kanade.tachiyomi.ui.reader.ReaderViewModel.SetAsCoverResult.Success +import eu.kanade.tachiyomi.ui.reader.chapter.ReaderChapterItem import eu.kanade.tachiyomi.ui.reader.loader.HttpPageLoader import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderPage @@ -101,6 +103,8 @@ import exh.source.isEhBasedSource import exh.ui.ifSourcesLoaded import exh.util.defaultReaderType import exh.util.mangaType +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableSet @@ -121,6 +125,7 @@ import tachiyomi.core.common.i18n.pluralStringResource import tachiyomi.core.common.i18n.stringResource import tachiyomi.core.common.util.lang.launchIO import tachiyomi.core.common.util.lang.launchNonCancellable +import tachiyomi.core.common.util.lang.withIOContext import tachiyomi.core.common.util.lang.withUIContext import tachiyomi.core.common.util.system.logcat import tachiyomi.domain.source.service.SourceManager @@ -394,28 +399,36 @@ class ReaderActivity : BaseActivity() { is ReaderViewModel.Dialog.ChapterList -> { var chapters by remember { - mutableStateOf(viewModel.getChapters().toImmutableList()) + mutableStateOf?>(null) + } + LaunchedEffect(state.dialog) { + withIOContext { + chapters = viewModel.getChapters().toImmutableList() + } + } + + if (chapters != null) { + ChapterListDialog( + onDismissRequest = onDismissRequest, + screenModel = settingsScreenModel, + chapters = chapters ?: persistentListOf(), + onClickChapter = { + viewModel.loadNewChapterFromDialog(it) + onDismissRequest() + }, + onBookmark = { chapter -> + viewModel.toggleBookmark(chapter.id, !chapter.bookmark) + chapters = chapters?.map { + if (it.chapter.id == chapter.id) { + it.copy(chapter = chapter.copy(bookmark = !chapter.bookmark)) + } else { + it + } + }?.toImmutableList() + }, + state.dateRelativeTime, + ) } - ChapterListDialog( - onDismissRequest = onDismissRequest, - screenModel = settingsScreenModel, - chapters = chapters, - onClickChapter = { - viewModel.loadNewChapterFromDialog(it) - onDismissRequest() - }, - onBookmark = { chapter -> - viewModel.toggleBookmark(chapter.id, !chapter.bookmark) - chapters = chapters.map { - if (it.chapter.id == chapter.id) { - it.copy(chapter = chapter.copy(bookmark = !chapter.bookmark)) - } else { - it - } - }.toImmutableList() - }, - state.dateRelativeTime, - ) } // SY --> ReaderViewModel.Dialog.AutoScrollHelp -> AlertDialog( @@ -591,8 +604,9 @@ class ReaderActivity : BaseActivity() { } else { cropBorderContinuousVertical } - val readerBottomButtons by readerPreferences.readerBottomButtons().changes().map { it.toImmutableSet() } - .collectAsState(persistentSetOf()) + val readerBottomButtons by remember { + readerPreferences.readerBottomButtons().changes().map { it.toImmutableSet() } + }.collectAsState(persistentSetOf()) val dualPageSplitPaged by readerPreferences.dualPageSplitPaged().collectAsState() val forceHorizontalSeekbar by readerPreferences.forceHorizontalSeekbar().collectAsState() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt index 7c316d567..5970c7702 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt @@ -59,7 +59,6 @@ import exh.source.isEhBasedManga import exh.util.defaultReaderType import exh.util.mangaType import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -183,19 +182,26 @@ class ReaderViewModel @JvmOverloads constructor( private var chapterToDownload: Download? = null - private val unfilteredChapterList by lazy { - val manga = manga!! - runBlocking { getChaptersByMangaId.await(manga.id, applyScanlatorFilter = false) } + private var unfilteredChapterListCache: List? = null + private suspend fun getUnfilteredChapterList(): List { + if (unfilteredChapterListCache == null) { + val manga = manga!! + unfilteredChapterListCache = getChaptersByMangaId.await(manga.id, applyScanlatorFilter = false) + } + return unfilteredChapterListCache!! } /** * Chapter list for the active manga. It's retrieved lazily and should be accessed for the first * time in a background thread to avoid blocking the UI. */ - private val chapterList by lazy { + private var chapterListCache: List? = null + private suspend fun getChapterList(): List { + chapterListCache?.let { return it } + val manga = manga!! // SY --> - val (chapters, mangaMap) = runBlocking { + val (chapters, mangaMap) = if (manga.source == MERGED_SOURCE_ID) { getMergedChaptersByMangaId.await(manga.id, applyScanlatorFilter = true) to getMergedMangaById.await(manga.id) @@ -203,7 +209,7 @@ class ReaderViewModel @JvmOverloads constructor( } else { getChaptersByMangaId.await(manga.id, applyScanlatorFilter = true) to null } - } + fun isChapterDownloaded(chapter: Chapter): Boolean { val chapterManga = mangaMap?.get(chapter.mangaId) ?: manga return downloadManager.isChapterDownloaded( @@ -253,7 +259,7 @@ class ReaderViewModel @JvmOverloads constructor( else -> chapters } - chaptersForReader + val result = chaptersForReader .sortedWith(getChapterSort(manga, sortDescending = false)) .run { if (readerPreferences.skipDupe().get()) { @@ -271,6 +277,8 @@ class ReaderViewModel @JvmOverloads constructor( } .map { it.toDbChapter() } .map(::ReaderChapter) + chapterListCache = result + return result } private val incognitoMode: Boolean by lazy { getIncognitoState.await(manga?.source) } @@ -409,7 +417,7 @@ class ReaderViewModel @JvmOverloads constructor( loadChapter( loader!!, - chapterList.first { chapterId == it.chapter.id }, + getChapterList().first { chapterId == it.chapter.id }, /* SY --> */page, /* SY <-- */ ) Result.success(true) @@ -427,10 +435,10 @@ class ReaderViewModel @JvmOverloads constructor( } // SY --> - fun getChapters(): List { + suspend fun getChapters(): List { val currentChapter = getCurrentChapter() - return chapterList.map { + return getChapterList().map { ReaderChapterItem( chapter = it.chapter.toDomainChapter()!!, manga = manga!!, @@ -454,6 +462,7 @@ class ReaderViewModel @JvmOverloads constructor( ): ViewerChapters { loader.loadChapter(chapter /* SY --> */, page/* SY <-- */) + val chapterList = getChapterList() val chapterPos = chapterList.indexOf(chapter) val newChapters = ViewerChapters( chapter, @@ -503,7 +512,7 @@ class ReaderViewModel @JvmOverloads constructor( fun loadNewChapterFromDialog(chapter: Chapter) { viewModelScope.launchIO { - val newChapter = chapterList.firstOrNull { it.chapter.id == chapter.id } ?: return@launchIO + val newChapter = getChapterList().firstOrNull { it.chapter.id == chapter.id } ?: return@launchIO loadAdjacent(newChapter) } } @@ -665,11 +674,12 @@ class ReaderViewModel @JvmOverloads constructor( * If both conditions are satisfied enqueues chapter for delete * @param currentChapter current chapter, which is going to be marked as read. */ - private fun deleteChapterIfNeeded(currentChapter: ReaderChapter) { + private suspend fun deleteChapterIfNeeded(currentChapter: ReaderChapter) { val removeAfterReadSlots = downloadPreferences.removeAfterReadSlots().get() if (removeAfterReadSlots == -1) return // Determine which chapter should be deleted and enqueue + val chapterList = getChapterList() val currentChapterPosition = chapterList.indexOf(currentChapter) val chapterToDelete = chapterList.getOrNull(currentChapterPosition - removeAfterReadSlots) @@ -739,7 +749,7 @@ class ReaderViewModel @JvmOverloads constructor( // SY --> if (manga?.isEhBasedManga() == true) { viewModelScope.launchNonCancellable { - val chapterUpdates = unfilteredChapterList + val chapterUpdates = getUnfilteredChapterList() .filter { it.sourceOrder > readerChapter.chapter.source_order } .map { chapter -> ChapterUpdate( @@ -759,7 +769,7 @@ class ReaderViewModel @JvmOverloads constructor( .contains(LibraryPreferences.MARK_DUPLICATE_CHAPTER_READ_EXISTING) if (!markDuplicateAsRead) return - val duplicateUnreadChapters = unfilteredChapterList + val duplicateUnreadChapters = getUnfilteredChapterList() .mapNotNull { chapter -> if ( !chapter.read && @@ -774,7 +784,7 @@ class ReaderViewModel @JvmOverloads constructor( updateChapter.awaitAll(duplicateUnreadChapters) // SY --> duplicateUnreadChapters.forEach { chapterUpdate -> - val chapter = unfilteredChapterList.first { it.id == chapterUpdate.id } + val chapter = getUnfilteredChapterList().first { it.id == chapterUpdate.id } deleteChapterIfNeeded(ReaderChapter(chapter)) } // SY <-- @@ -863,9 +873,9 @@ class ReaderViewModel @JvmOverloads constructor( // SY --> fun toggleBookmark(chapterId: Long, bookmarked: Boolean) { - val chapter = chapterList.find { it.chapter.id == chapterId }?.chapter ?: return - chapter.bookmark = bookmarked viewModelScope.launchNonCancellable { + val chapter = getChapterList().find { it.chapter.id == chapterId }?.chapter ?: return@launchNonCancellable + chapter.bookmark = bookmarked updateChapter.await( ChapterUpdate( id = chapterId, @@ -900,7 +910,7 @@ class ReaderViewModel @JvmOverloads constructor( */ fun setMangaReadingMode(readingMode: ReadingMode) { val manga = manga ?: return - runBlocking(Dispatchers.IO) { + viewModelScope.launchIO { setMangaViewerFlags.awaitSetReadingMode(manga.id, readingMode.flagValue.toLong()) val currChapters = state.value.viewerChapters if (currChapters != null) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt index 4954672b5..970fc8fb4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/updates/UpdatesScreenModel.kt @@ -251,7 +251,7 @@ class UpdatesScreenModel( } } - private fun startDownloadingNow(chapterId: Long) { + private suspend fun startDownloadingNow(chapterId: Long) { downloadManager.startDownloadNow(chapterId) } diff --git a/app/src/main/java/mihon/core/migration/Migrator.kt b/app/src/main/java/mihon/core/migration/Migrator.kt index c01a3873e..2168d58a1 100644 --- a/app/src/main/java/mihon/core/migration/Migrator.kt +++ b/app/src/main/java/mihon/core/migration/Migrator.kt @@ -35,7 +35,7 @@ object Migrator { result = null } - fun awaitAndRelease(): Boolean = runBlocking { - await().also { release() } + suspend fun awaitAndRelease(): Boolean { + return await().also { release() } } } diff --git a/data/src/main/java/tachiyomi/data/TransactionContext.kt b/data/src/main/java/tachiyomi/data/TransactionContext.kt index f804a0b8e..77154696b 100644 --- a/data/src/main/java/tachiyomi/data/TransactionContext.kt +++ b/data/src/main/java/tachiyomi/data/TransactionContext.kt @@ -5,6 +5,8 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.asContextElement import kotlinx.coroutines.runBlocking import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import java.util.concurrent.RejectedExecutionException import kotlin.concurrent.atomics.AtomicInt @@ -17,6 +19,10 @@ import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.coroutineContext import kotlin.coroutines.resume +// Global mutex to serialize transaction entry and prevent thread pool exhaustion. +// If you have multiple distinct database files/handlers, this should be a property of AndroidDatabaseHandler. +private val transactionMutex = Mutex() + /** * Returns the transaction dispatcher if we are on a transaction, or the database dispatchers. */ @@ -39,20 +45,41 @@ internal suspend fun AndroidDatabaseHandler.getCurrentDatabaseContext(): Corouti * The dispatcher used to execute the given [block] will utilize threads from SQLDelight's query executor. */ internal suspend fun AndroidDatabaseHandler.withTransaction(block: suspend () -> T): T { - // Use inherited transaction context if available, this allows nested suspending transactions. - val transactionContext = - coroutineContext[TransactionElement]?.transactionDispatcher ?: createTransactionContext() - return withContext(transactionContext) { - val transactionElement = coroutineContext[TransactionElement]!! - transactionElement.acquire() - try { - db.transactionWithResult { - runBlocking(transactionContext) { - block() + val transactionElement = coroutineContext[TransactionElement] + + // If we are already in a transaction, we don't need to lock the Mutex. + // We just reuse the existing thread/context. + if (transactionElement != null) { + return withContext(transactionElement.transactionDispatcher) { + transactionElement.acquire() + try { + db.transactionWithResult { + runBlocking(transactionElement.transactionDispatcher) { + block() + } } + } finally { + transactionElement.release() + } + } + } + + // transaction: Acquire Mutex BEFORE acquiring a thread. + // This ensures we only block a real thread when we have exclusive access. + return transactionMutex.withLock { + val transactionContext = createTransactionContext() + withContext(transactionContext) { + val element = coroutineContext[TransactionElement]!! + element.acquire() + try { + db.transactionWithResult { + runBlocking(transactionContext) { + block() + } + } + } finally { + element.release() } - } finally { - transactionElement.release() } } } diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml index 341ffe8aa..9f9e75728 100755 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -170,6 +170,7 @@ Loading… + Calculating… InternalError: Check crash logs for further information