Fix blocking calls in coroutines

This commit is contained in:
Koitharu
2022-01-26 19:24:27 +02:00
parent 970200aa40
commit 355933c742
8 changed files with 77 additions and 74 deletions

View File

@@ -24,10 +24,6 @@ android {
} }
} }
} }
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
buildTypes { buildTypes {
debug { debug {
applicationIdSuffix = '.debug' applicationIdSuffix = '.debug'
@@ -45,16 +41,10 @@ android {
sourceSets { sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
} }
lintOptions { compileOptions {
disable 'MissingTranslation' sourceCompatibility JavaVersion.VERSION_1_8
abortOnError false targetCompatibility JavaVersion.VERSION_1_8
} }
testOptions {
unitTests.includeAndroidResources = true
unitTests.returnDefaultValues = false
}
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions { kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString() jvmTarget = JavaVersion.VERSION_1_8.toString()
freeCompilerArgs += [ freeCompilerArgs += [
@@ -63,6 +53,14 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
'-Xopt-in=kotlin.contracts.ExperimentalContracts', '-Xopt-in=kotlin.contracts.ExperimentalContracts',
] ]
} }
lintOptions {
disable 'MissingTranslation'
abortOnError false
}
testOptions {
unitTests.includeAndroidResources = true
unitTests.returnDefaultValues = false
}
} }
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])

View File

