From 8a63ca23103a1718e953d7c7ddfd022f9bed3100 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 21 Sep 2022 18:35:38 +0300 Subject: [PATCH] Fix coroutines cancellation --- .idea/kotlinc.xml | 9 ++++ .../kotatsu/base/domain/ReversibleHandle.kt | 12 ++++- .../kotatsu/core/backup/BackupRepository.kt | 7 +-- .../kotatsu/core/os/ShortcutsUpdater.kt | 25 ++++++---- .../kotatsu/details/ui/DetailsViewModel.kt | 5 +- .../details/ui/MangaDetailsDelegate.kt | 5 +- .../download/domain/DownloadManager.kt | 13 ++++- .../ui/list/FavouritesListViewModel.kt | 6 ++- .../list/ui/filter/FilterCoordinator.kt | 15 ++++-- .../local/domain/LocalMangaRepository.kt | 24 +++++++-- .../kotatsu/local/ui/LocalListViewModel.kt | 17 ++++--- .../kotatsu/reader/ui/ReaderViewModel.kt | 15 +++--- .../reader/ui/pager/PageHolderDelegate.kt | 16 ++++-- .../ui/thumbnails/adapter/PageThumbnailAD.kt | 3 +- .../remotelist/ui/RemoteListViewModel.kt | 19 ++++++- .../kotatsu/scrobbling/domain/Scrobbler.kt | 15 ++++-- .../search/domain/MangaSearchRepository.kt | 7 +-- .../kotatsu/search/ui/SearchViewModel.kt | 18 +++++-- .../search/ui/multi/MultiSearchViewModel.kt | 8 ++- .../kotatsu/settings/AppUpdateChecker.kt | 15 +++--- .../settings/HistorySettingsFragment.kt | 15 ++++-- .../settings/SourceSettingsFragment.kt | 12 ++++- .../settings/backup/BackupDialogFragment.kt | 8 +-- .../suggestions/ui/SuggestionsWorker.kt | 8 +-- .../kotatsu/tracker/work/TrackWorker.kt | 3 +- .../kotatsu/utils/PausingDispatcher.kt | 50 ------------------- .../koitharu/kotatsu/utils/ext/AndroidExt.kt | 2 +- .../kotatsu/utils/ext/ThrowableExt.kt | 28 +++++++++-- 28 files changed, 241 insertions(+), 139 deletions(-) create mode 100644 .idea/kotlinc.xml delete mode 100644 app/src/main/java/org/koitharu/kotatsu/utils/PausingDispatcher.kt diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 000000000..13639f500 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/base/domain/ReversibleHandle.kt b/app/src/main/java/org/koitharu/kotatsu/base/domain/ReversibleHandle.kt index 43c9bf7e4..e15d59958 100644 --- a/app/src/main/java/org/koitharu/kotatsu/base/domain/ReversibleHandle.kt +++ b/app/src/main/java/org/koitharu/kotatsu/base/domain/ReversibleHandle.kt @@ -1,8 +1,12 @@ package org.koitharu.kotatsu.base.domain import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.processLifecycleScope +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable fun interface ReversibleHandle { @@ -10,7 +14,13 @@ fun interface ReversibleHandle { } fun ReversibleHandle.reverseAsync() = processLifecycleScope.launch(Dispatchers.Default) { - reverse() + runCatchingCancellable { + withContext(NonCancellable) { + reverse() + } + }.onFailure { + it.printStackTraceDebug() + } } operator fun ReversibleHandle.plus(other: ReversibleHandle) = ReversibleHandle { diff --git a/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupRepository.kt index 27dd10255..88b6048f2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupRepository.kt @@ -7,6 +7,7 @@ import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.parsers.util.json.JSONIterator import org.koitharu.kotatsu.parsers.util.json.mapJSON +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable private const val PAGE_SIZE = 10 @@ -84,7 +85,7 @@ class BackupRepository(private val db: MangaDatabase) { JsonDeserializer(it).toTagEntity() } val history = JsonDeserializer(item).toHistoryEntity() - result += runCatching { + result += runCatchingCancellable { db.withTransaction { db.tagsDao.upsert(tags) db.mangaDao.upsert(manga, tags) @@ -99,7 +100,7 @@ class BackupRepository(private val db: MangaDatabase) { val result = CompositeResult() for (item in entry.data.JSONIterator()) { val category = JsonDeserializer(item).toFavouriteCategoryEntity() - result += runCatching { + result += runCatchingCancellable { db.favouriteCategoriesDao.upsert(category) } } @@ -115,7 +116,7 @@ class BackupRepository(private val db: MangaDatabase) { JsonDeserializer(it).toTagEntity() } val favourite = JsonDeserializer(item).toFavouriteEntity() - result += runCatching { + result += runCatchingCancellable { db.withTransaction { db.tagsDao.upsert(tags) db.mangaDao.upsert(manga, tags) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsUpdater.kt b/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsUpdater.kt index a201295c5..c372605d9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsUpdater.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsUpdater.kt @@ -6,6 +6,7 @@ import android.content.pm.ShortcutManager import android.media.ThumbnailUtils import android.os.Build import android.util.Size +import androidx.annotation.RequiresApi import androidx.annotation.VisibleForTesting import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat @@ -25,6 +26,7 @@ import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.processLifecycleScope import org.koitharu.kotatsu.utils.ext.requireBitmap +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable class ShortcutsUpdater( private val context: Context, @@ -37,10 +39,12 @@ class ShortcutsUpdater( private var shortcutsUpdateJob: Job? = null override fun onInvalidated(tables: MutableSet) { - val prevJob = shortcutsUpdateJob - shortcutsUpdateJob = processLifecycleScope.launch(Dispatchers.Default) { - prevJob?.join() - updateShortcutsImpl() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + val prevJob = shortcutsUpdateJob + shortcutsUpdateJob = processLifecycleScope.launch(Dispatchers.Default) { + prevJob?.join() + updateShortcutsImpl() + } } } @@ -48,7 +52,7 @@ class ShortcutsUpdater( return ShortcutManagerCompat.requestPinShortcut( context, buildShortcutInfo(manga).build(), - null + null, ) } @@ -57,7 +61,8 @@ class ShortcutsUpdater( return shortcutsUpdateJob?.join() != null } - private suspend fun updateShortcutsImpl() = runCatching { + @RequiresApi(Build.VERSION_CODES.N_MR1) + private suspend fun updateShortcutsImpl() = runCatchingCancellable { val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager val shortcuts = historyRepository.getList(0, manager.maxShortcutCountPerActivity) .filter { x -> x.title.isNotEmpty() } @@ -68,17 +73,17 @@ class ShortcutsUpdater( } private suspend fun buildShortcutInfo(manga: Manga): ShortcutInfoCompat.Builder { - val icon = runCatching { + val icon = runCatchingCancellable { val bmp = coil.execute( ImageRequest.Builder(context) .data(manga.coverUrl) .size(iconSize.width, iconSize.height) - .build() + .build(), ).requireBitmap() ThumbnailUtils.extractThumbnail(bmp, iconSize.width, iconSize.height, 0) }.fold( onSuccess = { IconCompat.createWithAdaptiveBitmap(it) }, - onFailure = { IconCompat.createWithResource(context, R.drawable.ic_shortcut_default) } + onFailure = { IconCompat.createWithResource(context, R.drawable.ic_shortcut_default) }, ) mangaRepository.storeManga(manga) return ShortcutInfoCompat.Builder(context, manga.id.toString()) @@ -87,7 +92,7 @@ class ShortcutsUpdater( .setIcon(icon) .setIntent( ReaderActivity.newIntent(context, manga.id) - .setAction(ReaderActivity.ACTION_MANGA_READ) + .setAction(ReaderActivity.ACTION_MANGA_READ), ) } diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt index a8bade779..523d40e22 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt @@ -38,6 +38,7 @@ import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.printStackTraceDebug +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable class DetailsViewModel( intent: MangaIntent, @@ -165,7 +166,7 @@ class DetailsViewModel( checkNotNull(manga) { "Cannot find saved manga for ${m.title}" } val original = localMangaRepository.getRemoteManga(manga) localMangaRepository.delete(manga) || throw IOException("Unable to delete file") - runCatching { + runCatchingCancellable { historyRepository.deleteOrSwap(manga, original) } onMangaRemoved.postCall(manga) @@ -204,7 +205,7 @@ class DetailsViewModel( reload() } else { viewModelScope.launch(Dispatchers.Default) { - runCatching { + runCatchingCancellable { localMangaRepository.getDetails(downloadedManga) }.onSuccess { delegate.relatedManga.value = it diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt index 49bbde5ce..8f0355aa5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/MangaDetailsDelegate.kt @@ -17,6 +17,7 @@ import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.utils.ext.printStackTraceDebug +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable class MangaDetailsDelegate( private val intent: MangaIntent, @@ -44,9 +45,9 @@ class MangaDetailsDelegate( val hist = historyRepository.getOne(manga) selectedBranch.value = manga.getPreferredBranch(hist) mangaData.value = manga - relatedManga.value = runCatching { + relatedManga.value = runCatchingCancellable { if (manga.source == MangaSource.LOCAL) { - val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatching null + val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatchingCancellable null MangaRepository(m.source).getDetails(m) } else { localMangaRepository.findSavedManga(manga) diff --git a/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt b/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt index 8452512be..c37c2ab00 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadManager.kt @@ -6,10 +6,18 @@ import coil.ImageLoader import coil.request.ImageRequest import coil.size.Scale import java.io.File -import kotlinx.coroutines.* +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit +import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.internal.closeQuietly @@ -28,6 +36,7 @@ import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.utils.ext.deleteAwait import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.referer +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable import org.koitharu.kotatsu.utils.progress.PausingProgressJob private const val MAX_FAILSAFE_ATTEMPTS = 2 @@ -226,7 +235,7 @@ class DownloadManager( ) } - private suspend fun loadCover(manga: Manga) = runCatching { + private suspend fun loadCover(manga: Manga) = runCatchingCancellable { imageLoader.execute( ImageRequest.Builder(context) .data(manga.coverUrl) diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt index 44f89c767..a8375ff65 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/ui/list/FavouritesListViewModel.kt @@ -22,6 +22,7 @@ import org.koitharu.kotatsu.list.ui.model.toUi import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable class FavouritesListViewModel( private val categoryId: Long, @@ -45,7 +46,7 @@ class FavouritesListViewModel( } else { repository.observeAll(categoryId) }, - createListModeFlow() + createListModeFlow(), ) { list, mode -> when { list.isEmpty() -> listOf( @@ -58,8 +59,9 @@ class FavouritesListViewModel( R.string.favourites_category_empty }, actionStringRes = 0, - ) + ), ) + else -> list.toUi(mode, this) } }.catch { diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt index 5ea361168..7f8ec8b05 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/filter/FilterCoordinator.kt @@ -6,15 +6,22 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.update import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.printStackTraceDebug +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable import java.text.Collator -import java.util.* +import java.util.Locale +import java.util.TreeSet class FilterCoordinator( private val repository: RemoteMangaRepository, @@ -152,7 +159,7 @@ class FilterCoordinator( } private fun loadTagsAsync() = coroutineScope.async(Dispatchers.Default, CoroutineStart.LAZY) { - runCatching { + runCatchingCancellable { repository.getTags() }.onFailure { error -> error.printStackTraceDebug() @@ -203,4 +210,4 @@ class FilterCoordinator( return collator?.compare(t1, t2) ?: compareValues(t1, t2) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt index e7d79dea3..ffc101543 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/domain/LocalMangaRepository.kt @@ -8,19 +8,31 @@ import androidx.collection.ArraySet import androidx.core.net.toFile import androidx.core.net.toUri import java.io.File -import java.io.IOException -import java.util.* +import java.util.Enumeration import java.util.zip.ZipEntry import java.util.zip.ZipFile import kotlin.coroutines.CoroutineContext -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.runInterruptible +import kotlinx.coroutines.withContext +import okio.IOException import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.local.data.CbzFilter import org.koitharu.kotatsu.local.data.LocalStorageManager import org.koitharu.kotatsu.local.data.MangaIndex import org.koitharu.kotatsu.local.data.TempFileFilter -import org.koitharu.kotatsu.parsers.model.* +import org.koitharu.kotatsu.parsers.model.Manga +import org.koitharu.kotatsu.parsers.model.MangaChapter +import org.koitharu.kotatsu.parsers.model.MangaPage +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.model.MangaTag +import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.util.toCamelCase import org.koitharu.kotatsu.utils.AlphanumComparator import org.koitharu.kotatsu.utils.CompositeMutex @@ -28,6 +40,7 @@ import org.koitharu.kotatsu.utils.ext.deleteAwait import org.koitharu.kotatsu.utils.ext.longHashCode import org.koitharu.kotatsu.utils.ext.readText import org.koitharu.kotatsu.utils.ext.resolveName +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable private const val MAX_PARALLELISM = 4 @@ -70,6 +83,7 @@ class LocalMangaRepository(private val storageManager: LocalStorageManager) : Ma manga.source != MangaSource.LOCAL -> requireNotNull(findSavedManga(manga)) { "Manga is not local or saved" } + else -> getFromFile(Uri.parse(manga.url).toFile()) } @@ -226,7 +240,7 @@ class LocalMangaRepository(private val storageManager: LocalStorageManager) : Ma context: CoroutineContext, ): Deferred = async(context) { runInterruptible { - runCatching { getFromFile(file) }.getOrNull() + runCatchingCancellable { getFromFile(file) }.getOrNull() } } diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt index ebd347913..fe8703130 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt @@ -3,6 +3,7 @@ package org.koitharu.kotatsu.local.ui import android.net.Uri import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow @@ -10,6 +11,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import okio.IOException import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.download.ui.service.DownloadService @@ -21,8 +23,8 @@ import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.printStackTraceDebug +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable import org.koitharu.kotatsu.utils.progress.Progress -import java.io.IOException class LocalListViewModel( private val repository: LocalMangaRepository, @@ -40,7 +42,7 @@ class LocalListViewModel( override val content = combine( mangaList, createListModeFlow(), - listError + listError, ) { list, mode, error -> when { error != null -> listOf(error.toErrorState(canRetry = true)) @@ -51,8 +53,9 @@ class LocalListViewModel( textPrimary = R.string.text_local_holder_primary, textSecondary = R.string.text_local_holder_secondary, actionStringRes = R.string._import, - ) + ), ) + else -> ArrayList(list.size + 1).apply { add(headerModel) list.toUi(this, mode) @@ -60,7 +63,7 @@ class LocalListViewModel( } }.asLiveDataDistinct( viewModelScope.coroutineContext + Dispatchers.Default, - listOf(LoadingState) + listOf(LoadingState), ) init { @@ -97,7 +100,7 @@ class LocalListViewModel( for (manga in itemsToRemove) { val original = repository.getRemoteManga(manga) repository.delete(manga) || throw IOException("Unable to delete file") - runCatching { + runCatchingCancellable { historyRepository.deleteOrSwap(manga, original) } mangaList.update { list -> @@ -113,6 +116,8 @@ class LocalListViewModel( try { listError.value = null mangaList.value = repository.getList(0, null, null) + } catch (e: CancellationException) { + throw e } catch (e: Throwable) { listError.value = e } @@ -121,7 +126,7 @@ class LocalListViewModel( private fun cleanup() { if (!DownloadService.isRunning) { viewModelScope.launch { - runCatching { + runCatchingCancellable { repository.cleanup() }.onFailure { error -> error.printStackTraceDebug() diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt index 895bb6628..3ffeeb818 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt @@ -6,6 +6,7 @@ import androidx.activity.result.ActivityResultLauncher import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import java.util.Date import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import org.koitharu.kotatsu.R @@ -31,7 +32,7 @@ import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.processLifecycleScope -import java.util.* +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable private const val BOUNDS_PAGE_OFFSET = 2 private const val PAGES_TRIM_THRESHOLD = 120 @@ -69,7 +70,7 @@ class ReaderViewModel( mangaName = manga?.title, chapterName = chapter?.name, chapterNumber = chapter?.number ?: 0, - chaptersTotal = chapters.size() + chaptersTotal = chapters.size(), ) }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, null) @@ -80,7 +81,7 @@ class ReaderViewModel( val readerAnimation = settings.observeAsLiveData( context = viewModelScope.coroutineContext + Dispatchers.Default, key = AppSettings.KEY_READER_ANIMATION, - valueProducer = { readerAnimation } + valueProducer = { readerAnimation }, ) val isScreenshotsBlockEnabled = combine( @@ -123,12 +124,12 @@ class ReaderViewModel( val manga = checkNotNull(mangaData.value) dataRepository.savePreferences( manga = manga, - mode = newMode + mode = newMode, ) readerMode.value = newMode content.value?.run { content.value = copy( - state = getCurrentState() + state = getCurrentState(), ) } } @@ -358,7 +359,7 @@ class ReaderViewModel( ?: manga.chapters?.randomOrNull() ?: error("There are no chapters in this manga") val pages = repo.getPages(chapter) - return runCatching { + return runCatchingCancellable { val isWebtoon = MangaUtils.determineMangaIsWebtoon(pages) if (isWebtoon) ReaderMode.WEBTOON else defaultMode }.onSuccess { @@ -389,7 +390,7 @@ class ReaderViewModel( */ private fun HistoryRepository.saveStateAsync(manga: Manga, state: ReaderState, percent: Float): Job { return processLifecycleScope.launch(Dispatchers.Default) { - runCatching { + runCatchingCancellable { addOrUpdate( manga = manga, chapterId = state.chapterId, diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt index 5630cb1ba..b32ecc884 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/pager/PageHolderDelegate.kt @@ -3,24 +3,30 @@ package org.koitharu.kotatsu.reader.ui.pager import android.net.Uri import androidx.core.net.toUri import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView -import kotlinx.coroutines.* +import java.io.File +import java.io.IOException +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.plus import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.model.ZoomMode import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.reader.domain.PageLoader -import java.io.File -import java.io.IOException class PageHolderDelegate( private val loader: PageLoader, private val settings: AppSettings, private val callback: Callback, - private val exceptionResolver: ExceptionResolver + private val exceptionResolver: ExceptionResolver, ) : SubsamplingScaleImageView.DefaultOnImageEventListener() { private val scope = loader.loaderScope + Dispatchers.Main.immediate @@ -88,6 +94,8 @@ class PageHolderDelegate( loader.convertInPlace(file) state = State.CONVERTED callback.onImageReady(file.toUri()) + } catch (ce: CancellationException) { + throw ce } catch (e2: Throwable) { e.addSuppressed(e2) state = State.ERROR diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt index 42529810b..c5ab420ea 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/thumbnails/adapter/PageThumbnailAD.kt @@ -15,6 +15,7 @@ import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail import org.koitharu.kotatsu.utils.ext.referer +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable import org.koitharu.kotatsu.utils.ext.setTextColorAttr fun pageThumbnailAD( @@ -69,7 +70,7 @@ fun pageThumbnailAD( text = (item.number).toString() } job = scope.launch { - val drawable = runCatching { + val drawable = runCatchingCancellable { loadPageThumbnail(item) }.getOrNull() binding.imageViewThumb.setImageDrawable(drawable) diff --git a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt index 092194b96..7249b75bc 100644 --- a/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/remotelist/ui/RemoteListViewModel.kt @@ -2,10 +2,16 @@ package org.koitharu.kotatsu.remotelist.ui import androidx.lifecycle.LiveData import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.cancelAndJoin -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.base.ui.widgets.ChipsView @@ -16,7 +22,14 @@ import org.koitharu.kotatsu.list.ui.filter.FilterCoordinator import org.koitharu.kotatsu.list.ui.filter.FilterItem import org.koitharu.kotatsu.list.ui.filter.FilterState import org.koitharu.kotatsu.list.ui.filter.OnFilterChangedListener -import org.koitharu.kotatsu.list.ui.model.* +import org.koitharu.kotatsu.list.ui.model.CurrentFilterModel +import org.koitharu.kotatsu.list.ui.model.EmptyState +import org.koitharu.kotatsu.list.ui.model.ListHeader +import org.koitharu.kotatsu.list.ui.model.LoadingFooter +import org.koitharu.kotatsu.list.ui.model.LoadingState +import org.koitharu.kotatsu.list.ui.model.toErrorFooter +import org.koitharu.kotatsu.list.ui.model.toErrorState +import org.koitharu.kotatsu.list.ui.model.toUi import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct @@ -132,6 +145,8 @@ class RemoteListViewModel( mangaList.value = mangaList.value?.plus(list) ?: list } hasNextPage.value = list.isNotEmpty() + } catch (e: CancellationException) { + throw e } catch (e: Throwable) { e.printStackTraceDebug() listError.value = e diff --git a/app/src/main/java/org/koitharu/kotatsu/scrobbling/domain/Scrobbler.kt b/app/src/main/java/org/koitharu/kotatsu/scrobbling/domain/Scrobbler.kt index b730d19cd..1db58e204 100644 --- a/app/src/main/java/org/koitharu/kotatsu/scrobbling/domain/Scrobbler.kt +++ b/app/src/main/java/org/koitharu/kotatsu/scrobbling/domain/Scrobbler.kt @@ -3,15 +3,20 @@ package org.koitharu.kotatsu.scrobbling.domain import androidx.collection.LongSparseArray import androidx.collection.getOrElse import androidx.core.text.parseAsHtml -import java.util.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.scrobbling.data.ScrobblingEntity -import org.koitharu.kotatsu.scrobbling.domain.model.* +import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerManga +import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerMangaInfo +import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblerService +import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingInfo +import org.koitharu.kotatsu.scrobbling.domain.model.ScrobblingStatus import org.koitharu.kotatsu.utils.ext.findKeyByValue import org.koitharu.kotatsu.utils.ext.printStackTraceDebug +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable +import java.util.EnumMap abstract class Scrobbler( protected val db: MangaDatabase, @@ -47,7 +52,7 @@ abstract class Scrobbler( private suspend fun ScrobblingEntity.toScrobblingInfo(mangaId: Long): ScrobblingInfo? { val mangaInfo = infoCache.getOrElse(targetId) { - runCatching { + runCatchingCancellable { getMangaInfo(targetId) }.onFailure { it.printStackTraceDebug() @@ -72,9 +77,9 @@ abstract class Scrobbler( } suspend fun Scrobbler.tryScrobble(mangaId: Long, chapter: MangaChapter): Boolean { - return runCatching { + return runCatchingCancellable { scrobble(mangaId, chapter) }.onFailure { it.printStackTraceDebug() }.isSuccess -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt b/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt index 2c95fb632..b86bb5475 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/domain/MangaSearchRepository.kt @@ -19,6 +19,7 @@ import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.util.levenshteinDistance import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable class MangaSearchRepository( private val settings: AppSettings, @@ -30,7 +31,7 @@ class MangaSearchRepository( fun globalSearch(query: String, concurrency: Int = DEFAULT_CONCURRENCY): Flow = settings.getMangaSources(includeHidden = false).asFlow() .flatMapMerge(concurrency) { source -> - runCatching { + runCatchingCancellable { MangaRepository(source).getList( offset = 0, query = query, @@ -63,7 +64,7 @@ class MangaSearchRepository( SUGGESTION_PROJECTION, "${SearchManager.SUGGEST_COLUMN_QUERY} LIKE ?", arrayOf("%$query%"), - "date DESC" + "date DESC", )?.use { cursor -> val count = minOf(cursor.count, limit) if (count == 0) { @@ -113,7 +114,7 @@ class MangaSearchRepository( SUGGESTION_PROJECTION, null, arrayOfNulls(1), - null + null, )?.use { cursor -> cursor.count } ?: 0 } diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchViewModel.kt index 9615e84a6..713daf8f6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/SearchViewModel.kt @@ -1,6 +1,7 @@ package org.koitharu.kotatsu.search.ui import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow @@ -9,14 +10,20 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.list.ui.MangaListViewModel -import org.koitharu.kotatsu.list.ui.model.* +import org.koitharu.kotatsu.list.ui.model.EmptyState +import org.koitharu.kotatsu.list.ui.model.ListModel +import org.koitharu.kotatsu.list.ui.model.LoadingFooter +import org.koitharu.kotatsu.list.ui.model.LoadingState +import org.koitharu.kotatsu.list.ui.model.toErrorFooter +import org.koitharu.kotatsu.list.ui.model.toErrorState +import org.koitharu.kotatsu.list.ui.model.toUi import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct class SearchViewModel( private val repository: MangaRepository, private val query: String, - settings: AppSettings + settings: AppSettings, ) : MangaListViewModel(settings) { private val mangaList = MutableStateFlow?>(null) @@ -28,7 +35,7 @@ class SearchViewModel( mangaList, createListModeFlow(), listError, - hasNextPage + hasNextPage, ) { list, mode, error, hasNext -> when { list.isNullOrEmpty() && error != null -> listOf(error.toErrorState(canRetry = true)) @@ -39,8 +46,9 @@ class SearchViewModel( textPrimary = R.string.nothing_found, textSecondary = R.string.text_search_holder_secondary, actionStringRes = 0, - ) + ), ) + else -> { val result = ArrayList(list.size + 1) list.toUi(result, mode) @@ -88,6 +96,8 @@ class SearchViewModel( mangaList.value = mangaList.value?.plus(list) ?: list } hasNextPage.value = list.isNotEmpty() + } catch (e: CancellationException) { + throw e } catch (e: Throwable) { listError.value = e } diff --git a/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt index b2ababf6d..cd0f6d199 100644 --- a/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/search/ui/multi/MultiSearchViewModel.kt @@ -17,6 +17,7 @@ import org.koitharu.kotatsu.list.ui.model.* import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.printStackTraceDebug +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable private const val MAX_PARALLELISM = 4 private const val MIN_HAS_MORE_ITEMS = 8 @@ -48,8 +49,9 @@ class MultiSearchViewModel( textSecondary = R.string.text_search_holder_secondary, actionStringRes = 0, ) - } + }, ) + loading -> list + LoadingFooter else -> list } @@ -81,6 +83,8 @@ class MultiSearchViewModel( loadingData.value = true query.postValue(q) searchImpl(q) + } catch (e: CancellationException) { + throw e } catch (e: Throwable) { listError.value = e } finally { @@ -94,7 +98,7 @@ class MultiSearchViewModel( val dispatcher = Dispatchers.Default.limitedParallelism(MAX_PARALLELISM) val deferredList = sources.map { source -> async(dispatcher) { - runCatching { + runCatchingCancellable { val list = MangaRepository(source).getList(offset = 0, query = q) .toUi(ListMode.GRID) if (list.isNotEmpty()) { diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt b/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt index 603fe1ce5..62fe1b7ba 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt @@ -8,6 +8,12 @@ import androidx.activity.ComponentActivity import androidx.annotation.MainThread import androidx.core.net.toUri import com.google.android.material.dialog.MaterialAlertDialogBuilder +import java.io.ByteArrayInputStream +import java.io.InputStream +import java.security.MessageDigest +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import java.util.concurrent.TimeUnit import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.koin.android.ext.android.get @@ -20,12 +26,7 @@ import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.parsers.util.byte2HexFormatted import org.koitharu.kotatsu.utils.FileSize import org.koitharu.kotatsu.utils.ext.printStackTraceDebug -import java.io.ByteArrayInputStream -import java.io.InputStream -import java.security.MessageDigest -import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate -import java.util.concurrent.TimeUnit +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable class AppUpdateChecker(private val activity: ComponentActivity) { @@ -41,7 +42,7 @@ class AppUpdateChecker(private val activity: ComponentActivity) { null } - suspend fun checkNow() = runCatching { + suspend fun checkNow() = runCatchingCancellable { val version = repo.getLatestVersion() val newVersionId = VersionId(version.name) val currentVersionId = VersionId(BuildConfig.VERSION_NAME) diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt index c4c5f46ba..8d807be6b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/HistorySettingsFragment.kt @@ -7,6 +7,7 @@ import android.view.View import androidx.preference.Preference import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.launch import org.koin.android.ext.android.get import org.koin.android.ext.android.inject @@ -65,18 +66,22 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach clearCache(preference, CacheDir.PAGES) true } + AppSettings.KEY_THUMBS_CACHE_CLEAR -> { clearCache(preference, CacheDir.THUMBS) true } + AppSettings.KEY_COOKIES_CLEAR -> { clearCookies() true } + AppSettings.KEY_SEARCH_HISTORY_CLEAR -> { clearSearchHistory(preference) true } + AppSettings.KEY_UPDATES_FEED_CLEAR -> { viewLifecycleScope.launch { trackerRepo.clearLogs() @@ -85,11 +90,12 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach Snackbar.make( view ?: return@launch, R.string.updates_feed_cleared, - Snackbar.LENGTH_SHORT + Snackbar.LENGTH_SHORT, ).show() } true } + AppSettings.KEY_SHIKIMORI -> { if (!shikimoriRepository.isAuthorized) { launchShikimoriAuth() @@ -98,6 +104,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach super.onPreferenceTreeClick(preference) } } + else -> super.onPreferenceTreeClick(preference) } } @@ -110,6 +117,8 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach storageManager.clearCache(cache) val size = storageManager.computeCacheSize(cache) preference.summary = FileSize.BYTES.format(ctx, size) + } catch (e: CancellationException) { + throw e } catch (e: Exception) { preference.summary = e.getDisplayMessage(ctx.resources) } finally { @@ -136,7 +145,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach Snackbar.make( view ?: return@launch, R.string.search_history_cleared, - Snackbar.LENGTH_SHORT + Snackbar.LENGTH_SHORT, ).show() } }.show() @@ -154,7 +163,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach Snackbar.make( listView ?: return@launch, R.string.cookies_cleared, - Snackbar.LENGTH_SHORT + Snackbar.LENGTH_SHORT, ).show() } }.show() diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt index b5209896b..463d43972 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/SourceSettingsFragment.kt @@ -18,7 +18,13 @@ import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.parsers.exception.AuthRequiredException import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity -import org.koitharu.kotatsu.utils.ext.* +import org.koitharu.kotatsu.utils.ext.awaitViewLifecycle +import org.koitharu.kotatsu.utils.ext.getDisplayMessage +import org.koitharu.kotatsu.utils.ext.printStackTraceDebug +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable +import org.koitharu.kotatsu.utils.ext.serializableArgument +import org.koitharu.kotatsu.utils.ext.viewLifecycleScope +import org.koitharu.kotatsu.utils.ext.withArgs class SourceSettingsFragment : BasePreferenceFragment(0) { @@ -60,12 +66,13 @@ class SourceSettingsFragment : BasePreferenceFragment(0) { startActivity(SourceAuthActivity.newIntent(preference.context, source)) true } + else -> super.onPreferenceTreeClick(preference) } } private fun loadUsername(owner: LifecycleOwner, preference: Preference) = owner.lifecycleScope.launch { - runCatching { + runCatchingCancellable { preference.summary = null withContext(Dispatchers.Default) { requireNotNull(repository?.getAuthProvider()?.getUsername()) @@ -85,6 +92,7 @@ class SourceSettingsFragment : BasePreferenceFragment(0) { ).setAction(ExceptionResolver.getResolveStringId(error)) { resolveError(error) } .show() } + else -> preference.summary = error.getDisplayMessage(preference.context.resources) } error.printStackTraceDebug() diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt index b309a588b..281f57a12 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt @@ -9,14 +9,14 @@ import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.core.view.isVisible import com.google.android.material.dialog.MaterialAlertDialogBuilder +import java.io.File +import java.io.FileOutputStream import org.koin.androidx.viewmodel.ext.android.viewModel import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.AlertDialogFragment import org.koitharu.kotatsu.databinding.DialogProgressBinding import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.progress.Progress -import java.io.File -import java.io.FileOutputStream class BackupDialogFragment : AlertDialogFragment() { @@ -24,7 +24,7 @@ class BackupDialogFragment : AlertDialogFragment() { private var backup: File? = null private val saveFileContract = registerForActivityResult( - ActivityResultContracts.CreateDocument("*/*") + ActivityResultContracts.CreateDocument("*/*"), ) { uri -> val file = backup if (uri != null && file != null) { @@ -88,6 +88,8 @@ class BackupDialogFragment : AlertDialogFragment() { } Toast.makeText(requireContext(), R.string.backup_saved, Toast.LENGTH_LONG).show() dismiss() + } catch (e: InterruptedException) { + throw e } catch (e: Exception) { onError(e) } diff --git a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt index 7433e689b..eab01f9ee 100644 --- a/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt +++ b/app/src/main/java/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt @@ -8,6 +8,8 @@ import androidx.annotation.FloatRange import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import androidx.work.* +import java.util.concurrent.TimeUnit +import kotlin.math.pow import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -24,8 +26,6 @@ import org.koitharu.kotatsu.suggestions.domain.MangaSuggestion import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository import org.koitharu.kotatsu.utils.ext.asArrayList import org.koitharu.kotatsu.utils.ext.trySetForeground -import java.util.concurrent.TimeUnit -import kotlin.math.pow class SuggestionsWorker(appContext: Context, params: WorkerParameters) : CoroutineWorker(appContext, params), KoinComponent { @@ -47,7 +47,7 @@ class SuggestionsWorker(appContext: Context, params: WorkerParameters) : val channel = NotificationChannel( WORKER_CHANNEL_ID, title, - NotificationManager.IMPORTANCE_LOW + NotificationManager.IMPORTANCE_LOW, ) channel.setShowBadge(false) channel.enableVibration(false) @@ -118,7 +118,7 @@ class SuggestionsWorker(appContext: Context, params: WorkerParameters) : }.map { manga -> MangaSuggestion( manga = manga, - relevance = computeRelevance(manga.tags, allTags) + relevance = computeRelevance(manga.tags, allTags), ) }.sortedBy { it.relevance }.take(LIMIT) suggestionRepository.replace(suggestions) diff --git a/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt b/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt index 51aedc866..721f8080b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt +++ b/app/src/main/java/org/koitharu/kotatsu/tracker/work/TrackWorker.kt @@ -27,6 +27,7 @@ import org.koitharu.kotatsu.tracker.domain.Tracker import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates import org.koitharu.kotatsu.utils.PendingIntentCompat import org.koitharu.kotatsu.utils.ext.referer +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable import org.koitharu.kotatsu.utils.ext.toBitmapOrNull import org.koitharu.kotatsu.utils.ext.trySetForeground @@ -80,7 +81,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) : val deferredList = coroutineScope { tracks.map { (track, channelId) -> async(dispatcher) { - runCatching { + runCatchingCancellable { tracker.fetchUpdates(track, commit = true) }.onSuccess { updates -> if (updates.isValid && updates.isNotEmpty()) { diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/PausingDispatcher.kt b/app/src/main/java/org/koitharu/kotatsu/utils/PausingDispatcher.kt deleted file mode 100644 index 57eb100a4..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/utils/PausingDispatcher.kt +++ /dev/null @@ -1,50 +0,0 @@ -package org.koitharu.kotatsu.utils - -import androidx.annotation.MainThread -import java.util.concurrent.ConcurrentLinkedQueue -import kotlin.coroutines.CoroutineContext -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Runnable - -class PausingDispatcher( - private val dispatcher: CoroutineDispatcher, -) : CoroutineDispatcher() { - - @Volatile - private var isPaused = false - private val queue = ConcurrentLinkedQueue() - - override fun isDispatchNeeded(context: CoroutineContext): Boolean { - return isPaused || super.isDispatchNeeded(context) - } - - override fun dispatch(context: CoroutineContext, block: Runnable) { - if (isPaused) { - queue.add(Task(context, block)) - } else { - dispatcher.dispatch(context, block) - } - } - - @MainThread - fun pause() { - isPaused = true - } - - @MainThread - fun resume() { - if (!isPaused) { - return - } - isPaused = false - while (true) { - val task = queue.poll() ?: break - dispatcher.dispatch(task.context, task.block) - } - } - - private class Task( - val context: CoroutineContext, - val block: Runnable, - ) -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt index 28f50e7de..ba56a4b23 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/AndroidExt.kt @@ -29,7 +29,7 @@ val Context.activityManager: ActivityManager? fun String.toUriOrNull() = if (isEmpty()) null else Uri.parse(this) -suspend fun CoroutineWorker.trySetForeground(): Boolean = runCatching { +suspend fun CoroutineWorker.trySetForeground(): Boolean = runCatchingCancellable { val info = getForegroundInfo() setForeground(info) }.isSuccess diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ThrowableExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ThrowableExt.kt index d4f654525..0fe14cfd8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/ThrowableExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/ThrowableExt.kt @@ -4,10 +4,16 @@ import android.content.ActivityNotFoundException import android.content.res.Resources import androidx.collection.arraySetOf import java.net.SocketTimeoutException +import java.net.UnknownHostException +import kotlinx.coroutines.CancellationException import okio.FileNotFoundException import org.acra.ktx.sendWithAcra import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.exceptions.* +import org.koitharu.kotatsu.core.exceptions.CaughtException +import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException +import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException +import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException +import org.koitharu.kotatsu.core.exceptions.WrongPasswordException import org.koitharu.kotatsu.parsers.exception.AuthRequiredException import org.koitharu.kotatsu.parsers.exception.ContentUnavailableException import org.koitharu.kotatsu.parsers.exception.NotFoundException @@ -19,12 +25,16 @@ fun Throwable.getDisplayMessage(resources: Resources): String = when (this) { is ActivityNotFoundException, is UnsupportedOperationException, -> resources.getString(R.string.operation_not_supported) + is UnsupportedFileException -> resources.getString(R.string.text_file_not_supported) is FileNotFoundException -> resources.getString(R.string.file_not_found) is EmptyHistoryException -> resources.getString(R.string.history_is_empty) is ContentUnavailableException -> message is ParseException -> shortMessage - is SocketTimeoutException -> resources.getString(R.string.network_error) + is UnknownHostException, + is SocketTimeoutException, + -> resources.getString(R.string.network_error) + is WrongPasswordException -> resources.getString(R.string.wrong_password) is NotFoundException -> resources.getString(R.string.not_found_404) else -> localizedMessage @@ -46,4 +56,16 @@ private val reportableExceptions = arraySetOf>( IllegalArgumentException::class.java, ConcurrentModificationException::class.java, UnsupportedOperationException::class.java, -) \ No newline at end of file +) + +inline fun runCatchingCancellable(block: () -> R): Result { + return try { + Result.success(block()) + } catch (e: InterruptedException) { + throw e + } catch (e: CancellationException) { + throw e + } catch (e: Throwable) { + Result.failure(e) + } +} \ No newline at end of file