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.material.Surface
import androidx.compose.ui.Modifier
import app.dapk.st.design.components.SmallTalkTheme
import app.dapk.st.core.DapkActivity
import app.dapk.st.core.module
import app.dapk.st.core.viewModel
import app.dapk.st.design.components.SmallTalkTheme
import app.dapk.st.matrix.common.RoomId
import kotlinx.parcelize.Parcelize

View File

@ -1,5 +1,6 @@
package app.dapk.st.messenger
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
@ -179,16 +180,9 @@ private fun LazyItemScope.Image(self: UserId, message: RoomEvent.Image, wasPrevi
isNotSelf = false,
wasPreviousMessageSameSender = wasPreviousMessageSameSender
) {
Text(message.imageMeta.url)
androidx.compose.foundation.Image(
painter = rememberImagePainter(
data = message.imageMeta.url,
),
contentDescription = null,
modifier = Modifier
.size(100.dp)
.align(Alignment.Center)
)
Box {
MessageImage(message)
}
}
}
}
@ -200,25 +194,29 @@ private fun LazyItemScope.Image(self: UserId, message: RoomEvent.Image, wasPrevi
isNotSelf = true,
wasPreviousMessageSameSender = wasPreviousMessageSameSender
) {
Text(message.imageMeta.url)
androidx.compose.foundation.Image(
painter = rememberImagePainter(
data = message.imageMeta.url,
builder = {
Box {
MessageImage(message)
}
),
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
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("height") val height: Int,
@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
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("m.relates_to") override val relation: Relation? = null,
@SerialName("msgtype") val messageType: String = "m.image",
@ -468,14 +469,24 @@ internal sealed class ApiTimelineEvent {
@Serializable
data class File(
@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
internal data class Info(
@SerialName("h") val height: Int,
@SerialName("w") val width: Int,
)
}
@Serializable
@ -553,10 +564,7 @@ internal object ApiTimelineMessageContentDeserializer : KSerializer<ApiTimelineE
val element = decoder.decodeJsonElement()
return when (element.jsonObject["msgtype"]?.jsonPrimitive?.content) {
"m.text" -> ApiTimelineEvent.TimelineMessage.Content.Text.serializer().deserialize(decoder)
"m.image" -> when (element.jsonObject["file"]) {
null -> ApiTimelineEvent.TimelineMessage.Content.Ignored
else -> ApiTimelineEvent.TimelineMessage.Content.Image.serializer().deserialize(decoder)
}
"m.image" -> ApiTimelineEvent.TimelineMessage.Content.Image.serializer().deserialize(decoder)
else -> {
println(element)
ApiTimelineEvent.TimelineMessage.Content.Ignored

View File

@ -37,7 +37,8 @@ internal class RoomEventsDecrypter(
imageMeta = RoomEvent.Image.ImageMeta(
width = content.info.width,
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,
)

View File

@ -47,7 +47,8 @@ internal class RoomEventFactory(
return RoomEvent.Image.ImageMeta(
content.info.width,
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) }
)
}
}