rendering images in the messenger

This commit is contained in:
Adam Brown 2022-03-31 22:23:43 +01:00
parent 5a6b7cf1c0
commit d0b2627eb2
7 changed files with 117 additions and 34 deletions

View File

@ -0,0 +1,64 @@
package app.dapk.st.messenger
import android.util.Base64
import app.dapk.st.matrix.sync.RoomEvent
import coil.bitmap.BitmapPool
import coil.decode.DataSource
import coil.decode.Options
import coil.fetch.FetchResult
import coil.fetch.Fetcher
import coil.fetch.SourceResult
import coil.size.Size
import okhttp3.OkHttpClient
import okhttp3.Request
import okio.Buffer
import java.security.MessageDigest
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
private const val CRYPTO_BUFFER_SIZE = 32 * 1024
private const val CIPHER_ALGORITHM = "AES/CTR/NoPadding"
private const val SECRET_KEY_SPEC_ALGORITHM = "AES"
private const val MESSAGE_DIGEST_ALGORITHM = "SHA-256"
class DecryptingFetcher : Fetcher<RoomEvent.Image> {
private val http = OkHttpClient()
override suspend fun fetch(pool: BitmapPool, data: RoomEvent.Image, size: Size, options: Options): FetchResult {
val keys = data.imageMeta.keys!!
val key = Base64.decode(keys.k.replace('-', '+').replace('_', '/'), Base64.DEFAULT)
val initVectorBytes = Base64.decode(keys.iv, Base64.DEFAULT)
val decryptCipher = Cipher.getInstance(CIPHER_ALGORITHM)
val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
val ivParameterSpec = IvParameterSpec(initVectorBytes)
decryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec)
val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM)
var read: Int
val d = ByteArray(CRYPTO_BUFFER_SIZE)
var decodedBytes: ByteArray
val response = http.newCall(Request.Builder().url(data.imageMeta.url).build()).execute()
val outputStream = Buffer()
response.body()?.let {
it.byteStream().use {
read = it.read(d)
while (read != -1) {
messageDigest.update(d, 0, read)
decodedBytes = decryptCipher.update(d, 0, read)
outputStream.write(decodedBytes)
read = it.read(d)
}
}
}
return SourceResult(outputStream, null, DataSource.NETWORK)
}
override fun key(data: RoomEvent.Image) = data.imageMeta.url
}

View File

@ -9,10 +9,10 @@ import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import app.dapk.st.design.components.SmallTalkTheme
import app.dapk.st.core.DapkActivity import app.dapk.st.core.DapkActivity
import app.dapk.st.core.module import app.dapk.st.core.module
import app.dapk.st.core.viewModel import app.dapk.st.core.viewModel
import app.dapk.st.design.components.SmallTalkTheme
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize

View File

@ -1,5 +1,6 @@
package app.dapk.st.messenger package app.dapk.st.messenger
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@ -179,16 +180,9 @@ private fun LazyItemScope.Image(self: UserId, message: RoomEvent.Image, wasPrevi
isNotSelf = false, isNotSelf = false,
wasPreviousMessageSameSender = wasPreviousMessageSameSender wasPreviousMessageSameSender = wasPreviousMessageSameSender
) { ) {
Text(message.imageMeta.url) Box {
androidx.compose.foundation.Image( MessageImage(message)
painter = rememberImagePainter( }
data = message.imageMeta.url,
),
contentDescription = null,
modifier = Modifier
.size(100.dp)
.align(Alignment.Center)
)
} }
} }
} }
@ -200,25 +194,29 @@ private fun LazyItemScope.Image(self: UserId, message: RoomEvent.Image, wasPrevi
isNotSelf = true, isNotSelf = true,
wasPreviousMessageSameSender = wasPreviousMessageSameSender wasPreviousMessageSameSender = wasPreviousMessageSameSender
) { ) {
Text(message.imageMeta.url) Box {
androidx.compose.foundation.Image( MessageImage(message)
painter = rememberImagePainter(
data = message.imageMeta.url,
builder = {
} }
),
contentDescription = null,
modifier = Modifier
.size(100.dp)
.align(Alignment.Center)
)
} }
} }
} }
} }
} }
@Composable
private fun MessageImage(message: RoomEvent.Image) {
val width = with(LocalDensity.current) { message.imageMeta.width.toDp() }
val height = with(LocalDensity.current) { message.imageMeta.height.toDp() }
Image(
modifier = Modifier.size(width, height),
painter = rememberImagePainter(
data = message,
builder = { fetcher(DecryptingFetcher()) }
),
contentDescription = null,
)
}
@Composable @Composable
private fun LazyItemScope.Message(self: UserId, message: Message, wasPreviousMessageSameSender: Boolean) { private fun LazyItemScope.Message(self: UserId, message: Message, wasPreviousMessageSameSender: Boolean) {

View File

@ -93,7 +93,18 @@ sealed class RoomEvent {
@SerialName("width") val width: Int, @SerialName("width") val width: Int,
@SerialName("height") val height: Int, @SerialName("height") val height: Int,
@SerialName("url") val url: String, @SerialName("url") val url: String,
@SerialName("keys") val keys: Keys?,
) {
@Serializable
data class Keys(
@SerialName("k") val k: String,
@SerialName("iv") val iv: String,
@SerialName("v") val v: String,
@SerialName("hashes") val hashes: Map<String, String>,
) )
}
} }
} }

