From ccdeed24ef0a8aa7e0dd36fdb6ed61a62bd5ff7c Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 21 Sep 2022 23:29:01 +0100 Subject: [PATCH] using separate content provider query to find image file size rather than collect the content of the image itself into memory, should be much more effecient! --- .../kotlin/app/dapk/st/graph/AppModule.kt | 30 ++++++++----- .../dapk/st/matrix/crypto/CryptoService.kt | 1 + .../matrix/crypto/internal/MediaEncrypter.kt | 1 + .../dapk/st/matrix/message/MediaEncrypter.kt | 3 +- .../message/internal/ImageContentReader.kt | 12 ++--- .../message/internal/SendMessageUseCase.kt | 44 +++++++++++-------- .../st/matrix/message/internal/SendRequest.kt | 12 +++-- test-harness/src/test/kotlin/SmokeTest.kt | 1 - .../src/test/kotlin/test/TestMatrix.kt | 6 ++- 9 files changed, 65 insertions(+), 45 deletions(-) diff --git a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt index 677c984..296ee1f 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -8,6 +8,7 @@ import android.content.Intent import android.graphics.BitmapFactory import android.net.Uri import android.os.Build +import android.provider.OpenableColumns import app.dapk.db.DapkDb import app.dapk.st.BuildConfig import app.dapk.st.SharedPreferencesDelegate @@ -59,7 +60,7 @@ import app.dapk.st.work.TaskRunnerModule import app.dapk.st.work.WorkModule import com.squareup.sqldelight.android.AndroidSqliteDriver import kotlinx.coroutines.Dispatchers -import java.net.URI +import java.io.InputStream import java.time.Clock internal class AppModule(context: Application, logger: MatrixLogger) { @@ -303,6 +304,7 @@ internal class MatrixModules( val result = cryptoService.encrypt(input) MediaEncrypter.Result( uri = result.uri, + contentLength = result.contentLength, algorithm = result.algorithm, ext = result.ext, keyOperations = result.keyOperations, @@ -482,23 +484,27 @@ internal class DomainModules( } internal class AndroidImageContentReader(private val contentResolver: ContentResolver) : ImageContentReader { - override fun read(uri: String): ImageContentReader.ImageContent { + override fun meta(uri: String): ImageContentReader.ImageContent { val androidUri = Uri.parse(uri) val fileStream = contentResolver.openInputStream(androidUri) ?: throw IllegalArgumentException("Could not process $uri") val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } BitmapFactory.decodeStream(fileStream, null, options) - return contentResolver.openInputStream(androidUri)?.use { stream -> - val output = stream.readBytes() - ImageContentReader.ImageContent( - height = options.outHeight, - width = options.outWidth, - size = output.size.toLong(), - mimeType = options.outMimeType, - fileName = androidUri.lastPathSegment ?: "file", - uri = URI.create(uri) - ) + val fileSize = contentResolver.query(androidUri, null, null, null, null)?.use { cursor -> + cursor.moveToFirst() + val columnIndex = cursor.getColumnIndex(OpenableColumns.SIZE) + cursor.getLong(columnIndex) } ?: throw IllegalArgumentException("Could not process $uri") + + return ImageContentReader.ImageContent( + height = options.outHeight, + width = options.outWidth, + size = fileSize, + mimeType = options.outMimeType, + fileName = androidUri.lastPathSegment ?: "file", + ) } + + override fun inputStream(uri: String): InputStream = contentResolver.openInputStream(Uri.parse(uri))!! } \ No newline at end of file diff --git a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/CryptoService.kt b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/CryptoService.kt index 405a718..becaf3b 100644 --- a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/CryptoService.kt +++ b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/CryptoService.kt @@ -42,6 +42,7 @@ interface Crypto { data class MediaEncryptionResult( val uri: URI, + val contentLength: Long, val algorithm: String, val ext: Boolean, val keyOperations: List, diff --git a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/MediaEncrypter.kt b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/MediaEncrypter.kt index 17ca412..f8c2eea 100644 --- a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/MediaEncrypter.kt +++ b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/MediaEncrypter.kt @@ -63,6 +63,7 @@ class MediaEncrypter(private val base64: Base64) { return Crypto.MediaEncryptionResult( uri = outputFile.toURI(), + contentLength = outputFile.length(), algorithm = "A256CTR", ext = true, keyOperations = listOf("encrypt", "decrypt"), diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MediaEncrypter.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MediaEncrypter.kt index 2f6711a..10fa046 100644 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MediaEncrypter.kt +++ b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MediaEncrypter.kt @@ -10,6 +10,7 @@ fun interface MediaEncrypter { data class Result( val uri: URI, + val contentLength: Long, val algorithm: String, val ext: Boolean, val keyOperations: List, @@ -20,7 +21,7 @@ fun interface MediaEncrypter { val v: String, ) { - fun openStream() = File(uri).outputStream() + fun openStream() = File(uri).inputStream() } } diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/ImageContentReader.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/ImageContentReader.kt index dd8ecfa..ffce257 100644 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/ImageContentReader.kt +++ b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/ImageContentReader.kt @@ -1,10 +1,10 @@ package app.dapk.st.matrix.message.internal -import java.io.File -import java.net.URI +import java.io.InputStream interface ImageContentReader { - fun read(uri: String): ImageContent + fun meta(uri: String): ImageContent + fun inputStream(uri: String): InputStream data class ImageContent( val height: Int, @@ -12,9 +12,5 @@ interface ImageContentReader { val size: Long, val fileName: String, val mimeType: String, - val uri: URI - ) { - fun inputStream() = File(uri).inputStream() - fun outputStream() = File(uri).outputStream() - } + ) } \ No newline at end of file diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendMessageUseCase.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendMessageUseCase.kt index c5e52be..8398148 100644 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendMessageUseCase.kt +++ b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendMessageUseCase.kt @@ -7,8 +7,6 @@ import app.dapk.st.matrix.message.ApiSendResponse import app.dapk.st.matrix.message.MediaEncrypter import app.dapk.st.matrix.message.MessageEncrypter import app.dapk.st.matrix.message.MessageService.Message -import java.io.ByteArrayOutputStream -import java.io.File internal class SendMessageUseCase( private val httpClient: MatrixHttpClient, @@ -60,18 +58,23 @@ internal class SendMessageUseCase( } private suspend fun ApiMessageMapper.imageMessageRequest(message: Message.ImageMessage): HttpRequest { - val imageContent = imageContentReader.read(message.content.uri) + val imageMeta = imageContentReader.meta(message.content.uri) return when (message.sendEncrypted) { true -> { - val result = mediaEncrypter.encrypt(imageContent.inputStream()) - val bytes = File(result.uri).readBytes() - - val uri = httpClient.execute(uploadRequest(bytes, imageContent.fileName, "application/octet-stream")).contentUri + val result = mediaEncrypter.encrypt(imageContentReader.inputStream(message.content.uri)) + val uri = httpClient.execute( + uploadRequest( + result.openStream(), + result.contentLength, + imageMeta.fileName, + "application/octet-stream" + ) + ).contentUri val content = ApiMessage.ImageMessage.ImageContent( url = null, - filename = imageContent.fileName, + filename = imageMeta.fileName, file = ApiMessage.ImageMessage.ImageContent.File( url = uri, key = ApiMessage.ImageMessage.ImageContent.File.EncryptionMeta( @@ -86,9 +89,9 @@ internal class SendMessageUseCase( v = result.v, ), info = ApiMessage.ImageMessage.ImageContent.Info( - height = imageContent.height, - width = imageContent.width, - size = imageContent.size + height = imageMeta.height, + width = imageMeta.width, + size = imageMeta.size ) ) @@ -113,20 +116,25 @@ internal class SendMessageUseCase( } false -> { - val bytes = File(imageContent.uri).readBytes() - - val uri = httpClient.execute(uploadRequest(bytes, imageContent.fileName, imageContent.mimeType)).contentUri + val uri = httpClient.execute( + uploadRequest( + imageContentReader.inputStream(message.content.uri), + imageMeta.size, + imageMeta.fileName, + imageMeta.mimeType + ) + ).contentUri sendRequest( roomId = message.roomId, eventType = EventType.ROOM_MESSAGE, txId = message.localId, content = ApiMessage.ImageMessage.ImageContent( url = uri, - filename = imageContent.fileName, + filename = imageMeta.fileName, ApiMessage.ImageMessage.ImageContent.Info( - height = imageContent.height, - width = imageContent.width, - size = imageContent.size + height = imageMeta.height, + width = imageMeta.width, + size = imageMeta.size ) ), ) diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendRequest.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendRequest.kt index d3cc3d3..2057084 100644 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendRequest.kt +++ b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendRequest.kt @@ -11,8 +11,10 @@ import app.dapk.st.matrix.message.MessageEncrypter import app.dapk.st.matrix.message.MessageService.EventMessage import app.dapk.st.matrix.message.internal.ApiMessage.ImageMessage import app.dapk.st.matrix.message.internal.ApiMessage.TextMessage -import io.ktor.content.* import io.ktor.http.* +import io.ktor.http.content.* +import io.ktor.utils.io.jvm.javaio.* +import java.io.InputStream import java.util.* internal fun sendRequest(roomId: RoomId, eventType: EventType, txId: String, content: ApiMessageContent) = httpRequest( @@ -38,11 +40,15 @@ internal fun sendRequest(roomId: RoomId, eventType: EventType, content: EventMes } ) -internal fun uploadRequest(body: ByteArray, filename: String, contentType: String) = httpRequest( +internal fun uploadRequest(stream: InputStream, contentLength: Long, filename: String, contentType: String) = httpRequest( path = "_matrix/media/r0/upload/?filename=$filename", headers = listOf("Content-Type" to contentType), method = MatrixHttpClient.Method.POST, - body = ByteArrayContent(body, ContentType.parse(contentType)), + body = ChannelWriterContent( + body = { stream.copyTo(this) }, + contentType = ContentType.parse(contentType), + contentLength = contentLength, + ), ) fun txId() = "local.${UUID.randomUUID()}" \ No newline at end of file diff --git a/test-harness/src/test/kotlin/SmokeTest.kt b/test-harness/src/test/kotlin/SmokeTest.kt index 87be394..cccb5c8 100644 --- a/test-harness/src/test/kotlin/SmokeTest.kt +++ b/test-harness/src/test/kotlin/SmokeTest.kt @@ -87,7 +87,6 @@ class SmokeTest { bob.expectImageMessage(SharedState.sharedRoom, testImage, SharedState.alice.roomMember) } - @Test @Order(8) fun `can request and verify devices`() = testAfterInitialSync { alice, bob -> diff --git a/test-harness/src/test/kotlin/test/TestMatrix.kt b/test-harness/src/test/kotlin/test/TestMatrix.kt index 853ac60..023a3b1 100644 --- a/test-harness/src/test/kotlin/test/TestMatrix.kt +++ b/test-harness/src/test/kotlin/test/TestMatrix.kt @@ -146,6 +146,7 @@ class TestMatrix( val result = cryptoService.encrypt(input) MediaEncrypter.Result( uri = result.uri, + contentLength = result.contentLength, algorithm = result.algorithm, ext = result.ext, keyOperations = result.keyOperations, @@ -339,7 +340,7 @@ class JavaBase64 : Base64 { class JavaImageContentReader : ImageContentReader { - override fun read(uri: String): ImageContentReader.ImageContent { + override fun meta(uri: String): ImageContentReader.ImageContent { val file = File(uri) val size = file.length() val image = ImageIO.read(file) @@ -349,8 +350,9 @@ class JavaImageContentReader : ImageContentReader { size = size, mimeType = "image/${file.extension}", fileName = file.name, - uri = file.toURI(), ) } + override fun inputStream(uri: String) = File(uri).inputStream() + } \ No newline at end of file