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