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 new file mode 100644 index 000000000..ab8713642 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/fs/FileSequence.kt @@ -0,0 +1,20 @@ +package org.koitharu.kotatsu.core.fs + +import android.os.Build +import org.koitharu.kotatsu.core.util.iterator.CloseableIterator +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 { + + 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() + } + } +} 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 79877887b..54505f1ba 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 @@ -13,6 +13,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.withContext import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.fs.FileSequence import java.io.File import java.io.FileFilter import java.util.zip.ZipEntry @@ -73,11 +74,10 @@ suspend fun File.computeSize(): Long = runInterruptible(Dispatchers.IO) { @WorkerThread private fun computeSizeInternal(file: File): Long { - if (file.isDirectory) { - val files = file.listFiles() ?: return 0L - return files.sumOf { computeSizeInternal(it) } + return if (file.isDirectory) { + file.children().sumOf { computeSizeInternal(it) } } else { - return file.length() + file.length() } } @@ -86,9 +86,8 @@ fun File.listFilesRecursive(filter: FileFilter? = null): Sequence = sequen } private suspend fun SequenceScope.listFilesRecursiveImpl(root: File, filter: FileFilter?) { - val ss = root.list() ?: return - for (s in ss) { - val f = File(root, s) + val ss = root.children() + for (f in ss) { if (f.isDirectory) { listFilesRecursiveImpl(f, filter) } else if (filter == null || filter.accept(f)) { @@ -96,3 +95,7 @@ private suspend fun SequenceScope.listFilesRecursiveImpl(root: File, filte } } } + +fun File.children() = FileSequence(this) + +fun Sequence.filterWith(filter: FileFilter): Sequence = filter { f -> filter.accept(f) } 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 new file mode 100644 index 000000000..da59d5efc --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/iterator/CloseableIterator.kt @@ -0,0 +1,36 @@ +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/util/iterator/MappingIterator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/iterator/MappingIterator.kt new file mode 100644 index 000000000..8ac9f9d41 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/iterator/MappingIterator.kt @@ -0,0 +1,13 @@ +package org.koitharu.kotatsu.core.util.iterator + +import org.koitharu.kotatsu.R + +class MappingIterator( + private val upstream: Iterator, + private val mapper: (T) -> R, +) : Iterator { + + override fun hasNext(): Boolean = upstream.hasNext() + + override fun next(): R = mapper(upstream.next()) +} 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 fd38f1c04..448341678 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,6 +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 java.io.File import java.io.FileInputStream import java.util.zip.Deflater @@ -90,7 +91,7 @@ class ZipOutput( } putNextEntry(entry) closeEntry() - fileToZip.listFiles()?.forEach { childFile -> + fileToZip.children().forEach { childFile -> appendFile(childFile, "$name/${childFile.name}") } } else { 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 7950f4575..a5eb18cb8 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,7 +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.CompositeMutex +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.local.data.input.LocalMangaInput import org.koitharu.kotatsu.local.data.output.LocalMangaOutput @@ -128,9 +130,6 @@ class LocalMangaRepository @Inject constructor( suspend fun findSavedManga(remoteManga: Manga): LocalManga? { val files = getAllFiles() - if (files.isEmpty()) { - return null - } return channelFlow { for (file in files) { launch { @@ -172,7 +171,7 @@ class LocalMangaRepository @Inject constructor( val dirs = storageManager.getWriteableDirs() runInterruptible(Dispatchers.IO) { dirs.flatMap { dir -> - dir.listFiles(TempFileFilter())?.toList().orEmpty() + dir.children().filterWith(TempFileFilter()) }.forEach { file -> file.deleteRecursively() } @@ -189,7 +188,7 @@ class LocalMangaRepository @Inject constructor( } private suspend fun getRawList(): ArrayList { - val files = getAllFiles() + val files = getAllFiles().toList() // TODO remove toList() return coroutineScope { val dispatcher = Dispatchers.IO.limitedParallelism(MAX_PARALLELISM) files.map { file -> @@ -200,8 +199,8 @@ class LocalMangaRepository @Inject constructor( }.filterNotNullTo(ArrayList(files.size)) } - private suspend fun getAllFiles() = storageManager.getReadableDirs().flatMap { dir -> - dir.listFiles()?.toList().orEmpty() + private suspend fun getAllFiles() = storageManager.getReadableDirs().asSequence().flatMap { dir -> + dir.children() } private fun Collection.unwrap(): List = map { it.manga } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/TempFileFilter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/TempFileFilter.kt index 696a433b2..abd269f15 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/data/TempFileFilter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/data/TempFileFilter.kt @@ -1,11 +1,16 @@ package org.koitharu.kotatsu.local.data import java.io.File +import java.io.FileFilter import java.io.FilenameFilter -class TempFileFilter : FilenameFilter { +class TempFileFilter : FilenameFilter, FileFilter { override fun accept(dir: File, name: String): Boolean { return name.endsWith(".tmp", ignoreCase = true) } + + override fun accept(file: File): Boolean { + return file.name.endsWith(".tmp", ignoreCase = true) + } } 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 c2cac2c60..bdfe8adf7 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 @@ -4,6 +4,7 @@ import androidx.core.net.toFile import androidx.core.net.toUri import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible +import org.koitharu.kotatsu.core.util.AlphanumComparator import org.koitharu.kotatsu.core.util.ext.listFilesRecursive import org.koitharu.kotatsu.core.util.ext.longHashCode import org.koitharu.kotatsu.core.util.ext.toListSorted @@ -88,7 +89,7 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) { val file = chapter.url.toUri().toFile() if (file.isDirectory) { file.listFilesRecursive(ImageFileFilter()) - .toListSorted(compareBy(org.koitharu.kotatsu.core.util.AlphanumComparator()) { x -> x.name }) + .toListSorted(compareBy(AlphanumComparator()) { x -> x.name }) .map { val pageUri = it.toUri().toString() MangaPage( @@ -104,7 +105,7 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) { .asSequence() .filter { x -> !x.isDirectory } .map { it.name } - .toListSorted(org.koitharu.kotatsu.core.util.AlphanumComparator()) + .toListSorted(AlphanumComparator()) .map { val pageUri = zipUri(file, it) MangaPage( @@ -121,7 +122,7 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) { private fun String.toHumanReadable() = replace("_", " ").toCamelCase() private fun getChaptersFiles(): List = root.listFilesRecursive(CbzFilter()) - .toListSorted(compareBy(org.koitharu.kotatsu.core.util.AlphanumComparator()) { x -> x.name }) + .toListSorted(compareBy(AlphanumComparator()) { x -> x.name }) private fun findFirstImageEntry(): String? { val filter = ImageFileFilter()