View File

@ -459,7 +459,8 @@ internal sealed class ApiTimelineEvent {
@Serializable @Serializable
data class Image( data class Image(
@SerialName("file") val file: File, @SerialName("url") val url: MxUrl? = null,
@SerialName("file") val file: File? = null,
@SerialName("info") val info: Info, @SerialName("info") val info: Info,
@SerialName("m.relates_to") override val relation: Relation? = null, @SerialName("m.relates_to") override val relation: Relation? = null,
@SerialName("msgtype") val messageType: String = "m.image", @SerialName("msgtype") val messageType: String = "m.image",
@ -468,14 +469,24 @@ internal sealed class ApiTimelineEvent {
@Serializable @Serializable
data class File( data class File(
@SerialName("url") val url: MxUrl, @SerialName("url") val url: MxUrl,
@SerialName("iv") val iv: String,
@SerialName("v") val v: String,
@SerialName("hashes") val hashes: Map<String, String>,
@SerialName("key") val key: Key,
) {
@Serializable
data class Key(
@SerialName("k") val k: String,
) )
}
@Serializable @Serializable
internal data class Info( internal data class Info(
@SerialName("h") val height: Int, @SerialName("h") val height: Int,
@SerialName("w") val width: Int, @SerialName("w") val width: Int,
) )
} }
@Serializable @Serializable
@ -553,10 +564,7 @@ internal object ApiTimelineMessageContentDeserializer : KSerializer<ApiTimelineE
val element = decoder.decodeJsonElement() val element = decoder.decodeJsonElement()
return when (element.jsonObject["msgtype"]?.jsonPrimitive?.content) { return when (element.jsonObject["msgtype"]?.jsonPrimitive?.content) {
"m.text" -> ApiTimelineEvent.TimelineMessage.Content.Text.serializer().deserialize(decoder) "m.text" -> ApiTimelineEvent.TimelineMessage.Content.Text.serializer().deserialize(decoder)
"m.image" -> when (element.jsonObject["file"]) { "m.image" -> ApiTimelineEvent.TimelineMessage.Content.Image.serializer().deserialize(decoder)
null -> ApiTimelineEvent.TimelineMessage.Content.Ignored
else -> ApiTimelineEvent.TimelineMessage.Content.Image.serializer().deserialize(decoder)
}
else -> { else -> {
println(element) println(element)
ApiTimelineEvent.TimelineMessage.Content.Ignored ApiTimelineEvent.TimelineMessage.Content.Ignored

View File

@ -37,7 +37,8 @@ internal class RoomEventsDecrypter(
imageMeta = RoomEvent.Image.ImageMeta( imageMeta = RoomEvent.Image.ImageMeta(
width = content.info.width, width = content.info.width,
height = content.info.height, height = content.info.height,
url = content.file?.url?.convertMxUrToUrl(userCredentials.homeServer) ?: "" url = content.file?.url?.convertMxUrToUrl(userCredentials.homeServer) ?: content.url!!.convertMxUrToUrl(userCredentials.homeServer),
keys = content.file?.let { RoomEvent.Image.ImageMeta.Keys(it.key.k, it.iv, it.v, it.hashes) }
), ),
encryptedContent = null, encryptedContent = null,
) )

View File

@ -47,7 +47,8 @@ internal class RoomEventFactory(
return RoomEvent.Image.ImageMeta( return RoomEvent.Image.ImageMeta(
content.info.width, content.info.width,
content.info.height, content.info.height,
content.file?.url?.convertMxUrToUrl(userCredentials.homeServer) content.file?.url?.convertMxUrToUrl(userCredentials.homeServer) ?: content.url!!.convertMxUrToUrl(userCredentials.homeServer),
keys = content.file?.let { RoomEvent.Image.ImageMeta.Keys(it.key.k, it.iv, it.v, it.hashes) }
) )
} }
} }