From e5b69475869ece06e832e234b55c420592e08337 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 2 Oct 2024 16:39:21 +0300 Subject: [PATCH] Fix StrictMode errors --- .../kotlin/org/koitharu/kotatsu/KotatsuApp.kt | 83 +++++++++---------- .../koitharu/kotatsu/core/fs/FileSequence.kt | 29 +++++-- .../kotatsu/core/util/CloseableSequence.kt | 3 + .../koitharu/kotatsu/core/util/ext/File.kt | 9 +- .../core/util/iterator/CloseableIterator.kt | 36 -------- .../koitharu/kotatsu/core/zip/ZipOutput.kt | 8 +- .../ui/pager/pages/MangaPageFetcher.kt | 5 +- .../download/ui/worker/DownloadWorker.kt | 3 +- .../local/data/LocalMangaRepository.kt | 18 ++-- .../local/data/input/LocalMangaDirInput.kt | 19 +++-- .../local/data/output/LocalMangaDirOutput.kt | 2 +- .../kotatsu/reader/domain/PageLoader.kt | 4 +- .../koitharu/kotatsu/sync/data/SyncAuthApi.kt | 3 +- 13 files changed, 105 insertions(+), 117 deletions(-) create mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/core/util/CloseableSequence.kt delete mode 100644 app/src/main/kotlin/org/koitharu/kotatsu/core/util/iterator/CloseableIterator.kt diff --git a/app/src/debug/kotlin/org/koitharu/kotatsu/KotatsuApp.kt b/app/src/debug/kotlin/org/koitharu/kotatsu/KotatsuApp.kt index 187a7b3d8..7426b5575 100644 --- a/app/src/debug/kotlin/org/koitharu/kotatsu/KotatsuApp.kt +++ b/app/src/debug/kotlin/org/koitharu/kotatsu/KotatsuApp.kt @@ -25,51 +25,50 @@ class KotatsuApp : BaseApp() { null } StrictMode.setThreadPolicy( - StrictMode.ThreadPolicy.Builder() - .detectAll() - .penaltyLog() - .run { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && notifier != null) { - penaltyListener(notifier.executor, notifier) - } else { - this - } - }.build(), + StrictMode.ThreadPolicy.Builder().apply { + detectNetwork() + detectDiskWrites() + detectCustomSlowCalls() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) detectUnbufferedIo() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) detectResourceMismatches() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) detectExplicitGc() + penaltyLog() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && notifier != null) { + penaltyListener(notifier.executor, notifier) + } + }.build(), ) StrictMode.setVmPolicy( - StrictMode.VmPolicy.Builder() - .detectActivityLeaks() - .detectLeakedSqlLiteObjects() - .detectLeakedClosableObjects() - .detectLeakedRegistrationObjects() - .setClassInstanceLimit(LocalMangaRepository::class.java, 1) - .setClassInstanceLimit(PagesCache::class.java, 1) - .setClassInstanceLimit(MangaLoaderContext::class.java, 1) - .setClassInstanceLimit(PageLoader::class.java, 1) - .setClassInstanceLimit(ReaderViewModel::class.java, 1) - .penaltyLog() - .run { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && notifier != null) { - penaltyListener(notifier.executor, notifier) - } else { - this - } - }.build(), - ) - FragmentStrictMode.defaultPolicy = FragmentStrictMode.Policy.Builder() - .penaltyDeath() - .detectFragmentReuse() - .detectWrongFragmentContainer() - .detectRetainInstanceUsage() - .detectSetUserVisibleHint() - .detectFragmentTagUsage() - .penaltyLog() - .run { - if (notifier != null) { - penaltyListener(notifier) - } else { - this + StrictMode.VmPolicy.Builder().apply { + detectActivityLeaks() + detectLeakedSqlLiteObjects() + detectLeakedClosableObjects() + detectLeakedRegistrationObjects() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) detectContentUriWithoutPermission() + detectFileUriExposure() + setClassInstanceLimit(LocalMangaRepository::class.java, 1) + setClassInstanceLimit(PagesCache::class.java, 1) + setClassInstanceLimit(MangaLoaderContext::class.java, 1) + setClassInstanceLimit(PageLoader::class.java, 1) + setClassInstanceLimit(ReaderViewModel::class.java, 1) + penaltyLog() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && notifier != null) { + penaltyListener(notifier.executor, notifier) } }.build() + ) + FragmentStrictMode.defaultPolicy = FragmentStrictMode.Policy.Builder().apply { + detectWrongFragmentContainer() + detectFragmentTagUsage() + detectRetainInstanceUsage() + detectSetUserVisibleHint() + detectWrongNestedHierarchy() + detectTargetFragmentUsage() + detectFragmentReuse() + penaltyLog() + if (notifier != null) { + penaltyListener(notifier) + } + }.build() } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/fs/FileSequence.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/fs/FileSequence.kt index ab8713642..e58b96067 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/fs/FileSequence.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/fs/FileSequence.kt @@ -1,20 +1,31 @@ package org.koitharu.kotatsu.core.fs import android.os.Build -import org.koitharu.kotatsu.core.util.iterator.CloseableIterator +import androidx.annotation.RequiresApi +import org.koitharu.kotatsu.core.util.CloseableSequence import org.koitharu.kotatsu.core.util.iterator.MappingIterator import java.io.File import java.nio.file.Files import java.nio.file.Path -class FileSequence(private val dir: File) : Sequence { +sealed interface FileSequence : CloseableSequence { - override fun iterator(): Iterator { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val stream = Files.newDirectoryStream(dir.toPath()) - CloseableIterator(MappingIterator(stream.iterator(), Path::toFile), stream) - } else { - dir.listFiles().orEmpty().iterator() - } + @RequiresApi(Build.VERSION_CODES.O) + class StreamImpl(dir: File) : FileSequence { + + private val stream = Files.newDirectoryStream(dir.toPath()) + + override fun iterator(): Iterator = MappingIterator(stream.iterator(), Path::toFile) + + override fun close() = stream.close() + } + + class ListImpl(dir: File) : FileSequence { + + private val list = dir.listFiles().orEmpty() + + override fun iterator(): Iterator = list.iterator() + + override fun close() = Unit } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/CloseableSequence.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/CloseableSequence.kt new file mode 100644 index 000000000..9cf3b317b --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/CloseableSequence.kt @@ -0,0 +1,3 @@ +package org.koitharu.kotatsu.core.util + +interface CloseableSequence : Sequence, AutoCloseable diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt index 8618d1ec2..2343f8ec7 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/File.kt @@ -15,7 +15,6 @@ import org.jetbrains.annotations.Blocking import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.fs.FileSequence import java.io.File -import java.io.FileFilter import java.io.InputStream import java.nio.file.attribute.BasicFileAttributes import java.util.zip.ZipEntry @@ -87,9 +86,13 @@ suspend fun File.computeSize(): Long = runInterruptible(Dispatchers.IO) { walkCompat(includeDirectories = false).sumOf { it.length() } } -fun File.children() = FileSequence(this) +inline fun File.withChildren(block: (children: Sequence) -> R): R = FileSequence(this).use(block) -fun Sequence.filterWith(filter: FileFilter): Sequence = filter { f -> filter.accept(f) } +fun FileSequence(dir: File): FileSequence = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + FileSequence.StreamImpl(dir) +} else { + FileSequence.ListImpl(dir) +} val File.creationTime get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/iterator/CloseableIterator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/iterator/CloseableIterator.kt deleted file mode 100644 index da59d5efc..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/iterator/CloseableIterator.kt +++ /dev/null @@ -1,36 +0,0 @@ -package org.koitharu.kotatsu.core.util.iterator - -import okhttp3.internal.closeQuietly -import okio.Closeable - -class CloseableIterator( - private val upstream: Iterator, - private val closeable: Closeable, -) : Iterator, Closeable { - - private var isClosed = false - - override fun hasNext(): Boolean { - val result = upstream.hasNext() - if (!result) { - close() - } - return result - } - - override fun next(): T { - try { - return upstream.next() - } catch (e: NoSuchElementException) { - close() - throw e - } - } - - override fun close() { - if (!isClosed) { - closeable.closeQuietly() - isClosed = true - } - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/zip/ZipOutput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/zip/ZipOutput.kt index 448341678..88b435350 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/zip/ZipOutput.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/zip/ZipOutput.kt @@ -3,7 +3,7 @@ package org.koitharu.kotatsu.core.zip import androidx.annotation.WorkerThread import androidx.collection.ArraySet import okio.Closeable -import org.koitharu.kotatsu.core.util.ext.children +import org.koitharu.kotatsu.core.util.ext.withChildren import java.io.File import java.io.FileInputStream import java.util.zip.Deflater @@ -91,8 +91,10 @@ class ZipOutput( } putNextEntry(entry) closeEntry() - fileToZip.children().forEach { childFile -> - appendFile(childFile, "$name/${childFile.name}") + fileToZip.withChildren { children -> + children.forEach { childFile -> + appendFile(childFile, "$name/${childFile.name}") + } } } else { FileInputStream(fileToZip).use { fis -> diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/MangaPageFetcher.kt b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/MangaPageFetcher.kt index 344a54004..e2d541e64 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/MangaPageFetcher.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/details/ui/pager/pages/MangaPageFetcher.kt @@ -29,6 +29,7 @@ import org.koitharu.kotatsu.local.data.isZipUri import org.koitharu.kotatsu.local.data.util.withExtraCloseable import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.util.mimeType +import org.koitharu.kotatsu.parsers.util.requireBody import org.koitharu.kotatsu.reader.domain.PageLoader import java.util.zip.ZipFile import javax.inject.Inject @@ -98,9 +99,7 @@ class MangaPageFetcher( if (!response.isSuccessful) { throw HttpException(response) } - val body = checkNotNull(response.body) { - "Null response" - } + val body = response.requireBody() val mimeType = response.mimeType val file = body.use { pagesCache.put(pageUrl, it.source()) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt index c62b7f807..0f985027b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt @@ -81,6 +81,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.parsers.util.mapToSet +import org.koitharu.kotatsu.parsers.util.requireBody import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.reader.domain.PageLoader import java.io.File @@ -359,7 +360,7 @@ class DownloadWorker @AssistedInject constructor( .use { response -> val file = File(destination, UUID.randomUUID().toString() + ".tmp") try { - checkNotNull(response.body).use { body -> + response.requireBody().use { body -> file.sink(append = false).buffer().use { it.writeAllCancellable(body.source()) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt index 9b99de67a..7a3206e44 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/LocalMangaRepository.kt @@ -15,10 +15,9 @@ import org.koitharu.kotatsu.core.model.isLocal import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.util.AlphanumComparator -import org.koitharu.kotatsu.core.util.ext.children import org.koitharu.kotatsu.core.util.ext.deleteAwait -import org.koitharu.kotatsu.core.util.ext.filterWith import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug +import org.koitharu.kotatsu.core.util.ext.withChildren import org.koitharu.kotatsu.local.data.index.LocalMangaIndex import org.koitharu.kotatsu.local.data.input.LocalMangaInput import org.koitharu.kotatsu.local.data.output.LocalMangaOutput @@ -216,10 +215,15 @@ class LocalMangaRepository @Inject constructor( } val dirs = storageManager.getWriteableDirs() runInterruptible(Dispatchers.IO) { - dirs.flatMap { dir -> - dir.children().filterWith(TempFileFilter()) - }.forEach { file -> - file.deleteRecursively() + val filter = TempFileFilter() + dirs.forEach { dir -> + dir.withChildren { children -> + children.forEach { child -> + if (filter.accept(child)) { + child.deleteRecursively() + } + } + } } } return true @@ -246,7 +250,7 @@ class LocalMangaRepository @Inject constructor( private suspend fun getAllFiles() = storageManager.getReadableDirs() .asSequence() .flatMap { dir -> - dir.children().filterNot { it.isHidden } + dir.withChildren { children -> children.filterNot { it.isHidden }.toList() } } private fun Collection.unwrap(): List = map { it.manga } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaDirInput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaDirInput.kt index 3667f1e31..302e4a4e7 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaDirInput.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/input/LocalMangaDirInput.kt @@ -6,11 +6,11 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible import org.koitharu.kotatsu.core.model.LocalMangaSource import org.koitharu.kotatsu.core.util.AlphanumComparator -import org.koitharu.kotatsu.core.util.ext.children import org.koitharu.kotatsu.core.util.ext.creationTime import org.koitharu.kotatsu.core.util.ext.longHashCode import org.koitharu.kotatsu.core.util.ext.toListSorted import org.koitharu.kotatsu.core.util.ext.walkCompat +import org.koitharu.kotatsu.core.util.ext.withChildren import org.koitharu.kotatsu.local.data.MangaIndex import org.koitharu.kotatsu.local.data.hasCbzExtension import org.koitharu.kotatsu.local.data.hasImageExtension @@ -101,13 +101,14 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) { override suspend fun getPages(chapter: MangaChapter): List = runInterruptible(Dispatchers.IO) { val file = chapter.url.toUri().toFile() if (file.isDirectory) { - file.children() - .filter { it.isFile && hasImageExtension(it) } - .toListSorted(compareBy(AlphanumComparator()) { x -> x.name }) - .map { - val pageUri = it.toUri().toString() - MangaPage(pageUri.longHashCode(), pageUri, null, LocalMangaSource) - } + file.withChildren { children -> + children + .filter { it.isFile && hasImageExtension(it) } + .toListSorted(compareBy(AlphanumComparator()) { x -> x.name }) + }.map { + val pageUri = it.toUri().toString() + MangaPage(pageUri.longHashCode(), pageUri, null, LocalMangaSource) + } } else { ZipFile(file).use { zip -> zip.entries() @@ -153,6 +154,6 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) { } private fun File.isChapterDirectory(): Boolean { - return isDirectory && children().any { hasImageExtension(it) } + return isDirectory && withChildren { children -> children.any { hasImageExtension(it) } } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaDirOutput.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaDirOutput.kt index dc9904cfc..32501d50d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaDirOutput.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/output/LocalMangaDirOutput.kt @@ -129,7 +129,7 @@ class LocalMangaDirOutput( index.getChapterFileName(chapter.value.id)?.let { return it } - val baseName = "${chapter.index}_${chapter.value.name.toFileNameSafe()}".take(18) + val baseName = "${chapter.index}_${chapter.value.name.toFileNameSafe()}".take(32) var i = 0 while (true) { val name = (if (i == 0) baseName else baseName + "_$i") + ".cbz" diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt index 14e7cccc2..1b3464c3c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/reader/domain/PageLoader.kt @@ -56,6 +56,7 @@ import org.koitharu.kotatsu.local.data.isFileUri import org.koitharu.kotatsu.local.data.isZipUri import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.util.requireBody import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.reader.ui.pager.ReaderPage import java.util.LinkedList @@ -233,8 +234,7 @@ class PageLoader @Inject constructor( else -> { val request = createPageRequest(pageUrl, page.source) imageProxyInterceptor.interceptPageRequest(request, okHttp).ensureSuccess().use { response -> - val body = checkNotNull(response.body) { "Null response body" } - body.withProgress(progress).use { + response.requireBody().withProgress(progress).use { cache.put(pageUrl, it.source()) } }.toUri() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/sync/data/SyncAuthApi.kt b/app/src/main/kotlin/org/koitharu/kotatsu/sync/data/SyncAuthApi.kt index 0a27f1ca1..26e3833a7 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/sync/data/SyncAuthApi.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/sync/data/SyncAuthApi.kt @@ -9,6 +9,7 @@ import org.koitharu.kotatsu.core.network.BaseHttpClient import org.koitharu.kotatsu.core.util.ext.toRequestBody import org.koitharu.kotatsu.parsers.util.await import org.koitharu.kotatsu.parsers.util.parseJson +import org.koitharu.kotatsu.parsers.util.parseRaw import org.koitharu.kotatsu.parsers.util.removeSurrounding import javax.inject.Inject @@ -30,7 +31,7 @@ class SyncAuthApi @Inject constructor( return response.parseJson().getString("token") } else { val code = response.code - val message = response.use { checkNotNull(it.body).string() }.removeSurrounding('"') + val message = response.parseRaw().removeSurrounding('"') throw SyncApiException(message, code) } }