@@ -3,7 +3,9 @@ package org.koitharu.kotatsu.base.domain
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.util.Size import android.util.Size
import androidx.annotation.WorkerThread import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
@@ -24,18 +26,18 @@ object MangaUtils : KoinComponent {
* Automatic determine type of manga by page size * Automatic determine type of manga by page size
* @return ReaderMode.WEBTOON if page is wide * @return ReaderMode.WEBTOON if page is wide
*/ */
@WorkerThread
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun determineMangaIsWebtoon(pages: List<MangaPage>): Boolean? { suspend fun determineMangaIsWebtoon(pages: List<MangaPage>): Boolean? {
try { try {
val page = pages.medianOrNull() ?: return null val page = pages.medianOrNull() ?: return null
val url = MangaRepository(page.source).getPageUrl(page) val url = MangaRepository(page.source).getPageUrl(page)
val uri = Uri.parse(url) val uri = Uri.parse(url)
val size = if (uri.scheme == "cbz") { val size = if (uri.scheme == "cbz") {
val zip = ZipFile(uri.schemeSpecificPart) runInterruptible(Dispatchers.IO) {
val entry = zip.getEntry(uri.fragment) val zip = ZipFile(uri.schemeSpecificPart)
zip.getInputStream(entry).use { val entry = zip.getEntry(uri.fragment)
getBitmapSize(it) zip.getInputStream(entry).use {
getBitmapSize(it)
}
} }
} else { } else {
val client = get<OkHttpClient>() val client = get<OkHttpClient>()
@@ -46,7 +48,9 @@ object MangaUtils : KoinComponent {
.cacheControl(CacheUtils.CONTROL_DISABLED) .cacheControl(CacheUtils.CONTROL_DISABLED)
.build() .build()
client.newCall(request).await().use { client.newCall(request).await().use {
getBitmapSize(it.body?.byteStream()) withContext(Dispatchers.IO) {
getBitmapSize(it.body?.byteStream())
}
} }
} }
return size.width * 2 < size.height return size.width * 2 < size.height

View File

@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.core.backup
import android.content.Context import android.content.Context
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.json.JSONArray import org.json.JSONArray
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
@@ -33,8 +34,7 @@ class BackupArchive(file: File) : MutableZipFile(file) {
private const val DIR_BACKUPS = "backups" private const val DIR_BACKUPS = "backups"
@Suppress("BlockingMethodInNonBlockingContext") suspend fun createNew(context: Context): BackupArchive = runInterruptible(Dispatchers.IO) {
suspend fun createNew(context: Context): BackupArchive = withContext(Dispatchers.IO) {
val dir = context.run { val dir = context.run {
getExternalFilesDir(DIR_BACKUPS) ?: File(filesDir, DIR_BACKUPS) getExternalFilesDir(DIR_BACKUPS) ?: File(filesDir, DIR_BACKUPS)
} }

View File

@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.local.data
import androidx.annotation.CheckResult import androidx.annotation.CheckResult
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.koitharu.kotatsu.utils.ext.deleteAwait
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.FileOutputStream import java.io.FileOutputStream
@@ -13,7 +14,6 @@ class WritableCbzFile(private val file: File) {
private val dir = File(file.parentFile, file.nameWithoutExtension) private val dir = File(file.parentFile, file.nameWithoutExtension)
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun prepare() = withContext(Dispatchers.IO) { suspend fun prepare() = withContext(Dispatchers.IO) {
check(dir.list().isNullOrEmpty()) { check(dir.list().isNullOrEmpty()) {
"Dir ${dir.name} is not empty" "Dir ${dir.name} is not empty"
@@ -45,11 +45,10 @@ class WritableCbzFile(private val file: File) {
} }
@CheckResult @CheckResult
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun flush() = withContext(Dispatchers.IO) { suspend fun flush() = withContext(Dispatchers.IO) {
val tempFile = File(file.path + ".tmp") val tempFile = File(file.path + ".tmp")
if (tempFile.exists()) { if (tempFile.exists()) {
tempFile.delete() tempFile.deleteAwait()
} }
try { try {
runInterruptible { runInterruptible {
@@ -63,7 +62,7 @@ class WritableCbzFile(private val file: File) {
tempFile.renameTo(file) tempFile.renameTo(file)
} finally { } finally {
if (tempFile.exists()) { if (tempFile.exists()) {
tempFile.delete() tempFile.deleteAwait()
} }
} }
} }

View File

@@ -8,6 +8,7 @@ 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 kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.core.model.* import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository
@@ -43,38 +44,39 @@ class LocalMangaRepository(private val context: Context) : MangaRepository {
getFromFile(Uri.parse(manga.url).toFile()) getFromFile(Uri.parse(manga.url).toFile())
} else manga } else manga
@Suppress("BlockingMethodInNonBlockingContext")
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> { override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val uri = Uri.parse(chapter.url) return runInterruptible(Dispatchers.IO){
val file = uri.toFile() val uri = Uri.parse(chapter.url)
val zip = ZipFile(file) val file = uri.toFile()
val index = zip.getEntry(MangaZip.INDEX_ENTRY)?.let(zip::readText)?.let(::MangaIndex) val zip = ZipFile(file)
var entries = zip.entries().asSequence() val index = zip.getEntry(MangaZip.INDEX_ENTRY)?.let(zip::readText)?.let(::MangaIndex)
entries = if (index != null) { var entries = zip.entries().asSequence()
val pattern = index.getChapterNamesPattern(chapter) entries = if (index != null) {
entries.filter { x -> !x.isDirectory && x.name.substringBefore('.').matches(pattern) } val pattern = index.getChapterNamesPattern(chapter)
} else { entries.filter { x -> !x.isDirectory && x.name.substringBefore('.').matches(pattern) }
val parent = uri.fragment.orEmpty() } else {
entries.filter { x -> val parent = uri.fragment.orEmpty()
!x.isDirectory && x.name.substringBeforeLast( entries.filter { x ->
File.separatorChar, !x.isDirectory && x.name.substringBeforeLast(
"" File.separatorChar,
) == parent ""
) == parent
}
} }
entries
.toList()
.sortedWith(compareBy(AlphanumComparator()) { x -> x.name })
.map { x ->
val entryUri = zipUri(file, x.name)
MangaPage(
id = entryUri.longHashCode(),
url = entryUri,
preview = null,
referer = chapter.url,
source = MangaSource.LOCAL,
)
}
} }
return entries
.toList()
.sortedWith(compareBy(AlphanumComparator()) { x -> x.name })
.map { x ->
val entryUri = zipUri(file, x.name)
MangaPage(
id = entryUri.longHashCode(),
url = entryUri,
preview = null,
referer = chapter.url,
source = MangaSource.LOCAL,
)
}
} }
suspend fun delete(manga: Manga): Boolean { suspend fun delete(manga: Manga): Boolean {
@@ -138,20 +140,18 @@ class LocalMangaRepository(private val context: Context) : MangaRepository {
val file = runCatching { val file = runCatching {
Uri.parse(localManga.url).toFile() Uri.parse(localManga.url).toFile()
}.getOrNull() ?: return null }.getOrNull() ?: return null
return withContext(Dispatchers.IO) { return runInterruptible(Dispatchers.IO) {
@Suppress("BlockingMethodInNonBlockingContext")
ZipFile(file).use { zip -> ZipFile(file).use { zip ->
val entry = zip.getEntry(MangaZip.INDEX_ENTRY) val entry = zip.getEntry(MangaZip.INDEX_ENTRY)
val index = entry?.let(zip::readText)?.let(::MangaIndex) ?: return@withContext null val index = entry?.let(zip::readText)?.let(::MangaIndex)
index.getMangaInfo() index?.getMangaInfo()
} }
} }
} }
suspend fun findSavedManga(remoteManga: Manga): Manga? = withContext(Dispatchers.IO) { suspend fun findSavedManga(remoteManga: Manga): Manga? = runInterruptible(Dispatchers.IO) {
val files = getAllFiles() val files = getAllFiles()
for (file in files) { for (file in files) {
@Suppress("BlockingMethodInNonBlockingContext")
val index = ZipFile(file).use { zip -> val index = ZipFile(file).use { zip ->
val entry = zip.getEntry(MangaZip.INDEX_ENTRY) val entry = zip.getEntry(MangaZip.INDEX_ENTRY)
entry?.let(zip::readText)?.let(::MangaIndex) entry?.let(zip::readText)?.let(::MangaIndex)
@@ -159,7 +159,7 @@ class LocalMangaRepository(private val context: Context) : MangaRepository {
val info = index.getMangaInfo() ?: continue val info = index.getMangaInfo() ?: continue
if (info.id == remoteManga.id) { if (info.id == remoteManga.id) {
val fileUri = file.toUri().toString() val fileUri = file.toUri().toString()
return@withContext info.copy( return@runInterruptible info.copy(
source = MangaSource.LOCAL, source = MangaSource.LOCAL,
url = fileUri, url = fileUri,
chapters = info.chapters?.map { c -> c.copy(url = fileUri) } chapters = info.chapters?.map { c -> c.copy(url = fileUri) }

View File

@@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
@@ -81,10 +82,11 @@ class LocalListViewModel(
} }
val dest = settings.getStorageDir(context)?.let { File(it, name) } val dest = settings.getStorageDir(context)?.let { File(it, name) }
?: throw IOException("External files dir unavailable") ?: throw IOException("External files dir unavailable")
@Suppress("BlockingMethodInNonBlockingContext") runInterruptible {
contentResolver.openInputStream(uri)?.use { source -> contentResolver.openInputStream(uri)?.use { source ->
dest.outputStream().use { output -> dest.outputStream().use { output ->
source.copyTo(output) source.copyTo(output)
}
} }
} ?: throw IOException("Cannot open input stream: $uri") } ?: throw IOException("Cannot open input stream: $uri")
} }

View File

@@ -5,6 +5,7 @@ import android.net.Uri
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.backup.BackupArchive import org.koitharu.kotatsu.core.backup.BackupArchive
@@ -32,8 +33,7 @@ class RestoreViewModel(
} }
val contentResolver = context.contentResolver val contentResolver = context.contentResolver
@Suppress("BlockingMethodInNonBlockingContext") val backup = runInterruptible(Dispatchers.IO) {
val backup = withContext(Dispatchers.IO) {
val tempFile = File.createTempFile("backup_", ".tmp") val tempFile = File.createTempFile("backup_", ".tmp")
(contentResolver.openInputStream(uri) (contentResolver.openInputStream(uri)
?: throw FileNotFoundException()).use { input -> ?: throw FileNotFoundException()).use { input ->

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.utils
import androidx.annotation.CheckResult import androidx.annotation.CheckResult
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
@@ -11,12 +12,11 @@ import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream import java.util.zip.ZipOutputStream
@Suppress("BlockingMethodInNonBlockingContext")
open class MutableZipFile(val file: File) { open class MutableZipFile(val file: File) {
protected val dir = File(file.parentFile, file.nameWithoutExtension) protected val dir = File(file.parentFile, file.nameWithoutExtension)
suspend fun unpack(): Unit = withContext(Dispatchers.IO) { suspend fun unpack(): Unit = runInterruptible(Dispatchers.IO) {
check(dir.list().isNullOrEmpty()) { check(dir.list().isNullOrEmpty()) {
"Dir ${dir.name} is not empty" "Dir ${dir.name} is not empty"
} }
@@ -24,7 +24,7 @@ open class MutableZipFile(val file: File) {
dir.mkdir() dir.mkdir()
} }
if (!file.exists()) { if (!file.exists()) {
return@withContext return@runInterruptible
} }
ZipInputStream(FileInputStream(file)).use { zip -> ZipInputStream(FileInputStream(file)).use { zip ->
var entry = zip.nextEntry var entry = zip.nextEntry
@@ -45,7 +45,7 @@ open class MutableZipFile(val file: File) {
} }
@CheckResult @CheckResult
suspend fun flush(): Boolean = withContext(Dispatchers.IO) { suspend fun flush(): Boolean = runInterruptible(Dispatchers.IO) {
val tempFile = File(file.path + ".tmp") val tempFile = File(file.path + ".tmp")
if (tempFile.exists()) { if (tempFile.exists()) {
tempFile.delete() tempFile.delete()
@@ -57,7 +57,7 @@ open class MutableZipFile(val file: File) {
} }
zip.flush() zip.flush()
} }
return@withContext tempFile.renameTo(file) tempFile.renameTo(file)
} finally { } finally {
if (tempFile.exists()) { if (tempFile.exists()) {
tempFile.delete() tempFile.delete()