diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt index eb78f1091d..15064a8195 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt @@ -13,17 +13,21 @@ import android.net.Uri import android.provider.MediaStore import java.io.FileNotFoundException import java.io.IOException -import java.io.InputStream import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.readium.r2.shared.InternalReadiumApi -import org.readium.r2.shared.extensions.* +import org.readium.r2.shared.extensions.coerceFirstNonNegative +import org.readium.r2.shared.extensions.queryProjection +import org.readium.r2.shared.extensions.readFully +import org.readium.r2.shared.extensions.requireLengthFitInt import org.readium.r2.shared.util.AbsoluteUrl import org.readium.r2.shared.util.DebugError import org.readium.r2.shared.util.Try import org.readium.r2.shared.util.data.ReadError import org.readium.r2.shared.util.flatMap +import org.readium.r2.shared.util.getOrElse +import org.readium.r2.shared.util.io.CountingInputStream import org.readium.r2.shared.util.mediatype.MediaType import org.readium.r2.shared.util.resource.Resource import org.readium.r2.shared.util.resource.filename @@ -45,9 +49,12 @@ public class ContentResource( private lateinit var _properties: Try + private var stream: CountingInputStream? = null + override val sourceUrl: AbsoluteUrl? = uri.toUrl() as? AbsoluteUrl override fun close() { + stream?.close() } override suspend fun properties(): Try { @@ -95,22 +102,12 @@ public class ContentResource( } private suspend fun readFully(): Try = - withStream { it.readFully() } + withStream(fromIndex = 0) { it.readFully() } private suspend fun readRange(range: LongRange): Try = - withStream { + withStream(fromIndex = range.first) { withContext(Dispatchers.IO) { - var skipped: Long = 0 - - while (skipped != range.first) { - skipped += it.skip(range.first - skipped) - if (skipped == 0L) { - throw IOException("Could not skip InputStream to read ranges from $uri.") - } - } - - val length = range.last - range.first + 1 - it.read(length) + it.readRange(range) } } @@ -134,16 +131,37 @@ public class ContentResource( return _length } - private suspend fun withStream(block: suspend (InputStream) -> T): Try { + private suspend fun withStream( + fromIndex: Long, + block: suspend (CountingInputStream) -> T + ): Try { + val stream = stream(fromIndex) + .getOrElse { return Try.failure(it) } + return Try.catching { - val stream = contentResolver.openInputStream(uri) + block(stream) + } + } + + private fun stream(fromIndex: Long): Try { + // Reuse the current stream if it didn't exceed the requested index. + stream + ?.takeIf { it.count <= fromIndex } + ?.let { return Try.success(it) } + + stream?.close() + + val contentStream = + contentResolver.openInputStream(uri) ?: return Try.failure( ReadError.Access( ContentResolverError.NotAvailable() ) ) - stream.use { block(stream) } - } + + stream = CountingInputStream(contentStream) + + return Try.success(stream!!) } private inline fun Try.Companion.catching(closure: () -> T): Try = diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/io/CountingInputStream.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/io/CountingInputStream.kt index 7712eabd4e..284ada89fa 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/io/CountingInputStream.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/io/CountingInputStream.kt @@ -75,7 +75,16 @@ public class CountingInputStream( return ByteArray(0) } - skip(range.first - count) + val toSkip = range.first - count + var skipped: Long = 0 + + while (skipped != toSkip) { + skipped += skip(toSkip - skipped) + if (skipped == 0L) { + throw IOException("Could not skip InputStream to read ranges.") + } + } + val length = range.last - range.first + 1 return read(length) } diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt index b390d50665..9a002af05d 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt @@ -79,7 +79,13 @@ internal class StreamingZipArchiveProvider { val datasourceChannel = ReadableChannelAdapter(readable, wrapError) val channel = wrapBaseChannel(datasourceChannel) val zipFile = ZipFile(channel, true) - StreamingZipContainer(zipFile, sourceUrl) + val sourceScheme = (readable as? Resource)?.sourceUrl?.scheme + val cacheEntryMaxSize = + when { + sourceScheme?.isContent ?: false -> 5242880 + else -> 0 + } + StreamingZipContainer(zipFile, sourceUrl, cacheEntryMaxSize) } internal suspend fun openFile(file: File): Container = withContext(Dispatchers.IO) { diff --git a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt index a93262b63d..7dfde7d00a 100644 --- a/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt +++ b/readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt @@ -39,7 +39,8 @@ import org.readium.r2.shared.util.zip.compress.archivers.zip.ZipFile internal class StreamingZipContainer( private val zipFile: ZipFile, - override val sourceUrl: AbsoluteUrl? + override val sourceUrl: AbsoluteUrl?, + private val cacheEntryMaxSize: Int = 0 ) : Container { private inner class Entry( @@ -47,6 +48,9 @@ internal class StreamingZipContainer( private val entry: ZipArchiveEntry ) : Resource { + private var cache: ByteArray? = + null + override val sourceUrl: AbsoluteUrl? get() = null override suspend fun properties(): ReadTry = @@ -102,8 +106,25 @@ internal class StreamingZipContainer( it.readFully() } - private fun readRange(range: LongRange): ByteArray = - stream(range.first).readRange(range) + private suspend fun readRange(range: LongRange): ByteArray = + when { + cache != null -> { + // If the entry is cached, its size fit into an Int. + val rangeSize = (range.last - range.first + 1).toInt() + cache!!.copyInto( + ByteArray(rangeSize), + startIndex = range.first.toInt(), + endIndex = range.last.toInt() + 1 + ) + } + + entry.size in 0 until cacheEntryMaxSize -> { + cache = readFully() + readRange(range) + } + else -> + stream(range.first).readRange(range) + } /** * Reading an entry in chunks (e.g. from the HTTP server) can be really slow if the entry