Fix coroutines cancellation

pull/229/head
Koitharu 4 years ago
parent 73478d6a81
commit 1e75edf262
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

@ -3,4 +3,7 @@
<component name="Kotlin2JvmCompilerArguments"> <component name="Kotlin2JvmCompilerArguments">
<option name="jvmTarget" value="1.8" /> <option name="jvmTarget" value="1.8" />
</component> </component>
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.6.21" />
</component>
</project> </project>

@ -15,8 +15,8 @@ android {
applicationId 'org.koitharu.kotatsu' applicationId 'org.koitharu.kotatsu'
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 33 targetSdkVersion 33
versionCode 497 versionCode 498
versionName '4.0-beta1' versionName '4.0-beta2'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

@ -1,9 +1,12 @@
package org.koitharu.kotatsu.base.domain package org.koitharu.kotatsu.base.domain
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.processLifecycleScope import org.koitharu.kotatsu.utils.ext.processLifecycleScope
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
fun interface ReversibleHandle { fun interface ReversibleHandle {
@ -11,8 +14,10 @@ fun interface ReversibleHandle {
} }
fun ReversibleHandle.reverseAsync() = processLifecycleScope.launch(Dispatchers.Default) { fun ReversibleHandle.reverseAsync() = processLifecycleScope.launch(Dispatchers.Default) {
runCatching { runCatchingCancellable {
withContext(NonCancellable) {
reverse() reverse()
}
}.onFailure { }.onFailure {
it.printStackTraceDebug() it.printStackTraceDebug()
} }

@ -1,13 +1,14 @@
package org.koitharu.kotatsu.core.backup package org.koitharu.kotatsu.core.backup
import androidx.room.withTransaction import androidx.room.withTransaction
import javax.inject.Inject
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.parsers.util.json.JSONIterator import org.koitharu.kotatsu.parsers.util.json.JSONIterator
import org.koitharu.kotatsu.parsers.util.json.mapJSON import org.koitharu.kotatsu.parsers.util.json.mapJSON
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import javax.inject.Inject
private const val PAGE_SIZE = 10 private const val PAGE_SIZE = 10
@ -85,7 +86,7 @@ class BackupRepository @Inject constructor(private val db: MangaDatabase) {
JsonDeserializer(it).toTagEntity() JsonDeserializer(it).toTagEntity()
} }
val history = JsonDeserializer(item).toHistoryEntity() val history = JsonDeserializer(item).toHistoryEntity()
result += runCatching { result += runCatchingCancellable {
db.withTransaction { db.withTransaction {
db.tagsDao.upsert(tags) db.tagsDao.upsert(tags)
db.mangaDao.upsert(manga, tags) db.mangaDao.upsert(manga, tags)
@ -100,7 +101,7 @@ class BackupRepository @Inject constructor(private val db: MangaDatabase) {
val result = CompositeResult() val result = CompositeResult()
for (item in entry.data.JSONIterator()) { for (item in entry.data.JSONIterator()) {
val category = JsonDeserializer(item).toFavouriteCategoryEntity() val category = JsonDeserializer(item).toFavouriteCategoryEntity()
result += runCatching { result += runCatchingCancellable {
db.favouriteCategoriesDao.upsert(category) db.favouriteCategoriesDao.upsert(category)
} }
} }
@ -116,7 +117,7 @@ class BackupRepository @Inject constructor(private val db: MangaDatabase) {
JsonDeserializer(it).toTagEntity() JsonDeserializer(it).toTagEntity()
} }
val favourite = JsonDeserializer(item).toFavouriteEntity() val favourite = JsonDeserializer(item).toFavouriteEntity()
result += runCatching { result += runCatchingCancellable {
db.withTransaction { db.withTransaction {
db.tagsDao.upsert(tags) db.tagsDao.upsert(tags)
db.mangaDao.upsert(manga, tags) db.mangaDao.upsert(manga, tags)

@ -4,13 +4,6 @@ import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.security.MessageDigest
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@ -24,6 +17,14 @@ import org.koitharu.kotatsu.parsers.util.json.mapJSONNotNull
import org.koitharu.kotatsu.parsers.util.parseJsonArray import org.koitharu.kotatsu.parsers.util.parseJsonArray
import org.koitharu.kotatsu.utils.ext.asArrayList import org.koitharu.kotatsu.utils.ext.asArrayList
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.security.MessageDigest
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import javax.inject.Inject
import javax.inject.Singleton
private const val CERT_SHA1 = "2C:19:C7:E8:07:61:2B:8E:94:51:1B:FD:72:67:07:64:5D:C2:58:AE" private const val CERT_SHA1 = "2C:19:C7:E8:07:61:2B:8E:94:51:1B:FD:72:67:07:64:5D:C2:58:AE"
@ -59,7 +60,7 @@ class AppUpdateRepository @Inject constructor(
if (!isUpdateSupported()) { if (!isUpdateSupported()) {
return@withContext null return@withContext null
} }
runCatching { runCatchingCancellable {
val currentVersion = VersionId(BuildConfig.VERSION_NAME) val currentVersion = VersionId(BuildConfig.VERSION_NAME)
val available = getAvailableVersions().asArrayList() val available = getAvailableVersions().asArrayList()
available.sortBy { it.versionId } available.sortBy { it.versionId }

@ -17,8 +17,6 @@ import coil.request.ImageRequest
import coil.size.Precision import coil.size.Precision
import coil.size.Scale import coil.size.Scale
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -32,6 +30,9 @@ import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.processLifecycleScope import org.koitharu.kotatsu.utils.ext.processLifecycleScope
import org.koitharu.kotatsu.utils.ext.requireBitmap import org.koitharu.kotatsu.utils.ext.requireBitmap
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import javax.inject.Inject
import javax.inject.Singleton
@Singleton @Singleton
class ShortcutsUpdater @Inject constructor( class ShortcutsUpdater @Inject constructor(
@ -92,7 +93,7 @@ class ShortcutsUpdater @Inject constructor(
} }
@RequiresApi(Build.VERSION_CODES.N_MR1) @RequiresApi(Build.VERSION_CODES.N_MR1)
private suspend fun updateShortcutsImpl() = runCatching { private suspend fun updateShortcutsImpl() = runCatchingCancellable {
val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager
val shortcuts = historyRepository.getList(0, manager.maxShortcutCountPerActivity) val shortcuts = historyRepository.getList(0, manager.maxShortcutCountPerActivity)
.filter { x -> x.title.isNotEmpty() } .filter { x -> x.title.isNotEmpty() }
@ -112,7 +113,7 @@ class ShortcutsUpdater @Inject constructor(
} }
private suspend fun buildShortcutInfo(manga: Manga): ShortcutInfoCompat.Builder { private suspend fun buildShortcutInfo(manga: Manga): ShortcutInfoCompat.Builder {
val icon = runCatching { val icon = runCatchingCancellable {
coil.execute( coil.execute(
ImageRequest.Builder(context) ImageRequest.Builder(context)
.data(manga.coverUrl) .data(manga.coverUrl)

@ -45,6 +45,7 @@ import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.asFlowLiveData import org.koitharu.kotatsu.utils.asFlowLiveData
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
class DetailsViewModel @AssistedInject constructor( class DetailsViewModel @AssistedInject constructor(
@Assisted intent: MangaIntent, @Assisted intent: MangaIntent,
@ -189,7 +190,7 @@ class DetailsViewModel @AssistedInject constructor(
checkNotNull(manga) { "Cannot find saved manga for ${m.title}" } checkNotNull(manga) { "Cannot find saved manga for ${m.title}" }
val original = localMangaRepository.getRemoteManga(manga) val original = localMangaRepository.getRemoteManga(manga)
localMangaRepository.delete(manga) || throw IOException("Unable to delete file") localMangaRepository.delete(manga) || throw IOException("Unable to delete file")
runCatching { runCatchingCancellable {
historyRepository.deleteOrSwap(manga, original) historyRepository.deleteOrSwap(manga, original)
} }
onMangaRemoved.postCall(manga) onMangaRemoved.postCall(manga)
@ -228,7 +229,7 @@ class DetailsViewModel @AssistedInject constructor(
reload() reload()
} else { } else {
viewModelScope.launch(Dispatchers.Default) { viewModelScope.launch(Dispatchers.Default) {
runCatching { runCatchingCancellable {
localMangaRepository.getDetails(downloadedManga) localMangaRepository.getDetails(downloadedManga)
}.onSuccess { }.onSuccess {
delegate.relatedManga.value = it delegate.relatedManga.value = it

@ -17,6 +17,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
class MangaDetailsDelegate( class MangaDetailsDelegate(
private val intent: MangaIntent, private val intent: MangaIntent,
@ -45,9 +46,9 @@ class MangaDetailsDelegate(
val hist = historyRepository.getOne(manga) val hist = historyRepository.getOne(manga)
selectedBranch.value = manga.getPreferredBranch(hist) selectedBranch.value = manga.getPreferredBranch(hist)
mangaData.value = manga mangaData.value = manga
relatedManga.value = runCatching { relatedManga.value = runCatchingCancellable {
if (manga.source == MangaSource.LOCAL) { if (manga.source == MangaSource.LOCAL) {
val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatching null val m = localMangaRepository.getRemoteManga(manga) ?: return@runCatchingCancellable null
mangaRepositoryFactory.create(m.source).getDetails(m) mangaRepositoryFactory.create(m.source).getDetails(m)
} else { } else {
localMangaRepository.findSavedManga(manga) localMangaRepository.findSavedManga(manga)

@ -9,11 +9,18 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.File import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.* 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.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.internal.closeQuietly import okhttp3.internal.closeQuietly
@ -32,7 +39,9 @@ import org.koitharu.kotatsu.parsers.util.await
import org.koitharu.kotatsu.utils.ext.deleteAwait import org.koitharu.kotatsu.utils.ext.deleteAwait
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.referer import org.koitharu.kotatsu.utils.ext.referer
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import org.koitharu.kotatsu.utils.progress.PausingProgressJob import org.koitharu.kotatsu.utils.progress.PausingProgressJob
import java.io.File
private const val MAX_FAILSAFE_ATTEMPTS = 2 private const val MAX_FAILSAFE_ATTEMPTS = 2
private const val DOWNLOAD_ERROR_DELAY = 500L private const val DOWNLOAD_ERROR_DELAY = 500L
@ -231,7 +240,7 @@ class DownloadManager @AssistedInject constructor(
) )
} }
private suspend fun loadCover(manga: Manga) = runCatching { private suspend fun loadCover(manga: Manga) = runCatchingCancellable {
imageLoader.execute( imageLoader.execute(
ImageRequest.Builder(context) ImageRequest.Builder(context)
.data(manga.coverUrl) .data(manga.coverUrl)

@ -28,6 +28,7 @@ import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.asFlowLiveData import org.koitharu.kotatsu.utils.asFlowLiveData
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
class FavouritesListViewModel @AssistedInject constructor( class FavouritesListViewModel @AssistedInject constructor(
@Assisted private val categoryId: Long, @Assisted private val categoryId: Long,
@ -69,6 +70,7 @@ class FavouritesListViewModel @AssistedInject constructor(
actionStringRes = 0, actionStringRes = 0,
), ),
) )
else -> list.toUi(mode, this) else -> list.toUi(mode, this)
} }
}.catch { }.catch {
@ -79,7 +81,7 @@ class FavouritesListViewModel @AssistedInject constructor(
if (categoryId != NO_ID) { if (categoryId != NO_ID) {
launchJob { launchJob {
categoryName = withContext(Dispatchers.Default) { categoryName = withContext(Dispatchers.Default) {
runCatching { runCatchingCancellable {
repository.getCategory(categoryId).title repository.getCategory(categoryId).title
}.getOrNull() }.getOrNull()
} }

@ -6,15 +6,22 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async 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.R
import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import java.text.Collator import java.text.Collator
import java.util.* import java.util.Locale
import java.util.TreeSet
class FilterCoordinator( class FilterCoordinator(
private val repository: RemoteMangaRepository, private val repository: RemoteMangaRepository,
@ -153,7 +160,7 @@ class FilterCoordinator(
} }
private fun loadTagsAsync() = coroutineScope.async(Dispatchers.Default, CoroutineStart.LAZY) { private fun loadTagsAsync() = coroutineScope.async(Dispatchers.Default, CoroutineStart.LAZY) {
runCatching { runCatchingCancellable {
repository.getTags() repository.getTags()
}.onFailure { error -> }.onFailure { error ->
error.printStackTraceDebug() error.printStackTraceDebug()

@ -7,28 +7,40 @@ import androidx.annotation.WorkerThread
import androidx.collection.ArraySet import androidx.collection.ArraySet
import androidx.core.net.toFile import androidx.core.net.toFile
import androidx.core.net.toUri import androidx.core.net.toUri
import java.io.File import kotlinx.coroutines.CoroutineScope
import java.util.* import kotlinx.coroutines.Deferred
import java.util.zip.ZipEntry import kotlinx.coroutines.Dispatchers
import java.util.zip.ZipFile import kotlinx.coroutines.async
import javax.inject.Inject import kotlinx.coroutines.awaitAll
import javax.inject.Singleton import kotlinx.coroutines.coroutineScope
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.runInterruptible
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.local.data.CbzFilter import org.koitharu.kotatsu.local.data.CbzFilter
import org.koitharu.kotatsu.local.data.LocalStorageManager import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.local.data.MangaIndex import org.koitharu.kotatsu.local.data.MangaIndex
import org.koitharu.kotatsu.local.data.TempFileFilter 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.parsers.util.toCamelCase
import org.koitharu.kotatsu.utils.AlphanumComparator import org.koitharu.kotatsu.utils.AlphanumComparator
import org.koitharu.kotatsu.utils.CompositeMutex import org.koitharu.kotatsu.utils.CompositeMutex
import org.koitharu.kotatsu.utils.ext.deleteAwait import org.koitharu.kotatsu.utils.ext.deleteAwait
import org.koitharu.kotatsu.utils.ext.longHashCode import org.koitharu.kotatsu.utils.ext.longHashCode
import org.koitharu.kotatsu.utils.ext.readText import org.koitharu.kotatsu.utils.ext.readText
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import java.io.File
import java.util.Enumeration
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.CoroutineContext
private const val MAX_PARALLELISM = 4 private const val MAX_PARALLELISM = 4
@ -73,6 +85,7 @@ class LocalMangaRepository @Inject constructor(private val storageManager: Local
manga.source != MangaSource.LOCAL -> requireNotNull(findSavedManga(manga)) { manga.source != MangaSource.LOCAL -> requireNotNull(findSavedManga(manga)) {
"Manga is not local or saved" "Manga is not local or saved"
} }
else -> getFromFile(Uri.parse(manga.url).toFile()) else -> getFromFile(Uri.parse(manga.url).toFile())
} }
@ -236,7 +249,7 @@ class LocalMangaRepository @Inject constructor(private val storageManager: Local
context: CoroutineContext, context: CoroutineContext,
): Deferred<LocalManga?> = async(context) { ): Deferred<LocalManga?> = async(context) {
runInterruptible { runInterruptible {
runCatching { LocalManga(getFromFile(file), file) }.getOrNull() runCatchingCancellable { LocalManga(getFromFile(file), file) }.getOrNull()
} }
} }

@ -29,6 +29,7 @@ import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
@HiltViewModel @HiltViewModel
class LocalListViewModel @Inject constructor( class LocalListViewModel @Inject constructor(
@ -63,6 +64,7 @@ class LocalListViewModel @Inject constructor(
actionStringRes = R.string._import, actionStringRes = R.string._import,
), ),
) )
else -> buildList(list.size + 1) { else -> buildList(list.size + 1) {
add(createHeader(list, tags, order)) add(createHeader(list, tags, order))
list.toUi(this, mode, this@LocalListViewModel) list.toUi(this, mode, this@LocalListViewModel)
@ -104,7 +106,7 @@ class LocalListViewModel @Inject constructor(
for (manga in itemsToRemove) { for (manga in itemsToRemove) {
val original = repository.getRemoteManga(manga) val original = repository.getRemoteManga(manga)
repository.delete(manga) || throw IOException("Unable to delete file") repository.delete(manga) || throw IOException("Unable to delete file")
runCatching { runCatchingCancellable {
historyRepository.deleteOrSwap(manga, original) historyRepository.deleteOrSwap(manga, original)
} }
mangaList.update { list -> mangaList.update { list ->
@ -120,6 +122,8 @@ class LocalListViewModel @Inject constructor(
try { try {
listError.value = null listError.value = null
mangaList.value = repository.getList(0, selectedTags.value, sortOrder.value) mangaList.value = repository.getList(0, selectedTags.value, sortOrder.value)
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) { } catch (e: Throwable) {
listError.value = e listError.value = e
} }
@ -128,7 +132,7 @@ class LocalListViewModel @Inject constructor(
private fun cleanup() { private fun cleanup() {
if (!DownloadService.isRunning && !ImportService.isRunning && !LocalChaptersRemoveService.isRunning) { if (!DownloadService.isRunning && !ImportService.isRunning && !LocalChaptersRemoveService.isRunning) {
viewModelScope.launch { viewModelScope.launch {
runCatching { runCatchingCancellable {
repository.cleanup() repository.cleanup()
}.onFailure { error -> }.onFailure { error ->
error.printStackTraceDebug() error.printStackTraceDebug()

@ -38,6 +38,7 @@ import org.koitharu.kotatsu.utils.asFlowLiveData
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.processLifecycleScope import org.koitharu.kotatsu.utils.ext.processLifecycleScope
import org.koitharu.kotatsu.utils.ext.requireValue import org.koitharu.kotatsu.utils.ext.requireValue
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
private const val BOUNDS_PAGE_OFFSET = 2 private const val BOUNDS_PAGE_OFFSET = 2
private const val PREFETCH_LIMIT = 10 private const val PREFETCH_LIMIT = 10
@ -326,7 +327,7 @@ class ReaderViewModel @AssistedInject constructor(
?: manga.chapters?.randomOrNull() ?: manga.chapters?.randomOrNull()
?: error("There are no chapters in this manga") ?: error("There are no chapters in this manga")
val pages = repo.getPages(chapter) val pages = repo.getPages(chapter)
return runCatching { return runCatchingCancellable {
val isWebtoon = dataRepository.determineMangaIsWebtoon(repo, pages) val isWebtoon = dataRepository.determineMangaIsWebtoon(repo, pages)
if (isWebtoon) ReaderMode.WEBTOON else defaultMode if (isWebtoon) ReaderMode.WEBTOON else defaultMode
}.onSuccess { }.onSuccess {
@ -381,7 +382,7 @@ class ReaderViewModel @AssistedInject constructor(
*/ */
private fun HistoryRepository.saveStateAsync(manga: Manga, state: ReaderState, percent: Float): Job { private fun HistoryRepository.saveStateAsync(manga: Manga, state: ReaderState, percent: Float): Job {
return processLifecycleScope.launch(Dispatchers.Default) { return processLifecycleScope.launch(Dispatchers.Default) {
runCatching { runCatchingCancellable {
addOrUpdate( addOrUpdate(
manga = manga, manga = manga,
chapterId = state.chapterId, chapterId = state.chapterId,

@ -4,17 +4,23 @@ import android.net.Uri
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.davemorrissey.labs.subscaleview.DefaultOnImageEventListener import com.davemorrissey.labs.subscaleview.DefaultOnImageEventListener
import java.io.File import kotlinx.coroutines.CancellationException
import java.io.IOException import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.* import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach 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.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.reader.domain.PageLoader import org.koitharu.kotatsu.reader.domain.PageLoader
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
import java.io.File
import java.io.IOException
class PageHolderDelegate( class PageHolderDelegate(
private val loader: PageLoader, private val loader: PageLoader,
@ -102,6 +108,8 @@ class PageHolderDelegate(
loader.convertInPlace(file) loader.convertInPlace(file)
state = State.CONVERTED state = State.CONVERTED
callback.onImageReady(file.toUri()) callback.onImageReady(file.toUri())
} catch (ce: CancellationException) {
throw ce
} catch (e2: Throwable) { } catch (e2: Throwable) {
e.addSuppressed(e2) e.addSuppressed(e2)
state = State.ERROR state = State.ERROR

@ -17,6 +17,7 @@ import org.koitharu.kotatsu.reader.ui.thumbnails.PageThumbnail
import org.koitharu.kotatsu.utils.ext.decodeRegion import org.koitharu.kotatsu.utils.ext.decodeRegion
import org.koitharu.kotatsu.utils.ext.isLowRamDevice import org.koitharu.kotatsu.utils.ext.isLowRamDevice
import org.koitharu.kotatsu.utils.ext.referer import org.koitharu.kotatsu.utils.ext.referer
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import org.koitharu.kotatsu.utils.ext.setTextColorAttr import org.koitharu.kotatsu.utils.ext.setTextColorAttr
fun pageThumbnailAD( fun pageThumbnailAD(
@ -72,7 +73,7 @@ fun pageThumbnailAD(
text = (item.number).toString() text = (item.number).toString()
} }
job = scope.launch { job = scope.launch {
val drawable = runCatching { val drawable = runCatchingCancellable {
loadPageThumbnail(item) loadPageThumbnail(item)
}.getOrNull() }.getOrNull()
binding.imageViewThumb.setImageDrawable(drawable) binding.imageViewThumb.setImageDrawable(drawable)

@ -5,11 +5,16 @@ import androidx.lifecycle.viewModelScope
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import java.util.* import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin 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.R
import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.base.ui.widgets.ChipsView import org.koitharu.kotatsu.base.ui.widgets.ChipsView
@ -21,13 +26,20 @@ import org.koitharu.kotatsu.list.ui.filter.FilterCoordinator
import org.koitharu.kotatsu.list.ui.filter.FilterItem import org.koitharu.kotatsu.list.ui.filter.FilterItem
import org.koitharu.kotatsu.list.ui.filter.FilterState import org.koitharu.kotatsu.list.ui.filter.FilterState
import org.koitharu.kotatsu.list.ui.filter.OnFilterChangedListener import org.koitharu.kotatsu.list.ui.filter.OnFilterChangedListener
import org.koitharu.kotatsu.list.ui.model.* import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListHeader2
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.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.search.domain.MangaSearchRepository import org.koitharu.kotatsu.search.domain.MangaSearchRepository
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import java.util.LinkedList
private const val FILTER_MIN_INTERVAL = 250L private const val FILTER_MIN_INTERVAL = 250L
@ -138,6 +150,8 @@ class RemoteListViewModel @AssistedInject constructor(
mangaList.value = mangaList.value?.plus(list) ?: list mangaList.value = mangaList.value?.plus(list) ?: list
} }
hasNextPage.value = list.isNotEmpty() hasNextPage.value = list.isNotEmpty()
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) { } catch (e: Throwable) {
e.printStackTraceDebug() e.printStackTraceDebug()
listError.value = e listError.value = e

@ -3,15 +3,20 @@ package org.koitharu.kotatsu.scrobbling.domain
import androidx.collection.LongSparseArray import androidx.collection.LongSparseArray
import androidx.collection.getOrElse import androidx.collection.getOrElse
import androidx.core.text.parseAsHtml import androidx.core.text.parseAsHtml
import java.util.*
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaChapter
import org.koitharu.kotatsu.scrobbling.data.ScrobblingEntity 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.findKeyByValue
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import java.util.EnumMap
abstract class Scrobbler( abstract class Scrobbler(
protected val db: MangaDatabase, protected val db: MangaDatabase,
@ -47,7 +52,7 @@ abstract class Scrobbler(
private suspend fun ScrobblingEntity.toScrobblingInfo(mangaId: Long): ScrobblingInfo? { private suspend fun ScrobblingEntity.toScrobblingInfo(mangaId: Long): ScrobblingInfo? {
val mangaInfo = infoCache.getOrElse(targetId) { val mangaInfo = infoCache.getOrElse(targetId) {
runCatching { runCatchingCancellable {
getMangaInfo(targetId) getMangaInfo(targetId)
}.onFailure { }.onFailure {
it.printStackTraceDebug() it.printStackTraceDebug()
@ -72,7 +77,7 @@ abstract class Scrobbler(
} }
suspend fun Scrobbler.tryScrobble(mangaId: Long, chapter: MangaChapter): Boolean { suspend fun Scrobbler.tryScrobble(mangaId: Long, chapter: MangaChapter): Boolean {
return runCatching { return runCatchingCancellable {
scrobble(mangaId, chapter) scrobble(mangaId, chapter)
}.onFailure { }.onFailure {
it.printStackTraceDebug() it.printStackTraceDebug()

@ -21,6 +21,7 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.util.levenshteinDistance import org.koitharu.kotatsu.parsers.util.levenshteinDistance
import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider import org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
class MangaSearchRepository @Inject constructor( class MangaSearchRepository @Inject constructor(
private val settings: AppSettings, private val settings: AppSettings,
@ -33,7 +34,7 @@ class MangaSearchRepository @Inject constructor(
fun globalSearch(query: String, concurrency: Int = DEFAULT_CONCURRENCY): Flow<Manga> = fun globalSearch(query: String, concurrency: Int = DEFAULT_CONCURRENCY): Flow<Manga> =
settings.getMangaSources(includeHidden = false).asFlow() settings.getMangaSources(includeHidden = false).asFlow()
.flatMapMerge(concurrency) { source -> .flatMapMerge(concurrency) { source ->
runCatching { runCatchingCancellable {
mangaRepositoryFactory.create(source).getList( mangaRepositoryFactory.create(source).getList(
offset = 0, offset = 0,
query = query, query = query,

@ -4,6 +4,7 @@ import androidx.lifecycle.viewModelScope
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -12,7 +13,13 @@ import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.list.ui.MangaListViewModel 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.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
@ -47,6 +54,7 @@ class SearchViewModel @AssistedInject constructor(
actionStringRes = 0, actionStringRes = 0,
), ),
) )
else -> { else -> {
val result = ArrayList<ListModel>(list.size + 1) val result = ArrayList<ListModel>(list.size + 1)
list.toUi(result, mode) list.toUi(result, mode)
@ -94,6 +102,8 @@ class SearchViewModel @AssistedInject constructor(
mangaList.value = mangaList.value?.plus(list) ?: list mangaList.value = mangaList.value?.plus(list) ?: list
} }
hasNextPage.value = list.isNotEmpty() hasNextPage.value = list.isNotEmpty()
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) { } catch (e: Throwable) {
listError.value = e listError.value = e
} }

@ -20,6 +20,7 @@ import org.koitharu.kotatsu.list.ui.model.*
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
private const val MAX_PARALLELISM = 4 private const val MAX_PARALLELISM = 4
private const val MIN_HAS_MORE_ITEMS = 8 private const val MIN_HAS_MORE_ITEMS = 8
@ -54,6 +55,7 @@ class MultiSearchViewModel @AssistedInject constructor(
) )
}, },
) )
loading -> list + LoadingFooter loading -> list + LoadingFooter
else -> list else -> list
} }
@ -85,6 +87,8 @@ class MultiSearchViewModel @AssistedInject constructor(
loadingData.value = true loadingData.value = true
query.postValue(q) query.postValue(q)
searchImpl(q) searchImpl(q)
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) { } catch (e: Throwable) {
listError.value = e listError.value = e
} finally { } finally {
@ -98,7 +102,7 @@ class MultiSearchViewModel @AssistedInject constructor(
val dispatcher = Dispatchers.Default.limitedParallelism(MAX_PARALLELISM) val dispatcher = Dispatchers.Default.limitedParallelism(MAX_PARALLELISM)
val deferredList = sources.map { source -> val deferredList = sources.map { source ->
async(dispatcher) { async(dispatcher) {
runCatching { runCatchingCancellable {
val list = mangaRepositoryFactory.create(source).getList(offset = 0, query = q) val list = mangaRepositoryFactory.create(source).getList(offset = 0, query = q)
.toUi(ListMode.GRID) .toUi(ListMode.GRID)
if (list.isNotEmpty()) { if (list.isNotEmpty()) {

@ -8,7 +8,7 @@ import androidx.preference.Preference
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
@ -23,6 +23,7 @@ import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.FileSize import org.koitharu.kotatsu.utils.FileSize
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cache) { class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cache) {
@ -82,18 +83,22 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
clearCache(preference, CacheDir.PAGES) clearCache(preference, CacheDir.PAGES)
true true
} }
AppSettings.KEY_THUMBS_CACHE_CLEAR -> { AppSettings.KEY_THUMBS_CACHE_CLEAR -> {
clearCache(preference, CacheDir.THUMBS) clearCache(preference, CacheDir.THUMBS)
true true
} }
AppSettings.KEY_COOKIES_CLEAR -> { AppSettings.KEY_COOKIES_CLEAR -> {
clearCookies() clearCookies()
true true
} }
AppSettings.KEY_SEARCH_HISTORY_CLEAR -> { AppSettings.KEY_SEARCH_HISTORY_CLEAR -> {
clearSearchHistory(preference) clearSearchHistory(preference)
true true
} }
AppSettings.KEY_UPDATES_FEED_CLEAR -> { AppSettings.KEY_UPDATES_FEED_CLEAR -> {
viewLifecycleScope.launch { viewLifecycleScope.launch {
trackerRepo.clearLogs() trackerRepo.clearLogs()
@ -107,6 +112,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
} }
true true
} }
AppSettings.KEY_SHIKIMORI -> { AppSettings.KEY_SHIKIMORI -> {
if (!shikimoriRepository.isAuthorized) { if (!shikimoriRepository.isAuthorized) {
launchShikimoriAuth() launchShikimoriAuth()
@ -115,6 +121,7 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
super.onPreferenceTreeClick(preference) super.onPreferenceTreeClick(preference)
} }
} }
else -> super.onPreferenceTreeClick(preference) else -> super.onPreferenceTreeClick(preference)
} }
} }
@ -127,6 +134,8 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
storageManager.clearCache(cache) storageManager.clearCache(cache)
val size = storageManager.computeCacheSize(cache) val size = storageManager.computeCacheSize(cache)
preference.summary = FileSize.BYTES.format(ctx, size) preference.summary = FileSize.BYTES.format(ctx, size)
} catch (e: CancellationException) {
throw e
} catch (e: Exception) { } catch (e: Exception) {
preference.summary = e.getDisplayMessage(ctx.resources) preference.summary = e.getDisplayMessage(ctx.resources)
} finally { } finally {

@ -7,7 +7,6 @@ import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference import androidx.preference.Preference
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -20,7 +19,14 @@ import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity 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
import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class SourceSettingsFragment : BasePreferenceFragment(0) { class SourceSettingsFragment : BasePreferenceFragment(0) {
@ -66,12 +72,13 @@ class SourceSettingsFragment : BasePreferenceFragment(0) {
startActivity(SourceAuthActivity.newIntent(preference.context, source)) startActivity(SourceAuthActivity.newIntent(preference.context, source))
true true
} }
else -> super.onPreferenceTreeClick(preference) else -> super.onPreferenceTreeClick(preference)
} }
} }
private fun loadUsername(owner: LifecycleOwner, preference: Preference) = owner.lifecycleScope.launch { private fun loadUsername(owner: LifecycleOwner, preference: Preference) = owner.lifecycleScope.launch {
runCatching { runCatchingCancellable {
preference.summary = null preference.summary = null
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
requireNotNull(repository?.getAuthProvider()?.getUsername()) requireNotNull(repository?.getAuthProvider()?.getUsername())
@ -91,6 +98,7 @@ class SourceSettingsFragment : BasePreferenceFragment(0) {
).setAction(ExceptionResolver.getResolveStringId(error)) { resolveError(error) } ).setAction(ExceptionResolver.getResolveStringId(error)) { resolveError(error) }
.show() .show()
} }
else -> preference.summary = error.getDisplayMessage(preference.context.resources) else -> preference.summary = error.getDisplayMessage(preference.context.resources)
} }
error.printStackTraceDebug() error.printStackTraceDebug()

@ -11,13 +11,13 @@ import androidx.core.view.isVisible
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import java.io.File
import java.io.FileOutputStream
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.AlertDialogFragment import org.koitharu.kotatsu.base.ui.AlertDialogFragment
import org.koitharu.kotatsu.databinding.DialogProgressBinding import org.koitharu.kotatsu.databinding.DialogProgressBinding
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.progress.Progress import org.koitharu.kotatsu.utils.progress.Progress
import java.io.File
import java.io.FileOutputStream
@AndroidEntryPoint @AndroidEntryPoint
class BackupDialogFragment : AlertDialogFragment<DialogProgressBinding>() { class BackupDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
@ -91,6 +91,8 @@ class BackupDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
} }
Toast.makeText(requireContext(), R.string.backup_saved, Toast.LENGTH_LONG).show() Toast.makeText(requireContext(), R.string.backup_saved, Toast.LENGTH_LONG).show()
dismiss() dismiss()
} catch (e: InterruptedException) {
throw e
} catch (e: Exception) { } catch (e: Exception) {
onError(e) onError(e)
} }

@ -26,6 +26,7 @@ import org.koitharu.kotatsu.suggestions.domain.MangaSuggestion
import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository
import org.koitharu.kotatsu.utils.ext.asArrayList import org.koitharu.kotatsu.utils.ext.asArrayList
import org.koitharu.kotatsu.utils.ext.printStackTraceDebug import org.koitharu.kotatsu.utils.ext.printStackTraceDebug
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
import org.koitharu.kotatsu.utils.ext.trySetForeground import org.koitharu.kotatsu.utils.ext.trySetForeground
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.math.pow import kotlin.math.pow
@ -137,7 +138,7 @@ class SuggestionsWorker @AssistedInject constructor(
return (weight / maxWeight).pow(2.0).toFloat() return (weight / maxWeight).pow(2.0).toFloat()
} }
private suspend fun MangaRepository.getListSafe(tag: MangaTag) = runCatching { private suspend fun MangaRepository.getListSafe(tag: MangaTag) = runCatchingCancellable {
getList(offset = 0, sortOrder = SortOrder.UPDATED, tags = setOf(tag)) getList(offset = 0, sortOrder = SortOrder.UPDATED, tags = setOf(tag))
}.onFailure { error -> }.onFailure { error ->
error.printStackTraceDebug() error.printStackTraceDebug()

@ -9,6 +9,7 @@ import android.os.Bundle
import org.koitharu.kotatsu.sync.domain.SyncController import org.koitharu.kotatsu.sync.domain.SyncController
import org.koitharu.kotatsu.sync.domain.SyncHelper import org.koitharu.kotatsu.sync.domain.SyncHelper
import org.koitharu.kotatsu.utils.ext.onError import org.koitharu.kotatsu.utils.ext.onError
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
class FavouritesSyncAdapter(context: Context) : AbstractThreadedSyncAdapter(context, true) { class FavouritesSyncAdapter(context: Context) : AbstractThreadedSyncAdapter(context, true) {
@ -20,7 +21,7 @@ class FavouritesSyncAdapter(context: Context) : AbstractThreadedSyncAdapter(cont
syncResult: SyncResult, syncResult: SyncResult,
) { ) {
val syncHelper = SyncHelper(context, account, provider) val syncHelper = SyncHelper(context, account, provider)
runCatching { runCatchingCancellable {
syncHelper.syncFavourites(syncResult) syncHelper.syncFavourites(syncResult)
SyncController(context).setLastSync(account, authority, System.currentTimeMillis()) SyncController(context).setLastSync(account, authority, System.currentTimeMillis())
}.onFailure(syncResult::onError) }.onFailure(syncResult::onError)

@ -9,6 +9,7 @@ import android.os.Bundle
import org.koitharu.kotatsu.sync.domain.SyncController import org.koitharu.kotatsu.sync.domain.SyncController
import org.koitharu.kotatsu.sync.domain.SyncHelper import org.koitharu.kotatsu.sync.domain.SyncHelper
import org.koitharu.kotatsu.utils.ext.onError import org.koitharu.kotatsu.utils.ext.onError
import org.koitharu.kotatsu.utils.ext.runCatchingCancellable
class HistorySyncAdapter(context: Context) : AbstractThreadedSyncAdapter(context, true) { class HistorySyncAdapter(context: Context) : AbstractThreadedSyncAdapter(context, true) {
@ -20,7 +21,7 @@ class HistorySyncAdapter(context: Context) : AbstractThreadedSyncAdapter(context
syncResult: SyncResult, syncResult: SyncResult,
) { ) {
val syncHelper = SyncHelper(context, account, provider) val syncHelper = SyncHelper(context, account, provider)
runCatching { runCatchingCancellable {
syncHelper.syncHistory(syncResult) syncHelper.syncHistory(syncResult)
SyncController(context).setLastSync(account, authority, System.currentTimeMillis()) SyncController(context).setLastSync(account, authority, System.currentTimeMillis())
}.onFailure(syncResult::onError) }.onFailure(syncResult::onError)

@ -28,6 +28,7 @@ import org.koitharu.kotatsu.tracker.domain.Tracker
import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates
import org.koitharu.kotatsu.utils.PendingIntentCompat import org.koitharu.kotatsu.utils.PendingIntentCompat
import org.koitharu.kotatsu.utils.ext.referer 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.toBitmapOrNull
import org.koitharu.kotatsu.utils.ext.trySetForeground import org.koitharu.kotatsu.utils.ext.trySetForeground
@ -82,7 +83,7 @@ class TrackWorker @AssistedInject constructor(
val deferredList = coroutineScope { val deferredList = coroutineScope {
tracks.map { (track, channelId) -> tracks.map { (track, channelId) ->
async(dispatcher) { async(dispatcher) {
runCatching { runCatchingCancellable {
tracker.fetchUpdates(track, commit = true) tracker.fetchUpdates(track, commit = true)
}.onSuccess { updates -> }.onSuccess { updates ->
if (updates.isValid && updates.isNotEmpty()) { if (updates.isValid && updates.isNotEmpty()) {

@ -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<Task>()
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,
)
}

@ -46,7 +46,7 @@ val Context.connectivityManager: ConnectivityManager
fun String.toUriOrNull() = if (isEmpty()) null else Uri.parse(this) 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() val info = getForegroundInfo()
setForeground(info) setForeground(info)
}.isSuccess }.isSuccess
@ -95,6 +95,7 @@ fun SyncResult.onError(error: Throwable) {
is OperationApplicationException, is OperationApplicationException,
is SQLException, is SQLException,
-> databaseError = true -> databaseError = true
is JSONException -> stats.numParseExceptions++ is JSONException -> stats.numParseExceptions++
else -> if (BuildConfig.DEBUG) throw error else -> if (BuildConfig.DEBUG) throw error
} }

@ -3,16 +3,22 @@ package org.koitharu.kotatsu.utils.ext
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.res.Resources import android.content.res.Resources
import androidx.collection.arraySetOf import androidx.collection.arraySetOf
import java.net.SocketTimeoutException import kotlinx.coroutines.CancellationException
import java.net.UnknownHostException
import okio.FileNotFoundException import okio.FileNotFoundException
import org.acra.ktx.sendWithAcra import org.acra.ktx.sendWithAcra
import org.koitharu.kotatsu.R 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.SyncApiException
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.AuthRequiredException
import org.koitharu.kotatsu.parsers.exception.ContentUnavailableException import org.koitharu.kotatsu.parsers.exception.ContentUnavailableException
import org.koitharu.kotatsu.parsers.exception.NotFoundException import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.exception.ParseException import org.koitharu.kotatsu.parsers.exception.ParseException
import java.net.SocketTimeoutException
import java.net.UnknownHostException
fun Throwable.getDisplayMessage(resources: Resources): String = when (this) { fun Throwable.getDisplayMessage(resources: Resources): String = when (this) {
is AuthRequiredException -> resources.getString(R.string.auth_required) is AuthRequiredException -> resources.getString(R.string.auth_required)
@ -20,16 +26,19 @@ fun Throwable.getDisplayMessage(resources: Resources): String = when (this) {
is ActivityNotFoundException, is ActivityNotFoundException,
is UnsupportedOperationException, is UnsupportedOperationException,
-> resources.getString(R.string.operation_not_supported) -> resources.getString(R.string.operation_not_supported)
is UnsupportedFileException -> resources.getString(R.string.text_file_not_supported) is UnsupportedFileException -> resources.getString(R.string.text_file_not_supported)
is FileNotFoundException -> resources.getString(R.string.file_not_found) is FileNotFoundException -> resources.getString(R.string.file_not_found)
is EmptyHistoryException -> resources.getString(R.string.history_is_empty) is EmptyHistoryException -> resources.getString(R.string.history_is_empty)
is SyncApiException, is SyncApiException,
is ContentUnavailableException, is ContentUnavailableException,
-> message -> message
is ParseException -> shortMessage is ParseException -> shortMessage
is UnknownHostException, is UnknownHostException,
is SocketTimeoutException, is SocketTimeoutException,
-> resources.getString(R.string.network_error) -> resources.getString(R.string.network_error)
is WrongPasswordException -> resources.getString(R.string.wrong_password) is WrongPasswordException -> resources.getString(R.string.wrong_password)
is NotFoundException -> resources.getString(R.string.not_found_404) is NotFoundException -> resources.getString(R.string.not_found_404)
else -> localizedMessage else -> localizedMessage
@ -52,3 +61,15 @@ private val reportableExceptions = arraySetOf<Class<*>>(
ConcurrentModificationException::class.java, ConcurrentModificationException::class.java,
UnsupportedOperationException::class.java, UnsupportedOperationException::class.java,
) )
inline fun <R> runCatchingCancellable(block: () -> R): Result<R> {
return try {
Result.success(block())
} catch (e: InterruptedException) {
throw e
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
Result.failure(e)
}
}

@ -36,7 +36,7 @@ Object localProperty(String name, Object defaultValue = 'null') {
String currentBranch() { String currentBranch() {
def branchName = "" def branchName = ""
try { try {
branchName = "git rev-parse --abbrev-ref HEAD".execute().text.trim(); branchName = "git rev-parse --abbrev-ref HEAD".execute().text.trim()
} catch (ignored) { } catch (ignored) {
println "Git not found" println "Git not found"
} }

Loading…
Cancel
Save