Skip to content

Commit 94ba891

Browse files
authored
Speed up access to shared storage (#581)
1 parent f1deb9d commit 94ba891

File tree

10 files changed

+171
-200
lines changed

10 files changed

+171
-200
lines changed

readium/shared/src/main/java/org/readium/r2/shared/util/content/ContentResource.kt

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,21 @@ import android.net.Uri
1313
import android.provider.MediaStore
1414
import java.io.FileNotFoundException
1515
import java.io.IOException
16-
import java.io.InputStream
1716
import kotlinx.coroutines.Dispatchers
1817
import kotlinx.coroutines.runBlocking
1918
import kotlinx.coroutines.withContext
2019
import org.readium.r2.shared.InternalReadiumApi
21-
import org.readium.r2.shared.extensions.*
20+
import org.readium.r2.shared.extensions.coerceFirstNonNegative
21+
import org.readium.r2.shared.extensions.queryProjection
22+
import org.readium.r2.shared.extensions.readFully
23+
import org.readium.r2.shared.extensions.requireLengthFitInt
2224
import org.readium.r2.shared.util.AbsoluteUrl
2325
import org.readium.r2.shared.util.DebugError
2426
import org.readium.r2.shared.util.Try
2527
import org.readium.r2.shared.util.data.ReadError
2628
import org.readium.r2.shared.util.flatMap
29+
import org.readium.r2.shared.util.getOrElse
30+
import org.readium.r2.shared.util.io.CountingInputStream
2731
import org.readium.r2.shared.util.mediatype.MediaType
2832
import org.readium.r2.shared.util.resource.Resource
2933
import org.readium.r2.shared.util.resource.filename
@@ -45,9 +49,12 @@ public class ContentResource(
4549

4650
private lateinit var _properties: Try<Resource.Properties, ReadError>
4751

52+
private var stream: CountingInputStream? = null
53+
4854
override val sourceUrl: AbsoluteUrl? = uri.toUrl() as? AbsoluteUrl
4955

5056
override fun close() {
57+
stream?.close()
5158
}
5259

5360
override suspend fun properties(): Try<Resource.Properties, ReadError> {
@@ -95,22 +102,12 @@ public class ContentResource(
95102
}
96103

97104
private suspend fun readFully(): Try<ByteArray, ReadError> =
98-
withStream { it.readFully() }
105+
withStream(fromIndex = 0) { it.readFully() }
99106

100107
private suspend fun readRange(range: LongRange): Try<ByteArray, ReadError> =
101-
withStream {
108+
withStream(fromIndex = range.first) {
102109
withContext(Dispatchers.IO) {
103-
var skipped: Long = 0
104-
105-
while (skipped != range.first) {
106-
skipped += it.skip(range.first - skipped)
107-
if (skipped == 0L) {
108-
throw IOException("Could not skip InputStream to read ranges from $uri.")
109-
}
110-
}
111-
112-
val length = range.last - range.first + 1
113-
it.read(length)
110+
it.readRange(range)
114111
}
115112
}
116113

@@ -134,16 +131,37 @@ public class ContentResource(
134131
return _length
135132
}
136133

137-
private suspend fun <T> withStream(block: suspend (InputStream) -> T): Try<T, ReadError> {
134+
private suspend fun <T> withStream(
135+
fromIndex: Long,
136+
block: suspend (CountingInputStream) -> T
137+
): Try<T, ReadError> {
138+
val stream = stream(fromIndex)
139+
.getOrElse { return Try.failure(it) }
140+
138141
return Try.catching {
139-
val stream = contentResolver.openInputStream(uri)
142+
block(stream)
143+
}
144+
}
145+
146+
private fun stream(fromIndex: Long): Try<CountingInputStream, ReadError> {
147+
// Reuse the current stream if it didn't exceed the requested index.
148+
stream
149+
?.takeIf { it.count <= fromIndex }
150+
?.let { return Try.success(it) }
151+
152+
stream?.close()
153+
154+
val contentStream =
155+
contentResolver.openInputStream(uri)
140156
?: return Try.failure(
141157
ReadError.Access(
142158
ContentResolverError.NotAvailable()
143159
)
144160
)
145-
stream.use { block(stream) }
146-
}
161+
162+
stream = CountingInputStream(contentStream)
163+
164+
return Try.success(stream!!)
147165
}
148166

149167
private inline fun <T> Try.Companion.catching(closure: () -> T): Try<T, ReadError> =

readium/shared/src/main/java/org/readium/r2/shared/util/io/CountingInputStream.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,16 @@ public class CountingInputStream(
7575
return ByteArray(0)
7676
}
7777

78-
skip(range.first - count)
78+
val toSkip = range.first - count
79+
var skipped: Long = 0
80+
81+
while (skipped != toSkip) {
82+
skipped += skip(toSkip - skipped)
83+
if (skipped == 0L) {
84+
throw IOException("Could not skip InputStream to read ranges.")
85+
}
86+
}
87+
7988
val length = range.last - range.first + 1
8089
return read(length)
8190
}

readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipArchiveProvider.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,13 @@ internal class StreamingZipArchiveProvider {
7979
val datasourceChannel = ReadableChannelAdapter(readable, wrapError)
8080
val channel = wrapBaseChannel(datasourceChannel)
8181
val zipFile = ZipFile(channel, true)
82-
StreamingZipContainer(zipFile, sourceUrl)
82+
val sourceScheme = (readable as? Resource)?.sourceUrl?.scheme
83+
val cacheEntryMaxSize =
84+
when {
85+
sourceScheme?.isContent ?: false -> 5242880
86+
else -> 0
87+
}
88+
StreamingZipContainer(zipFile, sourceUrl, cacheEntryMaxSize)
8389
}
8490

8591
internal suspend fun openFile(file: File): Container<Resource> = withContext(Dispatchers.IO) {

readium/shared/src/main/java/org/readium/r2/shared/util/zip/StreamingZipContainer.kt

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,18 @@ import org.readium.r2.shared.util.zip.compress.archivers.zip.ZipFile
3939

4040
internal class StreamingZipContainer(
4141
private val zipFile: ZipFile,
42-
override val sourceUrl: AbsoluteUrl?
42+
override val sourceUrl: AbsoluteUrl?,
43+
private val cacheEntryMaxSize: Int = 0
4344
) : Container<Resource> {
4445

4546
private inner class Entry(
4647
private val url: Url,
4748
private val entry: ZipArchiveEntry
4849
) : Resource {
4950

51+
private var cache: ByteArray? =
52+
null
53+
5054
override val sourceUrl: AbsoluteUrl? get() = null
5155

5256
override suspend fun properties(): ReadTry<Resource.Properties> =
@@ -102,8 +106,25 @@ internal class StreamingZipContainer(
102106
it.readFully()
103107
}
104108

105-
private fun readRange(range: LongRange): ByteArray =
106-
stream(range.first).readRange(range)
109+
private suspend fun readRange(range: LongRange): ByteArray =
110+
when {
111+
cache != null -> {
112+
// If the entry is cached, its size fit into an Int.
113+
val rangeSize = (range.last - range.first + 1).toInt()
114+
cache!!.copyInto(
115+
ByteArray(rangeSize),
116+
startIndex = range.first.toInt(),
117+
endIndex = range.last.toInt() + 1
118+
)
119+
}
120+
121+
entry.size in 0 until cacheEntryMaxSize -> {
122+
cache = readFully()
123+
readRange(range)
124+
}
125+
else ->
126+
stream(range.first).readRange(range)
127+
}
107128

108129
/**
109130
* Reading an entry in chunks (e.g. from the HTTP server) can be really slow if the entry

test-app/src/main/java/org/readium/r2/testapp/domain/Bookshelf.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ class Bookshelf(
6363
}
6464
}
6565

66+
fun importPublicationFromHttp(
67+
url: AbsoluteUrl
68+
) {
69+
coroutineScope.launch {
70+
addBookFeedback(publicationRetriever.retrieveFromHttp(url))
71+
}
72+
}
73+
6674
fun importPublicationFromOpds(
6775
publication: Publication
6876
) {

test-app/src/main/java/org/readium/r2/testapp/domain/ImportError.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package org.readium.r2.testapp.domain
99
import org.readium.r2.lcp.LcpError
1010
import org.readium.r2.shared.util.DebugError
1111
import org.readium.r2.shared.util.Error
12+
import org.readium.r2.shared.util.content.ContentResolverError
1213
import org.readium.r2.shared.util.file.FileSystemError
1314
import org.readium.r2.shared.util.http.HttpError
1415
import org.readium.r2.testapp.R
@@ -36,6 +37,10 @@ sealed class ImportError(
3637
override val cause: FileSystemError
3738
) : ImportError(cause)
3839

40+
class ContentResolver(
41+
override val cause: ContentResolverError
42+
) : ImportError(cause)
43+
3944
class Download(
4045
override val cause: HttpError
4146
) : ImportError(cause)
@@ -57,6 +62,7 @@ sealed class ImportError(
5762
is Opds -> UserError(R.string.import_publication_no_acquisition, cause = this)
5863
is Publication -> cause.toUserError()
5964
is FileSystem -> cause.toUserError()
65+
is ContentResolver -> cause.toUserError()
6066
is InconsistentState -> UserError(
6167
R.string.import_publication_inconsistent_state,
6268
cause = this

test-app/src/main/java/org/readium/r2/testapp/domain/PublicationRetriever.kt

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,10 @@ import timber.log.Timber
4141
class PublicationRetriever(
4242
context: Context,
4343
private val assetRetriever: AssetRetriever,
44-
httpClient: HttpClient,
44+
private val httpClient: HttpClient,
4545
lcpService: LcpService?,
4646
private val bookshelfDir: File,
47-
tempDir: File
47+
private val tempDir: File
4848
) {
4949
data class Result(
5050
val publication: File,
@@ -109,6 +109,46 @@ class PublicationRetriever(
109109
)
110110
}
111111

112+
suspend fun retrieveFromHttp(
113+
url: AbsoluteUrl
114+
): Try<Result, ImportError> {
115+
val request = HttpRequest(
116+
url,
117+
headers = emptyMap()
118+
)
119+
120+
val tempFile = when (val result = httpClient.stream(request)) {
121+
is Try.Failure ->
122+
return Try.failure(ImportError.Download(result.value))
123+
is Try.Success -> {
124+
result.value.body
125+
.copyToNewFile(tempDir)
126+
.getOrElse { return Try.failure(ImportError.FileSystem(it)) }
127+
}
128+
}
129+
130+
val localResult = localPublicationRetriever
131+
.retrieve(tempFile)
132+
.getOrElse {
133+
tryOrLog { tempFile.delete() }
134+
return Try.failure(it)
135+
}
136+
137+
val finalResult = moveToBookshelfDir(
138+
localResult.tempFile,
139+
localResult.format,
140+
localResult.coverUrl
141+
)
142+
.getOrElse {
143+
tryOrLog { localResult.tempFile.delete() }
144+
return Try.failure(it)
145+
}
146+
147+
return Try.success(
148+
Result(finalResult.publication, finalResult.format, finalResult.coverUrl)
149+
)
150+
}
151+
112152
private suspend fun moveToBookshelfDir(
113153
tempFile: File,
114154
format: Format?,
@@ -167,7 +207,7 @@ private class LocalPublicationRetriever(
167207
): Try<Result, ImportError> {
168208
val tempFile = uri.copyToTempFile(context, tempDir)
169209
.getOrElse {
170-
return Try.failure(ImportError.FileSystem(FileSystemError.IO(it)))
210+
return Try.failure(ImportError.ContentResolver(it))
171211
}
172212
return retrieveFromStorage(tempFile, coverUrl = null)
173213
.onFailure { tryOrLog { tempFile.delete() } }

0 commit comments

Comments
 (0)