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!

This commit is contained in:
Adam Brown 2022-09-21 23:29:01 +01:00
parent 02e76548e3
commit ccdeed24ef
9 changed files with 65 additions and 45 deletions

View File

@ -8,6 +8,7 @@ import android.content.Intent
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.provider.OpenableColumns
import app.dapk.db.DapkDb import app.dapk.db.DapkDb
import app.dapk.st.BuildConfig import app.dapk.st.BuildConfig
import app.dapk.st.SharedPreferencesDelegate import app.dapk.st.SharedPreferencesDelegate
@ -59,7 +60,7 @@ import app.dapk.st.work.TaskRunnerModule
import app.dapk.st.work.WorkModule import app.dapk.st.work.WorkModule
import com.squareup.sqldelight.android.AndroidSqliteDriver import com.squareup.sqldelight.android.AndroidSqliteDriver
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import java.net.URI import java.io.InputStream
import java.time.Clock import java.time.Clock
internal class AppModule(context: Application, logger: MatrixLogger) { internal class AppModule(context: Application, logger: MatrixLogger) {
@ -303,6 +304,7 @@ internal class MatrixModules(
val result = cryptoService.encrypt(input) val result = cryptoService.encrypt(input)
MediaEncrypter.Result( MediaEncrypter.Result(
uri = result.uri, uri = result.uri,
contentLength = result.contentLength,
algorithm = result.algorithm, algorithm = result.algorithm,
ext = result.ext, ext = result.ext,
keyOperations = result.keyOperations, keyOperations = result.keyOperations,
@ -482,23 +484,27 @@ internal class DomainModules(
} }
internal class AndroidImageContentReader(private val contentResolver: ContentResolver) : ImageContentReader { 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 androidUri = Uri.parse(uri)
val fileStream = contentResolver.openInputStream(androidUri) ?: throw IllegalArgumentException("Could not process $uri") val fileStream = contentResolver.openInputStream(androidUri) ?: throw IllegalArgumentException("Could not process $uri")
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeStream(fileStream, null, options) BitmapFactory.decodeStream(fileStream, null, options)
return contentResolver.openInputStream(androidUri)?.use { stream -> val fileSize = contentResolver.query(androidUri, null, null, null, null)?.use { cursor ->
val output = stream.readBytes() cursor.moveToFirst()
ImageContentReader.ImageContent( val columnIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
height = options.outHeight, cursor.getLong(columnIndex)
width = options.outWidth,
size = output.size.toLong(),
mimeType = options.outMimeType,
fileName = androidUri.lastPathSegment ?: "file",
uri = URI.create(uri)
)
} ?: throw IllegalArgumentException("Could not process $uri") } ?: 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))!!
} }

View File

@ -42,6 +42,7 @@ interface Crypto {
data class MediaEncryptionResult( data class MediaEncryptionResult(
val uri: URI, val uri: URI,
val contentLength: Long,
val algorithm: String, val algorithm: String,
val ext: Boolean, val ext: Boolean,
val keyOperations: List<String>, val keyOperations: List<String>,

View File

@ -63,6 +63,7 @@ class MediaEncrypter(private val base64: Base64) {
return Crypto.MediaEncryptionResult( return Crypto.MediaEncryptionResult(
uri = outputFile.toURI(), uri = outputFile.toURI(),
contentLength = outputFile.length(),
algorithm = "A256CTR", algorithm = "A256CTR",
ext = true, ext = true,
keyOperations = listOf("encrypt", "decrypt"), keyOperations = listOf("encrypt", "decrypt"),

View File

@ -10,6 +10,7 @@ fun interface MediaEncrypter {
data class Result( data class Result(
val uri: URI, val uri: URI,
val contentLength: Long,
val algorithm: String, val algorithm: String,
val ext: Boolean, val ext: Boolean,
val keyOperations: List<String>, val keyOperations: List<String>,
@ -20,7 +21,7 @@ fun interface MediaEncrypter {
val v: String, val v: String,
) { ) {
fun openStream() = File(uri).outputStream() fun openStream() = File(uri).inputStream()
} }
} }

View File

@ -1,10 +1,10 @@
package app.dapk.st.matrix.message.internal package app.dapk.st.matrix.message.internal
import java.io.File import java.io.InputStream
import java.net.URI
interface ImageContentReader { interface ImageContentReader {
fun read(uri: String): ImageContent fun meta(uri: String): ImageContent
fun inputStream(uri: String): InputStream
data class ImageContent( data class ImageContent(
val height: Int, val height: Int,
@ -12,9 +12,5 @@ interface ImageContentReader {
val size: Long, val size: Long,
val fileName: String, val fileName: String,
val mimeType: String, val mimeType: String,
val uri: URI )
) {
fun inputStream() = File(uri).inputStream()
fun outputStream() = File(uri).outputStream()
}
} }

View File

@ -7,8 +7,6 @@ import app.dapk.st.matrix.message.ApiSendResponse
import app.dapk.st.matrix.message.MediaEncrypter import app.dapk.st.matrix.message.MediaEncrypter
import app.dapk.st.matrix.message.MessageEncrypter import app.dapk.st.matrix.message.MessageEncrypter
import app.dapk.st.matrix.message.MessageService.Message import app.dapk.st.matrix.message.MessageService.Message
import java.io.ByteArrayOutputStream
import java.io.File
internal class SendMessageUseCase( internal class SendMessageUseCase(
private val httpClient: MatrixHttpClient, private val httpClient: MatrixHttpClient,
@ -60,18 +58,23 @@ internal class SendMessageUseCase(
} }
private suspend fun ApiMessageMapper.imageMessageRequest(message: Message.ImageMessage): HttpRequest<ApiSendResponse> { private suspend fun ApiMessageMapper.imageMessageRequest(message: Message.ImageMessage): HttpRequest<ApiSendResponse> {
val imageContent = imageContentReader.read(message.content.uri) val imageMeta = imageContentReader.meta(message.content.uri)
return when (message.sendEncrypted) { return when (message.sendEncrypted) {
true -> { true -> {
val result = mediaEncrypter.encrypt(imageContent.inputStream()) val result = mediaEncrypter.encrypt(imageContentReader.inputStream(message.content.uri))
val bytes = File(result.uri).readBytes() val uri = httpClient.execute(
uploadRequest(
val uri = httpClient.execute(uploadRequest(bytes, imageContent.fileName, "application/octet-stream")).contentUri result.openStream(),
result.contentLength,
imageMeta.fileName,
"application/octet-stream"
)
).contentUri
val content = ApiMessage.ImageMessage.ImageContent( val content = ApiMessage.ImageMessage.ImageContent(
url = null, url = null,
filename = imageContent.fileName, filename = imageMeta.fileName,
file = ApiMessage.ImageMessage.ImageContent.File( file = ApiMessage.ImageMessage.ImageContent.File(
url = uri, url = uri,
key = ApiMessage.ImageMessage.ImageContent.File.EncryptionMeta( key = ApiMessage.ImageMessage.ImageContent.File.EncryptionMeta(
@ -86,9 +89,9 @@ internal class SendMessageUseCase(
v = result.v, v = result.v,
), ),
info = ApiMessage.ImageMessage.ImageContent.Info( info = ApiMessage.ImageMessage.ImageContent.Info(
height = imageContent.height, height = imageMeta.height,
width = imageContent.width, width = imageMeta.width,
size = imageContent.size size = imageMeta.size
) )
) )
@ -113,20 +116,25 @@ internal class SendMessageUseCase(
} }
false -> { false -> {
val bytes = File(imageContent.uri).readBytes() val uri = httpClient.execute(
uploadRequest(
val uri = httpClient.execute(uploadRequest(bytes, imageContent.fileName, imageContent.mimeType)).contentUri imageContentReader.inputStream(message.content.uri),
imageMeta.size,
imageMeta.fileName,
imageMeta.mimeType
)
).contentUri
sendRequest( sendRequest(
roomId = message.roomId, roomId = message.roomId,
eventType = EventType.ROOM_MESSAGE, eventType = EventType.ROOM_MESSAGE,
txId = message.localId, txId = message.localId,
content = ApiMessage.ImageMessage.ImageContent( content = ApiMessage.ImageMessage.ImageContent(
url = uri, url = uri,
filename = imageContent.fileName, filename = imageMeta.fileName,
ApiMessage.ImageMessage.ImageContent.Info( ApiMessage.ImageMessage.ImageContent.Info(
height = imageContent.height, height = imageMeta.height,
width = imageContent.width, width = imageMeta.width,
size = imageContent.size size = imageMeta.size
) )
), ),
) )

View File

@ -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.MessageService.EventMessage
import app.dapk.st.matrix.message.internal.ApiMessage.ImageMessage import app.dapk.st.matrix.message.internal.ApiMessage.ImageMessage
import app.dapk.st.matrix.message.internal.ApiMessage.TextMessage import app.dapk.st.matrix.message.internal.ApiMessage.TextMessage
import io.ktor.content.*
import io.ktor.http.* import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.utils.io.jvm.javaio.*
import java.io.InputStream
import java.util.* import java.util.*
internal fun sendRequest(roomId: RoomId, eventType: EventType, txId: String, content: ApiMessageContent) = httpRequest<ApiSendResponse>( internal fun sendRequest(roomId: RoomId, eventType: EventType, txId: String, content: ApiMessageContent) = httpRequest<ApiSendResponse>(
@ -38,11 +40,15 @@ internal fun sendRequest(roomId: RoomId, eventType: EventType, content: EventMes
} }
) )
internal fun uploadRequest(body: ByteArray, filename: String, contentType: String) = httpRequest<ApiUploadResponse>( internal fun uploadRequest(stream: InputStream, contentLength: Long, filename: String, contentType: String) = httpRequest<ApiUploadResponse>(
path = "_matrix/media/r0/upload/?filename=$filename", path = "_matrix/media/r0/upload/?filename=$filename",
headers = listOf("Content-Type" to contentType), headers = listOf("Content-Type" to contentType),
method = MatrixHttpClient.Method.POST, 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()}" fun txId() = "local.${UUID.randomUUID()}"

View File

@ -87,7 +87,6 @@ class SmokeTest {
bob.expectImageMessage(SharedState.sharedRoom, testImage, SharedState.alice.roomMember) bob.expectImageMessage(SharedState.sharedRoom, testImage, SharedState.alice.roomMember)
} }
@Test @Test
@Order(8) @Order(8)
fun `can request and verify devices`() = testAfterInitialSync { alice, bob -> fun `can request and verify devices`() = testAfterInitialSync { alice, bob ->

View File

@ -146,6 +146,7 @@ class TestMatrix(
val result = cryptoService.encrypt(input) val result = cryptoService.encrypt(input)
MediaEncrypter.Result( MediaEncrypter.Result(
uri = result.uri, uri = result.uri,
contentLength = result.contentLength,
algorithm = result.algorithm, algorithm = result.algorithm,
ext = result.ext, ext = result.ext,
keyOperations = result.keyOperations, keyOperations = result.keyOperations,
@ -339,7 +340,7 @@ class JavaBase64 : Base64 {
class JavaImageContentReader : ImageContentReader { class JavaImageContentReader : ImageContentReader {
override fun read(uri: String): ImageContentReader.ImageContent { override fun meta(uri: String): ImageContentReader.ImageContent {
val file = File(uri) val file = File(uri)
val size = file.length() val size = file.length()
val image = ImageIO.read(file) val image = ImageIO.read(file)
@ -349,8 +350,9 @@ class JavaImageContentReader : ImageContentReader {
size = size, size = size,
mimeType = "image/${file.extension}", mimeType = "image/${file.extension}",
fileName = file.name, fileName = file.name,
uri = file.toURI(),
) )
} }
override fun inputStream(uri: String) = File(uri).inputStream()
} }