From 04be41d699ebc8b6703b37ef86d1aef077994c13 Mon Sep 17 00:00:00 2001 From: Benoit Marty <benoit@matrix.org> Date: Wed, 1 Dec 2021 17:12:30 +0100 Subject: [PATCH] Import v1.3.9 from Element Android --- dependencies.gradle | 10 +- matrix-sdk-android/build.gradle | 6 +- .../android/sdk/api/logger/LoggerTag.kt | 1 + .../session/content/ContentAttachmentData.kt | 14 +- .../api/session/events/model/RelationType.kt | 4 + .../sdk/api/session/room/send/UserDraft.kt | 13 +- .../internal/crypto/DefaultCryptoService.kt | 68 ++--- .../EnsureOlmSessionsForDevicesAction.kt | 55 ++-- .../algorithms/megolm/MXMegolmDecryption.kt | 35 +-- .../algorithms/megolm/MXMegolmEncryption.kt | 78 +++--- .../megolm/MXMegolmEncryptionFactory.kt | 4 +- .../crypto/model/MXUsersDevicesMap.kt | 8 + .../internal/crypto/tasks/SendEventTask.kt | 5 +- .../internal/database/mapper/DraftMapper.kt | 20 +- .../internal/database/model/DraftEntity.kt | 2 +- .../internal/database/model/EventEntity.kt | 5 +- .../session/content/UploadContentWorker.kt | 10 +- .../session/events/DefaultEventService.kt | 1 - .../room/send/LocalEchoEventFactory.kt | 12 +- .../session/room/timeline/DefaultTimeline.kt | 22 ++ .../room/timeline/DefaultTimelineService.kt | 3 + .../session/room/timeline/GetEventTask.kt | 2 + .../room/timeline/TimelineEventDecryptor.kt | 19 +- .../session/sync/SyncResponseHandler.kt | 6 + .../session/sync/handler/CryptoSyncHandler.kt | 7 +- .../sync/handler/room/RoomSyncHandler.kt | 10 +- .../handler/room/ThreadsAwarenessHandler.kt | 263 ++++++++++++++++++ 27 files changed, 534 insertions(+), 149 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt diff --git a/dependencies.gradle b/dependencies.gradle index 8cc7b3b2..1a04fe07 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -11,7 +11,7 @@ def gradle = "7.0.3" // Ref: https://kotlinlang.org/releases.html def kotlin = "1.5.31" def kotlinCoroutines = "1.5.2" -def dagger = "2.40.1" +def dagger = "2.40.3" def retrofit = "2.9.0" def arrow = "0.8.2" def markwon = "4.6.2" @@ -45,13 +45,13 @@ ext.libs = [ 'coroutinesTest' : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines" ], androidx : [ - 'appCompat' : "androidx.appcompat:appcompat:1.3.1", + 'appCompat' : "androidx.appcompat:appcompat:1.4.0", 'core' : "androidx.core:core-ktx:1.7.0", 'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1", 'exifinterface' : "androidx.exifinterface:exifinterface:1.3.3", - 'fragmentKtx' : "androidx.fragment:fragment-ktx:1.3.6", - 'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.1", - 'work' : "androidx.work:work-runtime-ktx:2.7.0", + 'fragmentKtx' : "androidx.fragment:fragment-ktx:1.4.0", + 'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.2", + 'work' : "androidx.work:work-runtime-ktx:2.7.1", 'autoFill' : "androidx.autofill:autofill:1.1.0", 'preferenceKtx' : "androidx.preference:preference-ktx:1.1.1", 'junit' : "androidx.test.ext:junit:1.1.3", diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index a96ca6c8..3c59e345 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -118,7 +118,7 @@ dependencies { implementation libs.squareup.retrofit implementation libs.squareup.retrofitMoshi - implementation(platform("com.squareup.okhttp3:okhttp-bom:4.9.2")) + implementation(platform("com.squareup.okhttp3:okhttp-bom:4.9.3")) implementation 'com.squareup.okhttp3:okhttp' implementation 'com.squareup.okhttp3:logging-interceptor' implementation 'com.squareup.okhttp3:okhttp-urlconnection' @@ -161,10 +161,10 @@ dependencies { implementation libs.apache.commonsImaging // Phone number https://github.com/google/libphonenumber - implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.37' + implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.38' testImplementation libs.tests.junit - testImplementation 'org.robolectric:robolectric:4.7' + testImplementation 'org.robolectric:robolectric:4.7.2' //testImplementation 'org.robolectric:shadows-support-v4:3.0' // Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281 testImplementation libs.mockk.mockk diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/logger/LoggerTag.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/logger/LoggerTag.kt index 0d204edc..44ac439d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/logger/LoggerTag.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/logger/LoggerTag.kt @@ -26,6 +26,7 @@ open class LoggerTag(_value: String, parentTag: LoggerTag? = null) { object SYNC : LoggerTag("SYNC") object VOIP : LoggerTag("VOIP") + object CRYPTO : LoggerTag("CRYPTO") val value: String = if (parentTag == null) { _value diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentAttachmentData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentAttachmentData.kt index 7ee26de8..8b1df239 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentAttachmentData.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentAttachmentData.kt @@ -22,6 +22,7 @@ import androidx.exifinterface.media.ExifInterface import com.squareup.moshi.JsonClass import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.util.MimeTypes.normalizeMimeType +import org.matrix.android.sdk.internal.di.MoshiProvider @Parcelize @JsonClass(generateAdapter = true) @@ -44,8 +45,19 @@ data class ContentAttachmentData( FILE, IMAGE, AUDIO, - VIDEO + VIDEO, + VOICE_MESSAGE } fun getSafeMimeType() = mimeType?.normalizeMimeType() + + fun toJsonString(): String { + return MoshiProvider.providesMoshi().adapter(ContentAttachmentData::class.java).toJson(this) + } + + companion object { + fun fromJsonString(json: String): ContentAttachmentData? { + return MoshiProvider.providesMoshi().adapter(ContentAttachmentData::class.java).fromJson(json) + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt index 7d827f87..f67efc50 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/RelationType.kt @@ -28,6 +28,10 @@ object RelationType { /** Lets you define an event which references an existing event.*/ const val REFERENCE = "m.reference" + /** Lets you define an thread event that belongs to another existing event.*/ +// const val THREAD = "m.thread" // m.thread is not yet released in the backend + const val THREAD = "io.element.thread" // io.element.thread will be replaced by m.thread when it is released + /** Lets you define an event which adds a response to an existing event.*/ const val RESPONSE = "org.matrix.response" } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/UserDraft.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/UserDraft.kt index 9471b3db..a8c0de2f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/UserDraft.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/UserDraft.kt @@ -23,15 +23,16 @@ package org.matrix.android.sdk.api.session.room.send * EDIT: draft of an edition of a message * REPLY: draft of a reply of another message */ -sealed class UserDraft(open val text: String) { - data class REGULAR(override val text: String) : UserDraft(text) - data class QUOTE(val linkedEventId: String, override val text: String) : UserDraft(text) - data class EDIT(val linkedEventId: String, override val text: String) : UserDraft(text) - data class REPLY(val linkedEventId: String, override val text: String) : UserDraft(text) +sealed interface UserDraft { + data class Regular(val content: String) : UserDraft + data class Quote(val linkedEventId: String, val content: String) : UserDraft + data class Edit(val linkedEventId: String, val content: String) : UserDraft + data class Reply(val linkedEventId: String, val content: String) : UserDraft + data class Voice(val content: String) : UserDraft fun isValid(): Boolean { return when (this) { - is REGULAR -> text.isNotBlank() + is Regular -> content.isNotBlank() else -> true } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index 7b96148e..18f34488 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -36,6 +36,7 @@ import org.matrix.android.sdk.api.crypto.MXCryptoConfig import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.listeners.ProgressListener +import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME @@ -110,6 +111,9 @@ import kotlin.math.max * CryptoService maintains all necessary keys and their sharing with other devices required for the crypto. * Specially, it tracks all room membership changes events in order to do keys updates. */ + +private val loggerTag = LoggerTag("DefaultCryptoService", LoggerTag.CRYPTO) + @SessionScope internal class DefaultCryptoService @Inject constructor( // Olm Manager @@ -346,7 +350,7 @@ internal class DefaultCryptoService @Inject constructor( deviceListManager.startTrackingDeviceList(listOf(userId)) deviceListManager.refreshOutdatedDeviceLists() } catch (failure: Throwable) { - Timber.e(failure, "## CRYPTO onSyncWillProcess ") + Timber.tag(loggerTag.value).e(failure, "onSyncWillProcess ") } } } @@ -379,7 +383,7 @@ internal class DefaultCryptoService @Inject constructor( { isStarting.set(false) isStarted.set(false) - Timber.e(it, "Start failed") + Timber.tag(loggerTag.value).e(it, "Start failed") } ) } @@ -551,14 +555,14 @@ internal class DefaultCryptoService @Inject constructor( val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId) if (!existingAlgorithm.isNullOrEmpty() && existingAlgorithm != algorithm) { - Timber.e("## CRYPTO | setEncryptionInRoom() : Ignoring m.room.encryption event which requests a change of config in $roomId") + Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Ignoring m.room.encryption event which requests a change of config in $roomId") return false } val encryptingClass = MXCryptoAlgorithms.hasEncryptorClassForAlgorithm(algorithm) if (!encryptingClass) { - Timber.e("## CRYPTO | setEncryptionInRoom() : Unable to encrypt room $roomId with $algorithm") + Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Unable to encrypt room $roomId with $algorithm") return false } @@ -577,7 +581,7 @@ internal class DefaultCryptoService @Inject constructor( // e2e rooms with them, so there is room for optimisation here, but for now // we just invalidate everyone in the room. if (null == existingAlgorithm) { - Timber.v("Enabling encryption in $roomId for the first time; invalidating device lists for all users therein") + Timber.tag(loggerTag.value).d("Enabling encryption in $roomId for the first time; invalidating device lists for all users therein") val userIds = ArrayList(membersId) @@ -655,17 +659,17 @@ internal class DefaultCryptoService @Inject constructor( val safeAlgorithm = alg if (safeAlgorithm != null) { val t0 = System.currentTimeMillis() - Timber.v("## CRYPTO | encryptEventContent() starts") + Timber.tag(loggerTag.value).v("encryptEventContent() starts") runCatching { val content = safeAlgorithm.encryptEventContent(eventContent, eventType, userIds) - Timber.v("## CRYPTO | encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms") + Timber.tag(loggerTag.value).v("## CRYPTO | encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms") MXEncryptEventContentResult(content, EventType.ENCRYPTED) }.foldToCallback(callback) } else { val algorithm = getEncryptionAlgorithm(roomId) val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON, algorithm ?: MXCryptoError.NO_MORE_ALGORITHM_REASON) - Timber.e("## CRYPTO | encryptEventContent() : $reason") + Timber.tag(loggerTag.value).e("encryptEventContent() : failed $reason") callback.onFailure(Failure.CryptoError(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_ENCRYPT, reason))) } } @@ -677,7 +681,7 @@ internal class DefaultCryptoService @Inject constructor( if (roomEncryptor is IMXGroupEncryption) { roomEncryptor.discardSessionKey() } else { - Timber.e("## CRYPTO | discardOutboundSession() for:$roomId: Unable to handle IMXGroupEncryption") + Timber.tag(loggerTag.value).e("discardOutboundSession() for:$roomId: Unable to handle IMXGroupEncryption") } } } @@ -768,14 +772,14 @@ internal class DefaultCryptoService @Inject constructor( */ private fun onRoomKeyEvent(event: Event) { val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return - Timber.i("## CRYPTO | onRoomKeyEvent() from: ${event.senderId} type<${event.getClearType()}> , sessionId<${roomKeyContent.sessionId}>") + Timber.tag(loggerTag.value).i("onRoomKeyEvent() from: ${event.senderId} type<${event.getClearType()}> , sessionId<${roomKeyContent.sessionId}>") if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) { - Timber.e("## CRYPTO | onRoomKeyEvent() : missing fields") + Timber.tag(loggerTag.value).e("onRoomKeyEvent() : missing fields") return } val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(roomKeyContent.roomId, roomKeyContent.algorithm) if (alg == null) { - Timber.e("## CRYPTO | GOSSIP onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}") + Timber.tag(loggerTag.value).e("GOSSIP onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}") return } alg.onRoomKeyEvent(event, keysBackupService) @@ -783,29 +787,29 @@ internal class DefaultCryptoService @Inject constructor( private fun onKeyWithHeldReceived(event: Event) { val withHeldContent = event.getClearContent().toModel<RoomKeyWithHeldContent>() ?: return Unit.also { - Timber.i("## CRYPTO | Malformed onKeyWithHeldReceived() : missing fields") + Timber.tag(loggerTag.value).i("Malformed onKeyWithHeldReceived() : missing fields") } - Timber.i("## CRYPTO | onKeyWithHeldReceived() received from:${event.senderId}, content <$withHeldContent>") + Timber.tag(loggerTag.value).i("onKeyWithHeldReceived() received from:${event.senderId}, content <$withHeldContent>") val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(withHeldContent.roomId, withHeldContent.algorithm) if (alg is IMXWithHeldExtension) { alg.onRoomKeyWithHeldEvent(withHeldContent) } else { - Timber.e("## CRYPTO | onKeyWithHeldReceived() from:${event.senderId}: Unable to handle WithHeldContent for ${withHeldContent.algorithm}") + Timber.tag(loggerTag.value).e("onKeyWithHeldReceived() from:${event.senderId}: Unable to handle WithHeldContent for ${withHeldContent.algorithm}") return } } private fun onSecretSendReceived(event: Event) { - Timber.i("## CRYPTO | GOSSIP onSecretSend() from ${event.senderId} : onSecretSendReceived ${event.content?.get("sender_key")}") + Timber.tag(loggerTag.value).i("GOSSIP onSecretSend() from ${event.senderId} : onSecretSendReceived ${event.content?.get("sender_key")}") if (!event.isEncrypted()) { // secret send messages must be encrypted - Timber.e("## CRYPTO | GOSSIP onSecretSend() :Received unencrypted secret send event") + Timber.tag(loggerTag.value).e("GOSSIP onSecretSend() :Received unencrypted secret send event") return } // Was that sent by us? if (event.senderId != userId) { - Timber.e("## CRYPTO | GOSSIP onSecretSend() : Ignore secret from other user ${event.senderId}") + Timber.tag(loggerTag.value).e("GOSSIP onSecretSend() : Ignore secret from other user ${event.senderId}") return } @@ -815,13 +819,13 @@ internal class DefaultCryptoService @Inject constructor( .getOutgoingSecretKeyRequests().firstOrNull { it.requestId == secretContent.requestId } if (existingRequest == null) { - Timber.i("## CRYPTO | GOSSIP onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}") + Timber.tag(loggerTag.value).i("GOSSIP onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}") return } if (!handleSDKLevelGossip(existingRequest.secretName, secretContent.secretValue)) { // TODO Ask to application layer? - Timber.v("## CRYPTO | onSecretSend() : secret not handled by SDK") + Timber.tag(loggerTag.value).v("onSecretSend() : secret not handled by SDK") } } @@ -858,7 +862,7 @@ internal class DefaultCryptoService @Inject constructor( private fun onRoomEncryptionEvent(roomId: String, event: Event) { if (!event.isStateEvent()) { // Ignore - Timber.w("Invalid encryption event") + Timber.tag(loggerTag.value).w("Invalid encryption event") return } cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { @@ -912,7 +916,7 @@ internal class DefaultCryptoService @Inject constructor( */ private suspend fun uploadDeviceKeys() { if (cryptoStore.areDeviceKeysUploaded()) { - Timber.d("Keys already uploaded, nothing to do") + Timber.tag(loggerTag.value).d("Keys already uploaded, nothing to do") return } // Prepare the device keys data to send @@ -971,13 +975,13 @@ internal class DefaultCryptoService @Inject constructor( password: String, progressListener: ProgressListener?): ImportRoomKeysResult { return withContext(coroutineDispatchers.crypto) { - Timber.v("## CRYPTO | importRoomKeys starts") + Timber.tag(loggerTag.value).v("importRoomKeys starts") val t0 = System.currentTimeMillis() val roomKeys = MXMegolmExportEncryption.decryptMegolmKeyFile(roomKeysAsArray, password) val t1 = System.currentTimeMillis() - Timber.v("## CRYPTO | importRoomKeys : decryptMegolmKeyFile done in ${t1 - t0} ms") + Timber.tag(loggerTag.value).v("importRoomKeys : decryptMegolmKeyFile done in ${t1 - t0} ms") val importedSessions = MoshiProvider.providesMoshi() .adapter<List<MegolmSessionData>>(Types.newParameterizedType(List::class.java, MegolmSessionData::class.java)) @@ -985,7 +989,7 @@ internal class DefaultCryptoService @Inject constructor( val t2 = System.currentTimeMillis() - Timber.v("## CRYPTO | importRoomKeys : JSON parsing ${t2 - t1} ms") + Timber.tag(loggerTag.value).v("importRoomKeys : JSON parsing ${t2 - t1} ms") if (importedSessions == null) { throw Exception("Error") @@ -1122,7 +1126,7 @@ internal class DefaultCryptoService @Inject constructor( */ override fun reRequestRoomKeyForEvent(event: Event) { val wireContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also { - Timber.e("## CRYPTO | reRequestRoomKeyForEvent Failed to re-request key, null content") + Timber.tag(loggerTag.value).e("reRequestRoomKeyForEvent Failed to re-request key, null content") } val requestBody = RoomKeyRequestBody( @@ -1137,7 +1141,7 @@ internal class DefaultCryptoService @Inject constructor( override fun requestRoomKeyForEvent(event: Event) { val wireContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also { - Timber.e("## CRYPTO | requestRoomKeyForEvent Failed to request key, null content eventId: ${event.eventId}") + Timber.tag(loggerTag.value).e("requestRoomKeyForEvent Failed to request key, null content eventId: ${event.eventId}") } cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { @@ -1148,7 +1152,7 @@ internal class DefaultCryptoService @Inject constructor( roomDecryptorProvider .getOrCreateRoomDecryptor(event.roomId, wireContent.algorithm) ?.requestKeysForEvent(event, false) ?: run { - Timber.v("## CRYPTO | requestRoomKeyForEvent() : No room decryptor for roomId:${event.roomId} algorithm:${wireContent.algorithm}") + Timber.tag(loggerTag.value).v("requestRoomKeyForEvent() : No room decryptor for roomId:${event.roomId} algorithm:${wireContent.algorithm}") } } } @@ -1287,12 +1291,12 @@ internal class DefaultCryptoService @Inject constructor( override fun prepareToEncrypt(roomId: String, callback: MatrixCallback<Unit>) { cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { - Timber.d("## CRYPTO | prepareToEncrypt() : Check room members up to date") + Timber.tag(loggerTag.value).d("prepareToEncrypt() roomId:$roomId Check room members up to date") // Ensure to load all room members try { loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId)) } catch (failure: Throwable) { - Timber.e("## CRYPTO | prepareToEncrypt() : Failed to load room members") + Timber.tag(loggerTag.value).e("prepareToEncrypt() : Failed to load room members") callback.onFailure(failure) return@launch } @@ -1305,7 +1309,7 @@ internal class DefaultCryptoService @Inject constructor( if (alg == null) { val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON, MXCryptoError.NO_MORE_ALGORITHM_REASON) - Timber.e("## CRYPTO | prepareToEncrypt() : $reason") + Timber.tag(loggerTag.value).e("prepareToEncrypt() : $reason") callback.onFailure(IllegalArgumentException("Missing algorithm")) return@launch } @@ -1315,7 +1319,7 @@ internal class DefaultCryptoService @Inject constructor( }.fold( { callback.onSuccess(Unit) }, { - Timber.e("## CRYPTO | prepareToEncrypt() failed.") + Timber.tag(loggerTag.value).e(it, "prepareToEncrypt() failed.") callback.onFailure(it) } ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt index 3979ff1f..ab2ed04d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt @@ -16,17 +16,21 @@ package org.matrix.android.sdk.internal.crypto.actions +import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.internal.crypto.MXOlmDevice import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.internal.crypto.model.MXKey import org.matrix.android.sdk.internal.crypto.model.MXOlmSessionResult import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap +import org.matrix.android.sdk.internal.crypto.model.toDebugString import org.matrix.android.sdk.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask import timber.log.Timber import javax.inject.Inject private const val ONE_TIME_KEYS_RETRY_COUNT = 3 +private val loggerTag = LoggerTag("EnsureOlmSessionsForDevicesAction", LoggerTag.CRYPTO) + internal class EnsureOlmSessionsForDevicesAction @Inject constructor( private val olmDevice: MXOlmDevice, private val oneTimeKeysForUsersDeviceTask: ClaimOneTimeKeysForUsersDeviceTask) { @@ -36,15 +40,22 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor( val results = MXUsersDevicesMap<MXOlmSessionResult>() - for ((userId, deviceInfos) in devicesByUser) { - for (deviceInfo in deviceInfos) { + for ((userId, deviceList) in devicesByUser) { + for (deviceInfo in deviceList) { val deviceId = deviceInfo.deviceId val key = deviceInfo.identityKey() + if (key == null) { + Timber.w("## CRYPTO | Ignoring device (${deviceInfo.userId}|$deviceId) without identity key") + continue + } - val sessionId = olmDevice.getSessionId(key!!) + val sessionId = olmDevice.getSessionId(key) if (sessionId.isNullOrEmpty() || force) { + Timber.tag(loggerTag.value).d("Found no existing olm session (${deviceInfo.userId}|$deviceId) (force=$force)") devicesWithoutSession.add(deviceInfo) + } else { + Timber.tag(loggerTag.value).d("using olm session $sessionId for (${deviceInfo.userId}|$deviceId)") } val olmSessionResult = MXOlmSessionResult(deviceInfo, sessionId) @@ -52,6 +63,8 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor( } } + Timber.tag(loggerTag.value).d("Devices without olm session (count:${devicesWithoutSession.size}) :" + + " ${devicesWithoutSession.joinToString { "${it.userId}|${it.deviceId}" }}") if (devicesWithoutSession.size == 0) { return results } @@ -71,11 +84,11 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor( // // That should eventually resolve itself, but it's poor form. - Timber.i("## CRYPTO | claimOneTimeKeysForUsersDevices() : $usersDevicesToClaim") + Timber.tag(loggerTag.value).i("claimOneTimeKeysForUsersDevices() : ${usersDevicesToClaim.toDebugString()}") val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim) val oneTimeKeys = oneTimeKeysForUsersDeviceTask.executeRetry(claimParams, remainingRetry = ONE_TIME_KEYS_RETRY_COUNT) - Timber.v("## CRYPTO | claimOneTimeKeysForUsersDevices() : keysClaimResponse.oneTimeKeys: $oneTimeKeys") + Timber.tag(loggerTag.value).v("claimOneTimeKeysForUsersDevices() : keysClaimResponse.oneTimeKeys: $oneTimeKeys") for ((userId, deviceInfos) in devicesByUser) { for (deviceInfo in deviceInfos) { var oneTimeKey: MXKey? = null @@ -83,7 +96,7 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor( if (null != deviceIds) { for (deviceId in deviceIds) { val olmSessionResult = results.getObject(userId, deviceId) - if (olmSessionResult!!.sessionId != null && !force) { + if (olmSessionResult?.sessionId != null && !force) { // We already have a result for this device continue } @@ -92,12 +105,11 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor( oneTimeKey = key } if (oneTimeKey == null) { - Timber.w("## CRYPTO | ensureOlmSessionsForDevices() : No one-time keys " + oneTimeKeyAlgorithm + - " for device " + userId + " : " + deviceId) + Timber.tag(loggerTag.value).d("No one time key for $userId|$deviceId") continue } // Update the result for this device in results - olmSessionResult.sessionId = verifyKeyAndStartSession(oneTimeKey, userId, deviceInfo) + olmSessionResult?.sessionId = verifyKeyAndStartSession(oneTimeKey, userId, deviceInfo) } } } @@ -112,31 +124,36 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor( val signKeyId = "ed25519:$deviceId" val signature = oneTimeKey.signatureForUserId(userId, signKeyId) - if (!signature.isNullOrEmpty() && !deviceInfo.fingerprint().isNullOrEmpty()) { + val fingerprint = deviceInfo.fingerprint() + if (!signature.isNullOrEmpty() && !fingerprint.isNullOrEmpty()) { var isVerified = false var errorMessage: String? = null try { - olmDevice.verifySignature(deviceInfo.fingerprint()!!, oneTimeKey.signalableJSONDictionary(), signature) + olmDevice.verifySignature(fingerprint, oneTimeKey.signalableJSONDictionary(), signature) isVerified = true } catch (e: Exception) { + Timber.tag(loggerTag.value).d(e, "verifyKeyAndStartSession() : Verify error for otk: ${oneTimeKey.signalableJSONDictionary()}," + + " signature:$signature fingerprint:$fingerprint") + Timber.tag(loggerTag.value).e("verifyKeyAndStartSession() : Verify error for ${deviceInfo.userId}|${deviceInfo.deviceId} " + + " - signable json ${oneTimeKey.signalableJSONDictionary()}") errorMessage = e.message } // Check one-time key signature if (isVerified) { - sessionId = olmDevice.createOutboundSession(deviceInfo.identityKey()!!, oneTimeKey.value) + sessionId = deviceInfo.identityKey()?.let { identityKey -> + olmDevice.createOutboundSession(identityKey, oneTimeKey.value) + } - if (!sessionId.isNullOrEmpty()) { - Timber.v("## CRYPTO | verifyKeyAndStartSession() : Started new sessionid " + sessionId + - " for device " + deviceInfo + "(theirOneTimeKey: " + oneTimeKey.value + ")") - } else { + if (sessionId.isNullOrEmpty()) { // Possibly a bad key - Timber.e("## CRYPTO | verifyKeyAndStartSession() : Error starting session with device $userId:$deviceId") + Timber.tag(loggerTag.value).e("verifyKeyAndStartSession() : Error starting session with device $userId:$deviceId") + } else { + Timber.tag(loggerTag.value).d("verifyKeyAndStartSession() : Started new sessionId $sessionId for device $userId:$deviceId") } } else { - Timber.e("## CRYPTO | verifyKeyAndStartSession() : Unable to verify signature on one-time key for device " + userId + - ":" + deviceId + " Error " + errorMessage) + Timber.tag(loggerTag.value).e("verifyKeyAndStartSession() : Unable to verify otk signature for $userId:$deviceId: $errorMessage") } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt index d7411ad0..8bbc7154 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType @@ -44,6 +45,8 @@ import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import timber.log.Timber +private val loggerTag = LoggerTag("MXMegolmDecryption", LoggerTag.CRYPTO) + internal class MXMegolmDecryption(private val userId: String, private val olmDevice: MXOlmDevice, private val deviceListManager: DeviceListManager, @@ -74,7 +77,7 @@ internal class MXMegolmDecryption(private val userId: String, @Throws(MXCryptoError::class) private fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult { - Timber.v("## CRYPTO | decryptEvent ${event.eventId}, requestKeysOnFail:$requestKeysOnFail") + Timber.tag(loggerTag.value).v("decryptEvent ${event.eventId}, requestKeysOnFail:$requestKeysOnFail") if (event.roomId.isNullOrBlank()) { throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) } @@ -230,7 +233,7 @@ internal class MXMegolmDecryption(private val userId: String, * @param event the key event. */ override fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) { - Timber.v("## CRYPTO | onRoomKeyEvent()") + Timber.tag(loggerTag.value).v("onRoomKeyEvent()") var exportFormat = false val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return @@ -239,11 +242,11 @@ internal class MXMegolmDecryption(private val userId: String, val forwardingCurve25519KeyChain: MutableList<String> = ArrayList() if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.sessionId.isNullOrEmpty() || roomKeyContent.sessionKey.isNullOrEmpty()) { - Timber.e("## CRYPTO | onRoomKeyEvent() : Key event is missing fields") + Timber.tag(loggerTag.value).e("onRoomKeyEvent() : Key event is missing fields") return } if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) { - Timber.i("## CRYPTO | onRoomKeyEvent(), forward adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}") + Timber.tag(loggerTag.value).i("onRoomKeyEvent(), forward adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}") val forwardedRoomKeyContent = event.getClearContent().toModel<ForwardedRoomKeyContent>() ?: return @@ -252,7 +255,7 @@ internal class MXMegolmDecryption(private val userId: String, } if (senderKey == null) { - Timber.e("## CRYPTO | onRoomKeyEvent() : event is missing sender_key field") + Timber.tag(loggerTag.value).e("onRoomKeyEvent() : event is missing sender_key field") return } @@ -261,20 +264,20 @@ internal class MXMegolmDecryption(private val userId: String, exportFormat = true senderKey = forwardedRoomKeyContent.senderKey if (null == senderKey) { - Timber.e("## CRYPTO | onRoomKeyEvent() : forwarded_room_key event is missing sender_key field") + Timber.tag(loggerTag.value).e("onRoomKeyEvent() : forwarded_room_key event is missing sender_key field") return } if (null == forwardedRoomKeyContent.senderClaimedEd25519Key) { - Timber.e("## CRYPTO | forwarded_room_key_event is missing sender_claimed_ed25519_key field") + Timber.tag(loggerTag.value).e("forwarded_room_key_event is missing sender_claimed_ed25519_key field") return } keysClaimed["ed25519"] = forwardedRoomKeyContent.senderClaimedEd25519Key } else { - Timber.i("## CRYPTO | onRoomKeyEvent(), Adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}") + Timber.tag(loggerTag.value).i("onRoomKeyEvent(), Adding key : ${roomKeyContent.roomId}|${roomKeyContent.sessionId}") if (null == senderKey) { - Timber.e("## onRoomKeyEvent() : key event has no sender key (not encrypted?)") + Timber.tag(loggerTag.value).e("## onRoomKeyEvent() : key event has no sender key (not encrypted?)") return } @@ -282,7 +285,7 @@ internal class MXMegolmDecryption(private val userId: String, keysClaimed = event.getKeysClaimed().toMutableMap() } - Timber.i("## CRYPTO | onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}") + Timber.tag(loggerTag.value).i("onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}") val added = olmDevice.addInboundGroupSession(roomKeyContent.sessionId, roomKeyContent.sessionKey, roomKeyContent.roomId, @@ -314,7 +317,7 @@ internal class MXMegolmDecryption(private val userId: String, * @param sessionId the session id */ override fun onNewSession(senderKey: String, sessionId: String) { - Timber.v(" CRYPTO | ON NEW SESSION $sessionId - $senderKey") + Timber.tag(loggerTag.value).v("ON NEW SESSION $sessionId - $senderKey") newSessionListener?.onNewSession(null, senderKey, sessionId) } @@ -346,10 +349,10 @@ internal class MXMegolmDecryption(private val userId: String, if (olmSessionResult?.sessionId == null) { // no session with this device, probably because there // were no one-time keys. - Timber.e("no session with this device $deviceId, probably because there were no one-time keys.") + Timber.tag(loggerTag.value).e("no session with this device $deviceId, probably because there were no one-time keys.") return@mapCatching } - Timber.i("## CRYPTO | shareKeysWithDevice() : sharing session ${body.sessionId} with device $userId:$deviceId") + Timber.tag(loggerTag.value).i("shareKeysWithDevice() : sharing session ${body.sessionId} with device $userId:$deviceId") val payloadJson = mutableMapOf<String, Any>("type" to EventType.FORWARDED_ROOM_KEY) runCatching { olmDevice.getInboundGroupSession(body.sessionId, body.senderKey, body.roomId) } @@ -360,7 +363,7 @@ internal class MXMegolmDecryption(private val userId: String, }, { // TODO - Timber.e(it, "## CRYPTO | shareKeysWithDevice: failed to get session for request $body") + Timber.tag(loggerTag.value).e(it, "shareKeysWithDevice: failed to get session for request $body") } ) @@ -368,12 +371,12 @@ internal class MXMegolmDecryption(private val userId: String, val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) val sendToDeviceMap = MXUsersDevicesMap<Any>() sendToDeviceMap.setObject(userId, deviceId, encodedPayload) - Timber.i("## CRYPTO | shareKeysWithDevice() : sending ${body.sessionId} to $userId:$deviceId") + Timber.tag(loggerTag.value).i("shareKeysWithDevice() : sending ${body.sessionId} to $userId:$deviceId") val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) try { sendToDeviceTask.execute(sendToDeviceParams) } catch (failure: Throwable) { - Timber.e(failure, "## CRYPTO | shareKeysWithDevice() : Failed to send ${body.sessionId} to $userId:$deviceId") + Timber.tag(loggerTag.value).e(failure, "shareKeysWithDevice() : Failed to send ${body.sessionId} to $userId:$deviceId") } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt index 031bb4e1..389036a1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Event @@ -36,6 +37,8 @@ import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode import org.matrix.android.sdk.internal.crypto.model.forEach +import org.matrix.android.sdk.internal.crypto.model.toDebugCount +import org.matrix.android.sdk.internal.crypto.model.toDebugString import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask @@ -43,6 +46,8 @@ import org.matrix.android.sdk.internal.util.JsonCanonicalizer import org.matrix.android.sdk.internal.util.convertToUTF8 import timber.log.Timber +private val loggerTag = LoggerTag("MXMegolmEncryption", LoggerTag.CRYPTO) + internal class MXMegolmEncryption( // The id of the room we will be sending to. private val roomId: String, @@ -51,8 +56,8 @@ internal class MXMegolmEncryption( private val cryptoStore: IMXCryptoStore, private val deviceListManager: DeviceListManager, private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, - private val userId: String, - private val deviceId: String, + private val myUserId: String, + private val myDeviceId: String, private val sendToDeviceTask: SendToDeviceTask, private val messageEncrypter: MessageEncrypter, private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository, @@ -80,9 +85,10 @@ internal class MXMegolmEncryption( eventType: String, userIds: List<String>): Content { val ts = System.currentTimeMillis() - Timber.v("## CRYPTO | encryptEventContent : getDevicesInRoom") + Timber.tag(loggerTag.value).v("encryptEventContent : getDevicesInRoom") val devices = getDevicesInRoom(userIds) - Timber.v("## CRYPTO | encryptEventContent ${System.currentTimeMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.map}") + Timber.tag(loggerTag.value).d("encrypt event in room=$roomId - devices count in room ${devices.allowedDevices.toDebugCount()}") + Timber.tag(loggerTag.value).v("encryptEventContent ${System.currentTimeMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.map}") val outboundSession = ensureOutboundSession(devices.allowedDevices) return encryptContent(outboundSession, eventType, eventContent) @@ -91,7 +97,7 @@ internal class MXMegolmEncryption( // annoyingly we have to serialize again the saved outbound session to store message index :/ // if not we would see duplicate message index errors olmDevice.storeOutboundGroupSessionForRoom(roomId, outboundSession.sessionId) - Timber.v("## CRYPTO | encryptEventContent: Finished in ${System.currentTimeMillis() - ts} millis") + Timber.tag(loggerTag.value).d("encrypt event in room=$roomId Finished in ${System.currentTimeMillis() - ts} millis") } } @@ -118,13 +124,13 @@ internal class MXMegolmEncryption( override suspend fun preshareKey(userIds: List<String>) { val ts = System.currentTimeMillis() - Timber.v("## CRYPTO | preshareKey : getDevicesInRoom") + Timber.tag(loggerTag.value).d("preshareKey started in $roomId ...") val devices = getDevicesInRoom(userIds) val outboundSession = ensureOutboundSession(devices.allowedDevices) notifyWithheldForSession(devices.withHeldDevices, outboundSession) - Timber.v("## CRYPTO | preshareKey ${System.currentTimeMillis() - ts} millis") + Timber.tag(loggerTag.value).d("preshareKey in $roomId done in ${System.currentTimeMillis() - ts} millis") } /** @@ -133,7 +139,7 @@ internal class MXMegolmEncryption( * @return the session description */ private fun prepareNewSessionInRoom(): MXOutboundSessionInfo { - Timber.v("## CRYPTO | prepareNewSessionInRoom() ") + Timber.tag(loggerTag.value).v("prepareNewSessionInRoom() ") val sessionId = olmDevice.createOutboundGroupSessionForRoom(roomId) val keysClaimedMap = HashMap<String, String>() @@ -153,13 +159,14 @@ internal class MXMegolmEncryption( * @param devicesInRoom the devices list */ private suspend fun ensureOutboundSession(devicesInRoom: MXUsersDevicesMap<CryptoDeviceInfo>): MXOutboundSessionInfo { - Timber.v("## CRYPTO | ensureOutboundSession start") + Timber.tag(loggerTag.value).v("ensureOutboundSession roomId:$roomId") var session = outboundSession if (session == null || // Need to make a brand new session? session.needsRotation(sessionRotationPeriodMsgs, sessionRotationPeriodMs) || // Determine if we have shared with anyone we shouldn't have session.sharedWithTooManyDevices(devicesInRoom)) { + Timber.tag(loggerTag.value).d("roomId:$roomId Starting new megolm session because we need to rotate.") session = prepareNewSessionInRoom() outboundSession = session } @@ -176,6 +183,8 @@ internal class MXMegolmEncryption( } } } + val devicesCount = shareMap.entries.fold(0) { acc, new -> acc + new.value.size } + Timber.tag(loggerTag.value).d("roomId:$roomId found $devicesCount devices without megolm session(${session.sessionId})") shareKey(safeSession, shareMap) return safeSession } @@ -190,7 +199,7 @@ internal class MXMegolmEncryption( devicesByUsers: Map<String, List<CryptoDeviceInfo>>) { // nothing to send, the task is done if (devicesByUsers.isEmpty()) { - Timber.v("## CRYPTO | shareKey() : nothing more to do") + Timber.tag(loggerTag.value).v("shareKey() : nothing more to do") return } // reduce the map size to avoid request timeout when there are too many devices (Users size * devices per user) @@ -203,7 +212,7 @@ internal class MXMegolmEncryption( break } } - Timber.v("## CRYPTO | shareKey() ; sessionId<${session.sessionId}> userId ${subMap.keys}") + Timber.tag(loggerTag.value).v("shareKey() ; sessionId<${session.sessionId}> userId ${subMap.keys}") shareUserDevicesKey(session, subMap) val remainingDevices = devicesByUsers - subMap.keys shareKey(session, remainingDevices) @@ -232,11 +241,11 @@ internal class MXMegolmEncryption( payload["content"] = submap var t0 = System.currentTimeMillis() - Timber.v("## CRYPTO | shareUserDevicesKey() : starts") + Timber.tag(loggerTag.value).v("shareUserDevicesKey() : starts") val results = ensureOlmSessionsForDevicesAction.handle(devicesByUser) - Timber.v( - """## CRYPTO | shareUserDevicesKey(): ensureOlmSessionsForDevices succeeds after ${System.currentTimeMillis() - t0} ms""" + Timber.tag(loggerTag.value).v( + """shareUserDevicesKey(): ensureOlmSessionsForDevices succeeds after ${System.currentTimeMillis() - t0} ms""" .trimMargin() ) val contentMap = MXUsersDevicesMap<Any>() @@ -254,10 +263,11 @@ internal class MXMegolmEncryption( // MSC 2399 // send withheld m.no_olm: an olm session could not be established. // This may happen, for example, if the sender was unable to obtain a one-time key from the recipient. + Timber.tag(loggerTag.value).v("shareUserDevicesKey() : No Olm Session for $userId:$deviceID mark for withheld") noOlmToNotify.add(UserDevice(userId, deviceID)) continue } - Timber.i("## CRYPTO | shareUserDevicesKey() : Add to share keys contentMap for $userId:$deviceID") + Timber.tag(loggerTag.value).v("shareUserDevicesKey() : Add to share keys contentMap for $userId:$deviceID") contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, listOf(sessionResult.deviceInfo))) haveTargets = true } @@ -275,7 +285,7 @@ internal class MXMegolmEncryption( gossipingEventBuffer.add( Event( type = EventType.ROOM_KEY, - senderId = this.userId, + senderId = myUserId, content = submap.apply { this["session_key"] = "" // we add a fake key for trail @@ -289,17 +299,18 @@ internal class MXMegolmEncryption( if (haveTargets) { t0 = System.currentTimeMillis() - Timber.i("## CRYPTO | shareUserDevicesKey() ${session.sessionId} : has target") + Timber.tag(loggerTag.value).i("shareUserDevicesKey() ${session.sessionId} : has target") + Timber.tag(loggerTag.value).d("sending to device room key for ${session.sessionId} to ${contentMap.toDebugString()}") val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap) try { sendToDeviceTask.execute(sendToDeviceParams) - Timber.i("## CRYPTO | shareUserDevicesKey() : sendToDevice succeeds after ${System.currentTimeMillis() - t0} ms") + Timber.tag(loggerTag.value).i("shareUserDevicesKey() : sendToDevice succeeds after ${System.currentTimeMillis() - t0} ms") } catch (failure: Throwable) { // What to do here... - Timber.e("## CRYPTO | shareUserDevicesKey() : Failed to share session <${session.sessionId}> with $devicesByUser ") + Timber.tag(loggerTag.value).e("shareUserDevicesKey() : Failed to share session <${session.sessionId}> with $devicesByUser ") } } else { - Timber.i("## CRYPTO | shareUserDevicesKey() : no need to sharekey") + Timber.tag(loggerTag.value).i("shareUserDevicesKey() : no need to share key") } if (noOlmToNotify.isNotEmpty()) { @@ -317,7 +328,8 @@ internal class MXMegolmEncryption( sessionId: String, senderKey: String?, code: WithHeldCode) { - Timber.i("## CRYPTO | notifyKeyWithHeld() :sending withheld key for $targets session:$sessionId and code $code") + Timber.tag(loggerTag.value).d("notifyKeyWithHeld() :sending withheld for session:$sessionId and code $code to" + + " ${targets.joinToString { "${it.userId}|${it.deviceId}" }}") val withHeldContent = RoomKeyWithHeldContent( roomId = roomId, senderKey = senderKey, @@ -336,7 +348,7 @@ internal class MXMegolmEncryption( try { sendToDeviceTask.execute(params) } catch (failure: Throwable) { - Timber.e("## CRYPTO | notifyKeyWithHeld() : Failed to notify withheld key for $targets session: $sessionId ") + Timber.tag(loggerTag.value).e("notifyKeyWithHeld() : Failed to notify withheld key for $targets session: $sessionId ") } } @@ -363,7 +375,7 @@ internal class MXMegolmEncryption( // Include our device ID so that recipients can send us a // m.new_device message if they don't have our session key. - map["device_id"] = deviceId + map["device_id"] = myDeviceId session.useCount++ return map } @@ -424,9 +436,9 @@ internal class MXMegolmEncryption( userId: String, deviceId: String, senderKey: String): Boolean { - Timber.i("## Crypto process reshareKey for $sessionId to $userId:$deviceId") + Timber.tag(loggerTag.value).i("process reshareKey for $sessionId to $userId:$deviceId") val deviceInfo = cryptoStore.getUserDevice(userId, deviceId) ?: return false - .also { Timber.w("## Crypto reshareKey: Device not found") } + .also { Timber.tag(loggerTag.value).w("reshareKey: Device not found") } // Get the chain index of the key we previously sent this device val wasSessionSharedWithUser = cryptoStore.getSharedSessionInfo(roomId, sessionId, deviceInfo) @@ -434,13 +446,13 @@ internal class MXMegolmEncryption( // This session was never shared with this user // Send a room key with held notifyKeyWithHeld(listOf(UserDevice(userId, deviceId)), sessionId, senderKey, WithHeldCode.UNAUTHORISED) - Timber.w("## Crypto reshareKey: ERROR : Never shared megolm with this device") + Timber.tag(loggerTag.value).w("reshareKey: ERROR : Never shared megolm with this device") return false } // if found chain index should not be null val chainIndex = wasSessionSharedWithUser.chainIndex ?: return false .also { - Timber.w("## Crypto reshareKey: Null chain index") + Timber.tag(loggerTag.value).w("reshareKey: Null chain index") } val devicesByUser = mapOf(userId to listOf(deviceInfo)) @@ -449,10 +461,10 @@ internal class MXMegolmEncryption( olmSessionResult?.sessionId // no session with this device, probably because there were no one-time keys. // ensureOlmSessionsForDevicesAction has already done the logging, so just skip it. ?: return false.also { - Timber.w("## Crypto reshareKey: no session with this device, probably because there were no one-time keys") + Timber.tag(loggerTag.value).w("reshareKey: no session with this device, probably because there were no one-time keys") } - Timber.i("[MXMegolmEncryption] reshareKey: sharing keys for session $senderKey|$sessionId:$chainIndex with device $userId:$deviceId") + Timber.tag(loggerTag.value).i(" reshareKey: sharing keys for session $senderKey|$sessionId:$chainIndex with device $userId:$deviceId") val payloadJson = mutableMapOf<String, Any>("type" to EventType.FORWARDED_ROOM_KEY) @@ -464,7 +476,7 @@ internal class MXMegolmEncryption( }, { // TODO - Timber.e(it, "[MXMegolmEncryption] reshareKey: failed to get session $sessionId|$senderKey|$roomId") + Timber.tag(loggerTag.value).e(it, "reshareKey: failed to get session $sessionId|$senderKey|$roomId") } ) @@ -472,14 +484,14 @@ internal class MXMegolmEncryption( val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo)) val sendToDeviceMap = MXUsersDevicesMap<Any>() sendToDeviceMap.setObject(userId, deviceId, encodedPayload) - Timber.i("## CRYPTO | reshareKey() : sending session $sessionId to $userId:$deviceId") + Timber.tag(loggerTag.value).i("reshareKey() : sending session $sessionId to $userId:$deviceId") val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap) return try { sendToDeviceTask.execute(sendToDeviceParams) - Timber.i("## CRYPTO reshareKey() : successfully send <$sessionId> to $userId:$deviceId") + Timber.tag(loggerTag.value).i("reshareKey() : successfully send <$sessionId> to $userId:$deviceId") true } catch (failure: Throwable) { - Timber.e(failure, "## CRYPTO reshareKey() : fail to send <$sessionId> to $userId:$deviceId") + Timber.tag(loggerTag.value).e(failure, "reshareKey() : fail to send <$sessionId> to $userId:$deviceId") false } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt index 238d7eed..136fdc05 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt @@ -52,8 +52,8 @@ internal class MXMegolmEncryptionFactory @Inject constructor( cryptoStore = cryptoStore, deviceListManager = deviceListManager, ensureOlmSessionsForDevicesAction = ensureOlmSessionsForDevicesAction, - userId = userId, - deviceId = deviceId!!, + myUserId = userId, + myDeviceId = deviceId!!, sendToDeviceTask = sendToDeviceTask, messageEncrypter = messageEncrypter, warnOnUnknownDevicesRepository = warnOnUnknownDevicesRepository, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXUsersDevicesMap.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXUsersDevicesMap.kt index 9d7f2d98..66254142 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXUsersDevicesMap.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXUsersDevicesMap.kt @@ -129,3 +129,11 @@ inline fun <T> MXUsersDevicesMap<T>.forEach(action: (String, String, T) -> Unit) } } } + +internal fun <T> MXUsersDevicesMap<T>.toDebugString() = + map.entries.joinToString { "${it.key} [${it.value.keys.joinToString { it }}]" } + +internal fun <T> MXUsersDevicesMap<T>.toDebugCount() = + map.entries.fold(0) { acc, new -> + acc + new.value.keys.size + } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt index e40db6af..bdfe818c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt @@ -23,6 +23,7 @@ import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository import org.matrix.android.sdk.internal.task.Task +import timber.log.Timber import javax.inject.Inject internal interface SendEventTask : Task<SendEventTask.Params, String> { @@ -60,7 +61,9 @@ internal class DefaultSendEventTask @Inject constructor( ) } localEchoRepository.updateSendState(localId, params.event.roomId, SendState.SENT) - return response.eventId + return response.eventId.also { + Timber.d("Event: $it just sent in ${params.event.roomId}") + } } catch (e: Throwable) { // localEchoRepository.updateSendState(params.event.eventId!!, SendState.UNDELIVERED) throw e diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/DraftMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/DraftMapper.kt index 148f727b..737c4b46 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/DraftMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/DraftMapper.kt @@ -26,20 +26,22 @@ internal object DraftMapper { fun map(entity: DraftEntity): UserDraft { return when (entity.draftMode) { - DraftEntity.MODE_REGULAR -> UserDraft.REGULAR(entity.content) - DraftEntity.MODE_EDIT -> UserDraft.EDIT(entity.linkedEventId, entity.content) - DraftEntity.MODE_QUOTE -> UserDraft.QUOTE(entity.linkedEventId, entity.content) - DraftEntity.MODE_REPLY -> UserDraft.REPLY(entity.linkedEventId, entity.content) + DraftEntity.MODE_REGULAR -> UserDraft.Regular(entity.content) + DraftEntity.MODE_EDIT -> UserDraft.Edit(entity.linkedEventId, entity.content) + DraftEntity.MODE_QUOTE -> UserDraft.Quote(entity.linkedEventId, entity.content) + DraftEntity.MODE_REPLY -> UserDraft.Reply(entity.linkedEventId, entity.content) + DraftEntity.MODE_VOICE -> UserDraft.Voice(entity.content) else -> null - } ?: UserDraft.REGULAR("") + } ?: UserDraft.Regular("") } fun map(domain: UserDraft): DraftEntity { return when (domain) { - is UserDraft.REGULAR -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_REGULAR, linkedEventId = "") - is UserDraft.EDIT -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_EDIT, linkedEventId = domain.linkedEventId) - is UserDraft.QUOTE -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_QUOTE, linkedEventId = domain.linkedEventId) - is UserDraft.REPLY -> DraftEntity(content = domain.text, draftMode = DraftEntity.MODE_REPLY, linkedEventId = domain.linkedEventId) + is UserDraft.Regular -> DraftEntity(content = domain.content, draftMode = DraftEntity.MODE_REGULAR, linkedEventId = "") + is UserDraft.Edit -> DraftEntity(content = domain.content, draftMode = DraftEntity.MODE_EDIT, linkedEventId = domain.linkedEventId) + is UserDraft.Quote -> DraftEntity(content = domain.content, draftMode = DraftEntity.MODE_QUOTE, linkedEventId = domain.linkedEventId) + is UserDraft.Reply -> DraftEntity(content = domain.content, draftMode = DraftEntity.MODE_REPLY, linkedEventId = domain.linkedEventId) + is UserDraft.Voice -> DraftEntity(content = domain.content, draftMode = DraftEntity.MODE_VOICE, linkedEventId = "") } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/DraftEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/DraftEntity.kt index 15a5d379..fd09da44 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/DraftEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/DraftEntity.kt @@ -21,7 +21,6 @@ import io.realm.RealmObject internal open class DraftEntity(var content: String = "", var draftMode: String = MODE_REGULAR, var linkedEventId: String = "" - ) : RealmObject() { companion object { @@ -29,5 +28,6 @@ internal open class DraftEntity(var content: String = "", const val MODE_EDIT = "EDIT" const val MODE_REPLY = "REPLY" const val MODE_QUOTE = "QUOTE" + const val MODE_VOICE = "VOICE" } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt index bcd30cb5..836fc4ef 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.database.model import io.realm.RealmObject import io.realm.annotations.Index import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult import org.matrix.android.sdk.internal.di.MoshiProvider @@ -56,10 +57,10 @@ internal open class EventEntity(@Index var eventId: String = "", companion object - fun setDecryptionResult(result: MXEventDecryptionResult) { + fun setDecryptionResult(result: MXEventDecryptionResult, clearEvent: JsonDict? = null) { assertIsManaged() val decryptionResult = OlmDecryptionResult( - payload = result.clearEvent, + payload = clearEvent ?: result.clearEvent, senderKey = result.senderCurve25519Key, keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) }, forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt index b657d950..7f35c910 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt @@ -279,6 +279,11 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter Timber.e(failure, "## Failed to update file cache") } + // Delete the temporary voice message file + if (params.attachment.type == ContentAttachmentData.Type.VOICE_MESSAGE) { + context.contentResolver.delete(params.attachment.queryUri, null, null) + } + val uploadThumbnailResult = dealWithThumbnail(params) handleSuccess(params, @@ -299,11 +304,6 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter filesToDelete.forEach { tryOrNull { it.delete() } } - - // Delete the temporary voice message file - if (params.attachment.type == ContentAttachmentData.Type.AUDIO && params.attachment.mimeType == MimeTypes.Ogg) { - context.contentResolver.delete(params.attachment.queryUri, null, null) - } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/events/DefaultEventService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/events/DefaultEventService.kt index 9d80f27e..51d305f4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/events/DefaultEventService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/events/DefaultEventService.kt @@ -29,7 +29,6 @@ internal class DefaultEventService @Inject constructor( override suspend fun getEvent(roomId: String, eventId: String): Event { val event = getEventTask.execute(GetEventTask.Params(roomId, eventId)) - event.ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it } // Fast lane to the call event processors: try to make the incoming call ring faster if (callEventProcessor.shouldProcessFastLane(event.getClearType())) { callEventProcessor.processFastLane(event) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index 5cb96875..a31d0cde 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -205,10 +205,11 @@ internal class LocalEchoEventFactory @Inject constructor( fun createMediaEvent(roomId: String, attachment: ContentAttachmentData): Event { return when (attachment.type) { - ContentAttachmentData.Type.IMAGE -> createImageEvent(roomId, attachment) - ContentAttachmentData.Type.VIDEO -> createVideoEvent(roomId, attachment) - ContentAttachmentData.Type.AUDIO -> createAudioEvent(roomId, attachment) - ContentAttachmentData.Type.FILE -> createFileEvent(roomId, attachment) + ContentAttachmentData.Type.IMAGE -> createImageEvent(roomId, attachment) + ContentAttachmentData.Type.VIDEO -> createVideoEvent(roomId, attachment) + ContentAttachmentData.Type.AUDIO -> createAudioEvent(roomId, attachment, isVoiceMessage = false) + ContentAttachmentData.Type.VOICE_MESSAGE -> createAudioEvent(roomId, attachment, isVoiceMessage = true) + ContentAttachmentData.Type.FILE -> createFileEvent(roomId, attachment) } } @@ -296,8 +297,7 @@ internal class LocalEchoEventFactory @Inject constructor( return createMessageEvent(roomId, content) } - private fun createAudioEvent(roomId: String, attachment: ContentAttachmentData): Event { - val isVoiceMessage = attachment.waveform != null + private fun createAudioEvent(roomId: String, attachment: ContentAttachmentData, isVoiceMessage: Boolean): Event { val content = MessageAudioContent( msgType = MessageType.MSGTYPE_AUDIO, body = attachment.name ?: "audio", diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index 0c917448..2744b512 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -23,6 +23,7 @@ import io.realm.RealmConfiguration import io.realm.RealmQuery import io.realm.RealmResults import io.realm.Sort +import kotlinx.coroutines.runBlocking import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull @@ -33,6 +34,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.api.util.CancelableBag import org.matrix.android.sdk.internal.database.RealmSessionProvider +import org.matrix.android.sdk.internal.database.mapper.EventMapper import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.database.model.ChunkEntity import org.matrix.android.sdk.internal.database.model.RoomEntity @@ -43,6 +45,7 @@ import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.whereRoomId import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler +import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.util.Debouncer @@ -72,6 +75,7 @@ internal class DefaultTimeline( private val eventDecryptor: TimelineEventDecryptor, private val realmSessionProvider: RealmSessionProvider, private val loadRoomMembersTask: LoadRoomMembersTask, + private val threadsAwarenessHandler: ThreadsAwarenessHandler, private val readReceiptHandler: ReadReceiptHandler ) : Timeline, TimelineInput.Listener, @@ -577,6 +581,10 @@ internal class DefaultTimeline( } else { nextDisplayIndex = offsetIndex + 1 } + + // Prerequisite to in order for the ThreadsAwarenessHandler to work properly + fetchRootThreadEventsIfNeeded(offsetResults) + offsetResults.forEach { eventEntity -> val timelineEvent = buildTimelineEvent(eventEntity) @@ -601,6 +609,20 @@ internal class DefaultTimeline( return offsetResults.size } + /** + * This function is responsible to fetch and store the root event of a thread event + * in order to be able to display the event to the user appropriately + */ + private fun fetchRootThreadEventsIfNeeded(offsetResults: RealmResults<TimelineEventEntity>) = runBlocking { + val eventEntityList = offsetResults + .mapNotNull { + it?.root + }.map { + EventMapper.map(it) + } + threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(eventEntityList) + } + private fun buildTimelineEvent(eventEntity: TimelineEventEntity): TimelineEvent { return timelineEventMapper.map( timelineEventEntity = eventEntity, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt index 47e8f7e3..75e7e774 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt @@ -38,6 +38,7 @@ import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler +import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler import org.matrix.android.sdk.internal.task.TaskExecutor internal class DefaultTimelineService @AssistedInject constructor( @@ -52,6 +53,7 @@ internal class DefaultTimelineService @AssistedInject constructor( private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, private val timelineEventMapper: TimelineEventMapper, private val loadRoomMembersTask: LoadRoomMembersTask, + private val threadsAwarenessHandler: ThreadsAwarenessHandler, private val readReceiptHandler: ReadReceiptHandler ) : TimelineService { @@ -75,6 +77,7 @@ internal class DefaultTimelineService @AssistedInject constructor( fetchTokenAndPaginateTask = fetchTokenAndPaginateTask, realmSessionProvider = realmSessionProvider, loadRoomMembersTask = loadRoomMembersTask, + threadsAwarenessHandler = threadsAwarenessHandler, readReceiptHandler = readReceiptHandler ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt index cbbc54e9..9ede2f65 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt @@ -59,6 +59,8 @@ internal class DefaultGetEventTask @Inject constructor( } } + event.ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it } + return event } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt index 721dae0b..75d02dfd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt @@ -26,6 +26,7 @@ import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler import timber.log.Timber import java.util.concurrent.ExecutorService import java.util.concurrent.Executors @@ -34,7 +35,8 @@ import javax.inject.Inject internal class TimelineEventDecryptor @Inject constructor( @SessionDatabase private val realmConfiguration: RealmConfiguration, - private val cryptoService: CryptoService + private val cryptoService: CryptoService, + private val threadsAwarenessHandler: ThreadsAwarenessHandler ) { private val newSessionListener = object : NewSessionListener { @@ -106,10 +108,19 @@ internal class TimelineEventDecryptor @Inject constructor( val result = cryptoService.decryptEvent(request.event, timelineId) Timber.v("Successfully decrypted event ${event.eventId}") realm.executeTransaction { - val eventId = event.eventId ?: "" - EventEntity.where(it, eventId = eventId) + val eventId = event.eventId ?: return@executeTransaction + val eventEntity = EventEntity + .where(it, eventId = eventId) .findFirst() - ?.setDecryptionResult(result) + + eventEntity?.apply { + val decryptedPayload = threadsAwarenessHandler.handleIfNeededDuringDecryption( + it, + roomId = event.roomId, + event, + result) + setDecryptionResult(result, decryptedPayload) + } } } catch (e: MXCryptoError) { Timber.v("Failed to decrypt event ${event.eventId} : ${e.localizedMessage}") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt index 8fd969e3..f1780745 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt @@ -41,6 +41,7 @@ import org.matrix.android.sdk.internal.session.sync.handler.PresenceSyncHandler import org.matrix.android.sdk.internal.session.sync.handler.SyncResponsePostTreatmentAggregatorHandler import org.matrix.android.sdk.internal.session.sync.handler.UserAccountDataSyncHandler import org.matrix.android.sdk.internal.session.sync.handler.room.RoomSyncHandler +import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler import org.matrix.android.sdk.internal.util.awaitTransaction import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import timber.log.Timber @@ -65,6 +66,7 @@ internal class SyncResponseHandler @Inject constructor( private val tokenStore: SyncTokenStore, private val processEventForPushTask: ProcessEventForPushTask, private val pushRuleService: PushRuleService, + private val threadsAwarenessHandler: ThreadsAwarenessHandler, private val presenceSyncHandler: PresenceSyncHandler ) { @@ -97,6 +99,10 @@ internal class SyncResponseHandler @Inject constructor( Timber.v("Finish handling toDevice in $it ms") } val aggregator = SyncResponsePostTreatmentAggregator() + + // Prerequisite for thread events handling in RoomSyncHandler + threadsAwarenessHandler.fetchRootThreadEventsIfNeeded(syncResponse) + // Start one big transaction monarchy.awaitTransaction { realm -> measureTimeMillis { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt index c5ec3417..f299d3ef 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.session.sync.handler +import org.matrix.android.sdk.api.logger.LoggerTag import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType @@ -32,6 +33,8 @@ import org.matrix.android.sdk.internal.session.initsync.ProgressReporter import timber.log.Timber import javax.inject.Inject +private val loggerTag = LoggerTag("CryptoSyncHandler", LoggerTag.CRYPTO) + internal class CryptoSyncHandler @Inject constructor(private val cryptoService: DefaultCryptoService, private val verificationService: DefaultVerificationService) { @@ -40,11 +43,11 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService: toDevice.events?.forEachIndexed { index, event -> progressReporter?.reportProgress(index * 100F / total) // Decrypt event if necessary - Timber.i("## CRYPTO | To device event from ${event.senderId} of type:${event.type}") + Timber.tag(loggerTag.value).i("To device event from ${event.senderId} of type:${event.type}") decryptToDeviceEvent(event, null) if (event.getClearType() == EventType.MESSAGE && event.getClearContent()?.toModel<MessageContent>()?.msgType == "m.bad.encrypted") { - Timber.e("## CRYPTO | handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}") + Timber.tag(loggerTag.value).e("handleToDeviceEvent() : Warning: Unable to decrypt to-device event : ${event.content}") } else { verificationService.onToDeviceEvent(event) cryptoService.onToDeviceEvent(event) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt index 8c4af81c..1a7e15e1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt @@ -76,6 +76,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle private val cryptoService: DefaultCryptoService, private val roomMemberEventHandler: RoomMemberEventHandler, private val roomTypingUsersHandler: RoomTypingUsersHandler, + private val threadsAwarenessHandler: ThreadsAwarenessHandler, private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource, @UserId private val userId: String, private val timelineInput: TimelineInput) { @@ -362,10 +363,17 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle } eventIds.add(event.eventId) - if (event.isEncrypted() && insertType != EventInsertType.INITIAL_SYNC) { + val isInitialSync = insertType == EventInsertType.INITIAL_SYNC + + if (event.isEncrypted() && !isInitialSync) { decryptIfNeeded(event, roomId) } + threadsAwarenessHandler.handleIfNeeded( + realm = realm, + roomId = roomId, + event = event) + val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType) if (event.stateKey != null) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt new file mode 100644 index 00000000..767a9675 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt @@ -0,0 +1,263 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.sync.handler.room + +import com.zhuinden.monarchy.Monarchy +import io.realm.Realm +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageFormat +import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent +import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent +import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.api.session.sync.model.SyncResponse +import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult +import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult +import org.matrix.android.sdk.internal.database.mapper.EventMapper +import org.matrix.android.sdk.internal.database.mapper.toEntity +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.permalinks.PermalinkFactory +import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory +import org.matrix.android.sdk.internal.session.room.timeline.GetEventTask +import org.matrix.android.sdk.internal.util.awaitTransaction +import javax.inject.Inject + +/** + * This handler is responsible for a smooth threads migration. It will map all incoming + * threads as replies. So a device without threads enabled/updated will be able to view + * threads response as replies to the original message + */ +internal class ThreadsAwarenessHandler @Inject constructor( + private val permalinkFactory: PermalinkFactory, + private val cryptoService: CryptoService, + @SessionDatabase private val monarchy: Monarchy, + private val getEventTask: GetEventTask +) { + + /** + * Fetch root thread events if they are missing from the local storage + * @param syncResponse the sync response + */ + suspend fun fetchRootThreadEventsIfNeeded(syncResponse: SyncResponse) { + val handlingStrategy = syncResponse.rooms?.join?.let { + RoomSyncHandler.HandlingStrategy.JOINED(it) + } + if (handlingStrategy !is RoomSyncHandler.HandlingStrategy.JOINED) return + val eventList = handlingStrategy.data + .mapNotNull { (roomId, roomSync) -> + roomSync.timeline?.events?.map { + it.copy(roomId = roomId) + } + }.flatten() + + fetchRootThreadEventsIfNeeded(eventList) + } + + /** + * Fetch root thread events if they are missing from the local storage + * @param eventList a list with the events to examine + */ + suspend fun fetchRootThreadEventsIfNeeded(eventList: List<Event>) { + if (eventList.isNullOrEmpty()) return + + val threadsToFetch = emptyMap<String, String>().toMutableMap() + Realm.getInstance(monarchy.realmConfiguration).use { realm -> + eventList.asSequence() + .filter { + isThreadEvent(it) && it.roomId != null + }.mapNotNull { event -> + getRootThreadEventId(event)?.let { + Pair(it, event.roomId!!) + } + }.forEach { (rootThreadEventId, roomId) -> + EventEntity.where(realm, rootThreadEventId).findFirst() ?: run { threadsToFetch[rootThreadEventId] = roomId } + } + } + fetchThreadsEvents(threadsToFetch) + } + + /** + * Fetch multiple unique events using the fetchEvent function + */ + private suspend fun fetchThreadsEvents(threadsToFetch: Map<String, String>) { + val eventEntityList = threadsToFetch.mapNotNull { (eventId, roomId) -> + fetchEvent(eventId, roomId)?.let { + it.toEntity(roomId, SendState.SYNCED, it.ageLocalTs) + } + } + + if (eventEntityList.isNullOrEmpty()) return + + // Transaction should be done on its own thread, like below + monarchy.awaitTransaction { realm -> + eventEntityList.forEach { + it.copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC) + } + } + } + + /** + * This function will fetch the event from the homeserver, this is mandatory when the + * initial thread message is too old and is not saved in the device, so in order to + * construct the "reply to" format we need to know the event thread. + * @return the Event or null otherwise + */ + private suspend fun fetchEvent(eventId: String, roomId: String): Event? { + return runCatching { + getEventTask.execute(GetEventTask.Params(roomId = roomId, eventId = eventId)) + }.fold( + onSuccess = { + it + }, + onFailure = { + null + }) + } + + /** + * Handle events mainly coming from the RoomSyncHandler + */ + fun handleIfNeeded(realm: Realm, + roomId: String, + event: Event) { + val payload = transformThreadToReplyIfNeeded( + realm = realm, + roomId = roomId, + event = event, + decryptedResult = event.mxDecryptionResult?.payload) ?: return + + event.mxDecryptionResult = event.mxDecryptionResult?.copy(payload = payload) + } + + /** + * Handle events while they are being decrypted + */ + fun handleIfNeededDuringDecryption(realm: Realm, + roomId: String?, + event: Event, + result: MXEventDecryptionResult): JsonDict? { + return transformThreadToReplyIfNeeded( + realm = realm, + roomId = roomId, + event = event, + decryptedResult = result.clearEvent) + } + + /** + * If the event is a thread event then transform/enhance it to a visual Reply Event, + * If the event is not a thread event, null value will be returned + * If there is an error (ex. the root/origin thread event is not found), null willl be returend + */ + private fun transformThreadToReplyIfNeeded(realm: Realm, roomId: String?, event: Event, decryptedResult: JsonDict?): JsonDict? { + roomId ?: return null + if (!isThreadEvent(event)) return null + val rootThreadEventId = getRootThreadEventId(event) ?: return null + val payload = decryptedResult?.toMutableMap() ?: return null + val body = getValueFromPayload(payload, "body") ?: return null + val msgType = getValueFromPayload(payload, "msgtype") ?: return null + val rootThreadEvent = getEventFromDB(realm, rootThreadEventId) ?: return null + val rootThreadEventSenderId = rootThreadEvent.senderId ?: return null + + decryptIfNeeded(rootThreadEvent, roomId) + + val rootThreadEventBody = getValueFromPayload(rootThreadEvent.mxDecryptionResult?.payload?.toMutableMap(), "body") + + val permalink = permalinkFactory.createPermalink(roomId, rootThreadEventId, false) + val userLink = permalinkFactory.createPermalink(rootThreadEventSenderId, false) ?: "" + + val replyFormatted = LocalEchoEventFactory.REPLY_PATTERN.format( + permalink, + userLink, + rootThreadEventSenderId, + // Remove inner mx_reply tags if any + rootThreadEventBody, + body) + + val messageTextContent = MessageTextContent( + msgType = msgType, + format = MessageFormat.FORMAT_MATRIX_HTML, + body = body, + formattedBody = replyFormatted + ).toContent() + + payload["content"] = messageTextContent + + return payload + } + + /** + * Decrypt the event + */ + + private fun decryptIfNeeded(event: Event, roomId: String) { + try { + if (!event.isEncrypted() || event.mxDecryptionResult != null) return + + // Event from sync does not have roomId, so add it to the event first + val result = cryptoService.decryptEvent(event.copy(roomId = roomId), "") + event.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) + } catch (e: MXCryptoError) { + if (e is MXCryptoError.Base) { + event.mCryptoError = e.errorType + event.mCryptoErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription + } + } + } + + /** + * Try to get the event form the local DB, if the event does not exist null + * will be returned + */ + private fun getEventFromDB(realm: Realm, eventId: String): Event? { + val eventEntity = EventEntity.where(realm, eventId = eventId).findFirst() ?: return null + return EventMapper.map(eventEntity) + } + + /** + * Returns True if the event is a thread + * @param event + */ + private fun isThreadEvent(event: Event): Boolean = + event.content.toModel<MessageRelationContent>()?.relatesTo?.type == RelationType.THREAD + + /** + * Returns the root thread eventId or null otherwise + * @param event + */ + private fun getRootThreadEventId(event: Event): String? = + event.content.toModel<MessageRelationContent>()?.relatesTo?.eventId + + @Suppress("UNCHECKED_CAST") + private fun getValueFromPayload(payload: JsonDict?, key: String): String? { + val content = payload?.get("content") as? JsonDict + return content?.get(key) as? String + } +} -- GitLab