diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index 681f41ae2aee4749eb4ddda94f8c6a76c825c825..88ea3aa1e593800a41855fcc6e3cfbff8e41e169 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -1,5 +1,8 @@
 <component name="ProjectCodeStyleConfiguration">
   <code_scheme name="Project" version="173">
+    <JetCodeStyleSettings>
+      <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
+    </JetCodeStyleSettings>
     <codeStyleSettings language="XML">
       <indentOptions>
         <option name="CONTINUATION_INDENT_SIZE" value="4" />
@@ -112,5 +115,8 @@
         </rules>
       </arrangement>
     </codeStyleSettings>
+    <codeStyleSettings language="kotlin">
+      <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
+    </codeStyleSettings>
   </code_scheme>
 </component>
\ No newline at end of file
diff --git a/CHANGES.md b/CHANGES.md
index a4694b33ceb2b71963484b6485b9c1069c1e3ee2..71ad471453558637894bb01dd19278b620b39bd4 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,29 @@
 Please also refer to the Changelog of Element Android: https://github.com/vector-im/element-android/blob/main/CHANGES.md
 
+Changes in Matrix-SDK 1.4.13 (2022-04-26)
+===================================================
+
+Imported from Element 1.4.13. (https://github.com/vector-im/element-android/releases/tag/v1.4.13)
+
+SDK API changes ⚠️
+------------------
+ - Include original event in live decryption listeners and update sync status naming to InitialSyncProgressing for clarity. ([#5639](https://github.com/vector-im/element-android/issues/5639))
+ - KeysBackupService.getCurrentVersion takes a new type `KeysBackupLastVersionResult` in the callback. ([#5703](https://github.com/vector-im/element-android/issues/5703))
+ - A lot of classes which were exposed to the clients and were located in the package `org.matrix.android.sdk.internal` have been moved to the package `org.matrix.android.sdk.api`.
+  All the classes which are in the package `org.matrix.android.sdk.internal` should now be declared `internal`.
+  Some unused code and classes have been removed. ([#5744](https://github.com/vector-im/element-android/issues/5744))
+ - Some data classes are now immutable, using `val` instead of `var` ([#5762](https://github.com/vector-im/element-android/issues/5762))
+
+
+Changes in Matrix-SDK 1.4.11 (2022-04-12)
+===================================================
+
+Imported from Element 1.4.11. (https://github.com/vector-im/element-android/releases/tag/v1.4.11)
+
+SDK API changes ⚠️
+------------------
+ - Adds support for MSC3440, additional threads homeserver capabilities ([#5271](https://github.com/vector-im/element-android/issues/5271))
+
 Changes in Matrix-SDK 1.4.4 (2022-03-16)
 ===================================================
 
diff --git a/README.md b/README.md
index 45b20cbee0aeae698c97d975230882201317535b..7badd82641e23e1ede01d5cdf8e81b6bf0c0fff2 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,8 @@
 matrix-android-sdk
 
+To add remote upstream
+git remote add upstream https://github.com/matrix-org/matrix-android-sdk2.git
+
 To update upstream from origin sdk repo:
 git pull upstream develop --allow-unrelated-histories
 
diff --git a/dependencies.gradle b/dependencies.gradle
index 87b8e3c12ffabfac2fa80507b8587ef76d1cb181..7666a3bf9f8b4cc9929a65f2b0822eaae753cb33 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -9,13 +9,13 @@ ext.versions = [
 
 def gradle = "7.0.4"
 // Ref: https://kotlinlang.org/releases.html
-def kotlin = "1.5.31"
-def kotlinCoroutines = "1.5.2"
+def kotlin = "1.6.0"
+def kotlinCoroutines = "1.6.0"
 def dagger = "2.40.5"
 def retrofit = "2.9.0"
 def arrow = "0.8.2"
 def markwon = "4.6.2"
-def moshi = "1.12.0"
+def moshi = "1.13.0"
 def lifecycle = "2.4.0"
 def flowBinding = "1.2.0"
 def epoxy = "4.6.2"
@@ -58,6 +58,7 @@ ext.libs = [
                 'lifecycleCommon'         : "androidx.lifecycle:lifecycle-common:$lifecycle",
                 'lifecycleLivedata'       : "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle",
                 'lifecycleProcess'        : "androidx.lifecycle:lifecycle-process:$lifecycle",
+                'lifecycleRuntimeKtx'     : "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle",
                 'datastore'               : "androidx.datastore:datastore:1.0.0",
                 'datastorepreferences'    : "androidx.datastore:datastore-preferences:1.0.0",
                 'pagingRuntimeKtx'        : "androidx.paging:paging-runtime-ktx:2.1.2",
@@ -141,4 +142,4 @@ ext.libs = [
                 'timberJunitRule'        : "net.lachlanmckee:timber-junit-rule:1.0.1",
                 'junit'                  : "junit:junit:4.13.2"
         ]
-]
\ No newline at end of file
+]
diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle
index 45883f506d4326389bd26e24fb69856ea533e605..2c3f6a021927bfd37fc7b2cbb34cc5f66ccdf61d 100644
--- a/dependencies_groups.gradle
+++ b/dependencies_groups.gradle
@@ -7,6 +7,7 @@ ext.groups = [
                         'com.github.chrisbanes',
                         'com.github.hyuwah',
                         'com.github.jetradarmobile',
+                        'com.github.MatrixFrog',
                         'com.github.tapadoo',
                         'com.github.vector-im',
                         'com.github.yalantis',
@@ -39,6 +40,7 @@ ext.groups = [
                 regex: [
                 ],
                 group: [
+                        'ch.qos.logback',
                         'com.adevinta.android',
                         'com.airbnb.android',
                         'com.almworks.sqlite4java',
@@ -48,13 +50,19 @@ ext.groups = [
                         'com.beust',
                         'com.davemorrissey.labs',
                         'com.dropbox.core',
+                        'com.soywiz.korlibs.korte',
+                        'com.facebook.fbjni',
                         'com.facebook.fresco',
                         'com.facebook.infer.annotation',
                         'com.facebook.soloader',
                         'com.facebook.stetho',
+                        'com.facebook.yoga',
                         'com.fasterxml',
                         'com.fasterxml.jackson',
                         'com.fasterxml.jackson.core',
+                        'com.fasterxml.jackson.dataformat',
+                        'com.fasterxml.jackson.module',
+                        'com.fasterxml.woodstox',
                         'com.gabrielittner.threetenbp',
                         'com.getkeepsafe.relinker',
                         'com.github.bumptech.glide',
@@ -113,6 +121,7 @@ ext.groups = [
                         'info.picocli',
                         'io.arrow-kt',
                         'io.github.detekt.sarif4k',
+                        'io.github.microutils',
                         'io.github.reactivecircus.flowbinding',
                         'io.grpc',
                         'io.jsonwebtoken',
@@ -152,12 +161,14 @@ ext.groups = [
                         'org.codehaus',
                         'org.codehaus.groovy',
                         'org.codehaus.mojo',
+                        'org.codehaus.woodstox',
                         'org.eclipse.ee4j',
                         'org.ec4j.core',
                         'org.glassfish.jaxb',
                         'org.hamcrest',
                         'org.jacoco',
                         'org.jetbrains',
+                        'org.jetbrains.dokka',
                         'org.jetbrains.intellij.deps',
                         'org.jetbrains.kotlin',
                         'org.jetbrains.kotlinx',
@@ -203,4 +214,3 @@ ext.groups = [
                 ]
         ]
 ]
-
diff --git a/gradle.properties b/gradle.properties
index 917eb10cf076969580a8add91f1db9dfba67eb69..99aa02f1355b7f2cfbb32cf3b73d6b5377896b08 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -26,7 +26,7 @@ vector.httpLogLevel=NONE
 # Ref: https://github.com/vanniktech/gradle-maven-publish-plugin
 GROUP=org.matrix.android
 POM_ARTIFACT_ID=matrix-android-sdk2
-VERSION_NAME=1.4.4
+VERSION_NAME=1.4.13
 
 POM_PACKAGING=aar
 
diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle
index 0519c71addd8bc91fd8c745b0698aa5b8a1f225d..1069cc55f7cf66f8dcc15eef907077831ad4a2cd 100644
--- a/matrix-sdk-android/build.gradle
+++ b/matrix-sdk-android/build.gradle
@@ -37,9 +37,8 @@ android {
         buildConfigField "String", "SDK_VERSION", "\"${project.getProperties().getOrDefault("VERSION_NAME", "0.0.0")}\""
 
         buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
-        resValue "string", "git_sdk_revision", "\"${gitRevision()}\""
-        resValue "string", "git_sdk_revision_unix_date", "\"${gitRevisionUnixDate()}\""
-        resValue "string", "git_sdk_revision_date", "\"${gitRevisionDate()}\""
+        buildConfigField "String", "GIT_SDK_REVISION_UNIX_DATE", "\"${gitRevisionUnixDate()}\""
+        buildConfigField "String", "GIT_SDK_REVISION_DATE", "\"${gitRevisionDate()}\""
 
         defaultConfig {
             consumerProguardFiles 'proguard-rules.pro'
@@ -77,6 +76,10 @@ android {
 
     kotlinOptions {
         jvmTarget = "11"
+        freeCompilerArgs += [
+                // Disabled for now, there are too many errors. Could be handled in another dedicated PR
+                // '-Xexplicit-api=strict', // or warning
+        ]
     }
 
     sourceSets {
@@ -170,7 +173,7 @@ dependencies {
     implementation libs.apache.commonsImaging
 
     // Phone number https://github.com/google/libphonenumber
-    implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.44'
+    implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.46'
 
     testImplementation libs.tests.junit
     testImplementation 'org.robolectric:robolectric:4.7.3'
@@ -178,7 +181,7 @@ dependencies {
     // Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281
     testImplementation libs.mockk.mockk
     testImplementation libs.tests.kluent
-    implementation libs.jetbrains.coroutinesAndroid
+    testImplementation libs.jetbrains.coroutinesTest
     // Plant Timber tree for test
     testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
     // Transitively required for mocking realm as monarchy doesn't expose Rx
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt
index 71796192a8d347b16d4985ae18a38c8de249ce8b..e3ab1a4921a5d943a9cacf4a26b4263bc0e39692 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt
@@ -27,8 +27,12 @@ import org.matrix.android.sdk.api.auth.UIABaseAuth
 import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
 import org.matrix.android.sdk.api.auth.UserPasswordAuth
 import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
+import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
+import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
 import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupAuthData
+import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
 import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
 import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction
 import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
@@ -42,10 +46,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary
 import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
 import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
 import org.matrix.android.sdk.api.util.Optional
-import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
-import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
-import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupAuthData
-import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
 import java.util.UUID
 import kotlin.coroutines.Continuation
 import kotlin.coroutines.resume
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestConstants.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestConstants.kt
index 5c9b79361e891d545fbaaea1774a68f8c6f75145..0f79896b2cd28a6f42bdc7fd19ded9a3a15c1dbf 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestConstants.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestConstants.kt
@@ -23,7 +23,7 @@ object TestConstants {
     const val TESTS_HOME_SERVER_URL = "http://10.0.2.2:8080"
 
     // Time out to use when waiting for server response.
-    private const val AWAIT_TIME_OUT_MILLIS = 30_000
+    private const val AWAIT_TIME_OUT_MILLIS = 60_000
 
     // Time out to use when waiting for server response, when the debugger is connected. 10 minutes
     private const val AWAIT_TIME_OUT_WITH_DEBUGGER_MILLIS = 10 * 60_000
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt
index d0f0e2315245936aea43e2e02872324d3292ab58..dc58339498c720c4d211b5e112d1517eb610d5be 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt
@@ -26,6 +26,7 @@ import org.matrix.android.sdk.internal.di.MatrixModule
 import org.matrix.android.sdk.internal.di.MatrixScope
 import org.matrix.android.sdk.internal.di.NetworkModule
 import org.matrix.android.sdk.internal.raw.RawModule
+import org.matrix.android.sdk.internal.settings.SettingsModule
 import org.matrix.android.sdk.internal.util.system.SystemModule
 
 @Component(modules = [
@@ -34,6 +35,7 @@ import org.matrix.android.sdk.internal.util.system.SystemModule
     NetworkModule::class,
     AuthModule::class,
     RawModule::class,
+    SettingsModule::class,
     SystemModule::class
 ])
 @MatrixScope
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt
index aaf779212b617761cc0a6d2fc935cdc14479b90a..732f4f7dce28b28422d5417b8920e588b7f1c486 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt
@@ -25,10 +25,10 @@ import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
+import org.matrix.android.sdk.api.session.crypto.attachments.toElementToDecrypt
+import org.matrix.android.sdk.api.session.crypto.model.EncryptedFileInfo
+import org.matrix.android.sdk.api.session.crypto.model.EncryptedFileKey
 import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments
-import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
-import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
-import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileKey
 import java.io.ByteArrayOutputStream
 import java.io.InputStream
 
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt
new file mode 100644
index 0000000000000000000000000000000000000000..388468315109a9ea2b2350476a92c5a3327c9836
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt
@@ -0,0 +1,649 @@
+/*
+ * Copyright 2022 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.crypto
+
+import android.util.Log
+import androidx.test.filters.LargeTest
+import kotlinx.coroutines.delay
+import org.amshove.kluent.fail
+import org.amshove.kluent.internal.assertEquals
+import org.junit.Assert
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.junit.runners.MethodSorters
+import org.matrix.android.sdk.InstrumentedTest
+import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.api.session.crypto.MXCryptoError
+import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
+import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult
+import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
+import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
+import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
+import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.room.Room
+import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure
+import org.matrix.android.sdk.api.session.room.model.Membership
+import org.matrix.android.sdk.api.session.room.model.message.MessageContent
+import org.matrix.android.sdk.api.session.room.send.SendState
+import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
+import org.matrix.android.sdk.common.CommonTestHelper
+import org.matrix.android.sdk.common.CryptoTestHelper
+import org.matrix.android.sdk.common.SessionTestParams
+import org.matrix.android.sdk.common.TestConstants
+import org.matrix.android.sdk.common.TestMatrixCallback
+
+@RunWith(JUnit4::class)
+@FixMethodOrder(MethodSorters.JVM)
+@LargeTest
+class E2eeSanityTests : InstrumentedTest {
+
+    private val testHelper = CommonTestHelper(context())
+    private val cryptoTestHelper = CryptoTestHelper(testHelper)
+
+    /**
+     * Simple test that create an e2ee room.
+     * Some new members are added, and a message is sent.
+     * We check that the message is e2e and can be decrypted.
+     *
+     * Additional users join, we check that they can't decrypt history
+     *
+     * Alice sends a new message, then check that the new one can be decrypted
+     */
+    @Test
+    fun testSendingE2EEMessages() {
+        val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
+        val aliceSession = cryptoTestData.firstSession
+        val e2eRoomID = cryptoTestData.roomId
+
+        val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
+
+        // add some more users and invite them
+        val otherAccounts = listOf("benoit", "valere", "ganfra") // , "adam", "manu")
+                .map {
+                    testHelper.createAccount(it, SessionTestParams(true))
+                }
+
+        Log.v("#E2E TEST", "All accounts created")
+        // we want to invite them in the room
+        otherAccounts.forEach {
+            testHelper.runBlockingTest {
+                Log.v("#E2E TEST", "Alice invites ${it.myUserId}")
+                aliceRoomPOV.invite(it.myUserId)
+            }
+        }
+
+        // All user should accept invite
+        otherAccounts.forEach { otherSession ->
+            waitForAndAcceptInviteInRoom(otherSession, e2eRoomID)
+            Log.v("#E2E TEST", "${otherSession.myUserId} joined room $e2eRoomID")
+        }
+
+        // check that alice see them as joined (not really necessary?)
+        ensureMembersHaveJoined(aliceSession, otherAccounts, e2eRoomID)
+
+        Log.v("#E2E TEST", "All users have joined the room")
+        Log.v("#E2E TEST", "Alice is sending the message")
+
+        val text = "This is my message"
+        val sentEventId: String? = sendMessageInRoom(aliceRoomPOV, text)
+        //        val sentEvent = testHelper.sendTextMessage(aliceRoomPOV, "Hello all", 1).first()
+        Assert.assertTrue("Message should be sent", sentEventId != null)
+
+        // All should be able to decrypt
+        otherAccounts.forEach { otherSession ->
+            testHelper.waitWithLatch { latch ->
+                testHelper.retryPeriodicallyWithLatch(latch) {
+                    val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!)
+                    timelineEvent != null &&
+                            timelineEvent.isEncrypted() &&
+                            timelineEvent.root.getClearType() == EventType.MESSAGE
+                }
+            }
+        }
+
+        // Add a new user to the room, and check that he can't decrypt
+        val newAccount = listOf("adam") // , "adam", "manu")
+                .map {
+                    testHelper.createAccount(it, SessionTestParams(true))
+                }
+
+        newAccount.forEach {
+            testHelper.runBlockingTest {
+                Log.v("#E2E TEST", "Alice invites ${it.myUserId}")
+                aliceRoomPOV.invite(it.myUserId)
+            }
+        }
+
+        newAccount.forEach {
+            waitForAndAcceptInviteInRoom(it, e2eRoomID)
+        }
+
+        ensureMembersHaveJoined(aliceSession, newAccount, e2eRoomID)
+
+        // wait a bit
+        testHelper.runBlockingTest {
+            delay(3_000)
+        }
+
+        // check that messages are encrypted (uisi)
+        newAccount.forEach { otherSession ->
+            testHelper.waitWithLatch { latch ->
+                testHelper.retryPeriodicallyWithLatch(latch) {
+                    val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!).also {
+                        Log.v("#E2E TEST", "Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}")
+                    }
+                    timelineEvent != null &&
+                            timelineEvent.root.getClearType() == EventType.ENCRYPTED &&
+                            timelineEvent.root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID
+                }
+            }
+        }
+
+        // Let alice send a new message
+        Log.v("#E2E TEST", "Alice sends a new message")
+
+        val secondMessage = "2 This is my message"
+        val secondSentEventId: String? = sendMessageInRoom(aliceRoomPOV, secondMessage)
+
+        // new members should be able to decrypt it
+        newAccount.forEach { otherSession ->
+            testHelper.waitWithLatch { latch ->
+                testHelper.retryPeriodicallyWithLatch(latch) {
+                    val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(secondSentEventId!!).also {
+                        Log.v("#E2E TEST", "Second Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}")
+                    }
+                    timelineEvent != null &&
+                            timelineEvent.root.getClearType() == EventType.MESSAGE &&
+                            secondMessage == timelineEvent.root.getClearContent().toModel<MessageContent>()?.body
+                }
+            }
+        }
+
+        otherAccounts.forEach {
+            testHelper.signOutAndClose(it)
+        }
+        newAccount.forEach { testHelper.signOutAndClose(it) }
+
+        cryptoTestData.cleanUp(testHelper)
+    }
+
+    /**
+     * Quick test for basic key backup
+     * 1. Create e2e between Alice and Bob
+     * 2. Alice sends 3 messages, using 3 different sessions
+     * 3. Ensure bob can decrypt
+     * 4. Create backup for bob and upload keys
+     *
+     * 5. Sign out alice and bob to ensure no gossiping will happen
+     *
+     * 6. Let bob sign in with a new session
+     * 7. Ensure history is UISI
+     * 8. Import backup
+     * 9. Check that new session can decrypt
+     */
+    @Test
+    fun testBasicBackupImport() {
+        val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
+        val aliceSession = cryptoTestData.firstSession
+        val bobSession = cryptoTestData.secondSession!!
+        val e2eRoomID = cryptoTestData.roomId
+
+        Log.v("#E2E TEST", "Create and start key backup for bob ...")
+        val bobKeysBackupService = bobSession.cryptoService().keysBackupService()
+        val keyBackupPassword = "FooBarBaz"
+        val megolmBackupCreationInfo = testHelper.doSync<MegolmBackupCreationInfo> {
+            bobKeysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it)
+        }
+        val version = testHelper.doSync<KeysVersion> {
+            bobKeysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it)
+        }
+        Log.v("#E2E TEST", "... Key backup started and enabled for bob")
+        // Bob session should now have
+
+        val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
+
+        // let's send a few message to bob
+        val sentEventIds = mutableListOf<String>()
+        val messagesText = listOf("1. Hello", "2. Bob", "3. Good morning")
+        messagesText.forEach { text ->
+            val sentEventId = sendMessageInRoom(aliceRoomPOV, text)!!.also {
+                sentEventIds.add(it)
+            }
+
+            testHelper.waitWithLatch { latch ->
+                testHelper.retryPeriodicallyWithLatch(latch) {
+                    val timelineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
+                    timelineEvent != null &&
+                            timelineEvent.isEncrypted() &&
+                            timelineEvent.root.getClearType() == EventType.MESSAGE
+                }
+            }
+            // we want more so let's discard the session
+            aliceSession.cryptoService().discardOutboundSession(e2eRoomID)
+
+            testHelper.runBlockingTest {
+                delay(1_000)
+            }
+        }
+        Log.v("#E2E TEST", "Bob received all and can decrypt")
+
+        // Let's wait a bit to be sure that bob has backed up the session
+
+        Log.v("#E2E TEST", "Force key backup for Bob...")
+        testHelper.waitWithLatch { latch ->
+            bobKeysBackupService.backupAllGroupSessions(
+                    null,
+                    TestMatrixCallback(latch, true)
+            )
+        }
+        Log.v("#E2E TEST", "... Key backup done for Bob")
+
+        // Now lets logout both alice and bob to ensure that we won't have any gossiping
+
+        val bobUserId = bobSession.myUserId
+        Log.v("#E2E TEST", "Logout alice and bob...")
+        testHelper.signOutAndClose(aliceSession)
+        testHelper.signOutAndClose(bobSession)
+        Log.v("#E2E TEST", "..Logout alice and bob...")
+
+        testHelper.runBlockingTest {
+            delay(1_000)
+        }
+
+        // Create a new session for bob
+        Log.v("#E2E TEST", "Create a new session for Bob")
+        val newBobSession = testHelper.logIntoAccount(bobUserId, SessionTestParams(true))
+
+        // check that bob can't currently decrypt
+        Log.v("#E2E TEST", "check that bob can't currently decrypt")
+        sentEventIds.forEach { sentEventId ->
+            testHelper.waitWithLatch { latch ->
+                testHelper.retryPeriodicallyWithLatch(latch) {
+                    val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)?.also {
+                        Log.v("#E2E TEST", "Event seen by new user ${it.root.getClearType()}|${it.root.mCryptoError}")
+                    }
+                    timelineEvent != null &&
+                            timelineEvent.root.getClearType() == EventType.ENCRYPTED
+                }
+            }
+        }
+        // after initial sync events are not decrypted, so we have to try manually
+        ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
+
+        // Let's now import keys from backup
+
+        newBobSession.cryptoService().keysBackupService().let { keysBackupService ->
+            val keyVersionResult = testHelper.doSync<KeysVersionResult?> {
+                keysBackupService.getVersion(version.version, it)
+            }
+
+            val importedResult = testHelper.doSync<ImportRoomKeysResult> {
+                keysBackupService.restoreKeyBackupWithPassword(keyVersionResult!!,
+                        keyBackupPassword,
+                        null,
+                        null,
+                        null, it)
+            }
+
+            assertEquals(3, importedResult.totalNumberOfKeys)
+        }
+
+        // ensure bob can now decrypt
+        ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
+
+        testHelper.signOutAndClose(newBobSession)
+    }
+
+    /**
+     * Check that a new verified session that was not supposed to get the keys initially will
+     * get them from an older one.
+     */
+    @Test
+    fun testSimpleGossip() {
+        val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
+        val aliceSession = cryptoTestData.firstSession
+        val bobSession = cryptoTestData.secondSession!!
+        val e2eRoomID = cryptoTestData.roomId
+
+        val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
+
+        cryptoTestHelper.initializeCrossSigning(bobSession)
+
+        // let's send a few message to bob
+        val sentEventIds = mutableListOf<String>()
+        val messagesText = listOf("1. Hello", "2. Bob")
+
+        Log.v("#E2E TEST", "Alice sends some messages")
+        messagesText.forEach { text ->
+            val sentEventId = sendMessageInRoom(aliceRoomPOV, text)!!.also {
+                sentEventIds.add(it)
+            }
+
+            testHelper.waitWithLatch { latch ->
+                testHelper.retryPeriodicallyWithLatch(latch) {
+                    val timelineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
+                    timelineEvent != null &&
+                            timelineEvent.isEncrypted() &&
+                            timelineEvent.root.getClearType() == EventType.MESSAGE
+                }
+            }
+        }
+
+        // Ensure bob can decrypt
+        ensureIsDecrypted(sentEventIds, bobSession, e2eRoomID)
+
+        // Let's now add a new bob session
+        // Create a new session for bob
+        Log.v("#E2E TEST", "Create a new session for Bob")
+        val newBobSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true))
+
+        // check that new bob can't currently decrypt
+        Log.v("#E2E TEST", "check that new bob can't currently decrypt")
+
+        ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
+
+        // Try to request
+        sentEventIds.forEach { sentEventId ->
+            val event = newBobSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root
+            newBobSession.cryptoService().requestRoomKeyForEvent(event)
+        }
+
+        // wait a bit
+        testHelper.runBlockingTest {
+            delay(10_000)
+        }
+
+        // Ensure that new bob still can't decrypt (keys must have been withheld)
+        ensureCannotDecrypt(sentEventIds, newBobSession, e2eRoomID, MXCryptoError.ErrorType.KEYS_WITHHELD)
+
+        // Now mark new bob session as verified
+
+        bobSession.cryptoService().verificationService().markedLocallyAsManuallyVerified(newBobSession.myUserId, newBobSession.sessionParams.deviceId!!)
+        newBobSession.cryptoService().verificationService().markedLocallyAsManuallyVerified(bobSession.myUserId, bobSession.sessionParams.deviceId!!)
+
+        // now let new session re-request
+        sentEventIds.forEach { sentEventId ->
+            val event = newBobSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root
+            newBobSession.cryptoService().reRequestRoomKeyForEvent(event)
+        }
+
+        // wait a bit
+        testHelper.runBlockingTest {
+            delay(10_000)
+        }
+
+        ensureCanDecrypt(sentEventIds, newBobSession, e2eRoomID, messagesText)
+
+        cryptoTestData.cleanUp(testHelper)
+        testHelper.signOutAndClose(newBobSession)
+    }
+
+    /**
+     * Test that if a better key is forwarded (lower index, it is then used)
+     */
+    @Test
+    fun testForwardBetterKey() {
+        val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
+        val aliceSession = cryptoTestData.firstSession
+        val bobSessionWithBetterKey = cryptoTestData.secondSession!!
+        val e2eRoomID = cryptoTestData.roomId
+
+        val aliceRoomPOV = aliceSession.getRoom(e2eRoomID)!!
+
+        cryptoTestHelper.initializeCrossSigning(bobSessionWithBetterKey)
+
+        // let's send a few message to bob
+        var firstEventId: String
+        val firstMessage = "1. Hello"
+
+        Log.v("#E2E TEST", "Alice sends some messages")
+        firstMessage.let { text ->
+            firstEventId = sendMessageInRoom(aliceRoomPOV, text)!!
+
+            testHelper.waitWithLatch { latch ->
+                testHelper.retryPeriodicallyWithLatch(latch) {
+                    val timelineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId)
+                    timelineEvent != null &&
+                            timelineEvent.isEncrypted() &&
+                            timelineEvent.root.getClearType() == EventType.MESSAGE
+                }
+            }
+        }
+
+        // Ensure bob can decrypt
+        ensureIsDecrypted(listOf(firstEventId), bobSessionWithBetterKey, e2eRoomID)
+
+        // Let's add a new unverified session from bob
+        val newBobSession = testHelper.logIntoAccount(bobSessionWithBetterKey.myUserId, SessionTestParams(true))
+
+        // check that new bob can't currently decrypt
+        Log.v("#E2E TEST", "check that new bob can't currently decrypt")
+        ensureCannotDecrypt(listOf(firstEventId), newBobSession, e2eRoomID, MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
+
+        // Now let alice send a new message. this time the new bob session will be able to decrypt
+        var secondEventId: String
+        val secondMessage = "2. New Device?"
+
+        Log.v("#E2E TEST", "Alice sends some messages")
+        secondMessage.let { text ->
+            secondEventId = sendMessageInRoom(aliceRoomPOV, text)!!
+
+            testHelper.waitWithLatch { latch ->
+                testHelper.retryPeriodicallyWithLatch(latch) {
+                    val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId)
+                    timelineEvent != null &&
+                            timelineEvent.isEncrypted() &&
+                            timelineEvent.root.getClearType() == EventType.MESSAGE
+                }
+            }
+        }
+
+        // check that both messages have same sessionId (it's just that we don't have index 0)
+        val firstEventNewBobPov = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId)
+        val secondEventNewBobPov = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId)
+
+        val firstSessionId = firstEventNewBobPov!!.root.content.toModel<EncryptedEventContent>()!!.sessionId!!
+        val secondSessionId = secondEventNewBobPov!!.root.content.toModel<EncryptedEventContent>()!!.sessionId!!
+
+        Assert.assertTrue("Should be the same session id", firstSessionId == secondSessionId)
+
+        // Confirm we can decrypt one but not the other
+        testHelper.runBlockingTest {
+            try {
+                newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
+                fail("Should not be able to decrypt event")
+            } catch (error: MXCryptoError) {
+                val errorType = (error as? MXCryptoError.Base)?.errorType
+                assertEquals(MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX, errorType)
+            }
+        }
+
+        testHelper.runBlockingTest {
+            try {
+                newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "")
+            } catch (error: MXCryptoError) {
+                fail("Should be able to decrypt event")
+            }
+        }
+
+        // Now let's verify bobs session, and re-request keys
+        bobSessionWithBetterKey.cryptoService()
+                .verificationService()
+                .markedLocallyAsManuallyVerified(newBobSession.myUserId, newBobSession.sessionParams.deviceId!!)
+
+        newBobSession.cryptoService()
+                .verificationService()
+                .markedLocallyAsManuallyVerified(bobSessionWithBetterKey.myUserId, bobSessionWithBetterKey.sessionParams.deviceId!!)
+
+        // now let new session request
+        newBobSession.cryptoService().requestRoomKeyForEvent(firstEventNewBobPov.root)
+
+        // wait a bit
+        testHelper.runBlockingTest {
+            delay(10_000)
+        }
+
+        // old session should have shared the key at earliest known index now
+        // we should be able to decrypt both
+        testHelper.runBlockingTest {
+            try {
+                newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
+            } catch (error: MXCryptoError) {
+                fail("Should be able to decrypt first event now $error")
+            }
+        }
+        testHelper.runBlockingTest {
+            try {
+                newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "")
+            } catch (error: MXCryptoError) {
+                fail("Should be able to decrypt event $error")
+            }
+        }
+
+        cryptoTestData.cleanUp(testHelper)
+        testHelper.signOutAndClose(newBobSession)
+    }
+
+    private fun sendMessageInRoom(aliceRoomPOV: Room, text: String): String? {
+        aliceRoomPOV.sendTextMessage(text)
+        var sentEventId: String? = null
+        testHelper.waitWithLatch(4 * TestConstants.timeOutMillis) { latch ->
+            val timeline = aliceRoomPOV.createTimeline(null, TimelineSettings(60))
+            timeline.start()
+
+            testHelper.retryPeriodicallyWithLatch(latch) {
+                val decryptedMsg = timeline.getSnapshot()
+                        .filter { it.root.getClearType() == EventType.MESSAGE }
+                        .also { list ->
+                            val message = list.joinToString(",", "[", "]") { "${it.root.type}|${it.root.sendState}" }
+                            Log.v("#E2E TEST", "Timeline snapshot is $message")
+                        }
+                        .filter { it.root.sendState == SendState.SYNCED }
+                        .firstOrNull { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(text) == true }
+                sentEventId = decryptedMsg?.eventId
+                decryptedMsg != null
+            }
+
+            timeline.dispose()
+        }
+        return sentEventId
+    }
+
+    private fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String) {
+        testHelper.waitWithLatch { latch ->
+            testHelper.retryPeriodicallyWithLatch(latch) {
+                otherAccounts.map {
+                    aliceSession.getRoomMember(it.myUserId, e2eRoomID)?.membership
+                }.all {
+                    it == Membership.JOIN
+                }
+            }
+        }
+    }
+
+    private fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String) {
+        testHelper.waitWithLatch { latch ->
+            testHelper.retryPeriodicallyWithLatch(latch) {
+                val roomSummary = otherSession.getRoomSummary(e2eRoomID)
+                (roomSummary != null && roomSummary.membership == Membership.INVITE).also {
+                    if (it) {
+                        Log.v("#E2E TEST", "${otherSession.myUserId} can see the invite from alice")
+                    }
+                }
+            }
+        }
+
+        testHelper.runBlockingTest(60_000) {
+            Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID")
+            try {
+                otherSession.joinRoom(e2eRoomID)
+            } catch (ex: JoinRoomFailure.JoinedWithTimeout) {
+                // it's ok we will wait after
+            }
+        }
+
+        Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...")
+        testHelper.waitWithLatch {
+            testHelper.retryPeriodicallyWithLatch(it) {
+                val roomSummary = otherSession.getRoomSummary(e2eRoomID)
+                roomSummary != null && roomSummary.membership == Membership.JOIN
+            }
+        }
+    }
+
+    private fun ensureCanDecrypt(sentEventIds: MutableList<String>, session: Session, e2eRoomID: String, messagesText: List<String>) {
+        sentEventIds.forEachIndexed { index, sentEventId ->
+            testHelper.waitWithLatch { latch ->
+                testHelper.retryPeriodicallyWithLatch(latch) {
+                    val event = session.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root
+                    testHelper.runBlockingTest {
+                        try {
+                            session.cryptoService().decryptEvent(event, "").let { result ->
+                                event.mxDecryptionResult = OlmDecryptionResult(
+                                        payload = result.clearEvent,
+                                        senderKey = result.senderCurve25519Key,
+                                        keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
+                                        forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
+                                )
+                            }
+                        } catch (error: MXCryptoError) {
+                            // nop
+                        }
+                    }
+                    event.getClearType() == EventType.MESSAGE &&
+                            messagesText[index] == event.getClearContent()?.toModel<MessageContent>()?.body
+                }
+            }
+        }
+    }
+
+    private fun ensureIsDecrypted(sentEventIds: List<String>, session: Session, e2eRoomID: String) {
+        testHelper.waitWithLatch { latch ->
+            sentEventIds.forEach { sentEventId ->
+                testHelper.retryPeriodicallyWithLatch(latch) {
+                    val timelineEvent = session.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
+                    timelineEvent != null &&
+                            timelineEvent.isEncrypted() &&
+                            timelineEvent.root.getClearType() == EventType.MESSAGE
+                }
+            }
+        }
+    }
+
+    private fun ensureCannotDecrypt(sentEventIds: List<String>, newBobSession: Session, e2eRoomID: String, expectedError: MXCryptoError.ErrorType?) {
+        sentEventIds.forEach { sentEventId ->
+            val event = newBobSession.getRoom(e2eRoomID)!!.getTimelineEvent(sentEventId)!!.root
+            testHelper.runBlockingTest {
+                try {
+                    newBobSession.cryptoService().decryptEvent(event, "")
+                    fail("Should not be able to decrypt event")
+                } catch (error: MXCryptoError) {
+                    val errorType = (error as? MXCryptoError.Base)?.errorType
+                    if (expectedError == null) {
+                        Assert.assertNotNull(errorType)
+                    } else {
+                        assertEquals(expectedError, errorType, "Message expected to be UISI")
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt
index a7a81bacf50bd5d4775288d3b1e42130ba944625..aa9f09314fdd4a6a13c23dd0731484ff8213173c 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt
@@ -21,17 +21,16 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertNotNull
 import org.junit.FixMethodOrder
-import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
 import org.matrix.android.sdk.InstrumentedTest
 import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
+import org.matrix.android.sdk.api.session.events.model.content.RoomKeyContent
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.common.CommonTestHelper
 import org.matrix.android.sdk.common.CryptoTestHelper
-import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
-import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyContent
 
 @RunWith(AndroidJUnit4::class)
 @FixMethodOrder(MethodSorters.JVM)
@@ -41,7 +40,6 @@ class PreShareKeysTest : InstrumentedTest {
     private val cryptoTestHelper = CryptoTestHelper(testHelper)
 
     @Test
-    @Ignore("This test will be ignored until it is fixed")
     fun ensure_outbound_session_happy_path() {
         val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
         val e2eRoomID = testData.roomId
@@ -92,7 +90,7 @@ class PreShareKeysTest : InstrumentedTest {
         // Just send a real message as test
         val sentEvent = testHelper.sendTextMessage(aliceSession.getRoom(e2eRoomID)!!, "Allo", 1).first()
 
-        assertEquals(megolmSessionId, sentEvent.root.content.toModel<EncryptedEventContent>()?.sessionId, "Unexpected megolm session")
+        assertEquals("Unexpected megolm session", megolmSessionId, sentEvent.root.content.toModel<EncryptedEventContent>()?.sessionId)
         testHelper.waitWithLatch { latch ->
             testHelper.retryPeriodicallyWithLatch(latch) {
                 bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEvent.eventId)?.root?.getClearType() == EventType.MESSAGE
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt
index 0a8ce6768078f5d9160ec060853821990abbacf3..83464305307c15eab276deed8b1a77960b5e21ac 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt
@@ -21,7 +21,6 @@ import org.amshove.kluent.shouldBe
 import org.junit.Assert
 import org.junit.Before
 import org.junit.FixMethodOrder
-import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -33,6 +32,7 @@ import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
 import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
 import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.room.timeline.Timeline
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
@@ -41,7 +41,6 @@ import org.matrix.android.sdk.common.CommonTestHelper
 import org.matrix.android.sdk.common.CryptoTestHelper
 import org.matrix.android.sdk.common.TestConstants
 import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
-import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
 import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm
 import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm
 import org.matrix.olm.OlmSession
@@ -85,7 +84,6 @@ class UnwedgingTest : InstrumentedTest {
      * -> This is automatically fixed after SDKs restarted the olm session
      */
     @Test
-    @Ignore("This test will be ignored until it is fixed")
     fun testUnwedging() {
         val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
 
@@ -94,9 +92,7 @@ class UnwedgingTest : InstrumentedTest {
         val bobSession = cryptoTestData.secondSession!!
 
         val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting
-
-        // bobSession.cryptoService().setWarnOnUnknownDevices(false)
-        // aliceSession.cryptoService().setWarnOnUnknownDevices(false)
+        val olmDevice = (aliceSession.cryptoService() as DefaultCryptoService).olmDeviceForTest
 
         val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!!
         val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
@@ -175,6 +171,7 @@ class UnwedgingTest : InstrumentedTest {
         Timber.i("## CRYPTO | testUnwedging: wedge the session now. Set crypto state like after the first message")
 
         aliceCryptoStore.storeSession(OlmSessionWrapper(deserializeFromRealm<OlmSession>(oldSession)!!), bobSession.cryptoService().getMyDevice().identityKey()!!)
+        olmDevice.clearOlmSessionCache()
         Thread.sleep(6_000)
 
         // Force new session, and key share
@@ -227,8 +224,10 @@ class UnwedgingTest : InstrumentedTest {
         testHelper.waitWithLatch {
             testHelper.retryPeriodicallyWithLatch(it) {
                 // we should get back the key and be able to decrypt
-                val result = tryOrNull {
-                    bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "")
+                val result = testHelper.runBlockingTest {
+                    tryOrNull {
+                        bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "")
+                    }
                 }
                 Timber.i("## CRYPTO | testUnwedging: decrypt result  ${result?.clearEvent}")
                 result != null
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/ExtensionsKtTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/ExtensionsKtTest.kt
index 9fa7458ea77b131b9f3c3322f94af5a64222af74..936dc6a87206731f2c70b47e91fa06f3d10cd9f6 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/ExtensionsKtTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/ExtensionsKtTest.kt
@@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.crypto.crosssigning
 import org.amshove.kluent.shouldBeNull
 import org.amshove.kluent.shouldBeTrue
 import org.junit.Test
+import org.matrix.android.sdk.api.util.fromBase64
 
 @Suppress("SpellCheckingInspection")
 class ExtensionsKtTest {
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt
index a6e8f94c910e30eb1c5137dac9c5b554e08937e2..dc65cec187b088732b259e56b5f188a24bf7c87b 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt
@@ -34,12 +34,14 @@ import org.matrix.android.sdk.api.auth.UIABaseAuth
 import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
 import org.matrix.android.sdk.api.auth.UserPasswordAuth
 import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
+import org.matrix.android.sdk.api.session.crypto.crosssigning.isCrossSignedVerified
+import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified
+import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
+import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
 import org.matrix.android.sdk.common.CommonTestHelper
 import org.matrix.android.sdk.common.CryptoTestHelper
 import org.matrix.android.sdk.common.SessionTestParams
 import org.matrix.android.sdk.common.TestConstants
-import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
-import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
 import kotlin.coroutines.Continuation
 import kotlin.coroutines.resume
 
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt
index 060201d62452ad858d8a8c6bdd8340445462f47c..84c9487e030a27eeeeae5d325ea3b8a984d067bb 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt
@@ -24,7 +24,9 @@ import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
 import org.matrix.android.sdk.InstrumentedTest
+import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
 import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.content.EncryptionEventContent
 import org.matrix.android.sdk.api.session.events.model.toContent
 import org.matrix.android.sdk.api.session.room.Room
 import org.matrix.android.sdk.api.session.room.send.SendState
@@ -33,8 +35,6 @@ 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.common.CommonTestHelper
 import org.matrix.android.sdk.common.CryptoTestHelper
-import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
-import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent
 import java.util.concurrent.CountDownLatch
 
 @RunWith(AndroidJUnit4::class)
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt
index 82aee454eb361657f26b6f68748290b7272cfca4..5e271c6910180bc4a390d9797034499d0e9bd0f5 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt
@@ -35,12 +35,20 @@ import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
 import org.matrix.android.sdk.api.auth.UserPasswordAuth
 import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
 import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
+import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
+import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
+import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
+import org.matrix.android.sdk.api.session.crypto.model.GossipingRequestState
+import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
+import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState
 import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
 import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
 import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
 import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
 import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
 import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
+import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
 import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
@@ -48,14 +56,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent
 import org.matrix.android.sdk.common.CommonTestHelper
 import org.matrix.android.sdk.common.SessionTestParams
 import org.matrix.android.sdk.common.TestConstants
-import org.matrix.android.sdk.internal.crypto.GossipingRequestState
-import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestState
-import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel
-import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
-import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion
-import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
-import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
-import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
 import kotlin.coroutines.Continuation
 import kotlin.coroutines.resume
 
@@ -97,7 +97,9 @@ class KeyShareTests : InstrumentedTest {
         assert(receivedEvent!!.isEncrypted())
 
         try {
-            aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
+            commonTestHelper.runBlockingTest {
+                aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
+            }
             fail("should fail")
         } catch (failure: Throwable) {
         }
@@ -110,7 +112,7 @@ class KeyShareTests : InstrumentedTest {
 
         var outGoingRequestId: String? = null
 
-        commonTestHelper.waitWithLatch {  latch ->
+        commonTestHelper.waitWithLatch { latch ->
             commonTestHelper.retryPeriodicallyWithLatch(latch) {
                 aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
                         .filter { req ->
@@ -152,7 +154,9 @@ class KeyShareTests : InstrumentedTest {
         }
 
         try {
-            aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
+            commonTestHelper.runBlockingTest {
+                aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
+            }
             fail("should fail")
         } catch (failure: Throwable) {
         }
@@ -189,7 +193,9 @@ class KeyShareTests : InstrumentedTest {
         }
 
         try {
-            aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
+            commonTestHelper.runBlockingTest {
+                aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
+            }
         } catch (failure: Throwable) {
             fail("should have been able to decrypt")
         }
@@ -384,7 +390,11 @@ class KeyShareTests : InstrumentedTest {
         val roomRoomBobPov = aliceSession.getRoom(roomId)
         val beforeJoin = roomRoomBobPov!!.getTimelineEvent(secondEventId)
 
-        var dRes = tryOrNull { bobSession.cryptoService().decryptEvent(beforeJoin!!.root, "") }
+        var dRes = tryOrNull {
+            commonTestHelper.runBlockingTest {
+                bobSession.cryptoService().decryptEvent(beforeJoin!!.root, "")
+            }
+        }
 
         assert(dRes == null)
 
@@ -395,7 +405,11 @@ class KeyShareTests : InstrumentedTest {
         Thread.sleep(3_000)
 
         // With the bug the first session would have improperly reshare that key :/
-        dRes = tryOrNull { bobSession.cryptoService().decryptEvent(beforeJoin.root, "") }
+        dRes = tryOrNull {
+            commonTestHelper.runBlockingTest {
+                bobSession.cryptoService().decryptEvent(beforeJoin.root, "")
+            }
+        }
         Log.d("#TEST", "KS: sgould not decrypt that ${beforeJoin.root.getClearContent().toModel<MessageContent>()?.body}")
         assert(dRes?.clearEvent == null)
     }
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt
index 9fda21763a023c94f0d01f55cad1956ef9599d4a..55bb03278c77b8bf8de0b11343f6761a7682c36c 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt
@@ -30,14 +30,14 @@ import org.matrix.android.sdk.api.NoOpMatrixCallback
 import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
 import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
+import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.common.CommonTestHelper
 import org.matrix.android.sdk.common.CryptoTestHelper
 import org.matrix.android.sdk.common.MockOkHttpInterceptor
 import org.matrix.android.sdk.common.SessionTestParams
 import org.matrix.android.sdk.common.TestConstants
-import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
-import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
 
 @RunWith(AndroidJUnit4::class)
 @FixMethodOrder(MethodSorters.JVM)
@@ -93,7 +93,9 @@ class WithHeldTests : InstrumentedTest {
         // Bob should not be able to decrypt because the keys is withheld
         try {
             // .. might need to wait a bit for stability?
-            bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
+            testHelper.runBlockingTest {
+                bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
+            }
             Assert.fail("This session should not be able to decrypt")
         } catch (failure: Throwable) {
             val type = (failure as MXCryptoError.Base).errorType
@@ -118,7 +120,9 @@ class WithHeldTests : InstrumentedTest {
         // Previous message should still be undecryptable (partially withheld session)
         try {
             // .. might need to wait a bit for stability?
-            bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
+            testHelper.runBlockingTest {
+                bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
+            }
             Assert.fail("This session should not be able to decrypt")
         } catch (failure: Throwable) {
             val type = (failure as MXCryptoError.Base).errorType
@@ -134,7 +138,7 @@ class WithHeldTests : InstrumentedTest {
 
     @Test
     @Ignore("This test will be ignored until it is fixed")
-    fun  test_WithHeldNoOlm() {
+    fun test_WithHeldNoOlm() {
         val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
         val aliceSession = testData.firstSession
         val bobSession = testData.secondSession!!
@@ -165,7 +169,9 @@ class WithHeldTests : InstrumentedTest {
         val eventBobPOV = bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId)
         try {
             // .. might need to wait a bit for stability?
-            bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "")
+            testHelper.runBlockingTest {
+                bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "")
+            }
             Assert.fail("This session should not be able to decrypt")
         } catch (failure: Throwable) {
             val type = (failure as MXCryptoError.Base).errorType
@@ -233,7 +239,11 @@ class WithHeldTests : InstrumentedTest {
             testHelper.retryPeriodicallyWithLatch(latch) {
                 val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(eventId)?.also {
                     // try to decrypt and force key request
-                    tryOrNull { bobSecondSession.cryptoService().decryptEvent(it.root, "") }
+                    tryOrNull {
+                        testHelper.runBlockingTest {
+                            bobSecondSession.cryptoService().decryptEvent(it.root, "")
+                        }
+                    }
                 }
                 sessionId = timeLineEvent?.root?.content?.toModel<EncryptedEventContent>()?.sessionId
                 timeLineEvent != null
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt
index 864f3c12e447fb335d408da92ea5bc57657a9150..45fdb9e1e30f07b4e5b512243b850ede447d284a 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt
@@ -24,10 +24,12 @@ import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrappe
 /**
  * Data class to store result of [KeysBackupTestHelper.createKeysBackupScenarioWithPassword]
  */
-data class KeysBackupScenarioData(val cryptoTestData: CryptoTestData,
-                                  val aliceKeys: List<OlmInboundGroupSessionWrapper2>,
-                                  val prepareKeysBackupDataResult: PrepareKeysBackupDataResult,
-                                  val aliceSession2: Session) {
+internal data class KeysBackupScenarioData(
+        val cryptoTestData: CryptoTestData,
+        val aliceKeys: List<OlmInboundGroupSessionWrapper2>,
+        val prepareKeysBackupDataResult: PrepareKeysBackupDataResult,
+        val aliceSession2: Session
+) {
     fun cleanUp(testHelper: CommonTestHelper) {
         cryptoTestData.cleanUp(testHelper)
         testHelper.signOutAndClose(aliceSession2)
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt
index 4c9456621947a30cc94bfc7567de6030bf95538e..063c0c0ca397310a442a9eefb0d93a23893ae6a8 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt
@@ -29,21 +29,22 @@ import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
 import org.matrix.android.sdk.InstrumentedTest
+import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
 import org.matrix.android.sdk.api.listeners.ProgressListener
 import org.matrix.android.sdk.api.listeners.StepProgressListener
+import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
+import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupLastVersionResult
 import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
 import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener
+import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupVersionTrust
+import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
+import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
+import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult
+import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
 import org.matrix.android.sdk.common.CommonTestHelper
 import org.matrix.android.sdk.common.CryptoTestHelper
 import org.matrix.android.sdk.common.TestConstants
 import org.matrix.android.sdk.common.TestMatrixCallback
-import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
-import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel
-import org.matrix.android.sdk.internal.crypto.keysbackup.model.KeysBackupVersionTrust
-import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
-import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion
-import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult
-import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult
 import java.util.Collections
 import java.util.concurrent.CountDownLatch
 
@@ -403,9 +404,9 @@ class KeysBackupTest : InstrumentedTest {
         assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled)
 
         // - Retrieve the last version from the server
-        val keysVersionResult = testHelper.doSync<KeysVersionResult?> {
+        val keysVersionResult = testHelper.doSync<KeysBackupLastVersionResult> {
             testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it)
-        }
+        }.toKeysVersionResult()
 
         // - It must be the same
         assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version)
@@ -463,9 +464,9 @@ class KeysBackupTest : InstrumentedTest {
         assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled)
 
         // - Retrieve the last version from the server
-        val keysVersionResult = testHelper.doSync<KeysVersionResult?> {
+        val keysVersionResult = testHelper.doSync<KeysBackupLastVersionResult> {
             testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it)
-        }
+        }.toKeysVersionResult()
 
         // - It must be the same
         assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version)
@@ -565,9 +566,9 @@ class KeysBackupTest : InstrumentedTest {
         assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled)
 
         // - Retrieve the last version from the server
-        val keysVersionResult = testHelper.doSync<KeysVersionResult?> {
+        val keysVersionResult = testHelper.doSync<KeysBackupLastVersionResult> {
             testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it)
-        }
+        }.toKeysVersionResult()
 
         // - It must be the same
         assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version)
@@ -835,9 +836,9 @@ class KeysBackupTest : InstrumentedTest {
         keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup)
 
         // Get key backup version from the homeserver
-        val keysVersionResult = testHelper.doSync<KeysVersionResult?> {
+        val keysVersionResult = testHelper.doSync<KeysBackupLastVersionResult> {
             keysBackup.getCurrentVersion(it)
-        }
+        }.toKeysVersionResult()
 
         // - Check the returned KeyBackupVersion is trusted
         val keysBackupVersionTrust = testHelper.doSync<KeysBackupVersionTrust> {
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt
index 592b798bccee0f132bd43ee016715f8e874b0026..ac83cb88821dbab69503276b023d547468d41079 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt
@@ -22,16 +22,16 @@ import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService
 import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
 import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener
+import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
+import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
 import org.matrix.android.sdk.common.CommonTestHelper
 import org.matrix.android.sdk.common.CryptoTestHelper
 import org.matrix.android.sdk.common.assertDictEquals
 import org.matrix.android.sdk.common.assertListEquals
 import org.matrix.android.sdk.internal.crypto.MegolmSessionData
-import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
-import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion
 import java.util.concurrent.CountDownLatch
 
-class KeysBackupTestHelper(
+internal class KeysBackupTestHelper(
         private val testHelper: CommonTestHelper,
         private val cryptoTestHelper: CryptoTestHelper) {
 
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/PrepareKeysBackupDataResult.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/PrepareKeysBackupDataResult.kt
index 6aefe98f86743bbf7bb5de5ea81d527349d3c5c1..31bd3c9cce5d8670f3db0ad745034a9e2493c263 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/PrepareKeysBackupDataResult.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/PrepareKeysBackupDataResult.kt
@@ -16,7 +16,7 @@
 
 package org.matrix.android.sdk.internal.crypto.keysbackup
 
-import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
+import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
 
 data class PrepareKeysBackupDataResult(val megolmBackupCreationInfo: MegolmBackupCreationInfo,
                                        val version: String)
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt
index 67f17727b128e02bb8d67e3558de9899d711ddce..d6baa4b1e0d42430ec79bb91c295b6284bf4d576 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt
@@ -27,6 +27,7 @@ import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
 import org.matrix.android.sdk.InstrumentedTest
+import org.matrix.android.sdk.api.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2
 import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
 import org.matrix.android.sdk.api.session.securestorage.EncryptedSecretContent
@@ -37,11 +38,10 @@ import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageError
 import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
 import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo
 import org.matrix.android.sdk.api.util.Optional
+import org.matrix.android.sdk.api.util.toBase64NoPadding
 import org.matrix.android.sdk.common.CommonTestHelper
 import org.matrix.android.sdk.common.SessionTestParams
 import org.matrix.android.sdk.common.TestConstants
-import org.matrix.android.sdk.internal.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2
-import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding
 import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService
 
 @RunWith(AndroidJUnit4::class)
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt
index 8cd725504d33bfb0e0bdf621654c3440edc9e544..14e659e2b606bfa8b434b078206ff95db59d2347 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt
@@ -31,6 +31,8 @@ import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
 import org.matrix.android.sdk.InstrumentedTest
 import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
+import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
 import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
 import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
 import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction
@@ -44,8 +46,6 @@ import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.common.CommonTestHelper
 import org.matrix.android.sdk.common.CryptoTestHelper
-import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
-import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
 import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationCancel
 import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationStart
 import org.matrix.android.sdk.internal.crypto.model.rest.toValue
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt
index 35c5a4dab9cf72b0b99f6c3a88978f11b8bfc1f5..2c96568102904016eafb7439c897c9cf87011eee 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt
@@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.crypto.verification.qrcode
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import org.amshove.kluent.shouldBe
 import org.junit.FixMethodOrder
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -39,6 +40,7 @@ import kotlin.coroutines.resume
 
 @RunWith(AndroidJUnit4::class)
 @FixMethodOrder(MethodSorters.JVM)
+@Ignore("This test is flaky ; see issue #5449")
 class VerificationTest : InstrumentedTest {
 
     data class ExpectedResult(
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt
index 9856ee777068bc07c84dda507f299eef25f13845..ef98ed22c7112328b79ca1512ef0f93170dfc4eb 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt
@@ -27,6 +27,7 @@ import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
 import org.matrix.android.sdk.InstrumentedTest
 import org.matrix.android.sdk.api.MatrixConfiguration
+import org.matrix.android.sdk.api.util.TextContent
 import org.matrix.android.sdk.common.TestRoomDisplayNameFallbackProvider
 import org.matrix.android.sdk.internal.session.displayname.DisplayNameResolver
 import org.matrix.android.sdk.internal.session.room.send.pills.MentionLinkSpecComparator
@@ -60,7 +61,9 @@ class MarkdownParserTest : InstrumentedTest {
                                     applicationFlavor = "TestFlavor",
                                     roomDisplayNameFallbackProvider = TestRoomDisplayNameFallbackProvider()
                             )
-                    ))
+                    ),
+                    TestPermalinkService()
+            )
     )
 
     @Test
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/TestPermalinkService.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/TestPermalinkService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2f9a5e0a73a656f0f00dfdf048fafd85b5e252d2
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/TestPermalinkService.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2022 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.room.send
+
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.permalinks.PermalinkService
+import org.matrix.android.sdk.api.session.permalinks.PermalinkService.SpanTemplateType.HTML
+import org.matrix.android.sdk.api.session.permalinks.PermalinkService.SpanTemplateType.MARKDOWN
+
+class TestPermalinkService : PermalinkService {
+    override fun createPermalink(event: Event, forceMatrixTo: Boolean): String? {
+        return null
+    }
+
+    override fun createPermalink(id: String, forceMatrixTo: Boolean): String? {
+        return ""
+    }
+
+    override fun createPermalink(roomId: String, eventId: String, forceMatrixTo: Boolean): String {
+        return ""
+    }
+
+    override fun createRoomPermalink(roomId: String, viaServers: List<String>?, forceMatrixTo: Boolean): String? {
+        return null
+    }
+
+    override fun getLinkedId(url: String): String? {
+        return null
+    }
+
+    override fun createMentionSpanTemplate(type: PermalinkService.SpanTemplateType, forceMatrixTo: Boolean): String {
+        return when (type) {
+            HTML     -> "<a href=\"https://matrix.to/#/%1\$s\">%2\$s</a>"
+            MARKDOWN -> "[%2\$s](https://matrix.to/#/%1\$s)"
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtilsTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtilsTest.kt
index 7ee6caed0da5c0859b5ec27d3736e9096d69feba..6bcd12742b3d630a577f99355aca1e3064a74a60 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtilsTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtilsTest.kt
@@ -24,8 +24,8 @@ import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
 import org.matrix.android.sdk.InstrumentedTest
-import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64
-import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding
+import org.matrix.android.sdk.api.util.fromBase64
+import org.matrix.android.sdk.api.util.toBase64NoPadding
 import java.io.ByteArrayOutputStream
 import java.util.UUID
 
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt
index 69ae57e644f6d5b79559ee7f97e50135b9c00c96..5c011c8b2fb195711e3afe55bc2f31b389912573 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/ChunkEntityTest.kt
@@ -62,7 +62,11 @@ internal class ChunkEntityTest : InstrumentedTest {
             val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED, System.currentTimeMillis()).let {
                 realm.copyToRealm(it)
             }
-            chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap())
+            chunk.addTimelineEvent(
+                    roomId = ROOM_ID,
+                    eventEntity = fakeEvent,
+                    direction = PaginationDirection.FORWARDS,
+                    roomMemberContentsByUser = emptyMap())
             chunk.timelineEvents.size shouldBeEqualTo 1
         }
     }
@@ -74,8 +78,16 @@ internal class ChunkEntityTest : InstrumentedTest {
             val fakeEvent = createFakeMessageEvent().toEntity(ROOM_ID, SendState.SYNCED, System.currentTimeMillis()).let {
                 realm.copyToRealm(it)
             }
-            chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap())
-            chunk.addTimelineEvent(ROOM_ID, fakeEvent, PaginationDirection.FORWARDS, emptyMap())
+            chunk.addTimelineEvent(
+                    roomId = ROOM_ID,
+                    eventEntity = fakeEvent,
+                    direction = PaginationDirection.FORWARDS,
+                    roomMemberContentsByUser = emptyMap())
+            chunk.addTimelineEvent(
+                    roomId = ROOM_ID,
+                    eventEntity = fakeEvent,
+                    direction = PaginationDirection.FORWARDS,
+                    roomMemberContentsByUser = emptyMap())
             chunk.timelineEvents.size shouldBeEqualTo 1
         }
     }
@@ -144,7 +156,11 @@ internal class ChunkEntityTest : InstrumentedTest {
             val fakeEvent = event.toEntity(roomId, SendState.SYNCED, System.currentTimeMillis()).let {
                 realm.copyToRealm(it)
             }
-            addTimelineEvent(roomId, fakeEvent, direction, emptyMap())
+            addTimelineEvent(
+                    roomId = roomId,
+                    eventEntity = fakeEvent,
+                    direction = direction,
+                    roomMemberContentsByUser = emptyMap())
         }
     }
 
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6792d6ddfd039024870c2434b36cd2efb32b3399
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/PollAggregationTest.kt
@@ -0,0 +1,233 @@
+/*
+ * Copyright 2020 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.session.room.timeline
+
+import org.amshove.kluent.fail
+import org.amshove.kluent.shouldBe
+import org.amshove.kluent.shouldBeEqualTo
+import org.amshove.kluent.shouldBeGreaterThan
+import org.amshove.kluent.shouldContain
+import org.amshove.kluent.shouldContainAll
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.junit.runners.MethodSorters
+import org.matrix.android.sdk.InstrumentedTest
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.room.model.PollResponseAggregatedSummary
+import org.matrix.android.sdk.api.session.room.model.PollSummaryContent
+import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
+import org.matrix.android.sdk.api.session.room.model.message.PollType
+import org.matrix.android.sdk.api.session.room.timeline.Timeline
+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.common.CommonTestHelper
+import org.matrix.android.sdk.common.CryptoTestHelper
+import java.util.concurrent.CountDownLatch
+
+@RunWith(JUnit4::class)
+@FixMethodOrder(MethodSorters.JVM)
+class PollAggregationTest : InstrumentedTest {
+
+    @Test
+    fun testAllPollUseCases() {
+        val commonTestHelper = CommonTestHelper(context())
+        val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
+        val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
+
+        val aliceSession = cryptoTestData.firstSession
+        val aliceRoomId = cryptoTestData.roomId
+        val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
+
+        val roomFromBobPOV = cryptoTestData.secondSession!!.getRoom(cryptoTestData.roomId)!!
+        // Bob creates a poll
+        roomFromBobPOV.sendPoll(PollType.DISCLOSED, pollQuestion, pollOptions)
+
+        aliceSession.startSync(true)
+        val aliceTimeline = roomFromAlicePOV.createTimeline(null, TimelineSettings(30))
+        aliceTimeline.start()
+
+        val TOTAL_TEST_COUNT = 7
+        val lock = CountDownLatch(TOTAL_TEST_COUNT)
+
+        val aliceEventsListener = object : Timeline.Listener {
+            override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
+                snapshot.firstOrNull { it.root.getClearType() in EventType.POLL_START }?.let { pollEvent ->
+                    val pollEventId = pollEvent.eventId
+                    val pollContent = pollEvent.root.content?.toModel<MessagePollContent>()
+                    val pollSummary = pollEvent.annotations?.pollResponseSummary
+
+                    if (pollContent == null) {
+                        fail("Poll content is null")
+                        return
+                    }
+
+                    when (lock.count.toInt()) {
+                        TOTAL_TEST_COUNT     -> {
+                            // Poll has just been created.
+                            testInitialPollConditions(pollContent, pollSummary)
+                            lock.countDown()
+                            roomFromBobPOV.voteToPoll(pollEventId, pollContent.getBestPollCreationInfo()?.answers?.firstOrNull()?.id ?: "")
+                        }
+                        TOTAL_TEST_COUNT - 1 -> {
+                            // Bob: Option 1
+                            testBobVotesOption1(pollContent, pollSummary)
+                            lock.countDown()
+                            roomFromBobPOV.voteToPoll(pollEventId, pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id ?: "")
+                        }
+                        TOTAL_TEST_COUNT - 2 -> {
+                            // Bob: Option 2
+                            testBobChangesVoteToOption2(pollContent, pollSummary)
+                            lock.countDown()
+                            roomFromAlicePOV.voteToPoll(pollEventId, pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id ?: "")
+                        }
+                        TOTAL_TEST_COUNT - 3 -> {
+                            // Alice: Option 2, Bob: Option 2
+                            testAliceAndBobVoteToOption2(pollContent, pollSummary)
+                            lock.countDown()
+                            roomFromAlicePOV.voteToPoll(pollEventId, pollContent.getBestPollCreationInfo()?.answers?.firstOrNull()?.id ?: "")
+                        }
+                        TOTAL_TEST_COUNT - 4 -> {
+                            // Alice: Option 1, Bob: Option 2
+                            testAliceVotesOption1AndBobVotesOption2(pollContent, pollSummary)
+                            lock.countDown()
+                            roomFromBobPOV.endPoll(pollEventId)
+                        }
+                        TOTAL_TEST_COUNT - 5 -> {
+                            // Alice: Option 1, Bob: Option 2 [poll is ended]
+                            testEndedPoll(pollSummary)
+                            lock.countDown()
+                            roomFromAlicePOV.voteToPoll(pollEventId, pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id ?: "")
+                        }
+                        TOTAL_TEST_COUNT - 6 -> {
+                            // Alice: Option 1 (ignore change), Bob: Option 2 [poll is ended]
+                            testAliceVotesOption1AndBobVotesOption2(pollContent, pollSummary)
+                            testEndedPoll(pollSummary)
+                            lock.countDown()
+                        }
+                        else                 -> {
+                            fail("Lock count ${lock.count} didn't handled.")
+                        }
+                    }
+                }
+            }
+        }
+
+        aliceTimeline.addListener(aliceEventsListener)
+
+        commonTestHelper.await(lock)
+
+        aliceTimeline.removeAllListeners()
+
+        aliceSession.stopSync()
+        aliceTimeline.dispose()
+        cryptoTestData.cleanUp(commonTestHelper)
+    }
+
+    private fun testInitialPollConditions(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) {
+        // No votes yet, poll summary should be null
+        pollSummary shouldBe null
+        // Question should be the same as intended
+        pollContent.getBestPollCreationInfo()?.question?.getBestQuestion() shouldBeEqualTo pollQuestion
+        // Options should be the same as intended
+        pollContent.getBestPollCreationInfo()?.answers?.let { answers ->
+            answers.size shouldBeEqualTo pollOptions.size
+            answers.map { it.getBestAnswer() } shouldContainAll pollOptions
+        }
+    }
+
+    private fun testBobVotesOption1(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) {
+        if (pollSummary == null) {
+            fail("Poll summary shouldn't be null when someone votes")
+            return
+        }
+        val answerId = pollContent.getBestPollCreationInfo()?.answers?.first()?.id
+        // Check if the intended vote is in poll summary
+        pollSummary.aggregatedContent?.let { aggregatedContent ->
+            assertTotalVotesCount(aggregatedContent, 1)
+            aggregatedContent.votes?.first()?.option shouldBeEqualTo answerId
+            aggregatedContent.votesSummary?.get(answerId)?.total shouldBeEqualTo 1
+            aggregatedContent.votesSummary?.get(answerId)?.percentage shouldBeEqualTo 1.0
+        } ?: run { fail("Aggregated poll content shouldn't be null after someone votes") }
+    }
+
+    private fun testBobChangesVoteToOption2(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) {
+        if (pollSummary == null) {
+            fail("Poll summary shouldn't be null when someone votes")
+            return
+        }
+        val answerId = pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id
+        // Check if the intended vote is in poll summary
+        pollSummary.aggregatedContent?.let { aggregatedContent ->
+            assertTotalVotesCount(aggregatedContent, 1)
+            aggregatedContent.votes?.first()?.option shouldBeEqualTo answerId
+            aggregatedContent.votesSummary?.get(answerId)?.total shouldBeEqualTo 1
+            aggregatedContent.votesSummary?.get(answerId)?.percentage shouldBeEqualTo 1.0
+        } ?: run { fail("Aggregated poll content shouldn't be null after someone votes") }
+    }
+
+    private fun testAliceAndBobVoteToOption2(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) {
+        if (pollSummary == null) {
+            fail("Poll summary shouldn't be null when someone votes")
+            return
+        }
+        val answerId = pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id
+        // Check if the intended votes is in poll summary
+        pollSummary.aggregatedContent?.let { aggregatedContent ->
+            assertTotalVotesCount(aggregatedContent, 2)
+            aggregatedContent.votes?.first()?.option shouldBeEqualTo answerId
+            aggregatedContent.votes?.get(1)?.option shouldBeEqualTo answerId
+            aggregatedContent.votesSummary?.get(answerId)?.total shouldBeEqualTo 2
+            aggregatedContent.votesSummary?.get(answerId)?.percentage shouldBeEqualTo 1.0
+        } ?: run { fail("Aggregated poll content shouldn't be null after someone votes") }
+    }
+
+    private fun testAliceVotesOption1AndBobVotesOption2(pollContent: MessagePollContent, pollSummary: PollResponseAggregatedSummary?) {
+        if (pollSummary == null) {
+            fail("Poll summary shouldn't be null when someone votes")
+            return
+        }
+        val firstAnswerId = pollContent.getBestPollCreationInfo()?.answers?.firstOrNull()?.id
+        val secondAnswerId = pollContent.getBestPollCreationInfo()?.answers?.get(1)?.id
+        // Check if the intended votes is in poll summary
+        pollSummary.aggregatedContent?.let { aggregatedContent ->
+            assertTotalVotesCount(aggregatedContent, 2)
+            aggregatedContent.votes!!.map { it.option } shouldContain firstAnswerId
+            aggregatedContent.votes!!.map { it.option } shouldContain secondAnswerId
+            aggregatedContent.votesSummary?.get(firstAnswerId)?.total shouldBeEqualTo 1
+            aggregatedContent.votesSummary?.get(secondAnswerId)?.total shouldBeEqualTo 1
+            aggregatedContent.votesSummary?.get(firstAnswerId)?.percentage shouldBeEqualTo 0.5
+            aggregatedContent.votesSummary?.get(secondAnswerId)?.percentage shouldBeEqualTo 0.5
+        } ?: run { fail("Aggregated poll content shouldn't be null after someone votes") }
+    }
+
+    private fun testEndedPoll(pollSummary: PollResponseAggregatedSummary?) {
+        pollSummary?.closedTime ?: 0 shouldBeGreaterThan 0
+    }
+
+    private fun assertTotalVotesCount(aggregatedContent: PollSummaryContent, expectedVoteCount: Int) {
+        aggregatedContent.totalVotes shouldBeEqualTo expectedVoteCount
+        aggregatedContent.votes?.size shouldBeEqualTo expectedVoteCount
+    }
+
+    companion object {
+        const val pollQuestion = "Do you like creating polls?"
+        val pollOptions = listOf("Yes", "Absolutely", "As long as tests pass")
+    }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt
index 20faa81bb6eb6f3b0404edbba14d2773ee2f4313..50e4a6feb6d7c1af67c28122af82cce790165d73 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt
@@ -475,7 +475,9 @@ class SpaceHierarchyTest : InstrumentedTest {
         //   + C
         //     + c1, c2
 
-        val rootSpaces = session.spaceService().getRootSpaceSummaries()
+        val rootSpaces = commonTestHelper.runBlockingTest {
+            session.spaceService().getRootSpaceSummaries()
+        }
 
         assertEquals("Unexpected number of root spaces ${rootSpaces.map { it.name }}", 2, rootSpaces.size)
 
diff --git a/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt b/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt
index 34ed28d467a97d5439c21304556bcedebc939e3b..2661bd1f70b363781f924e1134f00f4f180ef8e0 100644
--- a/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt
+++ b/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt
@@ -23,7 +23,7 @@ import org.json.JSONException
 import org.json.JSONObject
 import timber.log.Timber
 
-class FormattedJsonHttpLogger : HttpLoggingInterceptor.Logger {
+internal class FormattedJsonHttpLogger : HttpLoggingInterceptor.Logger {
 
     companion object {
         private const val INDENT_SPACE = 2
diff --git a/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/DisplayMaths.kt b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/DisplayMaths.kt
index b8ee36e724005cfbf654ea44c97f233208858a5b..3e83594ca3ad8fd9ef88b16e34d92b5e0d382a5d 100644
--- a/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/DisplayMaths.kt
+++ b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/DisplayMaths.kt
@@ -17,7 +17,7 @@ package org.commonmark.ext.maths
 
 import org.commonmark.node.CustomBlock
 
-class DisplayMaths(private val delimiter: DisplayDelimiter) : CustomBlock() {
+internal class DisplayMaths(private val delimiter: DisplayDelimiter) : CustomBlock() {
     enum class DisplayDelimiter {
         DOUBLE_DOLLAR,
         SQUARE_BRACKET_ESCAPED
diff --git a/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/InlineMaths.kt b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/InlineMaths.kt
index 962b1b8cbf346053c870cde649a7ea5b1d00d5cf..3fe8d156962767af7871664bf24bd88baa5af343 100644
--- a/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/InlineMaths.kt
+++ b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/InlineMaths.kt
@@ -18,7 +18,7 @@ package org.commonmark.ext.maths
 import org.commonmark.node.CustomNode
 import org.commonmark.node.Delimited
 
-class InlineMaths(private val delimiter: InlineDelimiter) : CustomNode(), Delimited {
+internal class InlineMaths(private val delimiter: InlineDelimiter) : CustomNode(), Delimited {
     enum class InlineDelimiter {
         SINGLE_DOLLAR,
         ROUND_BRACKET_ESCAPED
diff --git a/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/MathsExtension.kt b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/MathsExtension.kt
index 18c0fc4284b219fed51825caf8cc782ef3430e0e..7a53253bd68271e598aa9d6d013b9b7d62d426d1 100644
--- a/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/MathsExtension.kt
+++ b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/MathsExtension.kt
@@ -21,7 +21,7 @@ import org.commonmark.ext.maths.internal.MathsHtmlNodeRenderer
 import org.commonmark.parser.Parser
 import org.commonmark.renderer.html.HtmlRenderer
 
-class MathsExtension private constructor() : Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension {
+internal class MathsExtension private constructor() : Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension {
     override fun extend(parserBuilder: Parser.Builder) {
         parserBuilder.customDelimiterProcessor(DollarMathsDelimiterProcessor())
     }
diff --git a/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/DollarMathsDelimiterProcessor.kt b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/DollarMathsDelimiterProcessor.kt
index cfd03fa8f166af00c737470d0886ee275b00c7f7..95ea1a1766fa08a2ffd788c712695fd358c01322 100644
--- a/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/DollarMathsDelimiterProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/DollarMathsDelimiterProcessor.kt
@@ -21,7 +21,7 @@ import org.commonmark.node.Text
 import org.commonmark.parser.delimiter.DelimiterProcessor
 import org.commonmark.parser.delimiter.DelimiterRun
 
-class DollarMathsDelimiterProcessor : DelimiterProcessor {
+internal class DollarMathsDelimiterProcessor : DelimiterProcessor {
     override fun getOpeningCharacter() = '$'
 
     override fun getClosingCharacter() = '$'
diff --git a/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/MathsHtmlNodeRenderer.kt b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/MathsHtmlNodeRenderer.kt
index 94652ed7ad88187c7fb6f67d31d8ad3b6729a9f1..0efecbbe8a9946a0b46a31c00337e33cf5cd1ebd 100644
--- a/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/MathsHtmlNodeRenderer.kt
+++ b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/MathsHtmlNodeRenderer.kt
@@ -22,7 +22,7 @@ import org.commonmark.renderer.html.HtmlNodeRendererContext
 import org.commonmark.renderer.html.HtmlWriter
 import java.util.Collections
 
-class MathsHtmlNodeRenderer(private val context: HtmlNodeRendererContext) : MathsNodeRenderer() {
+internal class MathsHtmlNodeRenderer(private val context: HtmlNodeRendererContext) : MathsNodeRenderer() {
     private val html: HtmlWriter = context.writer
     override fun render(node: Node) {
         val display = node.javaClass == DisplayMaths::class.java
diff --git a/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/MathsNodeRenderer.kt b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/MathsNodeRenderer.kt
index 55cdc05c398cc1c06d8494814dc48a73d1433e86..6924a9fb54efb08327ed4ea972b9fce5c0d6a44e 100644
--- a/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/MathsNodeRenderer.kt
+++ b/matrix-sdk-android/src/main/java/org/commonmark/ext/maths/internal/MathsNodeRenderer.kt
@@ -19,9 +19,8 @@ import org.commonmark.ext.maths.DisplayMaths
 import org.commonmark.ext.maths.InlineMaths
 import org.commonmark.node.Node
 import org.commonmark.renderer.NodeRenderer
-import java.util.HashSet
 
-abstract class MathsNodeRenderer : NodeRenderer {
+internal abstract class MathsNodeRenderer : NodeRenderer {
     override fun getNodeTypes(): Set<Class<out Node>> {
         val types: MutableSet<Class<out Node>> = HashSet()
         types.add(InlineMaths::class.java)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt
index 5fedff53f0d62b10e0167e73824da5fe10f992e7..e7b4b766adefd743132b539abc8eeb8165461890 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt
@@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.legacy.LegacySessionImporter
 import org.matrix.android.sdk.api.network.ApiInterceptorListener
 import org.matrix.android.sdk.api.network.ApiPath
 import org.matrix.android.sdk.api.raw.RawService
+import org.matrix.android.sdk.api.settings.LightweightSettingsStorage
 import org.matrix.android.sdk.internal.SessionManager
 import org.matrix.android.sdk.internal.di.DaggerMatrixComponent
 import org.matrix.android.sdk.internal.network.ApiInterceptor
@@ -56,6 +57,7 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
     @Inject internal lateinit var homeServerHistoryService: HomeServerHistoryService
     @Inject internal lateinit var apiInterceptor: ApiInterceptor
     @Inject internal lateinit var matrixWorkerFactory: MatrixWorkerFactory
+    @Inject internal lateinit var lightweightSettingsStorage: LightweightSettingsStorage
 
     init {
         Monarchy.init(context)
@@ -78,6 +80,8 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
 
     fun rawService() = rawService
 
+    fun lightweightSettingsStorage() = lightweightSettingsStorage
+
     fun homeServerHistoryService() = homeServerHistoryService
 
     fun legacySessionImporter(): LegacySessionImporter {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt
index c87f21d7ac3a5b1d6e475c855e2ab083848a085e..f8472319fdc30456a9316c1a9d094cb9a32b0126 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt
@@ -60,7 +60,11 @@ data class MatrixConfiguration(
         /**
          * RoomDisplayNameFallbackProvider to provide default room display name.
          */
-        val roomDisplayNameFallbackProvider: RoomDisplayNameFallbackProvider
+        val roomDisplayNameFallbackProvider: RoomDisplayNameFallbackProvider,
+        /**
+         * Thread messages default enable/disabled value
+         */
+        val threadMessagesEnabledDefault: Boolean = false,
 ) {
 
     /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/HomeServerHistoryService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/HomeServerHistoryService.kt
index 77e33b8934db8e690d5fe18e03c069d59a8f087c..6850bdd444f4bed07b51dd9a324b12e84ed63b45 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/HomeServerHistoryService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/HomeServerHistoryService.kt
@@ -20,10 +20,19 @@ package org.matrix.android.sdk.api.auth
  * A simple service to remember homeservers you already connected to.
  */
 interface HomeServerHistoryService {
-
+    /**
+     * Get a list of stored homeserver urls.
+     */
     fun getKnownServersUrls(): List<String>
 
+    /**
+     * Add a homeserver url to the list of stored homeserver urls.
+     * Will not be added again if already present in the list.
+     */
     fun addHomeServerToHistory(url: String)
 
+    /**
+     * Delete the list of stored homeserver urls.
+     */
     fun clearHistory()
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/Credentials.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/Credentials.kt
index 434e4a6e2ed33fc952d76a7a63efafcd350f5688..317acccfb513cbfa77e04b8e87731e8c2c9f8ad4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/Credentials.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/Credentials.kt
@@ -18,7 +18,7 @@ package org.matrix.android.sdk.api.auth.data
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
-import org.matrix.android.sdk.internal.util.md5
+import org.matrix.android.sdk.api.util.md5
 
 /**
  * This data class hold credentials user data.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/HomeServerConnectionConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/HomeServerConnectionConfig.kt
index e87824d6a5f50bc4ce4b685aa4b4c1393e284754..c2c1f043bbf4aa3b4ec69ebe46b2511fd0ab36fd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/HomeServerConnectionConfig.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/HomeServerConnectionConfig.kt
@@ -22,7 +22,7 @@ import okhttp3.CipherSuite
 import okhttp3.ConnectionSpec
 import okhttp3.TlsVersion
 import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig.Builder
-import org.matrix.android.sdk.internal.network.ssl.Fingerprint
+import org.matrix.android.sdk.api.network.ssl.Fingerprint
 import org.matrix.android.sdk.internal.util.ensureTrailingSlash
 
 /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/LocalizedFlowDataLoginTerms.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LocalizedFlowDataLoginTerms.kt
similarity index 79%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/LocalizedFlowDataLoginTerms.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LocalizedFlowDataLoginTerms.kt
index 5d119bb6177678fe6883903ba10411070b5f4739..1e844a1d947997acbb445319474cc04c129f5e36 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/LocalizedFlowDataLoginTerms.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LocalizedFlowDataLoginTerms.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.auth.registration
+package org.matrix.android.sdk.api.auth.data
 
 import android.os.Parcelable
 import kotlinx.parcelize.Parcelize
@@ -24,8 +24,8 @@ import kotlinx.parcelize.Parcelize
  */
 @Parcelize
 data class LocalizedFlowDataLoginTerms(
-        var policyName: String? = null,
-        var version: String? = null,
-        var localizedUrl: String? = null,
-        var localizedName: String? = null
+        val policyName: String?,
+        val version: String?,
+        val localizedUrl: String?,
+        val localizedName: String?
 ) : Parcelable
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationFlowResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationFlowResponse.kt
index ac740ddab719a936933f87a1be69388547c3e2e1..0da9eb4b7e157dc984fc5ad159b23ed08662d2c2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationFlowResponse.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationFlowResponse.kt
@@ -19,8 +19,8 @@ package org.matrix.android.sdk.api.auth.registration
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
 import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
+import org.matrix.android.sdk.api.session.uia.InteractiveAuthenticationFlow
 import org.matrix.android.sdk.api.util.JsonDict
-import org.matrix.android.sdk.internal.auth.data.InteractiveAuthenticationFlow
 
 @JsonClass(generateAdapter = true)
 data class RegistrationFlowResponse(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/CryptoConstants.kt
similarity index 96%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoConstants.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/CryptoConstants.kt
index 96635b33d6fd0036d798a120be443ee70e6e85ef..172cfa8360752d6a150a4e96be8d781ec047f837 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoConstants.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/CryptoConstants.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.crypto
+package org.matrix.android.sdk.api.crypto
 
 /**
  * Matrix algorithm value for olm.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/MatrixSdkExtensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/MatrixSdkExtensions.kt
index 1cdb6d49a53b43d8aa2350af966ffe612a2fa1b4..f09e9bb340aefabb29989fa77c84c6fdb311f9fc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/MatrixSdkExtensions.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/extensions/MatrixSdkExtensions.kt
@@ -16,8 +16,8 @@
 
 package org.matrix.android.sdk.api.extensions
 
-import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
-import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
+import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
+import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
 
 /* ==========================================================================================
  * MXDeviceInfo
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt
index aabe6e0d0698c37280687c39034cd9f9f2dce055..362ebcec26db18b2f0829fd215685ba90f9c66e2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt
@@ -22,29 +22,29 @@ import org.matrix.android.sdk.api.session.contentscanner.ContentScannerError
 import org.matrix.android.sdk.api.session.contentscanner.ScanFailure
 import org.matrix.android.sdk.internal.di.MoshiProvider
 import java.io.IOException
+import java.net.UnknownHostException
 import javax.net.ssl.HttpsURLConnection
 
-fun Throwable.is401() =
-        this is Failure.ServerError &&
-                httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED && /* 401 */
-                error.code == MatrixError.M_UNAUTHORIZED
-
-fun Throwable.isTokenError() =
-        this is Failure.ServerError &&
-                (error.code == MatrixError.M_UNKNOWN_TOKEN ||
-                        error.code == MatrixError.M_MISSING_TOKEN ||
-                        error.code == MatrixError.ORG_MATRIX_EXPIRED_ACCOUNT)
-
-fun Throwable.isLimitExceededError() =
-        this is Failure.ServerError &&
-                httpCode == 429 &&
-                error.code == MatrixError.M_LIMIT_EXCEEDED
-
-fun Throwable.shouldBeRetried(): Boolean {
-    return this is Failure.NetworkConnection ||
-            this is IOException ||
-            this.isLimitExceededError()
-}
+fun Throwable.is401() = this is Failure.ServerError &&
+        httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED && /* 401 */
+        error.code == MatrixError.M_UNAUTHORIZED
+
+fun Throwable.is404() = this is Failure.ServerError &&
+        httpCode == HttpsURLConnection.HTTP_NOT_FOUND && /* 404 */
+        error.code == MatrixError.M_NOT_FOUND
+
+fun Throwable.isTokenError() = this is Failure.ServerError &&
+        (error.code == MatrixError.M_UNKNOWN_TOKEN ||
+                error.code == MatrixError.M_MISSING_TOKEN ||
+                error.code == MatrixError.ORG_MATRIX_EXPIRED_ACCOUNT)
+
+fun Throwable.isLimitExceededError() = this is Failure.ServerError &&
+        httpCode == 429 &&
+        error.code == MatrixError.M_LIMIT_EXCEEDED
+
+fun Throwable.shouldBeRetried() = this is Failure.NetworkConnection ||
+        this is IOException ||
+        isLimitExceededError()
 
 /**
  * Get the retry delay in case of rate limit exceeded error, adding 100 ms, of defaultValue otherwise
@@ -58,17 +58,33 @@ fun Throwable.getRetryDelay(defaultValue: Long): Long {
             ?: defaultValue
 }
 
-fun Throwable.isInvalidPassword(): Boolean {
-    return this is Failure.ServerError &&
-            error.code == MatrixError.M_FORBIDDEN &&
-            error.message == "Invalid password"
-}
+fun Throwable.isUsernameInUse() = this is Failure.ServerError &&
+        error.code == MatrixError.M_USER_IN_USE
 
-fun Throwable.isInvalidUIAAuth(): Boolean {
-    return this is Failure.ServerError &&
-            error.code == MatrixError.M_FORBIDDEN &&
-            error.flows != null
-}
+fun Throwable.isInvalidUsername() = this is Failure.ServerError &&
+        error.code == MatrixError.M_INVALID_USERNAME
+
+fun Throwable.isInvalidPassword() = this is Failure.ServerError &&
+        error.code == MatrixError.M_FORBIDDEN &&
+        error.message == "Invalid password"
+
+fun Throwable.isRegistrationDisabled() = this is Failure.ServerError &&
+        error.code == MatrixError.M_FORBIDDEN &&
+        httpCode == HttpsURLConnection.HTTP_FORBIDDEN
+
+fun Throwable.isWeakPassword() = this is Failure.ServerError &&
+        error.code == MatrixError.M_WEAK_PASSWORD
+
+fun Throwable.isLoginEmailUnknown() = this is Failure.ServerError &&
+        error.code == MatrixError.M_FORBIDDEN &&
+        error.message.isEmpty()
+
+fun Throwable.isInvalidUIAAuth() = this is Failure.ServerError &&
+        error.code == MatrixError.M_FORBIDDEN &&
+        error.flows != null
+
+fun Throwable.isHomeserverUnavailable() = this is Failure.NetworkConnection &&
+        this.ioException is UnknownHostException
 
 /**
  * Try to convert to a RegistrationFlowResponse. Return null in the cases it's not possible
@@ -100,13 +116,11 @@ fun Throwable.toRegistrationFlowResponse(): RegistrationFlowResponse? {
     }
 }
 
-fun Throwable.isRegistrationAvailabilityError(): Boolean {
-    return this is Failure.ServerError &&
-            httpCode == HttpsURLConnection.HTTP_BAD_REQUEST && /* 400 */
-            (error.code == MatrixError.M_USER_IN_USE ||
-            error.code == MatrixError.M_INVALID_USERNAME ||
-            error.code == MatrixError.M_EXCLUSIVE)
-}
+fun Throwable.isRegistrationAvailabilityError() = this is Failure.ServerError &&
+        httpCode == HttpsURLConnection.HTTP_BAD_REQUEST && /* 400 */
+        (error.code == MatrixError.M_USER_IN_USE ||
+                error.code == MatrixError.M_INVALID_USERNAME ||
+                error.code == MatrixError.M_EXCLUSIVE)
 
 /**
  * Try to convert to a ScanFailure. Return null in the cases it's not possible
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Failure.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Failure.kt
index 8f1bbb69415304e017729891c44f093d17608087..be139fd82bd44c8c61e0182256d4962e63f449f5 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Failure.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Failure.kt
@@ -17,8 +17,8 @@
 package org.matrix.android.sdk.api.failure
 
 import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
+import org.matrix.android.sdk.api.network.ssl.Fingerprint
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
-import org.matrix.android.sdk.internal.network.ssl.Fingerprint
 import java.io.IOException
 
 /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/GlobalError.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/GlobalError.kt
index 50c84da02b88464d432f85abdc0a8965615f8cf2..b5165b6687d403512b43e1eb36c109be3ff62d44 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/GlobalError.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/GlobalError.kt
@@ -16,7 +16,7 @@
 
 package org.matrix.android.sdk.api.failure
 
-import org.matrix.android.sdk.internal.network.ssl.Fingerprint
+import org.matrix.android.sdk.api.network.ssl.Fingerprint
 
 // This class will be sent to the bus
 sealed class GlobalError {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/MatrixError.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/MatrixError.kt
index 1bc8636103dc34e43bbd42d0494096fe481280e3..32e1aca17d83efdc7bea4d21c84204d0e16c6e6e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/MatrixError.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/MatrixError.kt
@@ -18,8 +18,8 @@ package org.matrix.android.sdk.api.failure
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.session.uia.InteractiveAuthenticationFlow
 import org.matrix.android.sdk.api.util.JsonDict
-import org.matrix.android.sdk.internal.auth.data.InteractiveAuthenticationFlow
 
 /**
  * This data class holds the error defined by the matrix specifications.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/Fingerprint.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/network/ssl/Fingerprint.kt
similarity index 95%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/Fingerprint.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/network/ssl/Fingerprint.kt
index b096bd6c879e06dc173c6b4bc62cec06a4b719e6..93e93fd292115bef726b53d7efa9f0ca0f8e7c62 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/Fingerprint.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/network/ssl/Fingerprint.kt
@@ -14,10 +14,11 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.network.ssl
+package org.matrix.android.sdk.api.network.ssl
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.internal.network.ssl.CertUtil
 import java.security.cert.CertificateException
 import java.security.cert.X509Certificate
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/LiveEventListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/LiveEventListener.kt
index 6fda65953acccba97022df14c10edd229285e8ca..b4b283c86a24dd4c3f2dcee2c0e438e508952a46 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/LiveEventListener.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/LiveEventListener.kt
@@ -25,9 +25,9 @@ interface LiveEventListener {
 
     fun onPaginatedEvent(roomId: String, event: Event)
 
-    fun onEventDecrypted(eventId: String, roomId: String, clearEvent: JsonDict)
+    fun onEventDecrypted(event: Event, clearEvent: JsonDict)
 
-    fun onEventDecryptionError(eventId: String, roomId: String, throwable: Throwable)
+    fun onEventDecryptionError(event: Event, throwable: Throwable)
 
     fun onLiveToDeviceEvent(event: Event)
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/ToDeviceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/ToDeviceService.kt
index 45fd39fa954593329cbcb6b544456a3081fb6c2a..d7afad5b6c4ecd33f49201bc44e9973cabb725cc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/ToDeviceService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/ToDeviceService.kt
@@ -16,8 +16,8 @@
 
 package org.matrix.android.sdk.api.session
 
+import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
 import org.matrix.android.sdk.api.session.events.model.Content
-import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
 import java.util.UUID
 
 interface ToDeviceService {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUrlResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUrlResolver.kt
index 3dd096e1446af39a99718fe29d8a938b7c3ef9dc..523d60359b00309392c01c65fab5185049aa4185 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUrlResolver.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUrlResolver.kt
@@ -16,7 +16,7 @@
 
 package org.matrix.android.sdk.api.session.content
 
-import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
+import org.matrix.android.sdk.api.session.crypto.attachments.ElementToDecrypt
 
 /**
  * This interface defines methods for accessing content from the current session.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/contentscanner/ContentScannerService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/contentscanner/ContentScannerService.kt
index 1dd7bab01c5fa732c0edde2adea143db910deff2..7a85a89058a8ad011b4c68cea1248f2d7eaa1b34 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/contentscanner/ContentScannerService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/contentscanner/ContentScannerService.kt
@@ -17,8 +17,8 @@
 package org.matrix.android.sdk.api.session.contentscanner
 
 import androidx.lifecycle.LiveData
+import org.matrix.android.sdk.api.session.crypto.attachments.ElementToDecrypt
 import org.matrix.android.sdk.api.util.Optional
-import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
 
 interface ContentScannerService {
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt
index e3f00a24b669c6aebad3ff375700e8c26f37506f..d6d1248de78f01711a14f2599caccf968752115e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt
@@ -23,25 +23,24 @@ import org.matrix.android.sdk.api.MatrixCallback
 import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
 import org.matrix.android.sdk.api.listeners.ProgressListener
 import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
+import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
 import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService
 import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener
+import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
+import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
+import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse
+import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
+import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
+import org.matrix.android.sdk.api.session.crypto.model.MXDeviceInfo
+import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult
+import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
+import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
+import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest
+import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
 import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
 import org.matrix.android.sdk.api.session.events.model.Content
 import org.matrix.android.sdk.api.session.events.model.Event
-import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest
-import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
-import org.matrix.android.sdk.internal.crypto.NewSessionListener
-import org.matrix.android.sdk.internal.crypto.OutgoingRoomKeyRequest
-import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel
-import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
-import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult
-import org.matrix.android.sdk.internal.crypto.model.MXDeviceInfo
-import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult
-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.rest.DeviceInfo
-import org.matrix.android.sdk.internal.crypto.model.rest.DevicesListResponse
-import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
+import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
 
 interface CryptoService {
 
@@ -113,6 +112,7 @@ interface CryptoService {
 
     fun isRoomEncrypted(roomId: String): Boolean
 
+    // TODO This could be removed from this interface
     fun encryptEventContent(eventContent: Content,
                             eventType: String,
                             roomId: String,
@@ -121,7 +121,7 @@ interface CryptoService {
     fun discardOutboundSession(roomId: String)
 
     @Throws(MXCryptoError::class)
-    fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult
+    suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult
 
     fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>)
 
@@ -140,7 +140,6 @@ interface CryptoService {
     fun getLiveCryptoDeviceInfo(userIds: List<String>): LiveData<List<CryptoDeviceInfo>>
 
     fun addNewSessionListener(newSessionListener: NewSessionListener)
-
     fun removeSessionListener(listener: NewSessionListener)
 
     fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/MXCryptoError.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/MXCryptoError.kt
index 4956278a101e4a629f4eafb2a21b3af8683111c5..5ff4b54b110907fe8e4eb1860ff7aaa2568b7524 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/MXCryptoError.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/MXCryptoError.kt
@@ -16,8 +16,8 @@
 
 package org.matrix.android.sdk.api.session.crypto
 
-import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
-import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
+import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
+import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
 import org.matrix.olm.OlmException
 
 /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/NewSessionListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/NewSessionListener.kt
new file mode 100644
index 0000000000000000000000000000000000000000..73cbf5fb78a382d1c687985063e8850bbca951d2
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/NewSessionListener.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2022 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.api.session.crypto
+
+/**
+ * This listener notifies on new Megolm sessions being created
+ */
+interface NewSessionListener {
+
+    /**
+     * @param roomId the room id where the new Megolm session has been created for, may be null when importing from external sessions
+     * @param senderKey the sender key of the device which the Megolm session is shared with
+     * @param sessionId the session id of the Megolm session
+     */
+    fun onNewSession(roomId: String?, senderKey: String, sessionId: String)
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/ElementToDecrypt.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/attachments/ElementToDecrypt.kt
similarity index 90%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/ElementToDecrypt.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/attachments/ElementToDecrypt.kt
index 3d00e178a0c56c862b5217471a025095041bf91a..de168ac6e524b2501fc55bbdba6cfc50da073ccb 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/ElementToDecrypt.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/attachments/ElementToDecrypt.kt
@@ -14,11 +14,11 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.crypto.attachments
+package org.matrix.android.sdk.api.session.crypto.attachments
 
 import android.os.Parcelable
 import kotlinx.parcelize.Parcelize
-import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
+import org.matrix.android.sdk.api.session.crypto.model.EncryptedFileInfo
 
 fun EncryptedFileInfo.toElementToDecrypt(): ElementToDecrypt? {
     // Check the validity of some fields
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningService.kt
index 359e33cc2c6bcad8a8be51faa74ae5b2eb0a66c9..46b131f613f009d19f8eb8ba370e9bd2cde103cc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningService.kt
@@ -20,9 +20,6 @@ import androidx.lifecycle.LiveData
 import org.matrix.android.sdk.api.MatrixCallback
 import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
 import org.matrix.android.sdk.api.util.Optional
-import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustResult
-import org.matrix.android.sdk.internal.crypto.crosssigning.UserTrustResult
-import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo
 
 interface CrossSigningService {
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoCrossSigningKey.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CryptoCrossSigningKey.kt
similarity index 95%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoCrossSigningKey.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CryptoCrossSigningKey.kt
index 606d2e3fc0c99a01ddb666336e62c36ab079d060..11996e673e348b59e38b89eae91cd146447473e4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoCrossSigningKey.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CryptoCrossSigningKey.kt
@@ -14,9 +14,10 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.crypto.model
+package org.matrix.android.sdk.api.session.crypto.crosssigning
 
-import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel
+import org.matrix.android.sdk.internal.crypto.model.CryptoInfo
+import org.matrix.android.sdk.internal.crypto.model.CryptoInfoMapper
 import org.matrix.android.sdk.internal.crypto.model.rest.RestKeyInfo
 
 data class CryptoCrossSigningKey(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DeviceTrustLevel.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/DeviceTrustLevel.kt
similarity index 87%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DeviceTrustLevel.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/DeviceTrustLevel.kt
index fa0098e4a40e5c8c8acdfc5d8416923341720d47..a0ab5b3b95ab46168ee6c9acbeb3d4d07f3fe55f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DeviceTrustLevel.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/DeviceTrustLevel.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 The Matrix.org Foundation C.I.C.
+ * Copyright (c) 2022 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.
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.matrix.android.sdk.internal.crypto.crosssigning
+package org.matrix.android.sdk.api.session.crypto.crosssigning
 
 data class DeviceTrustLevel(
         val crossSigningVerified: Boolean,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DeviceTrustResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/DeviceTrustResult.kt
similarity index 88%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DeviceTrustResult.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/DeviceTrustResult.kt
index 6e7c620a0316a28c49eaa10759fabb4f27bed6fc..777d34221f3c7b764ac2e335af8ba6afc0a71802 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DeviceTrustResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/DeviceTrustResult.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 The Matrix.org Foundation C.I.C.
+ * Copyright (c) 2022 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.
@@ -13,9 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.matrix.android.sdk.internal.crypto.crosssigning
-
-import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
+package org.matrix.android.sdk.api.session.crypto.crosssigning
 
 sealed class DeviceTrustResult {
     data class Success(val level: DeviceTrustLevel) : DeviceTrustResult()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/MXCrossSigningInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/MXCrossSigningInfo.kt
index 20ee68d228dd76a7082c3862767759abcb26addc..9604decd62928492065c808c8cb6caf2a6c41431 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/MXCrossSigningInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/MXCrossSigningInfo.kt
@@ -16,9 +16,6 @@
 
 package org.matrix.android.sdk.api.session.crypto.crosssigning
 
-import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey
-import org.matrix.android.sdk.internal.crypto.model.KeyUsage
-
 data class MXCrossSigningInfo(
         val userId: String,
         val crossSigningKeys: List<CryptoCrossSigningKey>
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/PrivateKeysInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/PrivateKeysInfo.kt
similarity index 92%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/PrivateKeysInfo.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/PrivateKeysInfo.kt
index 04793f185a95fb883a90c01a3e2354e3ec5c1726..f15d7dc598b74f61e638d4d592460712f48e3f17 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/PrivateKeysInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/PrivateKeysInfo.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.crypto.store
+package org.matrix.android.sdk.api.session.crypto.crosssigning
 
 data class PrivateKeysInfo(
         val master: String? = null,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UserTrustResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/UserTrustResult.kt
similarity index 83%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UserTrustResult.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/UserTrustResult.kt
index 20e7ca09abd804968bd53b4a1d72f672c6e95c5a..7fc815cd20c971e02eb9de09365c5e97ccee64ee 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UserTrustResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/UserTrustResult.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 The Matrix.org Foundation C.I.C.
+ * Copyright (c) 2022 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.
@@ -14,10 +14,7 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.crypto.crosssigning
-
-import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
-import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey
+package org.matrix.android.sdk.api.session.crypto.crosssigning
 
 sealed class UserTrustResult {
     object Success : UserTrustResult()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Try.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupLastVersionResult.kt
similarity index 50%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Try.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupLastVersionResult.kt
index 2ce0534b49bc436111dc1a2440823b2e9916ec70..a7e985cea9764cdf5b72696e8861d42b67c3b79c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Try.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupLastVersionResult.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 The Matrix.org Foundation C.I.C.
+ * Copyright (c) 2022 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.
@@ -14,27 +14,15 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.extensions
+package org.matrix.android.sdk.api.session.crypto.keysbackup
 
-import arrow.core.Failure
-import arrow.core.Success
-import arrow.core.Try
-import arrow.core.TryOf
-import arrow.core.fix
-
-inline fun <A> TryOf<A>.onError(f: (Throwable) -> Unit): Try<A> = fix()
-        .fold(
-                {
-                    f(it)
-                    Failure(it)
-                },
-                { Success(it) }
-        )
+sealed interface KeysBackupLastVersionResult {
+    // No Keys backup found (404 error)
+    object NoKeysBackup : KeysBackupLastVersionResult
+    data class KeysBackup(val keysVersionResult: KeysVersionResult) : KeysBackupLastVersionResult
+}
 
-/**
- * Same as doOnNext for Observables
- */
-inline fun <A> Try<A>.alsoDo(f: (A) -> Unit) = map {
-    f(it)
-    it
+fun KeysBackupLastVersionResult.toKeysVersionResult(): KeysVersionResult? = when (this) {
+    is KeysBackupLastVersionResult.KeysBackup -> keysVersionResult
+    KeysBackupLastVersionResult.NoKeysBackup  -> null
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupService.kt
index 4464427b903ae748b91effc15043bd01656387a8..9ff99f8dce144f8f5b83a8bb3327742e9ceba868 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupService.kt
@@ -19,21 +19,16 @@ package org.matrix.android.sdk.api.session.crypto.keysbackup
 import org.matrix.android.sdk.api.MatrixCallback
 import org.matrix.android.sdk.api.listeners.ProgressListener
 import org.matrix.android.sdk.api.listeners.StepProgressListener
-import org.matrix.android.sdk.internal.crypto.keysbackup.model.KeysBackupVersionTrust
-import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
-import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion
-import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult
-import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult
-import org.matrix.android.sdk.internal.crypto.store.SavedKeyBackupKeyInfo
+import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
 
 interface KeysBackupService {
     /**
      * Retrieve the current version of the backup from the homeserver
      *
      * It can be different than keysBackupVersion.
-     * @param callback onSuccess(null) will be called if there is no backup on the server
+     * @param callback Asynchronous callback
      */
-    fun getCurrentVersion(callback: MatrixCallback<KeysVersionResult?>)
+    fun getCurrentVersion(callback: MatrixCallback<KeysBackupLastVersionResult>)
 
     /**
      * Create a new keys backup version and enable it, using the information return from [prepareKeysBackupVersion].
@@ -219,4 +214,9 @@ interface KeysBackupService {
     fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo?
 
     fun isValidRecoveryKeyForCurrentVersion(recoveryKey: String, callback: MatrixCallback<Boolean>)
+
+    fun computePrivateKey(passphrase: String,
+                          privateKeySalt: String,
+                          privateKeyIterations: Int,
+                          progressListener: ProgressListener): ByteArray
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeysBackupVersionTrust.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupVersionTrust.kt
similarity index 83%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeysBackupVersionTrust.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupVersionTrust.kt
index 497cb0eb490a6c553b4506654535eeed059c8a1b..c9a2d4e7a59da08ea9d463887f6689d2a4668a97 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeysBackupVersionTrust.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupVersionTrust.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.crypto.keysbackup.model
+package org.matrix.android.sdk.api.session.crypto.keysbackup
 
 /**
  * Data model for response to [KeysBackup.getKeysBackupTrust()].
@@ -24,10 +24,10 @@ data class KeysBackupVersionTrust(
          * Flag to indicate if the backup is trusted.
          * true if there is a signature that is valid & from a trusted device.
          */
-        var usable: Boolean = false,
+        val usable: Boolean,
 
         /**
          * Signatures found in the backup version.
          */
-        var signatures: MutableList<KeysBackupVersionTrustSignature> = ArrayList()
+        val signatures: List<KeysBackupVersionTrustSignature> = emptyList()
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeysBackupVersionTrustSignature.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupVersionTrustSignature.kt
similarity index 53%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeysBackupVersionTrustSignature.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupVersionTrustSignature.kt
index 1e3db288826ade318441bd63967ba7ee34d17135..219a328cfdd0411e406ab7685fdbab1ca7df7375 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeysBackupVersionTrustSignature.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysBackupVersionTrustSignature.kt
@@ -14,28 +14,27 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.crypto.keysbackup.model
+package org.matrix.android.sdk.api.session.crypto.keysbackup
 
-import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
+import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
 
 /**
  * A signature in a `KeysBackupVersionTrust` object.
  */
-class KeysBackupVersionTrustSignature {
+data class KeysBackupVersionTrustSignature(
+        /**
+         * The id of the device that signed the backup version.
+         */
+        val deviceId: String?,
 
-    /**
-     * The id of the device that signed the backup version.
-     */
-    var deviceId: String? = null
+        /**
+         * The device that signed the backup version.
+         * Can be null if the device is not known.
+         */
+        val device: CryptoDeviceInfo?,
 
-    /**
-     * The device that signed the backup version.
-     * Can be null if the device is not known.
-     */
-    var device: CryptoDeviceInfo? = null
-
-    /**
-     * Flag to indicate the signature from this device is valid.
-     */
-    var valid = false
-}
+        /**
+         * Flag to indicate the signature from this device is valid.
+         */
+        val valid: Boolean,
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysVersion.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysVersion.kt
similarity index 86%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysVersion.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysVersion.kt
index 7a4c3415fc0c14e5f3289790e2fa6359a3b3d3d2..1dbfe84dff5e4ef0474d6fb5df06302bc3374566 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysVersion.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysVersion.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 The Matrix.org Foundation C.I.C.
+ * Copyright (c) 2022 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.crypto.keysbackup.model.rest
+package org.matrix.android.sdk.api.session.crypto.keysbackup
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysVersionResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysVersionResult.kt
similarity index 91%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysVersionResult.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysVersionResult.kt
index 485fd48a8cfd3d428a17a247c0231765bbebe191..f283a34e98f665ded75c3cc32beb21c57a3d13a6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysVersionResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/KeysVersionResult.kt
@@ -14,11 +14,12 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.crypto.keysbackup.model.rest
+package org.matrix.android.sdk.api.session.crypto.keysbackup
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
 import org.matrix.android.sdk.api.util.JsonDict
+import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysAlgorithmAndData
 
 @JsonClass(generateAdapter = true)
 data class KeysVersionResult(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupAuthData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/MegolmBackupAuthData.kt
similarity index 79%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupAuthData.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/MegolmBackupAuthData.kt
index 17c895762c400bf6549dbdb1a31abebc42520391..2a620af843ce118318681aa3749929b7ca78eb39 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupAuthData.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/MegolmBackupAuthData.kt
@@ -14,11 +14,12 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.crypto.keysbackup.model
+package org.matrix.android.sdk.api.session.crypto.keysbackup
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
 import org.matrix.android.sdk.api.util.JsonDict
+import org.matrix.android.sdk.internal.crypto.keysbackup.model.SignalableMegolmBackupAuthData
 import org.matrix.android.sdk.internal.di.MoshiProvider
 
 /**
@@ -54,7 +55,7 @@ data class MegolmBackupAuthData(
         val signatures: Map<String, Map<String, String>>? = null
 ) {
 
-    fun toJsonDict(): JsonDict {
+    internal fun toJsonDict(): JsonDict {
         val moshi = MoshiProvider.providesMoshi()
         val adapter = moshi.adapter(Map::class.java)
 
@@ -67,7 +68,7 @@ data class MegolmBackupAuthData(
                 }
     }
 
-    fun signalableJSONDictionary(): JsonDict {
+    internal fun signalableJSONDictionary(): JsonDict {
         return SignalableMegolmBackupAuthData(
                 publicKey = publicKey,
                 privateKeySalt = privateKeySalt,
@@ -76,20 +77,3 @@ data class MegolmBackupAuthData(
                 .signalableJSONDictionary()
     }
 }
-
-internal data class SignalableMegolmBackupAuthData(
-        val publicKey: String,
-        val privateKeySalt: String? = null,
-        val privateKeyIterations: Int? = null
-) {
-    fun signalableJSONDictionary(): JsonDict = HashMap<String, Any>().apply {
-        put("public_key", publicKey)
-
-        privateKeySalt?.let {
-            put("private_key_salt", it)
-        }
-        privateKeyIterations?.let {
-            put("private_key_iterations", it)
-        }
-    }
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupCreationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/MegolmBackupCreationInfo.kt
similarity index 94%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupCreationInfo.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/MegolmBackupCreationInfo.kt
index c668e78a9e8a3c9f09b8c356ab14293b92d29550..0d708b8d735f9431400adc490d242d9a157dd3e6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupCreationInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/MegolmBackupCreationInfo.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.crypto.keysbackup.model
+package org.matrix.android.sdk.api.session.crypto.keysbackup
 
 /**
  * Data retrieved from Olm library. algorithm and authData will be send to the homeserver, and recoveryKey will be displayed to the user
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/RecoveryKey.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/RecoveryKey.kt
similarity index 93%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/RecoveryKey.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/RecoveryKey.kt
index 44774fd5a687eb64d7c97fbe1c4bedc7b8f75cb6..85d6ef43653a8481134c19aa57bb87fc76c7d66c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/RecoveryKey.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/RecoveryKey.kt
@@ -14,8 +14,10 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.crypto.keysbackup.util
+package org.matrix.android.sdk.api.session.crypto.keysbackup
 
+import org.matrix.android.sdk.internal.crypto.keysbackup.util.base58decode
+import org.matrix.android.sdk.internal.crypto.keysbackup.util.base58encode
 import kotlin.experimental.xor
 
 /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/SavedKeyBackupKeyInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/SavedKeyBackupKeyInfo.kt
similarity index 92%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/SavedKeyBackupKeyInfo.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/SavedKeyBackupKeyInfo.kt
index a48f4ecef5a4723111fd903313335ba51a2698bf..7f90fea9af028967097672bc9dde50809c53a7b1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/SavedKeyBackupKeyInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keysbackup/SavedKeyBackupKeyInfo.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.crypto.store
+package org.matrix.android.sdk.api.session.crypto.keysbackup
 
 data class SavedKeyBackupKeyInfo(
         val recoveryKey: String,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keyshare/GossipingRequestListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keyshare/GossipingRequestListener.kt
index ba2d4ba3f69be096989668c89d997e4dca3726ae..3cd36c2ce8cdd6a6bd9b18d70b6e9da1fe059171 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keyshare/GossipingRequestListener.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/keyshare/GossipingRequestListener.kt
@@ -16,9 +16,9 @@
 
 package org.matrix.android.sdk.api.session.crypto.keyshare
 
-import org.matrix.android.sdk.internal.crypto.IncomingRequestCancellation
-import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest
-import org.matrix.android.sdk.internal.crypto.IncomingSecretShareRequest
+import org.matrix.android.sdk.api.session.crypto.model.IncomingRequestCancellation
+import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
+import org.matrix.android.sdk.api.session.crypto.model.IncomingSecretShareRequest
 
 /**
  * Room keys events listener
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/CryptoDeviceInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/CryptoDeviceInfo.kt
new file mode 100644
index 0000000000000000000000000000000000000000..418b1e6ce3373ada25ead2ba80dea852ba3378fd
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/CryptoDeviceInfo.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2020 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.api.session.crypto.model
+
+import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
+import org.matrix.android.sdk.internal.crypto.model.CryptoInfo
+
+data class CryptoDeviceInfo(
+        val deviceId: String,
+        override val userId: String,
+        val algorithms: List<String>? = null,
+        override val keys: Map<String, String>? = null,
+        override val signatures: Map<String, Map<String, String>>? = null,
+        val unsigned: UnsignedDeviceInfo? = null,
+        var trustLevel: DeviceTrustLevel? = null,
+        val isBlocked: Boolean = false,
+        val firstTimeSeenLocalTs: Long? = null
+) : CryptoInfo {
+
+    val isVerified: Boolean
+        get() = trustLevel?.isVerified() == true
+
+    val isUnknown: Boolean
+        get() = trustLevel == null
+
+    /**
+     * @return the fingerprint
+     */
+    fun fingerprint(): String? {
+        return keys
+                ?.takeIf { deviceId.isNotBlank() }
+                ?.get("ed25519:$deviceId")
+    }
+
+    /**
+     * @return the identity key
+     */
+    fun identityKey(): String? {
+        return keys
+                ?.takeIf { deviceId.isNotBlank() }
+                ?.get("curve25519:$deviceId")
+    }
+
+    /**
+     * @return the display name
+     */
+    fun displayName(): String? {
+        return unsigned?.deviceDisplayName
+    }
+
+    override fun signalableJSONDictionary(): Map<String, Any> {
+        val map = HashMap<String, Any>()
+        map["device_id"] = deviceId
+        map["user_id"] = userId
+        algorithms?.let { map["algorithms"] = it }
+        keys?.let { map["keys"] = it }
+        return map
+    }
+
+    fun shortDebugString() = "$userId|$deviceId"
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/DeviceInfo.kt
similarity index 96%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceInfo.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/DeviceInfo.kt
index c5cd4003427929d41fe02549952cd3ca3f0f987e..221d0793d94e88a92d82115a5a8772d61a91cb85 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/DeviceInfo.kt
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.matrix.android.sdk.internal.crypto.model.rest
+package org.matrix.android.sdk.api.session.crypto.model
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DevicesListResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/DevicesListResponse.kt
similarity index 94%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DevicesListResponse.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/DevicesListResponse.kt
index eb325f332e2ecb9eeaf73cc0df211e3eee220468..01f33211661891811b320f7596121facc16f82ed 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DevicesListResponse.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/DevicesListResponse.kt
@@ -13,7 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.matrix.android.sdk.internal.crypto.model.rest
+
+package org.matrix.android.sdk.api.session.crypto.model
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedFileInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/EncryptedFileInfo.kt
similarity index 97%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedFileInfo.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/EncryptedFileInfo.kt
index 4fc3adb42cc43de630d10c250a3063d4261b3e3e..13ad1df47655389e5d2623508dc1b3bdce58054b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedFileInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/EncryptedFileInfo.kt
@@ -13,7 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.matrix.android.sdk.internal.crypto.model.rest
+
+package org.matrix.android.sdk.api.session.crypto.model
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedFileKey.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/EncryptedFileKey.kt
similarity index 97%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedFileKey.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/EncryptedFileKey.kt
index 71f266d7a518b7bb9f7282968507d44c2cfd0182..859c6ac43f0dc374a8c5fd6c32bc09abef705e0c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedFileKey.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/EncryptedFileKey.kt
@@ -13,7 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.matrix.android.sdk.internal.crypto.model.rest
+
+package org.matrix.android.sdk.api.session.crypto.model
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/ForwardedRoomKeyContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/ForwardedRoomKeyContent.kt
similarity index 95%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/ForwardedRoomKeyContent.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/ForwardedRoomKeyContent.kt
index bbc24f044716a6d7285ca8d75a473dcec7b4d289..3df4ef7c9a068296184d464105896e1441cdfdd0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/ForwardedRoomKeyContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/ForwardedRoomKeyContent.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 The Matrix.org Foundation C.I.C.
+ * Copyright (c) 2022 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.
@@ -13,7 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.matrix.android.sdk.internal.crypto.model.rest
+
+package org.matrix.android.sdk.api.session.crypto.model
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipingRequestState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/GossipingRequestState.kt
similarity index 76%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipingRequestState.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/GossipingRequestState.kt
index e398cbfcaf2a63c513e7c359a0bc0252e549b755..d9a6f4fcba1af402522b1b29ead0466f4e008630 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipingRequestState.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/GossipingRequestState.kt
@@ -14,12 +14,7 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.crypto
-
-enum class GossipRequestType {
-    KEY,
-    SECRET
-}
+package org.matrix.android.sdk.api.session.crypto.model
 
 enum class GossipingRequestState {
     NONE,
@@ -34,13 +29,3 @@ enum class GossipingRequestState {
     CANCELLED_BY_REQUESTER,
     RE_REQUESTED
 }
-
-enum class OutgoingGossipingRequestState {
-    UNSENT,
-    SENDING,
-    SENT,
-    CANCELLING,
-    CANCELLED,
-    FAILED_TO_SEND,
-    FAILED_TO_CANCEL
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/GossipingToDeviceObject.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/GossipingToDeviceObject.kt
similarity index 70%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/GossipingToDeviceObject.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/GossipingToDeviceObject.kt
index e2ae9d1d6c402906ecd2413eb04005fcb8a2dd5b..1922b2bceef7eaec21ded77307eddcb423d2b764 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/GossipingToDeviceObject.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/GossipingToDeviceObject.kt
@@ -13,10 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.matrix.android.sdk.internal.crypto.model.rest
 
-import com.squareup.moshi.Json
-import com.squareup.moshi.JsonClass
+package org.matrix.android.sdk.api.session.crypto.model
 
 /**
  * Interface representing an room key action request
@@ -35,10 +33,3 @@ interface GossipingToDeviceObject : SendToDeviceObject {
         const val ACTION_SHARE_CANCELLATION = "request_cancellation"
     }
 }
-
-@JsonClass(generateAdapter = true)
-data class GossipingDefaultContent(
-        @Json(name = "action") override val action: String?,
-        @Json(name = "requesting_device_id") override val requestingDeviceId: String?,
-        @Json(name = "m.request_id") override val requestId: String? = null
-) : GossipingToDeviceObject
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/ImportRoomKeysResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/ImportRoomKeysResult.kt
similarity index 76%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/ImportRoomKeysResult.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/ImportRoomKeysResult.kt
index e9d2a1bcd8bd5545940224e0a52fc525e6193637..b55f0e87479c87bb0c4858e7251805753dae7cf0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/ImportRoomKeysResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/ImportRoomKeysResult.kt
@@ -14,7 +14,9 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.crypto.model
+package org.matrix.android.sdk.api.session.crypto.model
 
-data class ImportRoomKeysResult(val totalNumberOfKeys: Int,
-                                val successfullyNumberOfImportedKeys: Int)
+data class ImportRoomKeysResult(
+        val totalNumberOfKeys: Int,
+        val successfullyNumberOfImportedKeys: Int
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingRequestCancellation.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRequestCancellation.kt
similarity index 94%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingRequestCancellation.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRequestCancellation.kt
index 181bc0c1b1c3f9889e54ee8c14ae72dfa9a4aac8..74ca7304f7348386e65b4040b938a773852d1b5e 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingRequestCancellation.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRequestCancellation.kt
@@ -14,10 +14,11 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.crypto
+package org.matrix.android.sdk.api.session.crypto.model
 
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon
 import org.matrix.android.sdk.internal.crypto.model.rest.ShareRequestCancellation
 
 /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingRoomKeyRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRoomKeyRequest.kt
similarity index 92%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingRoomKeyRequest.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRoomKeyRequest.kt
index babc6008a2a5a6124e429222972100254f1dce3b..45b0926d8912cca217193efffc8d13f880d13fae 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingRoomKeyRequest.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingRoomKeyRequest.kt
@@ -14,12 +14,11 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.crypto
+package org.matrix.android.sdk.api.session.crypto.model
 
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.toModel
-import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
-import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyShareRequest
+import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon
 
 /**
  * IncomingRoomKeyRequest class defines the incoming room keys request.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingSecretShareRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingSecretShareRequest.kt
similarity index 94%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingSecretShareRequest.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingSecretShareRequest.kt
index d2ee69196cb6a5f6427670d6298e4cf720ce649a..5afffef1aed4d2dafd4f6cb97c2ad2f931460468 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingSecretShareRequest.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/IncomingSecretShareRequest.kt
@@ -14,11 +14,11 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.crypto
+package org.matrix.android.sdk.api.session.crypto.model
 
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.toModel
-import org.matrix.android.sdk.internal.crypto.model.rest.SecretShareRequest
+import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon
 
 /**
  * IncomingSecretShareRequest class defines the incoming secret keys request.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXDeviceInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXDeviceInfo.kt
similarity index 90%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXDeviceInfo.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXDeviceInfo.kt
index 68cc41005eff82afab592e3a1695a4d4db9e11cc..286ab2b7d518ef0f55dd33b5bc569dbcc1ea5241 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXDeviceInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXDeviceInfo.kt
@@ -14,12 +14,11 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.crypto.model
+package org.matrix.android.sdk.api.session.crypto.model
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
 import org.matrix.android.sdk.api.util.JsonDict
-import org.matrix.android.sdk.internal.crypto.model.rest.DeviceKeys
 import java.io.Serializable
 
 @JsonClass(generateAdapter = true)
@@ -144,19 +143,6 @@ data class MXDeviceInfo(
         return map
     }
 
-    /**
-     * @return a dictionary of the parameters
-     */
-    fun toDeviceKeys(): DeviceKeys {
-        return DeviceKeys(
-                userId = userId,
-                deviceId = deviceId,
-                algorithms = algorithms!!,
-                keys = keys!!,
-                signatures = signatures!!
-        )
-    }
-
     override fun toString(): String {
         return "MXDeviceInfo $userId:$deviceId"
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXEncryptEventContentResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXEncryptEventContentResult.kt
similarity index 94%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXEncryptEventContentResult.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXEncryptEventContentResult.kt
index 524bc8084323a84da12c1909f74d1b4076a65f3b..706e40a769e485b298edb2911ac828b30e9bab0b 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXEncryptEventContentResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXEncryptEventContentResult.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.crypto.model
+package org.matrix.android.sdk.api.session.crypto.model
 
 import org.matrix.android.sdk.api.session.events.model.Content
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXEventDecryptionResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXEventDecryptionResult.kt
similarity index 96%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXEventDecryptionResult.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXEventDecryptionResult.kt
index c66c37574ab73eb8e2020eb430341be80a10ab5f..0a0ccc2db3e26be08aa2496f591f503090c3b48d 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXEventDecryptionResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXEventDecryptionResult.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.crypto
+package org.matrix.android.sdk.api.session.crypto.model
 
 import org.matrix.android.sdk.api.util.JsonDict
 
@@ -22,7 +22,6 @@ import org.matrix.android.sdk.api.util.JsonDict
  * The result of a (successful) call to decryptEvent.
  */
 data class MXEventDecryptionResult(
-
         /**
          * The plaintext payload for the event (typically containing "type" and "content" fields).
          */
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXUsersDevicesMap.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXUsersDevicesMap.kt
new file mode 100755
index 0000000000000000000000000000000000000000..dc5567e908d760354ff95efd1dc35b137e41de8b
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/MXUsersDevicesMap.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2020 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.api.session.crypto.model
+
+class MXUsersDevicesMap<E> {
+
+    // A map of maps (userId -> (deviceId -> Object)).
+    val map = HashMap<String /* userId */, HashMap<String /* deviceId */, E>>()
+
+    /**
+     * @return the user Ids
+     */
+    val userIds: List<String>
+        get() = map.keys.toList()
+
+    val isEmpty: Boolean
+        get() = map.isEmpty()
+
+    /**
+     * Provides the device ids list for a user id
+     * FIXME Should maybe return emptyList and not null, to avoid many !! in the code
+     *
+     * @param userId the user id
+     * @return the device ids list
+     */
+    fun getUserDeviceIds(userId: String?): List<String>? {
+        return if (!userId.isNullOrBlank() && map.containsKey(userId)) {
+            map[userId]!!.keys.toList()
+        } else null
+    }
+
+    /**
+     * Provides the object for a device id and a user Id
+     *
+     * @param deviceId the device id
+     * @param userId   the object id
+     * @return the object
+     */
+    fun getObject(userId: String?, deviceId: String?): E? {
+        return if (!userId.isNullOrBlank() && !deviceId.isNullOrBlank()) {
+            map[userId]?.get(deviceId)
+        } else null
+    }
+
+    /**
+     * Set an object for a dedicated user Id and device Id
+     *
+     * @param userId   the user Id
+     * @param deviceId the device id
+     * @param o        the object to set
+     */
+    fun setObject(userId: String?, deviceId: String?, o: E?) {
+        if (null != o && userId?.isNotBlank() == true && deviceId?.isNotBlank() == true) {
+            val devices = map.getOrPut(userId) { HashMap() }
+            devices[deviceId] = o
+        }
+    }
+
+    /**
+     * Defines the objects map for a user Id
+     *
+     * @param objectsPerDevices the objects maps
+     * @param userId            the user id
+     */
+    fun setObjects(userId: String?, objectsPerDevices: Map<String, E>?) {
+        if (!userId.isNullOrBlank()) {
+            if (null == objectsPerDevices) {
+                map.remove(userId)
+            } else {
+                map[userId] = HashMap(objectsPerDevices)
+            }
+        }
+    }
+
+    /**
+     * Removes objects for a dedicated user
+     *
+     * @param userId the user id.
+     */
+    fun removeUserObjects(userId: String?) {
+        if (!userId.isNullOrBlank()) {
+            map.remove(userId)
+        }
+    }
+
+    /**
+     * Clear the internal dictionary
+     */
+    fun removeAllObjects() {
+        map.clear()
+    }
+
+    /**
+     * Add entries from another MXUsersDevicesMap
+     *
+     * @param other the other one
+     */
+    fun addEntriesFromMap(other: MXUsersDevicesMap<E>?) {
+        if (null != other) {
+            map.putAll(other.map)
+        }
+    }
+
+    override fun toString(): String {
+        return "MXUsersDevicesMap $map"
+    }
+}
+
+inline fun <T> MXUsersDevicesMap<T>.forEach(action: (String, String, T) -> Unit) {
+    userIds.forEach { userId ->
+        getUserDeviceIds(userId)?.forEach { deviceId ->
+            getObject(userId, deviceId)?.let {
+                action(userId, deviceId, it)
+            }
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/OlmDecryptionResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OlmDecryptionResult.kt
similarity index 93%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/OlmDecryptionResult.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OlmDecryptionResult.kt
index 955f57afbaf8b36fa3727a9dd6e524223c024414..9cf2bf75fbd27206d91b683ebd0e46b75edd14b9 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/OlmDecryptionResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OlmDecryptionResult.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 The Matrix.org Foundation C.I.C.
+ * Copyright (c) 2022 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.crypto.algorithms.olm
+package org.matrix.android.sdk.api.session.crypto.model
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Exhaustive.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OutgoingGossipingRequestState.kt
similarity index 74%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Exhaustive.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OutgoingGossipingRequestState.kt
index 097bdaf153069e06e7ac31ceb2a1707fe5f45149..8c1bdf6768b5397f4457e113a09e91d20de5666f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Exhaustive.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OutgoingGossipingRequestState.kt
@@ -14,7 +14,14 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.util
+package org.matrix.android.sdk.api.session.crypto.model
 
-// Trick to ensure that when block is exhaustive
-internal val <T> T.exhaustive: T get() = this
+enum class OutgoingGossipingRequestState {
+    UNSENT,
+    SENDING,
+    SENT,
+    CANCELLING,
+    CANCELLED,
+    FAILED_TO_SEND,
+    FAILED_TO_CANCEL
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingRoomKeyRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OutgoingRoomKeyRequest.kt
similarity index 72%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingRoomKeyRequest.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OutgoingRoomKeyRequest.kt
index 88025952db47407478ee8a872195dcb5cc6c2e42..5f35cc908f9bd63d736cdc16082848972d18069d 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingRoomKeyRequest.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/OutgoingRoomKeyRequest.kt
@@ -14,10 +14,10 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.crypto
+package org.matrix.android.sdk.api.session.crypto.model
 
 import com.squareup.moshi.JsonClass
-import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
+import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequest
 
 /**
  * Represents an outgoing room key request
@@ -25,14 +25,14 @@ import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
 @JsonClass(generateAdapter = true)
 data class OutgoingRoomKeyRequest(
         // RequestBody
-        var requestBody: RoomKeyRequestBody?,
+        val requestBody: RoomKeyRequestBody?,
         // list of recipients for the request
-        override var recipients: Map<String, List<String>>,
+        override val recipients: Map<String, List<String>>,
         // Unique id for this request. Used for both
         // an id within the request for later pairing with a cancellation, and for
         // the transaction id when sending the to_device messages to our local
-        override var requestId: String, // current state of this request
-        override var state: OutgoingGossipingRequestState
+        override val requestId: String, // current state of this request
+        override val state: OutgoingGossipingRequestState
         // transaction id for the cancellation, if any
         // override var cancellationTxnId: String? = null
 ) : OutgoingGossipingRequest {
@@ -43,9 +43,7 @@ data class OutgoingRoomKeyRequest(
      * @return the room id.
      */
     val roomId: String?
-        get() = if (null != requestBody) {
-            requestBody!!.roomId
-        } else null
+        get() = requestBody?.roomId
 
     /**
      * Used only for log.
@@ -53,7 +51,5 @@ data class OutgoingRoomKeyRequest(
      * @return the session id
      */
     val sessionId: String?
-        get() = if (null != requestBody) {
-            requestBody!!.sessionId
-        } else null
+        get() = requestBody?.sessionId
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/RoomEncryptionTrustLevel.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/RoomEncryptionTrustLevel.kt
similarity index 95%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/RoomEncryptionTrustLevel.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/RoomEncryptionTrustLevel.kt
index 8ba99ad70b41762ce6a17854c8ce68016a9141e4..68c7496d58d65aa7d1a5a759b50b3fd9b114f368 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/RoomEncryptionTrustLevel.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/RoomEncryptionTrustLevel.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.api.crypto
+package org.matrix.android.sdk.api.session.crypto.model
 
 /**
  * RoomEncryptionTrustLevel represents the trust level in an encrypted room.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/RoomKeyRequestBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/RoomKeyRequestBody.kt
similarity index 96%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/RoomKeyRequestBody.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/RoomKeyRequestBody.kt
index 3eae2585a564842dde2b4222a4e462c9570577db..15163248dc94365e5faf0569dafacbe3ac7d551e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/RoomKeyRequestBody.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/RoomKeyRequestBody.kt
@@ -13,7 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.matrix.android.sdk.internal.crypto.model.rest
+
+package org.matrix.android.sdk.api.session.crypto.model
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/RoomKeyShareRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/RoomKeyShareRequest.kt
similarity index 95%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/RoomKeyShareRequest.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/RoomKeyShareRequest.kt
index 68fbf0b805e16062c4eae96349a4a022014fafb2..b6bb4c55af9f1f5acf217265968b92d429555d93 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/RoomKeyShareRequest.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/RoomKeyShareRequest.kt
@@ -13,7 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.matrix.android.sdk.internal.crypto.model.rest
+
+package org.matrix.android.sdk.api.session.crypto.model
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/SecretShareRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/SecretShareRequest.kt
similarity index 95%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/SecretShareRequest.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/SecretShareRequest.kt
index a4eeb50d8bac30a690114008dd35b1881bb5e0ec..6009077806146c541581a6a99618de17dd5e3d20 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/SecretShareRequest.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/SecretShareRequest.kt
@@ -13,7 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.matrix.android.sdk.internal.crypto.model.rest
+
+package org.matrix.android.sdk.api.session.crypto.model
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/SendToDeviceObject.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/SendToDeviceObject.kt
similarity index 91%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/SendToDeviceObject.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/SendToDeviceObject.kt
index b3a76b2a7c47d9ab69d57e8d253d88162bd8b5ca..b866cb76cc818478a6905a9e936f3261d0932636 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/SendToDeviceObject.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/SendToDeviceObject.kt
@@ -14,6 +14,6 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.crypto.model.rest
+package org.matrix.android.sdk.api.session.crypto.model
 
 interface SendToDeviceObject
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UnsignedDeviceInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/UnsignedDeviceInfo.kt
similarity index 94%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UnsignedDeviceInfo.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/UnsignedDeviceInfo.kt
index 5f316486b6eaa2342f63983b376ba04c82acfe02..1d9d1fce3e310a91e550fdea67ca174d7139041f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UnsignedDeviceInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/UnsignedDeviceInfo.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.crypto.model.rest
+package org.matrix.android.sdk.api.session.crypto.model
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/VerificationState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationState.kt
similarity index 87%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/VerificationState.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationState.kt
index 54276a6b51e3cab57d89c05dca9b8e28c6094f7d..fe855278a5eef22cf729c7df606a7eb2027c06b9 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/VerificationState.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/verification/VerificationState.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2021 The Matrix.org Foundation C.I.C.
+ * Copyright (c) 2022 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.api.crypto
+package org.matrix.android.sdk.api.session.crypto.verification
 
 enum class VerificationState {
     REQUEST,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt
index 34096d603fd14b877e4a7ee9811680a2267a717d..ae8ed3941fa3d4396044186eab396204a293791d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/AggregatedRelations.kt
@@ -49,5 +49,6 @@ import com.squareup.moshi.JsonClass
 @JsonClass(generateAdapter = true)
 data class AggregatedRelations(
         @Json(name = "m.annotation") val annotations: AggregatedAnnotation? = null,
-        @Json(name = "m.reference") val references: DefaultUnsignedRelationInfo? = null
+        @Json(name = "m.reference") val references: DefaultUnsignedRelationInfo? = null,
+        @Json(name = RelationType.THREAD) val latestThread: LatestThreadUnsignedRelation? = null
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
index df57ca568111d9c5144ee77e168d440dfd8a96f3..1ce51a2bded08c7c43c1e83ccd690bc8da7a6779 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
@@ -22,6 +22,8 @@ import org.json.JSONObject
 import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.failure.MatrixError
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
+import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
+import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
 import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageContent
@@ -34,8 +36,7 @@ import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.api.session.threads.ThreadDetails
 import org.matrix.android.sdk.api.util.ContentUtils
 import org.matrix.android.sdk.api.util.JsonDict
-import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
-import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
+import org.matrix.android.sdk.api.util.MatrixJsonParser
 import org.matrix.android.sdk.internal.di.MoshiProvider
 import org.matrix.android.sdk.internal.session.presence.model.PresenceContent
 import timber.log.Timber
@@ -46,7 +47,7 @@ typealias Content = JsonDict
  * This methods is a facility method to map a json content to a model.
  */
 inline fun <reified T> Content?.toModel(catchError: Boolean = true): T? {
-    val moshi = MoshiProvider.providesMoshi()
+    val moshi = MatrixJsonParser.getMoshi()
     val moshiAdapter = moshi.adapter(T::class.java)
     return try {
         moshiAdapter.fromJsonValue(this)
@@ -65,7 +66,7 @@ inline fun <reified T> Content?.toModel(catchError: Boolean = true): T? {
  */
 @Suppress("UNCHECKED_CAST")
 inline fun <reified T> T.toContent(): Content {
-    val moshi = MoshiProvider.providesMoshi()
+    val moshi = MatrixJsonParser.getMoshi()
     val moshiAdapter = moshi.adapter(T::class.java)
     return moshiAdapter.toJsonValue(this) as Content
 }
@@ -201,7 +202,13 @@ data class Event(
      */
     fun getDecryptedTextSummary(): String? {
         if (isRedacted()) return "Message Deleted"
-        val text = getDecryptedValue() ?: return null
+        val text = getDecryptedValue() ?: run {
+            if (isPoll()) {
+                return getPollQuestion() ?: "created a poll."
+            }
+            return null
+        }
+
         return when {
             isReplyRenderedInThread() || isQuote() -> ContentUtils.extractUsefulTextFromReply(text)
             isFileMessage()                        -> "sent a file."
@@ -296,60 +303,70 @@ data class Event(
     }
 }
 
+/**
+ * Return the value of "content.msgtype", if the Event type is "m.room.message"
+ * and if the content has it, and if it is a String
+ */
+fun Event.getMsgType(): String? {
+    if (getClearType() != EventType.MESSAGE) return null
+    return getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY) as? String
+}
+
 fun Event.isTextMessage(): Boolean {
-    return getClearType() == EventType.MESSAGE &&
-            when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
-                MessageType.MSGTYPE_TEXT,
-                MessageType.MSGTYPE_EMOTE,
-                MessageType.MSGTYPE_NOTICE -> true
-                else                       -> false
-            }
+    return when (getMsgType()) {
+        MessageType.MSGTYPE_TEXT,
+        MessageType.MSGTYPE_EMOTE,
+        MessageType.MSGTYPE_NOTICE -> true
+        else                       -> false
+    }
 }
 
 fun Event.isImageMessage(): Boolean {
-    return getClearType() == EventType.MESSAGE &&
-            when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
-                MessageType.MSGTYPE_IMAGE -> true
-                else                      -> false
-            }
+    return when (getMsgType()) {
+        MessageType.MSGTYPE_IMAGE -> true
+        else                      -> false
+    }
 }
 
 fun Event.isVideoMessage(): Boolean {
-    return getClearType() == EventType.MESSAGE &&
-            when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
-                MessageType.MSGTYPE_VIDEO -> true
-                else                      -> false
-            }
+    return when (getMsgType()) {
+        MessageType.MSGTYPE_VIDEO -> true
+        else                      -> false
+    }
 }
 
 fun Event.isAudioMessage(): Boolean {
-    return getClearType() == EventType.MESSAGE &&
-            when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
-                MessageType.MSGTYPE_AUDIO -> true
-                else                      -> false
-            }
+    return when (getMsgType()) {
+        MessageType.MSGTYPE_AUDIO -> true
+        else                      -> false
+    }
 }
 
 fun Event.isFileMessage(): Boolean {
-    return getClearType() == EventType.MESSAGE &&
-            when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
-                MessageType.MSGTYPE_FILE -> true
-                else                     -> false
-            }
+    return when (getMsgType()) {
+        MessageType.MSGTYPE_FILE -> true
+        else                     -> false
+    }
 }
 
 fun Event.isAttachmentMessage(): Boolean {
-    return getClearType() == EventType.MESSAGE &&
-            when (getClearContent()?.get(MessageContent.MSG_TYPE_JSON_KEY)) {
-                MessageType.MSGTYPE_IMAGE,
-                MessageType.MSGTYPE_AUDIO,
-                MessageType.MSGTYPE_VIDEO,
-                MessageType.MSGTYPE_FILE -> true
-                else                     -> false
-            }
+    return when (getMsgType()) {
+        MessageType.MSGTYPE_IMAGE,
+        MessageType.MSGTYPE_AUDIO,
+        MessageType.MSGTYPE_VIDEO,
+        MessageType.MSGTYPE_FILE -> true
+        else                     -> false
+    }
 }
 
-fun Event.isPoll(): Boolean = getClearType() == EventType.POLL_START || getClearType() == EventType.POLL_END
+fun Event.isLocationMessage(): Boolean {
+    return when (getMsgType()) {
+        MessageType.MSGTYPE_LOCATION -> true
+        else                         -> false
+    }
+}
+
+fun Event.isPoll(): Boolean = getClearType() in EventType.POLL_START || getClearType() in EventType.POLL_END
 
 fun Event.isSticker(): Boolean = getClearType() == EventType.STICKER
 
@@ -372,7 +389,7 @@ fun Event.getRelationContent(): RelationDefaultContent? {
  * Returns the poll question or null otherwise
  */
 fun Event.getPollQuestion(): String? =
-        getPollContent()?.pollCreationInfo?.question?.question
+        getPollContent()?.getBestPollCreationInfo()?.question?.getBestQuestion()
 
 /**
  * Returns the relation content for a specific type or null otherwise
@@ -385,18 +402,18 @@ fun Event.isReply(): Boolean {
 }
 
 fun Event.isReplyRenderedInThread(): Boolean {
-    return isReply() && getRelationContent()?.inReplyTo?.shouldRenderInThread() == true
+    return isReply() && getRelationContent()?.shouldRenderInThread() == true
 }
 
-fun Event.isThread(): Boolean = getRelationContentForType(RelationType.IO_THREAD)?.eventId != null
+fun Event.isThread(): Boolean = getRelationContentForType(RelationType.THREAD)?.eventId != null
 
-fun Event.getRootThreadEventId(): String? = getRelationContentForType(RelationType.IO_THREAD)?.eventId
+fun Event.getRootThreadEventId(): String? = getRelationContentForType(RelationType.THREAD)?.eventId
 
 fun Event.isEdition(): Boolean {
     return getRelationContentForType(RelationType.REPLACE)?.eventId != null
 }
 
-fun Event.getPresenceContent(): PresenceContent? {
+internal fun Event.getPresenceContent(): PresenceContent? {
     return content.toModel<PresenceContent>()
 }
 
@@ -406,3 +423,6 @@ fun Event.isInvitation(): Boolean = type == EventType.STATE_ROOM_MEMBER &&
 fun Event.getPollContent(): MessagePollContent? {
     return content.toModel<MessagePollContent>()
 }
+
+fun Event.supportsNotification() =
+        this.getClearType() in EventType.MESSAGE + EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt
index 0c77b574e7266dbc71a36c9f615428c3fbed0ea2..fa3a9f6acd6cd19cd1296f8056ff04bfbc97c125 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt
@@ -49,6 +49,8 @@ object EventType {
     const val STATE_ROOM_JOIN_RULES = "m.room.join_rules"
     const val STATE_ROOM_GUEST_ACCESS = "m.room.guest_access"
     const val STATE_ROOM_POWER_LEVELS = "m.room.power_levels"
+    val STATE_ROOM_BEACON_INFO = listOf("org.matrix.msc3672.beacon_info", "m.beacon_info")
+    val BEACON_LOCATION_DATA = listOf("org.matrix.msc3672.beacon", "m.beacon")
 
     const val STATE_SPACE_CHILD = "m.space.child"
 
@@ -103,9 +105,9 @@ object EventType {
     const val REACTION = "m.reaction"
 
     // Poll
-    const val POLL_START = "org.matrix.msc3381.poll.start"
-    const val POLL_RESPONSE = "org.matrix.msc3381.poll.response"
-    const val POLL_END = "org.matrix.msc3381.poll.end"
+    val POLL_START = listOf("org.matrix.msc3381.poll.start", "m.poll.start")
+    val POLL_RESPONSE = listOf("org.matrix.msc3381.poll.response", "m.poll.response")
+    val POLL_END = listOf("org.matrix.msc3381.poll.end", "m.poll.end")
 
     // Unwedging
     internal const val DUMMY = "m.dummy"
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/LatestThreadUnsignedRelation.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/LatestThreadUnsignedRelation.kt
new file mode 100644
index 0000000000000000000000000000000000000000..cc52dfc02c1a4cf67cf5e4996d098a59f0cb5e0e
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/LatestThreadUnsignedRelation.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2022 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.api.session.events.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class LatestThreadUnsignedRelation(
+        override val limited: Boolean? = false,
+        override val count: Int? = 0,
+        @Json(name = "latest_event")
+        val event: Event? = null,
+        @Json(name = "current_user_participated")
+        val isUserParticipating: Boolean? = false
+
+) : UnsignedRelationInfo
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 fb26264ad7708992935a5fe0dbdefb363af30964..74dc74b294964e2669c558405f2bec33c59eb386 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
@@ -30,7 +30,6 @@ object RelationType {
 
     /** Lets you define an event which is a thread reply to an existing event.*/
     const val THREAD = "m.thread"
-    const val IO_THREAD = "io.element.thread"
 
     /** 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/events/model/UnsignedData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/UnsignedData.kt
index dfe1db7b1c272a1f74d474b03efc5452875f4775..630a2fb91a538473d788df6e556c18aa573b3522 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/UnsignedData.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/UnsignedData.kt
@@ -46,3 +46,5 @@ data class UnsignedData(
         @Json(name = "replaces_state") val replacesState: String? = null
 
 )
+
+fun UnsignedData?.isRedacted() = this?.redactedEvent != null
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/EncryptedEventContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/EncryptedEventContent.kt
similarity index 93%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/EncryptedEventContent.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/EncryptedEventContent.kt
index 93a6377bbbeec7a6cf1e12f505cc015544ec56d3..4f39bb61e14871cc5d79b44e94fa57b994fc6026 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/EncryptedEventContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/EncryptedEventContent.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 The Matrix.org Foundation C.I.C.
+ * Copyright (c) 2022 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.
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.matrix.android.sdk.internal.crypto.model.event
+package org.matrix.android.sdk.api.session.events.model.content
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/EncryptionEventContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/EncryptionEventContent.kt
similarity index 95%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/EncryptionEventContent.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/EncryptionEventContent.kt
index dd76ae1d8e11b1d97395d6673585a304fab70b6c..103293ba8316fc88949389b51fcb9f92a1996f92 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/EncryptionEventContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/EncryptionEventContent.kt
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.matrix.android.sdk.internal.crypto.model.event
+package org.matrix.android.sdk.api.session.events.model.content
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/OlmEventContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/OlmEventContent.kt
similarity index 94%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/OlmEventContent.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/OlmEventContent.kt
index 6fd06270226ddac6c93b37b304f723781db32c9e..b972dd20bbf622a32b86e3b2ae2559dd52452d9a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/OlmEventContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/OlmEventContent.kt
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.matrix.android.sdk.internal.crypto.model.event
+package org.matrix.android.sdk.api.session.events.model.content
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/OlmPayloadContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/OlmPayloadContent.kt
similarity index 96%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/OlmPayloadContent.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/OlmPayloadContent.kt
index 3ce9d36f90836d62c1f107bada5807b55ec9e308..6060ab5c4b258f6a023818e04014114db47ebec2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/OlmPayloadContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/OlmPayloadContent.kt
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.matrix.android.sdk.internal.crypto.model.event
+package org.matrix.android.sdk.api.session.events.model.content
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/RoomKeyContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/RoomKeyContent.kt
similarity index 90%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/RoomKeyContent.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/RoomKeyContent.kt
index 7fa0e837254609ebd85a805b8199e05663b611b1..43a47b818f6bf856adf286a7a366ff72e2990456 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/RoomKeyContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/RoomKeyContent.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 The Matrix.org Foundation C.I.C.
+ * Copyright (c) 2022 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.
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.matrix.android.sdk.internal.crypto.model.event
+package org.matrix.android.sdk.api.session.events.model.content
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/RoomKeyWithHeldContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/RoomKeyWithHeldContent.kt
similarity index 98%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/RoomKeyWithHeldContent.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/RoomKeyWithHeldContent.kt
index 4c462357dbff7c3d6ed58e2ed53ac61acc1a8d4e..a577daf9e42cf0cb2729b384eed455feea447da9 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/RoomKeyWithHeldContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/RoomKeyWithHeldContent.kt
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.matrix.android.sdk.internal.crypto.model.event
+package org.matrix.android.sdk.api.session.events.model.content
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/SecretSendEventContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/SecretSendEventContent.kt
similarity index 93%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/SecretSendEventContent.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/SecretSendEventContent.kt
index 4dcca04e94202e88d57cff12db76a99bed421016..5099aba403066b7747271e23451b61a63f17c1c7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/SecretSendEventContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/SecretSendEventContent.kt
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.matrix.android.sdk.internal.crypto.model.event
+package org.matrix.android.sdk.api.session.events.model.content
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt
index f76e4be4401647ab6d452c838c4551a3fa42f3eb..72f8019ada3c45c1273324cfdb617c9e090e9ceb 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt
@@ -17,11 +17,11 @@
 package org.matrix.android.sdk.api.session.file
 
 import android.net.Uri
+import org.matrix.android.sdk.api.session.crypto.attachments.ElementToDecrypt
+import org.matrix.android.sdk.api.session.crypto.attachments.toElementToDecrypt
 import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
 import org.matrix.android.sdk.api.session.room.model.message.getFileName
 import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
-import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
-import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
 import java.io.File
 
 /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt
index 2256dfb8f0c52d0168daebf7e03727e90a72f7ab..597c1a0ca8df0087f0b6d44a0f5eceefadb4eec6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt
@@ -50,7 +50,11 @@ data class HomeServerCapabilities(
          * This capability describes the default and available room versions a server supports, and at what level of stability.
          * Clients should make use of this capability to determine if users need to be encouraged to upgrade their rooms.
          */
-        val roomVersions: RoomVersionCapabilities? = null
+        val roomVersions: RoomVersionCapabilities? = null,
+        /**
+         * True if the home server support threading
+         */
+        val canUseThreading: Boolean = false
 ) {
 
     enum class RoomCapabilitySupport {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityService.kt
index a22cd572fa468a4b1bfba269c04095765f2025f5..fdcb30a5c826390f8a0e1b68053d6586992f1e44 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/IdentityService.kt
@@ -16,7 +16,7 @@
 
 package org.matrix.android.sdk.api.session.identity
 
-import org.matrix.android.sdk.internal.session.identity.model.SignInvitationResult
+import org.matrix.android.sdk.api.session.identity.model.SignInvitationResult
 
 /**
  * Provides access to the identity server configuration and services identity server can provide
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/SignInvitationResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/model/SignInvitationResult.kt
similarity index 64%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/SignInvitationResult.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/model/SignInvitationResult.kt
index 27a3f3209f24861c5433043e783c5da1337493e1..b1662b9cf821349678afb5b0214fa9f8336773d6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/model/SignInvitationResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/model/SignInvitationResult.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2021 The Matrix.org Foundation C.I.C.
+ * Copyright (c) 2022 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.
@@ -14,18 +14,26 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.session.identity.model
+package org.matrix.android.sdk.api.session.identity.model
 
 import com.squareup.moshi.JsonClass
 
 @JsonClass(generateAdapter = true)
 data class SignInvitationResult(
-        /** The Matrix user ID of the user accepting the invitation.*/
+        /**
+         * The Matrix user ID of the user accepting the invitation.
+         */
         val mxid: String,
-        /** The Matrix user ID of the user who sent the invitation.*/
+        /**
+         * The Matrix user ID of the user who sent the invitation.
+         */
         val sender: String,
-        /**The token from the call to store- invite..*/
+        /**
+         * The token from the call to store- invite..
+         */
         val signatures: Map<String, *>,
-        /** The token for the invitation */
+        /**
+         * The token for the invitation
+         */
         val token: String
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/initsync/SyncStatusService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/initsync/SyncStatusService.kt
index daab6d976114925ff07cb792ff849367eb171790..267436916ec33c27dd087fe9f6e6e9f83d5f7a26 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/initsync/SyncStatusService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/initsync/SyncStatusService.kt
@@ -28,7 +28,7 @@ interface SyncStatusService {
         abstract class InitialSyncStatus : Status()
 
         object Idle : InitialSyncStatus()
-        data class Progressing(
+        data class InitialSyncProgressing(
                 val initSyncStep: InitSyncStep,
                 val percentProgress: Int = 0
         ) : InitialSyncStatus()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt
index 920dc85c7a066d1d5a02381d3bf8b906a79d596c..c139da813ab75484865f9540314de811e48b718a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt
@@ -28,6 +28,11 @@ interface PermalinkService {
         const val MATRIX_TO_URL_BASE = "https://matrix.to/#/"
     }
 
+    enum class SpanTemplateType {
+        HTML,
+        MARKDOWN
+    }
+
     /**
      * Creates a permalink for an event.
      * Ex: "https://matrix.to/#/!nbzmcXAqpxBXjAdgoX:matrix.org/$1531497316352799BevdV:matrix.org"
@@ -80,4 +85,15 @@ interface PermalinkService {
      * @return the id from the url, ex: "@benoit:matrix.org", or null if the url is not a permalink
      */
     fun getLinkedId(url: String): String?
+
+    /**
+     * Creates a HTML or Markdown mention span template. Can be used to replace a mention with a permalink to mentioned user.
+     * Ex: "<a href=\"https://matrix.to/#/%1\$s\">%2\$s</a>" or "[%2\$s](https://matrix.to/#/%1\$s)"
+     *
+     * @param type: type of template to create
+     * @param forceMatrixTo whether we should force using matrix.to base URL
+     *
+     * @return the created template
+     */
+    fun createMentionSpanTemplate(type: SpanTemplateType, forceMatrixTo: Boolean = false): String
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt
index 05fa24946a3fe21f61cfb993f5662e9df48ea29d..d2c677bb31425be809668548914608b3334a425a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt
@@ -21,6 +21,7 @@ import android.net.Uri
 import androidx.lifecycle.LiveData
 import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
 import org.matrix.android.sdk.api.session.identity.ThreePid
+import org.matrix.android.sdk.api.session.user.model.User
 import org.matrix.android.sdk.api.util.JsonDict
 import org.matrix.android.sdk.api.util.Optional
 
@@ -118,4 +119,17 @@ interface ProfileService {
      * Remove a 3Pid from the Matrix account.
      */
     suspend fun deleteThreePid(threePid: ThreePid)
+
+    /**
+     * Return a User object from a userId
+     */
+    suspend fun getProfileAsUser(userId: String): User {
+        return getProfile(userId).let { dict ->
+            User(
+                    userId = userId,
+                    displayName = dict[DISPLAY_NAME_KEY] as? String,
+                    avatarUrl = dict[AVATAR_URL_KEY] as? String
+            )
+        }
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt
index d930a5d0fd6cb4d392c2097b8ef7b1afa9bdb141..be65b883b36238509027a6398e61652138232e7f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt
@@ -33,6 +33,7 @@ import org.matrix.android.sdk.api.session.room.send.SendService
 import org.matrix.android.sdk.api.session.room.state.StateService
 import org.matrix.android.sdk.api.session.room.tags.TagsService
 import org.matrix.android.sdk.api.session.room.threads.ThreadsService
+import org.matrix.android.sdk.api.session.room.threads.local.ThreadsLocalService
 import org.matrix.android.sdk.api.session.room.timeline.TimelineService
 import org.matrix.android.sdk.api.session.room.typing.TypingService
 import org.matrix.android.sdk.api.session.room.uploads.UploadsService
@@ -47,6 +48,7 @@ import org.matrix.android.sdk.api.util.Optional
 interface Room :
         TimelineService,
         ThreadsService,
+        ThreadsLocalService,
         SendService,
         DraftService,
         ReadService,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt
index bca432320d92d456095240ef912163a392f30680..700e292b0c16fc7b26e93c899c73b5f4f270e7aa 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt
@@ -19,6 +19,8 @@ package org.matrix.android.sdk.api.session.room
 import androidx.lifecycle.LiveData
 import androidx.paging.PagedList
 import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.identity.model.SignInvitationResult
+import org.matrix.android.sdk.api.session.room.alias.RoomAliasDescription
 import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
 import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
@@ -27,8 +29,6 @@ import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
 import org.matrix.android.sdk.api.session.room.peeking.PeekResult
 import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
 import org.matrix.android.sdk.api.util.Optional
-import org.matrix.android.sdk.internal.session.identity.model.SignInvitationResult
-import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription
 
 /**
  * This interface defines methods to get rooms. It's implemented at the session level.
@@ -216,6 +216,12 @@ interface RoomService {
                                           pagedListConfig: PagedList.Config = defaultPagedListConfig,
                                           sortOrder: RoomSortOrder = RoomSortOrder.ACTIVITY): UpdatableLivePageResult
 
+    /**
+     * Return a LiveData on the number of rooms
+     * @param queryParams parameters to query the room summaries. It can be use to keep only joined rooms, for instance.
+     */
+    fun getRoomCountLive(queryParams: RoomSummaryQueryParams): LiveData<Int>
+
     /**
      * TODO Doc
      */
@@ -236,4 +242,12 @@ interface RoomService {
      */
     fun getFlattenRoomSummaryChildrenOfLive(spaceId: String?,
                                             memberships: List<Membership> = Membership.activeMemberships()): LiveData<List<RoomSummary>>
+
+    /**
+     * Refreshes the RoomSummary LatestPreviewContent for the given @param roomId
+     * If the roomId is null, all rooms are updated
+     *
+     * This is useful for refreshing summary content with encrypted messages after receiving new room keys
+     */
+    fun refreshJoinedRoomSummaryPreviews(roomId: String?)
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableLivePageResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableLivePageResult.kt
index b83f57f5ef83ac73626fc8d19118f81eb96eb151..db87f913b93df9975c027dbe93a398905c9c6871 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableLivePageResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableLivePageResult.kt
@@ -22,10 +22,8 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary
 
 interface UpdatableLivePageResult {
     val livePagedList: LiveData<PagedList<RoomSummary>>
-
-    fun updateQuery(builder: (RoomSummaryQueryParams) -> RoomSummaryQueryParams)
-
     val liveBoundaries: LiveData<ResultBoundaries>
+    var queryParams: RoomSummaryQueryParams
 }
 
 data class ResultBoundaries(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasDescription.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/alias/RoomAliasDescription.kt
similarity index 94%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasDescription.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/alias/RoomAliasDescription.kt
index d1f93c50bef289ef1ee7d6c1a5e465e26d2388f9..ce7b03d35b81e89103fc2ccfb214f4efcb4f8e02 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasDescription.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/alias/RoomAliasDescription.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.session.room.alias
+package org.matrix.android.sdk.api.session.room.alias
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt
index 445d16b72bcd2e6e20588da45562eb44506e7bcf..6967e0c455026268851987b57839bd86c9f2cea5 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/crypto/RoomCryptoService.kt
@@ -16,7 +16,7 @@
 
 package org.matrix.android.sdk.api.session.room.crypto
 
-import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
+import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
 
 interface RoomCryptoService {
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EventAnnotationsSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EventAnnotationsSummary.kt
index 3a4912e457cdee9aaef4cea638f84374a3461ae3..0238eb6c8dee4dbdd93c46efba0df588986fe517 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EventAnnotationsSummary.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/EventAnnotationsSummary.kt
@@ -16,9 +16,9 @@
 package org.matrix.android.sdk.api.session.room.model
 
 data class EventAnnotationsSummary(
-        var eventId: String,
-        var reactionsSummary: List<ReactionAggregatedSummary> = emptyList(),
-        var editSummary: EditAggregatedSummary? = null,
-        var pollResponseSummary: PollResponseAggregatedSummary? = null,
-        var referencesAggregatedSummary: ReferencesAggregatedSummary? = null
+        val eventId: String,
+        val reactionsSummary: List<ReactionAggregatedSummary> = emptyList(),
+        val editSummary: EditAggregatedSummary? = null,
+        val pollResponseSummary: PollResponseAggregatedSummary? = null,
+        val referencesAggregatedSummary: ReferencesAggregatedSummary? = null
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollResponseAggregatedSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollResponseAggregatedSummary.kt
index a15d8be084945487c0ebd405904262218376ea8b..b16852e47d151604516a49729389aea682fa039c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollResponseAggregatedSummary.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollResponseAggregatedSummary.kt
@@ -16,13 +16,11 @@
 package org.matrix.android.sdk.api.session.room.model
 
 data class PollResponseAggregatedSummary(
-
-        var aggregatedContent: PollSummaryContent? = null,
-
+        val aggregatedContent: PollSummaryContent? = null,
         // If set the poll is closed (Clients SHOULD NOT consider responses after the close event)
-        var closedTime: Long? = null,
+        val closedTime: Long? = null,
         // Clients SHOULD validate that the option in the relationship is a valid option, and ignore the response if invalid
-        var nbOptions: Int = 0,
+        val nbOptions: Int = 0,
         // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk)
         val sourceEvents: List<String>,
         val localEchos: List<String>
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollSummaryContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollSummaryContent.kt
index f1e4354314d6cd402e9c94527668fbe14eabdb59..09458ff12eaeee78eb5e2dc8c1fa11b254659500 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollSummaryContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollSummaryContent.kt
@@ -24,13 +24,13 @@ import com.squareup.moshi.JsonClass
  */
 @JsonClass(generateAdapter = true)
 data class PollSummaryContent(
-        var myVote: String? = null,
-        // Array of VoteInfo, list is constructed so that there is only one vote by user
+        val myVote: String? = null,
+        // List of VoteInfo, list is constructed so that there is only one vote by user
         // And that optionIndex is valid
-        var votes: List<VoteInfo>? = null,
-        var votesSummary: Map<String, VoteSummary>? = null,
-        var totalVotes: Int = 0,
-        var winnerVoteCount: Int = 0
+        val votes: List<VoteInfo>? = null,
+        val votesSummary: Map<String, VoteSummary>? = null,
+        val totalVotes: Int = 0,
+        val winnerVoteCount: Int = 0
 )
 
 @JsonClass(generateAdapter = true)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReferencesAggregatedContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReferencesAggregatedContent.kt
index 664d042e186f482774af3109af410fa078481076..c6b94c5dd15f4edfc4dd22c20566ee7ff490d6b8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReferencesAggregatedContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/ReferencesAggregatedContent.kt
@@ -17,7 +17,7 @@ package org.matrix.android.sdk.api.session.room.model
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
-import org.matrix.android.sdk.api.crypto.VerificationState
+import org.matrix.android.sdk.api.session.crypto.verification.VerificationState
 
 /**
  * Contains an aggregated summary info of the references.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomEncryptionAlgorithm.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomEncryptionAlgorithm.kt
index f68121692944c1d018a160f5dc3cad526210b011..5f728a4ed4494dc7a3723fbbc48f85c722978fa7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomEncryptionAlgorithm.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomEncryptionAlgorithm.kt
@@ -16,7 +16,7 @@
 
 package org.matrix.android.sdk.api.session.room.model
 
-import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
+import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
 
 sealed class RoomEncryptionAlgorithm {
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt
index c793a04f9de476581022c93a0fda03bd2efadc49..71c1d8303e5fe567e125d7a17e1747e45ca99d23 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt
@@ -16,7 +16,7 @@
 
 package org.matrix.android.sdk.api.session.room.model
 
-import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
+import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
 import org.matrix.android.sdk.api.session.presence.model.UserPresence
 import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
 import org.matrix.android.sdk.api.session.room.send.UserDraft
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt
index 566790600036cd34616faeea7261b9148e367e4b..ce1e0e0d144493ab328ee054673f05ff6bac54f2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt
@@ -17,12 +17,12 @@
 package org.matrix.android.sdk.api.session.room.model.create
 
 import android.net.Uri
+import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
 import org.matrix.android.sdk.api.session.identity.ThreePid
 import org.matrix.android.sdk.api.session.room.model.GuestAccess
 import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
 import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
 import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
-import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
 
 open class CreateRoomParams {
     /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeyBackupVersionTrust.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/BeaconInfo.kt
similarity index 56%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeyBackupVersionTrust.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/BeaconInfo.kt
index 07ca87fe33b3a7e66be767e3c27d9d1e87904ee6..873edc0f1fd3ac2976021ea87351039792b8f3e1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeyBackupVersionTrust.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/BeaconInfo.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 The Matrix.org Foundation C.I.C.
+ * Copyright 2022 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.
@@ -14,23 +14,20 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.crypto.keysbackup.model
+package org.matrix.android.sdk.api.session.room.model.livelocation
 
+import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
 
-/**
- * Data model for response to [KeysBackup.isKeyBackupTrusted()].
- */
 @JsonClass(generateAdapter = true)
-data class KeyBackupVersionTrust(
+data class BeaconInfo(
+        @Json(name = "description") val description: String? = null,
         /**
-         * Flag to indicate if the backup is trusted.
-         * true if there is a signature that is valid & from a trusted device.
+         * Beacon should be considered as inactive after this timeout as milliseconds.
          */
-        var usable: Boolean = false,
-
+        @Json(name = "timeout") val timeout: Long? = null,
         /**
-         * Signatures found in the backup version.
+         * Should be set true to start sharing beacon.
          */
-        var signatures: MutableList<KeyBackupVersionTrustSignature> = ArrayList()
+        @Json(name = "live") val isLive: Boolean? = null
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationBeaconContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationBeaconContent.kt
new file mode 100644
index 0000000000000000000000000000000000000000..106e76eafd08889017852326406aedd4738ab502
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/livelocation/LiveLocationBeaconContent.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2022 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.api.session.room.model.livelocation
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.session.events.model.Content
+import org.matrix.android.sdk.api.session.room.model.message.LocationAsset
+import org.matrix.android.sdk.api.session.room.model.message.LocationAssetType
+import org.matrix.android.sdk.api.session.room.model.message.MessageContent
+import org.matrix.android.sdk.api.session.room.model.message.MessageLiveLocationContent
+import org.matrix.android.sdk.api.session.room.model.message.MessageType
+import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
+
+@JsonClass(generateAdapter = true)
+data class LiveLocationBeaconContent(
+        /**
+         * Local message type, not from server
+         */
+        @Transient
+        override val msgType: String = MessageType.MSGTYPE_LIVE_LOCATION_STATE,
+
+        @Json(name = "body") override val body: String = "",
+        @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
+        @Json(name = "m.new_content") override val newContent: Content? = null,
+
+        /**
+         * Indicates user's intent to share ephemeral location.
+         */
+        @Json(name = "org.matrix.msc3672.beacon_info") val unstableBeaconInfo: BeaconInfo? = null,
+        @Json(name = "m.beacon_info") val beaconInfo: BeaconInfo? = null,
+        /**
+         * Beacon creation timestamp.
+         */
+        @Json(name = "org.matrix.msc3488.ts") val unstableTimestampAsMilliseconds: Long? = null,
+        @Json(name = "m.ts") val timestampAsMilliseconds: Long? = null,
+        /**
+         * Live location asset type.
+         */
+        @Json(name = "org.matrix.msc3488.asset") val unstableLocationAsset: LocationAsset = LocationAsset(LocationAssetType.SELF),
+        @Json(name = "m.asset") val locationAsset: LocationAsset? = null,
+
+        /**
+         * Client side tracking of the last location
+         */
+        var lastLocationContent: MessageLiveLocationContent? = null,
+
+        /**
+         * Client side tracking of whether the beacon has timed out.
+         */
+        var hasTimedOut: Boolean = false
+) : MessageContent {
+
+    fun getBestBeaconInfo() = beaconInfo ?: unstableBeaconInfo
+
+    fun getBestTimestampAsMilliseconds() = timestampAsMilliseconds ?: unstableTimestampAsMilliseconds
+
+    fun getBestLocationAsset() = locationAsset ?: unstableLocationAsset
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/FileInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/FileInfo.kt
index f21074096eff00850147cd62bfcb10e9bc830585..fa18bfd21f79485bc05a3baaf397a444b6b7752a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/FileInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/FileInfo.kt
@@ -18,7 +18,7 @@ package org.matrix.android.sdk.api.session.room.model.message
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
-import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
+import org.matrix.android.sdk.api.session.crypto.model.EncryptedFileInfo
 
 @JsonClass(generateAdapter = true)
 data class FileInfo(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ImageInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ImageInfo.kt
index c38ef5bc276839509783cd431580b48b38f4d633..0099208320ee6f28a54c45bf3a3c64d3b173d573 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ImageInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ImageInfo.kt
@@ -18,7 +18,7 @@ package org.matrix.android.sdk.api.session.room.model.message
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
-import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
+import org.matrix.android.sdk.api.session.crypto.model.EncryptedFileInfo
 
 @JsonClass(generateAdapter = true)
 data class ImageInfo(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAsset.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAsset.kt
index e8b3cf2488860a0bc6019d80db8844f771069546..35fa555a5bab38c4bf210c6c61dbd4aa6b411c1b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAsset.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAsset.kt
@@ -21,5 +21,5 @@ import com.squareup.moshi.JsonClass
 
 @JsonClass(generateAdapter = true)
 data class LocationAsset(
-        @Json(name = "type") val type: LocationAssetType? = null
+        @Json(name = "type") val type: String? = null
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAssetType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAssetType.kt
index ef40e21c47f582889fd015801131a937be268e20..f7d82d4b40909d1f59e3637e128a417191ecc883 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAssetType.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAssetType.kt
@@ -16,11 +16,20 @@
 
 package org.matrix.android.sdk.api.session.room.model.message
 
-import com.squareup.moshi.Json
-import com.squareup.moshi.JsonClass
+/**
+ * Define what particular asset is being referred to.
+ * We don't use enum type since it is not limited to a specific set of values.
+ * The way this type should be interpreted in client side is described in
+ * [MSC3488](https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md)
+ */
+object LocationAssetType {
+    /**
+     * Used for user location sharing.
+     **/
+    const val SELF = "m.self"
 
-@JsonClass(generateAdapter = false)
-enum class LocationAssetType {
-    @Json(name = "m.self")
-    SELF
+    /**
+     * Used for pin drop location sharing.
+     **/
+    const val PIN = "m.pin"
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioContent.kt
index ebf3d127ce48774b1ac12a5796df9b494ff21096..76a612b51ba4371f09d223ec800de561d1250133 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioContent.kt
@@ -18,10 +18,10 @@ package org.matrix.android.sdk.api.session.room.model.message
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.session.crypto.model.EncryptedFileInfo
 import org.matrix.android.sdk.api.session.events.model.Content
 import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
 import org.matrix.android.sdk.api.util.JsonDict
-import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
 
 @JsonClass(generateAdapter = true)
 data class MessageAudioContent(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageFileContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageFileContent.kt
index 78f9a5d2f2d2190b47c6e7e5bb51f882131332cf..b5303e6c5d71765b8497a40b595cf74b9571dab2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageFileContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageFileContent.kt
@@ -19,9 +19,9 @@ package org.matrix.android.sdk.api.session.room.model.message
 import android.webkit.MimeTypeMap
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.session.crypto.model.EncryptedFileInfo
 import org.matrix.android.sdk.api.session.events.model.Content
 import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
-import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
 
 @JsonClass(generateAdapter = true)
 data class MessageFileContent(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageImageContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageImageContent.kt
index ea7ab5068876785978d24db22534849fd21d35a5..f0caf52041cc7799541237c93ebce90f12411008 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageImageContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageImageContent.kt
@@ -18,9 +18,9 @@ package org.matrix.android.sdk.api.session.room.model.message
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.session.crypto.model.EncryptedFileInfo
 import org.matrix.android.sdk.api.session.events.model.Content
 import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
-import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
 
 @JsonClass(generateAdapter = true)
 data class MessageImageContent(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLiveLocationContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLiveLocationContent.kt
new file mode 100644
index 0000000000000000000000000000000000000000..548dc8536944d16e4d84f3187104840b704d37d8
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLiveLocationContent.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2022 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.api.session.room.model.message
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.session.events.model.Content
+import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
+
+@JsonClass(generateAdapter = true)
+data class MessageLiveLocationContent(
+        /**
+         * Local message type, not from server
+         */
+        @Transient
+        override val msgType: String = MessageType.MSGTYPE_LIVE_LOCATION,
+
+        @Json(name = "body") override val body: String = "",
+        @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
+        @Json(name = "m.new_content") override val newContent: Content? = null,
+
+        /**
+         * See [MSC3488](https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md)
+         */
+        @Json(name = "org.matrix.msc3488.location") val unstableLocationInfo: LocationInfo? = null,
+        @Json(name = "m.location") val locationInfo: LocationInfo? = null,
+
+        /**
+         * Exact time that the data in the event refers to (milliseconds since the UNIX epoch)
+         */
+        @Json(name = "org.matrix.msc3488.ts") val unstableTimestampAsMilliseconds: Long? = null,
+        @Json(name = "m.ts") val timestampAsMilliseconds: Long? = null
+) : MessageContent {
+
+    fun getBestLocationInfo() = locationInfo ?: unstableLocationInfo
+
+    fun getBestTimestampAsMilliseconds() = timestampAsMilliseconds ?: unstableTimestampAsMilliseconds
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt
index d07bd2d73ae5df8f6f8ad1e853f11881cbd26636..2052133b068cf360d7da16ae60ae7fb4f758edce 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt
@@ -39,37 +39,47 @@ data class MessageLocationContent(
          */
         @Json(name = "geo_uri") val geoUri: String,
 
-        /**
-         * See https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md
-         */
-        @Json(name = "org.matrix.msc3488.location") val locationInfo: LocationInfo? = null,
-
         @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
         @Json(name = "m.new_content") override val newContent: Content? = null,
-
         /**
-         * m.asset defines a generic asset that can be used for location tracking but also in other places like
+         * See [MSC3488](https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md)
+         */
+        @Json(name = "org.matrix.msc3488.location") val unstableLocationInfo: LocationInfo? = null,
+        @Json(name = "m.location") val locationInfo: LocationInfo? = null,
+        /**
+         * Exact time that the data in the event refers to (milliseconds since the UNIX epoch)
+         */
+        @Json(name = "org.matrix.msc3488.ts") val unstableTs: Long? = null,
+        @Json(name = "m.ts") val ts: Long? = null,
+        @Json(name = "org.matrix.msc1767.text") val unstableText: String? = null,
+        @Json(name = "m.text") val text: String? = null,
+        /**
+         * Defines a generic asset that can be used for location tracking but also in other places like
          * inventories, geofencing, checkins/checkouts etc.
          * It should contain a mandatory namespaced type key defining what particular asset is being referred to.
          * For the purposes of user location tracking m.self should be used in order to avoid duplicating the mxid.
+         * See [MSC3488](https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md)
          */
-        @Json(name = "m.asset") val locationAsset: LocationAsset? = null,
+        @Json(name = "org.matrix.msc3488.asset") val unstableLocationAsset: LocationAsset? = null,
+        @Json(name = "m.asset") val locationAsset: LocationAsset? = null
+) : MessageContent {
 
-        /**
-         * Exact time that the data in the event refers to (milliseconds since the UNIX epoch)
-         */
-        @Json(name = "org.matrix.msc3488.ts") val ts: Long? = null,
+    fun getBestLocationInfo() = locationInfo ?: unstableLocationInfo
 
-        @Json(name = "org.matrix.msc1767.text") val text: String? = null
-) : MessageContent {
+    fun getBestTs() = ts ?: unstableTs
+
+    fun getBestText() = text ?: unstableText
+
+    fun getBestLocationAsset() = locationAsset ?: unstableLocationAsset
 
-    fun getBestGeoUri() = locationInfo?.geoUri ?: geoUri
+    fun getBestGeoUri() = getBestLocationInfo()?.geoUri ?: geoUri
 
     /**
      * @return true if the location asset is a user location, not a generic one.
      */
     fun isSelfLocation(): Boolean {
         // Should behave like m.self if locationAsset is null
+        val locationAsset = getBestLocationAsset()
         return locationAsset?.type == null || locationAsset.type == LocationAssetType.SELF
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollContent.kt
index a4e131729066227a6655dd9f364ee333660ec94a..43c0c9006818342e9c4f531936df68299eba1688 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollContent.kt
@@ -31,5 +31,9 @@ data class MessagePollContent(
         @Json(name = "body") override val body: String = "",
         @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
         @Json(name = "m.new_content") override val newContent: Content? = null,
-        @Json(name = "org.matrix.msc3381.poll.start") val pollCreationInfo: PollCreationInfo? = null
-) : MessageContent
+        @Json(name = "org.matrix.msc3381.poll.start") val unstablePollCreationInfo: PollCreationInfo? = null,
+        @Json(name = "m.poll.start") val pollCreationInfo: PollCreationInfo? = null
+) : MessageContent {
+
+    fun getBestPollCreationInfo() = pollCreationInfo ?: unstablePollCreationInfo
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollResponseContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollResponseContent.kt
index f3b4e3dc23c1d1e8f0b9b1cdce9e59ed7f0ee415..022915ed695d1a4cc1d88c4447f57333d0b436b3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollResponseContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollResponseContent.kt
@@ -31,5 +31,9 @@ data class MessagePollResponseContent(
         @Json(name = "body") override val body: String = "",
         @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
         @Json(name = "m.new_content") override val newContent: Content? = null,
-        @Json(name = "org.matrix.msc3381.poll.response") val response: PollResponse? = null
-) : MessageContent
+        @Json(name = "org.matrix.msc3381.poll.response") val unstableResponse: PollResponse? = null,
+        @Json(name = "m.response") val response: PollResponse? = null
+) : MessageContent {
+
+    fun getBestResponse() = response ?: unstableResponse
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageStickerContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageStickerContent.kt
index 8e1d4d3d7589f286643740faddec1b1220720f04..3d774cadb292591d62e137a0827be277b5d49932 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageStickerContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageStickerContent.kt
@@ -18,9 +18,9 @@ package org.matrix.android.sdk.api.session.room.model.message
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.session.crypto.model.EncryptedFileInfo
 import org.matrix.android.sdk.api.session.events.model.Content
 import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
-import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
 
 @JsonClass(generateAdapter = true)
 data class MessageStickerContent(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt
index 2a6138ae60ca4fb0c10855ad9aa6d0a1fec19aa0..106bf2e03085fa4847ee829df59caf3a293616bb 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt
@@ -33,10 +33,14 @@ object MessageType {
     const val MSGTYPE_STICKER_LOCAL = "org.matrix.android.sdk.sticker"
 
     // Fake message types for poll events to be able to inherit them from MessageContent
-    // Because poll events are not message events and they don't hanve msgtype field
+    // Because poll events are not message events and they don't have msgtype field
     const val MSGTYPE_POLL_START = "org.matrix.android.sdk.poll.start"
     const val MSGTYPE_POLL_RESPONSE = "org.matrix.android.sdk.poll.response"
 
     const val MSGTYPE_CONFETTI = "nic.custom.confetti"
     const val MSGTYPE_SNOWFALL = "io.element.effect.snowfall"
+
+    // Fake message types for live location events to be able to inherit them from MessageContent
+    const val MSGTYPE_LIVE_LOCATION_STATE = "org.matrix.android.sdk.livelocation.state"
+    const val MSGTYPE_LIVE_LOCATION = "org.matrix.android.sdk.livelocation"
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVideoContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVideoContent.kt
index e1b0cd860741a7ef2036dabd15b6d46d76bc8439..9266a0fb0f368e18a2ed87b67c681e8b2250ada4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVideoContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageVideoContent.kt
@@ -18,9 +18,9 @@ package org.matrix.android.sdk.api.session.room.model.message
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.session.crypto.model.EncryptedFileInfo
 import org.matrix.android.sdk.api.session.events.model.Content
 import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
-import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
 
 @JsonClass(generateAdapter = true)
 data class MessageVideoContent(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageWithAttachmentContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageWithAttachmentContent.kt
index 7870db4f65aebe4fb2fdbb890570be7b63780de7..95dfb6b8648be044e0ce439768176fe56cc85511 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageWithAttachmentContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageWithAttachmentContent.kt
@@ -16,7 +16,7 @@
 
 package org.matrix.android.sdk.api.session.room.model.message
 
-import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
+import org.matrix.android.sdk.api.session.crypto.model.EncryptedFileInfo
 
 /**
  * Interface for message which can contains an encrypted file
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollAnswer.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollAnswer.kt
index 8f5ff53c85cf8329e996c7d400a895327563701f..34614d9d15d6b066a83740435bb74ef973ce07f0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollAnswer.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollAnswer.kt
@@ -22,5 +22,9 @@ import com.squareup.moshi.JsonClass
 @JsonClass(generateAdapter = true)
 data class PollAnswer(
         @Json(name = "id") val id: String? = null,
-        @Json(name = "org.matrix.msc1767.text") val answer: String? = null
-)
+        @Json(name = "org.matrix.msc1767.text") val unstableAnswer: String? = null,
+        @Json(name = "m.text") val answer: String? = null
+) {
+
+    fun getBestAnswer() = answer ?: unstableAnswer
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt
index a82c01b15989bbd5425a3e89085ec18d108fd81a..81b034a809b3debb3dd2faf8d13b241b0ae36c6b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt
@@ -21,8 +21,8 @@ import com.squareup.moshi.JsonClass
 
 @JsonClass(generateAdapter = true)
 data class PollCreationInfo(
-    @Json(name = "question") val question: PollQuestion? = null,
-    @Json(name = "kind") val kind: PollType? = PollType.DISCLOSED,
-    @Json(name = "max_selections") val maxSelections: Int = 1,
-    @Json(name = "answers") val answers: List<PollAnswer>? = null
+        @Json(name = "question") val question: PollQuestion? = null,
+        @Json(name = "kind") val kind: PollType? = PollType.DISCLOSED_UNSTABLE,
+        @Json(name = "max_selections") val maxSelections: Int = 1,
+        @Json(name = "answers") val answers: List<PollAnswer>? = null
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollQuestion.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollQuestion.kt
index 76025f745e6346cac622dbca8a429c2a530ab8d3..df9517892b18e2c9ee11f969ab2aba16d7b4264d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollQuestion.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollQuestion.kt
@@ -21,5 +21,9 @@ import com.squareup.moshi.JsonClass
 
 @JsonClass(generateAdapter = true)
 data class PollQuestion(
-        @Json(name = "org.matrix.msc1767.text") val question: String? = null
-)
+        @Json(name = "org.matrix.msc1767.text") val unstableQuestion: String? = null,
+        @Json(name = "m.text") val question: String? = null
+) {
+
+    fun getBestQuestion() = question ?: unstableQuestion
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollType.kt
index 3a8066b9bc10caea1f2ff1893ee070eb360a149a..54801e698dbdb3a25e8f9c0e0461d614c92c654a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollType.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollType.kt
@@ -25,11 +25,17 @@ enum class PollType {
      * Voters should see results as soon as they have voted.
      */
     @Json(name = "org.matrix.msc3381.poll.disclosed")
+    DISCLOSED_UNSTABLE,
+
+    @Json(name = "m.poll.disclosed")
     DISCLOSED,
 
     /**
      * Results should be only revealed when the poll is ended.
      */
     @Json(name = "org.matrix.msc3381.poll.undisclosed")
+    UNDISCLOSED_UNSTABLE,
+
+    @Json(name = "m.poll.undisclosed")
     UNDISCLOSED
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/VideoInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/VideoInfo.kt
index 8a36c26313d1d00ae7a0e2e9b7a4a19ad6e83f3c..28f3a47d114ef7986ab5207161624ffddfc72d6b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/VideoInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/VideoInfo.kt
@@ -18,7 +18,7 @@ package org.matrix.android.sdk.api.session.room.model.message
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
-import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
+import org.matrix.android.sdk.api.session.crypto.model.EncryptedFileInfo
 
 @JsonClass(generateAdapter = true)
 data class VideoInfo(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionInfo.kt
index 733d6c37e86d977c997e6d48c9b58682c0979cef..e7bebeeff66afa5e429186f6658875f381ff58f8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReactionInfo.kt
@@ -26,5 +26,6 @@ data class ReactionInfo(
         @Json(name = "key") val key: String,
         // always null for reaction
         @Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null,
-        @Json(name = "option") override val option: Int? = null
+        @Json(name = "option") override val option: Int? = null,
+        @Json(name = "is_falling_back") override val isFallingBack: Boolean? = null
 ) : RelationContent
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationContent.kt
index e2080bb4376fb24dcd07bd7555d6330415ceb93e..53b1fea873d5b43dd936a7b88c74dd528e9246b0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationContent.kt
@@ -24,4 +24,10 @@ interface RelationContent {
     val eventId: String?
     val inReplyTo: ReplyToContent?
     val option: Int?
+
+    /**
+     * This flag indicates that the message should be rendered as a reply
+     * fallback, when isFallingBack = false
+     */
+    val isFallingBack: Boolean?
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt
index 10b071a6013cc58a63fcea6e1e48d1db10ac05ec..5dcb1b4323bcf1d2edc803dafdae8cf0750d2971 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationDefaultContent.kt
@@ -23,5 +23,8 @@ data class RelationDefaultContent(
         @Json(name = "rel_type") override val type: String?,
         @Json(name = "event_id") override val eventId: String?,
         @Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null,
-        @Json(name = "option") override val option: Int? = null
+        @Json(name = "option") override val option: Int? = null,
+        @Json(name = "is_falling_back") override val isFallingBack: Boolean? = null
 ) : RelationContent
+
+fun RelationDefaultContent.shouldRenderInThread(): Boolean = isFallingBack == false
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt
index 09114436f04075cd9401717c4af0bd6a39e7961b..44098989084eb4d43435fc9f65960a93bd7ab0e4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt
@@ -163,13 +163,4 @@ interface RelationService {
                       autoMarkdown: Boolean = false,
                       formattedText: String? = null,
                       eventReplied: TimelineEvent? = null): Cancelable?
-
-    /**
-     * Get all the thread replies for the specified rootThreadEventId
-     * The return list will contain the original root thread event and all the thread replies to that event
-     * Note: We will use a large limit value in order to avoid using pagination until it would be 100% ready
-     * from the backend
-     * @param rootThreadEventId the root thread eventId
-     */
-    suspend fun fetchThreadTimeline(rootThreadEventId: String): Boolean
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReplyToContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReplyToContent.kt
index 412a1bfca9dcec11ab88a5d97520cc8f4aecc974..251328bea210065be3c79c58633453f935899f78 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReplyToContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/ReplyToContent.kt
@@ -21,8 +21,5 @@ import com.squareup.moshi.JsonClass
 
 @JsonClass(generateAdapter = true)
 data class ReplyToContent(
-        @Json(name = "event_id") val eventId: String? = null,
-        @Json(name = "render_in") val renderIn: List<String>? = null
+        @Json(name = "event_id") val eventId: String? = null
 )
-
-fun ReplyToContent.shouldRenderInThread(): Boolean = renderIn?.contains("m.thread") == true
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
index 913dbfd01096adf5f93ffbaa3751e4fd705a6206..af7ab11df13452e52a9cf7ca9560e862dae3a6b5 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
@@ -142,8 +142,18 @@ interface SendService {
      * @param latitude required latitude of the location
      * @param longitude required longitude of the location
      * @param uncertainty Accuracy of the location in meters
+     * @param isUserLocation indicates whether the location data corresponds to the user location or not
      */
-    fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?): Cancelable
+    fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?, isUserLocation: Boolean): Cancelable
+
+    /**
+     * Send a live location event to the room. beacon_info state event has to be sent before sending live location updates.
+     * @param beaconInfoEventId event id of the initial beacon info state event
+     * @param latitude required latitude of the location
+     * @param longitude required longitude of the location
+     * @param uncertainty Accuracy of the location in meters
+     */
+    fun sendLiveLocation(beaconInfoEventId: String, latitude: Double, longitude: Double, uncertainty: Double?): Cancelable
 
     /**
      * Remove this failed message from the timeline
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt
index e9b0e4f6760c156503d70e40014373ccb6c5c97a..f645f3ebf9cb94d021091995def4beaa9b8cb412 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt
@@ -66,6 +66,19 @@ interface StateService {
      */
     suspend fun deleteAvatar()
 
+    /**
+     * Stops sharing live location in the room
+     * @param userId user id
+     */
+    suspend fun stopLiveLocation(userId: String)
+
+    /**
+     * Returns beacon info state event of a user
+     * @param userId user id who is sharing location
+     * @param filterOnlyLive filters only ongoing live location sharing beacons if true else ended event is included
+     */
+    suspend fun getLiveLocationBeaconInfo(userId: String, filterOnlyLive: Boolean): Event?
+
     /**
      * Send a state event to the room
      * @param eventType The type of event to send.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt
index 3bba2deae53019430464d9c80d81f1aad9186c7a..8f214e0f89433ef2b5f2b104d192dde35f7fb3f2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomSummaryConstants.kt
@@ -32,7 +32,6 @@ object RoomSummaryConstants {
             EventType.CALL_ANSWER,
             EventType.ENCRYPTED,
             EventType.STICKER,
-            EventType.REACTION,
-            EventType.POLL_START
-    )
+            EventType.REACTION
+    ) + EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/taggedevents/TaggedEventsContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/taggedevents/TaggedEventsContent.kt
similarity index 97%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/taggedevents/TaggedEventsContent.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/taggedevents/TaggedEventsContent.kt
index 1b19d27e1d4094ae9ae65a837d951759fdd4ed3d..521a2315e47effc9c8fa4398c5364707657d3351 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/taggedevents/TaggedEventsContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/taggedevents/TaggedEventsContent.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.session.room.taggedevents
+package org.matrix.android.sdk.api.session.room.taggedevents
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt
index e4d1d979e1a8f8c61b55fa8afc5a99b58ee8a040..839cdff63badd9cb2052cb615e2ffec14f5d1875 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt
@@ -17,51 +17,43 @@
 package org.matrix.android.sdk.api.session.room.threads
 
 import androidx.lifecycle.LiveData
-import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
 
 /**
- * This interface defines methods to interact with threads related features.
- * It's implemented at the room level within the main timeline.
+ * This interface defines methods to interact with thread related features.
+ * It's the dynamic threads implementation and the homeserver must return
+ * a capability entry for threads. If the server do not support m.thread
+ * then [ThreadsLocalService] should be used instead
  */
 interface ThreadsService {
 
     /**
-     * Returns a [LiveData] list of all the thread root TimelineEvents that exists at the room level
+     * Returns a [LiveData] list of all the [ThreadSummary] that exists at the room level
      */
-    fun getAllThreadsLive(): LiveData<List<TimelineEvent>>
+    fun getAllThreadSummariesLive(): LiveData<List<ThreadSummary>>
 
     /**
-     * Returns a list of all the thread root TimelineEvents that exists at the room level
+     * Returns a list of all the [ThreadSummary] that exists at the room level
      */
-    fun getAllThreads(): List<TimelineEvent>
+    fun getAllThreadSummaries(): List<ThreadSummary>
 
     /**
-     * Returns a [LiveData] list of all the marked unread threads that exists at the room level
-     */
-    fun getMarkedThreadNotificationsLive(): LiveData<List<TimelineEvent>>
-
-    /**
-     * Returns a list of all the marked unread threads that exists at the room level
-     */
-    fun getMarkedThreadNotifications(): List<TimelineEvent>
-
-    /**
-     * Returns whether or not the current user is participating in the thread
-     * @param rootThreadEventId the eventId of the current thread
+     * Enhance the provided ThreadSummary[List] by adding the latest
+     * message edition for that thread
+     * @return the enhanced [List] with edited updates
      */
-    fun isUserParticipatingInThread(rootThreadEventId: String): Boolean
+    fun enhanceThreadWithEditions(threads: List<ThreadSummary>): List<ThreadSummary>
 
     /**
-     * Enhance the provided root thread TimelineEvent [List] by adding the latest
-     * message edition for that thread
-     * @return the enhanced [List] with edited updates
+     * Fetch all thread replies for the specified thread using the /relations api
+     * @param rootThreadEventId the root thread eventId
+     * @param from defines the token that will fetch from that position
+     * @param limit defines the number of max results the api will respond with
      */
-    fun mapEventsWithEdition(threads: List<TimelineEvent>): List<TimelineEvent>
+    suspend fun fetchThreadTimeline(rootThreadEventId: String, from: String, limit: Int)
 
     /**
-     * Marks the current thread as read in local DB.
-     * note: read receipts within threads are not yet supported with the API
-     * @param rootThreadEventId the root eventId of the current thread
+     * Fetch all thread summaries for the current room using the enhanced /messages api
      */
-    suspend fun markThreadAsRead(rootThreadEventId: String)
+    suspend fun fetchThreadSummaries()
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/local/ThreadsLocalService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/local/ThreadsLocalService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f7b379e3821b04d1a920b993139b0a647c8a6757
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/local/ThreadsLocalService.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2022 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.api.session.room.threads.local
+
+import androidx.lifecycle.LiveData
+import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+
+/**
+ * This interface defines methods to interact with thread related features.
+ * It's the local threads implementation and assumes that the homeserver
+ * do not support threads
+ */
+interface ThreadsLocalService {
+
+    /**
+     * Returns a [LiveData] list of all the thread root TimelineEvents that exists at the room level
+     */
+    fun getAllThreadsLive(): LiveData<List<TimelineEvent>>
+
+    /**
+     * Returns a list of all the thread root TimelineEvents that exists at the room level
+     */
+    fun getAllThreads(): List<TimelineEvent>
+
+    /**
+     * Returns a [LiveData] list of all the marked unread threads that exists at the room level
+     */
+    fun getMarkedThreadNotificationsLive(): LiveData<List<TimelineEvent>>
+
+    /**
+     * Returns a list of all the marked unread threads that exists at the room level
+     */
+    fun getMarkedThreadNotifications(): List<TimelineEvent>
+
+    /**
+     * Returns whether or not the current user is participating in the thread
+     * @param rootThreadEventId the eventId of the current thread
+     */
+    fun isUserParticipatingInThread(rootThreadEventId: String): Boolean
+
+    /**
+     * Enhance the provided root thread TimelineEvent [List] by adding the latest
+     * message edition for that thread
+     * @return the enhanced [List] with edited updates
+     */
+    fun mapEventsWithEdition(threads: List<TimelineEvent>): List<TimelineEvent>
+
+    /**
+     * Marks the current thread as read in local DB.
+     * note: read receipts within threads are not yet supported with the API
+     * @param rootThreadEventId the root eventId of the current thread
+     */
+    suspend fun markThreadAsRead(rootThreadEventId: String)
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadEditions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadEditions.kt
new file mode 100644
index 0000000000000000000000000000000000000000..c8353cf0de0c7255aae7472e55b0eda99653eb3c
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadEditions.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2022 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.api.session.room.threads.model
+
+data class ThreadEditions(var rootThreadEdition: String? = null,
+                          var latestThreadEdition: String? = null)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummary.kt
new file mode 100644
index 0000000000000000000000000000000000000000..017afba1baebee3b169a672542515b4ea55a81b8
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummary.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2022 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.api.session.room.threads.model
+
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.room.sender.SenderInfo
+
+/**
+ * The main thread Summary model, mainly used to display the thread list
+ */
+data class ThreadSummary(val roomId: String,
+                         val rootEvent: Event?,
+                         val latestEvent: Event?,
+                         val rootEventId: String,
+                         val rootThreadSenderInfo: SenderInfo,
+                         val latestThreadSenderInfo: SenderInfo,
+                         val isUserParticipating: Boolean,
+                         val numberOfThreads: Int,
+                         val threadEditions: ThreadEditions = ThreadEditions())
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummaryUpdateType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummaryUpdateType.kt
new file mode 100644
index 0000000000000000000000000000000000000000..95697f987f08b3c3fabfaebe10f77b5fae4e1b75
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/model/ThreadSummaryUpdateType.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2022 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.api.session.room.threads.model
+
+enum class ThreadSummaryUpdateType {
+    REPLACE,
+    ADD
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt
index 6f8bae876bded065bf675a015ee7b44bc3d1dfc4..a2ae8bfeb52e5b88068efff6e215a44a0b50a189 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt
@@ -17,6 +17,7 @@
 package org.matrix.android.sdk.api.session.room.timeline
 
 import org.matrix.android.sdk.BuildConfig
+import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.RelationType
@@ -28,6 +29,7 @@ import org.matrix.android.sdk.api.session.events.model.isSticker
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
 import org.matrix.android.sdk.api.session.room.model.ReadReceipt
+import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationBeaconContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageContent
 import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
@@ -54,6 +56,7 @@ data class TimelineEvent(
          * It's not unique on the timeline as it's reset on each chunk.
          */
         val displayIndex: Int,
+        val ownedByThreadChunk: Boolean = false,
         val senderInfo: SenderInfo,
         val annotations: EventAnnotationsSummary? = null,
         val readReceipts: List<ReadReceipt> = emptyList()
@@ -134,9 +137,10 @@ fun TimelineEvent.getEditedEventId(): String? {
  */
 fun TimelineEvent.getLastMessageContent(): MessageContent? {
     return when (root.getClearType()) {
-        EventType.STICKER    -> root.getClearContent().toModel<MessageStickerContent>()
-        EventType.POLL_START -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessagePollContent>()
-        else                 -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
+        EventType.STICKER                   -> root.getClearContent().toModel<MessageStickerContent>()
+        in EventType.POLL_START             -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<MessagePollContent>()
+        in EventType.STATE_ROOM_BEACON_INFO -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel<LiveLocationBeaconContent>()
+        else                                -> (annotations?.editSummary?.latestContent ?: root.getClearContent()).toModel()
     }
 }
 
@@ -158,6 +162,13 @@ fun TimelineEvent.isSticker(): Boolean {
     return root.isSticker()
 }
 
+/**
+ * Returns whether or not the event is a root thread event
+ */
+fun TimelineEvent.isRootThread(): Boolean {
+    return root.threadDetails?.isRootThread.orFalse()
+}
+
 /**
  * Get the latest message body, after a possible edition, stripping the reply prefix if necessary
  */
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt
index 61520696445913f8dac2b508b3773e0357b5f5fe..46433f387d36b2297d3571c687feadb122a9e2c0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt
@@ -38,7 +38,7 @@ interface TimelineService {
 
     /**
      * Returns a snapshot of TimelineEvent event with eventId.
-     * At the opposite of getTimeLineEventLive which will be updated when local echo event is synced, it will return null in this case.
+     * At the opposite of getTimelineEventLive which will be updated when local echo event is synced, it will return null in this case.
      * @param eventId the eventId to get the TimelineEvent
      */
     fun getTimelineEvent(eventId: String): TimelineEvent?
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SsssKeyCreationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SsssKeyCreationInfo.kt
index eeb1b31f9cc2a5b325f90fc996df4f3276d6f69c..7a91a16c8c08eba02bd0e4e977a0a86ee1265459 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SsssKeyCreationInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SsssKeyCreationInfo.kt
@@ -18,7 +18,7 @@ package org.matrix.android.sdk.api.session.securestorage
 
 data class SsssKeyCreationInfo(
         val keyId: String = "",
-        var content: SecretStorageKeyContent?,
+        val content: SecretStorageKeyContent?,
         val recoveryKey: String = "",
         val keySpec: SsssKeySpec
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SsssKeySpec.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SsssKeySpec.kt
index f791ea4e86f3d59f7923811ea4022b98c678e4e9..03efb9b3dbbbd4fca28b6a41491524db113cc32e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SsssKeySpec.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SsssKeySpec.kt
@@ -17,8 +17,8 @@
 package org.matrix.android.sdk.api.session.securestorage
 
 import org.matrix.android.sdk.api.listeners.ProgressListener
+import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey
 import org.matrix.android.sdk.internal.crypto.keysbackup.deriveKey
-import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
 
 /** Tag class */
 interface SsssKeySpec
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt
index 41c4e7eed1802a6d5384cc36a2cd0fb066769866..78267640675bea67c87b30a07aa55d283503bbfe 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt
@@ -22,7 +22,7 @@ import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.room.RoomSortOrder
 import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
 import org.matrix.android.sdk.api.session.room.model.RoomSummary
-import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult
+import org.matrix.android.sdk.api.session.space.peeking.SpacePeekResult
 
 typealias SpaceSummaryQueryParams = RoomSummaryQueryParams
 
@@ -106,5 +106,8 @@ interface SpaceService {
 
     suspend fun removeSpaceParent(childRoomId: String, parentSpaceId: String)
 
-    fun getRootSpaceSummaries(): List<RoomSummary>
+    /**
+     * Get the root spaces, i.e. all the spaces which do not have a parent space.
+     */
+    suspend fun getRootSpaceSummaries(): List<RoomSummary>
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/SpacePeekResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/peeking/SpacePeekResult.kt
similarity index 88%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/SpacePeekResult.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/peeking/SpacePeekResult.kt
index a2ffd8221ac2f68872d0d84808e5eac5b08aacb6..06dbd12d7a675ad7775b5b189e210fa565b2774d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/SpacePeekResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/peeking/SpacePeekResult.kt
@@ -14,11 +14,18 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.session.space.peeking
+package org.matrix.android.sdk.api.session.space.peeking
 
 import org.matrix.android.sdk.api.session.room.peeking.PeekResult
 
-// TODO Move to api package
+sealed class SpacePeekResult {
+    abstract class SpacePeekError : SpacePeekResult()
+    data class FailedToResolve(val spaceId: String, val roomPeekResult: PeekResult) : SpacePeekError()
+    data class NotSpaceType(val spaceId: String) : SpacePeekError()
+
+    data class Success(val summary: SpacePeekSummary) : SpacePeekResult()
+}
+
 data class SpacePeekSummary(
         val idOrAlias: String,
         val roomPeekResult: PeekResult.Success,
@@ -28,30 +35,18 @@ data class SpacePeekSummary(
 interface ISpaceChild {
     val id: String
     val roomPeekResult: PeekResult
-
-//    val default: Boolean?
     val order: String?
 }
 
 data class SpaceChildPeekResult(
         override val id: String,
         override val roomPeekResult: PeekResult,
-//        override val default: Boolean? = null,
         override val order: String? = null
 ) : ISpaceChild
 
 data class SpaceSubChildPeekResult(
         override val id: String,
         override val roomPeekResult: PeekResult,
-//        override val default: Boolean?,
         override val order: String?,
         val children: List<ISpaceChild>
 ) : ISpaceChild
-
-sealed class SpacePeekResult {
-    abstract class SpacePeekError : SpacePeekResult()
-    data class FailedToResolve(val spaceId: String, val roomPeekResult: PeekResult) : SpacePeekError()
-    data class NotSpaceType(val spaceId: String) : SpacePeekError()
-
-    data class Success(val summary: SpacePeekSummary) : SpacePeekResult()
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/InitialSyncStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/InitialSyncStrategy.kt
similarity index 94%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/InitialSyncStrategy.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/InitialSyncStrategy.kt
index 4bc866b36d492bd3167a6dbd642ccfb33569fefc..461d816ea7ca34f91f53cf46595d40eab5d5f8f6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/InitialSyncStrategy.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/InitialSyncStrategy.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 The Matrix.org Foundation C.I.C.
+ * Copyright (c) 2022 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.session.sync
+package org.matrix.android.sdk.api.session.sync
 
 var initialSyncStrategy: InitialSyncStrategy = InitialSyncStrategy.Optimized()
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/job/SyncService.kt
similarity index 98%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/job/SyncService.kt
index 97ae9b3a689ed8c84a9b3977a4ff8df86a8d7751..ac81be2174b2234a359830f154ab8a9deb3227d3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/job/SyncService.kt
@@ -1,11 +1,11 @@
 /*
- * Copyright 2020 The Matrix.org Foundation C.I.C.
+ * Copyright (c) 2022 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
+ *     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,
@@ -13,7 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.matrix.android.sdk.internal.session.sync.job
+
+package org.matrix.android.sdk.api.session.sync.job
 
 import android.app.Service
 import android.content.Intent
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/GetTermsResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/GetTermsResponse.kt
index e6d33cade68bb8efc8034200b17ac01e47d17491..b7bdc2b770c287f32818ed6d7b50d618b522a35d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/GetTermsResponse.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/GetTermsResponse.kt
@@ -16,8 +16,6 @@
 
 package org.matrix.android.sdk.api.session.terms
 
-import org.matrix.android.sdk.internal.session.terms.TermsResponse
-
 data class GetTermsResponse(
         val serverResponse: TermsResponse,
         val alreadyAcceptedTermUrls: Set<String>
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/TermsResponse.kt
similarity index 93%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsResponse.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/TermsResponse.kt
index a185e0b80f35afd103160ebd1cda612d3d80c626..9a30b4d764d26812d2784ee1e686e6742f4a3a60 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsResponse.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/TermsResponse.kt
@@ -14,12 +14,12 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.session.terms
+package org.matrix.android.sdk.api.session.terms
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.auth.data.LocalizedFlowDataLoginTerms
 import org.matrix.android.sdk.api.util.JsonDict
-import org.matrix.android.sdk.internal.auth.registration.LocalizedFlowDataLoginTerms
 
 /**
  * This class represent a localized privacy policy for registration Flow.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/TermsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/TermsService.kt
index e64cf1872ee3c73fc60e2ffa7e81110742ddfafa..6c357b2224d04a7a511c963773cacea9323d2931 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/TermsService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/terms/TermsService.kt
@@ -16,8 +16,6 @@
 
 package org.matrix.android.sdk.api.session.terms
 
-import org.matrix.android.sdk.internal.session.terms.TermsResponse
-
 interface TermsService {
     enum class ServiceType {
         IntegrationManager,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt
index fafe17b2c0957261a1992858f89344d1e65b5dac..c8fe1c85ea13dca2b7096acc74cbdc62d86b38e5 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/threads/ThreadDetails.kt
@@ -16,6 +16,7 @@
 
 package org.matrix.android.sdk.api.session.threads
 
+import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.room.sender.SenderInfo
 
 /**
@@ -26,9 +27,9 @@ data class ThreadDetails(
         val isRootThread: Boolean = false,
         val numberOfThreads: Int = 0,
         val threadSummarySenderInfo: SenderInfo? = null,
-        val threadSummaryLatestTextMessage: String? = null,
+        val threadSummaryLatestEvent: Event? = null,
         val lastMessageTimestamp: Long? = null,
-        var threadNotificationState: ThreadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE,
+        val threadNotificationState: ThreadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE,
         val isThread: Boolean = false,
         val lastRootThreadEdition: String? = null
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DefaultBaseAuth.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/uia/DefaultBaseAuth.kt
similarity index 91%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DefaultBaseAuth.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/uia/DefaultBaseAuth.kt
index bbb4a3a654844487b330858c8cf7054d15b76388..865e02daf28bd9df878544035883865459ad304c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DefaultBaseAuth.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/uia/DefaultBaseAuth.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 The Matrix.org Foundation C.I.C.
+ * Copyright (c) 2022 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.crypto.model.rest
+package org.matrix.android.sdk.api.session.uia
 
 import org.matrix.android.sdk.api.auth.UIABaseAuth
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/InteractiveAuthenticationFlow.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/uia/InteractiveAuthenticationFlow.kt
similarity index 94%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/InteractiveAuthenticationFlow.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/uia/InteractiveAuthenticationFlow.kt
index d66bcfb2740680674d229229c1cd113e5f53b0e7..a78c047221cc53494e86f41233bcb60b868b5871 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/InteractiveAuthenticationFlow.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/uia/InteractiveAuthenticationFlow.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.auth.data
+package org.matrix.android.sdk.api.session.uia
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/UserService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/UserService.kt
index cd4fb216d327331b943f46aee28e960b2c096ef1..063abdb5a0d3e1d6474f8c1569c00481a3ef8918 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/UserService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/UserService.kt
@@ -75,11 +75,14 @@ interface UserService {
 
     /**
      * Ignore users
+     * Note: once done, for the change to take effect, you have to request an initial sync.
+     * This may be improved in the future
      */
     suspend fun ignoreUserIds(userIds: List<String>)
 
     /**
      * Un-ignore some users
+     * Note: once done, for the change to take effect, you have to request an initial sync.
      */
     suspend fun unIgnoreUserIds(userIds: List<String>)
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/model/User.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/model/User.kt
index 54ae9e54f6ab388a2c22c70f6fe13410265a904b..79c86f3f23165b67c166446b3a2f35c808670d5f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/model/User.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/user/model/User.kt
@@ -16,6 +16,9 @@
 
 package org.matrix.android.sdk.api.session.user.model
 
+import org.matrix.android.sdk.api.session.profile.ProfileService
+import org.matrix.android.sdk.api.util.JsonDict
+
 /**
  * Data class which holds information about a user.
  * It can be retrieved with [org.matrix.android.sdk.api.session.user.UserService]
@@ -27,4 +30,14 @@ data class User(
          */
         val displayName: String? = null,
         val avatarUrl: String? = null
-)
+) {
+
+    companion object {
+
+        fun fromJson(userId: String, json: JsonDict) = User(
+                userId = userId,
+                displayName = json[ProfileService.DISPLAY_NAME_KEY] as? String,
+                avatarUrl = json[ProfileService.AVATAR_URL_KEY] as? String
+        )
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DummyContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/settings/LightweightSettingsStorage.kt
similarity index 69%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DummyContent.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/settings/LightweightSettingsStorage.kt
index 53d6e4a80a2d6880d489967ede46738f2a77cfc7..8dd20a702be27cd8c0c40922af5527d9fcf1e148 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DummyContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/settings/LightweightSettingsStorage.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 The Matrix.org Foundation C.I.C.
+ * Copyright (c) 2022 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.
@@ -13,10 +13,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.matrix.android.sdk.internal.crypto.model.rest
 
-/**
- * Class representing the dummy content
- * Ref: https://matrix.org/docs/spec/client_server/latest#id82
- */
-typealias DummyContent = Unit
+package org.matrix.android.sdk.api.settings
+
+interface LightweightSettingsStorage {
+    fun setThreadMessagesEnabled(enabled: Boolean)
+    fun areThreadMessagesEnabled(): Boolean
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Base64.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Base64.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e0596c1325c563db8c8189e10d376cd83fb6155b
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Base64.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2022 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.api.util
+
+import android.util.Base64
+
+fun ByteArray.toBase64NoPadding(): String {
+    return Base64.encodeToString(this, Base64.NO_PADDING or Base64.NO_WRAP)
+}
+
+fun String.fromBase64(): ByteArray {
+    return Base64.decode(this, Base64.DEFAULT)
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Hash.kt
similarity index 95%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Hash.kt
index 47f20913ecaac069dc8eb7bbb502faeaf6019fc6..7465eed3aeb1504e9320dda9fcae87a298bfd4d7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Hash.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.util
+package org.matrix.android.sdk.api.util
 
 import java.security.MessageDigest
 import java.util.Locale
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt
index 302f7387fad7363c72988c1a307f33a13bb4f1f8..4f5f4f82d9357c375985888d8a3b08da7c48855d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt
@@ -17,6 +17,7 @@
 package org.matrix.android.sdk.api.util
 
 import org.matrix.android.sdk.BuildConfig
+import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.session.group.model.GroupSummary
 import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
 import org.matrix.android.sdk.api.session.room.model.RoomSummary
@@ -36,6 +37,7 @@ sealed class MatrixItem(
                         override val displayName: String? = null,
                         override val avatarUrl: String? = null) :
             MatrixItem(id, displayName?.removeSuffix(IRC_PATTERN), avatarUrl) {
+
         init {
             if (BuildConfig.DEBUG) checkId()
         }
@@ -199,6 +201,8 @@ fun RoomMemberSummary.toMatrixItem() = MatrixItem.UserItem(userId, displayName,
 
 fun SenderInfo.toMatrixItem() = MatrixItem.UserItem(userId, disambiguatedDisplayName, avatarUrl)
 
+fun SenderInfo.toMatrixItemOrNull() = tryOrNull { MatrixItem.UserItem(userId, disambiguatedDisplayName, avatarUrl) }
+
 fun SpaceChildInfo.toMatrixItem() = if (roomType == RoomType.SPACE) {
     MatrixItem.SpaceItem(childRoomId, name ?: canonicalAlias, avatarUrl)
 } else {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixJsonParser.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixJsonParser.kt
new file mode 100644
index 0000000000000000000000000000000000000000..48a41667b2c669cf5af81a472a373f3d0ab5548c
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixJsonParser.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2022 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.api.util
+
+import com.squareup.moshi.Moshi
+import org.matrix.android.sdk.internal.di.MoshiProvider
+
+/**
+ * Entry point to get a Json parser
+ */
+object MatrixJsonParser {
+    /**
+     * @return a Moshi Json parser instance, configured to handle some Matrix Event contents
+     */
+    fun getMoshi(): Moshi {
+        return MoshiProvider.providesMoshi()
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/SuspendMatrixCallback.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/SuspendMatrixCallback.kt
similarity index 96%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/SuspendMatrixCallback.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/SuspendMatrixCallback.kt
index 145fc92feac12654d9d176222909c9d02f8f07cb..381dfb65fae5bf4f1836d22848dc27b614f63c43 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/SuspendMatrixCallback.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/SuspendMatrixCallback.kt
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.matrix.android.sdk.internal.util
+package org.matrix.android.sdk.api.util
 
 import org.matrix.android.sdk.api.MatrixCallback
 import kotlin.coroutines.resume
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedBodyFileInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/TextContent.kt
similarity index 58%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedBodyFileInfo.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/TextContent.kt
index 90f97b65ebf0f92f7185bbee3dc06f43e9c94040..fe12d7b1cfd0cb8712f886ea07b35ede25e891da 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedBodyFileInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/TextContent.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 The Matrix.org Foundation C.I.C.
+ * Copyright (c) 2022 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.
@@ -13,17 +13,15 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.matrix.android.sdk.internal.crypto.model.rest
 
-import org.matrix.olm.OlmPkMessage
+package org.matrix.android.sdk.api.util
 
 /**
- * Build from a OlmPkMessage object
- *
- * @param olmPkMessage OlmPkMessage
+ * Contains a text and eventually a formatted text
  */
-class EncryptedBodyFileInfo(olmPkMessage: OlmPkMessage) {
-    var ciphertext = olmPkMessage.mCipherText
-    var mac = olmPkMessage.mMac
-    var ephemeral = olmPkMessage.mEphemeralKey
+data class TextContent(
+        val text: String,
+        val formattedText: String? = null
+) {
+    fun takeFormatted() = formattedText ?: text
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultHomeServerHistoryService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultHomeServerHistoryService.kt
index 7415938ebc206714f9632cee8c3a67f530e029b1..c57ce6875d2935b860c209fa9735462e58bc2960 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultHomeServerHistoryService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultHomeServerHistoryService.kt
@@ -23,7 +23,7 @@ import org.matrix.android.sdk.internal.database.model.KnownServerUrlEntity
 import org.matrix.android.sdk.internal.di.GlobalDatabase
 import javax.inject.Inject
 
-class DefaultHomeServerHistoryService @Inject constructor(
+internal class DefaultHomeServerHistoryService @Inject constructor(
         @GlobalDatabase private val monarchy: Monarchy
 ) : HomeServerHistoryService {
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo001.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo001.kt
index 627f4e16bc578e2858ddeca9ffc901cc6a3aefbd..a8d5e296897b385e32d28843d5de871caeeae565 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo001.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo001.kt
@@ -21,7 +21,7 @@ import org.matrix.android.sdk.internal.auth.db.PendingSessionEntityFields
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 import timber.log.Timber
 
-class MigrateAuthTo001(realm: DynamicRealm) : RealmMigrator(realm, 1) {
+internal class MigrateAuthTo001(realm: DynamicRealm) : RealmMigrator(realm, 1) {
 
     override fun doMigrate(realm: DynamicRealm) {
         Timber.d("Create PendingSessionEntity")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo002.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo002.kt
index 6b133f8580d2b28b8a9cccc4bebb7828f98d7057..ef3a3e7f9bbd566517063e5c18389bc4d38a1b83 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo002.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo002.kt
@@ -21,7 +21,7 @@ import org.matrix.android.sdk.internal.auth.db.SessionParamsEntityFields
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 import timber.log.Timber
 
-class MigrateAuthTo002(realm: DynamicRealm) : RealmMigrator(realm, 2) {
+internal class MigrateAuthTo002(realm: DynamicRealm) : RealmMigrator(realm, 2) {
 
     override fun doMigrate(realm: DynamicRealm) {
         Timber.d("Add boolean isTokenValid in SessionParamsEntity, with value true")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo003.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo003.kt
index 9319ec99871d0dbef652dc8cec9b1f9e5c03806a..2584df189591e47c84f00e94da5448e0243f0e26 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo003.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo003.kt
@@ -24,7 +24,7 @@ import org.matrix.android.sdk.internal.di.MoshiProvider
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 import timber.log.Timber
 
-class MigrateAuthTo003(realm: DynamicRealm) : RealmMigrator(realm, 3) {
+internal class MigrateAuthTo003(realm: DynamicRealm) : RealmMigrator(realm, 3) {
 
     override fun doMigrate(realm: DynamicRealm) {
         Timber.d("Update SessionParamsEntity primary key, to allow several sessions with the same userId")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo004.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo004.kt
index 4a9b9022d58279bc99bf588d42739a9667c93d9b..6dfec6a1aae316f0094461957473ce5d4a4421fe 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo004.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo004.kt
@@ -24,7 +24,7 @@ import org.matrix.android.sdk.internal.di.MoshiProvider
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 import timber.log.Timber
 
-class MigrateAuthTo004(realm: DynamicRealm) : RealmMigrator(realm, 4) {
+internal class MigrateAuthTo004(realm: DynamicRealm) : RealmMigrator(realm, 4) {
 
     override fun doMigrate(realm: DynamicRealm) {
         Timber.d("Update SessionParamsEntity to add HomeServerConnectionConfig.homeServerUriBase value")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/AuthParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/AuthParams.kt
index 23fdbc613a19b869bda44d745993107a29e6d70c..cb17207741b9621c769f8a17e57dc52534b69462 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/AuthParams.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/AuthParams.kt
@@ -88,15 +88,3 @@ internal data class AuthParams(
         }
     }
 }
-
-@JsonClass(generateAdapter = true)
-data class ThreePidCredentials(
-        @Json(name = "client_secret")
-        val clientSecret: String? = null,
-
-        @Json(name = "id_server")
-        val idServer: String? = null,
-
-        @Json(name = "sid")
-        val sid: String? = null
-)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/SuccessResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/SuccessResult.kt
index 9b158cce90aa0e726ea52ad3cbe52064a7d4d1ab..c666eec749b3e0fb1c4cb4dc3f3d1c93efa5f25e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/SuccessResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/SuccessResult.kt
@@ -21,7 +21,7 @@ import com.squareup.moshi.JsonClass
 import org.matrix.android.sdk.api.extensions.orFalse
 
 @JsonClass(generateAdapter = true)
-data class SuccessResult(
+internal data class SuccessResult(
         @Json(name = "success")
         val success: Boolean?
 ) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/NewDeviceContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/ThreePidCredentials.kt
similarity index 64%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/NewDeviceContent.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/ThreePidCredentials.kt
index 2a63b4bee81e6ef4d6a8d1491801935904007a52..296ccadf91657a17f16692d0603ca42ed1aefdb8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/NewDeviceContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/ThreePidCredentials.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 The Matrix.org Foundation C.I.C.
+ * Copyright (c) 2022 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.
@@ -13,18 +13,20 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.matrix.android.sdk.internal.crypto.model.event
+
+package org.matrix.android.sdk.internal.auth.registration
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
 
 @JsonClass(generateAdapter = true)
-data class NewDeviceContent(
-        // the device id
-        @Json(name = "device_id")
-        val deviceId: String? = null,
+internal data class ThreePidCredentials(
+        @Json(name = "client_secret")
+        val clientSecret: String? = null,
+
+        @Json(name = "id_server")
+        val idServer: String? = null,
 
-        // the room ids list
-        @Json(name = "rooms")
-        val rooms: List<String>? = null
+        @Json(name = "sid")
+        val sid: String? = null
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/ValidationCodeBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/ValidationCodeBody.kt
index b6f3e83929949ed0e1c7c02da0bd44015deb7d88..ae71ae3a08a23120c956d64338669b15e6c11a64 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/ValidationCodeBody.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/ValidationCodeBody.kt
@@ -23,7 +23,7 @@ import com.squareup.moshi.JsonClass
  * This object is used to send a code received by SMS to validate Msisdn ownership
  */
 @JsonClass(generateAdapter = true)
-data class ValidationCodeBody(
+internal data class ValidationCodeBody(
         @Json(name = "client_secret")
         val clientSecret: String,
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt
index 0a9b8b73cc1c5084f7beb44a78998eb744dc3ab7..815f8de2de28b80c6a1067f943b2e67b3e21395c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt
@@ -38,7 +38,7 @@ internal data class HomeServerVersion(
     }
 
     companion object {
-        internal val pattern = Regex("""r(\d+)\.(\d+)\.(\d+)""")
+        internal val pattern = Regex("""[r|v](\d+)\.(\d+)\.(\d+)""")
 
         internal fun parse(value: String): HomeServerVersion? {
             val result = pattern.matchEntire(value) ?: return null
@@ -56,5 +56,6 @@ internal data class HomeServerVersion(
         val r0_4_0 = HomeServerVersion(major = 0, minor = 4, patch = 0)
         val r0_5_0 = HomeServerVersion(major = 0, minor = 5, patch = 0)
         val r0_6_0 = HomeServerVersion(major = 0, minor = 6, patch = 0)
+        val v1_3_0 = HomeServerVersion(major = 1, minor = 3, patch = 0)
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt
index 74cb3de2acf5bddf4af03aedfd8da6398eaca6ef..d07d5ecd64b62ba4eef581183557a9d98404f7bf 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt
@@ -51,6 +51,8 @@ private const val FEATURE_LAZY_LOAD_MEMBERS = "m.lazy_load_members"
 private const val FEATURE_REQUIRE_IDENTITY_SERVER = "m.require_identity_server"
 private const val FEATURE_ID_ACCESS_TOKEN = "m.id_access_token"
 private const val FEATURE_SEPARATE_ADD_AND_BIND = "m.separate_add_and_bind"
+private const val FEATURE_THREADS_MSC3440 = "org.matrix.msc3440"
+private const val FEATURE_THREADS_MSC3440_STABLE = "org.matrix.msc3440.stable"
 
 /**
  * Return true if the SDK supports this homeserver version
@@ -68,6 +70,14 @@ internal fun Versions.isLoginAndRegistrationSupportedBySdk(): Boolean {
             doesServerSeparatesAddAndBind()
 }
 
+/**
+ * Indicate if the homeserver support MSC3440 for threads
+ */
+internal fun Versions.doesServerSupportThreads(): Boolean {
+    return getMaxVersion() >= HomeServerVersion.v1_3_0 ||
+            unstableFeatures?.get(FEATURE_THREADS_MSC3440_STABLE) ?: false
+}
+
 /**
  * Return true if the server support the lazy loading of room members
  *
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CancelGossipRequestWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CancelGossipRequestWorker.kt
index 3a5f8e7668f1de044f9ccd23b174f0b7e4a44a78..98950374eddb2e12db908c4b9090570a881d50be 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CancelGossipRequestWorker.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CancelGossipRequestWorker.kt
@@ -21,11 +21,12 @@ import androidx.work.WorkerParameters
 import com.squareup.moshi.JsonClass
 import org.matrix.android.sdk.api.auth.data.Credentials
 import org.matrix.android.sdk.api.failure.shouldBeRetried
+import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
+import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.toContent
 import org.matrix.android.sdk.internal.SessionManager
-import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
 import org.matrix.android.sdk.internal.crypto.model.rest.ShareRequestCancellation
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt
index 3130a6382f1f8dd0346831f87bb18bd57a9d9d4f..2265526484bd5b734ea2cedeb5c111c953b3b3cc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt
@@ -137,8 +137,7 @@ internal abstract class CryptoModule {
         @JvmStatic
         @Provides
         @CryptoDatabase
-        fun providesClearCacheTask(@CryptoDatabase
-                                   realmConfiguration: RealmConfiguration): ClearCacheTask {
+        fun providesClearCacheTask(@CryptoDatabase realmConfiguration: RealmConfiguration): ClearCacheTask {
             return RealmClearCacheTask(realmConfiguration)
         }
 
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 0646e4d2b8c7e72925209d502fce6c68d1c4add2..6a57d94677dddcdfd5c48b964883e6347d92d6f4 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
@@ -32,6 +32,8 @@ import org.matrix.android.sdk.api.MatrixCallback
 import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.NoOpMatrixCallback
 import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
+import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
+import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM
 import org.matrix.android.sdk.api.crypto.MXCryptoConfig
 import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.failure.Failure
@@ -39,14 +41,31 @@ 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.NewSessionListener
+import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
 import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
 import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
 import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
 import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
 import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener
+import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
+import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
+import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse
+import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
+import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
+import org.matrix.android.sdk.api.session.crypto.model.MXDeviceInfo
+import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult
+import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
+import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
+import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest
+import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
 import org.matrix.android.sdk.api.session.events.model.Content
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
+import org.matrix.android.sdk.api.session.events.model.content.RoomKeyContent
+import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
+import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
@@ -61,21 +80,8 @@ import org.matrix.android.sdk.internal.crypto.algorithms.IMXWithHeldExtension
 import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmEncryptionFactory
 import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFactory
 import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService
-import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel
 import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
-import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
-import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult
-import org.matrix.android.sdk.internal.crypto.model.MXDeviceInfo
-import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult
 import org.matrix.android.sdk.internal.crypto.model.MXKey.Companion.KEY_SIGNED_CURVE_25519_TYPE
-import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
-import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
-import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyContent
-import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent
-import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent
-import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
-import org.matrix.android.sdk.internal.crypto.model.rest.DevicesListResponse
-import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
 import org.matrix.android.sdk.internal.crypto.model.toRest
 import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
@@ -434,6 +440,14 @@ internal class DefaultCryptoService @Inject constructor(
                     val currentCount = syncResponse.deviceOneTimeKeysCount.signedCurve25519 ?: 0
                     oneTimeKeysUploader.updateOneTimeKeyCount(currentCount)
                 }
+
+                // unwedge if needed
+                try {
+                    eventDecryptor.unwedgeDevicesIfNeeded()
+                } catch (failure: Throwable) {
+                    Timber.tag(loggerTag.value).w("unwedgeDevicesIfNeeded failed")
+                }
+
                 // There is a limit of to_device events returned per sync.
                 // If we are in a case of such limited to_device sync we can't try to generate/upload
                 // new otk now, because there might be some pending olm pre-key to_device messages that would fail if we rotate
@@ -723,7 +737,7 @@ internal class DefaultCryptoService @Inject constructor(
      * @return the MXEventDecryptionResult data, or throw in case of error
      */
     @Throws(MXCryptoError::class)
-    override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
+    override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
         return internalDecryptEvent(event, timeline)
     }
 
@@ -746,7 +760,7 @@ internal class DefaultCryptoService @Inject constructor(
      * @return the MXEventDecryptionResult data, or null in case of error
      */
     @Throws(MXCryptoError::class)
-    private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
+    private suspend fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
         return eventDecryptor.decryptEvent(event, timeline)
     }
 
@@ -1364,6 +1378,9 @@ internal class DefaultCryptoService @Inject constructor(
     @VisibleForTesting
     val cryptoStoreForTesting = cryptoStore
 
+    @VisibleForTesting
+    val olmDeviceForTest = olmDevice
+
     companion object {
         const val CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS = 3_600_000 // one hour
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt
index 494e6d7cc7a3138d43fe4709161ccce2b22224dd..6cae2d093586bfaf928e0a6e19656e26d3f83aa3 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt
@@ -21,10 +21,10 @@ import kotlinx.coroutines.launch
 import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.MatrixPatterns
 import org.matrix.android.sdk.api.auth.data.Credentials
-import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel
-import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
+import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
+import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
+import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
 import org.matrix.android.sdk.internal.crypto.model.CryptoInfoMapper
-import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import org.matrix.android.sdk.internal.crypto.tasks.DownloadKeysForUsersTask
 import org.matrix.android.sdk.internal.session.SessionScope
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt
index 57381eacfb47954160d19e3e4b8e05618fb85f72..1c8bce7377c6382d81336a6c20105b626bd70339 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt
@@ -21,16 +21,17 @@ import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import org.matrix.android.sdk.api.MatrixCallback
 import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
+import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM
+import org.matrix.android.sdk.api.logger.LoggerTag
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
+import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
+import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
 import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
-import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
-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.event.OlmEventContent
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
 import org.matrix.android.sdk.internal.extensions.foldToCallback
@@ -40,6 +41,8 @@ import javax.inject.Inject
 
 private const val SEND_TO_DEVICE_RETRY_COUNT = 3
 
+private val loggerTag = LoggerTag("CryptoSyncHandler", LoggerTag.CRYPTO)
+
 @SessionScope
 internal class EventDecryptor @Inject constructor(
         private val cryptoCoroutineScope: CoroutineScope,
@@ -47,13 +50,22 @@ internal class EventDecryptor @Inject constructor(
         private val roomDecryptorProvider: RoomDecryptorProvider,
         private val messageEncrypter: MessageEncrypter,
         private val sendToDeviceTask: SendToDeviceTask,
+        private val deviceListManager: DeviceListManager,
         private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
         private val cryptoStore: IMXCryptoStore
 ) {
 
-    // The date of the last time we forced establishment
-    // of a new session for each user:device.
-    private val lastNewSessionForcedDates = MXUsersDevicesMap<Long>()
+    /**
+     * Rate limit unwedge attempt, should we persist that?
+     */
+    private val lastNewSessionForcedDates = mutableMapOf<WedgedDeviceInfo, Long>()
+
+    data class WedgedDeviceInfo(
+            val userId: String,
+            val senderKey: String?
+    )
+
+    private val wedgedDevices = mutableListOf<WedgedDeviceInfo>()
 
     /**
      * Decrypt an event
@@ -63,7 +75,7 @@ internal class EventDecryptor @Inject constructor(
      * @return the MXEventDecryptionResult data, or throw in case of error
      */
     @Throws(MXCryptoError::class)
-    fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
+    suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
         return internalDecryptEvent(event, timeline)
     }
 
@@ -91,38 +103,32 @@ internal class EventDecryptor @Inject constructor(
      * @return the MXEventDecryptionResult data, or null in case of error
      */
     @Throws(MXCryptoError::class)
-    private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
+    private suspend fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
         val eventContent = event.content
         if (eventContent == null) {
-            Timber.e("## CRYPTO | decryptEvent : empty event content")
+            Timber.tag(loggerTag.value).e("decryptEvent : empty event content")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
         } else {
             val algorithm = eventContent["algorithm"]?.toString()
             val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm)
             if (alg == null) {
                 val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm)
-                Timber.e("## CRYPTO | decryptEvent() : $reason")
+                Timber.tag(loggerTag.value).e("decryptEvent() : $reason")
                 throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason)
             } else {
                 try {
                     return alg.decryptEvent(event, timeline)
                 } catch (mxCryptoError: MXCryptoError) {
-                    Timber.v("## CRYPTO | internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError")
+                    Timber.tag(loggerTag.value).d("internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError")
                     if (algorithm == MXCRYPTO_ALGORITHM_OLM) {
                         if (mxCryptoError is MXCryptoError.Base &&
                                 mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) {
                             // need to find sending device
-                            cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
-                                val olmContent = event.content.toModel<OlmEventContent>()
-                                cryptoStore.getUserDevices(event.senderId ?: "")
-                                        ?.values
-                                        ?.firstOrNull { it.identityKey() == olmContent?.senderKey }
-                                        ?.let {
-                                            markOlmSessionForUnwedging(event.senderId ?: "", it)
-                                        }
-                                        ?: run {
-                                            Timber.i("## CRYPTO | internalDecryptEvent() : Failed to find sender crypto device for unwedging")
-                                        }
+                            val olmContent = event.content.toModel<OlmEventContent>()
+                            if (event.senderId != null && olmContent?.senderKey != null) {
+                                markOlmSessionForUnwedging(event.senderId, olmContent.senderKey)
+                            } else {
+                                Timber.tag(loggerTag.value).d("Can't mark as wedge malformed")
                             }
                         }
                     }
@@ -132,53 +138,91 @@ internal class EventDecryptor @Inject constructor(
         }
     }
 
-    // coroutineDispatchers.crypto scope
-    private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) {
-        val deviceKey = deviceInfo.identityKey()
+    private fun markOlmSessionForUnwedging(senderId: String, senderKey: String) {
+        val info = WedgedDeviceInfo(senderId, senderKey)
+        if (!wedgedDevices.contains(info)) {
+            Timber.tag(loggerTag.value).d("Marking device from $senderId key:$senderKey as wedged")
+            wedgedDevices.add(info)
+        }
+    }
 
-        val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0
+    // coroutineDispatchers.crypto scope
+    suspend fun unwedgeDevicesIfNeeded() {
+        // handle wedged devices
+        // Some olm decryption have failed and some device are wedged
+        // we should force start a new session for those
+        Timber.tag(loggerTag.value).v("Unwedging:  ${wedgedDevices.size} are wedged")
+        // get the one that should be retried according to rate limit
         val now = System.currentTimeMillis()
-        if (now - lastForcedDate < DefaultCryptoService.CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
-            Timber.w("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another")
-            return
+        val toUnwedge = wedgedDevices.filter {
+            val lastForcedDate = lastNewSessionForcedDates[it] ?: 0
+            if (now - lastForcedDate < DefaultCryptoService.CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
+                Timber.tag(loggerTag.value).d("Unwedging, New session for $it already forced with device at $lastForcedDate")
+                return@filter false
+            }
+            // let's already mark that we tried now
+            lastNewSessionForcedDates[it] = now
+            true
         }
 
-        Timber.i("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}")
-        lastNewSessionForcedDates.setObject(senderId, deviceKey, now)
+        if (toUnwedge.isEmpty()) {
+            Timber.tag(loggerTag.value).v("Nothing to unwedge")
+            return
+        }
+        Timber.tag(loggerTag.value).d("Unwedging, trying to create new session for ${toUnwedge.size} devices")
 
-        // offload this from crypto thread (?)
-        cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
-            runCatching { ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true) }.fold(
-                    onSuccess = { sendDummyToDevice(ensured = it, deviceInfo, senderId) },
-                    onFailure = {
-                        Timber.e("## CRYPTO | markOlmSessionForUnwedging() : failed to ensure device info ${senderId}${deviceInfo.deviceId}")
+        toUnwedge
+                .chunked(100) // safer to chunk if we ever have lots of wedged devices
+                .forEach { wedgedList ->
+                    val groupedByUserId = wedgedList.groupBy { it.userId }
+                    // lets download keys if needed
+                    withContext(coroutineDispatchers.io) {
+                        deviceListManager.downloadKeys(groupedByUserId.keys.toList(), false)
                     }
-            )
-        }
-    }
 
-    private suspend fun sendDummyToDevice(ensured: MXUsersDevicesMap<MXOlmSessionResult>, deviceInfo: CryptoDeviceInfo, senderId: String) {
-        Timber.i("## CRYPTO | markOlmSessionForUnwedging() : ensureOlmSessionsForDevicesAction isEmpty:${ensured.isEmpty}")
-
-        // Now send a blank message on that session so the other side knows about it.
-        // (The keyshare request is sent in the clear so that won't do)
-        // We send this first such that, as long as the toDevice messages arrive in the
-        // same order we sent them, the other end will get this first, set up the new session,
-        // then get the keyshare request and send the key over this new session (because it
-        // is the session it has most recently received a message on).
-        val payloadJson = mapOf<String, Any>("type" to EventType.DUMMY)
-
-        val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
-        val sendToDeviceMap = MXUsersDevicesMap<Any>()
-        sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload)
-        Timber.i("## CRYPTO | markOlmSessionForUnwedging() : sending dummy to $senderId:${deviceInfo.deviceId}")
-        withContext(coroutineDispatchers.io) {
-            val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
-            try {
-                sendToDeviceTask.executeRetry(sendToDeviceParams, remainingRetry = SEND_TO_DEVICE_RETRY_COUNT)
-            } catch (failure: Throwable) {
-                Timber.e(failure, "## CRYPTO | markOlmSessionForUnwedging() : failed to send dummy to $senderId:${deviceInfo.deviceId}")
-            }
-        }
+                    // find the matching devices
+                    groupedByUserId
+                            .map { groupedByUser ->
+                                val userId = groupedByUser.key
+                                val wedgeSenderKeysForUser = groupedByUser.value.map { it.senderKey }
+                                val knownDevices = cryptoStore.getUserDevices(userId)?.values.orEmpty()
+                                userId to wedgeSenderKeysForUser.mapNotNull { senderKey ->
+                                    knownDevices.firstOrNull { it.identityKey() == senderKey }
+                                }
+                            }
+                            .toMap()
+                            .let { deviceList ->
+                                try {
+                                    // force creating new outbound session and mark them as most recent to
+                                    // be used for next encryption (dummy)
+                                    val sessionToUse = ensureOlmSessionsForDevicesAction.handle(deviceList, true)
+                                    Timber.tag(loggerTag.value).d("Unwedging, found ${sessionToUse.map.size} to send dummy to")
+
+                                    // Now send a dummy message on that session so the other side knows about it.
+                                    val payloadJson = mapOf(
+                                            "type" to EventType.DUMMY
+                                    )
+                                    val sendToDeviceMap = MXUsersDevicesMap<Any>()
+                                    sessionToUse.map.values
+                                            .flatMap { it.values }
+                                            .map { it.deviceInfo }
+                                            .forEach { deviceInfo ->
+                                                Timber.tag(loggerTag.value).v("encrypting dummy to ${deviceInfo.deviceId}")
+                                                val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
+                                                sendToDeviceMap.setObject(deviceInfo.userId, deviceInfo.deviceId, encodedPayload)
+                                            }
+
+                                    // now let's send that
+                                    val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
+                                    withContext(coroutineDispatchers.io) {
+                                        sendToDeviceTask.executeRetry(sendToDeviceParams, remainingRetry = SEND_TO_DEVICE_RETRY_COUNT)
+                                    }
+                                } catch (failure: Throwable) {
+                                    deviceList.flatMap { it.value }.joinToString { it.shortDebugString() }.let {
+                                        Timber.tag(loggerTag.value).e(failure, "## Failed to unwedge devices: $it}")
+                                    }
+                                }
+                            }
+                }
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/NewSessionListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipRequestType.kt
similarity index 86%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/NewSessionListener.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipRequestType.kt
index 301729680ce2e91208e8d004f9c5a70caf174e53..266c1a27442f1df9a1b98281ccdbacebbe67ca74 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/NewSessionListener.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/GossipRequestType.kt
@@ -13,8 +13,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package org.matrix.android.sdk.internal.crypto
 
-interface NewSessionListener {
-    fun onNewSession(roomId: String?, senderKey: String, sessionId: String)
+internal enum class GossipRequestType {
+    KEY,
+    SECRET
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt
index e7a46750b0a84afad9c7b07a3a26a37d5a349a22..a78444dff9ff222d49d8cbc7f3fd9edccff420bf 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt
@@ -19,8 +19,10 @@ package org.matrix.android.sdk.internal.crypto
 import android.util.LruCache
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
 import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.api.logger.LoggerTag
 import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import timber.log.Timber
@@ -28,6 +30,13 @@ import java.util.Timer
 import java.util.TimerTask
 import javax.inject.Inject
 
+internal data class InboundGroupSessionHolder(
+        val wrapper: OlmInboundGroupSessionWrapper2,
+        val mutex: Mutex = Mutex()
+)
+
+private val loggerTag = LoggerTag("InboundGroupSessionStore", LoggerTag.CRYPTO)
+
 /**
  * Allows to cache and batch store operations on inbound group session store.
  * Because it is used in the decrypt flow, that can be called quite rapidly
@@ -42,12 +51,13 @@ internal class InboundGroupSessionStore @Inject constructor(
             val senderKey: String
     )
 
-    private val sessionCache = object : LruCache<CacheKey, OlmInboundGroupSessionWrapper2>(30) {
-        override fun entryRemoved(evicted: Boolean, key: CacheKey?, oldValue: OlmInboundGroupSessionWrapper2?, newValue: OlmInboundGroupSessionWrapper2?) {
-            if (evicted && oldValue != null) {
+    private val sessionCache = object : LruCache<CacheKey, InboundGroupSessionHolder>(100) {
+        override fun entryRemoved(evicted: Boolean, key: CacheKey?, oldValue: InboundGroupSessionHolder?, newValue: InboundGroupSessionHolder?) {
+            if (oldValue != null) {
                 cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
-                    Timber.v("## Inbound: entryRemoved ${oldValue.roomId}-${oldValue.senderKey}")
-                    store.storeInboundGroupSessions(listOf(oldValue))
+                    Timber.tag(loggerTag.value).v("## Inbound: entryRemoved ${oldValue.wrapper.roomId}-${oldValue.wrapper.senderKey}")
+                    store.storeInboundGroupSessions(listOf(oldValue).map { it.wrapper })
+                    oldValue.wrapper.olmInboundGroupSession?.releaseSession()
                 }
             }
         }
@@ -59,27 +69,50 @@ internal class InboundGroupSessionStore @Inject constructor(
     private val dirtySession = mutableListOf<OlmInboundGroupSessionWrapper2>()
 
     @Synchronized
-    fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2? {
-        synchronized(sessionCache) {
+    fun clear() {
+        sessionCache.evictAll()
+    }
+
+    @Synchronized
+    fun getInboundGroupSession(sessionId: String, senderKey: String): InboundGroupSessionHolder? {
             val known = sessionCache[CacheKey(sessionId, senderKey)]
-            Timber.v("## Inbound: getInboundGroupSession in cache ${known != null}")
-            return known ?: store.getInboundGroupSession(sessionId, senderKey)?.also {
-                Timber.v("## Inbound: getInboundGroupSession cache populate ${it.roomId}")
-                sessionCache.put(CacheKey(sessionId, senderKey), it)
+            Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession  $sessionId in cache ${known != null}")
+            return known
+                    ?: store.getInboundGroupSession(sessionId, senderKey)?.also {
+                Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession cache populate ${it.roomId}")
+                sessionCache.put(CacheKey(sessionId, senderKey), InboundGroupSessionHolder(it))
+            }?.let {
+                InboundGroupSessionHolder(it)
             }
-        }
     }
 
     @Synchronized
-    fun storeInBoundGroupSession(wrapper: OlmInboundGroupSessionWrapper2, sessionId: String, senderKey: String) {
-        Timber.v("## Inbound: getInboundGroupSession mark as dirty ${wrapper.roomId}-${wrapper.senderKey}")
+    fun replaceGroupSession(old: InboundGroupSessionHolder, new: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
+        Timber.tag(loggerTag.value).v("## Replacing outdated session ${old.wrapper.roomId}-${old.wrapper.senderKey}")
+        dirtySession.remove(old.wrapper)
+        store.removeInboundGroupSession(sessionId, senderKey)
+        sessionCache.remove(CacheKey(sessionId, senderKey))
+
+        // release removed session
+        old.wrapper.olmInboundGroupSession?.releaseSession()
+
+        internalStoreGroupSession(new, sessionId, senderKey)
+    }
+
+    @Synchronized
+    fun storeInBoundGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
+        internalStoreGroupSession(holder, sessionId, senderKey)
+    }
+
+    private fun internalStoreGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
+        Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession mark as dirty ${holder.wrapper.roomId}-${holder.wrapper.senderKey}")
         // We want to batch this a bit for performances
-        dirtySession.add(wrapper)
+        dirtySession.add(holder.wrapper)
 
         if (sessionCache[CacheKey(sessionId, senderKey)] == null) {
             // first time seen, put it in memory cache while waiting for batch insert
             // If it's already known, no need to update cache it's already there
-            sessionCache.put(CacheKey(sessionId, senderKey), wrapper)
+            sessionCache.put(CacheKey(sessionId, senderKey), holder)
         }
 
         timerTask?.cancel()
@@ -96,7 +129,7 @@ internal class InboundGroupSessionStore @Inject constructor(
         val toSave = mutableListOf<OlmInboundGroupSessionWrapper2>().apply { addAll(dirtySession) }
         dirtySession.clear()
         cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
-            Timber.v("## Inbound: getInboundGroupSession batching save of ${dirtySession.size}")
+            Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession batching save of ${toSave.size}")
             tryOrNull {
                 store.storeInboundGroupSessions(toSave)
             }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt
index 220f25ec80096fb1b2b0c0214666639a05402a4e..3a409cf3fdbe4b73e672867e5e568784bbd63697 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt
@@ -20,21 +20,26 @@ import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.auth.data.Credentials
+import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
 import org.matrix.android.sdk.api.crypto.MXCryptoConfig
 import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
 import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
 import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
 import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
+import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey
 import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener
+import org.matrix.android.sdk.api.session.crypto.model.GossipingRequestState
+import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject
+import org.matrix.android.sdk.api.session.crypto.model.IncomingRequestCancellation
+import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
+import org.matrix.android.sdk.api.session.crypto.model.IncomingSecretShareRequest
+import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.util.toBase64NoPadding
 import org.matrix.android.sdk.internal.crypto.algorithms.IMXGroupEncryption
-import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding
-import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
 import org.matrix.android.sdk.internal.crypto.model.rest.GossipingDefaultContent
-import org.matrix.android.sdk.internal.crypto.model.rest.GossipingToDeviceObject
-import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId
 import org.matrix.android.sdk.internal.di.SessionId
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingShareRequestCommon.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingShareRequestCommon.kt
index 86e9610148bc21e8595cb438ba16374d060cc08c..97c369db3ec7cb48b1d9c0cffeefbeff995f5136 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingShareRequestCommon.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingShareRequestCommon.kt
@@ -16,7 +16,7 @@
 
 package org.matrix.android.sdk.internal.crypto
 
-interface IncomingShareRequestCommon {
+internal interface IncomingShareRequestCommon {
     /**
      * The user id
      */
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXCryptoAlgorithms.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXCryptoAlgorithms.kt
index 07881c7d797e4775376b8607dcd2d179cafbf550..5a5ee9e6968cffaf6dae8bf8569a6b2cd59891f9 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXCryptoAlgorithms.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXCryptoAlgorithms.kt
@@ -16,6 +16,9 @@
 
 package org.matrix.android.sdk.internal.crypto
 
+import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
+import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM
+
 // TODO Update comment
 internal object MXCryptoAlgorithms {
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXMegolmExportEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXMegolmExportEncryption.kt
index e5ffa0ed7d28f2ff9d01ce66b46e16ba26fa4179..f8235bf344f7ecf54e336a66c505278877a9236c 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXMegolmExportEncryption.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXMegolmExportEncryption.kt
@@ -33,7 +33,7 @@ import kotlin.math.min
 /**
  * Utility class to import/export the crypto data
  */
-object MXMegolmExportEncryption {
+internal object MXMegolmExportEncryption {
     private const val HEADER_LINE = "-----BEGIN MEGOLM SESSION DATA-----"
     private const val TRAILER_LINE = "-----END MEGOLM SESSION DATA-----"
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt
index e1a706df799a971f1d161eb5ba78da9408996902..4947761f05c3ca5e204f6c2c7864d6787c8331f7 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt
@@ -16,12 +16,17 @@
 
 package org.matrix.android.sdk.internal.crypto
 
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.api.logger.LoggerTag
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
+import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
 import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE
 import org.matrix.android.sdk.api.util.JsonDict
 import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXOutboundSessionInfo
 import org.matrix.android.sdk.internal.crypto.algorithms.megolm.SharedWithHelper
-import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
 import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
 import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
@@ -40,6 +45,8 @@ import timber.log.Timber
 import java.net.URLEncoder
 import javax.inject.Inject
 
+private val loggerTag = LoggerTag("MXOlmDevice", LoggerTag.CRYPTO)
+
 // The libolm wrapper.
 @SessionScope
 internal class MXOlmDevice @Inject constructor(
@@ -47,9 +54,12 @@ internal class MXOlmDevice @Inject constructor(
          * The store where crypto data is saved.
          */
         private val store: IMXCryptoStore,
+        private val olmSessionStore: OlmSessionStore,
         private val inboundGroupSessionStore: InboundGroupSessionStore
 ) {
 
+    val mutex = Mutex()
+
     /**
      * @return the Curve25519 key for the account.
      */
@@ -93,26 +103,26 @@ internal class MXOlmDevice @Inject constructor(
         try {
             store.getOrCreateOlmAccount()
         } catch (e: Exception) {
-            Timber.e(e, "MXOlmDevice : cannot initialize olmAccount")
+            Timber.tag(loggerTag.value).e(e, "MXOlmDevice : cannot initialize olmAccount")
         }
 
         try {
             olmUtility = OlmUtility()
         } catch (e: Exception) {
-            Timber.e(e, "## MXOlmDevice : OlmUtility failed with error")
+            Timber.tag(loggerTag.value).e(e, "## MXOlmDevice : OlmUtility failed with error")
             olmUtility = null
         }
 
         try {
-            deviceCurve25519Key = store.getOlmAccount().identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY]
+            deviceCurve25519Key = store.doWithOlmAccount { it.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY] }
         } catch (e: Exception) {
-            Timber.e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_IDENTITY_KEY} with error")
+            Timber.tag(loggerTag.value).e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_IDENTITY_KEY} with error")
         }
 
         try {
-            deviceEd25519Key = store.getOlmAccount().identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY]
+            deviceEd25519Key = store.doWithOlmAccount { it.identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY] }
         } catch (e: Exception) {
-            Timber.e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_FINGER_PRINT_KEY} with error")
+            Timber.tag(loggerTag.value).e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_FINGER_PRINT_KEY} with error")
         }
     }
 
@@ -121,9 +131,9 @@ internal class MXOlmDevice @Inject constructor(
      */
     fun getOneTimeKeys(): Map<String, Map<String, String>>? {
         try {
-            return store.getOlmAccount().oneTimeKeys()
+            return store.doWithOlmAccount { it.oneTimeKeys() }
         } catch (e: Exception) {
-            Timber.e(e, "## getOneTimeKeys() : failed")
+            Timber.tag(loggerTag.value).e(e, "## getOneTimeKeys() : failed")
         }
 
         return null
@@ -133,7 +143,7 @@ internal class MXOlmDevice @Inject constructor(
      * @return The maximum number of one-time keys the olm account can store.
      */
     fun getMaxNumberOfOneTimeKeys(): Long {
-        return store.getOlmAccount().maxOneTimeKeys()
+        return store.doWithOlmAccount { it.maxOneTimeKeys() }
     }
 
     /**
@@ -143,9 +153,9 @@ internal class MXOlmDevice @Inject constructor(
      */
     fun getFallbackKey(): MutableMap<String, MutableMap<String, String>>? {
         try {
-            return store.getOlmAccount().fallbackKey()
+            return store.doWithOlmAccount { it.fallbackKey() }
         } catch (e: Exception) {
-            Timber.e("## getFallbackKey() : failed")
+            Timber.tag(loggerTag.value).e("## getFallbackKey() : failed")
         }
         return null
     }
@@ -158,12 +168,14 @@ internal class MXOlmDevice @Inject constructor(
     fun generateFallbackKeyIfNeeded(): Boolean {
         try {
             if (!hasUnpublishedFallbackKey()) {
-                store.getOlmAccount().generateFallbackKey()
-                store.saveOlmAccount()
+                store.doWithOlmAccount {
+                    it.generateFallbackKey()
+                    store.saveOlmAccount()
+                }
                 return true
             }
         } catch (e: Exception) {
-            Timber.e("## generateFallbackKey() : failed")
+            Timber.tag(loggerTag.value).e("## generateFallbackKey() : failed")
         }
         return false
     }
@@ -174,10 +186,12 @@ internal class MXOlmDevice @Inject constructor(
 
     fun forgetFallbackKey() {
         try {
-            store.getOlmAccount().forgetFallbackKey()
-            store.saveOlmAccount()
+            store.doWithOlmAccount {
+                it.forgetFallbackKey()
+                store.saveOlmAccount()
+            }
         } catch (e: Exception) {
-            Timber.e("## forgetFallbackKey() : failed")
+            Timber.tag(loggerTag.value).e("## forgetFallbackKey() : failed")
         }
     }
 
@@ -190,6 +204,8 @@ internal class MXOlmDevice @Inject constructor(
             it.groupSession.releaseSession()
         }
         outboundGroupSessionCache.clear()
+        inboundGroupSessionStore.clear()
+        olmSessionStore.clear()
     }
 
     /**
@@ -200,9 +216,9 @@ internal class MXOlmDevice @Inject constructor(
      */
     fun signMessage(message: String): String? {
         try {
-            return store.getOlmAccount().signMessage(message)
+            return store.doWithOlmAccount { it.signMessage(message) }
         } catch (e: Exception) {
-            Timber.e(e, "## signMessage() : failed")
+            Timber.tag(loggerTag.value).e(e, "## signMessage() : failed")
         }
 
         return null
@@ -213,10 +229,12 @@ internal class MXOlmDevice @Inject constructor(
      */
     fun markKeysAsPublished() {
         try {
-            store.getOlmAccount().markOneTimeKeysAsPublished()
-            store.saveOlmAccount()
+            store.doWithOlmAccount {
+                it.markOneTimeKeysAsPublished()
+                store.saveOlmAccount()
+            }
         } catch (e: Exception) {
-            Timber.e(e, "## markKeysAsPublished() : failed")
+            Timber.tag(loggerTag.value).e(e, "## markKeysAsPublished() : failed")
         }
     }
 
@@ -227,10 +245,12 @@ internal class MXOlmDevice @Inject constructor(
      */
     fun generateOneTimeKeys(numKeys: Int) {
         try {
-            store.getOlmAccount().generateOneTimeKeys(numKeys)
-            store.saveOlmAccount()
+            store.doWithOlmAccount {
+                it.generateOneTimeKeys(numKeys)
+                store.saveOlmAccount()
+            }
         } catch (e: Exception) {
-            Timber.e(e, "## generateOneTimeKeys() : failed")
+            Timber.tag(loggerTag.value).e(e, "## generateOneTimeKeys() : failed")
         }
     }
 
@@ -243,12 +263,14 @@ internal class MXOlmDevice @Inject constructor(
      * @return the session id for the outbound session.
      */
     fun createOutboundSession(theirIdentityKey: String, theirOneTimeKey: String): String? {
-        Timber.v("## createOutboundSession() ; theirIdentityKey $theirIdentityKey theirOneTimeKey $theirOneTimeKey")
+        Timber.tag(loggerTag.value).d("## createOutboundSession() ; theirIdentityKey $theirIdentityKey theirOneTimeKey $theirOneTimeKey")
         var olmSession: OlmSession? = null
 
         try {
             olmSession = OlmSession()
-            olmSession.initOutboundSession(store.getOlmAccount(), theirIdentityKey, theirOneTimeKey)
+            store.doWithOlmAccount { olmAccount ->
+                olmSession.initOutboundSession(olmAccount, theirIdentityKey, theirOneTimeKey)
+            }
 
             val olmSessionWrapper = OlmSessionWrapper(olmSession, 0)
 
@@ -257,14 +279,14 @@ internal class MXOlmDevice @Inject constructor(
             // this session
             olmSessionWrapper.onMessageReceived()
 
-            store.storeSession(olmSessionWrapper, theirIdentityKey)
+            olmSessionStore.storeSession(olmSessionWrapper, theirIdentityKey)
 
             val sessionIdentifier = olmSession.sessionIdentifier()
 
-            Timber.v("## createOutboundSession() ;  olmSession.sessionIdentifier: $sessionIdentifier")
+            Timber.tag(loggerTag.value).v("## createOutboundSession() ;  olmSession.sessionIdentifier: $sessionIdentifier")
             return sessionIdentifier
         } catch (e: Exception) {
-            Timber.e(e, "## createOutboundSession() failed")
+            Timber.tag(loggerTag.value).e(e, "## createOutboundSession() failed")
 
             olmSession?.releaseSession()
         }
@@ -281,34 +303,38 @@ internal class MXOlmDevice @Inject constructor(
      * @return {{payload: string, session_id: string}} decrypted payload, and session id of new session.
      */
     fun createInboundSession(theirDeviceIdentityKey: String, messageType: Int, ciphertext: String): Map<String, String>? {
-        Timber.v("## createInboundSession() : theirIdentityKey: $theirDeviceIdentityKey")
+        Timber.tag(loggerTag.value).d("## createInboundSession() : theirIdentityKey: $theirDeviceIdentityKey")
 
         var olmSession: OlmSession? = null
 
         try {
             try {
                 olmSession = OlmSession()
-                olmSession.initInboundSessionFrom(store.getOlmAccount(), theirDeviceIdentityKey, ciphertext)
+                store.doWithOlmAccount { olmAccount ->
+                    olmSession.initInboundSessionFrom(olmAccount, theirDeviceIdentityKey, ciphertext)
+                }
             } catch (e: Exception) {
-                Timber.e(e, "## createInboundSession() : the session creation failed")
+                Timber.tag(loggerTag.value).e(e, "## createInboundSession() : the session creation failed")
                 return null
             }
 
-            Timber.v("## createInboundSession() : sessionId: ${olmSession.sessionIdentifier()}")
+            Timber.tag(loggerTag.value).v("## createInboundSession() : sessionId: ${olmSession.sessionIdentifier()}")
 
             try {
-                store.getOlmAccount().removeOneTimeKeys(olmSession)
-                store.saveOlmAccount()
+                store.doWithOlmAccount { olmAccount ->
+                    olmAccount.removeOneTimeKeys(olmSession)
+                    store.saveOlmAccount()
+                }
             } catch (e: Exception) {
-                Timber.e(e, "## createInboundSession() : removeOneTimeKeys failed")
+                Timber.tag(loggerTag.value).e(e, "## createInboundSession() : removeOneTimeKeys failed")
             }
 
-            Timber.v("## createInboundSession() : ciphertext: $ciphertext")
+            Timber.tag(loggerTag.value).v("## createInboundSession() : ciphertext: $ciphertext")
             try {
                 val sha256 = olmUtility!!.sha256(URLEncoder.encode(ciphertext, "utf-8"))
-                Timber.v("## createInboundSession() :ciphertext: SHA256: $sha256")
+                Timber.tag(loggerTag.value).v("## createInboundSession() :ciphertext: SHA256: $sha256")
             } catch (e: Exception) {
-                Timber.e(e, "## createInboundSession() :ciphertext: cannot encode ciphertext")
+                Timber.tag(loggerTag.value).e(e, "## createInboundSession() :ciphertext: cannot encode ciphertext")
             }
 
             val olmMessage = OlmMessage()
@@ -324,9 +350,9 @@ internal class MXOlmDevice @Inject constructor(
                 // This counts as a received message: set last received message time to now
                 olmSessionWrapper.onMessageReceived()
 
-                store.storeSession(olmSessionWrapper, theirDeviceIdentityKey)
+                olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey)
             } catch (e: Exception) {
-                Timber.e(e, "## createInboundSession() : decryptMessage failed")
+                Timber.tag(loggerTag.value).e(e, "## createInboundSession() : decryptMessage failed")
             }
 
             val res = HashMap<String, String>()
@@ -343,7 +369,7 @@ internal class MXOlmDevice @Inject constructor(
 
             return res
         } catch (e: Exception) {
-            Timber.e(e, "## createInboundSession() : OlmSession creation failed")
+            Timber.tag(loggerTag.value).e(e, "## createInboundSession() : OlmSession creation failed")
 
             olmSession?.releaseSession()
         }
@@ -357,8 +383,8 @@ internal class MXOlmDevice @Inject constructor(
      * @param theirDeviceIdentityKey the Curve25519 identity key for the remote device.
      * @return a list of known session ids for the device.
      */
-    fun getSessionIds(theirDeviceIdentityKey: String): List<String>? {
-        return store.getDeviceSessionIds(theirDeviceIdentityKey)
+    fun getSessionIds(theirDeviceIdentityKey: String): List<String> {
+        return olmSessionStore.getDeviceSessionIds(theirDeviceIdentityKey)
     }
 
     /**
@@ -368,7 +394,7 @@ internal class MXOlmDevice @Inject constructor(
      * @return the session id, or null if no established session.
      */
     fun getSessionId(theirDeviceIdentityKey: String): String? {
-        return store.getLastUsedSessionId(theirDeviceIdentityKey)
+        return olmSessionStore.getLastUsedSessionId(theirDeviceIdentityKey)
     }
 
     /**
@@ -379,30 +405,30 @@ internal class MXOlmDevice @Inject constructor(
      * @param payloadString          the payload to be encrypted and sent
      * @return the cipher text
      */
-    fun encryptMessage(theirDeviceIdentityKey: String, sessionId: String, payloadString: String): Map<String, Any>? {
-        var res: MutableMap<String, Any>? = null
-        val olmMessage: OlmMessage
+    suspend fun encryptMessage(theirDeviceIdentityKey: String, sessionId: String, payloadString: String): Map<String, Any>? {
         val olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId)
 
         if (olmSessionWrapper != null) {
             try {
-                Timber.v("## encryptMessage() : olmSession.sessionIdentifier: $sessionId")
-                // Timber.v("## encryptMessage() : payloadString: " + payloadString);
-
-                olmMessage = olmSessionWrapper.olmSession.encryptMessage(payloadString)
-                store.storeSession(olmSessionWrapper, theirDeviceIdentityKey)
-                res = HashMap()
+                Timber.tag(loggerTag.value).v("## encryptMessage() : olmSession.sessionIdentifier: $sessionId")
 
-                res["body"] = olmMessage.mCipherText
-                res["type"] = olmMessage.mType
-            } catch (e: Exception) {
-                Timber.e(e, "## encryptMessage() : failed")
+                val olmMessage = olmSessionWrapper.mutex.withLock {
+                    olmSessionWrapper.olmSession.encryptMessage(payloadString)
+                }
+                return mapOf(
+                        "body" to olmMessage.mCipherText,
+                        "type" to olmMessage.mType,
+                ).also {
+                    olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey)
+                }
+            } catch (e: Throwable) {
+                Timber.tag(loggerTag.value).e(e, "## encryptMessage() : failed to encrypt olm with device|session:$theirDeviceIdentityKey|$sessionId")
+                return null
             }
         } else {
-            Timber.e("## encryptMessage() : Failed to encrypt unknown session $sessionId")
+            Timber.tag(loggerTag.value).e("## encryptMessage() : Failed to encrypt unknown session $sessionId")
+            return null
         }
-
-        return res
     }
 
     /**
@@ -414,7 +440,8 @@ internal class MXOlmDevice @Inject constructor(
      * @param sessionId              the id of the active session.
      * @return the decrypted payload.
      */
-    fun decryptMessage(ciphertext: String, messageType: Int, sessionId: String, theirDeviceIdentityKey: String): String? {
+    @kotlin.jvm.Throws
+    suspend fun decryptMessage(ciphertext: String, messageType: Int, sessionId: String, theirDeviceIdentityKey: String): String? {
         var payloadString: String? = null
 
         val olmSessionWrapper = getSessionForDevice(theirDeviceIdentityKey, sessionId)
@@ -424,13 +451,13 @@ internal class MXOlmDevice @Inject constructor(
             olmMessage.mCipherText = ciphertext
             olmMessage.mType = messageType.toLong()
 
-            try {
-                payloadString = olmSessionWrapper.olmSession.decryptMessage(olmMessage)
-                olmSessionWrapper.onMessageReceived()
-                store.storeSession(olmSessionWrapper, theirDeviceIdentityKey)
-            } catch (e: Exception) {
-                Timber.e(e, "## decryptMessage() : decryptMessage failed")
-            }
+            payloadString =
+                    olmSessionWrapper.mutex.withLock {
+                        olmSessionWrapper.olmSession.decryptMessage(olmMessage).also {
+                            olmSessionWrapper.onMessageReceived()
+                        }
+                    }
+            olmSessionStore.storeSession(olmSessionWrapper, theirDeviceIdentityKey)
         }
 
         return payloadString
@@ -469,7 +496,7 @@ internal class MXOlmDevice @Inject constructor(
             store.storeCurrentOutboundGroupSessionForRoom(roomId, session)
             return session.sessionIdentifier()
         } catch (e: Exception) {
-            Timber.e(e, "createOutboundGroupSession")
+            Timber.tag(loggerTag.value).e(e, "createOutboundGroupSession")
 
             session?.releaseSession()
         }
@@ -521,7 +548,7 @@ internal class MXOlmDevice @Inject constructor(
             try {
                 return outboundGroupSessionCache[sessionId]!!.groupSession.sessionKey()
             } catch (e: Exception) {
-                Timber.e(e, "## getSessionKey() : failed")
+                Timber.tag(loggerTag.value).e(e, "## getSessionKey() : failed")
             }
         }
         return null
@@ -550,8 +577,8 @@ internal class MXOlmDevice @Inject constructor(
         if (sessionId.isNotEmpty() && payloadString.isNotEmpty()) {
             try {
                 return outboundGroupSessionCache[sessionId]!!.groupSession.encryptMessage(payloadString)
-            } catch (e: Exception) {
-                Timber.e(e, "## encryptGroupMessage() : failed")
+            } catch (e: Throwable) {
+                Timber.tag(loggerTag.value).e(e, "## encryptGroupMessage() : failed")
             }
         }
         return null
@@ -578,52 +605,64 @@ internal class MXOlmDevice @Inject constructor(
                                forwardingCurve25519KeyChain: List<String>,
                                keysClaimed: Map<String, String>,
                                exportFormat: Boolean): Boolean {
-        val session = OlmInboundGroupSessionWrapper2(sessionKey, exportFormat)
-        runCatching { getInboundGroupSession(sessionId, senderKey, roomId) }
-                .fold(
-                        {
-                            // If we already have this session, consider updating it
-                            Timber.e("## addInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
-
-                            val existingFirstKnown = it.firstKnownIndex!!
-                            val newKnownFirstIndex = session.firstKnownIndex
-
-                            // If our existing session is better we keep it
-                            if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) {
-                                session.olmInboundGroupSession?.releaseSession()
-                                return false
-                            }
-                        },
-                        {
-                            // Nothing to do in case of error
-                        }
-                )
+        val candidateSession = OlmInboundGroupSessionWrapper2(sessionKey, exportFormat)
+        val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) }
+        val existingSession = existingSessionHolder?.wrapper
+        // If we have an existing one we should check if the new one is not better
+        if (existingSession != null) {
+            Timber.tag(loggerTag.value).d("## addInboundGroupSession() check if known session is better than candidate session")
+            try {
+                val existingFirstKnown = existingSession.firstKnownIndex ?: return false.also {
+                    // This is quite unexpected, could throw if native was released?
+                    Timber.tag(loggerTag.value).e("## addInboundGroupSession() null firstKnownIndex on existing session")
+                    candidateSession.olmInboundGroupSession?.releaseSession()
+                    // Probably should discard it?
+                }
+                val newKnownFirstIndex = candidateSession.firstKnownIndex
+                // If our existing session is better we keep it
+                if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) {
+                    Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId")
+                    candidateSession.olmInboundGroupSession?.releaseSession()
+                    return false
+                }
+            } catch (failure: Throwable) {
+                Timber.tag(loggerTag.value).e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}")
+                candidateSession.olmInboundGroupSession?.releaseSession()
+                return false
+            }
+        }
 
-        // sanity check
-        if (null == session.olmInboundGroupSession) {
-            Timber.e("## addInboundGroupSession : invalid session")
+        Timber.tag(loggerTag.value).d("## addInboundGroupSession() : Candidate session should be added $senderKey/$sessionId")
+
+        // sanity check on the new session
+        val candidateOlmInboundSession = candidateSession.olmInboundGroupSession
+        if (null == candidateOlmInboundSession) {
+            Timber.tag(loggerTag.value).e("## addInboundGroupSession : invalid session <null>")
             return false
         }
 
         try {
-            if (session.olmInboundGroupSession!!.sessionIdentifier() != sessionId) {
-                Timber.e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
-                session.olmInboundGroupSession!!.releaseSession()
+            if (candidateOlmInboundSession.sessionIdentifier() != sessionId) {
+                Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
+                candidateOlmInboundSession.releaseSession()
                 return false
             }
-        } catch (e: Exception) {
-            session.olmInboundGroupSession?.releaseSession()
-            Timber.e(e, "## addInboundGroupSession : sessionIdentifier() failed")
+        } catch (e: Throwable) {
+            candidateOlmInboundSession.releaseSession()
+            Timber.tag(loggerTag.value).e(e, "## addInboundGroupSession : sessionIdentifier() failed")
             return false
         }
 
-        session.senderKey = senderKey
-        session.roomId = roomId
-        session.keysClaimed = keysClaimed
-        session.forwardingCurve25519KeyChain = forwardingCurve25519KeyChain
+        candidateSession.senderKey = senderKey
+        candidateSession.roomId = roomId
+        candidateSession.keysClaimed = keysClaimed
+        candidateSession.forwardingCurve25519KeyChain = forwardingCurve25519KeyChain
 
-        inboundGroupSessionStore.storeInBoundGroupSession(session, sessionId, senderKey)
-//        store.storeInboundGroupSessions(listOf(session))
+        if (existingSession != null) {
+            inboundGroupSessionStore.replaceGroupSession(existingSessionHolder, InboundGroupSessionHolder(candidateSession), sessionId, senderKey)
+        } else {
+            inboundGroupSessionStore.storeInBoundGroupSession(InboundGroupSessionHolder(candidateSession), sessionId, senderKey)
+        }
 
         return true
     }
@@ -638,57 +677,70 @@ internal class MXOlmDevice @Inject constructor(
         val sessions = ArrayList<OlmInboundGroupSessionWrapper2>(megolmSessionsData.size)
 
         for (megolmSessionData in megolmSessionsData) {
-            val sessionId = megolmSessionData.sessionId
-            val senderKey = megolmSessionData.senderKey
+            val sessionId = megolmSessionData.sessionId ?: continue
+            val senderKey = megolmSessionData.senderKey ?: continue
             val roomId = megolmSessionData.roomId
 
-            var session: OlmInboundGroupSessionWrapper2? = null
+            var candidateSessionToImport: OlmInboundGroupSessionWrapper2? = null
 
             try {
-                session = OlmInboundGroupSessionWrapper2(megolmSessionData)
+                candidateSessionToImport = OlmInboundGroupSessionWrapper2(megolmSessionData)
             } catch (e: Exception) {
-                Timber.e(e, "## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
+                Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
             }
 
             // sanity check
-            if (session?.olmInboundGroupSession == null) {
-                Timber.e("## importInboundGroupSession : invalid session")
+            if (candidateSessionToImport?.olmInboundGroupSession == null) {
+                Timber.tag(loggerTag.value).e("## importInboundGroupSession : invalid session")
                 continue
             }
 
+            val candidateOlmInboundGroupSession = candidateSessionToImport.olmInboundGroupSession
             try {
-                if (session.olmInboundGroupSession?.sessionIdentifier() != sessionId) {
-                    Timber.e("## importInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
-                    if (session.olmInboundGroupSession != null) session.olmInboundGroupSession!!.releaseSession()
+                if (candidateOlmInboundGroupSession?.sessionIdentifier() != sessionId) {
+                    Timber.tag(loggerTag.value).e("## importInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
+                    candidateOlmInboundGroupSession?.releaseSession()
                     continue
                 }
             } catch (e: Exception) {
-                Timber.e(e, "## importInboundGroupSession : sessionIdentifier() failed")
-                session.olmInboundGroupSession!!.releaseSession()
+                Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession : sessionIdentifier() failed")
+                candidateOlmInboundGroupSession?.releaseSession()
                 continue
             }
 
-            runCatching { getInboundGroupSession(sessionId, senderKey, roomId) }
-                    .fold(
-                            {
-                                // If we already have this session, consider updating it
-                                Timber.e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
-
-                                // For now we just ignore updates. TODO: implement something here
-                                if (it.firstKnownIndex!! <= session.firstKnownIndex!!) {
-                                    // Ignore this, keep existing
-                                    session.olmInboundGroupSession!!.releaseSession()
-                                } else {
-                                    sessions.add(session)
-                                }
-                                Unit
-                            },
-                            {
-                                // Session does not already exist, add it
-                                sessions.add(session)
-                            }
-
-                    )
+            val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) }
+            val existingSession = existingSessionHolder?.wrapper
+
+            if (existingSession == null) {
+                // Session does not already exist, add it
+                Timber.tag(loggerTag.value).d("## importInboundGroupSession() : importing new megolm session $senderKey/$sessionId")
+                sessions.add(candidateSessionToImport)
+            } else {
+                Timber.tag(loggerTag.value).e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
+                val existingFirstKnown = tryOrNull { existingSession.firstKnownIndex }
+                val candidateFirstKnownIndex = tryOrNull { candidateSessionToImport.firstKnownIndex }
+
+                if (existingFirstKnown == null || candidateFirstKnownIndex == null) {
+                    // should not happen?
+                    candidateSessionToImport.olmInboundGroupSession?.releaseSession()
+                    Timber.tag(loggerTag.value)
+                            .w("## importInboundGroupSession() : Can't check session null index $existingFirstKnown/$candidateFirstKnownIndex")
+                } else {
+                    if (existingFirstKnown <= candidateSessionToImport.firstKnownIndex!!) {
+                        // Ignore this, keep existing
+                        candidateOlmInboundGroupSession.releaseSession()
+                    } else {
+                        // update cache with better session
+                        inboundGroupSessionStore.replaceGroupSession(
+                                existingSessionHolder,
+                                InboundGroupSessionHolder(candidateSessionToImport),
+                                sessionId,
+                                senderKey
+                        )
+                        sessions.add(candidateSessionToImport)
+                    }
+                }
+            }
         }
 
         store.storeInboundGroupSessions(sessions)
@@ -696,18 +748,6 @@ internal class MXOlmDevice @Inject constructor(
         return sessions
     }
 
-    /**
-     * Remove an inbound group session
-     *
-     * @param sessionId  the session identifier.
-     * @param sessionKey base64-encoded secret key.
-     */
-    fun removeInboundGroupSession(sessionId: String?, sessionKey: String?) {
-        if (null != sessionId && null != sessionKey) {
-            store.removeInboundGroupSession(sessionId, sessionKey)
-        }
-    }
-
     /**
      * Decrypt a received message with an inbound group session.
      *
@@ -719,19 +759,24 @@ internal class MXOlmDevice @Inject constructor(
      * @return the decrypting result. Nil if the sessionId is unknown.
      */
     @Throws(MXCryptoError::class)
-    fun decryptGroupMessage(body: String,
-                            roomId: String,
-                            timeline: String?,
-                            sessionId: String,
-                            senderKey: String): OlmDecryptionResult {
-        val session = getInboundGroupSession(sessionId, senderKey, roomId)
+    suspend fun decryptGroupMessage(body: String,
+                                    roomId: String,
+                                    timeline: String?,
+                                    sessionId: String,
+                                    senderKey: String): OlmDecryptionResult {
+        val sessionHolder = getInboundGroupSession(sessionId, senderKey, roomId)
+        val wrapper = sessionHolder.wrapper
+        val inboundGroupSession = wrapper.olmInboundGroupSession
+                ?: throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, "Session is null")
         // Check that the room id matches the original one for the session. This stops
         // the HS pretending a message was targeting a different room.
-        if (roomId == session.roomId) {
+        if (roomId == wrapper.roomId) {
             val decryptResult = try {
-                session.olmInboundGroupSession!!.decryptMessage(body)
+                sessionHolder.mutex.withLock {
+                    inboundGroupSession.decryptMessage(body)
+                }
             } catch (e: OlmException) {
-                Timber.e(e, "## decryptGroupMessage () : decryptMessage failed")
+                Timber.tag(loggerTag.value).e(e, "## decryptGroupMessage () : decryptMessage failed")
                 throw MXCryptoError.OlmError(e)
             }
 
@@ -742,32 +787,32 @@ internal class MXOlmDevice @Inject constructor(
 
                 if (timelineSet.contains(messageIndexKey)) {
                     val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex)
-                    Timber.e("## decryptGroupMessage() : $reason")
+                    Timber.tag(loggerTag.value).e("## decryptGroupMessage() : $reason")
                     throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason)
                 }
 
                 timelineSet.add(messageIndexKey)
             }
 
-            inboundGroupSessionStore.storeInBoundGroupSession(session, sessionId, senderKey)
+            inboundGroupSessionStore.storeInBoundGroupSession(sessionHolder, sessionId, senderKey)
             val payload = try {
                 val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
                 val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage)
                 adapter.fromJson(payloadString)
             } catch (e: Exception) {
-                Timber.e("## decryptGroupMessage() : fails to parse the payload")
+                Timber.tag(loggerTag.value).e("## decryptGroupMessage() : fails to parse the payload")
                 throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON)
             }
 
             return OlmDecryptionResult(
                     payload,
-                    session.keysClaimed,
+                    wrapper.keysClaimed,
                     senderKey,
-                    session.forwardingCurve25519KeyChain
+                    wrapper.forwardingCurve25519KeyChain
             )
         } else {
-            val reason = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId)
-            Timber.e("## decryptGroupMessage() : $reason")
+            val reason = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, wrapper.roomId)
+            Timber.tag(loggerTag.value).e("## decryptGroupMessage() : $reason")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, reason)
         }
     }
@@ -819,7 +864,7 @@ internal class MXOlmDevice @Inject constructor(
     private fun getSessionForDevice(theirDeviceIdentityKey: String, sessionId: String): OlmSessionWrapper? {
         // sanity check
         return if (theirDeviceIdentityKey.isEmpty() || sessionId.isEmpty()) null else {
-            store.getDeviceSession(sessionId, theirDeviceIdentityKey)
+            olmSessionStore.getDeviceSession(sessionId, theirDeviceIdentityKey)
         }
     }
 
@@ -832,25 +877,26 @@ internal class MXOlmDevice @Inject constructor(
      * @param senderKey the base64-encoded curve25519 key of the sender.
      * @return the inbound group session.
      */
-    fun getInboundGroupSession(sessionId: String?, senderKey: String?, roomId: String?): OlmInboundGroupSessionWrapper2 {
+    fun getInboundGroupSession(sessionId: String?, senderKey: String?, roomId: String?): InboundGroupSessionHolder {
         if (sessionId.isNullOrBlank() || senderKey.isNullOrBlank()) {
             throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.ERROR_MISSING_PROPERTY_REASON)
         }
 
-        val session = inboundGroupSessionStore.getInboundGroupSession(sessionId, senderKey)
+        val holder = inboundGroupSessionStore.getInboundGroupSession(sessionId, senderKey)
+        val session = holder?.wrapper
 
         if (session != null) {
             // Check that the room id matches the original one for the session. This stops
             // the HS pretending a message was targeting a different room.
             if (roomId != session.roomId) {
                 val errorDescription = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId)
-                Timber.e("## getInboundGroupSession() : $errorDescription")
+                Timber.tag(loggerTag.value).e("## getInboundGroupSession() : $errorDescription")
                 throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, errorDescription)
             } else {
-                return session
+                return holder
             }
         } else {
-            Timber.w("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId")
+            Timber.tag(loggerTag.value).w("## getInboundGroupSession() : UISI $sessionId")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON)
         }
     }
@@ -866,4 +912,9 @@ internal class MXOlmDevice @Inject constructor(
     fun hasInboundSessionKeys(roomId: String, senderKey: String, sessionId: String): Boolean {
         return runCatching { getInboundGroupSession(sessionId, senderKey, roomId) }.isSuccess
     }
+
+    @VisibleForTesting
+    fun clearOlmSessionCache() {
+        olmSessionStore.clear()
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MegolmSessionData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MegolmSessionData.kt
index caff2d76f107f26032c0354b7726d70faa31ff74..f6bc9a9148af14285a23d32ed0d32645eddf92f2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MegolmSessionData.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MegolmSessionData.kt
@@ -23,7 +23,7 @@ import com.squareup.moshi.JsonClass
  * The type of object we use for importing and exporting megolm session data.
  */
 @JsonClass(generateAdapter = true)
-data class MegolmSessionData(
+internal data class MegolmSessionData(
         /**
          * The algorithm used.
          */
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MyDeviceInfoHolder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MyDeviceInfoHolder.kt
index 70846515a7dd4239d0666d5c7f571b06d3639a8d..9798d21576077b553c5f53efdf01b3cefce7d130 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MyDeviceInfoHolder.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MyDeviceInfoHolder.kt
@@ -17,8 +17,8 @@
 package org.matrix.android.sdk.internal.crypto
 
 import org.matrix.android.sdk.api.auth.data.Credentials
-import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel
-import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
+import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
+import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import org.matrix.android.sdk.internal.session.SessionScope
 import javax.inject.Inject
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f4fbca6a0fe5bc7a29cc5f2f7eacf8d366e0c3a6
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmSessionStore.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2022 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.crypto
+
+import org.matrix.android.sdk.api.logger.LoggerTag
+import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
+import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
+import org.matrix.olm.OlmSession
+import timber.log.Timber
+import javax.inject.Inject
+
+private val loggerTag = LoggerTag("OlmSessionStore", LoggerTag.CRYPTO)
+
+/**
+ * Keep the used olm session in memory and load them from the data layer when needed
+ * Access is synchronized for thread safety
+ */
+internal class OlmSessionStore @Inject constructor(private val store: IMXCryptoStore) {
+    /**
+     * map of device key to list of olm sessions (it is possible to have several active sessions with a device)
+     */
+    private val olmSessions = HashMap<String, MutableList<OlmSessionWrapper>>()
+
+    /**
+     * Store a session between our own device and another device.
+     * This will be called after the session has been created but also every time it has been used
+     * in order to persist the correct state for next run
+     * @param olmSessionWrapper   the end-to-end session.
+     * @param deviceKey the public key of the other device.
+     */
+    @Synchronized
+    fun storeSession(olmSessionWrapper: OlmSessionWrapper, deviceKey: String) {
+        // This could be a newly created session or one that was just created
+        // Anyhow we should persist ratchet state for future app lifecycle
+        addNewSessionInCache(olmSessionWrapper, deviceKey)
+        store.storeSession(olmSessionWrapper, deviceKey)
+    }
+
+    /**
+     * Get all the Olm Sessions we are sharing with the given device.
+     *
+     * @param deviceKey the public key of the other device.
+     * @return A set of sessionId, or empty if device is not known
+     */
+    @Synchronized
+    fun getDeviceSessionIds(deviceKey: String): List<String> {
+        // we need to get the persisted ids first
+        val persistedKnownSessions = store.getDeviceSessionIds(deviceKey)
+                .orEmpty()
+                .toMutableList()
+        // Do we have some in cache not yet persisted?
+        olmSessions.getOrPut(deviceKey) { mutableListOf() }.forEach { cached ->
+            getSafeSessionIdentifier(cached.olmSession)?.let { cachedSessionId ->
+                if (!persistedKnownSessions.contains(cachedSessionId)) {
+                    persistedKnownSessions.add(cachedSessionId)
+                }
+            }
+        }
+        return persistedKnownSessions
+    }
+
+    /**
+     * Retrieve an end-to-end session between our own device and another
+     * device.
+     *
+     * @param sessionId the session Id.
+     * @param deviceKey the public key of the other device.
+     * @return the session wrapper if found
+     */
+    @Synchronized
+    fun getDeviceSession(sessionId: String, deviceKey: String): OlmSessionWrapper? {
+        // get from cache or load and add to cache
+        return internalGetSession(sessionId, deviceKey)
+    }
+
+    /**
+     * Retrieve the last used sessionId, regarding `lastReceivedMessageTs`, or null if no session exist
+     *
+     * @param deviceKey the public key of the other device.
+     * @return last used sessionId, or null if not found
+     */
+    @Synchronized
+    fun getLastUsedSessionId(deviceKey: String): String? {
+        // We want to avoid to load in memory old session if possible
+        val lastPersistedUsedSession = store.getLastUsedSessionId(deviceKey)
+        var candidate = lastPersistedUsedSession?.let { internalGetSession(it, deviceKey) }
+        // we should check if we have one in cache with a higher last message received?
+        olmSessions[deviceKey].orEmpty().forEach { inCache ->
+            if (inCache.lastReceivedMessageTs > (candidate?.lastReceivedMessageTs ?: 0L)) {
+                candidate = inCache
+            }
+        }
+
+        return candidate?.olmSession?.sessionIdentifier()
+    }
+
+    /**
+     * Release all sessions and clear cache
+     */
+    @Synchronized
+    fun clear() {
+        olmSessions.entries.onEach { entry ->
+            entry.value.onEach { it.olmSession.releaseSession() }
+        }
+        olmSessions.clear()
+    }
+
+    private fun internalGetSession(sessionId: String, deviceKey: String): OlmSessionWrapper? {
+        return getSessionInCache(sessionId, deviceKey)
+                ?: // deserialize from store
+                return store.getDeviceSession(sessionId, deviceKey)?.also {
+                    addNewSessionInCache(it, deviceKey)
+                }
+    }
+
+    private fun getSessionInCache(sessionId: String, deviceKey: String): OlmSessionWrapper? {
+        return olmSessions[deviceKey]?.firstOrNull {
+            getSafeSessionIdentifier(it.olmSession) == sessionId
+        }
+    }
+
+    private fun getSafeSessionIdentifier(session: OlmSession): String? {
+        return try {
+            session.sessionIdentifier()
+        } catch (throwable: Throwable) {
+            Timber.tag(loggerTag.value).w("Failed to load sessionId from loaded olm session")
+            null
+        }
+    }
+
+    private fun addNewSessionInCache(session: OlmSessionWrapper, deviceKey: String) {
+        val sessionId = getSafeSessionIdentifier(session.olmSession) ?: return
+        olmSessions.getOrPut(deviceKey) { mutableListOf() }.let {
+            val existing = it.firstOrNull { getSafeSessionIdentifier(it.olmSession) == sessionId }
+            it.add(session)
+            // remove and release if was there but with different instance
+            if (existing != null && existing.olmSession != session.olmSession) {
+                // mm not sure when this could happen
+                // anyhow we should remove and release the one known
+                it.remove(existing)
+                existing.olmSession.releaseSession()
+            }
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt
index 4aebe091c48118ae807850cdb26c297d4abc6763..792c9a25dcdb3d1a484417b37bbc73277b2a64fe 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt
@@ -31,7 +31,7 @@ import kotlin.math.min
 
 // The spec recommend a 5mn delay, but due to federation
 // or server downtime we give it a bit more time (1 hour)
-const val FALLBACK_KEY_FORGET_DELAY = 60 * 60_000L
+private const val FALLBACK_KEY_FORGET_DELAY = 60 * 60_000L
 
 @SessionScope
 internal class OneTimeKeysUploader @Inject constructor(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequest.kt
index 8e13daec94e06b14fd26922f8cc6a47fcb00b49a..2438e011029ddc02628e7e41db1ee45b71f5a8c5 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequest.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequest.kt
@@ -16,10 +16,12 @@
 
 package org.matrix.android.sdk.internal.crypto
 
-interface OutgoingGossipingRequest {
-    var recipients: Map<String, List<String>>
-    var requestId: String
-    var state: OutgoingGossipingRequestState
+import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState
+
+internal interface OutgoingGossipingRequest {
+    val recipients: Map<String, List<String>>
+    val requestId: String
+    val state: OutgoingGossipingRequestState
     // transaction id for the cancellation, if any
     // var cancellationTxnId: String?
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt
index fd60e4326041d6248d560ce0b38c49c1e151ba6a..e6f6ac505310250970da3e089484d8884f2c83c2 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt
@@ -20,7 +20,9 @@ import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
 import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
-import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
+import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState
+import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest
+import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId
 import org.matrix.android.sdk.internal.crypto.util.RequestIdHelper
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingSecretRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingSecretRequest.kt
index def7a1567a9d52d7ada3a8a28d62f62b43ea624e..2ba2f5c817add137c63fe7fdabaf9095de928026 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingSecretRequest.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingSecretRequest.kt
@@ -17,12 +17,13 @@
 package org.matrix.android.sdk.internal.crypto
 
 import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState
 
 /**
  * Represents an outgoing room key request
  */
 @JsonClass(generateAdapter = true)
-class OutgoingSecretRequest(
+internal class OutgoingSecretRequest(
         // Secret Name
         val secretName: String?,
         // list of recipients for the request
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomDecryptorProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomDecryptorProvider.kt
index 89fb43ef2ed3ce340fbd91e1092686d5e3b6ca5e..dab806a5657c159f9e09d64f1745cbf440242d55 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomDecryptorProvider.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/RoomDecryptorProvider.kt
@@ -16,6 +16,8 @@
 
 package org.matrix.android.sdk.internal.crypto
 
+import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
+import org.matrix.android.sdk.api.session.crypto.NewSessionListener
 import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting
 import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmDecryptionFactory
 import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmDecryptionFactory
@@ -74,7 +76,7 @@ internal class RoomDecryptorProvider @Inject constructor(
                     this.newSessionListener = object : NewSessionListener {
                         override fun onNewSession(roomId: String?, senderKey: String, sessionId: String) {
                             // PR reviewer: the parameter has been renamed so is now in conflict with the parameter of getOrCreateRoomDecryptor
-                            newSessionListeners.forEach {
+                            newSessionListeners.toList().forEach {
                                 try {
                                     it.onNewSession(roomId, senderKey, sessionId)
                                 } catch (e: Throwable) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipRequestWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipRequestWorker.kt
index 3129ccae3b0b691cc36536f60c6bb5165f2609e8..dbdea9741107897cb1d6b1648b145a1b11f22b5b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipRequestWorker.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipRequestWorker.kt
@@ -21,14 +21,16 @@ import androidx.work.WorkerParameters
 import com.squareup.moshi.JsonClass
 import org.matrix.android.sdk.api.auth.data.Credentials
 import org.matrix.android.sdk.api.failure.shouldBeRetried
+import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject
+import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
+import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState
+import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest
+import org.matrix.android.sdk.api.session.crypto.model.RoomKeyShareRequest
+import org.matrix.android.sdk.api.session.crypto.model.SecretShareRequest
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.toContent
 import org.matrix.android.sdk.internal.SessionManager
-import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
-import org.matrix.android.sdk.internal.crypto.model.rest.GossipingToDeviceObject
-import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyShareRequest
-import org.matrix.android.sdk.internal.crypto.model.rest.SecretShareRequest
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
 import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipWorker.kt
index ff206a3c96819706ecfab5d31e18bd2fb48c0a81..fd472fe73bc323cadf77f760202aefab4e95b136 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipWorker.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/SendGossipWorker.kt
@@ -21,14 +21,15 @@ import androidx.work.WorkerParameters
 import com.squareup.moshi.JsonClass
 import org.matrix.android.sdk.api.auth.data.Credentials
 import org.matrix.android.sdk.api.failure.shouldBeRetried
+import org.matrix.android.sdk.api.session.crypto.model.GossipingRequestState
+import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.content.SecretSendEventContent
 import org.matrix.android.sdk.api.session.events.model.toContent
 import org.matrix.android.sdk.internal.SessionManager
 import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
 import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
-import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
-import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
 import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId
@@ -38,8 +39,11 @@ import org.matrix.android.sdk.internal.worker.SessionWorkerParams
 import timber.log.Timber
 import javax.inject.Inject
 
-internal class SendGossipWorker(context: Context, params: WorkerParameters, sessionManager: SessionManager) :
-    SessionSafeCoroutineWorker<SendGossipWorker.Params>(context, params, sessionManager, Params::class.java) {
+internal class SendGossipWorker(
+        context: Context,
+        params: WorkerParameters,
+        sessionManager: SessionManager
+) : SessionSafeCoroutineWorker<SendGossipWorker.Params>(context, params, sessionManager, Params::class.java) {
 
     @JsonClass(generateAdapter = true)
     internal data class Params(
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 ab2ed04dfbe5793db1c13780bc2e9d7afd9fdd93..fffc2b4d4bbd05e01432cb54a888ef29d9e1f10d 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,14 +16,18 @@
 
 package org.matrix.android.sdk.internal.crypto.actions
 
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withContext
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.logger.LoggerTag
+import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
+import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
 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 org.matrix.android.sdk.internal.session.SessionScope
 import timber.log.Timber
 import javax.inject.Inject
 
@@ -31,90 +35,90 @@ private const val ONE_TIME_KEYS_RETRY_COUNT = 3
 
 private val loggerTag = LoggerTag("EnsureOlmSessionsForDevicesAction", LoggerTag.CRYPTO)
 
+@SessionScope
 internal class EnsureOlmSessionsForDevicesAction @Inject constructor(
         private val olmDevice: MXOlmDevice,
+        private val coroutineDispatchers: MatrixCoroutineDispatchers,
         private val oneTimeKeysForUsersDeviceTask: ClaimOneTimeKeysForUsersDeviceTask) {
 
-    suspend fun handle(devicesByUser: Map<String, List<CryptoDeviceInfo>>, force: Boolean = false): MXUsersDevicesMap<MXOlmSessionResult> {
-        val devicesWithoutSession = ArrayList<CryptoDeviceInfo>()
+    private val ensureMutex = Mutex()
 
-        val results = MXUsersDevicesMap<MXOlmSessionResult>()
+    /**
+     * We want to synchronize a bit here, because we are iterating to check existing olm session and
+     * also adding some
+     */
+    suspend fun handle(devicesByUser: Map<String, List<CryptoDeviceInfo>>, force: Boolean = false): MXUsersDevicesMap<MXOlmSessionResult> {
+        ensureMutex.withLock {
+            val results = MXUsersDevicesMap<MXOlmSessionResult>()
+            val deviceList = devicesByUser.flatMap { it.value }
+            Timber.tag(loggerTag.value)
+                    .d("ensure olm forced:$force for ${deviceList.joinToString { it.shortDebugString() }}")
+            val devicesToCreateSessionWith = mutableListOf<CryptoDeviceInfo>()
+            if (force) {
+                // we take all devices and will query otk for them
+                devicesToCreateSessionWith.addAll(deviceList)
+            } else {
+                // only peek devices without active session
+                deviceList.forEach { deviceInfo ->
+                    val deviceId = deviceInfo.deviceId
+                    val userId = deviceInfo.userId
+                    val key = deviceInfo.identityKey() ?: return@forEach Unit.also {
+                        Timber.tag(loggerTag.value).w("Ignoring device ${deviceInfo.shortDebugString()} without identity key")
+                    }
 
-        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
+                    // is there a session that as been already used?
+                    val sessionId = olmDevice.getSessionId(key)
+                    if (sessionId.isNullOrEmpty()) {
+                        Timber.tag(loggerTag.value).d("Found no existing olm session ${deviceInfo.shortDebugString()} add to claim list")
+                        devicesToCreateSessionWith.add(deviceInfo)
+                    } else {
+                        Timber.tag(loggerTag.value).d("using olm session $sessionId for (${deviceInfo.userId}|$deviceId)")
+                        val olmSessionResult = MXOlmSessionResult(deviceInfo, sessionId)
+                        results.setObject(userId, deviceId, olmSessionResult)
+                    }
                 }
+            }
 
-                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)")
+            if (devicesToCreateSessionWith.isEmpty()) {
+                // no session to create
+                return results
+            }
+            val usersDevicesToClaim = MXUsersDevicesMap<String>().apply {
+                devicesToCreateSessionWith.forEach {
+                    setObject(it.userId, it.deviceId, MXKey.KEY_SIGNED_CURVE_25519_TYPE)
                 }
-
-                val olmSessionResult = MXOlmSessionResult(deviceInfo, sessionId)
-                results.setObject(userId, deviceId, olmSessionResult)
             }
-        }
-
-        Timber.tag(loggerTag.value).d("Devices without olm session (count:${devicesWithoutSession.size}) :" +
-                " ${devicesWithoutSession.joinToString { "${it.userId}|${it.deviceId}" }}")
-        if (devicesWithoutSession.size == 0) {
-            return results
-        }
-
-        // Prepare the request for claiming one-time keys
-        val usersDevicesToClaim = MXUsersDevicesMap<String>()
-
-        val oneTimeKeyAlgorithm = MXKey.KEY_SIGNED_CURVE_25519_TYPE
 
-        for (device in devicesWithoutSession) {
-            usersDevicesToClaim.setObject(device.userId, device.deviceId, oneTimeKeyAlgorithm)
-        }
+            // Let's now claim one time keys
+            val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim)
+            val oneTimeKeys = withContext(coroutineDispatchers.io) {
+                oneTimeKeysForUsersDeviceTask.executeRetry(claimParams, ONE_TIME_KEYS_RETRY_COUNT)
+            }
 
-        // TODO: this has a race condition - if we try to send another message
-        // while we are claiming a key, we will end up claiming two and setting up
-        // two sessions.
-        //
-        // That should eventually resolve itself, but it's poor form.
-
-        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.tag(loggerTag.value).v("claimOneTimeKeysForUsersDevices() : keysClaimResponse.oneTimeKeys: $oneTimeKeys")
-        for ((userId, deviceInfos) in devicesByUser) {
-            for (deviceInfo in deviceInfos) {
-                var oneTimeKey: MXKey? = null
-                val deviceIds = oneTimeKeys.getUserDeviceIds(userId)
-                if (null != deviceIds) {
-                    for (deviceId in deviceIds) {
-                        val olmSessionResult = results.getObject(userId, deviceId)
-                        if (olmSessionResult?.sessionId != null && !force) {
-                            // We already have a result for this device
-                            continue
-                        }
-                        val key = oneTimeKeys.getObject(userId, deviceId)
-                        if (key?.type == oneTimeKeyAlgorithm) {
-                            oneTimeKey = key
-                        }
-                        if (oneTimeKey == null) {
-                            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)
+            // let now start olm session using the new otks
+            devicesToCreateSessionWith.forEach { deviceInfo ->
+                val userId = deviceInfo.userId
+                val deviceId = deviceInfo.deviceId
+                // Did we get an OTK
+                val oneTimeKey = oneTimeKeys.getObject(userId, deviceId)
+                if (oneTimeKey == null) {
+                    Timber.tag(loggerTag.value).d("No otk for ${deviceInfo.shortDebugString()}")
+                } else if (oneTimeKey.type != MXKey.KEY_SIGNED_CURVE_25519_TYPE) {
+                    Timber.tag(loggerTag.value).d("Bad otk type (${oneTimeKey.type}) for ${deviceInfo.shortDebugString()}")
+                } else {
+                    val olmSessionId = verifyKeyAndStartSession(oneTimeKey, userId, deviceInfo)
+                    if (olmSessionId != null) {
+                        val olmSessionResult = MXOlmSessionResult(deviceInfo, olmSessionId)
+                        results.setObject(userId, deviceId, olmSessionResult)
+                    } else {
+                        Timber
+                                .tag(loggerTag.value)
+                                .d("## CRYPTO | cant unwedge failed to create outbound ${deviceInfo.shortDebugString()}")
                     }
                 }
             }
+            return results
         }
-        return results
     }
 
     private fun verifyKeyAndStartSession(oneTimeKey: MXKey, userId: String, deviceInfo: CryptoDeviceInfo): String? {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt
index a3cfbd91f00467d704f3e61948a7d46fcb9594e4..fc211537a6872f02dece4f11d9ce77e82df9ec99 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForUsersAction.kt
@@ -16,9 +16,9 @@
 
 package org.matrix.android.sdk.internal.crypto.actions
 
+import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
 import org.matrix.android.sdk.internal.crypto.MXOlmDevice
 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.store.IMXCryptoStore
 import timber.log.Timber
 import javax.inject.Inject
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt
index 0d78f68e5c6527f5c5b4e17ebce369859560d171..f9bcdf2c68091a3c64e9c6df2340b8908bbea3b0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt
@@ -18,12 +18,13 @@ package org.matrix.android.sdk.internal.crypto.actions
 
 import androidx.annotation.WorkerThread
 import org.matrix.android.sdk.api.listeners.ProgressListener
+import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
+import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
 import org.matrix.android.sdk.internal.crypto.MXOlmDevice
 import org.matrix.android.sdk.internal.crypto.MegolmSessionData
 import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
 import org.matrix.android.sdk.internal.crypto.RoomDecryptorProvider
-import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult
-import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
+import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXMegolmDecryption
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import timber.log.Timber
 import javax.inject.Inject
@@ -76,7 +77,11 @@ internal class MegolmSessionDataImporter @Inject constructor(private val olmDevi
                     outgoingGossipingRequestManager.cancelRoomKeyRequest(roomKeyRequestBody)
 
                     // Have another go at decrypting events sent with this session
-                    decrypting.onNewSession(megolmSessionData.senderKey!!, sessionId!!)
+                    when (decrypting) {
+                        is MXMegolmDecryption -> {
+                            decrypting.onNewSession(megolmSessionData.roomId, megolmSessionData.senderKey!!, sessionId!!)
+                        }
+                    }
                 } catch (e: Exception) {
                     Timber.e(e, "## importRoomKeys() : onNewSession failed")
                 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt
index 165f200bacfab78d40c09c7914bc6ae1914a85db..9bbbab4992a4b1bb2bb676b3edbe1227e7c61419 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MessageEncrypter.kt
@@ -16,10 +16,11 @@
 
 package org.matrix.android.sdk.internal.crypto.actions
 
+import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_OLM
+import org.matrix.android.sdk.api.logger.LoggerTag
+import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
 import org.matrix.android.sdk.api.session.events.model.Content
-import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_OLM
 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.rest.EncryptedMessage
 import org.matrix.android.sdk.internal.di.DeviceId
 import org.matrix.android.sdk.internal.di.UserId
@@ -28,6 +29,8 @@ import org.matrix.android.sdk.internal.util.convertToUTF8
 import timber.log.Timber
 import javax.inject.Inject
 
+private val loggerTag = LoggerTag("MessageEncrypter", LoggerTag.CRYPTO)
+
 internal class MessageEncrypter @Inject constructor(
         @UserId
         private val userId: String,
@@ -42,7 +45,7 @@ internal class MessageEncrypter @Inject constructor(
      * @param deviceInfos   list of device infos to encrypt for.
      * @return the content for an m.room.encrypted event.
      */
-    fun encryptMessage(payloadFields: Content, deviceInfos: List<CryptoDeviceInfo>): EncryptedMessage {
+    suspend fun encryptMessage(payloadFields: Content, deviceInfos: List<CryptoDeviceInfo>): EncryptedMessage {
         val deviceInfoParticipantKey = deviceInfos.associateBy { it.identityKey()!! }
 
         val payloadJson = payloadFields.toMutableMap()
@@ -66,7 +69,7 @@ internal class MessageEncrypter @Inject constructor(
             val sessionId = olmDevice.getSessionId(deviceKey)
 
             if (!sessionId.isNullOrEmpty()) {
-                Timber.v("Using sessionid $sessionId for device $deviceKey")
+                Timber.tag(loggerTag.value).d("Using sessionid $sessionId for device $deviceKey")
 
                 payloadJson["recipient"] = deviceInfo.userId
                 payloadJson["recipient_keys"] = mapOf("ed25519" to deviceInfo.fingerprint()!!)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/SetDeviceVerificationAction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/SetDeviceVerificationAction.kt
index 40eddc82bd0d2be16abd32cf1309f5c434efd099..60181138fb976f820119caecfc9edcd8570c3acb 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/SetDeviceVerificationAction.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/SetDeviceVerificationAction.kt
@@ -16,7 +16,7 @@
 
 package org.matrix.android.sdk.internal.crypto.actions
 
-import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel
+import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
 import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import org.matrix.android.sdk.internal.di.UserId
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt
index 79c7608cbf1ba9e6820f86bcb1dadf64b8cda8c5..2ea4e1dd29f5ddec3466990554233c345f4d0bcb 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt
@@ -17,10 +17,10 @@
 package org.matrix.android.sdk.internal.crypto.algorithms
 
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
+import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
+import org.matrix.android.sdk.api.session.crypto.model.IncomingSecretShareRequest
+import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
 import org.matrix.android.sdk.api.session.events.model.Event
-import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest
-import org.matrix.android.sdk.internal.crypto.IncomingSecretShareRequest
-import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
 import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
 
 /**
@@ -36,7 +36,7 @@ internal interface IMXDecrypting {
      * @return the decryption information, or an error
      */
     @Throws(MXCryptoError::class)
-    fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult
+    suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult
 
     /**
      * Handle a key event.
@@ -45,14 +45,6 @@ internal interface IMXDecrypting {
      */
     fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) {}
 
-    /**
-     * Check if the some messages can be decrypted with a new session
-     *
-     * @param senderKey the session sender key
-     * @param sessionId the session id
-     */
-    fun onNewSession(senderKey: String, sessionId: String) {}
-
     /**
      * Determine if we have the keys necessary to respond to a room key request
      *
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXGroupEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXGroupEncryption.kt
index 1fd5061a65c8e1b99c8aca3c064d153c4275a029..6f488def0af0d366df361e76999efcfa1e0f637f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXGroupEncryption.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXGroupEncryption.kt
@@ -45,7 +45,7 @@ internal interface IMXGroupEncryption {
      *
      * @return true in case of success
      */
-    suspend fun reshareKey(sessionId: String,
+    suspend fun reshareKey(groupSessionId: String,
                            userId: String,
                            deviceId: String,
                            senderKey: String): Boolean
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXWithHeldExtension.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXWithHeldExtension.kt
index 91f10adf4c00cefe4d0d21fbb912314f5b3e6ce4..585bcdbbdef98f5f542ef0af5156eceaacad2d27 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXWithHeldExtension.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXWithHeldExtension.kt
@@ -16,7 +16,7 @@
 
 package org.matrix.android.sdk.internal.crypto.algorithms
 
-import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent
+import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
 
 internal interface IMXWithHeldExtension {
     fun onRoomKeyWithHeldEvent(withHeldInfo: RoomKeyWithHeldContent)
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 2ee24dfbb064d29cfa93a99778fd6e88801e0ee6..4c407c9eb967a8e251ad065ac108eb4d569f0630 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,29 +19,30 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm
 import dagger.Lazy
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.withLock
 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.crypto.NewSessionListener
+import org.matrix.android.sdk.api.session.crypto.model.ForwardedRoomKeyContent
+import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
+import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
+import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
+import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
+import org.matrix.android.sdk.api.session.events.model.content.RoomKeyContent
+import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.internal.crypto.DeviceListManager
-import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest
-import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
 import org.matrix.android.sdk.internal.crypto.MXOlmDevice
-import org.matrix.android.sdk.internal.crypto.NewSessionListener
 import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
 import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
 import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
 import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting
 import org.matrix.android.sdk.internal.crypto.algorithms.IMXWithHeldExtension
 import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
-import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
-import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
-import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyContent
-import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent
-import org.matrix.android.sdk.internal.crypto.model.rest.ForwardedRoomKeyContent
-import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
 import org.matrix.android.sdk.internal.session.StreamEventsManager
@@ -71,7 +72,7 @@ internal class MXMegolmDecryption(private val userId: String,
 //    private var pendingEvents: MutableMap<String /* senderKey|sessionId */, MutableMap<String /* timelineId */, MutableList<Event>>> = HashMap()
 
     @Throws(MXCryptoError::class)
-    override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
+    override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
         // If cross signing is enabled, we don't send request until the keys are trusted
         // There could be a race effect here when xsigning is enabled, we should ensure that keys was downloaded once
         val requestOnFail = cryptoStore.getMyCrossSigningInfo()?.isTrusted() == true
@@ -79,7 +80,7 @@ internal class MXMegolmDecryption(private val userId: String,
     }
 
     @Throws(MXCryptoError::class)
-    private fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult {
+    private suspend fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult {
         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)
@@ -317,19 +318,20 @@ internal class MXMegolmDecryption(private val userId: String,
 
             outgoingGossipingRequestManager.cancelRoomKeyRequest(content)
 
-            onNewSession(senderKey, roomKeyContent.sessionId)
+            onNewSession(roomKeyContent.roomId, senderKey, roomKeyContent.sessionId)
         }
     }
 
     /**
      * Check if the some messages can be decrypted with a new session
      *
+     * @param roomId the room id where the new Megolm session has been created for, may be null when importing from external sessions
      * @param senderKey the session sender key
      * @param sessionId the session id
      */
-    override fun onNewSession(senderKey: String, sessionId: String) {
+    fun onNewSession(roomId: String?, senderKey: String, sessionId: String) {
         Timber.tag(loggerTag.value).v("ON NEW SESSION $sessionId - $senderKey")
-        newSessionListener?.onNewSession(null, senderKey, sessionId)
+        newSessionListener?.onNewSession(roomId, senderKey, sessionId)
     }
 
     override fun hasKeysForKeyRequest(request: IncomingRoomKeyRequest): Boolean {
@@ -345,7 +347,22 @@ internal class MXMegolmDecryption(private val userId: String,
             return
         }
         val userId = request.userId ?: return
+
         cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
+            val body = request.requestBody
+            val sessionHolder = try {
+                olmDevice.getInboundGroupSession(body.sessionId, body.senderKey, body.roomId)
+            } catch (failure: Throwable) {
+                Timber.tag(loggerTag.value).e(failure, "shareKeysWithDevice: failed to get session for request $body")
+                return@launch
+            }
+
+            val export = sessionHolder.mutex.withLock {
+                sessionHolder.wrapper.exportKeys()
+            } ?: return@launch Unit.also {
+                Timber.tag(loggerTag.value).e("shareKeysWithDevice: failed to export group session ${body.sessionId}")
+            }
+
             runCatching { deviceListManager.downloadKeys(listOf(userId), false) }
                     .mapCatching {
                         val deviceId = request.deviceId
@@ -355,7 +372,6 @@ internal class MXMegolmDecryption(private val userId: String,
                         } else {
                             val devicesByUser = mapOf(userId to listOf(deviceInfo))
                             val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser)
-                            val body = request.requestBody
                             val olmSessionResult = usersDeviceMap.getObject(userId, deviceId)
                             if (olmSessionResult?.sessionId == null) {
                                 // no session with this device, probably because there
@@ -365,19 +381,10 @@ internal class MXMegolmDecryption(private val userId: String,
                             }
                             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) }
-                                    .fold(
-                                            {
-                                                // TODO
-                                                payloadJson["content"] = it.exportKeys() ?: ""
-                                            },
-                                            {
-                                                // TODO
-                                                Timber.tag(loggerTag.value).e(it, "shareKeysWithDevice: failed to get session for request $body")
-                                            }
-
-                                    )
+                            val payloadJson = mapOf(
+                                    "type" to EventType.FORWARDED_ROOM_KEY,
+                                    "content" to export
+                            )
 
                             val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
                             val sendToDeviceMap = MXUsersDevicesMap<Any>()
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 389036a1f86bd964907d50774e71bac5937afa44..f0521942304acb1877e17c4c4cd4b6ad96748c17 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
@@ -18,25 +18,27 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm
 
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withContext
 import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
+import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
 import org.matrix.android.sdk.api.logger.LoggerTag
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
+import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
+import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
+import org.matrix.android.sdk.api.session.crypto.model.forEach
 import org.matrix.android.sdk.api.session.events.model.Content
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
+import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
 import org.matrix.android.sdk.internal.crypto.DeviceListManager
-import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
 import org.matrix.android.sdk.internal.crypto.MXOlmDevice
 import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
 import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
 import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
 import org.matrix.android.sdk.internal.crypto.algorithms.IMXGroupEncryption
 import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
-import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
-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
@@ -88,7 +90,7 @@ internal class MXMegolmEncryption(
         Timber.tag(loggerTag.value).v("encryptEventContent : getDevicesInRoom")
         val devices = getDevicesInRoom(userIds)
         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}")
+        Timber.tag(loggerTag.value).v("encryptEventContent ${System.currentTimeMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.toDebugString()}")
         val outboundSession = ensureOutboundSession(devices.allowedDevices)
 
         return encryptContent(outboundSession, eventType, eventContent)
@@ -142,8 +144,9 @@ internal class MXMegolmEncryption(
         Timber.tag(loggerTag.value).v("prepareNewSessionInRoom() ")
         val sessionId = olmDevice.createOutboundGroupSessionForRoom(roomId)
 
-        val keysClaimedMap = HashMap<String, String>()
-        keysClaimedMap["ed25519"] = olmDevice.deviceEd25519Key!!
+        val keysClaimedMap = mapOf(
+                "ed25519" to olmDevice.deviceEd25519Key!!
+        )
 
         olmDevice.addInboundGroupSession(sessionId!!, olmDevice.getSessionKey(sessionId)!!, roomId, olmDevice.deviceCurve25519Key!!,
                 emptyList(), keysClaimedMap, false)
@@ -303,11 +306,13 @@ internal class MXMegolmEncryption(
             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)
+                withContext(coroutineDispatchers.io) {
+                    sendToDeviceTask.execute(sendToDeviceParams)
+                }
                 Timber.tag(loggerTag.value).i("shareUserDevicesKey() : sendToDevice succeeds after ${System.currentTimeMillis() - t0} ms")
             } catch (failure: Throwable) {
                 // What to do here...
-                Timber.tag(loggerTag.value).e("shareUserDevicesKey() : Failed to share session <${session.sessionId}> with $devicesByUser ")
+                Timber.tag(loggerTag.value).e("shareUserDevicesKey() : Failed to share <${session.sessionId}>")
             }
         } else {
             Timber.tag(loggerTag.value).i("shareUserDevicesKey() : no need to share key")
@@ -346,9 +351,12 @@ internal class MXMegolmEncryption(
                 }
         )
         try {
-            sendToDeviceTask.execute(params)
+            withContext(coroutineDispatchers.io) {
+                sendToDeviceTask.execute(params)
+            }
         } catch (failure: Throwable) {
-            Timber.tag(loggerTag.value).e("notifyKeyWithHeld() : Failed to notify withheld key for $targets session: $sessionId ")
+            Timber.tag(loggerTag.value)
+                    .e("notifyKeyWithHeld() :$sessionId Failed to send withheld  ${targets.map { "${it.userId}|${it.deviceId}" }}")
         }
     }
 
@@ -432,20 +440,20 @@ internal class MXMegolmEncryption(
         }
     }
 
-    override suspend fun reshareKey(sessionId: String,
+    override suspend fun reshareKey(groupSessionId: String,
                                     userId: String,
                                     deviceId: String,
                                     senderKey: String): Boolean {
-        Timber.tag(loggerTag.value).i("process reshareKey for $sessionId to $userId:$deviceId")
+        Timber.tag(loggerTag.value).i("process reshareKey for $groupSessionId to $userId:$deviceId")
         val deviceInfo = cryptoStore.getUserDevice(userId, deviceId) ?: return false
                 .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)
+        val wasSessionSharedWithUser = cryptoStore.getSharedSessionInfo(roomId, groupSessionId, deviceInfo)
         if (!wasSessionSharedWithUser.found) {
             // This session was never shared with this user
             // Send a room key with held
-            notifyKeyWithHeld(listOf(UserDevice(userId, deviceId)), sessionId, senderKey, WithHeldCode.UNAUTHORISED)
+            notifyKeyWithHeld(listOf(UserDevice(userId, deviceId)), groupSessionId, senderKey, WithHeldCode.UNAUTHORISED)
             Timber.tag(loggerTag.value).w("reshareKey: ERROR : Never shared megolm with this device")
             return false
         }
@@ -456,42 +464,47 @@ internal class MXMegolmEncryption(
                 }
 
         val devicesByUser = mapOf(userId to listOf(deviceInfo))
-        val usersDeviceMap = ensureOlmSessionsForDevicesAction.handle(devicesByUser)
-        val olmSessionResult = usersDeviceMap.getObject(userId, deviceId)
-        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.tag(loggerTag.value).w("reshareKey: no session with this device, probably because there were no one-time keys")
-                }
-
-        Timber.tag(loggerTag.value).i(" reshareKey: sharing keys for session $senderKey|$sessionId:$chainIndex with device $userId:$deviceId")
+        val usersDeviceMap = try {
+            ensureOlmSessionsForDevicesAction.handle(devicesByUser)
+        } catch (failure: Throwable) {
+            null
+        }
+        val olmSessionResult = usersDeviceMap?.getObject(userId, deviceId)
+        if (olmSessionResult?.sessionId == null) {
+            Timber.tag(loggerTag.value).w("reshareKey: no session with this device, probably because there were no one-time keys")
+            return false
+        }
+        Timber.tag(loggerTag.value).i(" reshareKey: $groupSessionId:$chainIndex with device $userId:$deviceId using session ${olmSessionResult.sessionId}")
 
-        val payloadJson = mutableMapOf<String, Any>("type" to EventType.FORWARDED_ROOM_KEY)
+        val sessionHolder = try {
+            olmDevice.getInboundGroupSession(groupSessionId, senderKey, roomId)
+        } catch (failure: Throwable) {
+            Timber.tag(loggerTag.value).e(failure, "shareKeysWithDevice: failed to get session $groupSessionId")
+            return false
+        }
 
-        runCatching { olmDevice.getInboundGroupSession(sessionId, senderKey, roomId) }
-                .fold(
-                        {
-                            // TODO
-                            payloadJson["content"] = it.exportKeys(chainIndex.toLong()) ?: ""
-                        },
-                        {
-                            // TODO
-                            Timber.tag(loggerTag.value).e(it, "reshareKey: failed to get session $sessionId|$senderKey|$roomId")
-                        }
+        val export = sessionHolder.mutex.withLock {
+            sessionHolder.wrapper.exportKeys()
+        } ?: return false.also {
+            Timber.tag(loggerTag.value).e("shareKeysWithDevice: failed to export group session $groupSessionId")
+        }
 
-                )
+        val payloadJson = mapOf(
+                "type" to EventType.FORWARDED_ROOM_KEY,
+                "content" to export
+        )
 
         val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
         val sendToDeviceMap = MXUsersDevicesMap<Any>()
         sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
-        Timber.tag(loggerTag.value).i("reshareKey() : sending session $sessionId to $userId:$deviceId")
+        Timber.tag(loggerTag.value).i("reshareKey() : sending session $groupSessionId to $userId:$deviceId")
         val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
         return try {
             sendToDeviceTask.execute(sendToDeviceParams)
-            Timber.tag(loggerTag.value).i("reshareKey() : successfully send <$sessionId> to $userId:$deviceId")
+            Timber.tag(loggerTag.value).i("reshareKey() : successfully send <$groupSessionId> to $userId:$deviceId")
             true
         } catch (failure: Throwable) {
-            Timber.tag(loggerTag.value).e(failure, "reshareKey() : fail to send <$sessionId> to $userId:$deviceId")
+            Timber.tag(loggerTag.value).e(failure, "reshareKey() : fail to send <$groupSessionId> to $userId:$deviceId")
             false
         }
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt
index b70e6c1f80f9862d11366df643bca96bbab9e200..091abd49746f1c5e8dd2889e68bd1b261433b1dc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt
@@ -16,8 +16,8 @@
 
 package org.matrix.android.sdk.internal.crypto.algorithms.megolm
 
-import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
-import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
+import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
+import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
 import timber.log.Timber
 
 internal class MXOutboundSessionInfo(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/SharedWithHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/SharedWithHelper.kt
index a64e5af0d331c81d922d4a7e129af103351a6b81..59d78c3e05c8036a98b33720c24c14dd88868258 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/SharedWithHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/SharedWithHelper.kt
@@ -16,8 +16,8 @@
 
 package org.matrix.android.sdk.internal.crypto.algorithms.megolm
 
-import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
-import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
+import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
+import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 
 internal class SharedWithHelper(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt
index f1bca4fbc6b181c0f6902ea8835bfd7aa3370e4a..0db8700852fba67366db25d80614c5346d2e397c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt
@@ -16,49 +16,53 @@
 
 package org.matrix.android.sdk.internal.crypto.algorithms.olm
 
+import kotlinx.coroutines.sync.withLock
+import org.matrix.android.sdk.api.logger.LoggerTag
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
+import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
 import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent
+import org.matrix.android.sdk.api.session.events.model.content.OlmPayloadContent
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE
 import org.matrix.android.sdk.api.util.JsonDict
-import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
 import org.matrix.android.sdk.internal.crypto.MXOlmDevice
 import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting
-import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent
-import org.matrix.android.sdk.internal.crypto.model.event.OlmPayloadContent
 import org.matrix.android.sdk.internal.di.MoshiProvider
 import org.matrix.android.sdk.internal.util.convertFromUTF8
 import timber.log.Timber
 
+private val loggerTag = LoggerTag("MXOlmDecryption", LoggerTag.CRYPTO)
+
 internal class MXOlmDecryption(
         // The olm device interface
         private val olmDevice: MXOlmDevice,
         // the matrix userId
         private val userId: String) :
-    IMXDecrypting {
+        IMXDecrypting {
 
     @Throws(MXCryptoError::class)
-    override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
+    override suspend fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
         val olmEventContent = event.content.toModel<OlmEventContent>() ?: run {
-            Timber.e("## decryptEvent() : bad event format")
+            Timber.tag(loggerTag.value).e("## decryptEvent() : bad event format")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_EVENT_FORMAT,
                     MXCryptoError.BAD_EVENT_FORMAT_TEXT_REASON)
         }
 
         val cipherText = olmEventContent.ciphertext ?: run {
-            Timber.e("## decryptEvent() : missing cipher text")
+            Timber.tag(loggerTag.value).e("## decryptEvent() : missing cipher text")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_CIPHER_TEXT,
                     MXCryptoError.MISSING_CIPHER_TEXT_REASON)
         }
 
         val senderKey = olmEventContent.senderKey ?: run {
-            Timber.e("## decryptEvent() : missing sender key")
+            Timber.tag(loggerTag.value).e("## decryptEvent() : missing sender key")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY,
                     MXCryptoError.MISSING_SENDER_KEY_TEXT_REASON)
         }
 
         val messageAny = cipherText[olmDevice.deviceCurve25519Key] ?: run {
-            Timber.e("## decryptEvent() : our device ${olmDevice.deviceCurve25519Key} is not included in recipients")
+            Timber.tag(loggerTag.value).e("## decryptEvent() : our device ${olmDevice.deviceCurve25519Key} is not included in recipients")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.NOT_INCLUDE_IN_RECIPIENTS, MXCryptoError.NOT_INCLUDED_IN_RECIPIENT_REASON)
         }
 
@@ -69,7 +73,7 @@ internal class MXOlmDecryption(
         val decryptedPayload = decryptMessage(message, senderKey)
 
         if (decryptedPayload == null) {
-            Timber.e("## decryptEvent() Failed to decrypt Olm event (id= ${event.eventId} from $senderKey")
+            Timber.tag(loggerTag.value).e("## decryptEvent() Failed to decrypt Olm event (id= ${event.eventId} from $senderKey")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
         }
         val payloadString = convertFromUTF8(decryptedPayload)
@@ -78,30 +82,30 @@ internal class MXOlmDecryption(
         val payload = adapter.fromJson(payloadString)
 
         if (payload == null) {
-            Timber.e("## decryptEvent failed : null payload")
+            Timber.tag(loggerTag.value).e("## decryptEvent failed : null payload")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, MXCryptoError.MISSING_CIPHER_TEXT_REASON)
         }
 
         val olmPayloadContent = OlmPayloadContent.fromJsonString(payloadString) ?: run {
-            Timber.e("## decryptEvent() : bad olmPayloadContent format")
+            Timber.tag(loggerTag.value).e("## decryptEvent() : bad olmPayloadContent format")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON)
         }
 
         if (olmPayloadContent.recipient.isNullOrBlank()) {
             val reason = String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient")
-            Timber.e("## decryptEvent() : $reason")
+            Timber.tag(loggerTag.value).e("## decryptEvent() : $reason")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, reason)
         }
 
         if (olmPayloadContent.recipient != userId) {
-            Timber.e("## decryptEvent() : Event ${event.eventId}:" +
+            Timber.tag(loggerTag.value).e("## decryptEvent() : Event ${event.eventId}:" +
                     " Intended recipient ${olmPayloadContent.recipient} does not match our id $userId")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_RECIPIENT,
                     String.format(MXCryptoError.BAD_RECIPIENT_REASON, olmPayloadContent.recipient))
         }
 
         val recipientKeys = olmPayloadContent.recipientKeys ?: run {
-            Timber.e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'recipient_keys'" +
+            Timber.tag(loggerTag.value).e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'recipient_keys'" +
                     " property; cannot prevent unknown-key attack")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY,
                     String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "recipient_keys"))
@@ -110,31 +114,34 @@ internal class MXOlmDecryption(
         val ed25519 = recipientKeys["ed25519"]
 
         if (ed25519 != olmDevice.deviceEd25519Key) {
-            Timber.e("## decryptEvent() : Event ${event.eventId}: Intended recipient ed25519 key $ed25519 did not match ours")
+            Timber.tag(loggerTag.value).e("## decryptEvent() : Event ${event.eventId}: Intended recipient ed25519 key $ed25519 did not match ours")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_RECIPIENT_KEY,
                     MXCryptoError.BAD_RECIPIENT_KEY_REASON)
         }
 
         if (olmPayloadContent.sender.isNullOrBlank()) {
-            Timber.e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'sender' property; cannot prevent unknown-key attack")
+            Timber.tag(loggerTag.value)
+                    .e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'sender' property; cannot prevent unknown-key attack")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY,
                     String.format(MXCryptoError.ERROR_MISSING_PROPERTY_REASON, "sender"))
         }
 
         if (olmPayloadContent.sender != event.senderId) {
-            Timber.e("Event ${event.eventId}: original sender ${olmPayloadContent.sender} does not match reported sender ${event.senderId}")
+            Timber.tag(loggerTag.value)
+                    .e("Event ${event.eventId}:  sender ${olmPayloadContent.sender} does not match reported sender ${event.senderId}")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.FORWARDED_MESSAGE,
                     String.format(MXCryptoError.FORWARDED_MESSAGE_REASON, olmPayloadContent.sender))
         }
 
         if (olmPayloadContent.roomId != event.roomId) {
-            Timber.e("## decryptEvent() : Event ${event.eventId}: original room ${olmPayloadContent.roomId} does not match reported room ${event.roomId}")
+            Timber.tag(loggerTag.value)
+                    .e("## decryptEvent() : Event ${event.eventId}:  room ${olmPayloadContent.roomId} does not match reported room ${event.roomId}")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ROOM,
                     String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.roomId))
         }
 
         val keys = olmPayloadContent.keys ?: run {
-            Timber.e("## decryptEvent failed : null keys")
+            Timber.tag(loggerTag.value).e("## decryptEvent failed : null keys")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT,
                     MXCryptoError.MISSING_CIPHER_TEXT_REASON)
         }
@@ -153,8 +160,8 @@ internal class MXOlmDecryption(
      * @param message                message object, with 'type' and 'body' fields.
      * @return payload, if decrypted successfully.
      */
-    private fun decryptMessage(message: JsonDict, theirDeviceIdentityKey: String): String? {
-        val sessionIds = olmDevice.getSessionIds(theirDeviceIdentityKey).orEmpty()
+    private suspend fun decryptMessage(message: JsonDict, theirDeviceIdentityKey: String): String? {
+        val sessionIds = olmDevice.getSessionIds(theirDeviceIdentityKey)
 
         val messageBody = message["body"] as? String ?: return null
         val messageType = when (val typeAsVoid = message["type"]) {
@@ -166,11 +173,32 @@ internal class MXOlmDecryption(
 
         // Try each session in turn
         // decryptionErrors = {};
+
+        val isPreKey = messageType == 0
+        // we want to synchronize on prekey if not we could end up create two olm sessions
+        // Not very clear but it looks like the js-sdk for consistency
+        return if (isPreKey) {
+            olmDevice.mutex.withLock {
+                reallyDecryptMessage(sessionIds, messageBody, messageType, theirDeviceIdentityKey)
+            }
+        } else {
+            reallyDecryptMessage(sessionIds, messageBody, messageType, theirDeviceIdentityKey)
+        }
+    }
+
+    private suspend fun reallyDecryptMessage(sessionIds: List<String>, messageBody: String, messageType: Int, theirDeviceIdentityKey: String): String? {
+        Timber.tag(loggerTag.value).d("decryptMessage() try to decrypt olm message type:$messageType from ${sessionIds.size} known sessions")
         for (sessionId in sessionIds) {
-            val payload = olmDevice.decryptMessage(messageBody, messageType, sessionId, theirDeviceIdentityKey)
+            val payload = try {
+                olmDevice.decryptMessage(messageBody, messageType, sessionId, theirDeviceIdentityKey)
+            } catch (throwable: Exception) {
+                // As we are trying one by one, we don't really care of the error here
+                Timber.tag(loggerTag.value).d("decryptMessage() failed with session $sessionId")
+                null
+            }
 
             if (null != payload) {
-                Timber.v("## decryptMessage() : Decrypted Olm message from $theirDeviceIdentityKey with session $sessionId")
+                Timber.tag(loggerTag.value).v("## decryptMessage() : Decrypted Olm message from $theirDeviceIdentityKey with session $sessionId")
                 return payload
             } else {
                 val foundSession = olmDevice.matchesSession(theirDeviceIdentityKey, sessionId, messageType, messageBody)
@@ -178,7 +206,7 @@ internal class MXOlmDecryption(
                 if (foundSession) {
                     // Decryption failed, but it was a prekey message matching this
                     // session, so it should have worked.
-                    Timber.e("## decryptMessage() : Error decrypting prekey message with existing session id $sessionId:TODO")
+                    Timber.tag(loggerTag.value).e("## decryptMessage() : Error decrypting prekey message with existing session id $sessionId:TODO")
                     return null
                 }
             }
@@ -189,9 +217,9 @@ internal class MXOlmDecryption(
             // didn't work.
 
             if (sessionIds.isEmpty()) {
-                Timber.e("## decryptMessage() : No existing sessions")
+                Timber.tag(loggerTag.value).e("## decryptMessage() : No existing sessions")
             } else {
-                Timber.e("## decryptMessage() : Error decrypting non-prekey message with existing sessions")
+                Timber.tag(loggerTag.value).e("## decryptMessage() : Error decrypting non-prekey message with existing sessions")
             }
 
             return null
@@ -199,14 +227,17 @@ internal class MXOlmDecryption(
 
         // prekey message which doesn't match any existing sessions: make a new
         // session.
+        // XXXX Possible races here? if concurrent access for same prekey message, we might create 2 sessions?
+        Timber.tag(loggerTag.value).d("## decryptMessage() :  Create inbound group session from prekey sender:$theirDeviceIdentityKey")
+
         val res = olmDevice.createInboundSession(theirDeviceIdentityKey, messageType, messageBody)
 
         if (null == res) {
-            Timber.e("## decryptMessage() :  Error decrypting non-prekey message with existing sessions")
+            Timber.tag(loggerTag.value).e("## decryptMessage() :  Error decrypting non-prekey message with existing sessions")
             return null
         }
 
-        Timber.v("## decryptMessage() :  Created new inbound Olm session get id ${res["session_id"]} with $theirDeviceIdentityKey")
+        Timber.tag(loggerTag.value).v("## decryptMessage() :  Created new inbound Olm session get id ${res["session_id"]} with $theirDeviceIdentityKey")
 
         return res["payload"]
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryption.kt
index 63f2533ac32f2831378ec855cadbfb8f25de9824..7fdfd5a2876420745209733f323ed2b3a04a8839 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryption.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryption.kt
@@ -16,6 +16,7 @@
 
 package org.matrix.android.sdk.internal.crypto.algorithms.olm
 
+import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
 import org.matrix.android.sdk.api.session.events.model.Content
 import org.matrix.android.sdk.api.session.events.model.toContent
 import org.matrix.android.sdk.internal.crypto.DeviceListManager
@@ -23,7 +24,6 @@ import org.matrix.android.sdk.internal.crypto.MXOlmDevice
 import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForUsersAction
 import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
 import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
-import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 
 internal class MXOlmEncryption(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt
index cef86e8b5ec03f1fde51e4ca1f5d909f9187072a..f21f5e05e1416d081cdf9e0d54bba9e3d9e1bed9 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/api/CryptoApi.kt
@@ -15,9 +15,9 @@
  */
 package org.matrix.android.sdk.internal.crypto.api
 
+import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
+import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse
 import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams
-import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
-import org.matrix.android.sdk.internal.crypto.model.rest.DevicesListResponse
 import org.matrix.android.sdk.internal.crypto.model.rest.KeyChangesResponse
 import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimBody
 import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimResponse
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/EncryptionResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/EncryptionResult.kt
index ba5baba60d48499b28a01fd23805bae6889a545c..80090cf4a8a4612dbe698cc9dfa738fe772747e4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/EncryptionResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/EncryptionResult.kt
@@ -16,12 +16,12 @@
 
 package org.matrix.android.sdk.internal.crypto.attachments
 
-import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
+import org.matrix.android.sdk.api.session.crypto.model.EncryptedFileInfo
 
 /**
  * Define the result of an encryption file
  */
 internal data class EncryptionResult(
-        var encryptedFileInfo: EncryptedFileInfo,
-        var encryptedByteArray: ByteArray
+        val encryptedFileInfo: EncryptedFileInfo,
+        val encryptedByteArray: ByteArray
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt
index 70730326da12d90fa0397e12a9adf0777b571a3e..91b6af6fc3ad845e3e9f8119db198b7b9e1f99bd 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt
@@ -17,8 +17,9 @@
 package org.matrix.android.sdk.internal.crypto.attachments
 
 import android.util.Base64
-import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
-import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileKey
+import org.matrix.android.sdk.api.session.crypto.attachments.ElementToDecrypt
+import org.matrix.android.sdk.api.session.crypto.model.EncryptedFileInfo
+import org.matrix.android.sdk.api.session.crypto.model.EncryptedFileKey
 import org.matrix.android.sdk.internal.util.base64ToBase64Url
 import org.matrix.android.sdk.internal.util.base64ToUnpaddedBase64
 import org.matrix.android.sdk.internal.util.base64UrlToBase64
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MatrixDigestCheckInputStream.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MatrixDigestCheckInputStream.kt
deleted file mode 100644
index 2cbe0e3702749876395a495862ec2396f8c53ae2..0000000000000000000000000000000000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MatrixDigestCheckInputStream.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright 2020 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.crypto.attachments
-
-import android.util.Base64
-import org.matrix.android.sdk.internal.util.base64ToUnpaddedBase64
-import java.io.FilterInputStream
-import java.io.IOException
-import java.io.InputStream
-import java.security.MessageDigest
-
-class MatrixDigestCheckInputStream(
-        inputStream: InputStream?,
-        private val expectedDigest: String
-) : FilterInputStream(inputStream) {
-
-    private val digest = MessageDigest.getInstance("SHA-256")
-
-    @Throws(IOException::class)
-    override fun read(): Int {
-        val b = `in`.read()
-        if (b >= 0) {
-            digest.update(b.toByte())
-        }
-
-        if (b == -1) {
-            ensureDigest()
-        }
-        return b
-    }
-
-    @Throws(IOException::class)
-    override fun read(
-            b: ByteArray,
-            off: Int,
-            len: Int): Int {
-        val n = `in`.read(b, off, len)
-        if (n > 0) {
-            digest.update(b, off, n)
-        }
-
-        if (n == -1) {
-            ensureDigest()
-        }
-        return n
-    }
-
-    @Throws(IOException::class)
-    private fun ensureDigest() {
-        val currentDigestValue = base64ToUnpaddedBase64(Base64.encodeToString(digest.digest(), Base64.DEFAULT))
-        if (currentDigestValue != expectedDigest) {
-            throw IOException("Bad digest")
-        }
-    }
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ComputeTrustTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ComputeTrustTask.kt
index b470ab34bb703161487ef19ec9cdb36463dd2fa9..02ea9432848d36b19f99bffea984d0f4c34e1f33 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ComputeTrustTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ComputeTrustTask.kt
@@ -17,9 +17,9 @@ package org.matrix.android.sdk.internal.crypto.crosssigning
 
 import kotlinx.coroutines.withContext
 import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
-import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
 import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
+import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import org.matrix.android.sdk.internal.di.UserId
 import org.matrix.android.sdk.internal.task.Task
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt
index 83de06a66872abb103e9654744f4e5cde678fe55..ba1718688fd33c907d552a80d588e77d6df61ff5 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt
@@ -26,13 +26,20 @@ import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
 import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
+import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
+import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustResult
 import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
+import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo
+import org.matrix.android.sdk.api.session.crypto.crosssigning.UserTrustResult
+import org.matrix.android.sdk.api.session.crypto.crosssigning.isCrossSignedVerified
+import org.matrix.android.sdk.api.session.crypto.crosssigning.isLocallyVerified
+import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified
+import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
 import org.matrix.android.sdk.api.util.Optional
+import org.matrix.android.sdk.api.util.fromBase64
 import org.matrix.android.sdk.internal.crypto.DeviceListManager
-import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
 import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
-import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo
 import org.matrix.android.sdk.internal.crypto.tasks.InitializeCrossSigningTask
 import org.matrix.android.sdk.internal.crypto.tasks.UploadSignaturesTask
 import org.matrix.android.sdk.internal.di.SessionId
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/Extensions.kt
index cf2d6aa2693a4e5561f59e8e8ba58172b9990c1f..16098e5210ae6ee7b179110008bd6adc467214f1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/Extensions.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/Extensions.kt
@@ -16,8 +16,8 @@
 package org.matrix.android.sdk.internal.crypto.crosssigning
 
 import android.util.Base64
-import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey
-import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
+import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey
+import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
 import org.matrix.android.sdk.internal.util.JsonCanonicalizer
 import timber.log.Timber
 
@@ -29,14 +29,6 @@ internal fun CryptoCrossSigningKey.canonicalSignable(): String {
     return JsonCanonicalizer.getCanonicalJson(Map::class.java, signalableJSONDictionary())
 }
 
-fun ByteArray.toBase64NoPadding(): String {
-    return Base64.encodeToString(this, Base64.NO_PADDING or Base64.NO_WRAP)
-}
-
-fun String.fromBase64(): ByteArray {
-    return Base64.decode(this, Base64.DEFAULT)
-}
-
 /**
  * Decode the base 64. Return null in case of bad format. Should be used when parsing received data from external source
  */
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt
index 5cd647ff6fa05058ca1189f4638c32e0a1686341..74f0f5745d39d0745b923ef6f177b392e86f1dc6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt
@@ -22,9 +22,12 @@ import com.squareup.moshi.JsonClass
 import io.realm.Realm
 import io.realm.RealmConfiguration
 import io.realm.kotlin.where
-import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
 import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
+import org.matrix.android.sdk.api.session.crypto.crosssigning.UserTrustResult
+import org.matrix.android.sdk.api.session.crypto.crosssigning.isCrossSignedVerified
+import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified
+import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
 import org.matrix.android.sdk.internal.SessionManager
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper
@@ -52,7 +55,7 @@ import timber.log.Timber
 import javax.inject.Inject
 
 internal class UpdateTrustWorker(context: Context, params: WorkerParameters, sessionManager: SessionManager) :
-    SessionSafeCoroutineWorker<UpdateTrustWorker.Params>(context, params, sessionManager, Params::class.java) {
+        SessionSafeCoroutineWorker<UpdateTrustWorker.Params>(context, params, sessionManager, Params::class.java) {
 
     @JsonClass(generateAdapter = true)
     internal data class Params(
@@ -96,7 +99,7 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
         if (userList.isNotEmpty()) {
             // Unfortunately we don't have much info on what did exactly changed (is it the cross signing keys of that user,
             // or a new device?) So we check all again :/
-            Timber.d("## CrossSigning - Updating trust for users: ${userList.logLimit()}")
+            Timber.v("## CrossSigning - Updating trust for users: ${userList.logLimit()}")
             updateTrust(userList)
         }
 
@@ -148,7 +151,7 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
                     myUserId -> myTrustResult
                     else     -> {
                         crossSigningService.checkOtherMSKTrusted(myCrossSigningInfo, entry.value).also {
-                            Timber.d("## CrossSigning - user:${entry.key} result:$it")
+                            Timber.v("## CrossSigning - user:${entry.key} result:$it")
                         }
                     }
                 }
@@ -178,7 +181,7 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
                 // Update trust if needed
                 devicesEntities?.forEach { device ->
                     val crossSignedVerified = trustMap?.get(device)?.isCrossSignedVerified()
-                    Timber.d("## CrossSigning - Trust for ${device.userId}|${device.deviceId} : cross verified: ${trustMap?.get(device)}")
+                    Timber.v("## CrossSigning - Trust for ${device.userId}|${device.deviceId} : cross verified: ${trustMap?.get(device)}")
                     if (device.trustLevelEntity?.crossSignedVerified != crossSignedVerified) {
                         Timber.d("## CrossSigning - Trust change detected for ${device.userId}|${device.deviceId} : cross verified: $crossSignedVerified")
                         // need to save
@@ -216,7 +219,7 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
                                     .equalTo(RoomSummaryEntityFields.IS_ENCRYPTED, true)
                                     .findFirst()
                                     ?.let { roomSummary ->
-                                        Timber.d("## CrossSigning - Check shield state for room $roomId")
+                                        Timber.v("## CrossSigning - Check shield state for room $roomId")
                                         val allActiveRoomMembers = RoomMemberHelper(sessionRealm, roomId).getActiveRoomMemberIds()
                                         try {
                                             val updatedTrust = computeRoomShield(
@@ -277,7 +280,7 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
                                   cryptoRealm: Realm,
                                   activeMemberUserIds: List<String>,
                                   roomSummaryEntity: RoomSummaryEntity): RoomEncryptionTrustLevel {
-        Timber.d("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} -> ${activeMemberUserIds.logLimit()}")
+        Timber.v("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} -> ${activeMemberUserIds.logLimit()}")
         // The set of “all users” depends on the type of room:
         // For regular / topic rooms which have more than 2 members (including yourself) are considered when decorating a room
         // For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt
index b20168eaa3cd25ffe869d8ce252ae3595ee8f142..e63a6dc791c2a541688380de45c3eb9f57bd1374 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt
@@ -28,30 +28,37 @@ import kotlinx.coroutines.withContext
 import org.matrix.android.sdk.api.MatrixCallback
 import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.auth.data.Credentials
+import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
 import org.matrix.android.sdk.api.failure.Failure
 import org.matrix.android.sdk.api.failure.MatrixError
 import org.matrix.android.sdk.api.listeners.ProgressListener
 import org.matrix.android.sdk.api.listeners.StepProgressListener
+import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupLastVersionResult
 import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService
 import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
 import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener
-import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
+import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupVersionTrust
+import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupVersionTrustSignature
+import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
+import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult
+import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupAuthData
+import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
+import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo
+import org.matrix.android.sdk.api.session.crypto.keysbackup.computeRecoveryKey
+import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey
+import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult
+import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
+import org.matrix.android.sdk.api.util.awaitCallback
+import org.matrix.android.sdk.api.util.fromBase64
 import org.matrix.android.sdk.internal.crypto.MXOlmDevice
 import org.matrix.android.sdk.internal.crypto.MegolmSessionData
 import org.matrix.android.sdk.internal.crypto.ObjectSigner
 import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter
-import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64
-import org.matrix.android.sdk.internal.crypto.keysbackup.model.KeysBackupVersionTrust
-import org.matrix.android.sdk.internal.crypto.keysbackup.model.KeysBackupVersionTrustSignature
-import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupAuthData
-import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
 import org.matrix.android.sdk.internal.crypto.keysbackup.model.SignalableMegolmBackupAuthData
 import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.BackupKeysResult
 import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody
 import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeyBackupData
 import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysBackupData
-import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion
-import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult
 import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.RoomKeysBackupData
 import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.UpdateKeysBackupVersionBody
 import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask
@@ -68,12 +75,8 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreRoomSessionD
 import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreRoomSessionsDataTask
 import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreSessionsDataTask
 import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask
-import org.matrix.android.sdk.internal.crypto.keysbackup.util.computeRecoveryKey
-import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
-import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult
 import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
-import org.matrix.android.sdk.internal.crypto.store.SavedKeyBackupKeyInfo
 import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
 import org.matrix.android.sdk.internal.di.MoshiProvider
 import org.matrix.android.sdk.internal.di.UserId
@@ -84,7 +87,6 @@ import org.matrix.android.sdk.internal.task.TaskExecutor
 import org.matrix.android.sdk.internal.task.TaskThread
 import org.matrix.android.sdk.internal.task.configureWith
 import org.matrix.android.sdk.internal.util.JsonCanonicalizer
-import org.matrix.android.sdk.internal.util.awaitCallback
 import org.matrix.olm.OlmException
 import org.matrix.olm.OlmPkDecryption
 import org.matrix.olm.OlmPkEncryption
@@ -407,20 +409,22 @@ internal class DefaultKeysBackupService @Inject constructor(
      */
     @WorkerThread
     private fun getKeysBackupTrustBg(keysBackupVersion: KeysVersionResult): KeysBackupVersionTrust {
-        val keysBackupVersionTrust = KeysBackupVersionTrust()
         val authData = keysBackupVersion.getAuthDataAsMegolmBackupAuthData()
 
         if (authData == null || authData.publicKey.isEmpty() || authData.signatures.isNullOrEmpty()) {
             Timber.v("getKeysBackupTrust: Key backup is absent or missing required data")
-            return keysBackupVersionTrust
+            return KeysBackupVersionTrust(usable = false)
         }
 
         val mySigs = authData.signatures[userId]
         if (mySigs.isNullOrEmpty()) {
             Timber.v("getKeysBackupTrust: Ignoring key backup because it lacks any signatures from this user")
-            return keysBackupVersionTrust
+            return KeysBackupVersionTrust(usable = false)
         }
 
+        var keysBackupVersionTrustIsUsable = false
+        val keysBackupVersionTrustSignatures = mutableListOf<KeysBackupVersionTrustSignature>()
+
         for ((keyId, mySignature) in mySigs) {
             // XXX: is this how we're supposed to get the device id?
             var deviceId: String? = null
@@ -447,19 +451,23 @@ internal class DefaultKeysBackupService @Inject constructor(
                     }
 
                     if (isSignatureValid && device.isVerified) {
-                        keysBackupVersionTrust.usable = true
+                        keysBackupVersionTrustIsUsable = true
                     }
                 }
 
-                val signature = KeysBackupVersionTrustSignature()
-                signature.device = device
-                signature.valid = isSignatureValid
-                signature.deviceId = deviceId
-                keysBackupVersionTrust.signatures.add(signature)
+                val signature = KeysBackupVersionTrustSignature(
+                        deviceId = deviceId,
+                        device = device,
+                        valid = isSignatureValid,
+                )
+                keysBackupVersionTrustSignatures.add(signature)
             }
         }
 
-        return keysBackupVersionTrust
+        return KeysBackupVersionTrust(
+                usable = keysBackupVersionTrustIsUsable,
+                signatures = keysBackupVersionTrustSignatures
+        )
     }
 
     override fun trustKeysBackupVersion(keysBackupVersion: KeysVersionResult,
@@ -586,21 +594,28 @@ internal class DefaultKeysBackupService @Inject constructor(
 
         cryptoCoroutineScope.launch(coroutineDispatchers.main) {
             try {
-                val keysBackupVersion = getKeysBackupLastVersionTask.execute(Unit)
-                val recoveryKey = computeRecoveryKey(secret.fromBase64())
-                if (isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)) {
-                    awaitCallback<Unit> {
-                        trustKeysBackupVersion(keysBackupVersion, true, it)
+                when (val keysBackupLastVersionResult = getKeysBackupLastVersionTask.execute(Unit)) {
+                    KeysBackupLastVersionResult.NoKeysBackup  -> {
+                        Timber.d("No keys backup found")
                     }
-                    val importResult = awaitCallback<ImportRoomKeysResult> {
-                        restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, null, null, null, it)
-                    }
-                    withContext(coroutineDispatchers.crypto) {
-                        cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version)
+                    is KeysBackupLastVersionResult.KeysBackup -> {
+                        val keysBackupVersion = keysBackupLastVersionResult.keysVersionResult
+                        val recoveryKey = computeRecoveryKey(secret.fromBase64())
+                        if (isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)) {
+                            awaitCallback<Unit> {
+                                trustKeysBackupVersion(keysBackupVersion, true, it)
+                            }
+                            val importResult = awaitCallback<ImportRoomKeysResult> {
+                                restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, null, null, null, it)
+                            }
+                            withContext(coroutineDispatchers.crypto) {
+                                cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version)
+                            }
+                            Timber.i("onSecretKeyGossip: Recovered keys $importResult")
+                        } else {
+                            Timber.e("onSecretKeyGossip: Recovery key is not valid ${keysBackupVersion.version}")
+                        }
                     }
-                    Timber.i("onSecretKeyGossip: Recovered keys ${importResult.successfullyNumberOfImportedKeys} out of ${importResult.totalNumberOfKeys}")
-                } else {
-                    Timber.e("onSecretKeyGossip: Recovery key is not valid ${keysBackupVersion.version}")
                 }
             } catch (failure: Throwable) {
                 Timber.e("onSecretKeyGossip: failed to trust key backup version ${keysBackupVersion?.version}")
@@ -671,7 +686,6 @@ internal class DefaultKeysBackupService @Inject constructor(
                         Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version")
                         throw InvalidParameterException("Invalid recovery key")
                     }
-
                     // Get a PK decryption instance
                     pkDecryptionFromRecoveryKey(recoveryKey)
                 }
@@ -681,6 +695,10 @@ internal class DefaultKeysBackupService @Inject constructor(
                     throw InvalidParameterException("Invalid recovery key")
                 }
 
+                // Save for next time and for gossiping
+                // Save now as it's valid, don't wait for the import as it could take long.
+                saveBackupRecoveryKey(recoveryKey, keysVersionResult.version)
+
                 stepProgressListener?.onStepProgress(StepProgressListener.Step.DownloadingKey)
 
                 // Get backed up keys from the homeserver
@@ -729,8 +747,6 @@ internal class DefaultKeysBackupService @Inject constructor(
                     if (backUp) {
                         maybeBackupKeys()
                     }
-                    // Save for next time and for gossiping
-                    saveBackupRecoveryKey(recoveryKey, keysVersionResult.version)
                     result
                 }
             }.foldToCallback(callback)
@@ -874,63 +890,49 @@ internal class DefaultKeysBackupService @Inject constructor(
                 .executeBy(taskExecutor)
     }
 
-    override fun getCurrentVersion(callback: MatrixCallback<KeysVersionResult?>) {
+    override fun getCurrentVersion(callback: MatrixCallback<KeysBackupLastVersionResult>) {
         getKeysBackupLastVersionTask
                 .configureWith {
-                    this.callback = object : MatrixCallback<KeysVersionResult> {
-                        override fun onSuccess(data: KeysVersionResult) {
-                            callback.onSuccess(data)
-                        }
-
-                        override fun onFailure(failure: Throwable) {
-                            if (failure is Failure.ServerError &&
-                                    failure.error.code == MatrixError.M_NOT_FOUND) {
-                                // Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup
-                                callback.onSuccess(null)
-                            } else {
-                                // Transmit the error
-                                callback.onFailure(failure)
-                            }
-                        }
-                    }
+                    this.callback = callback
                 }
                 .executeBy(taskExecutor)
     }
 
     override fun forceUsingLastVersion(callback: MatrixCallback<Boolean>) {
-        getCurrentVersion(object : MatrixCallback<KeysVersionResult?> {
-            override fun onSuccess(data: KeysVersionResult?) {
+        getCurrentVersion(object : MatrixCallback<KeysBackupLastVersionResult> {
+            override fun onSuccess(data: KeysBackupLastVersionResult) {
                 val localBackupVersion = keysBackupVersion?.version
-                val serverBackupVersion = data?.version
-
-                if (serverBackupVersion == null) {
-                    if (localBackupVersion == null) {
-                        // No backup on the server, and backup is not active
-                        callback.onSuccess(true)
-                    } else {
-                        // No backup on the server, and we are currently backing up, so stop backing up
-                        callback.onSuccess(false)
-                        resetKeysBackupData()
-                        keysBackupVersion = null
-                        keysBackupStateManager.state = KeysBackupState.Disabled
-                    }
-                } else {
-                    if (localBackupVersion == null) {
-                        // backup on the server, and backup is not active
-                        callback.onSuccess(false)
-                        // Do a check
-                        checkAndStartWithKeysBackupVersion(data)
-                    } else {
-                        // Backup on the server, and we are currently backing up, compare version
-                        if (localBackupVersion == serverBackupVersion) {
-                            // We are already using the last version of the backup
+                when (data) {
+                    KeysBackupLastVersionResult.NoKeysBackup  -> {
+                        if (localBackupVersion == null) {
+                            // No backup on the server, and backup is not active
                             callback.onSuccess(true)
                         } else {
-                            // We are not using the last version, so delete the current version we are using on the server
+                            // No backup on the server, and we are currently backing up, so stop backing up
+                            callback.onSuccess(false)
+                            resetKeysBackupData()
+                            keysBackupVersion = null
+                            keysBackupStateManager.state = KeysBackupState.Disabled
+                        }
+                    }
+                    is KeysBackupLastVersionResult.KeysBackup -> {
+                        if (localBackupVersion == null) {
+                            // backup on the server, and backup is not active
                             callback.onSuccess(false)
+                            // Do a check
+                            checkAndStartWithKeysBackupVersion(data.keysVersionResult)
+                        } else {
+                            // Backup on the server, and we are currently backing up, compare version
+                            if (localBackupVersion == data.keysVersionResult.version) {
+                                // We are already using the last version of the backup
+                                callback.onSuccess(true)
+                            } else {
+                                // We are not using the last version, so delete the current version we are using on the server
+                                callback.onSuccess(false)
 
-                            // This will automatically check for the last version then
-                            deleteBackup(localBackupVersion, null)
+                                // This will automatically check for the last version then
+                                deleteBackup(localBackupVersion, null)
+                            }
                         }
                     }
                 }
@@ -953,9 +955,9 @@ internal class DefaultKeysBackupService @Inject constructor(
         keysBackupVersion = null
         keysBackupStateManager.state = KeysBackupState.CheckingBackUpOnHomeserver
 
-        getCurrentVersion(object : MatrixCallback<KeysVersionResult?> {
-            override fun onSuccess(data: KeysVersionResult?) {
-                checkAndStartWithKeysBackupVersion(data)
+        getCurrentVersion(object : MatrixCallback<KeysBackupLastVersionResult> {
+            override fun onSuccess(data: KeysBackupLastVersionResult) {
+                checkAndStartWithKeysBackupVersion(data.toKeysVersionResult())
             }
 
             override fun onFailure(failure: Throwable) {
@@ -1103,6 +1105,13 @@ internal class DefaultKeysBackupService @Inject constructor(
         }
     }
 
+    override fun computePrivateKey(passphrase: String,
+                                   privateKeySalt: String,
+                                   privateKeyIterations: Int,
+                                   progressListener: ProgressListener): ByteArray {
+        return deriveKey(passphrase, privateKeySalt, privateKeyIterations, progressListener)
+    }
+
     /**
      * Enable backing up of keys.
      * This method will update the state and will start sending keys in nominal case
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPassword.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPassword.kt
index 24c3942055dcc38582f9847145827a8994fe7a95..c12879dbeeb7d72221376817a00ae0ac7b85fbd6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPassword.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupPassword.kt
@@ -30,13 +30,14 @@ import kotlin.experimental.xor
 private const val SALT_LENGTH = 32
 private const val DEFAULT_ITERATION = 500_000
 
-data class GeneratePrivateKeyResult(
+internal data class GeneratePrivateKeyResult(
         // The private key
         val privateKey: ByteArray,
         // the salt used to generate the private key
         val salt: String,
         // number of key derivations done on the generated private key.
-        val iterations: Int)
+        val iterations: Int
+)
 
 /**
  * Compute a private key from a password.
@@ -46,7 +47,9 @@ data class GeneratePrivateKeyResult(
  * @return a {privateKey, salt, iterations} tuple.
  */
 @WorkerThread
-fun generatePrivateKeyWithPassword(password: String, progressListener: ProgressListener?): GeneratePrivateKeyResult {
+internal fun generatePrivateKeyWithPassword(password: String,
+                                            progressListener: ProgressListener?
+): GeneratePrivateKeyResult {
     val salt = generateSalt()
     val iterations = DEFAULT_ITERATION
     val privateKey = deriveKey(password, salt, iterations, progressListener)
@@ -65,10 +68,10 @@ fun generatePrivateKeyWithPassword(password: String, progressListener: ProgressL
  * @return a private key.
  */
 @WorkerThread
-fun retrievePrivateKeyWithPassword(password: String,
-                                   salt: String,
-                                   iterations: Int,
-                                   progressListener: ProgressListener? = null): ByteArray {
+internal fun retrievePrivateKeyWithPassword(password: String,
+                                            salt: String,
+                                            iterations: Int,
+                                            progressListener: ProgressListener? = null): ByteArray {
     return deriveKey(password, salt, iterations, progressListener)
 }
 
@@ -83,10 +86,10 @@ fun retrievePrivateKeyWithPassword(password: String,
  * @return a private key.
  */
 @WorkerThread
-fun deriveKey(password: String,
-              salt: String,
-              iterations: Int,
-              progressListener: ProgressListener?): ByteArray {
+internal fun deriveKey(password: String,
+                       salt: String,
+                       iterations: Int,
+                       progressListener: ProgressListener?): ByteArray {
     // Note: copied and adapted from MXMegolmExportEncryption
     val t0 = System.currentTimeMillis()
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/api/RoomKeysApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/api/RoomKeysApi.kt
index eb4c55a3e7f93343c88b99945d6ec603cd71c285..8464b33526fe4abaac5bcfe167c10b18a9f48257 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/api/RoomKeysApi.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/api/RoomKeysApi.kt
@@ -16,12 +16,12 @@
 
 package org.matrix.android.sdk.internal.crypto.keysbackup.api
 
+import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
+import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult
 import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.BackupKeysResult
 import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody
 import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeyBackupData
 import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysBackupData
-import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion
-import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult
 import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.RoomKeysBackupData
 import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.UpdateKeysBackupVersionBody
 import org.matrix.android.sdk.internal.network.NetworkConstants
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeyBackupVersionTrustSignature.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/SignalableMegolmBackupAuthData.kt
similarity index 56%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeyBackupVersionTrustSignature.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/SignalableMegolmBackupAuthData.kt
index 5256c781762ad3be81c5239d6898b6387d7c32ba..85f75a61e2d8f4b7bb52f27b49e3461474b0d043 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/KeyBackupVersionTrustSignature.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/SignalableMegolmBackupAuthData.kt
@@ -16,20 +16,21 @@
 
 package org.matrix.android.sdk.internal.crypto.keysbackup.model
 
-import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
+import org.matrix.android.sdk.api.util.JsonDict
 
-/**
- * A signature in a the `KeyBackupVersionTrust` object.
- */
-class KeyBackupVersionTrustSignature {
-
-    /**
-     * The device that signed the backup version.
-     */
-    var device: CryptoDeviceInfo? = null
+internal data class SignalableMegolmBackupAuthData(
+        val publicKey: String,
+        val privateKeySalt: String? = null,
+        val privateKeyIterations: Int? = null
+) {
+    fun signalableJSONDictionary(): JsonDict = HashMap<String, Any>().apply {
+        put("public_key", publicKey)
 
-    /**
-     *Flag to indicate the signature from this device is valid.
-     */
-    var valid = false
+        privateKeySalt?.let {
+            put("private_key_salt", it)
+        }
+        privateKeyIterations?.let {
+            put("private_key_iterations", it)
+        }
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeyBackupData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeyBackupData.kt
index 3f8129b8f65eb0cf531a13619cb12f31f0778b7b..5c3d0c12b0ec16ac06ee87fb658f566e68c52524 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeyBackupData.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeyBackupData.kt
@@ -25,7 +25,7 @@ import org.matrix.android.sdk.internal.network.parsing.ForceToBoolean
  * Backup data for one key.
  */
 @JsonClass(generateAdapter = true)
-data class KeyBackupData(
+internal data class KeyBackupData(
         /**
          * Required. The index of the first message in the session that the key can decrypt.
          */
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysAlgorithmAndData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysAlgorithmAndData.kt
index e098aa0440e9cfddfda140694b1b692d599e98d8..898b357c51791b19f75be713db7675665c6b46cc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysAlgorithmAndData.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysAlgorithmAndData.kt
@@ -16,9 +16,9 @@
 
 package org.matrix.android.sdk.internal.crypto.keysbackup.model.rest
 
+import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
+import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupAuthData
 import org.matrix.android.sdk.api.util.JsonDict
-import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
-import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupAuthData
 import org.matrix.android.sdk.internal.di.MoshiProvider
 
 /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysBackupData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysBackupData.kt
index 6b55f2002050f15a69fe4babc34ffff54a29d269..4237458859320ad898862800fdccaa419df7d740 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysBackupData.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeysBackupData.kt
@@ -23,7 +23,7 @@ import com.squareup.moshi.JsonClass
  * Backup data for several keys in several rooms.
  */
 @JsonClass(generateAdapter = true)
-data class KeysBackupData(
+internal data class KeysBackupData(
         // the keys are the room IDs, and the values are RoomKeysBackupData
         @Json(name = "rooms")
         val roomIdToRoomKeysBackupData: MutableMap<String, RoomKeysBackupData> = HashMap()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/RoomKeysBackupData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/RoomKeysBackupData.kt
index ce42a3bc35ee0f59bf04c4e483a700b5ea6bc70c..5ea6a2f890a1feb1a0aefc40642d3f251240d442 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/RoomKeysBackupData.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/RoomKeysBackupData.kt
@@ -23,7 +23,7 @@ import com.squareup.moshi.JsonClass
  * Backup data for several keys within a room.
  */
 @JsonClass(generateAdapter = true)
-data class RoomKeysBackupData(
+internal data class RoomKeysBackupData(
         // the keys are the session IDs, and the values are KeyBackupData
         @Json(name = "sessions")
         val sessionIdToKeyBackupData: MutableMap<String, KeyBackupData> = HashMap()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/UpdateKeysBackupVersionBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/UpdateKeysBackupVersionBody.kt
index 4512ed7a55a344eb1c9c8b36e8c2198335f90885..3f2def84d556d244a063efb714d27f33ff5f3255 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/UpdateKeysBackupVersionBody.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/UpdateKeysBackupVersionBody.kt
@@ -21,7 +21,7 @@ import com.squareup.moshi.JsonClass
 import org.matrix.android.sdk.api.util.JsonDict
 
 @JsonClass(generateAdapter = true)
-data class UpdateKeysBackupVersionBody(
+internal data class UpdateKeysBackupVersionBody(
         /**
          * The algorithm used for storing backups. Currently, only "m.megolm_backup.v1.curve25519-aes-sha2" is defined
          */
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/CreateKeysBackupVersionTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/CreateKeysBackupVersionTask.kt
index 62610a0b7bd686783346e62b1d6848b72bd54366..10d6e923e7b0b33b0646054b4550af9f2f907684 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/CreateKeysBackupVersionTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/CreateKeysBackupVersionTask.kt
@@ -16,9 +16,9 @@
 
 package org.matrix.android.sdk.internal.crypto.keysbackup.tasks
 
+import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
 import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi
 import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody
-import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion
 import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
 import org.matrix.android.sdk.internal.network.executeRequest
 import org.matrix.android.sdk.internal.task.Task
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupLastVersionTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupLastVersionTask.kt
index 54dbf85e302730784737b23b97bf0c23f0ab7964..e5621c0cb5c8dff35bd1c12455155d67b3cf9d22 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupLastVersionTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupLastVersionTask.kt
@@ -16,23 +16,34 @@
 
 package org.matrix.android.sdk.internal.crypto.keysbackup.tasks
 
+import org.matrix.android.sdk.api.failure.is404
+import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupLastVersionResult
 import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi
-import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult
 import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
 import org.matrix.android.sdk.internal.network.executeRequest
 import org.matrix.android.sdk.internal.task.Task
 import javax.inject.Inject
 
-internal interface GetKeysBackupLastVersionTask : Task<Unit, KeysVersionResult>
+internal interface GetKeysBackupLastVersionTask : Task<Unit, KeysBackupLastVersionResult>
 
 internal class DefaultGetKeysBackupLastVersionTask @Inject constructor(
         private val roomKeysApi: RoomKeysApi,
         private val globalErrorReceiver: GlobalErrorReceiver
 ) : GetKeysBackupLastVersionTask {
 
-    override suspend fun execute(params: Unit): KeysVersionResult {
-        return executeRequest(globalErrorReceiver) {
-            roomKeysApi.getKeysBackupLastVersion()
+    override suspend fun execute(params: Unit): KeysBackupLastVersionResult {
+        return try {
+            val keysVersionResult = executeRequest(globalErrorReceiver) {
+                roomKeysApi.getKeysBackupLastVersion()
+            }
+            KeysBackupLastVersionResult.KeysBackup(keysVersionResult)
+        } catch (throwable: Throwable) {
+            if (throwable.is404()) {
+                KeysBackupLastVersionResult.NoKeysBackup
+            } else {
+                // Propagate other errors
+                throw throwable
+            }
         }
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupVersionTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupVersionTask.kt
index 390873eb68dc249eef0f1628b6299339c6591b68..fe1ca297981952712509fe0e9f1cb6053f6e3d5b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupVersionTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/tasks/GetKeysBackupVersionTask.kt
@@ -16,8 +16,8 @@
 
 package org.matrix.android.sdk.internal.crypto.keysbackup.tasks
 
+import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult
 import org.matrix.android.sdk.internal.crypto.keysbackup.api.RoomKeysApi
-import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult
 import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
 import org.matrix.android.sdk.internal.network.executeRequest
 import org.matrix.android.sdk.internal.task.Task
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/Base58.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/Base58.kt
index def9c1b6757dd7f3b6d6b3bdb904c87a78f8501c..0e746f289b8c921a4876c1a1d1d2202dceb27bee 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/Base58.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/Base58.kt
@@ -41,7 +41,7 @@ private val BASE = BigInteger.valueOf(58)
 /**
  * Encode a byte array to a human readable string with base58 chars
  */
-fun base58encode(input: ByteArray): String {
+internal fun base58encode(input: ByteArray): String {
     var bi = BigInteger(1, input)
     val s = StringBuffer()
     while (bi >= BASE) {
@@ -64,7 +64,7 @@ fun base58encode(input: ByteArray): String {
 /**
  * Decode a base58 String to a byte array
  */
-fun base58decode(input: String): ByteArray {
+internal fun base58decode(input: String): ByteArray {
     var result = decodeToBigInteger(input).toByteArray()
 
     // Remove the first leading zero if any
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoDeviceInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoDeviceInfo.kt
index 5e7744853aa464a9515fb1fb9c807e1c5321617d..727f739866465aee53271ae2e0c4d8a7cfa03f7a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoDeviceInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoDeviceInfo.kt
@@ -15,62 +15,8 @@
  */
 package org.matrix.android.sdk.internal.crypto.model
 
-import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel
+import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
 import org.matrix.android.sdk.internal.crypto.model.rest.DeviceKeys
-import org.matrix.android.sdk.internal.crypto.model.rest.UnsignedDeviceInfo
-
-data class CryptoDeviceInfo(
-        val deviceId: String,
-        override val userId: String,
-        var algorithms: List<String>? = null,
-        override val keys: Map<String, String>? = null,
-        override val signatures: Map<String, Map<String, String>>? = null,
-        val unsigned: UnsignedDeviceInfo? = null,
-        var trustLevel: DeviceTrustLevel? = null,
-        var isBlocked: Boolean = false,
-        val firstTimeSeenLocalTs: Long? = null
-) : CryptoInfo {
-
-    val isVerified: Boolean
-        get() = trustLevel?.isVerified() == true
-
-    val isUnknown: Boolean
-        get() = trustLevel == null
-
-    /**
-     * @return the fingerprint
-     */
-    fun fingerprint(): String? {
-        return keys
-                ?.takeIf { deviceId.isNotBlank() }
-                ?.get("ed25519:$deviceId")
-    }
-
-    /**
-     * @return the identity key
-     */
-    fun identityKey(): String? {
-        return keys
-                ?.takeIf { deviceId.isNotBlank() }
-                ?.get("curve25519:$deviceId")
-    }
-
-    /**
-     * @return the display name
-     */
-    fun displayName(): String? {
-        return unsigned?.deviceDisplayName
-    }
-
-    override fun signalableJSONDictionary(): Map<String, Any> {
-        val map = HashMap<String, Any>()
-        map["device_id"] = deviceId
-        map["user_id"] = userId
-        algorithms?.let { map["algorithms"] = it }
-        keys?.let { map["keys"] = it }
-        return map
-    }
-}
 
 internal fun CryptoDeviceInfo.toRest(): DeviceKeys {
     return CryptoInfoMapper.map(this)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoInfo.kt
index 39981e01f7fd362ef091f353b547b5a31b421ba8..e49f17262cf877a56e6c1ed9cce408c225e4b1f4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoInfo.kt
@@ -20,7 +20,7 @@ package org.matrix.android.sdk.internal.crypto.model
  * Generic crypto info.
  * Can be a device (CryptoDeviceInfo), as well as a CryptoCrossSigningInfo (can be seen as a kind of virtual device)
  */
-interface CryptoInfo {
+internal interface CryptoInfo {
 
     val userId: String
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoInfoMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoInfoMapper.kt
index 6cc6f5400ff6a6423d1ec52bc0b3f19f47fd8223..de9b3f24fff6898a55ddcd1676cf02f36ebc4da4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoInfoMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoInfoMapper.kt
@@ -15,6 +15,8 @@
  */
 package org.matrix.android.sdk.internal.crypto.model
 
+import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey
+import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
 import org.matrix.android.sdk.internal.crypto.model.rest.DeviceKeys
 import org.matrix.android.sdk.internal.crypto.model.rest.DeviceKeysWithUnsigned
 import org.matrix.android.sdk.internal.crypto.model.rest.RestKeyInfo
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXKey.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXKey.kt
index 9f425eee7ff44baee02a69d911be8737d7b23c20..cff713bf8f72ed1f2e6d6edb7127f0beb1a65f7f 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXKey.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXKey.kt
@@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.crypto.model
 import org.matrix.android.sdk.api.util.JsonDict
 import timber.log.Timber
 
-data class MXKey(
+internal data class MXKey(
         /**
          * The type of the key (in the example: "signed_curve25519").
          */
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXOlmSessionResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXOlmSessionResult.kt
index b07a08c30f7d203b7e1f3cb4660a9681a2117215..671827799e273bae75e9e29a4d61e366047c613f 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXOlmSessionResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXOlmSessionResult.kt
@@ -16,9 +16,10 @@
 
 package org.matrix.android.sdk.internal.crypto.model
 
+import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
 import java.io.Serializable
 
-data class MXOlmSessionResult(
+internal data class MXOlmSessionResult(
         /**
          * the device
          */
@@ -27,4 +28,5 @@ data class MXOlmSessionResult(
          * Base64 olm session id.
          * null if no session could be established.
          */
-        var sessionId: String?) : Serializable
+        var sessionId: String?
+) : Serializable
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 662541428e91bedaa19fee4ffee382a682a20314..58aff14a3dc2fdf190a10a0c3e8dbfda70485b73 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
@@ -16,121 +16,9 @@
 
 package org.matrix.android.sdk.internal.crypto.model
 
-class MXUsersDevicesMap<E> {
+import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
 
-    // A map of maps (userId -> (deviceId -> Object)).
-    val map = HashMap<String /* userId */, HashMap<String /* deviceId */, E>>()
-
-    /**
-     * @return the user Ids
-     */
-    val userIds: List<String>
-        get() = map.keys.toList()
-
-    val isEmpty: Boolean
-        get() = map.isEmpty()
-
-    /**
-     * Provides the device ids list for a user id
-     * FIXME Should maybe return emptyList and not null, to avoid many !! in the code
-     *
-     * @param userId the user id
-     * @return the device ids list
-     */
-    fun getUserDeviceIds(userId: String?): List<String>? {
-        return if (!userId.isNullOrBlank() && map.containsKey(userId)) {
-            map[userId]!!.keys.toList()
-        } else null
-    }
-
-    /**
-     * Provides the object for a device id and a user Id
-     *
-     * @param deviceId the device id
-     * @param userId   the object id
-     * @return the object
-     */
-    fun getObject(userId: String?, deviceId: String?): E? {
-        return if (!userId.isNullOrBlank() && !deviceId.isNullOrBlank()) {
-            map[userId]?.get(deviceId)
-        } else null
-    }
-
-    /**
-     * Set an object for a dedicated user Id and device Id
-     *
-     * @param userId   the user Id
-     * @param deviceId the device id
-     * @param o        the object to set
-     */
-    fun setObject(userId: String?, deviceId: String?, o: E?) {
-        if (null != o && userId?.isNotBlank() == true && deviceId?.isNotBlank() == true) {
-            val devices = map.getOrPut(userId) { HashMap() }
-            devices[deviceId] = o
-        }
-    }
-
-    /**
-     * Defines the objects map for a user Id
-     *
-     * @param objectsPerDevices the objects maps
-     * @param userId            the user id
-     */
-    fun setObjects(userId: String?, objectsPerDevices: Map<String, E>?) {
-        if (!userId.isNullOrBlank()) {
-            if (null == objectsPerDevices) {
-                map.remove(userId)
-            } else {
-                map[userId] = HashMap(objectsPerDevices)
-            }
-        }
-    }
-
-    /**
-     * Removes objects for a dedicated user
-     *
-     * @param userId the user id.
-     */
-    fun removeUserObjects(userId: String?) {
-        if (!userId.isNullOrBlank()) {
-            map.remove(userId)
-        }
-    }
-
-    /**
-     * Clear the internal dictionary
-     */
-    fun removeAllObjects() {
-        map.clear()
-    }
-
-    /**
-     * Add entries from another MXUsersDevicesMap
-     *
-     * @param other the other one
-     */
-    fun addEntriesFromMap(other: MXUsersDevicesMap<E>?) {
-        if (null != other) {
-            map.putAll(other.map)
-        }
-    }
-
-    override fun toString(): String {
-        return "MXUsersDevicesMap $map"
-    }
-}
-
-inline fun <T> MXUsersDevicesMap<T>.forEach(action: (String, String, T) -> Unit) {
-    userIds.forEach { userId ->
-        getUserDeviceIds(userId)?.forEach { deviceId ->
-            getObject(userId, deviceId)?.let {
-                action(userId, deviceId, it)
-            }
-        }
-    }
-}
-
-internal  fun <T> MXUsersDevicesMap<T>.toDebugString() =
+internal fun <T> MXUsersDevicesMap<T>.toDebugString() =
         map.entries.joinToString { "${it.key} [${it.value.keys.joinToString { it }}]" }
 
 internal fun <T> MXUsersDevicesMap<T>.toDebugCount() =
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper.kt
index 086a236a2bf4ea07e9ed8f335a353f3564eb9c1e..45ffcc66064c2d831611410e375cfda52dfca10d 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper.kt
@@ -16,7 +16,7 @@
 
 package org.matrix.android.sdk.internal.crypto.model
 
-import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
+import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
 import org.matrix.android.sdk.internal.crypto.MegolmSessionData
 import org.matrix.olm.OlmInboundGroupSession
 import timber.log.Timber
@@ -26,7 +26,7 @@ import java.io.Serializable
  * This class adds more context to a OlmInboundGroupSession object.
  * This allows additional checks. The class implements Serializable so that the context can be stored.
  */
-class OlmInboundGroupSessionWrapper : Serializable {
+internal class OlmInboundGroupSessionWrapper : Serializable {
 
     // The associated olm inbound group session.
     var olmInboundGroupSession: OlmInboundGroupSession? = null
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper2.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper2.kt
index 1dc27c75ca39d96eb156317e815f601f54716873..1f671aa896228c1f02d9aaf500b6f5801682b9e8 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper2.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper2.kt
@@ -16,7 +16,7 @@
 
 package org.matrix.android.sdk.internal.crypto.model
 
-import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
+import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
 import org.matrix.android.sdk.internal.crypto.MegolmSessionData
 import org.matrix.olm.OlmInboundGroupSession
 import timber.log.Timber
@@ -26,7 +26,7 @@ import java.io.Serializable
  * This class adds more context to a OlmInboundGroupSession object.
  * This allows additional checks. The class implements Serializable so that the context can be stored.
  */
-class OlmInboundGroupSessionWrapper2 : Serializable {
+internal class OlmInboundGroupSessionWrapper2 : Serializable {
 
     // The associated olm inbound group session.
     var olmInboundGroupSession: OlmInboundGroupSession? = null
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmSessionWrapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmSessionWrapper.kt
index 15b92f105ae63d2ccac18b885ee11f198eb89e25..927d049eca9c13a325f786b5b881c53b0e720637 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmSessionWrapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmSessionWrapper.kt
@@ -16,16 +16,20 @@
 
 package org.matrix.android.sdk.internal.crypto.model
 
+import kotlinx.coroutines.sync.Mutex
 import org.matrix.olm.OlmSession
 
 /**
  * Encapsulate a OlmSession and a last received message Timestamp
  */
-data class OlmSessionWrapper(
+internal data class OlmSessionWrapper(
         // The associated olm session.
         val olmSession: OlmSession,
         // Timestamp at which the session last received a message.
-        var lastReceivedMessageTs: Long = 0) {
+        var lastReceivedMessageTs: Long = 0,
+
+        val mutex: Mutex = Mutex()
+) {
 
     /**
      * Notify that a message has been received on this olm session so that it updates `lastReceivedMessageTs`
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OutboundGroupSessionWrapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OutboundGroupSessionWrapper.kt
index b616284e14a93c2bac4e08ae9effea6f0fca8886..4ac87f44cef04359dfb5e1e64b4ead7947ae4d73 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OutboundGroupSessionWrapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OutboundGroupSessionWrapper.kt
@@ -18,7 +18,7 @@ package org.matrix.android.sdk.internal.crypto.model
 
 import org.matrix.olm.OlmOutboundGroupSession
 
-data class OutboundGroupSessionWrapper(
+internal data class OutboundGroupSessionWrapper(
         val outboundGroupSession: OlmOutboundGroupSession,
         val creationTime: Long
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceKeys.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceKeys.kt
index 3a845b1f2a7b5c82739f1adaa71b810a338b242d..611d9b72b5082954f6742488fbcf07d4f7dff241 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceKeys.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceKeys.kt
@@ -20,7 +20,7 @@ import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
 
 @JsonClass(generateAdapter = true)
-data class DeviceKeys(
+internal data class DeviceKeys(
         /**
          * Required. The ID of the user the device belongs to. Must match the user ID used when logging in.
          */
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceKeysWithUnsigned.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceKeysWithUnsigned.kt
index 35fce323940a7fd65fbf4f830c04867d5bc9e8bd..32f577c99b9db552afe9b5a6b3863edb50a1b416 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceKeysWithUnsigned.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceKeysWithUnsigned.kt
@@ -18,9 +18,10 @@ package org.matrix.android.sdk.internal.crypto.model.rest
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.session.crypto.model.UnsignedDeviceInfo
 
 @JsonClass(generateAdapter = true)
-data class DeviceKeysWithUnsigned(
+internal data class DeviceKeysWithUnsigned(
         /**
          * Required. The ID of the user the device belongs to. Must match the user ID used when logging in.
          */
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedMessage.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedMessage.kt
index f32676a919f3b2e9fc6cbf714e61a215865fcd30..c2f76f9dea88ffa77bc0e95e83bae33780a027af 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedMessage.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedMessage.kt
@@ -18,9 +18,10 @@ package org.matrix.android.sdk.internal.crypto.model.rest
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject
 
 @JsonClass(generateAdapter = true)
-data class EncryptedMessage(
+internal data class EncryptedMessage(
         @Json(name = "algorithm")
         val algorithm: String? = null,
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/GossipingDefaultContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/GossipingDefaultContent.kt
new file mode 100644
index 0000000000000000000000000000000000000000..8f789a638cf82068460f4dbc43355eba97834251
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/GossipingDefaultContent.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2022 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.crypto.model.rest
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject
+
+@JsonClass(generateAdapter = true)
+internal data class GossipingDefaultContent(
+        @Json(name = "action") override val action: String?,
+        @Json(name = "requesting_device_id") override val requestingDeviceId: String?,
+        @Json(name = "m.request_id") override val requestId: String? = null
+) : GossipingToDeviceObject
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationAccept.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationAccept.kt
index f695425c2abe8a34d77b28af40c04bbea8d4194b..7a5773bf24d02b25de8d01aec4b9a30b0a2abfd1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationAccept.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationAccept.kt
@@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.crypto.model.rest
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject
 import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoAccept
 import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoAcceptFactory
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationCancel.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationCancel.kt
index 4dfa5984dfdf21421f07e0a2c0a796cfc49a30fc..90272bf0e4bfafae0c50235603d7428972b18f61 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationCancel.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationCancel.kt
@@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.crypto.model.rest
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject
 import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
 import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoCancel
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationDone.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationDone.kt
index 96afba060b67e1c280545ffd4afe6296c918648d..e3907914ac4d1dc220f92872873dcad8378bb937 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationDone.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationDone.kt
@@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.crypto.model.rest
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject
 import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoDone
 
 /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationKey.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationKey.kt
index 7ded437cea9fad5bc47046e308435cb4d42a6930..19d8c32ddf7c9f2df333d198f69f68397dfdda30 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationKey.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationKey.kt
@@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.crypto.model.rest
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject
 import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoKey
 import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoKeyFactory
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationMac.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationMac.kt
index 6c055aee2a86e12bfa4a8a151d271178a618e062..5335428c0f066f1971bfb5ad351aaa94a08345bc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationMac.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationMac.kt
@@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.crypto.model.rest
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject
 import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoMac
 import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoMacFactory
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationReady.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationReady.kt
index 3562613c2e5824cd6fb69185aef3b07fd19e8485..e6770be9a07abfd6e6617dc6b3acf8277e589383 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationReady.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationReady.kt
@@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.crypto.model.rest
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject
 import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoReady
 
 /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationRequest.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationRequest.kt
index c30b2a306ffbf86464f8c291412267a412b53e0d..191d5abb60f604e41e3f7881828b6250e59d4f7e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationRequest.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationRequest.kt
@@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.crypto.model.rest
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject
 import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoRequest
 
 /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationStart.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationStart.kt
index 52a66a9db6fbb29659d9544eacc9ecf4d69555d7..f74bad844d6cf887434460600440fcf61559722c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationStart.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/KeyVerificationStart.kt
@@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.crypto.model.rest
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject
 import org.matrix.android.sdk.internal.crypto.verification.VerificationInfoStart
 import org.matrix.android.sdk.internal.util.JsonCanonicalizer
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/RestKeyInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/RestKeyInfo.kt
index 0d41e5b648a23846f6950844228d7c226ee55b1d..66247d07d1e02c4b135909a58a860bc7b9bbdd02 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/RestKeyInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/RestKeyInfo.kt
@@ -17,7 +17,7 @@ package org.matrix.android.sdk.internal.crypto.model.rest
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
-import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey
+import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey
 import org.matrix.android.sdk.internal.crypto.model.CryptoInfoMapper
 
 @JsonClass(generateAdapter = true)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/ShareRequestCancellation.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/ShareRequestCancellation.kt
index 820ff6974616cf42bebe5e1f6efb138555facdae..a96534fc3af1ac4f72b49297f4ac6d3c83f78c1e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/ShareRequestCancellation.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/ShareRequestCancellation.kt
@@ -17,7 +17,8 @@ package org.matrix.android.sdk.internal.crypto.model.rest
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
-import org.matrix.android.sdk.internal.crypto.model.rest.GossipingToDeviceObject.Companion.ACTION_SHARE_CANCELLATION
+import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject
+import org.matrix.android.sdk.api.session.crypto.model.GossipingToDeviceObject.Companion.ACTION_SHARE_CANCELLATION
 
 /**
  * Class representing a room key request cancellation content
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UploadSignatureQueryBuilder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UploadSignatureQueryBuilder.kt
index 1347c2f4b6551265ca0db78dc7eb28f882fd09d2..dd0ce47cd36162ffd15e2a1d1d4f12026e69a8b2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UploadSignatureQueryBuilder.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UploadSignatureQueryBuilder.kt
@@ -15,8 +15,9 @@
  */
 package org.matrix.android.sdk.internal.crypto.model.rest
 
-import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey
-import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
+import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey
+import org.matrix.android.sdk.api.session.crypto.crosssigning.toRest
+import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
 import org.matrix.android.sdk.internal.crypto.model.toRest
 
 /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt
index e6d8b5e84f246d5ee171184875fa3cf54b403214..19e66635c748812168e67d3d8b4c3e6b454ec39e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt
@@ -19,9 +19,12 @@ package org.matrix.android.sdk.internal.crypto.secrets
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.withContext
 import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
+import org.matrix.android.sdk.api.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2
+import org.matrix.android.sdk.api.crypto.SSSS_ALGORITHM_CURVE25519_AES_SHA2
 import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.listeners.ProgressListener
 import org.matrix.android.sdk.api.session.accountdata.SessionAccountDataService
+import org.matrix.android.sdk.api.session.crypto.keysbackup.computeRecoveryKey
 import org.matrix.android.sdk.api.session.events.model.toContent
 import org.matrix.android.sdk.api.session.securestorage.EncryptedSecretContent
 import org.matrix.android.sdk.api.session.securestorage.IntegrityResult
@@ -35,13 +38,10 @@ import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageServi
 import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo
 import org.matrix.android.sdk.api.session.securestorage.SsssKeySpec
 import org.matrix.android.sdk.api.session.securestorage.SsssPassphrase
+import org.matrix.android.sdk.api.util.fromBase64
+import org.matrix.android.sdk.api.util.toBase64NoPadding
 import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
-import org.matrix.android.sdk.internal.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2
-import org.matrix.android.sdk.internal.crypto.SSSS_ALGORITHM_CURVE25519_AES_SHA2
-import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64
-import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding
 import org.matrix.android.sdk.internal.crypto.keysbackup.generatePrivateKeyWithPassword
-import org.matrix.android.sdk.internal.crypto.keysbackup.util.computeRecoveryKey
 import org.matrix.android.sdk.internal.crypto.tools.HkdfSha256
 import org.matrix.android.sdk.internal.crypto.tools.withOlmDecryption
 import org.matrix.android.sdk.internal.di.UserId
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt
index 96ea5c03fa2540533d33558d08d6e1dafdf55853..8bedb78808f0e793f93df89f7069fae7a538768d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt
@@ -18,25 +18,27 @@ package org.matrix.android.sdk.internal.crypto.store
 
 import androidx.lifecycle.LiveData
 import androidx.paging.PagedList
+import org.matrix.android.sdk.api.session.crypto.NewSessionListener
+import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey
 import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
+import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo
+import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo
+import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
+import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
+import org.matrix.android.sdk.api.session.crypto.model.GossipingRequestState
+import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
+import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
+import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState
+import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest
+import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
 import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
 import org.matrix.android.sdk.api.util.Optional
-import org.matrix.android.sdk.internal.crypto.GossipingRequestState
-import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest
 import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon
-import org.matrix.android.sdk.internal.crypto.NewSessionListener
-import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestState
-import org.matrix.android.sdk.internal.crypto.OutgoingRoomKeyRequest
 import org.matrix.android.sdk.internal.crypto.OutgoingSecretRequest
-import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey
-import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
-import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
 import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
 import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
 import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
-import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent
-import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
-import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
 import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
 import org.matrix.olm.OlmAccount
 import org.matrix.olm.OlmOutboundGroupSession
@@ -54,7 +56,7 @@ internal interface IMXCryptoStore {
     /**
      * @return the olm account
      */
-    fun getOlmAccount(): OlmAccount
+    fun <T> doWithOlmAccount(block: (OlmAccount) -> T): T
 
     fun getOrCreateOlmAccount(): OlmAccount
 
@@ -261,7 +263,7 @@ internal interface IMXCryptoStore {
     fun storeSession(olmSessionWrapper: OlmSessionWrapper, deviceKey: String)
 
     /**
-     * Retrieve the end-to-end session ids between the logged-in user and another
+     * Retrieve all end-to-end session ids between our own device and another
      * device.
      *
      * @param deviceKey the public key of the other device.
@@ -270,7 +272,7 @@ internal interface IMXCryptoStore {
     fun getDeviceSessionIds(deviceKey: String): List<String>?
 
     /**
-     * Retrieve an end-to-end session between the logged-in user and another
+     * Retrieve an end-to-end session between our own device and another
      * device.
      *
      * @param sessionId the session Id.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/Helper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/Helper.kt
index 493e7fbc39ec4f0fb88a068210373ea9c52850d9..b841e9c6e58a2843322d40b5168b68a8331f2a86 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/Helper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/Helper.kt
@@ -28,7 +28,7 @@ import java.util.zip.GZIPOutputStream
 /**
  * Get realm, invoke the action, close realm, and return the result of the action
  */
-fun <T> doWithRealm(realmConfiguration: RealmConfiguration, action: (Realm) -> T): T {
+internal fun <T> doWithRealm(realmConfiguration: RealmConfiguration, action: (Realm) -> T): T {
     return Realm.getInstance(realmConfiguration).use { realm ->
         action.invoke(realm)
     }
@@ -37,7 +37,7 @@ fun <T> doWithRealm(realmConfiguration: RealmConfiguration, action: (Realm) -> T
 /**
  * Get realm, do the query, copy from realm, close realm, and return the copied result
  */
-fun <T : RealmObject> doRealmQueryAndCopy(realmConfiguration: RealmConfiguration, action: (Realm) -> T?): T? {
+internal fun <T : RealmObject> doRealmQueryAndCopy(realmConfiguration: RealmConfiguration, action: (Realm) -> T?): T? {
     return Realm.getInstance(realmConfiguration).use { realm ->
         action.invoke(realm)?.let { realm.copyFromRealm(it) }
     }
@@ -46,7 +46,7 @@ fun <T : RealmObject> doRealmQueryAndCopy(realmConfiguration: RealmConfiguration
 /**
  * Get realm, do the list query, copy from realm, close realm, and return the copied result
  */
-fun <T : RealmObject> doRealmQueryAndCopyList(realmConfiguration: RealmConfiguration, action: (Realm) -> Iterable<T>): Iterable<T> {
+internal fun <T : RealmObject> doRealmQueryAndCopyList(realmConfiguration: RealmConfiguration, action: (Realm) -> Iterable<T>): Iterable<T> {
     return Realm.getInstance(realmConfiguration).use { realm ->
         action.invoke(realm).let { realm.copyFromRealm(it) }
     }
@@ -55,13 +55,13 @@ fun <T : RealmObject> doRealmQueryAndCopyList(realmConfiguration: RealmConfigura
 /**
  * Get realm instance, invoke the action in a transaction and close realm
  */
-fun doRealmTransaction(realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) {
+internal fun doRealmTransaction(realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) {
     Realm.getInstance(realmConfiguration).use { realm ->
         realm.executeTransaction { action.invoke(it) }
     }
 }
 
-fun doRealmTransactionAsync(realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) {
+internal fun doRealmTransactionAsync(realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) {
     Realm.getInstance(realmConfiguration).use { realm ->
         realm.executeTransactionAsync { action.invoke(it) }
     }
@@ -70,7 +70,7 @@ fun doRealmTransactionAsync(realmConfiguration: RealmConfiguration, action: (Rea
 /**
  * Serialize any Serializable object, zip it and convert to Base64 String
  */
-fun serializeForRealm(o: Any?): String? {
+internal fun serializeForRealm(o: Any?): String? {
     if (o == null) {
         return null
     }
@@ -88,7 +88,7 @@ fun serializeForRealm(o: Any?): String? {
  * Do the opposite of serializeForRealm.
  */
 @Suppress("UNCHECKED_CAST")
-fun <T> deserializeFromRealm(string: String?): T? {
+internal fun <T> deserializeFromRealm(string: String?): T? {
     if (string == null) {
         return null
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt
index a07827c033a94c7c58b927600ca47ef1d45cfa7d..99adbbfbae00615d4bb5f05fa051b6413bf53e96 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt
@@ -25,35 +25,35 @@ import io.realm.Realm
 import io.realm.RealmConfiguration
 import io.realm.Sort
 import io.realm.kotlin.where
+import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
 import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.api.session.crypto.NewSessionListener
+import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey
 import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
+import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo
+import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo
+import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
+import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
+import org.matrix.android.sdk.api.session.crypto.model.GossipingRequestState
+import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
+import org.matrix.android.sdk.api.session.crypto.model.IncomingSecretShareRequest
+import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
+import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
+import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState
+import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest
+import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
 import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
 import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.api.util.Optional
 import org.matrix.android.sdk.api.util.toOptional
 import org.matrix.android.sdk.internal.crypto.GossipRequestType
-import org.matrix.android.sdk.internal.crypto.GossipingRequestState
-import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest
-import org.matrix.android.sdk.internal.crypto.IncomingSecretShareRequest
 import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon
-import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
-import org.matrix.android.sdk.internal.crypto.NewSessionListener
-import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestState
-import org.matrix.android.sdk.internal.crypto.OutgoingRoomKeyRequest
 import org.matrix.android.sdk.internal.crypto.OutgoingSecretRequest
-import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
-import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey
-import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
-import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
 import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
 import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
 import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
-import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent
-import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
-import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
-import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo
-import org.matrix.android.sdk.internal.crypto.store.SavedKeyBackupKeyInfo
 import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper
 import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity
 import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields
@@ -104,7 +104,6 @@ import timber.log.Timber
 import java.util.concurrent.Executors
 import java.util.concurrent.TimeUnit
 import javax.inject.Inject
-import kotlin.collections.set
 
 @SessionScope
 internal class RealmCryptoStore @Inject constructor(
@@ -124,12 +123,6 @@ internal class RealmCryptoStore @Inject constructor(
     // The olm account
     private var olmAccount: OlmAccount? = null
 
-    // Cache for OlmSession, to release them properly
-    private val olmSessionsToRelease = HashMap<String, OlmSessionWrapper>()
-
-    // Cache for InboundGroupSession, to release them properly
-    private val inboundGroupSessionToRelease = HashMap<String, OlmInboundGroupSessionWrapper2>()
-
     private val newSessionListeners = ArrayList<NewSessionListener>()
 
     override fun addNewSessionListener(listener: NewSessionListener) {
@@ -213,16 +206,6 @@ internal class RealmCryptoStore @Inject constructor(
             monarchyWriteAsyncExecutor.awaitTermination(1, TimeUnit.MINUTES)
         }
 
-        olmSessionsToRelease.forEach {
-            it.value.olmSession.releaseSession()
-        }
-        olmSessionsToRelease.clear()
-
-        inboundGroupSessionToRelease.forEach {
-            it.value.olmInboundGroupSession?.releaseSession()
-        }
-        inboundGroupSessionToRelease.clear()
-
         olmAccount?.releaseAccount()
 
         realmLocker?.close()
@@ -247,10 +230,18 @@ internal class RealmCryptoStore @Inject constructor(
         }
     }
 
-    override fun getOlmAccount(): OlmAccount {
-        return olmAccount!!
+    /**
+     * Olm account access should be synchronized
+     */
+    override fun <T> doWithOlmAccount(block: (OlmAccount) -> T): T {
+        return olmAccount!!.let { olmAccount ->
+            synchronized(olmAccount) {
+                block.invoke(olmAccount)
+            }
+        }
     }
 
+    @Synchronized
     override fun getOrCreateOlmAccount(): OlmAccount {
         doRealmTransaction(realmConfiguration) {
             val metaData = it.where<CryptoMetadataEntity>().findFirst()
@@ -680,13 +671,6 @@ internal class RealmCryptoStore @Inject constructor(
         if (sessionIdentifier != null) {
             val key = OlmSessionEntity.createPrimaryKey(sessionIdentifier, deviceKey)
 
-            // Release memory of previously known session, if it is not the same one
-            if (olmSessionsToRelease[key]?.olmSession != olmSessionWrapper.olmSession) {
-                olmSessionsToRelease[key]?.olmSession?.releaseSession()
-            }
-
-            olmSessionsToRelease[key] = olmSessionWrapper
-
             doRealmTransaction(realmConfiguration) {
                 val realmOlmSession = OlmSessionEntity().apply {
                     primaryKey = key
@@ -703,23 +687,18 @@ internal class RealmCryptoStore @Inject constructor(
 
     override fun getDeviceSession(sessionId: String, deviceKey: String): OlmSessionWrapper? {
         val key = OlmSessionEntity.createPrimaryKey(sessionId, deviceKey)
-
-        // If not in cache (or not found), try to read it from realm
-        if (olmSessionsToRelease[key] == null) {
-            doRealmQueryAndCopy(realmConfiguration) {
-                it.where<OlmSessionEntity>()
-                        .equalTo(OlmSessionEntityFields.PRIMARY_KEY, key)
-                        .findFirst()
-            }
-                    ?.let {
-                        val olmSession = it.getOlmSession()
-                        if (olmSession != null && it.sessionId != null) {
-                            olmSessionsToRelease[key] = OlmSessionWrapper(olmSession, it.lastReceivedMessageTs)
-                        }
-                    }
+        return doRealmQueryAndCopy(realmConfiguration) {
+            it.where<OlmSessionEntity>()
+                    .equalTo(OlmSessionEntityFields.PRIMARY_KEY, key)
+                    .findFirst()
         }
-
-        return olmSessionsToRelease[key]
+                ?.let {
+                    val olmSession = it.getOlmSession()
+                    if (olmSession != null && it.sessionId != null) {
+                        return@let OlmSessionWrapper(olmSession, it.lastReceivedMessageTs)
+                    }
+                    null
+                }
     }
 
     override fun getLastUsedSessionId(deviceKey: String): String? {
@@ -761,13 +740,6 @@ internal class RealmCryptoStore @Inject constructor(
                 if (sessionIdentifier != null) {
                     val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, session.senderKey)
 
-                    // Release memory of previously known session, if it is not the same one
-                    if (inboundGroupSessionToRelease[key] != session) {
-                        inboundGroupSessionToRelease[key]?.olmInboundGroupSession?.releaseSession()
-                    }
-
-                    inboundGroupSessionToRelease[key] = session
-
                     val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply {
                         primaryKey = key
                         sessionId = sessionIdentifier
@@ -784,20 +756,12 @@ internal class RealmCryptoStore @Inject constructor(
     override fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2? {
         val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey)
 
-        // If not in cache (or not found), try to read it from realm
-        if (inboundGroupSessionToRelease[key] == null) {
-            doWithRealm(realmConfiguration) {
-                it.where<OlmInboundGroupSessionEntity>()
-                        .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
-                        .findFirst()
-                        ?.getInboundGroupSession()
-            }
-                    ?.let {
-                        inboundGroupSessionToRelease[key] = it
-                    }
+        return doWithRealm(realmConfiguration) {
+            it.where<OlmInboundGroupSessionEntity>()
+                    .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
+                    .findFirst()
+                    ?.getInboundGroupSession()
         }
-
-        return inboundGroupSessionToRelease[key]
     }
 
     override fun getCurrentOutboundGroupSessionForRoom(roomId: String): OutboundGroupSessionWrapper? {
@@ -853,10 +817,6 @@ internal class RealmCryptoStore @Inject constructor(
     override fun removeInboundGroupSession(sessionId: String, senderKey: String) {
         val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey)
 
-        // Release memory of previously known session
-        inboundGroupSessionToRelease[key]?.olmInboundGroupSession?.releaseSession()
-        inboundGroupSessionToRelease.remove(key)
-
         doRealmTransaction(realmConfiguration) {
             it.where<OlmInboundGroupSessionEntity>()
                     .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/CrossSigningKeysMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/CrossSigningKeysMapper.kt
index c15414a8dd351acc09bc68c80658ca6292aa9164..5e4b9b96da8ac3739606c62883b084d9818cad9e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/CrossSigningKeysMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/CrossSigningKeysMapper.kt
@@ -19,8 +19,8 @@ package org.matrix.android.sdk.internal.crypto.store.db.mapper
 import com.squareup.moshi.Moshi
 import com.squareup.moshi.Types
 import io.realm.RealmList
-import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel
-import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey
+import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey
+import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
 import org.matrix.android.sdk.internal.crypto.store.db.model.KeyInfoEntity
 import timber.log.Timber
 import javax.inject.Inject
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo001Legacy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo001Legacy.kt
index 0e44689428ab896e1804aefcfe468408ab10ea50..7dee42e51a6dc557b1e06091dd4c3db47b36349c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo001Legacy.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo001Legacy.kt
@@ -21,7 +21,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntityFie
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 import timber.log.Timber
 
-class MigrateCryptoTo001Legacy(realm: DynamicRealm) : RealmMigrator(realm, 1) {
+internal class MigrateCryptoTo001Legacy(realm: DynamicRealm) : RealmMigrator(realm, 1) {
 
     override fun doMigrate(realm: DynamicRealm) {
         Timber.d("Add field lastReceivedMessageTs (Long) and set the value to 0")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo002Legacy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo002Legacy.kt
index 84e627a688e987a9772beca7b84df260a5d36db2..1b53e1928ad95771406269bed98733a11bc3d65c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo002Legacy.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo002Legacy.kt
@@ -22,7 +22,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntit
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 import timber.log.Timber
 
-class MigrateCryptoTo002Legacy(realm: DynamicRealm) : RealmMigrator(realm, 2) {
+internal class MigrateCryptoTo002Legacy(realm: DynamicRealm) : RealmMigrator(realm, 2) {
 
     override fun doMigrate(realm: DynamicRealm) {
         Timber.d("Update IncomingRoomKeyRequestEntity format: requestBodyString field is exploded into several fields")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo003RiotX.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo003RiotX.kt
index b468a56af6e39751d7a77b2cb280ccf6161a65d5..34d1afa2d868717ebbd6d6fcb2c1d11b7cc203d7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo003RiotX.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo003RiotX.kt
@@ -26,7 +26,7 @@ import org.matrix.androidsdk.crypto.data.MXDeviceInfo
 import org.matrix.androidsdk.crypto.data.MXOlmInboundGroupSession2
 import timber.log.Timber
 
-class MigrateCryptoTo003RiotX(realm: DynamicRealm) : RealmMigrator(realm, 3) {
+internal class MigrateCryptoTo003RiotX(realm: DynamicRealm) : RealmMigrator(realm, 3) {
 
     override fun doMigrate(realm: DynamicRealm) {
         Timber.d("Migrate to RiotX model")
@@ -40,7 +40,7 @@ class MigrateCryptoTo003RiotX(realm: DynamicRealm) : RealmMigrator(realm, 3) {
                     try {
                         val oldSerializedData = obj.getString("deviceInfoData")
                         deserializeFromRealm<MXDeviceInfo>(oldSerializedData)?.let { legacyMxDeviceInfo ->
-                            val newMxDeviceInfo = org.matrix.android.sdk.internal.crypto.model.MXDeviceInfo(
+                            val newMxDeviceInfo = org.matrix.android.sdk.api.session.crypto.model.MXDeviceInfo(
                                     deviceId = legacyMxDeviceInfo.deviceId,
                                     userId = legacyMxDeviceInfo.userId,
                                     algorithms = legacyMxDeviceInfo.algorithms,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo004.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo004.kt
index 20a4814b8d1d69e1ced0cfa578e1f3eb05f65dbf..52d0124c2bc3d386bcff82216ca20c290afb2c72 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo004.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo004.kt
@@ -19,8 +19,8 @@ package org.matrix.android.sdk.internal.crypto.store.db.migration
 import com.squareup.moshi.Moshi
 import com.squareup.moshi.Types
 import io.realm.DynamicRealm
+import org.matrix.android.sdk.api.session.crypto.model.MXDeviceInfo
 import org.matrix.android.sdk.api.util.JsonDict
-import org.matrix.android.sdk.internal.crypto.model.MXDeviceInfo
 import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm
 import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields
 import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntityFields
@@ -33,7 +33,7 @@ import org.matrix.android.sdk.internal.util.database.RealmMigrator
 import timber.log.Timber
 
 // Version 4L added Cross Signing info persistence
-class MigrateCryptoTo004(realm: DynamicRealm) : RealmMigrator(realm, 4) {
+internal class MigrateCryptoTo004(realm: DynamicRealm) : RealmMigrator(realm, 4) {
 
     override fun doMigrate(realm: DynamicRealm) {
         if (realm.schema.contains("TrustLevelEntity")) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo005.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo005.kt
index 8365d3446440828b53fb90e84d830e5b382602b1..e1d759876708ad992e6b817118da8df298a690c6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo005.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo005.kt
@@ -22,7 +22,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRe
 import org.matrix.android.sdk.internal.crypto.store.db.model.OutgoingGossipingRequestEntityFields
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
-class MigrateCryptoTo005(realm: DynamicRealm) : RealmMigrator(realm, 5) {
+internal class MigrateCryptoTo005(realm: DynamicRealm) : RealmMigrator(realm, 5) {
 
     override fun doMigrate(realm: DynamicRealm) {
         realm.schema.remove("OutgoingRoomKeyRequestEntity")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo006.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo006.kt
index a29a7918268e062532c4de0bccaba060f82cad8f..39b28985140c4357c80e321a66aeac129ce72312 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo006.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo006.kt
@@ -21,7 +21,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntit
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 import timber.log.Timber
 
-class MigrateCryptoTo006(realm: DynamicRealm) : RealmMigrator(realm, 6) {
+internal class MigrateCryptoTo006(realm: DynamicRealm) : RealmMigrator(realm, 6) {
 
     override fun doMigrate(realm: DynamicRealm) {
         Timber.d("Updating CryptoMetadataEntity table")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo007.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo007.kt
index 7ae58e7fc052082c88bfcc291209a2b2c11da028..718b9787d268684a16dec7d8784869bbd2569880 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo007.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo007.kt
@@ -28,7 +28,7 @@ import org.matrix.android.sdk.internal.di.MoshiProvider
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 import timber.log.Timber
 
-class MigrateCryptoTo007(realm: DynamicRealm) : RealmMigrator(realm, 7) {
+internal class MigrateCryptoTo007(realm: DynamicRealm) : RealmMigrator(realm, 7) {
 
     override fun doMigrate(realm: DynamicRealm) {
         Timber.d("Updating KeyInfoEntity table")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo008.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo008.kt
index e3bd3f035a290bf076b1ea69de4edc59406de053..785e6a04f42a6fe8ee02ce534b8ea3f92c929c83 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo008.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo008.kt
@@ -22,7 +22,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFie
 import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntityFields
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
-class MigrateCryptoTo008(realm: DynamicRealm) : RealmMigrator(realm, 8) {
+internal class MigrateCryptoTo008(realm: DynamicRealm) : RealmMigrator(realm, 8) {
 
     override fun doMigrate(realm: DynamicRealm) {
         realm.schema.create("MyDeviceLastSeenInfoEntity")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo009.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo009.kt
index ed705318f9e0637f15d9d56b226e8f4faf8d1f45..8d9d24dfba807b084ec46a28073fd24f0135b9db 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo009.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo009.kt
@@ -23,7 +23,7 @@ import org.matrix.android.sdk.internal.util.database.RealmMigrator
 import timber.log.Timber
 
 // Fixes duplicate devices in UserEntity#devices
-class MigrateCryptoTo009(realm: DynamicRealm) : RealmMigrator(realm, 9) {
+internal class MigrateCryptoTo009(realm: DynamicRealm) : RealmMigrator(realm, 9) {
 
     override fun doMigrate(realm: DynamicRealm) {
         val userEntities = realm.where("UserEntity").findAll()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo010.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo010.kt
index 8d69ee5558890013b6fd786f5f0e5a7c401c1f94..faf0d58832008d69db972970cfde5fb67be908b6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo010.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo010.kt
@@ -22,7 +22,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEnti
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
 // Version 10L added WithHeld Keys Info (MSC2399)
-class MigrateCryptoTo010(realm: DynamicRealm) : RealmMigrator(realm, 10) {
+internal class MigrateCryptoTo010(realm: DynamicRealm) : RealmMigrator(realm, 10) {
 
     override fun doMigrate(realm: DynamicRealm) {
         realm.schema.create("WithHeldSessionEntity")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo011.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo011.kt
index c9825a7f3d44042e365972c5775be9b72e9948de..feaab4bb193d6af34211fb21495d7c100b0ee797 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo011.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo011.kt
@@ -21,7 +21,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntit
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
 // Version 11L added deviceKeysSentToServer boolean to CryptoMetadataEntity
-class MigrateCryptoTo011(realm: DynamicRealm) : RealmMigrator(realm, 11) {
+internal class MigrateCryptoTo011(realm: DynamicRealm) : RealmMigrator(realm, 11) {
 
     override fun doMigrate(realm: DynamicRealm) {
         realm.schema.get("CryptoMetadataEntity")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo012.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo012.kt
index 6b1460d9d6c7c14e3f36de9e01c1f434f5abb943..4626757a06f03ab3dccf901d8b6210d7a98feefb 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo012.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo012.kt
@@ -22,7 +22,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.OutboundGroupSessio
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
 // Version 12L added outbound group session persistence
-class MigrateCryptoTo012(realm: DynamicRealm) : RealmMigrator(realm, 12) {
+internal class MigrateCryptoTo012(realm: DynamicRealm) : RealmMigrator(realm, 12) {
 
     override fun doMigrate(realm: DynamicRealm) {
         val outboundEntitySchema = realm.schema.create("OutboundGroupSessionInfoEntity")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo013.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo013.kt
index dc22c5f133d6f80d83cd4ecbb5f1ccea9359ab92..dc8984da418bfd266527c48601078374dbd6f7be 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo013.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo013.kt
@@ -21,7 +21,7 @@ import org.matrix.android.sdk.internal.util.database.RealmMigrator
 import timber.log.Timber
 
 // Version 13L delete unreferenced TrustLevelEntity
-class MigrateCryptoTo013(realm: DynamicRealm) : RealmMigrator(realm, 13) {
+internal class MigrateCryptoTo013(realm: DynamicRealm) : RealmMigrator(realm, 13) {
 
     override fun doMigrate(realm: DynamicRealm) {
         // Use a trick to do that... Ref: https://stackoverflow.com/questions/55221366
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo014.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo014.kt
index f0089e3427727152f852f97cf9cb5713a3ba6050..548672790a9bedcafcc7772407bf3cb9c9eb07f1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo014.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo014.kt
@@ -22,7 +22,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntity
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
 // Version 14L Update the way we remember key sharing
-class MigrateCryptoTo014(realm: DynamicRealm) : RealmMigrator(realm, 14) {
+internal class MigrateCryptoTo014(realm: DynamicRealm) : RealmMigrator(realm, 14) {
 
     override fun doMigrate(realm: DynamicRealm) {
         realm.schema.get("SharedSessionEntity")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo015.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo015.kt
index 465c18555a43ff47201591e978de7b1f7bdc8d74..bca02c2e6e257cb94015264997907be7587b95ef 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo015.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo015.kt
@@ -17,12 +17,12 @@
 package org.matrix.android.sdk.internal.crypto.store.db.migration
 
 import io.realm.DynamicRealm
-import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
+import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
 import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFields
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
 // Version 15L adds wasEncryptedOnce field to CryptoRoomEntity
-class MigrateCryptoTo015(realm: DynamicRealm) : RealmMigrator(realm, 15) {
+internal class MigrateCryptoTo015(realm: DynamicRealm) : RealmMigrator(realm, 15) {
 
     override fun doMigrate(realm: DynamicRealm) {
         realm.schema.get("CryptoRoomEntity")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CrossSigningInfoEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CrossSigningInfoEntity.kt
index 8599c972e9cedeb7d17c76928a6a25705526f178..5aba9bb9ba627b82efb756e7367a2a65a164288d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CrossSigningInfoEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CrossSigningInfoEntity.kt
@@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.crypto.store.db.model
 import io.realm.RealmList
 import io.realm.RealmObject
 import io.realm.annotations.PrimaryKey
-import org.matrix.android.sdk.internal.crypto.model.KeyUsage
+import org.matrix.android.sdk.api.session.crypto.crosssigning.KeyUsage
 import org.matrix.android.sdk.internal.extensions.clearWith
 
 internal open class CrossSigningInfoEntity(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMapper.kt
index 7ba986699ab60bf26acf5e2891225096a0fb6897..c71d5e73a228b1f755ee703c350466943e8d9661 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMapper.kt
@@ -17,14 +17,14 @@ package org.matrix.android.sdk.internal.crypto.store.db.model
 
 import com.squareup.moshi.Moshi
 import com.squareup.moshi.Types
+import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
+import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
+import org.matrix.android.sdk.api.session.crypto.model.UnsignedDeviceInfo
 import org.matrix.android.sdk.api.util.JsonDict
-import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel
-import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
-import org.matrix.android.sdk.internal.crypto.model.rest.UnsignedDeviceInfo
 import org.matrix.android.sdk.internal.di.SerializeNulls
 import timber.log.Timber
 
-object CryptoMapper {
+internal object CryptoMapper {
 
     private val moshi = Moshi.Builder().add(SerializeNulls.JSON_ADAPTER_FACTORY).build()
     private val listMigrationAdapter = moshi.adapter<List<String>>(Types.newParameterizedType(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/GossipingEventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/GossipingEventEntity.kt
index 75094f01db5d9332f0261972b1bfc43d86385745..a024e092b7c40e0f2644a016395f8385cdaa9ee2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/GossipingEventEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/GossipingEventEntity.kt
@@ -20,10 +20,10 @@ import com.squareup.moshi.JsonDataException
 import io.realm.RealmObject
 import io.realm.annotations.Index
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
+import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
+import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.room.send.SendState
-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.ContentMapper
 import org.matrix.android.sdk.internal.di.MoshiProvider
 import timber.log.Timber
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt
index 4457a44cb2deddee303aafda76a5f885acba4701..f05c8853c8dc60aa68416f82efed3b98b689dc7f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/IncomingGossipingRequestEntity.kt
@@ -19,12 +19,12 @@ package org.matrix.android.sdk.internal.crypto.store.db.model
 import io.realm.RealmObject
 import io.realm.annotations.Index
 import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.api.session.crypto.model.GossipingRequestState
+import org.matrix.android.sdk.api.session.crypto.model.IncomingRoomKeyRequest
+import org.matrix.android.sdk.api.session.crypto.model.IncomingSecretShareRequest
+import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
 import org.matrix.android.sdk.internal.crypto.GossipRequestType
-import org.matrix.android.sdk.internal.crypto.GossipingRequestState
-import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest
-import org.matrix.android.sdk.internal.crypto.IncomingSecretShareRequest
 import org.matrix.android.sdk.internal.crypto.IncomingShareRequestCommon
-import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
 
 internal open class IncomingGossipingRequestEntity(@Index var requestId: String? = "",
                                                    @Index var typeStr: String? = null,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt
index a19547fddc37e0f9acf16fcfb67237f1549f8e1b..0e1278967eb4adcb1be665856498c62e6110b555 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingGossipingRequestEntity.kt
@@ -21,12 +21,12 @@ import com.squareup.moshi.Types
 import io.realm.RealmObject
 import io.realm.annotations.Index
 import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.api.session.crypto.model.OutgoingGossipingRequestState
+import org.matrix.android.sdk.api.session.crypto.model.OutgoingRoomKeyRequest
+import org.matrix.android.sdk.api.session.crypto.model.RoomKeyRequestBody
 import org.matrix.android.sdk.internal.crypto.GossipRequestType
 import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequest
-import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestState
-import org.matrix.android.sdk.internal.crypto.OutgoingRoomKeyRequest
 import org.matrix.android.sdk.internal.crypto.OutgoingSecretRequest
-import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
 import org.matrix.android.sdk.internal.di.MoshiProvider
 
 internal open class OutgoingGossipingRequestEntity(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/WithHeldSessionEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/WithHeldSessionEntity.kt
index 6d7889053b94cd80c8f916c85f8bf4f1e5c9d148..93048e5775c1e5102728c7200cf9f19d2d840935 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/WithHeldSessionEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/WithHeldSessionEntity.kt
@@ -18,7 +18,7 @@ package org.matrix.android.sdk.internal.crypto.store.db.model
 
 import io.realm.RealmObject
 import io.realm.annotations.Index
-import org.matrix.android.sdk.internal.crypto.model.event.WithHeldCode
+import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
 
 /**
  * When an encrypted message is sent in a room, the megolm key might not be sent to all devices present in the room.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/SharedSessionQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/SharedSessionQueries.kt
index 2784e58425aeb14b574b42e87655c3210c10ebd5..8bf9794375fb0be360695b1577daf4432c6d958b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/SharedSessionQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/SharedSessionQueries.kt
@@ -20,7 +20,7 @@ import io.realm.Realm
 import io.realm.RealmResults
 import io.realm.kotlin.createObject
 import io.realm.kotlin.where
-import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
+import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
 import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntity
 import org.matrix.android.sdk.internal.crypto.store.db.model.SharedSessionEntityFields
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/WithHeldSessionQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/WithHeldSessionQueries.kt
index b8a3e3613718317e4ba71331d3d226b7fdfccb20..c253af2bf69e7e60a89deaf7542157a7897cf2d3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/WithHeldSessionQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/query/WithHeldSessionQueries.kt
@@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.crypto.store.db.query
 import io.realm.Realm
 import io.realm.kotlin.createObject
 import io.realm.kotlin.where
-import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
+import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
 import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEntity
 import org.matrix.android.sdk.internal.crypto.store.db.model.WithHeldSessionEntityFields
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/ClaimOneTimeKeysForUsersDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/ClaimOneTimeKeysForUsersDeviceTask.kt
index d5cf749db738376f4a486f753fdd1da0123a65f1..96848a264d2d736343d7be566f740e3a7e22d840 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/ClaimOneTimeKeysForUsersDeviceTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/ClaimOneTimeKeysForUsersDeviceTask.kt
@@ -16,9 +16,9 @@
 
 package org.matrix.android.sdk.internal.crypto.tasks
 
+import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
 import org.matrix.android.sdk.internal.crypto.api.CryptoApi
 import org.matrix.android.sdk.internal.crypto.model.MXKey
-import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
 import org.matrix.android.sdk.internal.crypto.model.rest.KeysClaimBody
 import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
 import org.matrix.android.sdk.internal.network.executeRequest
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt
index 627352f5680f2193a7b5452518966d90edf5da83..1e395796a98a3c147cb00a9b118e071d8a0f1f6f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt
@@ -15,18 +15,18 @@
  */
 package org.matrix.android.sdk.internal.crypto.tasks
 
+import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
 import org.matrix.android.sdk.api.session.crypto.CryptoService
+import org.matrix.android.sdk.api.session.crypto.model.MXEncryptEventContentResult
+import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.toContent
 import org.matrix.android.sdk.api.session.room.send.SendState
-import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
-import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
-import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult
+import org.matrix.android.sdk.api.util.awaitCallback
 import org.matrix.android.sdk.internal.database.mapper.ContentMapper
 import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
 import org.matrix.android.sdk.internal.task.Task
-import org.matrix.android.sdk.internal.util.awaitCallback
 import javax.inject.Inject
 
 internal interface EncryptEventTask : Task<EncryptEventTask.Params, Event> {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetDeviceInfoTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetDeviceInfoTask.kt
index 9f20ea598de6331d3d46fa2972d896232a413a3b..87dbd8d1a09ecf3defaf204c9179489b8a20af61 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetDeviceInfoTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetDeviceInfoTask.kt
@@ -16,8 +16,8 @@
 
 package org.matrix.android.sdk.internal.crypto.tasks
 
+import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
 import org.matrix.android.sdk.internal.crypto.api.CryptoApi
-import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
 import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
 import org.matrix.android.sdk.internal.network.executeRequest
 import org.matrix.android.sdk.internal.task.Task
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetDevicesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetDevicesTask.kt
index 52f9f732994ca52a96903bf6bca735f1a6a56216..27cb17f2abe603eacaae6767f8c363136ca3574d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetDevicesTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/GetDevicesTask.kt
@@ -16,8 +16,8 @@
 
 package org.matrix.android.sdk.internal.crypto.tasks
 
+import org.matrix.android.sdk.api.session.crypto.model.DevicesListResponse
 import org.matrix.android.sdk.internal.crypto.api.CryptoApi
-import org.matrix.android.sdk.internal.crypto.model.rest.DevicesListResponse
 import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
 import org.matrix.android.sdk.internal.network.executeRequest
 import org.matrix.android.sdk.internal.task.Task
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt
index e2fd54f0d8744de2c943e0b55e2c69866cd8e435..eefdd25044eeef01cd730ac0bb091ddc86b774a7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt
@@ -18,13 +18,13 @@ package org.matrix.android.sdk.internal.crypto.tasks
 
 import dagger.Lazy
 import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
+import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey
+import org.matrix.android.sdk.api.session.crypto.crosssigning.KeyUsage
+import org.matrix.android.sdk.api.util.toBase64NoPadding
 import org.matrix.android.sdk.internal.auth.registration.handleUIA
 import org.matrix.android.sdk.internal.crypto.MXOlmDevice
 import org.matrix.android.sdk.internal.crypto.MyDeviceInfoHolder
 import org.matrix.android.sdk.internal.crypto.crosssigning.canonicalSignable
-import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding
-import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey
-import org.matrix.android.sdk.internal.crypto.model.KeyUsage
 import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder
 import org.matrix.android.sdk.internal.di.UserId
 import org.matrix.android.sdk.internal.task.Task
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt
index c6af9b0cd1b0542d96deb0dfdf07493684beeb92..fc4d422360c72e1657453b80c83f82d53cd833d5 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendToDeviceTask.kt
@@ -16,8 +16,8 @@
 
 package org.matrix.android.sdk.internal.crypto.tasks
 
+import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
 import org.matrix.android.sdk.internal.crypto.api.CryptoApi
-import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
 import org.matrix.android.sdk.internal.crypto.model.rest.SendToDeviceBody
 import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
 import org.matrix.android.sdk.internal.network.executeRequest
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSigningKeysTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSigningKeysTask.kt
index 6cb14ded63138379812dcc33d514ba17afecc4d0..0a0df11bd339b6e5264f3554bd2d62b9fa65d7f0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSigningKeysTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSigningKeysTask.kt
@@ -18,10 +18,10 @@ package org.matrix.android.sdk.internal.crypto.tasks
 
 import org.matrix.android.sdk.api.auth.UIABaseAuth
 import org.matrix.android.sdk.api.failure.Failure
+import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey
+import org.matrix.android.sdk.api.session.crypto.crosssigning.toRest
 import org.matrix.android.sdk.internal.crypto.api.CryptoApi
-import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey
 import org.matrix.android.sdk.internal.crypto.model.rest.UploadSigningKeysBody
-import org.matrix.android.sdk.internal.crypto.model.toRest
 import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
 import org.matrix.android.sdk.internal.network.executeRequest
 import org.matrix.android.sdk.internal.task.Task
@@ -42,7 +42,7 @@ internal interface UploadSigningKeysTask : Task<UploadSigningKeysTask.Params, Un
     )
 }
 
-data class UploadSigningKeys(val failures: Map<String, Any>?) : Failure.FeatureFailure()
+internal data class UploadSigningKeys(val failures: Map<String, Any>?) : Failure.FeatureFailure()
 
 internal class DefaultUploadSigningKeysTask @Inject constructor(
         private val cryptoApi: CryptoApi,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/HkdfSha256.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/HkdfSha256.kt
index 6839ccd3262c5679c27d3e977196a0405f0e1752..b230f0d02954336bf62b8ca8f72f8917ff4ae4aa 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/HkdfSha256.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/HkdfSha256.kt
@@ -26,7 +26,7 @@ import kotlin.math.ceil
  * HMAC-based Extract-and-Expand Key Derivation Function (HkdfSha256)
  * [RFC-5869] https://tools.ietf.org/html/rfc5869
  */
-object HkdfSha256 {
+internal object HkdfSha256 {
 
     fun deriveSecret(inputKeyMaterial: ByteArray, salt: ByteArray?, info: ByteArray, outputLength: Int): ByteArray {
         return expand(extract(salt, inputKeyMaterial), info, outputLength)
@@ -70,7 +70,7 @@ object HkdfSha256 {
            T(2) = HMAC-Hash(PRK, T(1) | info | 0x02)
            T(3) = HMAC-Hash(PRK, T(2) | info | 0x03)
            ...
-        */
+         */
         val n = ceil(outputLength.toDouble() / HASH_LEN.toDouble()).toInt()
 
         var stepHash = ByteArray(0) // T(0) empty string (zero length)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt
index 388ecb965952802695fff2d543fa7b8347599a9d..28bf1d70f7fb09f867916430d93214baae04123c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt
@@ -23,10 +23,13 @@ import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
+import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
 import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
 import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
 import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
 import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
+import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
+import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
 import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
 import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
 import org.matrix.android.sdk.api.session.crypto.verification.QrCodeVerificationTransaction
@@ -41,6 +44,7 @@ import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.LocalEcho
 import org.matrix.android.sdk.api.session.events.model.RelationType
+import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.room.model.message.MessageContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent
@@ -59,10 +63,6 @@ import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager
 import org.matrix.android.sdk.internal.crypto.MyDeviceInfoHolder
 import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
 import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
-import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel
-import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
-import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
-import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
 import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationAccept
 import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationCancel
 import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationDone
@@ -364,14 +364,14 @@ internal class DefaultVerificationService @Inject constructor(
         dispatchRequestAdded(pendingVerificationRequest)
 
         /*
-        * After the m.key.verification.ready event is sent, either party can send an m.key.verification.start event
-        * to begin the verification.
-        * If both parties send an m.key.verification.start event, and they both specify the same verification method,
-        * then the event sent by the user whose user ID is the smallest is used, and the other m.key.verification.start
-        * event is ignored.
-        * In the case of a single user verifying two of their devices, the device ID is compared instead.
-        * If both parties send an m.key.verification.start event, but they specify different verification methods,
-        * the verification should be cancelled with a code of m.unexpected_message.
+         * After the m.key.verification.ready event is sent, either party can send an m.key.verification.start event
+         * to begin the verification.
+         * If both parties send an m.key.verification.start event, and they both specify the same verification method,
+         * then the event sent by the user whose user ID is the smallest is used, and the other m.key.verification.start
+         * event is ignored.
+         * In the case of a single user verifying two of their devices, the device ID is compared instead.
+         * If both parties send an m.key.verification.start event, but they specify different verification methods,
+         * the verification should be cancelled with a code of m.unexpected_message.
          */
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationTransaction.kt
index 6043c21b665b225d78cdf934cde045f0663b3554..69d9388c5ff6754ee4229193c0acef99e8f3c4e9 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationTransaction.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationTransaction.kt
@@ -17,12 +17,12 @@ package org.matrix.android.sdk.internal.crypto.verification
 
 import org.matrix.android.sdk.api.MatrixCallback
 import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
+import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel
 import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
 import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
 import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager
 import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
 import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
-import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel
 import timber.log.Timber
 
 /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfo.kt
index 368a9b6b54367203ae540d9e095df0dec30b3a7b..0615773a7ba4631020a7d92d08bbadbbc8bf4ec2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfo.kt
@@ -15,10 +15,10 @@
  */
 package org.matrix.android.sdk.internal.crypto.verification
 
+import org.matrix.android.sdk.api.session.crypto.model.SendToDeviceObject
 import org.matrix.android.sdk.api.session.events.model.Content
-import org.matrix.android.sdk.internal.crypto.model.rest.SendToDeviceObject
 
-interface VerificationInfo<ValidObjectType> {
+internal interface VerificationInfo<ValidObjectType> {
     fun toEventContent(): Content? = null
     fun toSendToDeviceObject(): SendToDeviceObject? = null
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoStart.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoStart.kt
index f727aff39d3803c227016d44df8675093597fac5..40c96dfa9591dc5e32fa62890060376f7e5c5230 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoStart.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationInfoStart.kt
@@ -103,7 +103,7 @@ internal interface VerificationInfoStart : VerificationInfo<ValidVerificationInf
     }
 }
 
-sealed class ValidVerificationInfoStart(
+internal sealed class ValidVerificationInfoStart(
         open val transactionId: String,
         open val fromDevice: String) {
     data class SasVerificationInfoStart(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt
index 4a2a15312799f5959acfa464626eae16cd3ca237..52166761abbdf02b43a0e7767676a6adcf3781e2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt
@@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.crypto.verification
 
 import io.realm.Realm
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
+import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
 import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
@@ -29,13 +30,11 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageVerification
 import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationStartContent
 import org.matrix.android.sdk.internal.crypto.EventDecryptor
-import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
 import org.matrix.android.sdk.internal.database.model.EventInsertType
 import org.matrix.android.sdk.internal.di.DeviceId
 import org.matrix.android.sdk.internal.di.UserId
 import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
 import timber.log.Timber
-import java.util.ArrayList
 import javax.inject.Inject
 
 internal class VerificationMessageProcessor @Inject constructor(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationStateExt.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationStateExt.kt
index 0617f32c2424b7637796cc91b488f58cf82adfac..d8cba1fffa7f089bfa17c80a22ea5b903b07e526 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationStateExt.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationStateExt.kt
@@ -16,8 +16,8 @@
 
 package org.matrix.android.sdk.internal.crypto.verification
 
-import org.matrix.android.sdk.api.crypto.VerificationState
-import org.matrix.android.sdk.api.crypto.isCanceled
+import org.matrix.android.sdk.api.session.crypto.verification.VerificationState
+import org.matrix.android.sdk.api.session.crypto.verification.isCanceled
 
 // State transition with control
 internal fun VerificationState?.toState(newState: VerificationState): VerificationState {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt
index 45f81439377b97a00ae26d332b86ecddbea8144c..40deda2745531dbab494a28a32e8c93a1a6c3f19 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt
@@ -16,12 +16,12 @@
 package org.matrix.android.sdk.internal.crypto.verification
 
 import org.matrix.android.sdk.api.MatrixCallback
+import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
 import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
 import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest
 import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.room.model.message.MessageType
-import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
 import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationAccept
 import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationCancel
 import org.matrix.android.sdk.internal.crypto.model.rest.KeyVerificationDone
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt
index 829e066bf3924ef1a8f40d06ff0cf5867fa536ec..06f0b367980a26838f0d853b58028bb5b352911b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt
@@ -21,15 +21,14 @@ import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
 import org.matrix.android.sdk.api.session.crypto.verification.QrCodeVerificationTransaction
 import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
 import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.util.fromBase64
 import org.matrix.android.sdk.internal.crypto.IncomingGossipingRequestManager
 import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
 import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
-import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64
 import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64Safe
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationTransaction
 import org.matrix.android.sdk.internal.crypto.verification.ValidVerificationInfoStart
-import org.matrix.android.sdk.internal.util.exhaustive
 import timber.log.Timber
 
 internal class DefaultQrCodeVerificationTransaction(
@@ -129,7 +128,7 @@ internal class DefaultQrCodeVerificationTransaction(
                     // Nothing special here, we will send a reciprocate start event, and then the other session will trust it's view of the MSK
                 }
             }
-        }.exhaustive
+        }
 
         val toVerifyDeviceIds = mutableListOf<String>()
 
@@ -174,7 +173,7 @@ internal class DefaultQrCodeVerificationTransaction(
                     Unit
                 }
             }
-        }.exhaustive
+        }
 
         if (!canTrustOtherUserMasterKey && toVerifyDeviceIds.isEmpty()) {
             // Nothing to verify
@@ -272,6 +271,7 @@ internal class DefaultQrCodeVerificationTransaction(
                 // I now know that i can trust my MSK
                 trust(true, emptyList(), true)
             }
+            null                                           -> Unit
         }
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/Extensions.kt
index 76e88442b60759cc492971feffdcf801a883708e..b80c29c244af1b3b057dda558a4d8f47329c346d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/Extensions.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/Extensions.kt
@@ -16,14 +16,14 @@
 
 package org.matrix.android.sdk.internal.crypto.verification.qrcode
 
-import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64
-import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding
+import org.matrix.android.sdk.api.util.fromBase64
+import org.matrix.android.sdk.api.util.toBase64NoPadding
 import org.matrix.android.sdk.internal.extensions.toUnsignedInt
 
 // MATRIX
 private val prefix = "MATRIX".toByteArray(Charsets.ISO_8859_1)
 
-fun QrCodeData.toEncodedString(): String {
+internal fun QrCodeData.toEncodedString(): String {
     var result = ByteArray(0)
 
     // MATRIX
@@ -67,7 +67,7 @@ fun QrCodeData.toEncodedString(): String {
     return result.toString(Charsets.ISO_8859_1)
 }
 
-fun String.toQrCodeData(): QrCodeData? {
+internal fun String.toQrCodeData(): QrCodeData? {
     val byteArray = toByteArray(Charsets.ISO_8859_1)
 
     // Size should be min 6 + 1 + 1 + 2 + ? + 32 + 32 + ? = 74 + transactionLength + secretLength
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeData.kt
index 25c04efde7db72bf5f72b95700cbb7b69907b512..0ac57db9bc779bc11b5bf9267b94f0856c7fb0a6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeData.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeData.kt
@@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.crypto.verification.qrcode
 /**
  * Ref: https://github.com/uhoreg/matrix-doc/blob/qr_key_verification/proposals/1543-qr_code_key_verification.md#qr-code-format
  */
-sealed class QrCodeData(
+internal sealed class QrCodeData(
         /**
          * the event ID or transaction_id of the associated verification
          */
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecret.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecret.kt
index 858c0ab6af942b54703253dfd8923ea81a967c26..52b09be49c39b031ecd1ecb556c69e995d45c9a7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecret.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/SharedSecret.kt
@@ -16,10 +16,10 @@
 
 package org.matrix.android.sdk.internal.crypto.verification.qrcode
 
-import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding
+import org.matrix.android.sdk.api.util.toBase64NoPadding
 import java.security.SecureRandom
 
-fun generateSharedSecretV2(): String {
+internal fun generateSharedSecretV2(): String {
     val secureRandom = SecureRandom()
 
     // 8 bytes long
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt
index ebc9bcce5ad7b173905eeecd6e0053cd1fcded46..315d77d932e3733a7859ec3531c5086ea0746484 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/AsyncTransaction.kt
@@ -35,7 +35,7 @@ internal fun <T> CoroutineScope.asyncTransaction(realmConfiguration: RealmConfig
     }
 }
 
-suspend fun <T> awaitTransaction(config: RealmConfiguration, transaction: suspend (realm: Realm) -> T): T {
+internal suspend fun <T> awaitTransaction(config: RealmConfiguration, transaction: suspend (realm: Realm) -> T): T {
     return withContext(Realm.WRITE_EXECUTOR.asCoroutineDispatcher()) {
         Realm.getInstance(config).use { bgRealm ->
             bgRealm.beginTransaction()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
index 12e60da114541268264c3c84b74c8eafa26b24ce..a57397dad5f10595c3c4fa72d1e32ced108f73e2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
@@ -43,6 +43,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo022
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo023
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo024
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo025
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo026
 import org.matrix.android.sdk.internal.util.Normalizer
 import timber.log.Timber
 import javax.inject.Inject
@@ -57,7 +58,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
     override fun equals(other: Any?) = other is RealmSessionStoreMigration
     override fun hashCode() = 1000
 
-    val schemaVersion = 25L
+    val schemaVersion = 26L
 
     override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
         Timber.d("Migrating Realm Session from $oldVersion to $newVersion")
@@ -87,5 +88,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
         if (oldVersion < 23) MigrateSessionTo023(realm).perform()
         if (oldVersion < 24) MigrateSessionTo024(realm).perform()
         if (oldVersion < 25) MigrateSessionTo025(realm).perform()
+        if (oldVersion < 26) MigrateSessionTo026(realm).perform()
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt
index 289db9fa15b4351adc99ae2df221d3fd619cbd60..4fdedabd705dc5223aef54e91c19c00d0a721a71 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt
@@ -82,17 +82,18 @@ internal fun ChunkEntity.addStateEvent(roomId: String, stateEvent: EventEntity,
 internal fun ChunkEntity.addTimelineEvent(roomId: String,
                                           eventEntity: EventEntity,
                                           direction: PaginationDirection,
-                                          roomMemberContentsByUser: Map<String, RoomMemberContent?>? = null) {
+                                          ownedByThreadChunk: Boolean = false,
+                                          roomMemberContentsByUser: Map<String, RoomMemberContent?>? = null): TimelineEventEntity? {
     val eventId = eventEntity.eventId
     if (timelineEvents.find(eventId) != null) {
-        return
+        return null
     }
     val displayIndex = nextDisplayIndex(direction)
     val localId = TimelineEventEntity.nextId(realm)
     val senderId = eventEntity.sender ?: ""
 
     // Update RR for the sender of a new message with a dummy one
-    val readReceiptsSummaryEntity = handleReadReceipts(realm, roomId, eventEntity, senderId)
+    val readReceiptsSummaryEntity = if (!ownedByThreadChunk) handleReadReceipts(realm, roomId, eventEntity, senderId) else null
     val timelineEventEntity = realm.createObject<TimelineEventEntity>().apply {
         this.localId = localId
         this.root = eventEntity
@@ -102,6 +103,7 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String,
                 ?.also { it.cleanUp(eventEntity.sender) }
         this.readReceipts = readReceiptsSummaryEntity
         this.displayIndex = displayIndex
+        this.ownedByThreadChunk = ownedByThreadChunk
         val roomMemberContent = roomMemberContentsByUser?.get(senderId)
         this.senderAvatar = roomMemberContent?.avatarUrl
         this.senderName = roomMemberContent?.displayName
@@ -113,9 +115,10 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String,
     }
     // numberOfTimelineEvents++
     timelineEvents.add(timelineEventEntity)
+    return timelineEventEntity
 }
 
-private fun computeIsUnique(
+internal fun computeIsUnique(
         realm: Realm,
         roomId: String,
         isLastForward: Boolean,
@@ -223,6 +226,9 @@ internal fun ChunkEntity.isMoreRecentThan(chunkToCheck: ChunkEntity): Boolean {
     if (chunkToCheck.doesNextChunksVerifyCondition { it == this }) {
         return true
     }
+    if (this.doesNextChunksVerifyCondition { it == chunkToCheck }) {
+        return false
+    }
     // Otherwise check if this chunk is linked to last forward
     if (this.doesNextChunksVerifyCondition { it.isLastForward }) {
         return true
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/RoomEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/RoomEntityHelper.kt
index 724f307e3bda3648bf01b53cb11e5dd7c4f7bb80..9ad2708b4381ae456234042efb6c4c4b9b373b0f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/RoomEntityHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/RoomEntityHelper.kt
@@ -18,9 +18,16 @@ package org.matrix.android.sdk.internal.database.helper
 
 import org.matrix.android.sdk.internal.database.model.ChunkEntity
 import org.matrix.android.sdk.internal.database.model.RoomEntity
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
 
 internal fun RoomEntity.addIfNecessary(chunkEntity: ChunkEntity) {
     if (!chunks.contains(chunkEntity)) {
         chunks.add(chunkEntity)
     }
 }
+
+internal fun RoomEntity.addIfNecessary(threadSummary: ThreadSummaryEntity) {
+    if (!threadSummaries.contains(threadSummary)) {
+        threadSummaries.add(threadSummary)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
index f703bfaf82adcac49a4c96697ae90b099ac7267d..04cf5b78af79e8dc4d3994782ac11768d6a55580 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadEventsHelper.kt
@@ -16,9 +16,12 @@
 
 package org.matrix.android.sdk.internal.database.helper
 
+import com.squareup.moshi.JsonDataException
 import io.realm.Realm
 import io.realm.RealmQuery
 import io.realm.Sort
+import org.matrix.android.sdk.api.session.events.model.UnsignedData
+import org.matrix.android.sdk.api.session.events.model.isRedacted
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
 import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
 import org.matrix.android.sdk.internal.database.mapper.asDomain
@@ -33,8 +36,10 @@ import org.matrix.android.sdk.internal.database.query.findIncludingEvent
 import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
 import org.matrix.android.sdk.internal.database.query.where
 import org.matrix.android.sdk.internal.database.query.whereRoomId
+import org.matrix.android.sdk.internal.di.MoshiProvider
+import timber.log.Timber
 
-private typealias ThreadSummary = Pair<Int, TimelineEventEntity>?
+private typealias Summary = Pair<Int, TimelineEventEntity>?
 
 /**
  * Finds the root thread event and update it with the latest message summary along with the number
@@ -48,14 +53,14 @@ internal fun Map<String, EventEntity>.updateThreadSummaryIfNeeded(
     for ((rootThreadEventId, eventEntity) in this) {
         eventEntity.threadSummaryInThread(eventEntity.realm, rootThreadEventId, chunkEntity)?.let { threadSummary ->
 
-            val numberOfMessages = threadSummary.first
+            val inThreadMessages = threadSummary.first
             val latestEventInThread = threadSummary.second
 
             // If this is a thread message, find its root event if exists
             val rootThreadEvent = if (eventEntity.isThread()) eventEntity.findRootThreadEvent() else eventEntity
 
             rootThreadEvent?.markEventAsRoot(
-                    threadsCounted = numberOfMessages,
+                    inThreadMessages = inThreadMessages,
                     latestMessageTimelineEventEntity = latestEventInThread
             )
         }
@@ -81,27 +86,27 @@ internal fun EventEntity.findRootThreadEvent(): EventEntity? =
  * Mark or update the current event a root thread event
  */
 internal fun EventEntity.markEventAsRoot(
-        threadsCounted: Int,
+        inThreadMessages: Int,
         latestMessageTimelineEventEntity: TimelineEventEntity?) {
     isRootThread = true
-    numberOfThreads = threadsCounted
+    numberOfThreads = inThreadMessages
     threadSummaryLatestMessage = latestMessageTimelineEventEntity
 }
 
 /**
  * Count the number of threads for the provided root thread eventId, and finds the latest event message
+ * note: Redactions are handled by RedactionEventProcessor
  * @param rootThreadEventId The root eventId that will find the number of threads
  * @return A ThreadSummary containing the counted threads and the latest event message
  */
-internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId: String, chunkEntity: ChunkEntity?): ThreadSummary {
-    // Number of messages
-    val messages = TimelineEventEntity
-            .whereRoomId(realm, roomId = roomId)
-            .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId)
-            .count()
-            .toInt()
+internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId: String, chunkEntity: ChunkEntity?): Summary {
+    val inThreadMessages = countInThreadMessages(
+            realm = realm,
+            roomId = roomId,
+            rootThreadEventId = rootThreadEventId
+    )
 
-    if (messages <= 0) return null
+    if (inThreadMessages <= 0) return null
 
     // Find latest thread event, we know it exists
     var chunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId) ?: chunkEntity ?: return null
@@ -123,9 +128,38 @@ internal fun EventEntity.threadSummaryInThread(realm: Realm, rootThreadEventId:
 
     result ?: return null
 
-    return ThreadSummary(messages, result)
+    return Summary(inThreadMessages, result)
 }
 
+/**
+ * Counts the number of thread replies in the main timeline thread summary,
+ * with respect to redactions.
+ */
+internal fun countInThreadMessages(realm: Realm, roomId: String, rootThreadEventId: String): Int =
+        TimelineEventEntity
+                .whereRoomId(realm, roomId = roomId)
+                .equalTo(TimelineEventEntityFields.ROOT.ROOT_THREAD_EVENT_ID, rootThreadEventId)
+                .distinct(TimelineEventEntityFields.ROOT.EVENT_ID)
+                .findAll()
+                .filterNot { timelineEvent ->
+                    timelineEvent.root
+                            ?.unsignedData
+                            ?.takeIf { it.isNotBlank() }
+                            ?.toUnsignedData()
+                            .isRedacted()
+                }.size
+
+/**
+ * Mapping string to UnsignedData using Moshi
+ */
+private fun String.toUnsignedData(): UnsignedData? =
+        try {
+            MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).fromJson(this)
+        } catch (ex: JsonDataException) {
+            Timber.e(ex, "Failed to parse UnsignedData")
+            null
+        }
+
 /**
  * Lets compare them in case user is moving forward in the timeline and we cannot know the
  * exact chunk sequence while currentChunk is not yet committed in the DB
@@ -156,6 +190,7 @@ internal fun TimelineEventEntity.Companion.findAllThreadsForRoomId(realm: Realm,
         TimelineEventEntity
                 .whereRoomId(realm, roomId = roomId)
                 .equalTo(TimelineEventEntityFields.ROOT.IS_ROOT_THREAD, true)
+                .equalTo(TimelineEventEntityFields.OWNED_BY_THREAD_CHUNK, false)
                 .sort("${TimelineEventEntityFields.ROOT.THREAD_SUMMARY_LATEST_MESSAGE}.${TimelineEventEntityFields.ROOT.ORIGIN_SERVER_TS}", Sort.DESCENDING)
 
 /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt
new file mode 100644
index 0000000000000000000000000000000000000000..24de26eeea9e701268181671fb0e35f745536686
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt
@@ -0,0 +1,328 @@
+/*
+ * Copyright 2022 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.database.helper
+
+import io.realm.Realm
+import io.realm.RealmQuery
+import io.realm.Sort
+import io.realm.kotlin.createObject
+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.model.OlmDecryptionResult
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
+import org.matrix.android.sdk.api.session.room.send.SendState
+import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
+import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummaryUpdateType
+import org.matrix.android.sdk.internal.database.mapper.asDomain
+import org.matrix.android.sdk.internal.database.mapper.toEntity
+import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
+import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
+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.model.RoomEntity
+import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields
+import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
+import org.matrix.android.sdk.internal.database.query.getOrCreate
+import org.matrix.android.sdk.internal.database.query.getOrNull
+import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent
+import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor
+import timber.log.Timber
+import java.util.UUID
+
+internal fun ThreadSummaryEntity.updateThreadSummary(
+        rootThreadEventEntity: EventEntity,
+        numberOfThreads: Int?,
+        latestThreadEventEntity: EventEntity?,
+        isUserParticipating: Boolean,
+        roomMemberContentsByUser: HashMap<String, RoomMemberContent?>) {
+    updateThreadSummaryRootEvent(rootThreadEventEntity, roomMemberContentsByUser)
+    updateThreadSummaryLatestEvent(latestThreadEventEntity, roomMemberContentsByUser)
+    this.isUserParticipating = isUserParticipating
+    numberOfThreads?.let {
+        // Update number of threads only when there is an actual value
+        this.numberOfThreads = it
+    }
+}
+
+/**
+ * Updates the root thread event properties
+ */
+internal fun ThreadSummaryEntity.updateThreadSummaryRootEvent(
+        rootThreadEventEntity: EventEntity,
+        roomMemberContentsByUser: HashMap<String, RoomMemberContent?>
+) {
+    val roomId = rootThreadEventEntity.roomId
+    val rootThreadRoomMemberContent = roomMemberContentsByUser[rootThreadEventEntity.sender ?: ""]
+    this.rootThreadEventEntity = rootThreadEventEntity
+    this.rootThreadSenderAvatar = rootThreadRoomMemberContent?.avatarUrl
+    this.rootThreadSenderName = rootThreadRoomMemberContent?.displayName
+    this.rootThreadIsUniqueDisplayName = if (rootThreadRoomMemberContent?.displayName != null) {
+        computeIsUnique(realm, roomId, false, rootThreadRoomMemberContent, roomMemberContentsByUser)
+    } else {
+        true
+    }
+}
+
+/**
+ * Updates the latest thread event properties
+ */
+internal fun ThreadSummaryEntity.updateThreadSummaryLatestEvent(
+        latestThreadEventEntity: EventEntity?,
+        roomMemberContentsByUser: HashMap<String, RoomMemberContent?>
+) {
+    val roomId = latestThreadEventEntity?.roomId ?: return
+    val latestThreadRoomMemberContent = roomMemberContentsByUser[latestThreadEventEntity.sender ?: ""]
+    this.latestThreadEventEntity = latestThreadEventEntity
+    this.latestThreadSenderAvatar = latestThreadRoomMemberContent?.avatarUrl
+    this.latestThreadSenderName = latestThreadRoomMemberContent?.displayName
+    this.latestThreadIsUniqueDisplayName = if (latestThreadRoomMemberContent?.displayName != null) {
+        computeIsUnique(realm, roomId, false, latestThreadRoomMemberContent, roomMemberContentsByUser)
+    } else {
+        true
+    }
+}
+
+private fun EventEntity.toTimelineEventEntity(roomMemberContentsByUser: HashMap<String, RoomMemberContent?>): TimelineEventEntity {
+    val roomId = roomId
+    val eventId = eventId
+    val localId = TimelineEventEntity.nextId(realm)
+    val senderId = sender ?: ""
+
+    val timelineEventEntity = realm.createObject<TimelineEventEntity>().apply {
+        this.localId = localId
+        this.root = this@toTimelineEventEntity
+        this.eventId = eventId
+        this.roomId = roomId
+        this.annotations = EventAnnotationsSummaryEntity.where(realm, roomId, eventId).findFirst()
+                ?.also { it.cleanUp(sender) }
+        this.ownedByThreadChunk = true  // To skip it from the original event flow
+        val roomMemberContent = roomMemberContentsByUser[senderId]
+        this.senderAvatar = roomMemberContent?.avatarUrl
+        this.senderName = roomMemberContent?.displayName
+        isUniqueDisplayName = if (roomMemberContent?.displayName != null) {
+            computeIsUnique(realm, roomId, false, roomMemberContent, roomMemberContentsByUser)
+        } else {
+            true
+        }
+    }
+    return timelineEventEntity
+}
+
+internal suspend fun ThreadSummaryEntity.Companion.createOrUpdate(
+        threadSummaryType: ThreadSummaryUpdateType,
+        realm: Realm,
+        roomId: String,
+        threadEventEntity: EventEntity? = null,
+        rootThreadEvent: Event? = null,
+        roomMemberContentsByUser: HashMap<String, RoomMemberContent?>,
+        roomEntity: RoomEntity,
+        userId: String,
+        cryptoService: CryptoService? = null
+) {
+    when (threadSummaryType) {
+        ThreadSummaryUpdateType.REPLACE -> {
+            rootThreadEvent?.eventId ?: return
+            rootThreadEvent.senderId ?: return
+
+            val numberOfThreads = rootThreadEvent.unsignedData?.relations?.latestThread?.count ?: return
+
+            // Something is wrong with the server return
+            if (numberOfThreads <= 0) return
+
+            val threadSummary = ThreadSummaryEntity.getOrCreate(realm, roomId, rootThreadEvent.eventId).also {
+                Timber.i("###THREADS ThreadSummaryHelper REPLACE eventId:${it.rootThreadEventId} ")
+            }
+
+            val rootThreadEventEntity = createEventEntity(roomId, rootThreadEvent, realm).also {
+                decryptIfNeeded(cryptoService, it, roomId)
+            }
+            val latestThreadEventEntity = createLatestEventEntity(roomId, rootThreadEvent, roomMemberContentsByUser, realm)?.also {
+                decryptIfNeeded(cryptoService, it, roomId)
+            }
+            val isUserParticipating = rootThreadEvent.unsignedData.relations.latestThread.isUserParticipating == true || rootThreadEvent.senderId == userId
+            roomMemberContentsByUser.addSenderState(realm, roomId, rootThreadEvent.senderId)
+            threadSummary.updateThreadSummary(
+                    rootThreadEventEntity = rootThreadEventEntity,
+                    numberOfThreads = numberOfThreads,
+                    latestThreadEventEntity = latestThreadEventEntity,
+                    isUserParticipating = isUserParticipating,
+                    roomMemberContentsByUser = roomMemberContentsByUser
+            )
+
+            roomEntity.addIfNecessary(threadSummary)
+        }
+        ThreadSummaryUpdateType.ADD     -> {
+            val rootThreadEventId = threadEventEntity?.rootThreadEventId ?: return
+            Timber.i("###THREADS ThreadSummaryHelper ADD for root eventId:$rootThreadEventId")
+
+            val threadSummary = ThreadSummaryEntity.getOrNull(realm, roomId, rootThreadEventId)
+            if (threadSummary != null) {
+                // ThreadSummary exists so lets add the latest event
+                Timber.i("###THREADS ThreadSummaryHelper ADD root eventId:$rootThreadEventId exists, lets update latest thread event.")
+                threadSummary.updateThreadSummaryLatestEvent(threadEventEntity, roomMemberContentsByUser)
+                threadSummary.numberOfThreads++
+                if (threadEventEntity.sender == userId) {
+                    threadSummary.isUserParticipating = true
+                }
+            } else {
+                // ThreadSummary do not exists lets try to create one
+                Timber.i("###THREADS ThreadSummaryHelper ADD root eventId:$rootThreadEventId do not exists, lets try to create one")
+                threadEventEntity.findRootThreadEvent()?.let { rootThreadEventEntity ->
+                    // Root thread event entity exists so lets create a new record
+                    ThreadSummaryEntity.getOrCreate(realm, roomId, rootThreadEventEntity.eventId).let {
+                        it.updateThreadSummary(
+                                rootThreadEventEntity = rootThreadEventEntity,
+                                numberOfThreads = 1,
+                                latestThreadEventEntity = threadEventEntity,
+                                isUserParticipating = threadEventEntity.sender == userId,
+                                roomMemberContentsByUser = roomMemberContentsByUser
+                        )
+                        roomEntity.addIfNecessary(it)
+                    }
+                }
+            }
+        }
+    }
+}
+
+private suspend fun decryptIfNeeded(cryptoService: CryptoService?, eventEntity: EventEntity, roomId: String) {
+    cryptoService ?: return
+    val event = eventEntity.asDomain()
+    if (event.isEncrypted() && event.mxDecryptionResult == null && event.eventId != null) {
+        try {
+            Timber.i("###THREADS ThreadSummaryHelper request decryption for eventId:${event.eventId}")
+            // 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
+            )
+            // Save decryption result, to not decrypt every time we enter the thread list
+            eventEntity.setDecryptionResult(result)
+        } catch (e: MXCryptoError) {
+            if (e is MXCryptoError.Base) {
+                event.mCryptoError = e.errorType
+                event.mCryptoErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
+            }
+        }
+    }
+}
+
+/**
+ * Request decryption
+ */
+private fun requestDecryption(eventDecryptor: TimelineEventDecryptor?, event: Event?) {
+    eventDecryptor ?: return
+    event ?: return
+    if (event.isEncrypted() &&
+            event.mxDecryptionResult == null && event.eventId != null) {
+        Timber.i("###THREADS ThreadSummaryHelper request decryption for eventId:${event.eventId}")
+
+        eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(event, UUID.randomUUID().toString()))
+    }
+}
+
+/**
+ * If we don't have any new state on this user, get it from db
+ */
+private fun HashMap<String, RoomMemberContent?>.addSenderState(realm: Realm, roomId: String, senderId: String) {
+    getOrPut(senderId) {
+        CurrentStateEventEntity
+                .getOrNull(realm, roomId, senderId, EventType.STATE_ROOM_MEMBER)
+                ?.root?.asDomain()
+                ?.getFixedRoomMemberContent()
+    }
+}
+
+/**
+ * Create an EventEntity for the root thread event or get an existing one
+ */
+private fun createEventEntity(roomId: String, event: Event, realm: Realm): EventEntity {
+    val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
+    return event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
+}
+
+/**
+ * Create an EventEntity for the latest thread event or get an existing one. Also update the user room member
+ * state
+ */
+private fun createLatestEventEntity(
+        roomId: String,
+        rootThreadEvent: Event,
+        roomMemberContentsByUser: HashMap<String, RoomMemberContent?>,
+        realm: Realm): EventEntity? {
+    return getLatestEvent(rootThreadEvent)?.let {
+        it.senderId?.let { senderId ->
+            roomMemberContentsByUser.addSenderState(realm, roomId, senderId)
+        }
+        createEventEntity(roomId, it, realm)
+    }
+}
+
+/**
+ * Returned the latest event message, if any
+ */
+private fun getLatestEvent(rootThreadEvent: Event): Event? {
+    return rootThreadEvent.unsignedData?.relations?.latestThread?.event
+}
+
+/**
+ * Find all ThreadSummaryEntity for the specified roomId, sorted by origin server
+ * note: Sorting cannot be provided by server, so we have to use that unstable property
+ * @param roomId The id of the room
+ */
+internal fun ThreadSummaryEntity.Companion.findAllThreadsForRoomId(realm: Realm, roomId: String): RealmQuery<ThreadSummaryEntity> =
+        ThreadSummaryEntity
+                .where(realm, roomId = roomId)
+                .sort(ThreadSummaryEntityFields.LATEST_THREAD_EVENT_ENTITY.ORIGIN_SERVER_TS, Sort.DESCENDING)
+
+/**
+ * Enhance each [ThreadSummary] root and latest event with the equivalent decrypted text edition/replacement
+ */
+internal fun List<ThreadSummary>.enhanceWithEditions(realm: Realm, roomId: String): List<ThreadSummary> =
+        this.map {
+            it.addEditionIfNeeded(realm, roomId, true)
+            it.addEditionIfNeeded(realm, roomId, false)
+            it
+        }
+
+private fun ThreadSummary.addEditionIfNeeded(realm: Realm, roomId: String, enhanceRoot: Boolean) {
+    val eventId = if (enhanceRoot) rootEventId else latestEvent?.eventId ?: return
+    EventAnnotationsSummaryEntity
+            .where(realm, roomId, eventId)
+            .findFirst()
+            ?.editSummary
+            ?.editions
+            ?.lastOrNull()
+            ?.eventId
+            ?.let { editedEventId ->
+                TimelineEventEntity.where(realm, roomId, eventId = editedEventId).findFirst()?.let { editedEvent ->
+                    if (enhanceRoot) {
+                        threadEditions.rootThreadEdition = editedEvent.root?.asDomain()?.getDecryptedTextSummary() ?: "(edited)"
+                    } else {
+                        threadEditions.latestThreadEdition = editedEvent.root?.asDomain()?.getDecryptedTextSummary() ?: "(edited)"
+                    }
+                }
+            }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/lightweight/LightweightSettingsStorage.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/lightweight/LightweightSettingsStorage.kt
deleted file mode 100644
index 700b94a98582b1600e25b9b18d7678099c50c7a2..0000000000000000000000000000000000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/lightweight/LightweightSettingsStorage.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright 2022 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.database.lightweight
-
-import android.content.Context
-import androidx.core.content.edit
-import androidx.preference.PreferenceManager
-import javax.inject.Inject
-
-/**
- * The purpose of this class is to provide an alternative and lightweight way to store settings/data
- * on the sdi without using the database. This should be used just for sdk/user preferences and
- * not for large data sets
- */
-
-class LightweightSettingsStorage  @Inject constructor(context: Context) {
-
-    private val sdkDefaultPrefs = PreferenceManager.getDefaultSharedPreferences(context.applicationContext)
-
-    fun setThreadMessagesEnabled(enabled: Boolean) {
-        sdkDefaultPrefs.edit {
-            putBoolean(MATRIX_SDK_SETTINGS_THREAD_MESSAGES_ENABLED, enabled)
-        }
-    }
-
-    fun areThreadMessagesEnabled(): Boolean {
-        return sdkDefaultPrefs.getBoolean(MATRIX_SDK_SETTINGS_THREAD_MESSAGES_ENABLED, false)
-    }
-
-    companion object {
-        const val MATRIX_SDK_SETTINGS_THREAD_MESSAGES_ENABLED = "MATRIX_SDK_SETTINGS_THREAD_MESSAGES_ENABLED"
-    }
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt
index 9c420e81fd70759fdaf70876d43e79fe755daddd..3083df062e622ce2599a1ef9f3b1ae8c08a7dc46 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.database.mapper
 
 import com.squareup.moshi.JsonDataException
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
+import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.UnsignedData
@@ -26,7 +27,6 @@ import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.api.session.room.sender.SenderInfo
 import org.matrix.android.sdk.api.session.threads.ThreadDetails
 import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
-import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
 import org.matrix.android.sdk.internal.database.model.EventEntity
 import org.matrix.android.sdk.internal.di.MoshiProvider
 import timber.log.Timber
@@ -114,7 +114,7 @@ internal object EventMapper {
                         )
                     },
                     threadNotificationState = eventEntity.threadNotificationState,
-                    threadSummaryLatestTextMessage = eventEntity.threadSummaryLatestMessage?.root?.asDomain()?.getDecryptedTextSummary(),
+                    threadSummaryLatestEvent = eventEntity.threadSummaryLatestMessage?.root?.asDomain(),
                     lastMessageTimestamp = eventEntity.threadSummaryLatestMessage?.root?.originServerTs
 
             )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt
index 7869506015e282845a4dab84bd17ed19f6a1fd2c..2e33988a22b853f941add399e3da3464928e2417 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt
@@ -41,7 +41,8 @@ internal object HomeServerCapabilitiesMapper {
                 maxUploadFileSize = entity.maxUploadFileSize,
                 lastVersionIdentityServerSupported = entity.lastVersionIdentityServerSupported,
                 defaultIdentityServerUrl = entity.defaultIdentityServerUrl,
-                roomVersions = mapRoomVersion(entity.roomVersionsJson)
+                roomVersions = mapRoomVersion(entity.roomVersionsJson),
+                canUseThreading = entity.canUseThreading
         )
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt
index 63b326096a8c0deb47b3b00c54717829b1a4ace9..41faf30a82e726c56290521151b7e8ce2a7ce811 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt
@@ -16,7 +16,8 @@
 
 package org.matrix.android.sdk.internal.database.mapper
 
-import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
+import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
+import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
 import org.matrix.android.sdk.api.session.room.model.RoomEncryptionAlgorithm
 import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
 import org.matrix.android.sdk.api.session.room.model.RoomSummary
@@ -24,7 +25,6 @@ import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
 import org.matrix.android.sdk.api.session.room.model.SpaceParentInfo
 import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
 import org.matrix.android.sdk.api.session.typing.TypingUsersTracker
-import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
 import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
 import org.matrix.android.sdk.internal.database.model.presence.toUserPresence
 import javax.inject.Inject
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ThreadSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ThreadSummaryMapper.kt
new file mode 100644
index 0000000000000000000000000000000000000000..cedb9e3d452a409dad32fffe8e14d9a9209c35f2
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/ThreadSummaryMapper.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2022 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.database.mapper
+
+import org.matrix.android.sdk.api.session.room.sender.SenderInfo
+import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
+import javax.inject.Inject
+
+internal class ThreadSummaryMapper @Inject constructor() {
+
+    fun map(threadSummary: ThreadSummaryEntity): ThreadSummary {
+        return ThreadSummary(
+                roomId = threadSummary.room?.firstOrNull()?.roomId.orEmpty(),
+                rootEvent = threadSummary.rootThreadEventEntity?.asDomain(),
+                latestEvent = threadSummary.latestThreadEventEntity?.asDomain(),
+                rootEventId = threadSummary.rootThreadEventId.orEmpty(),
+                rootThreadSenderInfo = SenderInfo(
+                        userId = threadSummary.rootThreadEventEntity?.sender ?: "",
+                        displayName = threadSummary.rootThreadSenderName,
+                        isUniqueDisplayName = threadSummary.rootThreadIsUniqueDisplayName,
+                        avatarUrl = threadSummary.rootThreadSenderAvatar
+                ),
+                latestThreadSenderInfo = SenderInfo(
+                        userId = threadSummary.latestThreadEventEntity?.sender ?: "",
+                        displayName = threadSummary.latestThreadSenderName,
+                        isUniqueDisplayName = threadSummary.latestThreadIsUniqueDisplayName,
+                        avatarUrl = threadSummary.latestThreadSenderAvatar
+                ),
+                isUserParticipating = threadSummary.isUserParticipating,
+                numberOfThreads = threadSummary.numberOfThreads
+        )
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/TimelineEventMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/TimelineEventMapper.kt
index 55c7f2a8ee605a7283e54bf0d66622275ae3e4c4..e6586224444cd7de1c5aa938d13e8d7059910bc1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/TimelineEventMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/TimelineEventMapper.kt
@@ -46,6 +46,7 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS
                         isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
                         avatarUrl = timelineEventEntity.senderAvatar
                 ),
+                ownedByThreadChunk = timelineEventEntity.ownedByThreadChunk,
                 readReceipts = readReceipts
                         ?.distinctBy {
                             it.roomMember
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo001.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo001.kt
index 831c6280ad5607a964342c93321d5b14e57a9901..7bed23066c5c447107dbbf6c9a0ecae064375972 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo001.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo001.kt
@@ -20,7 +20,7 @@ import io.realm.DynamicRealm
 import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
-class MigrateSessionTo001(realm: DynamicRealm) : RealmMigrator(realm, 1) {
+internal class MigrateSessionTo001(realm: DynamicRealm) : RealmMigrator(realm, 1) {
 
     override fun doMigrate(realm: DynamicRealm) {
         // Add hasFailedSending in RoomSummary and a small warning icon on room list
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo002.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo002.kt
index 215e558e2af40432c6e8b3ad80d93c18ae5fee85..9fa36248f8620d453bd7d9afd46b3e82b1a3bc51 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo002.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo002.kt
@@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.database.migration
 import io.realm.DynamicRealm
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
-class MigrateSessionTo002(realm: DynamicRealm) : RealmMigrator(realm, 2) {
+internal class MigrateSessionTo002(realm: DynamicRealm) : RealmMigrator(realm, 2) {
 
     override fun doMigrate(realm: DynamicRealm) {
         realm.schema.get("HomeServerCapabilitiesEntity")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo003.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo003.kt
index bc0b79d7e640814c4e7f8ca0ababc5f4196da4e1..b4aca53ece1aa4f11d21334b6e51b50e12e5ab34 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo003.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo003.kt
@@ -20,7 +20,7 @@ import io.realm.DynamicRealm
 import org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabilities
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
-class MigrateSessionTo003(realm: DynamicRealm) : RealmMigrator(realm, 3) {
+internal class MigrateSessionTo003(realm: DynamicRealm) : RealmMigrator(realm, 3) {
 
     override fun doMigrate(realm: DynamicRealm) {
         realm.schema.get("HomeServerCapabilitiesEntity")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo004.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo004.kt
index be13ae2c2f6386dbc062977af87b3cd7d7ae803c..0d91aab7d80d24b5e8a7f49e0252e97cd3101b55 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo004.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo004.kt
@@ -20,7 +20,7 @@ import io.realm.DynamicRealm
 import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
-class MigrateSessionTo004(realm: DynamicRealm) : RealmMigrator(realm, 4) {
+internal class MigrateSessionTo004(realm: DynamicRealm) : RealmMigrator(realm, 4) {
 
     override fun doMigrate(realm: DynamicRealm) {
         realm.schema.create("PendingThreePidEntity")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo005.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo005.kt
index b4826b23a450dced5acd28612e5d92f5b0ec3de2..67e91d85cce8991be2cacd89055a5f4e76a99234 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo005.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo005.kt
@@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.database.migration
 import io.realm.DynamicRealm
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
-class MigrateSessionTo005(realm: DynamicRealm) : RealmMigrator(realm, 5) {
+internal class MigrateSessionTo005(realm: DynamicRealm) : RealmMigrator(realm, 5) {
 
     override fun doMigrate(realm: DynamicRealm) {
         realm.schema.get("HomeServerCapabilitiesEntity")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo006.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo006.kt
index 3d7f26cceeee627ea2bcee56b073acabb6a63dfb..8eccc229e7c81bb0d6ce3f675d1d7294691cae77 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo006.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo006.kt
@@ -20,7 +20,7 @@ import io.realm.DynamicRealm
 import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntityFields
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
-class MigrateSessionTo006(realm: DynamicRealm) : RealmMigrator(realm, 6) {
+internal class MigrateSessionTo006(realm: DynamicRealm) : RealmMigrator(realm, 6) {
 
     override fun doMigrate(realm: DynamicRealm) {
         realm.schema.create("PreviewUrlCacheEntity")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo007.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo007.kt
index be8c8ce9c6d9ccc1331c09132513ab2352200d6e..5d1ff80367913cd4badd451ec4c09025dd2032cf 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo007.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo007.kt
@@ -21,7 +21,7 @@ import org.matrix.android.sdk.internal.database.model.RoomEntityFields
 import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
-class MigrateSessionTo007(realm: DynamicRealm) : RealmMigrator(realm, 7) {
+internal class MigrateSessionTo007(realm: DynamicRealm) : RealmMigrator(realm, 7) {
 
     override fun doMigrate(realm: DynamicRealm) {
         realm.schema.get("RoomEntity")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo008.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo008.kt
index d46730ef701165515e4066c624997301ff0ce8dc..b61bf7e6fa8c57e64ae059992973007e104dc086 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo008.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo008.kt
@@ -21,7 +21,7 @@ import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntit
 import org.matrix.android.sdk.internal.database.model.EditionOfEventFields
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
-class MigrateSessionTo008(realm: DynamicRealm) : RealmMigrator(realm, 8) {
+internal class MigrateSessionTo008(realm: DynamicRealm) : RealmMigrator(realm, 8) {
 
     override fun doMigrate(realm: DynamicRealm) {
         val editionOfEventSchema = realm.schema.create("EditionOfEvent")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo009.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo009.kt
index 370430b9e3274daa5548d423a26dfab0bcc8177f..149d322f66cf2660de21456469297a5b72492ef4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo009.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo009.kt
@@ -25,7 +25,7 @@ import org.matrix.android.sdk.internal.database.model.RoomTagEntityFields
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
-class MigrateSessionTo009(realm: DynamicRealm) : RealmMigrator(realm, 9) {
+internal class MigrateSessionTo009(realm: DynamicRealm) : RealmMigrator(realm, 9) {
 
     override fun doMigrate(realm: DynamicRealm) {
         realm.schema.get("RoomSummaryEntity")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo010.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo010.kt
index b968862d1096090a719f0f56705f3871bb654b04..aae80423ac5118ab2dde48a47fbfa8b10a0dadf5 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo010.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo010.kt
@@ -27,7 +27,7 @@ import org.matrix.android.sdk.internal.database.model.SpaceParentSummaryEntityFi
 import org.matrix.android.sdk.internal.di.MoshiProvider
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
-class MigrateSessionTo010(realm: DynamicRealm) : RealmMigrator(realm, 10) {
+internal class MigrateSessionTo010(realm: DynamicRealm) : RealmMigrator(realm, 10) {
 
     override fun doMigrate(realm: DynamicRealm) {
         realm.schema.create("SpaceChildSummaryEntity")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo011.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo011.kt
index 92ee26df42854a26c7ad46733381189a1eb43b39..5ba201dd46390ef319d06be5ed578b1765c15c4b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo011.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo011.kt
@@ -20,7 +20,7 @@ import io.realm.DynamicRealm
 import org.matrix.android.sdk.internal.database.model.EventEntityFields
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
-class MigrateSessionTo011(realm: DynamicRealm) : RealmMigrator(realm, 11) {
+internal class MigrateSessionTo011(realm: DynamicRealm) : RealmMigrator(realm, 11) {
 
     override fun doMigrate(realm: DynamicRealm) {
         realm.schema.get("EventEntity")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo012.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo012.kt
index a914cadd80634ab2cfa4e12b5d0c812678beb37e..f72cd3064fb59b70522e44e9eb40cea85eb2099b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo012.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo012.kt
@@ -26,7 +26,7 @@ import org.matrix.android.sdk.internal.database.model.SpaceChildSummaryEntityFie
 import org.matrix.android.sdk.internal.di.MoshiProvider
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
-class MigrateSessionTo012(realm: DynamicRealm) : RealmMigrator(realm, 12) {
+internal class MigrateSessionTo012(realm: DynamicRealm) : RealmMigrator(realm, 12) {
 
     override fun doMigrate(realm: DynamicRealm) {
         val joinRulesContentAdapter = MoshiProvider.providesMoshi().adapter(RoomJoinRulesContent::class.java)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo013.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo013.kt
index 2ea030380227d7cc9800be0483954ed0de50bef2..2823a69ff147be476462d167f6799b3242738684 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo013.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo013.kt
@@ -20,7 +20,7 @@ import io.realm.DynamicRealm
 import org.matrix.android.sdk.internal.database.model.SpaceChildSummaryEntityFields
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
-class MigrateSessionTo013(realm: DynamicRealm) : RealmMigrator(realm, 13) {
+internal class MigrateSessionTo013(realm: DynamicRealm) : RealmMigrator(realm, 13) {
 
     override fun doMigrate(realm: DynamicRealm) {
         // Fix issue with the nightly build. Eventually play again the migration which has been included in migrateTo12()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo014.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo014.kt
index c524b6f2849d5e8ccd6c24d0051f63a03169c22a..4a27c8bb12cefb67c63a82c58b85b530d38688a1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo014.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo014.kt
@@ -24,7 +24,7 @@ import org.matrix.android.sdk.internal.database.model.RoomEntityFields
 import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
-class MigrateSessionTo014(realm: DynamicRealm) : RealmMigrator(realm, 14) {
+internal class MigrateSessionTo014(realm: DynamicRealm) : RealmMigrator(realm, 14) {
 
     override fun doMigrate(realm: DynamicRealm) {
         val roomAccountDataSchema = realm.schema.create("RoomAccountDataEntity")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo015.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo015.kt
index 329964a9a406c597ceb3f8c6a4fc4312d288d2ca..f45f9b39b1869fffcbd25d76952fe675b72e8eb4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo015.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo015.kt
@@ -22,7 +22,7 @@ import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
 import org.matrix.android.sdk.internal.query.process
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
-class MigrateSessionTo015(realm: DynamicRealm) : RealmMigrator(realm, 15) {
+internal class MigrateSessionTo015(realm: DynamicRealm) : RealmMigrator(realm, 15) {
 
     override fun doMigrate(realm: DynamicRealm) {
         // fix issue with flattenParentIds on DM that kept growing with duplicate
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo016.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo016.kt
index b2fa54a05c30c6fce2ba02bef44420e2d612898f..69f6c9f1726b5aa3098586290b72b961b0f0342f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo016.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo016.kt
@@ -21,7 +21,7 @@ import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEnti
 import org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabilities
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
-class MigrateSessionTo016(realm: DynamicRealm) : RealmMigrator(realm, 16) {
+internal class MigrateSessionTo016(realm: DynamicRealm) : RealmMigrator(realm, 16) {
 
     override fun doMigrate(realm: DynamicRealm) {
         realm.schema.get("HomeServerCapabilitiesEntity")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo017.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo017.kt
index 95d67b9ad842b23d5b6e4108706aa16900398365..4d8db92b6988bb8f3102b72379356a44fd6bea75 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo017.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo017.kt
@@ -20,7 +20,7 @@ import io.realm.DynamicRealm
 import org.matrix.android.sdk.internal.database.model.EventInsertEntityFields
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
-class MigrateSessionTo017(realm: DynamicRealm) : RealmMigrator(realm, 17) {
+internal class MigrateSessionTo017(realm: DynamicRealm) : RealmMigrator(realm, 17) {
 
     override fun doMigrate(realm: DynamicRealm) {
         realm.schema.get("EventInsertEntity")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo018.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo018.kt
index b415c51d4b53e9fb7ca78b54b657d5160ffc2a7b..559b8979e1040bb7738b1767e8d0c7ce5c1b37b7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo018.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo018.kt
@@ -22,7 +22,7 @@ import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
 import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntityFields
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
-class MigrateSessionTo018(realm: DynamicRealm) : RealmMigrator(realm, 18) {
+internal class MigrateSessionTo018(realm: DynamicRealm) : RealmMigrator(realm, 18) {
 
     override fun doMigrate(realm: DynamicRealm) {
         realm.schema.create("UserPresenceEntity")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo019.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo019.kt
index d0b368be46e95c0ff082ce54036145afb965fcb1..d63ef62889b71db469b79e3c2eb1b460b40afe31 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo019.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo019.kt
@@ -21,7 +21,7 @@ import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
 import org.matrix.android.sdk.internal.util.Normalizer
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
-class MigrateSessionTo019(realm: DynamicRealm,
+internal class MigrateSessionTo019(realm: DynamicRealm,
                           private val normalizer: Normalizer) : RealmMigrator(realm, 19) {
 
     override fun doMigrate(realm: DynamicRealm) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo020.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo020.kt
index c7f6e3ceed4c7046283eaf10769252ca54f6e3cd..e0075894edb4ea52150ef3c02e88b6318e77f033 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo020.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo020.kt
@@ -20,7 +20,7 @@ import io.realm.DynamicRealm
 import org.matrix.android.sdk.internal.database.model.ChunkEntityFields
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
-class MigrateSessionTo020(realm: DynamicRealm) : RealmMigrator(realm, 20) {
+internal class MigrateSessionTo020(realm: DynamicRealm) : RealmMigrator(realm, 20) {
 
     override fun doMigrate(realm: DynamicRealm) {
         realm.schema.get("ChunkEntity")?.apply {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo021.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo021.kt
index 6b6952e69784b895e0d4e8a3c83aa07d7e8837b0..2f880a29dc46b4283f992a70b3db2fb817155947 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo021.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo021.kt
@@ -18,14 +18,14 @@ package org.matrix.android.sdk.internal.database.migration
 
 import io.realm.DynamicRealm
 import org.matrix.android.sdk.api.session.events.model.EventType
-import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent
+import org.matrix.android.sdk.api.session.events.model.content.EncryptionEventContent
 import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields
 import org.matrix.android.sdk.internal.database.model.EventEntityFields
 import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
 import org.matrix.android.sdk.internal.di.MoshiProvider
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
-class MigrateSessionTo021(realm: DynamicRealm) : RealmMigrator(realm, 21) {
+internal class MigrateSessionTo021(realm: DynamicRealm) : RealmMigrator(realm, 21) {
 
     override fun doMigrate(realm: DynamicRealm) {
         realm.schema.get("RoomSummaryEntity")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo022.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo022.kt
index e78a9d05da2996d09e2bd7ffbd7193d2a4b0a494..f55700d36de9eee789db4481deb568de9f482540 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo022.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo022.kt
@@ -23,7 +23,7 @@ import org.matrix.android.sdk.internal.database.model.RoomEntityFields
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 import timber.log.Timber
 
-class MigrateSessionTo022(realm: DynamicRealm) : RealmMigrator(realm, 22) {
+internal class MigrateSessionTo022(realm: DynamicRealm) : RealmMigrator(realm, 22) {
 
     override fun doMigrate(realm: DynamicRealm) {
         val listJoinedRoomIds = realm.where("RoomEntity")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo023.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo023.kt
index 0bb8ceeaa5eef8cf0cfbfb461b112cad9571ba72..a3ce0b5414639ecc910452329032a24ca0956664 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo023.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo023.kt
@@ -22,7 +22,7 @@ import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
 import org.matrix.android.sdk.internal.database.model.EventEntityFields
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
-class MigrateSessionTo023(realm: DynamicRealm) : RealmMigrator(realm, 23) {
+internal class MigrateSessionTo023(realm: DynamicRealm) : RealmMigrator(realm, 23) {
 
     override fun doMigrate(realm: DynamicRealm) {
         val eventEntity = realm.schema.get("TimelineEventEntity") ?: return
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo024.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo024.kt
index ff8897256691098dbf8dcef71843c9d108102187..fc17bf9b2899e54247388ad03fca37cc855bc718 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo024.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo024.kt
@@ -20,7 +20,7 @@ import io.realm.DynamicRealm
 import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntityFields
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
-class MigrateSessionTo024(realm: DynamicRealm) : RealmMigrator(realm, 24) {
+internal class MigrateSessionTo024(realm: DynamicRealm) : RealmMigrator(realm, 24) {
 
     override fun doMigrate(realm: DynamicRealm) {
         realm.schema.get("PreviewUrlCacheEntity")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo025.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo025.kt
index 237b016ac254ed5bdcedff959dda77a45c45210e..a57fd52ec1f29ba8321d96c042413fc25975d523 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo025.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo025.kt
@@ -21,7 +21,7 @@ import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEnti
 import org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabilities
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
-class MigrateSessionTo025(realm: DynamicRealm) : RealmMigrator(realm, 25) {
+internal class MigrateSessionTo025(realm: DynamicRealm) : RealmMigrator(realm, 25) {
 
     override fun doMigrate(realm: DynamicRealm) {
         realm.schema.get("HomeServerCapabilitiesEntity")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt
new file mode 100644
index 0000000000000000000000000000000000000000..35a6135ba203aac3f2f1a7e75543821eb9ea49db
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo026.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2022 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.database.migration
+
+import io.realm.DynamicRealm
+import io.realm.FieldAttribute
+import org.matrix.android.sdk.internal.database.model.ChunkEntityFields
+import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
+import org.matrix.android.sdk.internal.database.model.RoomEntityFields
+import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields
+import org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabilities
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+/**
+ * Migrating to:
+ * Live thread list: using enhanced /messages api MSC3440
+ * Live thread timeline: using /relations api
+ */
+internal class MigrateSessionTo026(realm: DynamicRealm) : RealmMigrator(realm, 26) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.get("ChunkEntity")
+                ?.addField(ChunkEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED)
+                ?.addField(ChunkEntityFields.IS_LAST_FORWARD_THREAD, Boolean::class.java, FieldAttribute.INDEXED)
+
+        realm.schema.get("TimelineEventEntity")
+                ?.addField(TimelineEventEntityFields.OWNED_BY_THREAD_CHUNK, Boolean::class.java)
+
+        val eventEntity = realm.schema.get("EventEntity") ?: return
+        val threadSummaryEntity = realm.schema.create("ThreadSummaryEntity")
+                .addField(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ID, String::class.java, FieldAttribute.INDEXED)
+                .addField(ThreadSummaryEntityFields.ROOT_THREAD_SENDER_NAME, String::class.java)
+                .addField(ThreadSummaryEntityFields.ROOT_THREAD_SENDER_AVATAR, String::class.java)
+                .addField(ThreadSummaryEntityFields.ROOT_THREAD_IS_UNIQUE_DISPLAY_NAME, Boolean::class.java)
+                .addField(ThreadSummaryEntityFields.LATEST_THREAD_SENDER_NAME, String::class.java)
+                .addField(ThreadSummaryEntityFields.LATEST_THREAD_SENDER_AVATAR, String::class.java)
+                .addField(ThreadSummaryEntityFields.LATEST_THREAD_IS_UNIQUE_DISPLAY_NAME, Boolean::class.java)
+                .addField(ThreadSummaryEntityFields.NUMBER_OF_THREADS, Int::class.java)
+                .addField(ThreadSummaryEntityFields.IS_USER_PARTICIPATING, Boolean::class.java)
+                .addRealmObjectField(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ENTITY.`$`, eventEntity)
+                .addRealmObjectField(ThreadSummaryEntityFields.LATEST_THREAD_EVENT_ENTITY.`$`, eventEntity)
+
+        realm.schema.get("RoomEntity")
+                ?.addRealmListField(RoomEntityFields.THREAD_SUMMARIES.`$`, threadSummaryEntity)
+
+        realm.schema.get("HomeServerCapabilitiesEntity")
+                ?.addField(HomeServerCapabilitiesEntityFields.CAN_USE_THREADING, Boolean::class.java)
+                ?.forceRefreshOfHomeServerCapabilities()
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt
index c45c27ed08a0fe2f361ae17d3ccf64b1d4384ac0..88eb821aa9d9088a2241a30d0309cc30a3983dcf 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/ChunkEntity.kt
@@ -33,7 +33,10 @@ internal open class ChunkEntity(@Index var prevToken: String? = null,
                                 var timelineEvents: RealmList<TimelineEventEntity> = RealmList(),
         // Only one chunk will have isLastForward == true
                                 @Index var isLastForward: Boolean = false,
-                                @Index var isLastBackward: Boolean = false
+                                @Index var isLastBackward: Boolean = false,
+        // Threads
+                                @Index var rootThreadEventId: String? = null,
+                                @Index var isLastForwardThread: Boolean = false,
 ) : RealmObject() {
 
     fun identifier() = "${prevToken}_$nextToken"
@@ -47,14 +50,32 @@ internal open class ChunkEntity(@Index var prevToken: String? = null,
     companion object
 }
 
-internal fun ChunkEntity.deleteOnCascade(deleteStateEvents: Boolean, canDeleteRoot: Boolean) {
+internal fun ChunkEntity.deleteOnCascade(
+        deleteStateEvents: Boolean,
+        canDeleteRoot: Boolean) {
     assertIsManaged()
     if (deleteStateEvents) {
         stateEvents.deleteAllFromRealm()
     }
     timelineEvents.clearWith {
         val deleteRoot = canDeleteRoot && (it.root?.stateKey == null || deleteStateEvents)
+        if (deleteRoot) {
+            room?.firstOrNull()?.removeThreadSummaryIfNeeded(it.eventId)
+        }
         it.deleteOnCascade(deleteRoot)
     }
     deleteFromRealm()
 }
+
+/**
+ * Delete the chunk along with the thread events that were temporarily created
+ */
+internal fun ChunkEntity.deleteAndClearThreadEvents() {
+    assertIsManaged()
+    timelineEvents
+            .filter { it.ownedByThreadChunk }
+            .forEach {
+                it.deleteOnCascade(false)
+            }
+    deleteFromRealm()
+}
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 445181e576416c50a287701c98cf6d2efb8bf1c9..ba80cc8302aec1d0da5568e7bc2edd67a667111e 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
@@ -18,10 +18,10 @@ package org.matrix.android.sdk.internal.database.model
 
 import io.realm.RealmObject
 import io.realm.annotations.Index
+import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
+import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
 import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
-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
 import org.matrix.android.sdk.internal.extensions.assertIsManaged
 
@@ -34,16 +34,17 @@ internal open class EventEntity(@Index var eventId: String = "",
                                 @Index var stateKey: String? = null,
                                 var originServerTs: Long? = null,
                                 @Index var sender: String? = null,
-                                // Can contain a serialized MatrixError
+        // Can contain a serialized MatrixError
                                 var sendStateDetails: String? = null,
                                 var age: Long? = 0,
                                 var unsignedData: String? = null,
                                 var redacts: String? = null,
                                 var decryptionResultJson: String? = null,
                                 var ageLocalTs: Long? = null,
-                                // Thread related, no need to create a new Entity for performance
+        // Thread related, no need to create a new Entity for performance
                                 @Index var isRootThread: Boolean = false,
                                 @Index var rootThreadEventId: String? = null,
+        // Number messages within the thread
                                 var numberOfThreads: Int = 0,
                                 var threadSummaryLatestMessage: TimelineEventEntity? = null
 ) : RealmObject() {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt
index 08ecd5995ec490c8a14cfc35f1186b35740e2f9e..47a83f0ed998fbbd27713416a302b90bde103809 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt
@@ -28,7 +28,8 @@ internal open class HomeServerCapabilitiesEntity(
         var maxUploadFileSize: Long = HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN,
         var lastVersionIdentityServerSupported: Boolean = false,
         var defaultIdentityServerUrl: String? = null,
-        var lastUpdatedTimestamp: Long = 0L
+        var lastUpdatedTimestamp: Long = 0L,
+        var canUseThreading: Boolean = false
 ) : RealmObject() {
 
     companion object
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt
index 2997d5d7d8ad4a4a1e3f1f17cb3fbefdbd50c6b3..4a6f6a7bf8970a893e6f018431db61ee57a0a7ee 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt
@@ -20,10 +20,14 @@ import io.realm.RealmList
 import io.realm.RealmObject
 import io.realm.annotations.PrimaryKey
 import org.matrix.android.sdk.api.session.room.model.Membership
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
+import org.matrix.android.sdk.internal.database.query.findRootOrLatest
+import org.matrix.android.sdk.internal.extensions.assertIsManaged
 
 internal open class RoomEntity(@PrimaryKey var roomId: String = "",
                                var chunks: RealmList<ChunkEntity> = RealmList(),
                                var sendingTimelineEvents: RealmList<TimelineEventEntity> = RealmList(),
+                               var threadSummaries: RealmList<ThreadSummaryEntity> = RealmList(),
                                var accountData: RealmList<RoomAccountDataEntity> = RealmList()
 ) : RealmObject() {
 
@@ -46,3 +50,10 @@ internal open class RoomEntity(@PrimaryKey var roomId: String = "",
         }
     companion object
 }
+internal fun RoomEntity.removeThreadSummaryIfNeeded(eventId: String) {
+    assertIsManaged()
+    threadSummaries.findRootOrLatest(eventId)?.let {
+        threadSummaries.remove(it)
+        it.deleteFromRealm()
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt
index febedc34563b8646967c667a19522638f69d1890..cd755590be0f253a296559d7f04f596b10694812 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt
@@ -20,8 +20,8 @@ import io.realm.RealmList
 import io.realm.RealmObject
 import io.realm.annotations.Index
 import io.realm.annotations.PrimaryKey
-import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
 import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
 import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
 import org.matrix.android.sdk.api.session.room.model.RoomSummary
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt
index c090777972194dc6cef0f40f286bb796f6f7d1a4..d0d23dd491b3336676cd4561a184cf8d5655c17e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.database.model
 
 import io.realm.annotations.RealmModule
 import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
 
 /**
  * Realm module for Session
@@ -66,6 +67,7 @@ import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntit
             RoomAccountDataEntity::class,
             SpaceChildSummaryEntity::class,
             SpaceParentSummaryEntity::class,
-            UserPresenceEntity::class
+            UserPresenceEntity::class,
+            ThreadSummaryEntity::class
         ])
 internal class SessionRealmModule
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt
index 185f0e2dcc4bd52ff202879a0c37729c884bee8b..aacd6570bc4768ed37c7ea26e292b3f3e3051cef 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/TimelineEventEntity.kt
@@ -32,6 +32,9 @@ internal open class TimelineEventEntity(var localId: Long = 0,
                                         var isUniqueDisplayName: Boolean = false,
                                         var senderAvatar: String? = null,
                                         var senderMembershipEventId: String? = null,
+                                        // ownedByThreadChunk indicates that the current TimelineEventEntity belongs
+                                        // to a thread chunk and is a temporarily event.
+                                        var ownedByThreadChunk: Boolean = false,
                                         var readReceipts: ReadReceiptsSummaryEntity? = null
 ) : RealmObject() {
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ab9d66548ed418a47652a8f0f4944b084869c8aa
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/threads/ThreadSummaryEntity.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2022 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.database.model.threads
+
+import io.realm.RealmObject
+import io.realm.RealmResults
+import io.realm.annotations.Index
+import io.realm.annotations.LinkingObjects
+import org.matrix.android.sdk.internal.database.model.EventEntity
+import org.matrix.android.sdk.internal.database.model.RoomEntity
+
+internal open class ThreadSummaryEntity(@Index var rootThreadEventId: String? = "",
+                                        var rootThreadEventEntity: EventEntity? = null,
+                                        var latestThreadEventEntity: EventEntity? = null,
+                                        var rootThreadSenderName: String? = null,
+                                        var latestThreadSenderName: String? = null,
+                                        var rootThreadSenderAvatar: String? = null,
+                                        var latestThreadSenderAvatar: String? = null,
+                                        var rootThreadIsUniqueDisplayName: Boolean = false,
+                                        var isUserParticipating: Boolean = false,
+                                        var latestThreadIsUniqueDisplayName: Boolean = false,
+                                        var numberOfThreads: Int = 0
+) : RealmObject() {
+
+    @LinkingObjects("threadSummaries")
+    val room: RealmResults<RoomEntity>? = null
+
+    companion object
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt
index 156a8dd767c9acd266d171a9f51365da28ea7cf4..a33ba82f7aa000df11165e3c028b5273c48fa97f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ChunkEntityQueries.kt
@@ -40,15 +40,38 @@ internal fun ChunkEntity.Companion.find(realm: Realm, roomId: String, prevToken:
     return query.findFirst()
 }
 
+internal fun ChunkEntity.Companion.findAll(realm: Realm, roomId: String, prevToken: String? = null, nextToken: String? = null): RealmResults<ChunkEntity>? {
+    val query = where(realm, roomId)
+    if (prevToken != null) {
+        query.equalTo(ChunkEntityFields.PREV_TOKEN, prevToken)
+    }
+    if (nextToken != null) {
+        query.equalTo(ChunkEntityFields.NEXT_TOKEN, nextToken)
+    }
+    return query.findAll()
+}
+
 internal fun ChunkEntity.Companion.findLastForwardChunkOfRoom(realm: Realm, roomId: String): ChunkEntity? {
     return where(realm, roomId)
             .equalTo(ChunkEntityFields.IS_LAST_FORWARD, true)
             .findFirst()
 }
-
+internal fun ChunkEntity.Companion.findLastForwardChunkOfThread(realm: Realm, roomId: String, rootThreadEventId: String): ChunkEntity? {
+    return where(realm, roomId)
+            .equalTo(ChunkEntityFields.ROOT_THREAD_EVENT_ID, rootThreadEventId)
+            .equalTo(ChunkEntityFields.IS_LAST_FORWARD_THREAD, true)
+            .findFirst()
+}
+internal fun ChunkEntity.Companion.findEventInThreadChunk(realm: Realm, roomId: String, event: String): ChunkEntity? {
+    return where(realm, roomId)
+            .`in`(ChunkEntityFields.TIMELINE_EVENTS.EVENT_ID, arrayListOf(event).toTypedArray())
+            .equalTo(ChunkEntityFields.IS_LAST_FORWARD_THREAD, true)
+            .findFirst()
+}
 internal fun ChunkEntity.Companion.findAllIncludingEvents(realm: Realm, eventIds: List<String>): RealmResults<ChunkEntity> {
     return realm.where<ChunkEntity>()
             .`in`(ChunkEntityFields.TIMELINE_EVENTS.EVENT_ID, eventIds.toTypedArray())
+            .isNull(ChunkEntityFields.ROOT_THREAD_EVENT_ID)
             .findAll()
 }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt
index 14cb7e22da2dabc9ee2132a8efdb7ea4c4e25078..6caa832110ccee65587a7420a8b5f97f796b0d7c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt
@@ -34,7 +34,7 @@ internal fun EventAnnotationsSummaryEntity.Companion.create(realm: Realm, roomId
         this.roomId = roomId
     }
     // Denormalization
-    TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId).findFirst()?.let {
+    TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId).findAll()?.forEach {
         it.annotations = obj
     }
     return obj
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt
index 8cc99c3d2fdf21c6705686e6bd812d7290c2beac..6c587dfcae8381743c97383a8b7fefd64ea0115e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt
@@ -94,7 +94,7 @@ internal fun isReadMarkerMoreRecent(realmConfiguration: RealmConfiguration,
             val eventToCheckIndex = eventToCheck?.displayIndex ?: Int.MAX_VALUE
             eventToCheckIndex <= readMarkerIndex
         } else {
-            eventToCheckChunk?.isLastForward == false
+            eventToCheckChunk != null && readMarkerChunk?.isMoreRecentThan(eventToCheckChunk) == true
         }
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomSummaryEntityQueries.kt
index d1b05a4932c2dc70e7028dec264a82fe292b230f..8993c36a300f1d41b7c510b2eb06d3e315ca9f1e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomSummaryEntityQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomSummaryEntityQueries.kt
@@ -49,6 +49,10 @@ internal fun RoomSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: Strin
     return where(realm, roomId).findFirst() ?: realm.createObject(roomId)
 }
 
+internal fun RoomSummaryEntity.Companion.getOrNull(realm: Realm, roomId: String): RoomSummaryEntity? {
+    return where(realm, roomId).findFirst()
+}
+
 internal fun RoomSummaryEntity.Companion.getDirectRooms(realm: Realm,
                                                         excludeRoomIds: Set<String>? = null): RealmResults<RoomSummaryEntity> {
     return RoomSummaryEntity.where(realm)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ThreadSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ThreadSummaryEntityQueries.kt
new file mode 100644
index 0000000000000000000000000000000000000000..517d43d7cf965efe4593be5144d3028e45a2ddfe
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ThreadSummaryEntityQueries.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2022 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.database.query
+
+import io.realm.Realm
+import io.realm.RealmList
+import io.realm.RealmQuery
+import io.realm.kotlin.createObject
+import io.realm.kotlin.where
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntityFields
+
+internal fun ThreadSummaryEntity.Companion.where(realm: Realm, roomId: String): RealmQuery<ThreadSummaryEntity> {
+    return realm.where<ThreadSummaryEntity>()
+            .equalTo(ThreadSummaryEntityFields.ROOM.ROOM_ID, roomId)
+}
+
+internal fun ThreadSummaryEntity.Companion.where(realm: Realm, roomId: String, rootThreadEventId: String): RealmQuery<ThreadSummaryEntity> {
+    return where(realm, roomId)
+            .equalTo(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ID, rootThreadEventId)
+}
+
+internal fun ThreadSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: String, rootThreadEventId: String): ThreadSummaryEntity {
+    return where(realm, roomId, rootThreadEventId).findFirst() ?: realm.createObject<ThreadSummaryEntity>().apply {
+        this.rootThreadEventId = rootThreadEventId
+    }
+}
+internal fun ThreadSummaryEntity.Companion.getOrNull(realm: Realm, roomId: String, rootThreadEventId: String): ThreadSummaryEntity? {
+    return where(realm, roomId, rootThreadEventId).findFirst()
+}
+internal fun RealmList<ThreadSummaryEntity>.find(rootThreadEventId: String): ThreadSummaryEntity? {
+    return this.where()
+            .equalTo(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ID, rootThreadEventId)
+            .findFirst()
+}
+
+internal fun RealmList<ThreadSummaryEntity>.findRootOrLatest(eventId: String): ThreadSummaryEntity? {
+    return this.where()
+            .beginGroup()
+            .equalTo(ThreadSummaryEntityFields.ROOT_THREAD_EVENT_ID, eventId)
+            .or()
+            .equalTo(ThreadSummaryEntityFields.LATEST_THREAD_EVENT_ENTITY.EVENT_ID, eventId)
+            .endGroup()
+            .findFirst()
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt
index 63f41ebf2cf47473104e34192f759bf254b1e1fc..81d5ac835f1465b7249a786ab224a3e409eab6cd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt
@@ -97,6 +97,7 @@ internal fun RealmQuery<TimelineEventEntity>.filterEvents(filters: TimelineEvent
     if (filters.filterEdits) {
         not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT)
         not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.RESPONSE)
+        not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.REFERENCE)
     }
     if (filters.filterRedacted) {
         not().like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, TimelineEventFilter.Unsigned.REDACTED)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt
index 10a0d1dcec01f4bb8dbcd82b7dc86ee62bb1d109..a7317506a0aafbf7e3d05eafb0f2dd1f365d85a0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt
@@ -26,6 +26,7 @@ internal object TimelineEventFilter {
     internal object Content {
         internal const val EDIT = """{*"m.relates_to"*"rel_type":*"m.replace"*}"""
         internal const val RESPONSE = """{*"m.relates_to"*"rel_type":*"org.matrix.response"*}"""
+        internal const val REFERENCE = """{*"m.relates_to"*"rel_type":*"m.reference"*}"""
     }
 
     /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt
index d9a4f1bde15611254a4365699c0d9f14da9d6965..2fad2d8e786153b3c9704fe686b26c91aa9005d8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt
@@ -28,12 +28,14 @@ import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.auth.AuthenticationService
 import org.matrix.android.sdk.api.auth.HomeServerHistoryService
 import org.matrix.android.sdk.api.raw.RawService
+import org.matrix.android.sdk.api.settings.LightweightSettingsStorage
 import org.matrix.android.sdk.internal.SessionManager
 import org.matrix.android.sdk.internal.auth.AuthModule
 import org.matrix.android.sdk.internal.auth.SessionParamsStore
 import org.matrix.android.sdk.internal.raw.RawModule
 import org.matrix.android.sdk.internal.session.MockHttpInterceptor
 import org.matrix.android.sdk.internal.session.TestInterceptor
+import org.matrix.android.sdk.internal.settings.SettingsModule
 import org.matrix.android.sdk.internal.task.TaskExecutor
 import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver
 import org.matrix.android.sdk.internal.util.system.SystemModule
@@ -46,6 +48,7 @@ import java.io.File
     NetworkModule::class,
     AuthModule::class,
     RawModule::class,
+    SettingsModule::class,
     SystemModule::class,
     NoOpTestModule::class
 ])
@@ -66,6 +69,8 @@ internal interface MatrixComponent {
 
     fun rawService(): RawService
 
+    fun lightweightSettingsStorage(): LightweightSettingsStorage
+
     fun homeServerHistoryService(): HomeServerHistoryService
 
     fun context(): Context
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MoshiProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MoshiProvider.kt
index 9e50e9efe82e162a5a71e12ac26e0100fc948b7a..10b0d4fb13136029292bf774ce30f6789cdcaa07 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MoshiProvider.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MoshiProvider.kt
@@ -37,7 +37,7 @@ import org.matrix.android.sdk.internal.network.parsing.TlsVersionMoshiAdapter
 import org.matrix.android.sdk.internal.network.parsing.UriMoshiAdapter
 import org.matrix.android.sdk.internal.session.sync.parsing.DefaultLazyRoomSyncEphemeralJsonAdapter
 
-object MoshiProvider {
+internal object MoshiProvider {
 
     private val moshi: Moshi = Moshi.Builder()
             .add(UriMoshiAdapter())
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/SerializeNulls.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/SerializeNulls.kt
index 0d8fdde81395ab13ea734038d0e358540a4d53fb..9bd197e42eda5aceaf1f8156e240f9ef57ef25a6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/SerializeNulls.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/SerializeNulls.kt
@@ -25,7 +25,7 @@ import java.lang.reflect.Type
 
 @Retention(AnnotationRetention.RUNTIME)
 @JsonQualifier
-annotation class SerializeNulls {
+internal annotation class SerializeNulls {
     companion object {
         val JSON_ADAPTER_FACTORY: JsonAdapter.Factory = object : JsonAdapter.Factory {
             @Nullable
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt
index 7d004bc5c075514596956145013e8f7195e7a51c..fedd7d05f90117a9a1176c6d9d7a3db7db8f8f9a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/WorkManagerProvider.kt
@@ -80,8 +80,8 @@ internal class WorkManagerProvider @Inject constructor(
             workManager.enqueue(checkWorkerRequest)
             val checkWorkerLiveState = workManager.getWorkInfoByIdLiveData(checkWorkerRequest.id)
             val observer = object : Observer<WorkInfo> {
-                override fun onChanged(workInfo: WorkInfo) {
-                    if (workInfo.state.isFinished) {
+                override fun onChanged(workInfo: WorkInfo?) {
+                    if (workInfo?.state?.isFinished == true) {
                         checkWorkerLiveState.removeObserver(this)
                         if (workInfo.state == WorkInfo.State.FAILED) {
                             throw RuntimeException("MatrixWorkerFactory is not being set on your worker configuration.\n" +
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/LiveData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/LiveData.kt
index fecbb874d0b798d20896c9203c735ae94eca1803..8f57960b95221e880a2ab13760477c617679b194 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/LiveData.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/LiveData.kt
@@ -18,35 +18,7 @@ package org.matrix.android.sdk.internal.extensions
 
 import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.LiveData
-import androidx.lifecycle.MediatorLiveData
-import androidx.lifecycle.Observer
 
-inline fun <T> LiveData<T>.observeK(owner: LifecycleOwner, crossinline observer: (T?) -> Unit) {
-    this.observe(owner, Observer { observer(it) })
-}
-
-inline fun <T> LiveData<T>.observeNotNull(owner: LifecycleOwner, crossinline observer: (T) -> Unit) {
-    this.observe(owner, Observer { it?.run(observer) })
-}
-
-fun <T1, T2, R> combineLatest(source1: LiveData<T1>, source2: LiveData<T2>, mapper: (T1, T2) -> R): LiveData<R> {
-    val combined = MediatorLiveData<R>()
-    var source1Result: T1? = null
-    var source2Result: T2? = null
-
-    fun notify() {
-        if (source1Result != null && source2Result != null) {
-            combined.value = mapper(source1Result!!, source2Result!!)
-        }
-    }
-
-    combined.addSource(source1) {
-        source1Result = it
-        notify()
-    }
-    combined.addSource(source2) {
-        source2Result = it
-        notify()
-    }
-    return combined
+internal inline fun <T> LiveData<T>.observeNotNull(owner: LifecycleOwner, crossinline observer: (T) -> Unit) {
+    this.observe(owner) { it?.run(observer) }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Primitives.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Primitives.kt
index 855e7edac3e9e219ef84cbc929830445a398bfdb..290f06142c6e0f435070bcb8096cb825af900a8c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Primitives.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Primitives.kt
@@ -19,4 +19,4 @@ package org.matrix.android.sdk.internal.extensions
 /**
  * Convert a signed byte to a int value
  */
-fun Byte.toUnsignedInt() = toInt() and 0xff
+internal fun Byte.toUnsignedInt() = toInt() and 0xff
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Result.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Result.kt
index 3734c5dc1daddace0130b80f91547b511dcf71f0..b85102ef1720bfd301c25ca0b9806bea755d181d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Result.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/Result.kt
@@ -17,7 +17,7 @@ package org.matrix.android.sdk.internal.extensions
 
 import org.matrix.android.sdk.api.MatrixCallback
 
-fun <A> Result<A>.foldToCallback(callback: MatrixCallback<A>): Unit = fold(
+internal fun <A> Result<A>.foldToCallback(callback: MatrixCallback<A>): Unit = fold(
         { callback.onSuccess(it) },
         { callback.onFailure(it) }
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/DefaultLegacySessionImporter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/DefaultLegacySessionImporter.kt
index 22085e30fc0902009e77181a5919ac666ba27a10..0a76fb2eef7172b0e1a83590ab876ab07d685f95 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/DefaultLegacySessionImporter.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/DefaultLegacySessionImporter.kt
@@ -26,13 +26,13 @@ import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
 import org.matrix.android.sdk.api.auth.data.SessionParams
 import org.matrix.android.sdk.api.auth.data.WellKnownBaseConfig
 import org.matrix.android.sdk.api.legacy.LegacySessionImporter
+import org.matrix.android.sdk.api.network.ssl.Fingerprint
+import org.matrix.android.sdk.api.util.md5
 import org.matrix.android.sdk.internal.auth.SessionParamsStore
 import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreMigration
 import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule
 import org.matrix.android.sdk.internal.database.RealmKeysUtils
 import org.matrix.android.sdk.internal.legacy.riot.LoginStorage
-import org.matrix.android.sdk.internal.network.ssl.Fingerprint
-import org.matrix.android.sdk.internal.util.md5
 import timber.log.Timber
 import java.io.File
 import javax.inject.Inject
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/Credentials.java b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/Credentials.java
index 0ca0c7db85f622ac132ffc9c533572c42c8bd7ff..bbed159e3c870771c0a99f94bc627d1672865e47 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/Credentials.java
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/Credentials.java
@@ -21,11 +21,9 @@ import org.jetbrains.annotations.Nullable;
 import org.json.JSONException;
 import org.json.JSONObject;
 
-/*
- * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose
- */
-
 /**
+ * <b>IMPORTANT:</b> This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose
+ *
  * The user's credentials.
  */
 public class Credentials {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/Fingerprint.java b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/Fingerprint.java
index 74a3f1ac55020b3966d4efc6bd14f91e0791084f..82541d38f64eb599f6a7a02320a5851ed7b3a348 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/Fingerprint.java
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/Fingerprint.java
@@ -23,11 +23,9 @@ import org.json.JSONObject;
 
 import java.util.Arrays;
 
-/*
- * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose
- */
-
 /**
+ * <b>IMPORTANT:</b> This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose
+ *
  * Represents a X509 Certificate fingerprint.
  */
 public class Fingerprint {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/HomeServerConnectionConfig.java b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/HomeServerConnectionConfig.java
index 75fc187c45814da75484ef703817f6363101ee77..a1b46f6c09450e7e9f5ec7e6eb981bc84ec01811 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/HomeServerConnectionConfig.java
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/HomeServerConnectionConfig.java
@@ -35,11 +35,9 @@ import okhttp3.CipherSuite;
 import okhttp3.TlsVersion;
 import timber.log.Timber;
 
-/*
- * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose
- */
-
 /**
+ * <b>IMPORTANT:</b> This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose
+ *
  * Represents how to connect to a specific Homeserver, may include credentials to use.
  */
 public class HomeServerConnectionConfig {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/LoginStorage.java b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/LoginStorage.java
index 2820b6688617a236a627f6709f959294ca94c90c..924bd461ed5eab684b0f73ec7f6de541d06bdd8f 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/LoginStorage.java
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/LoginStorage.java
@@ -16,6 +16,7 @@
 
 package org.matrix.android.sdk.internal.legacy.riot;
 
+import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.SharedPreferences;
 
@@ -28,11 +29,9 @@ import java.util.List;
 
 import timber.log.Timber;
 
-/*
- * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose
- */
-
 /**
+ * <b>IMPORTANT:</b> This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose
+ *
  * Stores login credentials in SharedPreferences.
  */
 public class LoginStorage {
@@ -196,6 +195,7 @@ public class LoginStorage {
     /**
      * Clear the stored values
      */
+    @SuppressLint("ApplySharedPref")
     public void clear() {
         SharedPreferences prefs = mContext.getSharedPreferences(PREFS_LOGIN, Context.MODE_PRIVATE);
         SharedPreferences.Editor editor = prefs.edit();
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnown.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnown.kt
index 17fd0925f88f018c7487dd4402a35c5df76027b5..3b4bd1b1a3e87986588b9fea0fbfa0646c4184aa 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnown.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnown.kt
@@ -18,11 +18,9 @@ package org.matrix.android.sdk.internal.legacy.riot
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
 
-/*
- * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose
- */
-
 /**
+ * <b>IMPORTANT:</b> This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose
+ *
  * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
  * <pre>
  * {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownBaseConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownBaseConfig.kt
index 7bbdda5eaaa3883e14a33e4a3462f03d92e0b450..2a4ae295fdfff7a2eec52a719e4e60e1148b9660 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownBaseConfig.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownBaseConfig.kt
@@ -18,11 +18,9 @@ package org.matrix.android.sdk.internal.legacy.riot
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
 
-/*
- * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose
- */
-
 /**
+ * <b>IMPORTANT:</b> This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose
+ *
  * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
  * <pre>
  * {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownManagerConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownManagerConfig.kt
index 4efb52d61c3f0504c8549b968d85fb58d9f23e7c..6b1c67f7cbecee5bc3238a3d20406094d631b625 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownManagerConfig.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownManagerConfig.kt
@@ -15,10 +15,9 @@
  */
 package org.matrix.android.sdk.internal.legacy.riot
 
-/*
- * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose
+/**
+ * <b>IMPORTANT:</b> This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose
  */
-
 data class WellKnownManagerConfig(
         val apiUrl: String,
         val uiUrl: String
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownPreferredConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownPreferredConfig.kt
index feefdf920d5d358a16eae259074b01f32712f46f..beb95a1d6f60b72f6ad9f69c894572d9eb87b9be 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownPreferredConfig.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/riot/WellKnownPreferredConfig.kt
@@ -18,11 +18,9 @@ package org.matrix.android.sdk.internal.legacy.riot
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
 
-/*
- * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose
- */
-
 /**
+ * <b>IMPORTANT:</b> This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose
+ *
  * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
  * <pre>
  * {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/HttpHeaders.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/HttpHeaders.kt
index 26bdd905078e46efe4f4e9de8b2bef4d0b9dc144..402e956caa4f6b552e4bd4beb734546cb54b8337 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/HttpHeaders.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/HttpHeaders.kt
@@ -16,7 +16,7 @@
 
 package org.matrix.android.sdk.internal.network
 
-object HttpHeaders {
+internal object HttpHeaders {
 
     const val Authorization = "Authorization"
     const val UserAgent = "User-Agent"
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConnectivityChecker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConnectivityChecker.kt
index e32f6be6fcb27e5c53a178b674e9a6d3b4c1c73f..cd7c99b8f964e6adff32c5fb242f5d91abd83a30 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConnectivityChecker.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConnectivityChecker.kt
@@ -25,7 +25,7 @@ import java.util.Collections
 import java.util.concurrent.atomic.AtomicBoolean
 import javax.inject.Inject
 
-interface NetworkConnectivityChecker {
+internal interface NetworkConnectivityChecker {
     /**
      * Returns true when internet is available
      */
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt
index 1ab1042129f72743a1b42667723360eaf622d7eb..5aec7db66cc08848103e15bf8b11d081b20c4299 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt
@@ -21,6 +21,7 @@ internal object NetworkConstants {
     private const val URI_API_PREFIX_PATH = "_matrix/client"
     const val URI_API_PREFIX_PATH_ = "$URI_API_PREFIX_PATH/"
     const val URI_API_PREFIX_PATH_R0 = "$URI_API_PREFIX_PATH/r0/"
+    const val URI_API_PREFIX_PATH_V1 = "$URI_API_PREFIX_PATH/v1/"
     const val URI_API_PREFIX_PATH_UNSTABLE = "$URI_API_PREFIX_PATH/unstable/"
 
     // Media
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UnitConverterFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UnitConverterFactory.kt
index 513d8c5c861e58d2560618b3cb7521174b164c98..f2571ab73fafaf60d9b5a96a0d92a2600b86b257 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UnitConverterFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UnitConverterFactory.kt
@@ -21,7 +21,7 @@ import retrofit2.Converter
 import retrofit2.Retrofit
 import java.lang.reflect.Type
 
-object UnitConverterFactory : Converter.Factory() {
+internal object UnitConverterFactory : Converter.Factory() {
     override fun responseBodyConverter(type: Type, annotations: Array<out Annotation>,
                                        retrofit: Retrofit): Converter<ResponseBody, *>? {
         return if (type == Unit::class.java) UnitConverter else null
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt
index 27684bbf1afdb82fc5510ded149f3d81f7011e5a..6efa347d3ac374075f0eaddbc95996a0a8f26330 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt
@@ -28,7 +28,7 @@ import java.math.BigDecimal
 /**
  * This is used to check if NUMBER in json is integer or double, so we can preserve typing when serializing/deserializing in a row.
  */
-interface CheckNumberType {
+internal interface CheckNumberType {
 
     companion object {
         val JSON_ADAPTER_FACTORY = object : JsonAdapter.Factory {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/ForceToBoolean.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/ForceToBoolean.kt
index f3b4cff34cd882b9c0e4c5c78c51c5dbd1fb1c93..628486bb5fab22258b77f105e042b0e4ada7a601 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/ForceToBoolean.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/ForceToBoolean.kt
@@ -25,7 +25,7 @@ import timber.log.Timber
 @JsonQualifier
 @Retention(AnnotationRetention.RUNTIME)
 @Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.FUNCTION)
-annotation class ForceToBoolean
+internal annotation class ForceToBoolean
 
 internal class ForceToBooleanJsonAdapter {
     @ToJson
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/RuntimeJsonAdapterFactory.java b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/RuntimeJsonAdapterFactory.java
deleted file mode 100644
index c9bf6cc662d98d1aaeb05d3fc53ae8f37d9f8177..0000000000000000000000000000000000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/RuntimeJsonAdapterFactory.java
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * Copyright 2020 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.network.parsing;
-
-import com.squareup.moshi.JsonAdapter;
-import com.squareup.moshi.JsonDataException;
-import com.squareup.moshi.JsonReader;
-import com.squareup.moshi.JsonWriter;
-import com.squareup.moshi.Moshi;
-import com.squareup.moshi.Types;
-
-import java.io.IOException;
-import java.lang.annotation.Annotation;
-import java.lang.reflect.Type;
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.Set;
-
-import javax.annotation.CheckReturnValue;
-
-/**
- * A JsonAdapter factory for polymorphic types. This is useful when the type is not known before
- * decoding the JSON. This factory's adapters expect JSON in the format of a JSON object with a
- * key whose value is a label that determines the type to which to map the JSON object.
- */
-public final class RuntimeJsonAdapterFactory<T> implements JsonAdapter.Factory {
-    final Class<T> baseType;
-    final String labelKey;
-    final Class fallbackType;
-    final Map<String, Type> labelToType = new LinkedHashMap<>();
-
-    /**
-     * @param baseType The base type for which this factory will create adapters. Cannot be Object.
-     * @param labelKey The key in the JSON object whose value determines the type to which to map the
-     *                 JSON object.
-     */
-    @CheckReturnValue
-    public static <T> RuntimeJsonAdapterFactory<T> of(Class<T> baseType, String labelKey, Class<? extends T> fallbackType) {
-        if (baseType == null) throw new NullPointerException("baseType == null");
-        if (labelKey == null) throw new NullPointerException("labelKey == null");
-        if (baseType == Object.class) {
-            throw new IllegalArgumentException(
-                    "The base type must not be Object. Consider using a marker interface.");
-        }
-        return new RuntimeJsonAdapterFactory<>(baseType, labelKey, fallbackType);
-    }
-
-    RuntimeJsonAdapterFactory(Class<T> baseType, String labelKey, Class fallbackType) {
-        this.baseType = baseType;
-        this.labelKey = labelKey;
-        this.fallbackType = fallbackType;
-    }
-
-    /**
-     * Register the subtype that can be created based on the label. When an unknown type is found
-     * during encoding an {@linkplain IllegalArgumentException} will be thrown. When an unknown label
-     * is found during decoding a {@linkplain JsonDataException} will be thrown.
-     */
-    public RuntimeJsonAdapterFactory<T> registerSubtype(Class<? extends T> subtype, String label) {
-        if (subtype == null) throw new NullPointerException("subtype == null");
-        if (label == null) throw new NullPointerException("label == null");
-        if (labelToType.containsKey(label) || labelToType.containsValue(subtype)) {
-            throw new IllegalArgumentException("Subtypes and labels must be unique.");
-        }
-        labelToType.put(label, subtype);
-        return this;
-    }
-
-    @Override
-    public JsonAdapter<?> create(Type type, Set<? extends Annotation> annotations, Moshi moshi) {
-        if (Types.getRawType(type) != baseType || !annotations.isEmpty()) {
-            return null;
-        }
-        int size = labelToType.size();
-        Map<String, JsonAdapter<Object>> labelToAdapter = new LinkedHashMap<>(size);
-        Map<Type, String> typeToLabel = new LinkedHashMap<>(size);
-        for (Map.Entry<String, Type> entry : labelToType.entrySet()) {
-            String label = entry.getKey();
-            Type typeValue = entry.getValue();
-            typeToLabel.put(typeValue, label);
-            labelToAdapter.put(label, moshi.adapter(typeValue));
-        }
-
-        final JsonAdapter<Object> fallbackAdapter = moshi.adapter(fallbackType);
-        JsonAdapter<Object> objectJsonAdapter = moshi.adapter(Object.class);
-
-        return new RuntimeJsonAdapter(labelKey, labelToAdapter, typeToLabel,
-                objectJsonAdapter, fallbackAdapter).nullSafe();
-    }
-
-    static final class RuntimeJsonAdapter extends JsonAdapter<Object> {
-        final String labelKey;
-        final Map<String, JsonAdapter<Object>> labelToAdapter;
-        final Map<Type, String> typeToLabel;
-        final JsonAdapter<Object> objectJsonAdapter;
-        final JsonAdapter<Object> fallbackAdapter;
-
-        RuntimeJsonAdapter(String labelKey, Map<String, JsonAdapter<Object>> labelToAdapter,
-                           Map<Type, String> typeToLabel, JsonAdapter<Object> objectJsonAdapter,
-                           JsonAdapter<Object> fallbackAdapter) {
-            this.labelKey = labelKey;
-            this.labelToAdapter = labelToAdapter;
-            this.typeToLabel = typeToLabel;
-            this.objectJsonAdapter = objectJsonAdapter;
-            this.fallbackAdapter = fallbackAdapter;
-        }
-
-        @Override
-        public Object fromJson(JsonReader reader) throws IOException {
-            JsonReader.Token peekedToken = reader.peek();
-            if (peekedToken != JsonReader.Token.BEGIN_OBJECT) {
-                throw new JsonDataException("Expected BEGIN_OBJECT but was " + peekedToken
-                        + " at path " + reader.getPath());
-            }
-            Object jsonValue = reader.readJsonValue();
-            Map<String, Object> jsonObject = (Map<String, Object>) jsonValue;
-            Object label = jsonObject.get(labelKey);
-            if (!(label instanceof String)) {
-                return null;
-            }
-            JsonAdapter<Object> adapter = labelToAdapter.get(label);
-            if (adapter == null) {
-                return fallbackAdapter.fromJsonValue(jsonValue);
-            }
-            return adapter.fromJsonValue(jsonValue);
-        }
-
-        @Override
-        public void toJson(JsonWriter writer, Object value) throws IOException {
-            Class<?> type = value.getClass();
-            String label = typeToLabel.get(type);
-            if (label == null) {
-                throw new IllegalArgumentException("Expected one of "
-                        + typeToLabel.keySet()
-                        + " but found "
-                        + value
-                        + ", a "
-                        + value.getClass()
-                        + ". Register this subtype.");
-            }
-            JsonAdapter<Object> adapter = labelToAdapter.get(label);
-            Map<String, Object> jsonValue = (Map<String, Object>) adapter.toJsonValue(value);
-
-            Map<String, Object> valueWithLabel = new LinkedHashMap<>(1 + jsonValue.size());
-            valueWithLabel.put(labelKey, label);
-            valueWithLabel.putAll(jsonValue);
-            objectJsonAdapter.toJson(writer, valueWithLabel);
-        }
-
-        @Override
-        public String toString() {
-            return "RuntimeJsonAdapter(" + labelKey + ")";
-        }
-    }
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/RuntimeJsonAdapterFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/RuntimeJsonAdapterFactory.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0aaa4991cde768ef84d047f25fffe6ac739ee1f2
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/RuntimeJsonAdapterFactory.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2020 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.network.parsing
+
+import com.squareup.moshi.JsonAdapter
+import com.squareup.moshi.JsonDataException
+import com.squareup.moshi.JsonReader
+import com.squareup.moshi.JsonWriter
+import com.squareup.moshi.Moshi
+import com.squareup.moshi.Types
+import java.io.IOException
+import java.lang.reflect.Type
+import javax.annotation.CheckReturnValue
+
+/**
+ * A JsonAdapter factory for polymorphic types. This is useful when the type is not known before
+ * decoding the JSON. This factory's adapters expect JSON in the format of a JSON object with a
+ * key whose value is a label that determines the type to which to map the JSON object.
+ */
+internal class RuntimeJsonAdapterFactory<T>(
+        private val baseType: Class<T>,
+        private val labelKey: String,
+        private val fallbackType: Class<*>
+) : JsonAdapter.Factory {
+    private val labelToType: MutableMap<String, Type> = LinkedHashMap()
+
+    /**
+     * Register the subtype that can be created based on the label. When an unknown type is found
+     * during encoding an [IllegalArgumentException] will be thrown. When an unknown label
+     * is found during decoding a [JsonDataException] will be thrown.
+     */
+    fun registerSubtype(subtype: Class<out T>?, label: String?): RuntimeJsonAdapterFactory<T> {
+        if (subtype == null) throw NullPointerException("subtype == null")
+        if (label == null) throw NullPointerException("label == null")
+        require(!(labelToType.containsKey(label) || labelToType.containsValue(subtype))) { "Subtypes and labels must be unique." }
+        labelToType[label] = subtype
+        return this
+    }
+
+    override fun create(type: Type, annotations: Set<Annotation?>, moshi: Moshi): JsonAdapter<*>? {
+        if (Types.getRawType(type) != baseType || !annotations.isEmpty()) {
+            return null
+        }
+        val size = labelToType.size
+        val labelToAdapter: MutableMap<String, JsonAdapter<Any>> = LinkedHashMap(size)
+        val typeToLabel: MutableMap<Type, String> = LinkedHashMap(size)
+        for ((label, typeValue) in labelToType) {
+            typeToLabel[typeValue] = label
+            labelToAdapter[label] = moshi.adapter(typeValue)
+        }
+        val fallbackAdapter = moshi.adapter<Any>(fallbackType)
+        val objectJsonAdapter = moshi.adapter(Any::class.java)
+        return RuntimeJsonAdapter(labelKey, labelToAdapter, typeToLabel,
+                objectJsonAdapter, fallbackAdapter).nullSafe()
+    }
+
+    @Suppress("UNCHECKED_CAST")
+    internal class RuntimeJsonAdapter(val labelKey: String,
+                                      val labelToAdapter: Map<String, JsonAdapter<Any>>,
+                                      val typeToLabel: Map<Type, String>,
+                                      val objectJsonAdapter: JsonAdapter<Any>,
+                                      val fallbackAdapter: JsonAdapter<Any>) : JsonAdapter<Any?>() {
+        @Throws(IOException::class)
+        override fun fromJson(reader: JsonReader): Any? {
+            val peekedToken = reader.peek()
+            if (peekedToken != JsonReader.Token.BEGIN_OBJECT) {
+                throw JsonDataException("Expected BEGIN_OBJECT but was " + peekedToken +
+                        " at path " + reader.path)
+            }
+            val jsonValue = reader.readJsonValue()
+            val jsonObject = jsonValue as Map<String, Any>?
+            val label = jsonObject!![labelKey] as? String ?: return null
+            val adapter = labelToAdapter[label] ?: return fallbackAdapter.fromJsonValue(jsonValue)
+            return adapter.fromJsonValue(jsonValue)
+        }
+
+        @Throws(IOException::class)
+        override fun toJson(writer: JsonWriter, value: Any?) {
+            val type: Class<*> = value!!.javaClass
+            val label = typeToLabel[type]
+                    ?: throw IllegalArgumentException("Expected one of " +
+                            typeToLabel.keys +
+                            " but found " +
+                            value +
+                            ", a " +
+                            value.javaClass +
+                            ". Register this subtype.")
+            val adapter = labelToAdapter[label]!!
+            val jsonValue = adapter.toJsonValue(value) as Map<String, Any>?
+            val valueWithLabel: MutableMap<String, Any> = LinkedHashMap(1 + jsonValue!!.size)
+            valueWithLabel[labelKey] = label
+            valueWithLabel.putAll(jsonValue)
+            objectJsonAdapter.toJson(writer, valueWithLabel)
+        }
+
+        override fun toString(): String {
+            return "RuntimeJsonAdapter($labelKey)"
+        }
+    }
+
+    companion object {
+        /**
+         * @param baseType The base type for which this factory will create adapters. Cannot be Object.
+         * @param labelKey The key in the JSON object whose value determines the type to which to map the
+         * JSON object.
+         */
+        @CheckReturnValue
+        fun <T> of(baseType: Class<T>, labelKey: String, fallbackType: Class<out T>): RuntimeJsonAdapterFactory<T> {
+            require(baseType != Any::class.java) { "The base type must not be Object. Consider using a marker interface." }
+            return RuntimeJsonAdapterFactory(baseType, labelKey, fallbackType)
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManager.kt
index 6f245aa6d8d70a53cdc130daec97d777cd6ff8b8..ccae5ad14f98c08c84c632f3fa56fbdf16b67d68 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManager.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManager.kt
@@ -16,6 +16,7 @@
 
 package org.matrix.android.sdk.internal.network.ssl
 
+import org.matrix.android.sdk.api.network.ssl.Fingerprint
 import java.security.cert.CertificateException
 import java.security.cert.X509Certificate
 import javax.net.ssl.X509TrustManager
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManagerApi24.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManagerApi24.kt
index 4e58a0f2e5a9a0ee38e18c624221c3a3cd2b2b77..574f1ef81db6fcfe8cfcc642bbea6ac1ef26d2e3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManagerApi24.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManagerApi24.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.network.ssl
 
 import android.os.Build
 import androidx.annotation.RequiresApi
+import org.matrix.android.sdk.api.network.ssl.Fingerprint
 import java.net.Socket
 import java.security.cert.CertificateException
 import java.security.cert.X509Certificate
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManagerProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManagerProvider.kt
index 57b97c75c50e0ebcff4bb1f1d65fe122e7b873a6..f01ee7af24fa04ed6e993fac9a21ae4d0aaa436e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManagerProvider.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/PinnedTrustManagerProvider.kt
@@ -17,6 +17,7 @@
 package org.matrix.android.sdk.internal.network.ssl
 
 import android.os.Build
+import org.matrix.android.sdk.api.network.ssl.Fingerprint
 import javax.net.ssl.X509ExtendedTrustManager
 import javax.net.ssl.X509TrustManager
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/UnrecognizedCertificateException.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/UnrecognizedCertificateException.kt
index ca841f0ffb9d91725217dfaf4b77a6d4044876a7..62eb6cf1f64b00e2c7bd2ac8badd7a379a897b84 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/UnrecognizedCertificateException.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ssl/UnrecognizedCertificateException.kt
@@ -16,6 +16,7 @@
 
 package org.matrix.android.sdk.internal.network.ssl
 
+import org.matrix.android.sdk.api.network.ssl.Fingerprint
 import java.security.cert.CertificateException
 import java.security.cert.X509Certificate
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryEnumListProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryEnumListProcessor.kt
index 5653d7171dd2bc8faac28dbf61e21ffafc6dc1ad..b4415afcbcc13d7a51b046edc5ab34f2bdf65b5b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryEnumListProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryEnumListProcessor.kt
@@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.query
 import io.realm.RealmObject
 import io.realm.RealmQuery
 
-fun <T : RealmObject, E : Enum<E>> RealmQuery<T>.process(field: String, enums: List<Enum<E>>): RealmQuery<T> {
+internal fun <T : RealmObject, E : Enum<E>> RealmQuery<T>.process(field: String, enums: List<Enum<E>>): RealmQuery<T> {
     val lastEnumValue = enums.lastOrNull()
     beginGroup()
     for (enumValue in enums) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryStringValueProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryStringValueProcessor.kt
index b42bf2b8c789f835044326ed317ec0c4f658c5fb..ba4d05e747a47b246a59ed54d067f04774cb7c7d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryStringValueProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryStringValueProcessor.kt
@@ -24,7 +24,7 @@ import org.matrix.android.sdk.api.query.QueryStringValue.ContentQueryStringValue
 import org.matrix.android.sdk.internal.util.Normalizer
 import javax.inject.Inject
 
-class QueryStringValueProcessor @Inject constructor(
+internal class QueryStringValueProcessor @Inject constructor(
         private val normalizer: Normalizer
 ) {
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/migration/MigrateGlobalTo001.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/migration/MigrateGlobalTo001.kt
index cff2f7b8e86ba3610ea22febf8da95d1b9abd2aa..7b332573684adc8542ab4a3fc6f8c53820c548e4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/migration/MigrateGlobalTo001.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/migration/MigrateGlobalTo001.kt
@@ -20,7 +20,7 @@ import io.realm.DynamicRealm
 import org.matrix.android.sdk.internal.database.model.KnownServerUrlEntityFields
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
-class MigrateGlobalTo001(realm: DynamicRealm) : RealmMigrator(realm, 1) {
+internal class MigrateGlobalTo001(realm: DynamicRealm) : RealmMigrator(realm, 1) {
 
     override fun doMigrate(realm: DynamicRealm) {
         realm.schema.create("KnownServerUrlEntity")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt
index 08651764c2f7ba95d1e6135cc1933e46a17ad3a9..ac097f57ee604159f9b6447ade65fa6aae2b3d47 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt
@@ -30,14 +30,14 @@ import okhttp3.RequestBody.Companion.toRequestBody
 import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.failure.Failure
 import org.matrix.android.sdk.api.session.content.ContentUrlResolver
+import org.matrix.android.sdk.api.session.crypto.attachments.ElementToDecrypt
 import org.matrix.android.sdk.api.session.file.FileService
-import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
+import org.matrix.android.sdk.api.util.md5
 import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments
 import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory
 import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificateWithProgress
 import org.matrix.android.sdk.internal.session.download.DownloadProgressInterceptor.Companion.DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER
 import org.matrix.android.sdk.internal.util.file.AtomicFileCreator
-import org.matrix.android.sdk.internal.util.md5
 import org.matrix.android.sdk.internal.util.writeToFile
 import timber.log.Timber
 import java.io.File
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultToDeviceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultToDeviceService.kt
index 1615b8eef9aeeb8ca6a2dc0da313c915eeab1cb3..609acdd89c3f5f404db9b2307e98e58599d9b669 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultToDeviceService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultToDeviceService.kt
@@ -17,10 +17,10 @@
 package org.matrix.android.sdk.internal.session
 
 import org.matrix.android.sdk.api.session.ToDeviceService
+import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
 import org.matrix.android.sdk.api.session.events.model.Content
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
-import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
 import javax.inject.Inject
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt
index 531dea1d5a6591aa68d17d7e7a868a505639af07..0aae9f31058a253b92d65c3e40e50e42f7c5c7d1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt
@@ -45,6 +45,7 @@ import org.matrix.android.sdk.api.session.permalinks.PermalinkService
 import org.matrix.android.sdk.api.session.securestorage.SecureStorageService
 import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
 import org.matrix.android.sdk.api.session.typing.TypingUsersTracker
+import org.matrix.android.sdk.api.util.md5
 import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService
 import org.matrix.android.sdk.internal.crypto.tasks.DefaultRedactEventTask
 import org.matrix.android.sdk.internal.crypto.tasks.RedactEventTask
@@ -87,6 +88,8 @@ import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationMan
 import org.matrix.android.sdk.internal.session.openid.DefaultOpenIdService
 import org.matrix.android.sdk.internal.session.permalinks.DefaultPermalinkService
 import org.matrix.android.sdk.internal.session.room.EventRelationsAggregationProcessor
+import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.DefaultLiveLocationAggregationProcessor
+import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.LiveLocationAggregationProcessor
 import org.matrix.android.sdk.internal.session.room.create.RoomCreateEventProcessor
 import org.matrix.android.sdk.internal.session.room.prune.RedactionEventProcessor
 import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
@@ -96,7 +99,6 @@ import org.matrix.android.sdk.internal.session.securestorage.DefaultSecureStorag
 import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker
 import org.matrix.android.sdk.internal.session.user.accountdata.DefaultSessionAccountDataService
 import org.matrix.android.sdk.internal.session.widgets.DefaultWidgetURLFormatter
-import org.matrix.android.sdk.internal.util.md5
 import retrofit2.Retrofit
 import java.io.File
 import javax.inject.Provider
@@ -104,7 +106,7 @@ import javax.inject.Qualifier
 
 @Qualifier
 @Retention(AnnotationRetention.RUNTIME)
-annotation class MockHttpInterceptor
+internal annotation class MockHttpInterceptor
 
 @Module
 internal abstract class SessionModule {
@@ -390,4 +392,7 @@ internal abstract class SessionModule {
 
     @Binds
     abstract fun bindEventSenderProcessor(processor: EventSenderProcessorCoroutine): EventSenderProcessor
+
+    @Binds
+    abstract fun bindLiveLocationAggregationProcessor(processor: DefaultLiveLocationAggregationProcessor): LiveLocationAggregationProcessor
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/StreamEventsManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/StreamEventsManager.kt
index bb0ca1144542090f8deb18b31f79c795a5a80e16..cfc26045a0e6b748a251e0f5dd57afaa9a70aa5e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/StreamEventsManager.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/StreamEventsManager.kt
@@ -22,8 +22,8 @@ import kotlinx.coroutines.SupervisorJob
 import kotlinx.coroutines.launch
 import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.session.LiveEventListener
+import org.matrix.android.sdk.api.session.crypto.model.MXEventDecryptionResult
 import org.matrix.android.sdk.api.session.events.model.Event
-import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
 import timber.log.Timber
 import javax.inject.Inject
 
@@ -71,7 +71,7 @@ internal class StreamEventsManager @Inject constructor() {
         coroutineScope.launch {
             listeners.forEach {
                 tryOrNull {
-                    it.onEventDecrypted(event.eventId ?: "", event.roomId ?: "", result.clearEvent)
+                    it.onEventDecrypted(event, result.clearEvent)
                 }
             }
         }
@@ -82,7 +82,7 @@ internal class StreamEventsManager @Inject constructor() {
         coroutineScope.launch {
             listeners.forEach {
                 tryOrNull {
-                    it.onEventDecryptionError(event.eventId ?: "", event.roomId ?: "", error)
+                    it.onEventDecryptionError(event, error)
                 }
             }
         }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/TestInterceptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/TestInterceptor.kt
index fad68afd8aa3df65daa3f76506166c0e5bbe0b3a..5b2ba91ba291bebbbb0a79635bda8aab34607c64 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/TestInterceptor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/TestInterceptor.kt
@@ -18,6 +18,6 @@ package org.matrix.android.sdk.internal.session
 
 import okhttp3.Interceptor
 
-interface TestInterceptor : Interceptor {
+internal interface TestInterceptor : Interceptor {
     var sessionId: String?
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/DefaultCacheService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/DefaultCacheService.kt
index 6d0cd37e1fa9c2d60b25edba7ff900967d7a4498..93b0dba13e1f7695303363b2561696bd05bc0af4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/DefaultCacheService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cache/DefaultCacheService.kt
@@ -21,9 +21,9 @@ import org.matrix.android.sdk.internal.di.SessionDatabase
 import org.matrix.android.sdk.internal.task.TaskExecutor
 import javax.inject.Inject
 
-internal class DefaultCacheService @Inject constructor(@SessionDatabase
-                                                       private val clearCacheTask: ClearCacheTask,
-                                                       private val taskExecutor: TaskExecutor
+internal class DefaultCacheService @Inject constructor(
+        @SessionDatabase private val clearCacheTask: ClearCacheTask,
+        private val taskExecutor: TaskExecutor
 ) : CacheService {
 
     override suspend fun clearCache() {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUrlResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUrlResolver.kt
index 660ab8726f7f0646f61fdf20e2868e16fc2764fc..5d7742448234d467904f33273489d5da515623e3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUrlResolver.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUrlResolver.kt
@@ -21,7 +21,7 @@ import org.matrix.android.sdk.api.MatrixUrls.removeMxcPrefix
 import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
 import org.matrix.android.sdk.api.session.content.ContentUrlResolver
 import org.matrix.android.sdk.api.session.contentscanner.ContentScannerService
-import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
+import org.matrix.android.sdk.api.session.crypto.attachments.ElementToDecrypt
 import org.matrix.android.sdk.internal.network.NetworkConstants
 import org.matrix.android.sdk.internal.session.contentscanner.ScanEncryptorUtils
 import org.matrix.android.sdk.internal.session.contentscanner.model.toJson
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 52dee0ee55c509afa5452e39d5783b3682c80b19..75606f2e7a7cb407122db257cd6e95af70d50a33 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
@@ -25,6 +25,7 @@ import com.squareup.moshi.JsonClass
 import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.listeners.ProgressListener
 import org.matrix.android.sdk.api.session.content.ContentAttachmentData
+import org.matrix.android.sdk.api.session.crypto.model.EncryptedFileInfo
 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.MessageAudioContent
@@ -35,7 +36,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
 import org.matrix.android.sdk.api.util.MimeTypes
 import org.matrix.android.sdk.internal.SessionManager
 import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments
-import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
 import org.matrix.android.sdk.internal.database.mapper.ContentMapper
 import org.matrix.android.sdk.internal.database.mapper.asDomain
 import org.matrix.android.sdk.internal.network.ProgressRequestBody
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/DefaultContentScannerService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/DefaultContentScannerService.kt
index 4ecb3376033ca7fa51e816afc04c9386690497e5..da7e2d102e081ca99399cecbc38a855f54c342ed 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/DefaultContentScannerService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/DefaultContentScannerService.kt
@@ -23,8 +23,8 @@ import okhttp3.OkHttpClient
 import org.matrix.android.sdk.api.session.contentscanner.ContentScannerService
 import org.matrix.android.sdk.api.session.contentscanner.ScanState
 import org.matrix.android.sdk.api.session.contentscanner.ScanStatusInfo
+import org.matrix.android.sdk.api.session.crypto.attachments.ElementToDecrypt
 import org.matrix.android.sdk.api.util.Optional
-import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
 import org.matrix.android.sdk.internal.di.Unauthenticated
 import org.matrix.android.sdk.internal.network.RetrofitFactory
 import org.matrix.android.sdk.internal.session.SessionScope
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/DisabledContentScannerService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/DisabledContentScannerService.kt
index 9087c715663791aecbe866f2615c8da6ff1f9611..41c444ad83729e06e67bd3a9ff7ef2429e9e73ce 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/DisabledContentScannerService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/DisabledContentScannerService.kt
@@ -20,8 +20,8 @@ import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
 import org.matrix.android.sdk.api.session.contentscanner.ContentScannerService
 import org.matrix.android.sdk.api.session.contentscanner.ScanStatusInfo
+import org.matrix.android.sdk.api.session.crypto.attachments.ElementToDecrypt
 import org.matrix.android.sdk.api.util.Optional
-import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
 import org.matrix.android.sdk.internal.session.SessionScope
 import javax.inject.Inject
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/ScanEncryptorUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/ScanEncryptorUtils.kt
index 8fc84a487e58167e270b1c28c0098c05e1de75b2..7d14e4ed8004a02d7998db739c074899746e7363 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/ScanEncryptorUtils.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/ScanEncryptorUtils.kt
@@ -16,9 +16,9 @@
 
 package org.matrix.android.sdk.internal.session.contentscanner
 
-import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
-import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
-import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileKey
+import org.matrix.android.sdk.api.session.crypto.attachments.ElementToDecrypt
+import org.matrix.android.sdk.api.session.crypto.model.EncryptedFileInfo
+import org.matrix.android.sdk.api.session.crypto.model.EncryptedFileKey
 import org.matrix.android.sdk.internal.crypto.tools.withOlmEncryption
 import org.matrix.android.sdk.internal.session.contentscanner.model.DownloadBody
 import org.matrix.android.sdk.internal.session.contentscanner.model.EncryptedBody
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/model/DownloadBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/model/DownloadBody.kt
index 5bac96a0c0e2df645385e6f0ebd80cae31a2d3ab..5ffb4e798323ce4045109639ddc8542b03c58b71 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/model/DownloadBody.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/model/DownloadBody.kt
@@ -18,7 +18,7 @@ package org.matrix.android.sdk.internal.session.contentscanner.model
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
-import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
+import org.matrix.android.sdk.api.session.crypto.model.EncryptedFileInfo
 import org.matrix.android.sdk.internal.di.MoshiProvider
 import org.matrix.android.sdk.internal.util.JsonCanonicalizer
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/DownloadEncryptedTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/DownloadEncryptedTask.kt
index f92c869cb890161901d1ad724fa5d4153573e0f9..abde84b6afc5b75a971c3a3b65253f0f67a84744 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/DownloadEncryptedTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/DownloadEncryptedTask.kt
@@ -17,7 +17,7 @@
 package org.matrix.android.sdk.internal.session.contentscanner.tasks
 
 import okhttp3.ResponseBody
-import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
+import org.matrix.android.sdk.api.session.crypto.attachments.ElementToDecrypt
 import org.matrix.android.sdk.internal.network.executeRequest
 import org.matrix.android.sdk.internal.session.contentscanner.ContentScannerApiProvider
 import org.matrix.android.sdk.internal.session.contentscanner.ScanEncryptorUtils
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/ScanEncryptedTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/ScanEncryptedTask.kt
index dab9b5538fe47debf2bcbbfba81a190f8646512d..e098607eb6c760b7fa0a704a61f87e1ddd204327 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/ScanEncryptedTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/ScanEncryptedTask.kt
@@ -18,7 +18,7 @@ package org.matrix.android.sdk.internal.session.contentscanner.tasks
 
 import org.matrix.android.sdk.api.failure.toScanFailure
 import org.matrix.android.sdk.api.session.contentscanner.ScanState
-import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
+import org.matrix.android.sdk.api.session.crypto.attachments.ElementToDecrypt
 import org.matrix.android.sdk.internal.network.executeRequest
 import org.matrix.android.sdk.internal.session.contentscanner.ContentScannerApiProvider
 import org.matrix.android.sdk.internal.session.contentscanner.ScanEncryptorUtils
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/directory/DirectoryAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/directory/DirectoryAPI.kt
index 19bc7e1908bb3ddb2974d3957cc0fdabd09605e3..16c57baafc59ddd5f76c75889fea5bfe35e5240f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/directory/DirectoryAPI.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/directory/DirectoryAPI.kt
@@ -16,9 +16,9 @@
 
 package org.matrix.android.sdk.internal.session.directory
 
+import org.matrix.android.sdk.api.session.room.alias.RoomAliasDescription
 import org.matrix.android.sdk.internal.network.NetworkConstants
 import org.matrix.android.sdk.internal.session.room.alias.AddRoomAliasBody
-import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription
 import retrofit2.http.Body
 import retrofit2.http.DELETE
 import retrofit2.http.GET
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/ProgressResponseBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/ProgressResponseBody.kt
index f4cb1a80e5873fbfa623e019dcef114a6061e4a0..4fd4fda7d1a9ea90c9c9ff357a0bb638cddc4aa9 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/ProgressResponseBody.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/ProgressResponseBody.kt
@@ -24,7 +24,7 @@ import okio.ForwardingSource
 import okio.Source
 import okio.buffer
 
-class ProgressResponseBody(
+internal class ProgressResponseBody(
         private val responseBody: ResponseBody,
         private val chainUrl: String,
         private val progressListener: ProgressListener) : ResponseBody() {
@@ -56,7 +56,7 @@ class ProgressResponseBody(
     }
 }
 
-interface ProgressListener {
+internal interface ProgressListener {
     fun update(url: String, bytesRead: Long, contentLength: Long, done: Boolean)
     fun error(url: String, errorCode: Int)
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/EventFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/EventFilter.kt
index 37630ef8badf8537d0c7f9b40cad3b060ca84106..27a12a61453e833b93e8b3daa2631aac42f1121e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/EventFilter.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/EventFilter.kt
@@ -23,7 +23,7 @@ import com.squareup.moshi.JsonClass
  * https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-user-userid-filter
  */
 @JsonClass(generateAdapter = true)
-data class EventFilter(
+internal data class EventFilter(
         /**
          * The maximum number of events to return.
          */
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt
index 7415b988a43eadd878a12006e35328445aea2da5..676a4f6a38cfcd9f45b546ca839d0b1fdf5845ec 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt
@@ -17,9 +17,21 @@
 package org.matrix.android.sdk.internal.session.filter
 
 import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.RelationType
+import timber.log.Timber
 
 internal object FilterFactory {
 
+    fun createThreadsFilter(numberOfEvents: Int, userId: String?): RoomEventFilter {
+        Timber.i("$userId")
+        return RoomEventFilter(
+                limit = numberOfEvents,
+//                senders = listOf(userId),
+//                relationSenders = userId?.let { listOf(it) },
+                relationTypes = listOf(RelationType.THREAD)
+        )
+    }
+
     fun createUploadsFilter(numberOfEvents: Int): RoomEventFilter {
         return RoomEventFilter(
                 limit = numberOfEvents,
@@ -58,8 +70,8 @@ internal object FilterFactory {
 
     private fun createElementTimelineFilter(): RoomEventFilter? {
         return null // RoomEventFilter().apply {
-            // TODO Enable this for optimization
-            // types = listOfSupportedEventTypes.toMutableList()
+        // TODO Enable this for optimization
+        // types = listOfSupportedEventTypes.toMutableList()
         // }
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterResponse.kt
index b2d542921637494dfe0a0f87ac6fda40ae7a585b..3719c803ccb72b4d8774559d0485819e813fdbcc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterResponse.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterResponse.kt
@@ -23,7 +23,7 @@ import com.squareup.moshi.JsonClass
  * https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-user-userid-filter
  */
 @JsonClass(generateAdapter = true)
-data class FilterResponse(
+internal data class FilterResponse(
         /**
          * Required. The ID of the filter that was created. Cannot start with a { as this character
          * is used to determine if the filter provided is inline JSON or a previously declared
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt
index f49832296708f98ff7743224048411e1cbce0b1f..220c401137521778d83810d2744968400210915c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt
@@ -24,7 +24,7 @@ import org.matrix.android.sdk.internal.di.MoshiProvider
  * https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-user-userid-filter
  */
 @JsonClass(generateAdapter = true)
-data class RoomEventFilter(
+internal data class RoomEventFilter(
         /**
          * The maximum number of events to return.
          */
@@ -52,12 +52,13 @@ data class RoomEventFilter(
          * A list of relation types which must be exist pointing to the event being filtered.
          * If this list is absent then no filtering is done on relation types.
          */
-        @Json(name = "relation_types") val relationTypes: List<String>? = null,
+        @Json(name = "related_by_rel_types") val relationTypes: List<String>? = null,
         /**
          *  A list of senders of relations which must exist pointing to the event being filtered.
          *  If this list is absent then no filtering is done on relation types.
          */
-        @Json(name = "relation_senders") val relationSenders: List<String>? = null,
+        @Json(name = "related_by_senders") val relationSenders: List<String>? = null,
+
         /**
          * A list of room IDs to include. If this list is absent then all rooms are included.
          */
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomFilter.kt
index 2c56a30d3943fed01f40d0bb8c6703302770c676..585d013eaef4e3105acb29292948b890dbff0cbc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomFilter.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomFilter.kt
@@ -23,7 +23,7 @@ import com.squareup.moshi.JsonClass
  * https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-user-userid-filter
  */
 @JsonClass(generateAdapter = true)
-data class RoomFilter(
+internal data class RoomFilter(
         /**
          * A list of room IDs to exclude. If this list is absent then no rooms are excluded.
          * A matching room will be excluded even if it is listed in the 'rooms' filter.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt
index 830a58cd12813398d0df5efe6b1b55892947172a..55526b41db604d0126d0fe69e604bbcccfe3f5f0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt
@@ -65,7 +65,13 @@ internal data class Capabilities(
          * Clients should make use of this capability to determine if users need to be encouraged to upgrade their rooms.
          */
         @Json(name = "m.room_versions")
-        val roomVersions: RoomVersions? = null
+        val roomVersions: RoomVersions? = null,
+        /**
+         * Capability to indicate if the server supports MSC3440 Threading
+         * True if the user can use m.thread relation, false otherwise
+         */
+        @Json(name = "m.thread")
+        val threads: BooleanCapability? = null
 )
 
 @JsonClass(generateAdapter = true)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
index e822cbdcdbf42c06fa2c282dff5b8189400cd8fd..44e13d971a84b3d52a278a1b78783c73e907e75b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
@@ -20,9 +20,11 @@ import com.zhuinden.monarchy.Monarchy
 import org.matrix.android.sdk.api.MatrixPatterns.getDomain
 import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
 import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
+import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.extensions.orTrue
 import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
 import org.matrix.android.sdk.internal.auth.version.Versions
+import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreads
 import org.matrix.android.sdk.internal.auth.version.isLoginAndRegistrationSupportedBySdk
 import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity
 import org.matrix.android.sdk.internal.database.query.getOrCreate
@@ -121,6 +123,8 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
                 homeServerCapabilitiesEntity.roomVersionsJson = capabilities?.roomVersions?.let {
                     MoshiProvider.providesMoshi().adapter(RoomVersions::class.java).toJson(it)
                 }
+                homeServerCapabilitiesEntity.canUseThreading = /* capabilities?.threads?.enabled.orFalse() || */
+                        getVersionResult?.doesServerSupportThreads().orFalse()
             }
 
             if (getMediaConfigResult != null) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt
index c8a9c0f09ad490b090f4007a5d8d8a588bb57e3f..4285f38893cdde2da859a85465d1c65aff6c8bbf 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt
@@ -39,13 +39,13 @@ import org.matrix.android.sdk.api.session.identity.IdentityServiceError
 import org.matrix.android.sdk.api.session.identity.IdentityServiceListener
 import org.matrix.android.sdk.api.session.identity.SharedState
 import org.matrix.android.sdk.api.session.identity.ThreePid
+import org.matrix.android.sdk.api.session.identity.model.SignInvitationResult
 import org.matrix.android.sdk.internal.di.AuthenticatedIdentity
 import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate
 import org.matrix.android.sdk.internal.extensions.observeNotNull
 import org.matrix.android.sdk.internal.network.RetrofitFactory
 import org.matrix.android.sdk.internal.session.SessionScope
 import org.matrix.android.sdk.internal.session.identity.data.IdentityStore
-import org.matrix.android.sdk.internal.session.identity.model.SignInvitationResult
 import org.matrix.android.sdk.internal.session.openid.GetOpenIdTokenTask
 import org.matrix.android.sdk.internal.session.profile.BindThreePidsTask
 import org.matrix.android.sdk.internal.session.profile.UnbindThreePidsTask
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAPI.kt
index 99bd740463f03b2d5ac419977c7bf5e1f4c9d21e..7ca875867706eab5030c38d2b5464b595fb75fa8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAPI.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAPI.kt
@@ -16,6 +16,7 @@
 
 package org.matrix.android.sdk.internal.session.identity
 
+import org.matrix.android.sdk.api.session.identity.model.SignInvitationResult
 import org.matrix.android.sdk.internal.auth.registration.SuccessResult
 import org.matrix.android.sdk.internal.network.NetworkConstants
 import org.matrix.android.sdk.internal.session.identity.model.IdentityAccountResponse
@@ -26,7 +27,6 @@ import org.matrix.android.sdk.internal.session.identity.model.IdentityRequestOwn
 import org.matrix.android.sdk.internal.session.identity.model.IdentityRequestTokenForEmailBody
 import org.matrix.android.sdk.internal.session.identity.model.IdentityRequestTokenForMsisdnBody
 import org.matrix.android.sdk.internal.session.identity.model.IdentityRequestTokenResponse
-import org.matrix.android.sdk.internal.session.identity.model.SignInvitationResult
 import retrofit2.http.Body
 import retrofit2.http.GET
 import retrofit2.http.POST
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/Sign3pidInvitationTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/Sign3pidInvitationTask.kt
index d491af33cadebf24b97578c79aca9003337d7d27..06a6cecc05da6455507f9c9f33ce72c78dc0e13e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/Sign3pidInvitationTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/Sign3pidInvitationTask.kt
@@ -18,10 +18,10 @@ package org.matrix.android.sdk.internal.session.identity
 
 import dagger.Lazy
 import okhttp3.OkHttpClient
+import org.matrix.android.sdk.api.session.identity.model.SignInvitationResult
 import org.matrix.android.sdk.internal.di.Unauthenticated
 import org.matrix.android.sdk.internal.di.UserId
 import org.matrix.android.sdk.internal.network.RetrofitFactory
-import org.matrix.android.sdk.internal.session.identity.model.SignInvitationResult
 import org.matrix.android.sdk.internal.task.Task
 import javax.inject.Inject
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/migration/MigrateIdentityTo001.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/migration/MigrateIdentityTo001.kt
index 002601470d8dd9cd5839f03ceafd55ba5aa2bf93..17a23b828ad1337bea53d5d26edce7ed68ffd4ff 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/migration/MigrateIdentityTo001.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/migration/MigrateIdentityTo001.kt
@@ -21,7 +21,7 @@ import org.matrix.android.sdk.internal.session.identity.db.IdentityDataEntityFie
 import org.matrix.android.sdk.internal.util.database.RealmMigrator
 import timber.log.Timber
 
-class MigrateIdentityTo001(realm: DynamicRealm) : RealmMigrator(realm, 1) {
+internal class MigrateIdentityTo001(realm: DynamicRealm) : RealmMigrator(realm, 1) {
 
     override fun doMigrate(realm: DynamicRealm) {
         Timber.d("Add field userConsent (Boolean) and set the value to false")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/initsync/DefaultSyncStatusService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/initsync/DefaultSyncStatusService.kt
index 079b0d0115e2f1357ef28b2a886e9227060c99b2..4a1e6661b0c8e8e19bd3d706d88238a8b21103e2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/initsync/DefaultSyncStatusService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/initsync/DefaultSyncStatusService.kt
@@ -72,7 +72,7 @@ internal class DefaultSyncStatusService @Inject constructor() :
                 // Update the progress of the leaf and all its parents
                 leaf.setProgress(progress)
                 // Then update the live data using leaf wording and root progress
-                status.postValue(SyncStatusService.Status.Progressing(leaf.initSyncStep, root.currentProgress.toInt()))
+                status.postValue(SyncStatusService.Status.InitialSyncProgressing(leaf.initSyncStep, root.currentProgress.toInt()))
             }
         }
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt
index 8b05d2ea629adf4db7495235d77eb1546adc5713..899bce4c8ddf318effb7c810ff6fae207b768702 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt
@@ -56,7 +56,8 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
 
         val allEvents = (newJoinEvents + inviteEvents).filter { event ->
             when (event.type) {
-                EventType.POLL_START,
+                in EventType.POLL_START,
+                in EventType.STATE_ROOM_BEACON_INFO,
                 EventType.MESSAGE,
                 EventType.REDACTION,
                 EventType.ENCRYPTED,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/DefaultPermalinkService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/DefaultPermalinkService.kt
index 144ebb54042dc7774ee9d082a4577a2dade83f6f..196a8c122df8c32d45c6006d7cdb172d7b0e2a3c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/DefaultPermalinkService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/DefaultPermalinkService.kt
@@ -43,4 +43,8 @@ internal class DefaultPermalinkService @Inject constructor(
     override fun getLinkedId(url: String): String? {
         return permalinkFactory.getLinkedId(url)
     }
+
+    override fun createMentionSpanTemplate(type: PermalinkService.SpanTemplateType, forceMatrixTo: Boolean): String {
+        return permalinkFactory.createMentionSpanTemplate(type, forceMatrixTo)
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt
index 39c1ddfdce3b569f1c5234bf29a9b0cc3642a5b6..0aeb0467de8ebf4ad3f7520448da03becb3a1baa 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt
@@ -21,7 +21,10 @@ import org.matrix.android.sdk.api.MatrixPatterns
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.permalinks.PermalinkData
 import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
+import org.matrix.android.sdk.api.session.permalinks.PermalinkService
 import org.matrix.android.sdk.api.session.permalinks.PermalinkService.Companion.MATRIX_TO_URL_BASE
+import org.matrix.android.sdk.api.session.permalinks.PermalinkService.SpanTemplateType.HTML
+import org.matrix.android.sdk.api.session.permalinks.PermalinkService.SpanTemplateType.MARKDOWN
 import org.matrix.android.sdk.internal.di.UserId
 import javax.inject.Inject
 
@@ -105,6 +108,23 @@ internal class PermalinkFactory @Inject constructor(
                 ?.substringBeforeLast("?")
     }
 
+    fun createMentionSpanTemplate(type: PermalinkService.SpanTemplateType, forceMatrixTo: Boolean): String {
+        return buildString {
+            when (type) {
+                HTML     -> append(MENTION_SPAN_TO_HTML_TEMPLATE_BEGIN)
+                MARKDOWN -> append(MENTION_SPAN_TO_MD_TEMPLATE_BEGIN)
+            }
+            append(baseUrl(forceMatrixTo))
+            if (useClientFormat(forceMatrixTo)) {
+                append(USER_PATH)
+            }
+            when (type) {
+                HTML     -> append(MENTION_SPAN_TO_HTML_TEMPLATE_END)
+                MARKDOWN -> append(MENTION_SPAN_TO_MD_TEMPLATE_END)
+            }
+        }
+    }
+
     /**
      * Escape '/' in id, because it is used as a separator
      *
@@ -147,5 +167,9 @@ internal class PermalinkFactory @Inject constructor(
         private const val ROOM_PATH = "room/"
         private const val USER_PATH = "user/"
         private const val GROUP_PATH = "group/"
+        private const val MENTION_SPAN_TO_HTML_TEMPLATE_BEGIN = "<a href=\""
+        private const val MENTION_SPAN_TO_HTML_TEMPLATE_END = "%1\$s\">%2\$s</a>"
+        private const val MENTION_SPAN_TO_MD_TEMPLATE_BEGIN = "[%2\$s]("
+        private const val MENTION_SPAN_TO_MD_TEMPLATE_END = "%1\$s)"
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/model/GetPresenceResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/model/GetPresenceResponse.kt
index a7552f7b027900f4e3cdc524d795a1013cee9185..1cd6d7d3f20e9db74c95a7ade798e4a318024235 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/model/GetPresenceResponse.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/model/GetPresenceResponse.kt
@@ -21,7 +21,7 @@ import com.squareup.moshi.JsonClass
 import org.matrix.android.sdk.api.session.presence.model.PresenceEnum
 
 @JsonClass(generateAdapter = true)
-data class GetPresenceResponse(
+internal data class GetPresenceResponse(
         @Json(name = "presence")
         val presence: PresenceEnum,
         @Json(name = "last_active_ago")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/model/PresenceContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/model/PresenceContent.kt
index 45e0fcf06efa07c175568aea621d679e415e2386..b1ca5126528f3a66af87f1d30d7a874ab7b254cd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/model/PresenceContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/model/PresenceContent.kt
@@ -24,7 +24,7 @@ import org.matrix.android.sdk.api.session.presence.model.PresenceEnum
  * Class representing the EventType.PRESENCE event content
  */
 @JsonClass(generateAdapter = true)
-data class PresenceContent(
+internal data class PresenceContent(
         /**
          * Required. The presence state for this user. One of: ["online", "offline", "unavailable"]
          */
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/service/DefaultPresenceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/service/DefaultPresenceService.kt
index 1083d5b4c2e4878cb56a084222d9fc695c0e4f96..ca89ef684f5e13eb9e0bb63ed699cd45b40cde16 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/service/DefaultPresenceService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/service/DefaultPresenceService.kt
@@ -23,15 +23,19 @@ import org.matrix.android.sdk.api.session.presence.model.UserPresence
 import org.matrix.android.sdk.internal.di.UserId
 import org.matrix.android.sdk.internal.session.presence.service.task.GetPresenceTask
 import org.matrix.android.sdk.internal.session.presence.service.task.SetPresenceTask
+import org.matrix.android.sdk.internal.session.sync.SyncPresence
+import org.matrix.android.sdk.internal.settings.DefaultLightweightSettingsStorage
 import javax.inject.Inject
 
 internal class DefaultPresenceService @Inject constructor(
         @UserId private val userId: String,
         private val setPresenceTask: SetPresenceTask,
-        private val getPresenceTask: GetPresenceTask
+        private val getPresenceTask: GetPresenceTask,
+        private val lightweightSettingsStorage: DefaultLightweightSettingsStorage
 ) : PresenceService {
 
     override suspend fun setMyPresence(presence: PresenceEnum, statusMsg: String?) {
+        lightweightSettingsStorage.setSyncPresenceStatus(SyncPresence.from(presence))
         setPresenceTask.execute(SetPresenceTask.Params(userId, presence, statusMsg))
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/BindThreePidBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/BindThreePidBody.kt
index fa45ae994025b23059f56be8e9fbc1d7797ab709..4d2a999137e54cb8562287155c1f08cb500b9a37 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/BindThreePidBody.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/BindThreePidBody.kt
@@ -30,17 +30,17 @@ internal data class BindThreePidBody(
          * Required. The identity server to use. (without "https://")
          */
         @Json(name = "id_server")
-        var identityServerUrlWithoutProtocol: String,
+        val identityServerUrlWithoutProtocol: String,
 
         /**
          * Required. An access token previously registered with the identity server.
          */
         @Json(name = "id_access_token")
-        var identityServerAccessToken: String,
+        val identityServerAccessToken: String,
 
         /**
          * Required. The session identifier given by the identity server.
          */
         @Json(name = "sid")
-        var sid: String
+        val sid: String
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt
index 2d8c3e9c78eaecd443d207259b3d0e9998127a9c..3f129c4d71be31af769053f0979d085610086b72 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room
 
 import androidx.lifecycle.LiveData
 import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
+import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
 import org.matrix.android.sdk.api.session.crypto.CryptoService
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.room.Room
@@ -36,6 +37,7 @@ import org.matrix.android.sdk.api.session.room.send.SendService
 import org.matrix.android.sdk.api.session.room.state.StateService
 import org.matrix.android.sdk.api.session.room.tags.TagsService
 import org.matrix.android.sdk.api.session.room.threads.ThreadsService
+import org.matrix.android.sdk.api.session.room.threads.local.ThreadsLocalService
 import org.matrix.android.sdk.api.session.room.timeline.TimelineService
 import org.matrix.android.sdk.api.session.room.typing.TypingService
 import org.matrix.android.sdk.api.session.room.uploads.UploadsService
@@ -43,19 +45,19 @@ import org.matrix.android.sdk.api.session.room.version.RoomVersionService
 import org.matrix.android.sdk.api.session.search.SearchResult
 import org.matrix.android.sdk.api.session.space.Space
 import org.matrix.android.sdk.api.util.Optional
-import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
+import org.matrix.android.sdk.api.util.awaitCallback
 import org.matrix.android.sdk.internal.session.permalinks.ViaParameterFinder
 import org.matrix.android.sdk.internal.session.room.state.SendStateTask
 import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
 import org.matrix.android.sdk.internal.session.search.SearchTask
 import org.matrix.android.sdk.internal.session.space.DefaultSpace
-import org.matrix.android.sdk.internal.util.awaitCallback
 import java.security.InvalidParameterException
 
 internal class DefaultRoom(override val roomId: String,
                            private val roomSummaryDataSource: RoomSummaryDataSource,
                            private val timelineService: TimelineService,
                            private val threadsService: ThreadsService,
+                           private val threadsLocalService: ThreadsLocalService,
                            private val sendService: SendService,
                            private val draftService: DraftService,
                            private val stateService: StateService,
@@ -80,6 +82,7 @@ internal class DefaultRoom(override val roomId: String,
         Room,
         TimelineService by timelineService,
         ThreadsService by threadsService,
+        ThreadsLocalService by threadsLocalService,
         SendService by sendService,
         DraftService by draftService,
         StateService by stateService,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt
index 4a02c55db0fe6c007c609f8deaa89324b33ca65c..8424ee8a36384a5ea91e8c223f2f958fd67b589e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt
@@ -20,28 +20,30 @@ import androidx.lifecycle.LiveData
 import androidx.lifecycle.Transformations
 import androidx.paging.PagedList
 import com.zhuinden.monarchy.Monarchy
+import org.matrix.android.sdk.api.query.QueryStringValue
 import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.identity.model.SignInvitationResult
 import org.matrix.android.sdk.api.session.room.Room
 import org.matrix.android.sdk.api.session.room.RoomService
 import org.matrix.android.sdk.api.session.room.RoomSortOrder
 import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
 import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult
+import org.matrix.android.sdk.api.session.room.alias.RoomAliasDescription
 import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
 import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
 import org.matrix.android.sdk.api.session.room.model.RoomSummary
 import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
 import org.matrix.android.sdk.api.session.room.peeking.PeekResult
+import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
 import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
 import org.matrix.android.sdk.api.util.Optional
 import org.matrix.android.sdk.api.util.toOptional
 import org.matrix.android.sdk.internal.database.mapper.asDomain
 import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
 import org.matrix.android.sdk.internal.di.SessionDatabase
-import org.matrix.android.sdk.internal.session.identity.model.SignInvitationResult
 import org.matrix.android.sdk.internal.session.room.alias.DeleteRoomAliasTask
 import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask
-import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription
 import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask
 import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource
 import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
@@ -51,6 +53,7 @@ import org.matrix.android.sdk.internal.session.room.peeking.PeekRoomTask
 import org.matrix.android.sdk.internal.session.room.peeking.ResolveRoomStateTask
 import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask
 import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
+import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater
 import org.matrix.android.sdk.internal.session.user.accountdata.UpdateBreadcrumbsTask
 import org.matrix.android.sdk.internal.util.fetchCopied
 import javax.inject.Inject
@@ -69,6 +72,7 @@ internal class DefaultRoomService @Inject constructor(
         private val roomSummaryDataSource: RoomSummaryDataSource,
         private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
         private val leaveRoomTask: LeaveRoomTask,
+        private val roomSummaryUpdater: RoomSummaryUpdater
 ) : RoomService {
 
     override suspend fun createRoom(createRoomParams: CreateRoomParams): String {
@@ -92,6 +96,23 @@ internal class DefaultRoomService @Inject constructor(
         return roomSummaryDataSource.getRoomSummaries(queryParams, sortOrder)
     }
 
+    override fun refreshJoinedRoomSummaryPreviews(roomId: String?) {
+        val roomSummaries = getRoomSummaries(roomSummaryQueryParams {
+            if (roomId != null) {
+                this.roomId = QueryStringValue.Equals(roomId)
+            }
+            memberships = listOf(Membership.JOIN)
+        })
+
+        if (roomSummaries.isNotEmpty()) {
+            monarchy.runTransactionSync { realm ->
+                roomSummaries.forEach {
+                    roomSummaryUpdater.refreshLatestPreviewContent(realm, it.roomId)
+                }
+            }
+        }
+    }
+
     override fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams,
                                       sortOrder: RoomSortOrder): LiveData<List<RoomSummary>> {
         return roomSummaryDataSource.getRoomSummariesLive(queryParams, sortOrder)
@@ -109,6 +130,10 @@ internal class DefaultRoomService @Inject constructor(
         return roomSummaryDataSource.getUpdatablePagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder)
     }
 
+    override fun getRoomCountLive(queryParams: RoomSummaryQueryParams): LiveData<Int> {
+        return roomSummaryDataSource.getCountLive(queryParams)
+    }
+
     override fun getNotificationCountForRooms(queryParams: RoomSummaryQueryParams): RoomAggregateNotificationCount {
         return roomSummaryDataSource.getNotificationCountForRooms(queryParams)
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
index 1e0eb8b49787921d29a5a94bbb9bbe5a04d01fac..15ce5810c812abf53427dfc0502bd16cfdbd7417 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
@@ -16,14 +16,15 @@
 package org.matrix.android.sdk.internal.session.room
 
 import io.realm.Realm
-import org.matrix.android.sdk.api.crypto.VerificationState
 import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.query.QueryStringValue
+import org.matrix.android.sdk.api.session.crypto.verification.VerificationState
 import org.matrix.android.sdk.api.session.events.model.AggregatedAnnotation
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.LocalEcho
 import org.matrix.android.sdk.api.session.events.model.RelationType
+import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
 import org.matrix.android.sdk.api.session.events.model.getRelationContent
 import org.matrix.android.sdk.api.session.events.model.toContent
 import org.matrix.android.sdk.api.session.events.model.toModel
@@ -34,6 +35,7 @@ import org.matrix.android.sdk.api.session.room.model.VoteInfo
 import org.matrix.android.sdk.api.session.room.model.VoteSummary
 import org.matrix.android.sdk.api.session.room.model.message.MessageContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageEndPollContent
+import org.matrix.android.sdk.api.session.room.model.message.MessageLiveLocationContent
 import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
 import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent
@@ -42,7 +44,6 @@ import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
 import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
 import org.matrix.android.sdk.internal.SessionManager
-import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
 import org.matrix.android.sdk.internal.crypto.verification.toState
 import org.matrix.android.sdk.internal.database.helper.findRootThreadEvent
 import org.matrix.android.sdk.internal.database.mapper.ContentMapper
@@ -57,12 +58,14 @@ import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryE
 import org.matrix.android.sdk.internal.database.model.ReactionAggregatedSummaryEntityFields
 import org.matrix.android.sdk.internal.database.model.ReferencesAggregatedSummaryEntity
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
+import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
 import org.matrix.android.sdk.internal.database.query.create
 import org.matrix.android.sdk.internal.database.query.getOrCreate
 import org.matrix.android.sdk.internal.database.query.where
 import org.matrix.android.sdk.internal.di.SessionId
 import org.matrix.android.sdk.internal.di.UserId
 import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
+import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.LiveLocationAggregationProcessor
 import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
 import timber.log.Timber
 import javax.inject.Inject
@@ -71,7 +74,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
         @UserId private val userId: String,
         private val stateEventDataSource: StateEventDataSource,
         @SessionId private val sessionId: String,
-        private val sessionManager: SessionManager
+        private val sessionManager: SessionManager,
+        private val liveLocationAggregationProcessor: LiveLocationAggregationProcessor
 ) : EventInsertLiveProcessor {
 
     private val allowedTypes = listOf(
@@ -86,11 +90,8 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
             // TODO Add ?
             // EventType.KEY_VERIFICATION_READY,
             EventType.KEY_VERIFICATION_KEY,
-            EventType.ENCRYPTED,
-            EventType.POLL_START,
-            EventType.POLL_RESPONSE,
-            EventType.POLL_END
-    )
+            EventType.ENCRYPTED
+    ) + EventType.POLL_START + EventType.POLL_RESPONSE + EventType.POLL_END + EventType.BEACON_LOCATION_DATA
 
     override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean {
         return allowedTypes.contains(eventType)
@@ -105,20 +106,20 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
             }
             val isLocalEcho = LocalEcho.isLocalEchoId(event.eventId ?: "")
             when (event.type) {
-                EventType.REACTION             -> {
+                EventType.REACTION                -> {
                     // we got a reaction!!
                     Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}")
                     handleReaction(realm, event, roomId, isLocalEcho)
                 }
-                EventType.MESSAGE              -> {
+                EventType.MESSAGE                 -> {
                     if (event.unsignedData?.relations?.annotations != null) {
                         Timber.v("###REACTION Aggregation in room $roomId for event ${event.eventId}")
                         handleInitialAggregatedRelations(realm, event, roomId, event.unsignedData.relations.annotations)
 
                         EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId ?: "").findFirst()
                                 ?.let {
-                                    TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findFirst()
-                                            ?.let { tet -> tet.annotations = it }
+                                    TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findAll()
+                                            ?.forEach { tet -> tet.annotations = it }
                                 }
                     }
 
@@ -136,7 +137,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                 EventType.KEY_VERIFICATION_START,
                 EventType.KEY_VERIFICATION_MAC,
                 EventType.KEY_VERIFICATION_READY,
-                EventType.KEY_VERIFICATION_KEY -> {
+                EventType.KEY_VERIFICATION_KEY    -> {
                     Timber.v("## SAS REF in room $roomId for event ${event.eventId}")
                     event.content.toModel<MessageRelationContent>()?.relatesTo?.let {
                         if (it.type == RelationType.REFERENCE && it.eventId != null) {
@@ -145,7 +146,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                     }
                 }
 
-                EventType.ENCRYPTED            -> {
+                EventType.ENCRYPTED               -> {
                     // Relation type is in clear
                     val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
                     if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE ||
@@ -156,7 +157,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                                 Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
                                 // A replace!
                                 handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
-                            } else if (event.getClearType() == EventType.POLL_RESPONSE) {
+                            } else if (event.getClearType() in EventType.POLL_RESPONSE) {
                                 event.getClearContent().toModel<MessagePollResponseContent>(catchError = true)?.let { pollResponseContent ->
                                     Timber.v("###RESPONSE in room $roomId for event ${event.eventId}")
                                     handleResponse(realm, event, pollResponseContent, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
@@ -171,22 +172,27 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                             EventType.KEY_VERIFICATION_START,
                             EventType.KEY_VERIFICATION_MAC,
                             EventType.KEY_VERIFICATION_READY,
-                            EventType.KEY_VERIFICATION_KEY -> {
+                            EventType.KEY_VERIFICATION_KEY    -> {
                                 Timber.v("## SAS REF in room $roomId for event ${event.eventId}")
                                 encryptedEventContent.relatesTo.eventId?.let {
                                     handleVerification(realm, event, roomId, isLocalEcho, it)
                                 }
                             }
-                            EventType.POLL_RESPONSE        -> {
+                            in EventType.POLL_RESPONSE        -> {
                                 event.getClearContent().toModel<MessagePollResponseContent>(catchError = true)?.let {
                                     handleResponse(realm, event, it, roomId, isLocalEcho, event.getRelationContent()?.eventId)
                                 }
                             }
-                            EventType.POLL_END             -> {
+                            in EventType.POLL_END             -> {
                                 event.content.toModel<MessageEndPollContent>(catchError = true)?.let {
                                     handleEndPoll(realm, event, it, roomId, isLocalEcho)
                                 }
                             }
+                            in EventType.BEACON_LOCATION_DATA -> {
+                                event.content.toModel<MessageLiveLocationContent>(catchError = true)?.let {
+                                    liveLocationAggregationProcessor.handleLiveLocation(realm, event, it, roomId, isLocalEcho)
+                                }
+                            }
                         }
                     } else if (encryptedEventContent?.relatesTo?.type == RelationType.ANNOTATION) {
                         // Reaction
@@ -196,8 +202,18 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                             handleReaction(realm, event, roomId, isLocalEcho)
                         }
                     }
+                    // HandleInitialAggregatedRelations should also be applied in encrypted messages with annotations
+//                    else if (event.unsignedData?.relations?.annotations != null) {
+//                        Timber.v("###REACTION e2e Aggregation in room $roomId for event ${event.eventId}")
+//                        handleInitialAggregatedRelations(realm, event, roomId, event.unsignedData.relations.annotations)
+//                         EventAnnotationsSummaryEntity.where(realm, roomId, event.eventId ?: "").findFirst()
+//                                 ?.let {
+//                                     TimelineEventEntity.where(realm, roomId = roomId, eventId = event.eventId ?: "").findAll()
+//                                             ?.forEach { tet -> tet.annotations = it }
+//                                 }
+//                    }
                 }
-                EventType.REDACTION            -> {
+                EventType.REDACTION               -> {
                     val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() }
                             ?: return
                     when (eventToPrune.type) {
@@ -217,7 +233,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                         }
                     }
                 }
-                EventType.POLL_START           -> {
+                in EventType.POLL_START           -> {
                     val content: MessagePollContent? = event.content.toModel()
                     if (content?.relatesTo?.type == RelationType.REPLACE) {
                         Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
@@ -225,17 +241,22 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                         handleReplace(realm, event, content, roomId, isLocalEcho)
                     }
                 }
-                EventType.POLL_RESPONSE        -> {
+                in EventType.POLL_RESPONSE        -> {
                     event.content.toModel<MessagePollResponseContent>(catchError = true)?.let {
                         handleResponse(realm, event, it, roomId, isLocalEcho)
                     }
                 }
-                EventType.POLL_END             -> {
+                in EventType.POLL_END             -> {
                     event.content.toModel<MessageEndPollContent>(catchError = true)?.let {
                         handleEndPoll(realm, event, it, roomId, isLocalEcho)
                     }
                 }
-                else                           -> Timber.v("UnHandled event ${event.eventId}")
+                in EventType.BEACON_LOCATION_DATA -> {
+                    event.content.toModel<MessageLiveLocationContent>(catchError = true)?.let {
+                        liveLocationAggregationProcessor.handleLiveLocation(realm, event, it, roomId, isLocalEcho)
+                    }
+                }
+                else                              -> Timber.v("UnHandled event ${event.eventId}")
             }
         } catch (t: Throwable) {
             Timber.e(t, "## Should not happen ")
@@ -243,7 +264,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
     }
 
     // OPT OUT serer aggregation until API mature enough
-    private val SHOULD_HANDLE_SERVER_AGREGGATION = false
+    private val SHOULD_HANDLE_SERVER_AGREGGATION = false // should be true to work with e2e
 
     private fun handleReplace(realm: Realm,
                               event: Event,
@@ -295,14 +316,16 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
             ContentMapper
                     .map(eventAnnotationsSummaryEntity.pollResponseSummary?.aggregatedContent)
                     ?.toModel<PollSummaryContent>()
-                    ?.apply {
-                        totalVotes = 0
-                        winnerVoteCount = 0
-                        votes = emptyList()
-                        votesSummary = emptyMap()
-                    }
-                    ?.apply {
-                        eventAnnotationsSummaryEntity.pollResponseSummary?.aggregatedContent = ContentMapper.map(toContent())
+                    ?.let { existingPollSummaryContent ->
+                        eventAnnotationsSummaryEntity.pollResponseSummary?.aggregatedContent = ContentMapper.map(
+                                PollSummaryContent(
+                                        myVote = existingPollSummaryContent.myVote,
+                                        votes = emptyList(),
+                                        votesSummary = emptyMap(),
+                                        totalVotes = 0,
+                                        winnerVoteCount = 0,
+                                )
+                                        .toContent())
                     }
 
             val txId = event.unsignedData?.transactionId
@@ -335,13 +358,18 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
         }
 
         if (!isLocalEcho) {
-            val replaceEvent = TimelineEventEntity.where(realm, roomId, eventId).findFirst()
+            val replaceEvent = TimelineEventEntity
+                    .where(realm, roomId, eventId)
+                    .equalTo(TimelineEventEntityFields.OWNED_BY_THREAD_CHUNK, false)
+                    .findFirst()
             handleThreadSummaryEdition(editedEvent, replaceEvent, existingSummary?.editions)
         }
     }
 
     /**
      * Check if the edition is on the latest thread event, and update it accordingly
+     * @param editedEvent The event that will be changed
+     * @param replaceEvent The new event
      */
     private fun handleThreadSummaryEdition(editedEvent: EventEntity?,
                                            replaceEvent: TimelineEventEntity?,
@@ -384,15 +412,15 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                     existing.pollResponseSummary = it
                 }
 
-        val closedTime = existingPollSummary?.closedTime
+        val closedTime = existingPollSummary.closedTime
         if (closedTime != null && eventTimestamp > closedTime) {
             Timber.v("## POLL is closed ignore event poll:$targetEventId, event :${event.eventId}")
             return
         }
 
-        val sumModel = ContentMapper.map(existingPollSummary?.aggregatedContent).toModel<PollSummaryContent>() ?: PollSummaryContent()
+        val currentModel = ContentMapper.map(existingPollSummary.aggregatedContent).toModel<PollSummaryContent>()
 
-        if (existingPollSummary!!.sourceEvents.contains(eventId)) {
+        if (existingPollSummary.sourceEvents.contains(eventId)) {
             // ignore this event, we already know it (??)
             Timber.v("## POLL  ignoring event for summary, it's known eventId:$eventId")
             return
@@ -407,17 +435,19 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
             return
         }
 
-        val option = content.response?.answers?.first() ?: return Unit.also {
+        val option = content.getBestResponse()?.answers?.first() ?: return Unit.also {
             Timber.d("## POLL Ignoring malformed response no option eventId:$eventId content: ${event.content}")
         }
 
         // Check if this option is in available options
-        if (!targetPollContent.pollCreationInfo?.answers?.map { it.id }?.contains(option).orFalse()) {
+        if (!targetPollContent.getBestPollCreationInfo()?.answers?.map { it.id }?.contains(option).orFalse()) {
             Timber.v("## POLL $targetEventId doesn't contain option $option")
             return
         }
 
-        val votes = sumModel.votes?.toMutableList() ?: ArrayList()
+        val votes = currentModel?.votes.orEmpty().toMutableList()
+
+        var myVote: String? = null
         val existingVoteIndex = votes.indexOfFirst { it.userId == senderId }
         if (existingVoteIndex != -1) {
             // Is the vote newer?
@@ -426,7 +456,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                 // Take the new one
                 votes[existingVoteIndex] = VoteInfo(senderId, option, eventTimestamp)
                 if (userId == senderId) {
-                    sumModel.myVote = option
+                    myVote = option
                 }
                 Timber.v("## POLL adding vote $option for user $senderId in poll :$targetEventId ")
             } else {
@@ -435,16 +465,14 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
         } else {
             votes.add(VoteInfo(senderId, option, eventTimestamp))
             if (userId == senderId) {
-                sumModel.myVote = option
+                myVote = option
             }
             Timber.v("## POLL adding vote $option for user $senderId in poll :$targetEventId ")
         }
-        sumModel.votes = votes
 
         // Precompute the percentage of votes for all options
         val totalVotes = votes.size
-        sumModel.totalVotes = totalVotes
-        sumModel.votesSummary = votes
+        val newVotesSummary = votes
                 .groupBy({ it.option }, { it.userId })
                 .mapValues {
                     VoteSummary(
@@ -452,7 +480,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                             percentage = if (totalVotes == 0 && it.value.isEmpty()) 0.0 else it.value.size.toDouble() / totalVotes
                     )
                 }
-        sumModel.winnerVoteCount = sumModel.votesSummary?.maxOf { it.value.total } ?: 0
+        val newWinnerVoteCount = newVotesSummary.maxOf { it.value.total }
 
         if (isLocalEcho) {
             existingPollSummary.sourceLocalEchoEvents.add(eventId)
@@ -460,7 +488,15 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
             existingPollSummary.sourceEvents.add(eventId)
         }
 
-        existingPollSummary.aggregatedContent = ContentMapper.map(sumModel.toContent())
+        val newSumModel = PollSummaryContent(
+                myVote = myVote,
+                votes = votes,
+                votesSummary = newVotesSummary,
+                totalVotes = totalVotes,
+                winnerVoteCount = newWinnerVoteCount
+        )
+
+        existingPollSummary.aggregatedContent = ContentMapper.map(newSumModel.toContent())
     }
 
     private fun handleEndPoll(realm: Realm,
@@ -469,46 +505,39 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                               roomId: String,
                               isLocalEcho: Boolean) {
         val pollEventId = content.relatesTo?.eventId ?: return
-
         val pollOwnerId = getPollEvent(roomId, pollEventId)?.root?.senderId
         val isPollOwner = pollOwnerId == event.senderId
-
         val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition)
                 ?.content?.toModel<PowerLevelsContent>()
                 ?.let { PowerLevelsHelper(it) }
+
         if (!isPollOwner && !powerLevelsHelper?.isUserAbleToRedact(event.senderId ?: "").orFalse()) {
             Timber.v("## Received poll.end event $pollEventId but user ${event.senderId} doesn't have enough power level in room $roomId")
             return
         }
 
-        var existing = EventAnnotationsSummaryEntity.where(realm, roomId, pollEventId).findFirst()
-        if (existing == null) {
+        var existingPoll = EventAnnotationsSummaryEntity.where(realm, roomId, pollEventId).findFirst()
+        if (existingPoll == null) {
             Timber.v("## POLL creating new relation summary for $pollEventId")
-            existing = EventAnnotationsSummaryEntity.create(realm, roomId, pollEventId)
+            existingPoll = EventAnnotationsSummaryEntity.create(realm, roomId, pollEventId)
         }
 
         // we have it
-        val existingPollSummary = existing.pollResponseSummary
+        val existingPollSummary = existingPoll.pollResponseSummary
                 ?: realm.createObject(PollResponseAggregatedSummaryEntity::class.java).also {
-                    existing.pollResponseSummary = it
+                    existingPoll.pollResponseSummary = it
                 }
 
-        if (existingPollSummary.closedTime != null) {
-            Timber.v("## Received poll.end event for already ended poll $pollEventId")
-            return
-        }
-
         val txId = event.unsignedData?.transactionId
+        existingPollSummary.closedTime = event.originServerTs
+
         // is it a remote echo?
         if (!isLocalEcho && existingPollSummary.sourceLocalEchoEvents.contains(txId)) {
             // ok it has already been managed
             Timber.v("## POLL  Receiving remote echo of response eventId:$pollEventId")
             existingPollSummary.sourceLocalEchoEvents.remove(txId)
             existingPollSummary.sourceEvents.add(event.eventId)
-            return
         }
-
-        existingPollSummary.closedTime = event.originServerTs
     }
 
     private fun getPollEvent(roomId: String, eventId: String): TimelineEvent? {
@@ -578,11 +607,11 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                 sum.key = reaction
                 sum.firstTimestamp = event.originServerTs ?: 0
                 if (isLocalEcho) {
-                    Timber.v("Adding local echo reaction $reaction")
+                    Timber.v("Adding local echo reaction")
                     sum.sourceLocalEcho.add(txId)
                     sum.count = 1
                 } else {
-                    Timber.v("Adding synced reaction $reaction")
+                    Timber.v("Adding synced reaction")
                     sum.count = 1
                     sum.sourceEvents.add(reactionEventId)
                 }
@@ -594,16 +623,16 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
                     // check if it's not the sync of a local echo
                     if (!isLocalEcho && sum.sourceLocalEcho.contains(txId)) {
                         // ok it has already been counted, just sync the list, do not touch count
-                        Timber.v("Ignoring synced of local echo for reaction $reaction")
+                        Timber.v("Ignoring synced of local echo for reaction")
                         sum.sourceLocalEcho.remove(txId)
                         sum.sourceEvents.add(reactionEventId)
                     } else {
                         sum.count += 1
                         if (isLocalEcho) {
-                            Timber.v("Adding local echo reaction $reaction")
+                            Timber.v("Adding local echo reaction")
                             sum.sourceLocalEcho.add(txId)
                         } else {
-                            Timber.v("Adding synced reaction $reaction")
+                            Timber.v("Adding synced reaction")
                             sum.sourceEvents.add(reactionEventId)
                         }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
index 399bfbd0e45b00e64fb790b2186241b4768f160f..10f75473b71e3256c85e486bcf9238a4b6bf57f2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room
 
 import org.matrix.android.sdk.api.session.events.model.Content
 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.room.model.Membership
 import org.matrix.android.sdk.api.session.room.model.RoomStrippedState
 import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams
@@ -86,7 +87,7 @@ internal interface RoomAPI {
     suspend fun getRoomMessagesFrom(@Path("roomId") roomId: String,
                                     @Query("from") from: String,
                                     @Query("dir") dir: String,
-                                    @Query("limit") limit: Int,
+                                    @Query("limit") limit: Int?,
                                     @Query("filter") filter: String?
     ): PaginationResponse
 
@@ -218,7 +219,6 @@ internal interface RoomAPI {
 
     /**
      * Paginate relations for event based in normal topological order
-     *
      * @param relationType filter for this relation type
      * @param eventType filter for this event type
      */
@@ -227,9 +227,24 @@ internal interface RoomAPI {
                              @Path("eventId") eventId: String,
                              @Path("relationType") relationType: String,
                              @Path("eventType") eventType: String,
+                             @Query("from") from: String? = null,
+                             @Query("to") to: String? = null,
                              @Query("limit") limit: Int? = null
     ): RelationsResponse
 
+    /**
+     * Paginate relations for thread events based in normal topological order
+     * @param relationType filter for this relation type
+     */
+    @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "rooms/{roomId}/relations/{eventId}/{relationType}")
+    suspend fun getThreadsRelations(@Path("roomId") roomId: String,
+                                    @Path("eventId") eventId: String,
+                                    @Path("relationType") relationType: String = RelationType.THREAD,
+                                    @Query("from") from: String? = null,
+                                    @Query("to") to: String? = null,
+                                    @Query("limit") limit: Int? = null
+    ): RelationsResponse
+
     /**
      * Join the given room.
      *
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt
index 70c1ab4f42453412aa2a5b258b3979121bdcd966..72a3f9ab22036910d5624c0aa109fc47ab194cad 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt
@@ -36,6 +36,7 @@ import org.matrix.android.sdk.internal.session.room.state.SendStateTask
 import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
 import org.matrix.android.sdk.internal.session.room.tags.DefaultTagsService
 import org.matrix.android.sdk.internal.session.room.threads.DefaultThreadsService
+import org.matrix.android.sdk.internal.session.room.threads.local.DefaultThreadsLocalService
 import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimelineService
 import org.matrix.android.sdk.internal.session.room.typing.DefaultTypingService
 import org.matrix.android.sdk.internal.session.room.uploads.DefaultUploadsService
@@ -52,6 +53,7 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService:
                                                       private val roomSummaryDataSource: RoomSummaryDataSource,
                                                       private val timelineServiceFactory: DefaultTimelineService.Factory,
                                                       private val threadsServiceFactory: DefaultThreadsService.Factory,
+                                                      private val threadsLocalServiceFactory: DefaultThreadsLocalService.Factory,
                                                       private val sendServiceFactory: DefaultSendService.Factory,
                                                       private val draftServiceFactory: DefaultDraftService.Factory,
                                                       private val stateServiceFactory: DefaultStateService.Factory,
@@ -79,6 +81,7 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService:
                 roomSummaryDataSource = roomSummaryDataSource,
                 timelineService = timelineServiceFactory.create(roomId),
                 threadsService = threadsServiceFactory.create(roomId),
+                threadsLocalService = threadsLocalServiceFactory.create(roomId),
                 sendService = sendServiceFactory.create(roomId),
                 draftService = draftServiceFactory.create(roomId),
                 stateService = stateServiceFactory.create(roomId),
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
index f831a77a5d7c72539c8f4339309b62206caa1d41..5e90076b8af24fb73116c5d52fc9af1830cd56dc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
@@ -77,7 +77,9 @@ import org.matrix.android.sdk.internal.session.room.relation.DefaultUpdateQuickR
 import org.matrix.android.sdk.internal.session.room.relation.FetchEditHistoryTask
 import org.matrix.android.sdk.internal.session.room.relation.FindReactionEventForUndoTask
 import org.matrix.android.sdk.internal.session.room.relation.UpdateQuickReactionTask
+import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadSummariesTask
 import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadTimelineTask
+import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadSummariesTask
 import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
 import org.matrix.android.sdk.internal.session.room.reporting.DefaultReportContentTask
 import org.matrix.android.sdk.internal.session.room.reporting.ReportContentTask
@@ -294,4 +296,7 @@ internal abstract class RoomModule {
 
     @Binds
     abstract fun bindFetchThreadTimelineTask(task: DefaultFetchThreadTimelineTask): FetchThreadTimelineTask
+
+    @Binds
+    abstract fun bindFetchThreadSummariesTask(task: DefaultFetchThreadSummariesTask): FetchThreadSummariesTask
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/DefaultLiveLocationAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/DefaultLiveLocationAggregationProcessor.kt
new file mode 100644
index 0000000000000000000000000000000000000000..95e196c7621303292ce101357c20596daf38fabc
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/DefaultLiveLocationAggregationProcessor.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2022 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.room.aggregation.livelocation
+
+import io.realm.Realm
+import org.matrix.android.sdk.api.extensions.orFalse
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.EventType
+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.livelocation.LiveLocationBeaconContent
+import org.matrix.android.sdk.api.session.room.model.message.MessageLiveLocationContent
+import org.matrix.android.sdk.internal.database.mapper.ContentMapper
+import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
+import org.matrix.android.sdk.internal.database.query.getOrNull
+import timber.log.Timber
+import javax.inject.Inject
+
+internal class DefaultLiveLocationAggregationProcessor @Inject constructor() : LiveLocationAggregationProcessor {
+
+    override fun handleLiveLocation(realm: Realm, event: Event, content: MessageLiveLocationContent, roomId: String, isLocalEcho: Boolean) {
+        val locationSenderId = event.senderId ?: return
+
+        // We shouldn't process local echos
+        if (isLocalEcho) {
+            return
+        }
+
+        // A beacon info state event has to be sent before sending location
+        // TODO handle missing check of m_relatesTo field
+        var beaconInfoEntity: CurrentStateEventEntity? = null
+        val eventTypesIterator = EventType.STATE_ROOM_BEACON_INFO.iterator()
+        while (beaconInfoEntity == null && eventTypesIterator.hasNext()) {
+            beaconInfoEntity = CurrentStateEventEntity.getOrNull(realm, roomId, locationSenderId, eventTypesIterator.next())
+        }
+
+        if (beaconInfoEntity == null) {
+            Timber.v("## LIVE LOCATION. There is not any beacon info which should be emitted before sending location updates")
+            return
+        }
+        val beaconInfoContent = ContentMapper.map(beaconInfoEntity.root?.content)?.toModel<LiveLocationBeaconContent>(catchError = true)
+        if (beaconInfoContent == null) {
+            Timber.v("## LIVE LOCATION. Beacon info content is invalid")
+            return
+        }
+
+        // Check if live location is ended
+        if (!beaconInfoContent.getBestBeaconInfo()?.isLive.orFalse()) {
+            Timber.v("## LIVE LOCATION. Beacon info is not live anymore")
+            return
+        }
+
+        // Check if beacon info is outdated
+        if (isBeaconInfoOutdated(beaconInfoContent, content)) {
+            Timber.v("## LIVE LOCATION. Beacon info has timeout")
+            beaconInfoContent.hasTimedOut = true
+        } else {
+            beaconInfoContent.lastLocationContent = content
+        }
+
+        beaconInfoEntity.root?.content = ContentMapper.map(beaconInfoContent.toContent())
+    }
+
+    private fun isBeaconInfoOutdated(beaconInfoContent: LiveLocationBeaconContent,
+                                     liveLocationContent: MessageLiveLocationContent): Boolean {
+        val beaconInfoStartTime = beaconInfoContent.getBestTimestampAsMilliseconds() ?: 0
+        val liveLocationEventTime = liveLocationContent.getBestTimestampAsMilliseconds() ?: 0
+        val timeout = beaconInfoContent.getBestBeaconInfo()?.timeout ?: 0
+        return liveLocationEventTime - beaconInfoStartTime > timeout
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7b5f23e2433cf7c567642a7e9c416a1759cff48b
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2022 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.room.aggregation.livelocation
+
+import io.realm.Realm
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.room.model.message.MessageLiveLocationContent
+
+internal interface LiveLocationAggregationProcessor {
+    fun handleLiveLocation(realm: Realm,
+                           event: Event,
+                           content: MessageLiveLocationContent,
+                           roomId: String,
+                           isLocalEcho: Boolean)
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt
index 71c8c9cd38cf0472c585b4b846c7fb6362c4327c..dc3ea55a019854d675f1f1a11341ee72128ed6ec 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomIdByAliasTask.kt
@@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.room.alias
 import com.zhuinden.monarchy.Monarchy
 import io.realm.Realm
 import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.api.session.room.alias.RoomAliasDescription
 import org.matrix.android.sdk.api.util.Optional
 import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
 import org.matrix.android.sdk.internal.database.query.findByAlias
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt
index c9914449c3c7871c1d469c81dcda8e47899eded6..3b2e9d3d2287515dc5796498b611178cf4d82466 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt
@@ -16,6 +16,7 @@
 
 package org.matrix.android.sdk.internal.session.room.create
 
+import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
 import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
@@ -24,7 +25,6 @@ import org.matrix.android.sdk.api.session.identity.toMedium
 import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
 import org.matrix.android.sdk.api.util.MimeTypes
 import org.matrix.android.sdk.internal.crypto.DeviceListManager
-import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
 import org.matrix.android.sdk.internal.di.AuthenticatedIdentity
 import org.matrix.android.sdk.internal.di.UserId
 import org.matrix.android.sdk.internal.network.token.AccessTokenProvider
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt
index 3d0f51b831cf67c98394608a0d0b67f51f758a79..70ba9287a2a37dee85f8f0c5c16463a80f774f4e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt
@@ -123,7 +123,7 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(
                     eventId = roomMemberEvent.eventId
                     root = eventEntity
                 }
-                roomMemberEventHandler.handle(realm, roomId, roomMemberEvent)
+                roomMemberEventHandler.handle(realm, roomId, roomMemberEvent, false)
             }
             roomEntity.membersLoadStatus = RoomMembersLoadStatusType.LOADED
             roomSummaryUpdater.update(realm, roomId, updateMembers = true)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEventHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEventHandler.kt
index 25c124bd6bea067456c2b97471e948fb89ecc814..85300fa3518de924f1e6dbf3b108ecb56b7e0951 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEventHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEventHandler.kt
@@ -17,6 +17,7 @@
 package org.matrix.android.sdk.internal.session.room.membership
 
 import io.realm.Realm
+import org.matrix.android.sdk.api.session.events.model.Content
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
@@ -33,23 +34,49 @@ internal class RoomMemberEventHandler @Inject constructor(
         @UserId private val myUserId: String
 ) {
 
-    fun handle(realm: Realm, roomId: String, event: Event, aggregator: SyncResponsePostTreatmentAggregator? = null): Boolean {
+    fun handle(realm: Realm,
+               roomId: String,
+               event: Event,
+               isInitialSync: Boolean,
+               aggregator: SyncResponsePostTreatmentAggregator? = null): Boolean {
         if (event.type != EventType.STATE_ROOM_MEMBER) {
             return false
         }
-        val userId = event.stateKey ?: return false
-        val roomMember = event.getFixedRoomMemberContent()
-        return handle(realm, roomId, userId, roomMember, aggregator)
+        val eventUserId = event.stateKey ?: return false
+        val roomMember = event.getFixedRoomMemberContent() ?: return false
+
+        return if (isInitialSync) {
+            handleInitialSync(realm, roomId, myUserId, eventUserId, roomMember, aggregator)
+        } else {
+            handleIncrementalSync(
+                    realm,
+                    roomId,
+                    eventUserId,
+                    roomMember,
+                    event.resolvedPrevContent(),
+                    aggregator
+            )
+        }
     }
 
-    fun handle(realm: Realm,
-               roomId: String,
-               userId: String,
-               roomMember: RoomMemberContent?,
-               aggregator: SyncResponsePostTreatmentAggregator? = null): Boolean {
-        if (roomMember == null) {
-            return false
+    private fun handleInitialSync(realm: Realm,
+                                  roomId: String,
+                                  currentUserId: String,
+                                  eventUserId: String,
+                                  roomMember: RoomMemberContent,
+                                  aggregator: SyncResponsePostTreatmentAggregator?): Boolean {
+        if (currentUserId != eventUserId) {
+            saveUserEntityLocallyIfNecessary(realm, eventUserId, roomMember)
         }
+        saveRoomMemberEntityLocally(realm, roomId, eventUserId, roomMember)
+        updateDirectChatsIfNecessary(roomId, roomMember, aggregator)
+        return true
+    }
+
+    private fun saveRoomMemberEntityLocally(realm: Realm,
+                                            roomId: String,
+                                            userId: String,
+                                            roomMember: RoomMemberContent) {
         val roomMemberEntity = RoomMemberEntityFactory.create(
                 roomId,
                 userId,
@@ -58,26 +85,58 @@ internal class RoomMemberEventHandler @Inject constructor(
                 // but we want to preserve presence record value and not replace it with null
                 getExistingPresenceState(realm, roomId, userId))
         realm.insertOrUpdate(roomMemberEntity)
+    }
+
+    /**
+     * Get the already existing presence state for a specific user & room in order NOT to be replaced in RoomMemberSummaryEntity
+     * by NULL value.
+     */
+    private fun getExistingPresenceState(realm: Realm, roomId: String, userId: String): UserPresenceEntity? {
+        return RoomMemberSummaryEntity.where(realm, roomId, userId).findFirst()?.userPresenceEntity
+    }
+
+    private fun saveUserEntityLocallyIfNecessary(realm: Realm,
+                                                 userId: String,
+                                                 roomMember: RoomMemberContent) {
         if (roomMember.membership.isActive()) {
-            val userEntity = UserEntityFactory.create(userId, roomMember)
-            realm.insertOrUpdate(userEntity)
+            saveUserLocally(realm, userId, roomMember)
         }
+    }
 
+    private fun saveUserLocally(realm: Realm, userId: String, roomMember: RoomMemberContent) {
+        val userEntity = UserEntityFactory.create(userId, roomMember)
+        realm.insertOrUpdate(userEntity)
+    }
+
+    private fun updateDirectChatsIfNecessary(roomId: String,
+                                             roomMember: RoomMemberContent,
+                                             aggregator: SyncResponsePostTreatmentAggregator?) {
         // check whether this new room member event may be used to update the directs dictionary in account data
         // this is required to handle correctly invite by email in DM
         val mxId = roomMember.thirdPartyInvite?.signed?.mxid
         if (mxId != null && mxId != myUserId) {
             aggregator?.directChatsToCheck?.put(roomId, mxId)
         }
-        return true
     }
 
-    /**
-     * Get the already existing presence state for a specific user & room in order NOT to be replaced in RoomMemberSummaryEntity
-     * by NULL value.
-     */
+    private fun handleIncrementalSync(realm: Realm,
+                                      roomId: String,
+                                      eventUserId: String,
+                                      roomMember: RoomMemberContent,
+                                      prevContent: Content?,
+                                      aggregator: SyncResponsePostTreatmentAggregator?): Boolean {
+        if (aggregator != null) {
+            val previousDisplayName = prevContent?.get("displayname") as? String
+            val previousAvatar = prevContent?.get("avatar_url") as? String
 
-    private fun getExistingPresenceState(realm: Realm, roomId: String, userId: String): UserPresenceEntity? {
-        return RoomMemberSummaryEntity.where(realm, roomId, userId).findFirst()?.userPresenceEntity
+            if (previousDisplayName != roomMember.displayName || previousAvatar != roomMember.avatarUrl) {
+                aggregator.userIdsToFetch.add(eventUserId)
+            }
+        }
+
+        saveRoomMemberEntityLocally(realm, roomId, eventUserId, roomMember)
+        // At the end of the sync, fetch all the profiles from the aggregator
+        updateDirectChatsIfNecessary(roomId, roomMember, aggregator)
+        return true
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/InviteBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/InviteBody.kt
index 06b75709a2e6d6c25cbcb1474fd026529df14b08..300cc210bca3bdb94db5276be022d00bbe289861 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/InviteBody.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/InviteBody.kt
@@ -20,7 +20,7 @@ import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
 
 @JsonClass(generateAdapter = true)
-data class InviteBody(
+internal data class InviteBody(
         @Json(name = "user_id") val userId: String,
         @Json(name = "reason") val reason: String?
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt
index 22a46b6cfc96e1cad099678c5de643d81d4c5166..f883cc33ec6cd87e0ef3c43ccf793974e365dfb0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/joining/JoinRoomTask.kt
@@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.room.membership.joining
 import io.realm.RealmConfiguration
 import kotlinx.coroutines.TimeoutCancellationException
 import org.matrix.android.sdk.api.session.events.model.toContent
+import org.matrix.android.sdk.api.session.identity.model.SignInvitationResult
 import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure
 import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
 import org.matrix.android.sdk.api.session.room.model.Membership
@@ -30,7 +31,6 @@ import org.matrix.android.sdk.internal.database.query.where
 import org.matrix.android.sdk.internal.di.SessionDatabase
 import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
 import org.matrix.android.sdk.internal.network.executeRequest
-import org.matrix.android.sdk.internal.session.identity.model.SignInvitationResult
 import org.matrix.android.sdk.internal.session.room.RoomAPI
 import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource
 import org.matrix.android.sdk.internal.session.room.read.SetReadMarkersTask
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt
index ee52fe574b932ba0cf0aaac736746a51f32b5a0a..fe7dc28228eda5f39c0a90885d4234b1734df0f1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt
@@ -21,11 +21,14 @@ import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.LocalEcho
 import org.matrix.android.sdk.api.session.events.model.UnsignedData
+import org.matrix.android.sdk.internal.database.helper.countInThreadMessages
+import org.matrix.android.sdk.internal.database.helper.findRootThreadEvent
 import org.matrix.android.sdk.internal.database.mapper.ContentMapper
 import org.matrix.android.sdk.internal.database.mapper.EventMapper
 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.model.TimelineEventEntity
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
 import org.matrix.android.sdk.internal.database.query.findWithSenderMembershipEvent
 import org.matrix.android.sdk.internal.database.query.where
 import org.matrix.android.sdk.internal.di.MoshiProvider
@@ -71,7 +74,7 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr
             when (typeToPrune) {
                 EventType.ENCRYPTED,
                 EventType.MESSAGE,
-                EventType.POLL_START -> {
+                in EventType.POLL_START -> {
                     Timber.d("REDACTION for message ${eventToPrune.eventId}")
                     val unsignedData = EventMapper.map(eventToPrune).unsignedData
                             ?: UnsignedData(null, null)
@@ -83,12 +86,14 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr
 //                    }
 
                     val modified = unsignedData.copy(redactedEvent = redactionEvent)
-                    // I Commented the line below, it should not be empty while we lose all the previous info about
-                    // the redacted event
-//                    eventToPrune.content = ContentMapper.map(emptyMap())
+                    // Deleting the content of a thread message will result to delete the thread relation, however threads are now dynamic
+                    // so there is not much of a problem
+                    eventToPrune.content = ContentMapper.map(emptyMap())
                     eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified)
                     eventToPrune.decryptionResultJson = null
                     eventToPrune.decryptionErrorCode = null
+
+                    handleTimelineThreadSummaryIfNeeded(realm, eventToPrune, isLocalEcho)
                 }
 //                EventType.REACTION -> {
 //                    eventRelationsAggregationUpdater.handleReactionRedact(eventToPrune, realm, userId)
@@ -104,6 +109,39 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr
         }
     }
 
+    /**
+     * Invalidates the number of threads in the main timeline thread summary,
+     * with respect to redactions.
+     */
+    private fun handleTimelineThreadSummaryIfNeeded(
+            realm: Realm,
+            eventToPrune: EventEntity,
+            isLocalEcho: Boolean,
+    ) {
+        if (eventToPrune.isThread() && !isLocalEcho) {
+            val roomId = eventToPrune.roomId
+            val rootThreadEvent = eventToPrune.findRootThreadEvent() ?: return
+            val rootThreadEventId = eventToPrune.rootThreadEventId ?: return
+
+            val inThreadMessages = countInThreadMessages(
+                    realm = realm,
+                    roomId = roomId,
+                    rootThreadEventId = rootThreadEventId
+            )
+
+            rootThreadEvent.numberOfThreads = inThreadMessages
+            if (inThreadMessages == 0) {
+                // We should also clear the thread summary list
+                rootThreadEvent.isRootThread = false
+                rootThreadEvent.threadSummaryLatestMessage = null
+                ThreadSummaryEntity
+                        .where(realm, roomId = roomId, rootThreadEventId)
+                        .findFirst()
+                        ?.deleteFromRealm()
+            }
+        }
+    }
+
     private fun computeAllowedKeys(type: String): List<String> {
         // Add filtered content, allowed keys in content depends on the event type
         return when (type) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/FullyReadContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/FullyReadContent.kt
index 9b4db795ec4f36648b61af165406e53ee36ed3a6..00dfe6d29cc83f1fe03a3fba15bb9bc264bb503f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/FullyReadContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/FullyReadContent.kt
@@ -20,6 +20,6 @@ import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
 
 @JsonClass(generateAdapter = true)
-data class FullyReadContent(
+internal data class FullyReadContent(
         @Json(name = "event_id") val eventId: String
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt
index d5019aea7b842448b91636324db1720b9333511e..ab514d31c8480951f7ff567eb803562566aba1e8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt
@@ -34,7 +34,6 @@ import org.matrix.android.sdk.internal.database.mapper.asDomain
 import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
 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.relation.threads.FetchThreadTimelineTask
 import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
 import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
 import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDataSource
@@ -48,7 +47,6 @@ internal class DefaultRelationService @AssistedInject constructor(
         private val eventFactory: LocalEchoEventFactory,
         private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
         private val fetchEditHistoryTask: FetchEditHistoryTask,
-        private val fetchThreadTimelineTask: FetchThreadTimelineTask,
         private val timelineEventDataSource: TimelineEventDataSource,
         @SessionDatabase private val monarchy: Monarchy
 ) : RelationService {
@@ -196,10 +194,6 @@ internal class DefaultRelationService @AssistedInject constructor(
         return eventSenderProcessor.postEvent(event)
     }
 
-    override suspend fun fetchThreadTimeline(rootThreadEventId: String): Boolean {
-        return fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params(roomId, rootThreadEventId))
-    }
-
     /**
      * Saves the event in database as a local echo.
      * SendState is set to UNSENT and it's added to a the sendingTimelineEvents list of the room.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b596f2288ec82f849ca708b5a0a7225ea83cff78
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadSummariesTask.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2022 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.room.relation.threads
+
+import com.zhuinden.monarchy.Monarchy
+import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
+import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummaryUpdateType
+import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
+import org.matrix.android.sdk.internal.database.helper.createOrUpdate
+import org.matrix.android.sdk.internal.database.model.RoomEntity
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
+import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.di.SessionDatabase
+import org.matrix.android.sdk.internal.di.UserId
+import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
+import org.matrix.android.sdk.internal.network.executeRequest
+import org.matrix.android.sdk.internal.session.filter.FilterFactory
+import org.matrix.android.sdk.internal.session.room.RoomAPI
+import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
+import org.matrix.android.sdk.internal.session.room.timeline.PaginationResponse
+import org.matrix.android.sdk.internal.task.Task
+import org.matrix.android.sdk.internal.util.awaitTransaction
+import timber.log.Timber
+import javax.inject.Inject
+
+/***
+ * This class is responsible to Fetch all the thread in the current room,
+ * To fetch all threads in a room, the /messages API is used with newly added filtering options.
+ */
+internal interface FetchThreadSummariesTask : Task<FetchThreadSummariesTask.Params, DefaultFetchThreadSummariesTask.Result> {
+    data class Params(
+            val roomId: String,
+            val from: String = "",
+            val limit: Int = 500,
+            val isUserParticipating: Boolean = true
+    )
+}
+
+internal class DefaultFetchThreadSummariesTask @Inject constructor(
+        private val roomAPI: RoomAPI,
+        private val globalErrorReceiver: GlobalErrorReceiver,
+        @SessionDatabase private val monarchy: Monarchy,
+        private val cryptoService: DefaultCryptoService,
+        @UserId private val userId: String,
+) : FetchThreadSummariesTask {
+
+    override suspend fun execute(params: FetchThreadSummariesTask.Params): Result {
+        val filter = FilterFactory.createThreadsFilter(
+                numberOfEvents = params.limit,
+                userId = if (params.isUserParticipating) userId else null).toJSONString()
+
+        val response = executeRequest(
+                globalErrorReceiver,
+                canRetry = true
+        ) {
+            roomAPI.getRoomMessagesFrom(params.roomId, params.from, PaginationDirection.BACKWARDS.value, params.limit, filter)
+        }
+
+        Timber.i("###THREADS DefaultFetchThreadSummariesTask Fetched size:${response.events.size} nextBatch:${response.end} ")
+
+        return handleResponse(response, params)
+    }
+
+    private suspend fun handleResponse(response: PaginationResponse,
+                                       params: FetchThreadSummariesTask.Params): Result {
+        val rootThreadList = response.events
+        monarchy.awaitTransaction { realm ->
+            val roomEntity = RoomEntity.where(realm, roomId = params.roomId).findFirst() ?: return@awaitTransaction
+
+            val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
+            for (rootThreadEvent in rootThreadList) {
+                if (rootThreadEvent.eventId == null || rootThreadEvent.senderId == null || rootThreadEvent.type == null) {
+                    continue
+                }
+
+                ThreadSummaryEntity.createOrUpdate(
+                        threadSummaryType = ThreadSummaryUpdateType.REPLACE,
+                        realm = realm,
+                        roomId = params.roomId,
+                        rootThreadEvent = rootThreadEvent,
+                        roomMemberContentsByUser = roomMemberContentsByUser,
+                        roomEntity = roomEntity,
+                        userId = userId,
+                        cryptoService = cryptoService)
+            }
+        }
+        return Result.SUCCESS
+    }
+
+    enum class Result {
+        SHOULD_FETCH_MORE,
+        REACHED_END,
+        SUCCESS
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
index e0d501c515f7ea3ec595227d813c9d62a0f5fa6a..116d4aa0a1ab5f427fe0e1e8737e8b13a8cdb633 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2021 The Matrix.org Foundation C.I.C.
+ * Copyright 2022 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.
@@ -18,16 +18,14 @@ package org.matrix.android.sdk.internal.session.room.relation.threads
 import com.zhuinden.monarchy.Monarchy
 import io.realm.Realm
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
+import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
-import org.matrix.android.sdk.api.session.events.model.RelationType
 import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
 import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
 import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
-import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
 import org.matrix.android.sdk.internal.database.helper.addTimelineEvent
-import org.matrix.android.sdk.internal.database.helper.updateThreadSummaryIfNeeded
 import org.matrix.android.sdk.internal.database.mapper.asDomain
 import org.matrix.android.sdk.internal.database.mapper.toEntity
 import org.matrix.android.sdk.internal.database.model.ChunkEntity
@@ -36,8 +34,10 @@ import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEnt
 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.model.ReactionAggregatedSummaryEntity
+import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
 import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
-import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
+import org.matrix.android.sdk.internal.database.query.find
+import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfThread
 import org.matrix.android.sdk.internal.database.query.getOrCreate
 import org.matrix.android.sdk.internal.database.query.getOrNull
 import org.matrix.android.sdk.internal.database.query.where
@@ -47,16 +47,38 @@ import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
 import org.matrix.android.sdk.internal.network.executeRequest
 import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent
 import org.matrix.android.sdk.internal.session.room.RoomAPI
+import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse
 import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
 import org.matrix.android.sdk.internal.task.Task
 import org.matrix.android.sdk.internal.util.awaitTransaction
 import timber.log.Timber
 import javax.inject.Inject
 
-internal interface FetchThreadTimelineTask : Task<FetchThreadTimelineTask.Params, Boolean> {
+/***
+ * This class is responsible to Fetch paginated chunks of the thread timeline using the /relations API
+ *
+ * How it works
+ *
+ * The problem?
+ *  - We cannot use the existing timeline architecture to paginate through the timeline
+ *  - We want our new events to be live, so any interactions with them like reactions will continue to work. We should
+ *    handle appropriately the existing events from /messages api with the new events from /relations.
+ *  - Handling edge cases like receiving an event from /messages while you have already created a new one from the /relations response
+ *
+ * The solution
+ * We generate a temporarily thread chunk that will be used to store any new paginated results from the /relations api
+ * We bind the timeline events from that chunk with the already existing ones. So we will have one common instance, and
+ * all reactions, edits etc will continue to work. If the events do not exists we create them
+ * and we will reuse the same EventEntity instance when (and if) the same event will be fetched from the main (/messages) timeline
+ *
+ */
+internal interface FetchThreadTimelineTask : Task<FetchThreadTimelineTask.Params, DefaultFetchThreadTimelineTask.Result> {
     data class Params(
             val roomId: String,
-            val rootThreadEventId: String
+            val rootThreadEventId: String,
+            val from: String?,
+            val limit: Int
+
     )
 }
 
@@ -69,94 +91,130 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor(
         private val cryptoService: DefaultCryptoService
 ) : FetchThreadTimelineTask {
 
-    override suspend fun execute(params: FetchThreadTimelineTask.Params): Boolean {
-        val isRoomEncrypted = cryptoSessionInfoProvider.isRoomEncrypted(params.roomId)
+    enum class Result {
+        SHOULD_FETCH_MORE,
+        REACHED_END,
+        SUCCESS
+    }
+
+    override suspend fun execute(params: FetchThreadTimelineTask.Params): Result {
         val response = executeRequest(globalErrorReceiver) {
-            roomAPI.getRelations(
+            roomAPI.getThreadsRelations(
                     roomId = params.roomId,
                     eventId = params.rootThreadEventId,
-                    relationType = RelationType.IO_THREAD,
-                    eventType = if (isRoomEncrypted) EventType.ENCRYPTED else EventType.MESSAGE,
-                    limit = 2000
+                    from = params.from,
+                    limit = params.limit
             )
         }
 
-        val threadList = response.chunks + listOfNotNull(response.originalEvent)
-
-        return storeNewEventsIfNeeded(threadList, params.roomId)
+        Timber.i("###THREADS FetchThreadTimelineTask Fetched size:${response.chunks.size} nextBatch:${response.nextBatch} ")
+        return handleRelationsResponse(response, params)
     }
 
-    /**
-     * Store new events if they are not already received, and returns weather or not,
-     * a timeline update should be made
-     * @param threadList is the list containing the thread replies
-     * @param roomId the roomId of the the thread
-     * @return
-     */
-    private suspend fun storeNewEventsIfNeeded(threadList: List<Event>, roomId: String): Boolean {
-        var eventsSkipped = 0
-        monarchy
-                .awaitTransaction { realm ->
-                    val chunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)
-
-                    val optimizedThreadSummaryMap = hashMapOf<String, EventEntity>()
-                    val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
-
-                    for (event in threadList.reversed()) {
-                        if (event.eventId == null || event.senderId == null || event.type == null) {
-                            eventsSkipped++
-                            continue
-                        }
-
-                        if (EventEntity.where(realm, event.eventId).findFirst() != null) {
-                            //  Skip if event already exists
-                            eventsSkipped++
-                            continue
-                        }
-                        if (event.isEncrypted()) {
-                            // Decrypt events that will be stored
-                            decryptIfNeeded(event, roomId)
-                        }
-
-                        handleReaction(realm, event, roomId)
-
-                        val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
-                        val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC)
-
-                        // Sender info
-                        roomMemberContentsByUser.getOrPut(event.senderId) {
-                            // If we don't have any new state on this user, get it from db
-                            val rootStateEvent = CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root
-                            rootStateEvent?.asDomain()?.getFixedRoomMemberContent()
-                        }
-
-                        chunk?.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser)
-                        eventEntity.rootThreadEventId?.let {
-                            // This is a thread event
-                            optimizedThreadSummaryMap[it] = eventEntity
-                        } ?: run {
-                            // This is a normal event or a root thread one
-                            optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity
-                        }
+    private suspend fun handleRelationsResponse(response: RelationsResponse,
+                                                params: FetchThreadTimelineTask.Params): Result {
+        val threadList = response.chunks
+        val threadRootEvent = response.originalEvent
+        val hasReachEnd = response.nextBatch == null
+
+        monarchy.awaitTransaction { realm ->
+
+            val threadChunk = ChunkEntity.findLastForwardChunkOfThread(realm, params.roomId, params.rootThreadEventId)
+                    ?: run {
+                        return@awaitTransaction
                     }
 
-                    optimizedThreadSummaryMap.updateThreadSummaryIfNeeded(
-                            roomId = roomId,
-                            realm = realm,
-                            currentUserId = userId,
-                            shouldUpdateNotifications = false
-                    )
+            threadChunk.prevToken = response.nextBatch
+            val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
+
+            for (event in threadList) {
+                if (event.eventId == null || event.senderId == null || event.type == null) {
+                    continue
+                }
+
+                if (threadChunk.timelineEvents.find(event.eventId) != null) {
+                    // Event already exists in thread chunk, skip it
+                    Timber.i("###THREADS FetchThreadTimelineTask event: ${event.eventId} already exists in thread chunk, skip it")
+                    continue
+                }
+
+                val timelineEvent = TimelineEventEntity
+                        .where(realm, roomId = params.roomId, event.eventId)
+                        .findFirst()
+
+                if (timelineEvent != null) {
+                    // Event already exists but not in the thread chunk
+                    // Lets added there
+                    Timber.i("###THREADS FetchThreadTimelineTask event: ${event.eventId} exists but not in the thread chunk, add it at the end")
+                    threadChunk.timelineEvents.add(timelineEvent)
+                } else {
+                    Timber.i("###THREADS FetchThreadTimelineTask event: ${event.eventId} is brand NEW create an entity and add it!")
+                    val eventEntity = createEventEntity(params.roomId, event, realm)
+                    roomMemberContentsByUser.addSenderState(realm, params.roomId, event.senderId)
+                    threadChunk.addTimelineEvent(
+                            roomId = params.roomId,
+                            eventEntity = eventEntity,
+                            direction = PaginationDirection.FORWARDS,
+                            ownedByThreadChunk = true,
+                            roomMemberContentsByUser = roomMemberContentsByUser)
                 }
-        Timber.i("----> size: ${threadList.size} | skipped: $eventsSkipped | threads: ${threadList.map { it.eventId }}")
+            }
 
-        return eventsSkipped == threadList.size
+            if (hasReachEnd) {
+                val rootThread = TimelineEventEntity
+                        .where(realm, roomId = params.roomId, params.rootThreadEventId)
+                        .findFirst()
+                if (rootThread != null) {
+                    // If root thread event already exists add it to our chunk
+                    threadChunk.timelineEvents.add(rootThread)
+                    Timber.i("###THREADS FetchThreadTimelineTask root thread event: ${params.rootThreadEventId} found and added!")
+                } else if (threadRootEvent?.senderId != null) {
+                    // Case when thread event is not in the device
+                    Timber.i("###THREADS FetchThreadTimelineTask root thread event: ${params.rootThreadEventId} NOT FOUND! Lets create a temp one")
+                    val eventEntity = createEventEntity(params.roomId, threadRootEvent, realm)
+                    roomMemberContentsByUser.addSenderState(realm, params.roomId, threadRootEvent.senderId)
+                    threadChunk.addTimelineEvent(
+                            roomId = params.roomId,
+                            eventEntity = eventEntity,
+                            direction = PaginationDirection.FORWARDS,
+                            ownedByThreadChunk = true,
+                            roomMemberContentsByUser = roomMemberContentsByUser)
+                }
+            }
+        }
+
+        return if (hasReachEnd) {
+            Result.REACHED_END
+        } else {
+            Result.SHOULD_FETCH_MORE
+        }
     }
 
+    // TODO Reuse this function to all the app
     /**
-     * Invoke the event decryption mechanism for a specific event
+     * If we don't have any new state on this user, get it from db
      */
+    private fun HashMap<String, RoomMemberContent?>.addSenderState(realm: Realm, roomId: String, senderId: String) {
+        getOrPut(senderId) {
+            CurrentStateEventEntity
+                    .getOrNull(realm, roomId, senderId, EventType.STATE_ROOM_MEMBER)
+                    ?.root?.asDomain()
+                    ?.getFixedRoomMemberContent()
+        }
+    }
 
-    private fun decryptIfNeeded(event: Event, roomId: String) {
+    /**
+     * Create an EventEntity to be added in the TimelineEventEntity
+     */
+    private fun createEventEntity(roomId: String, event: Event, realm: Realm): EventEntity {
+        val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
+        return event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
+    }
+
+    /**
+     * Invoke the event decryption mechanism for a specific event
+     */
+    private suspend fun decryptIfNeeded(event: Event, roomId: String) {
         try {
             // Event from sync does not have roomId, so add it to the event first
             val result = cryptoService.decryptEvent(event.copy(roomId = roomId), "")
@@ -193,7 +251,7 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor(
                     sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java)
                     sum.key = reaction
                     sum.firstTimestamp = event.originServerTs ?: 0
-                    Timber.v("Adding synced reaction $reaction")
+                    Timber.v("Adding synced reaction")
                     sum.count = 1
                     // reactionEventId not included in the /relations API
 //                    sum.sourceEvents.add(reactionEventId)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
index 28c17f38b6a51cf881d1d19c6aea439f246d5fd6..34e38581d1fc0ccd2710387c46b7b2f598fa43e0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
@@ -46,6 +46,7 @@ import org.matrix.android.sdk.api.util.Cancelable
 import org.matrix.android.sdk.api.util.CancelableBag
 import org.matrix.android.sdk.api.util.JsonDict
 import org.matrix.android.sdk.api.util.NoOpCancellable
+import org.matrix.android.sdk.api.util.TextContent
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import org.matrix.android.sdk.internal.di.SessionId
 import org.matrix.android.sdk.internal.di.WorkManagerProvider
@@ -128,8 +129,14 @@ internal class DefaultSendService @AssistedInject constructor(
                 .let { sendEvent(it) }
     }
 
-    override fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?): Cancelable {
-        return localEchoEventFactory.createLocationEvent(roomId, latitude, longitude, uncertainty)
+    override fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?, isUserLocation: Boolean): Cancelable {
+        return localEchoEventFactory.createLocationEvent(roomId, latitude, longitude, uncertainty, isUserLocation)
+                .also { createLocalEcho(it) }
+                .let { sendEvent(it) }
+    }
+
+    override fun sendLiveLocation(beaconInfoEventId: String, latitude: Double, longitude: Double, uncertainty: Double?): Cancelable {
+        return localEchoEventFactory.createLiveLocationEvent(beaconInfoEventId, roomId, latitude, longitude, uncertainty)
                 .also { createLocalEcho(it) }
                 .let { sendEvent(it) }
     }
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 3c36d587107c55df72f09177a2cbafc6233359fd..bb16563f96ac622d8ca9057040d30d5de4d2517e 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
@@ -43,6 +43,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageEndPollConte
 import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageFormat
 import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent
+import org.matrix.android.sdk.api.session.room.model.message.MessageLiveLocationContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageLocationContent
 import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
 import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent
@@ -64,6 +65,7 @@ import org.matrix.android.sdk.api.session.room.model.relation.ReplyToContent
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
 import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
 import org.matrix.android.sdk.api.session.room.timeline.isReply
+import org.matrix.android.sdk.api.util.TextContent
 import org.matrix.android.sdk.internal.di.UserId
 import org.matrix.android.sdk.internal.session.content.ThumbnailExtractor
 import org.matrix.android.sdk.internal.session.permalinks.PermalinkFactory
@@ -137,16 +139,11 @@ internal class LocalEchoEventFactory @Inject constructor(
                                   options: List<String>,
                                   pollType: PollType): MessagePollContent {
         return MessagePollContent(
-                pollCreationInfo = PollCreationInfo(
-                        question = PollQuestion(
-                                question = question
-                        ),
+                unstablePollCreationInfo = PollCreationInfo(
+                        question = PollQuestion(unstableQuestion = question),
                         kind = pollType,
                         answers = options.map { option ->
-                            PollAnswer(
-                                    id = UUID.randomUUID().toString(),
-                                    answer = option
-                            )
+                            PollAnswer(id = UUID.randomUUID().toString(), unstableAnswer = option)
                         }
                 )
         )
@@ -167,7 +164,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                 originServerTs = dummyOriginServerTs(),
                 senderId = userId,
                 eventId = localId,
-                type = EventType.POLL_START,
+                type = EventType.POLL_START.first(),
                 content = newContent.toContent()
         )
     }
@@ -179,11 +176,9 @@ internal class LocalEchoEventFactory @Inject constructor(
                 body = answerId,
                 relatesTo = RelationDefaultContent(
                         type = RelationType.REFERENCE,
-                        eventId = pollEventId),
-                response = PollResponse(
-                        answers = listOf(answerId)
-                )
-
+                        eventId = pollEventId
+                ),
+                unstableResponse = PollResponse(answers = listOf(answerId))
         )
         val localId = LocalEcho.createLocalEchoId()
         return Event(
@@ -191,7 +186,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                 originServerTs = dummyOriginServerTs(),
                 senderId = userId,
                 eventId = localId,
-                type = EventType.POLL_RESPONSE,
+                type = EventType.POLL_RESPONSE.first(),
                 content = content.toContent(),
                 unsignedData = UnsignedData(age = null, transactionId = localId))
     }
@@ -207,7 +202,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                 originServerTs = dummyOriginServerTs(),
                 senderId = userId,
                 eventId = localId,
-                type = EventType.POLL_START,
+                type = EventType.POLL_START.first(),
                 content = content.toContent(),
                 unsignedData = UnsignedData(age = null, transactionId = localId))
     }
@@ -226,7 +221,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                 originServerTs = dummyOriginServerTs(),
                 senderId = userId,
                 eventId = localId,
-                type = EventType.POLL_END,
+                type = EventType.POLL_END.first(),
                 content = content.toContent(),
                 unsignedData = UnsignedData(age = null, transactionId = localId))
     }
@@ -234,24 +229,47 @@ internal class LocalEchoEventFactory @Inject constructor(
     fun createLocationEvent(roomId: String,
                             latitude: Double,
                             longitude: Double,
-                            uncertainty: Double?): Event {
+                            uncertainty: Double?,
+                            isUserLocation: Boolean): Event {
         val geoUri = buildGeoUri(latitude, longitude, uncertainty)
+        val assetType = if (isUserLocation) LocationAssetType.SELF else LocationAssetType.PIN
         val content = MessageLocationContent(
                 geoUri = geoUri,
                 body = geoUri,
-                locationInfo = LocationInfo(
-                        geoUri = geoUri,
-                        description = geoUri
-                ),
-                locationAsset = LocationAsset(
-                        type = LocationAssetType.SELF
-                ),
-                ts = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()),
-                text = geoUri
+                unstableLocationInfo = LocationInfo(geoUri = geoUri, description = geoUri),
+                unstableLocationAsset = LocationAsset(type = assetType),
+                unstableTs = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()),
+                unstableText = geoUri
         )
         return createMessageEvent(roomId, content)
     }
 
+    fun createLiveLocationEvent(beaconInfoEventId: String,
+                                roomId: String,
+                                latitude: Double,
+                                longitude: Double,
+                                uncertainty: Double?): Event {
+        val geoUri = buildGeoUri(latitude, longitude, uncertainty)
+        val content = MessageLiveLocationContent(
+                body = geoUri,
+                relatesTo = RelationDefaultContent(
+                        type = RelationType.REFERENCE,
+                        eventId = beaconInfoEventId
+                ),
+                unstableLocationInfo = LocationInfo(geoUri = geoUri, description = geoUri),
+                unstableTimestampAsMilliseconds = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()),
+        )
+        val localId = LocalEcho.createLocalEchoId()
+        return Event(
+                roomId = roomId,
+                originServerTs = dummyOriginServerTs(),
+                senderId = userId,
+                eventId = localId,
+                type = EventType.BEACON_LOCATION_DATA.first(),
+                content = content.toContent(),
+                unsignedData = UnsignedData(age = null, transactionId = localId))
+    }
+
     fun createReplaceTextOfReply(roomId: String,
                                  eventReplaced: TimelineEvent,
                                  originalEvent: TimelineEvent,
@@ -353,8 +371,9 @@ internal class LocalEchoEventFactory @Inject constructor(
                 url = attachment.queryUri.toString(),
                 relatesTo = rootThreadEventId?.let {
                     RelationDefaultContent(
-                            type = RelationType.IO_THREAD,
+                            type = RelationType.THREAD,
                             eventId = it,
+                            isFallingBack = true,
                             inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
                     )
                 }
@@ -396,8 +415,9 @@ internal class LocalEchoEventFactory @Inject constructor(
                 url = attachment.queryUri.toString(),
                 relatesTo = rootThreadEventId?.let {
                     RelationDefaultContent(
-                            type = RelationType.IO_THREAD,
+                            type = RelationType.THREAD,
                             eventId = it,
+                            isFallingBack = true,
                             inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
                     )
                 }
@@ -426,8 +446,9 @@ internal class LocalEchoEventFactory @Inject constructor(
                 voiceMessageIndicator = if (!isVoiceMessage) null else emptyMap(),
                 relatesTo = rootThreadEventId?.let {
                     RelationDefaultContent(
-                            type = RelationType.IO_THREAD,
+                            type = RelationType.THREAD,
                             eventId = it,
+                            isFallingBack = true,
                             inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
                     )
                 }
@@ -446,8 +467,9 @@ internal class LocalEchoEventFactory @Inject constructor(
                 url = attachment.queryUri.toString(),
                 relatesTo = rootThreadEventId?.let {
                     RelationDefaultContent(
-                            type = RelationType.IO_THREAD,
+                            type = RelationType.THREAD,
                             eventId = it,
+                            isFallingBack = true,
                             inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it))
                     )
                 }
@@ -479,7 +501,7 @@ internal class LocalEchoEventFactory @Inject constructor(
     private fun enhanceStickerIfNeeded(type: String, content: Content?): Content? {
         var newContent: Content? = null
         if (type == EventType.STICKER) {
-            val isThread = (content.toModel<MessageStickerContent>())?.relatesTo?.type == RelationType.IO_THREAD
+            val isThread = (content.toModel<MessageStickerContent>())?.relatesTo?.type == RelationType.THREAD
             val rootThreadEventId = (content.toModel<MessageStickerContent>())?.relatesTo?.eventId
             if (isThread && rootThreadEventId != null) {
                 val newRelationalDefaultContent = (content.toModel<MessageStickerContent>())?.relatesTo?.copy(
@@ -560,7 +582,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                 relatesTo = generateReplyRelationContent(
                         eventId = eventId,
                         rootThreadEventId = rootThreadEventId,
-                        showAsReply = showInThread))
+                        showInThread = showInThread))
         return createMessageEvent(roomId, content)
     }
 
@@ -570,18 +592,20 @@ internal class LocalEchoEventFactory @Inject constructor(
      * "m.relates_to": {
      *      "rel_type": "m.thread",
      *      "event_id": "$thread_root",
+     *      "is_falling_back": false,
      *      "m.in_reply_to": {
-     *          "event_id": "$event_target",
-     *          "render_in": ["m.thread"]
-     *        }
-     *   }
+     *          "event_id": "$event_target"
+     *      }
+     *  }
      */
-    private fun generateReplyRelationContent(eventId: String, rootThreadEventId: String? = null, showAsReply: Boolean): RelationDefaultContent =
+    private fun generateReplyRelationContent(eventId: String, rootThreadEventId: String? = null, showInThread: Boolean): RelationDefaultContent =
             rootThreadEventId?.let {
                 RelationDefaultContent(
-                        type = RelationType.IO_THREAD,
+                        type = RelationType.THREAD,
                         eventId = it,
-                        inReplyTo = ReplyToContent(eventId = eventId, renderIn = if (showAsReply) arrayListOf("m.thread") else null))
+                        isFallingBack = showInThread,
+                        // False when is a rich reply from within a thread, and true when is a reply that should be visible from threads
+                        inReplyTo = ReplyToContent(eventId = eventId))
             } ?: RelationDefaultContent(null, null, ReplyToContent(eventId = eventId))
 
     private fun buildFormattedReply(permalink: String, userLink: String, userId: String, bodyFormatted: String, newBodyFormatted: String): String {
@@ -638,7 +662,9 @@ internal class LocalEchoEventFactory @Inject constructor(
             MessageType.MSGTYPE_AUDIO      -> return TextContent("sent an audio file.")
             MessageType.MSGTYPE_IMAGE      -> return TextContent("sent an image.")
             MessageType.MSGTYPE_VIDEO      -> return TextContent("sent a video.")
-            MessageType.MSGTYPE_POLL_START -> return TextContent((content as? MessagePollContent)?.pollCreationInfo?.question?.question ?: "")
+            MessageType.MSGTYPE_POLL_START -> {
+                return TextContent((content as? MessagePollContent)?.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "")
+            }
             else                           -> return TextContent(content?.body ?: "")
         }
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParser.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParser.kt
index ef7945cf8cc5416858e81cd7a5bbc66ff5696e72..05585a4cb5c6c138963fa993a65ad10a0a90ac51 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParser.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParser.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.send
 
 import org.commonmark.parser.Parser
 import org.commonmark.renderer.html.HtmlRenderer
+import org.matrix.android.sdk.api.util.TextContent
 import org.matrix.android.sdk.internal.session.room.AdvancedCommonmarkParser
 import org.matrix.android.sdk.internal.session.room.SimpleCommonmarkParser
 import org.matrix.android.sdk.internal.session.room.send.pills.TextPillsUtils
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt
index f44c255f1e0a1620acbdffcaa5abb94f580b8153..b59f1b174f2d16f7a321834aac1b0566bd1c10d3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt
@@ -29,7 +29,6 @@ import org.matrix.android.sdk.internal.session.room.timeline.TimelineSendEventWo
 import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
 import org.matrix.android.sdk.internal.worker.SessionWorkerParams
 import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
-import org.matrix.android.sdk.internal.worker.startChain
 import timber.log.Timber
 import javax.inject.Inject
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/TextContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/TextContentExtension.kt
similarity index 84%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/TextContent.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/TextContentExtension.kt
index 5c629f87f0e31608e79b6a6ee7a98dfcf2bf354d..8caa99d90a25798af721958d9d73099f17582215 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/TextContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/TextContentExtension.kt
@@ -24,18 +24,9 @@ import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultCon
 import org.matrix.android.sdk.api.session.room.model.relation.ReplyToContent
 import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromHtmlReply
 import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromReply
+import org.matrix.android.sdk.api.util.TextContent
 
-/**
- * Contains a text and eventually a formatted text
- */
-data class TextContent(
-        val text: String,
-        val formattedText: String? = null
-) {
-    fun takeFormatted() = formattedText ?: text
-}
-
-fun TextContent.toMessageTextContent(msgType: String = MessageType.MSGTYPE_TEXT): MessageTextContent {
+internal fun TextContent.toMessageTextContent(msgType: String = MessageType.MSGTYPE_TEXT): MessageTextContent {
     return MessageTextContent(
             msgType = msgType,
             format = MessageFormat.FORMAT_MATRIX_HTML.takeIf { formattedText != null },
@@ -49,7 +40,7 @@ fun TextContent.toMessageTextContent(msgType: String = MessageType.MSGTYPE_TEXT)
  * latestThreadEventId in order for the clients without threads enabled to render it appropriately
  * If latest event not found, we pass rootThreadEventId
  */
-fun TextContent.toThreadTextContent(
+internal fun TextContent.toThreadTextContent(
         rootThreadEventId: String,
         latestThreadEventId: String,
         msgType: String = MessageType.MSGTYPE_TEXT): MessageTextContent {
@@ -58,8 +49,9 @@ fun TextContent.toThreadTextContent(
             format = MessageFormat.FORMAT_MATRIX_HTML.takeIf { formattedText != null },
             body = text,
             relatesTo = RelationDefaultContent(
-                    type = RelationType.IO_THREAD,
+                    type = RelationType.THREAD,
                     eventId = rootThreadEventId,
+                    isFallingBack = true,
                     inReplyTo = ReplyToContent(
                             eventId = latestThreadEventId
                     )),
@@ -67,7 +59,7 @@ fun TextContent.toThreadTextContent(
     )
 }
 
-fun TextContent.removeInReplyFallbacks(): TextContent {
+internal fun TextContent.removeInReplyFallbacks(): TextContent {
     return copy(
             text = extractUsefulTextFromReply(this.text),
             formattedText = this.formattedText?.let { extractUsefulTextFromHtmlReply(it) }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt
index ccbfbfcded2ab238efe4fff13ee6f84f93b81bf5..fa2e0052abd8c197a934da65cb366074c438d2be 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt
@@ -16,6 +16,7 @@
 package org.matrix.android.sdk.internal.session.room.send.pills
 
 import android.text.SpannableString
+import org.matrix.android.sdk.api.session.permalinks.PermalinkService
 import org.matrix.android.sdk.api.session.room.send.MatrixItemSpan
 import org.matrix.android.sdk.api.util.MatrixItem
 import org.matrix.android.sdk.internal.session.displayname.DisplayNameResolver
@@ -28,7 +29,8 @@ import javax.inject.Inject
  */
 internal class TextPillsUtils @Inject constructor(
         private val mentionLinkSpecComparator: MentionLinkSpecComparator,
-        private val displayNameResolver: DisplayNameResolver
+        private val displayNameResolver: DisplayNameResolver,
+        private val permalinkService: PermalinkService
 ) {
 
     /**
@@ -36,7 +38,7 @@ internal class TextPillsUtils @Inject constructor(
      * @return the transformed String or null if no Span found
      */
     fun processSpecialSpansToHtml(text: CharSequence): String? {
-        return transformPills(text, MENTION_SPAN_TO_HTML_TEMPLATE)
+        return transformPills(text, permalinkService.createMentionSpanTemplate(PermalinkService.SpanTemplateType.HTML))
     }
 
     /**
@@ -44,7 +46,7 @@ internal class TextPillsUtils @Inject constructor(
      * @return the transformed String or null if no Span found
      */
     fun processSpecialSpansToMarkdown(text: CharSequence): String? {
-        return transformPills(text, MENTION_SPAN_TO_MD_TEMPLATE)
+        return transformPills(text, permalinkService.createMentionSpanTemplate(PermalinkService.SpanTemplateType.MARKDOWN))
     }
 
     private fun transformPills(text: CharSequence, template: String): String? {
@@ -108,10 +110,4 @@ internal class TextPillsUtils @Inject constructor(
             i++
         }
     }
-
-    companion object {
-        private const val MENTION_SPAN_TO_HTML_TEMPLATE = "<a href=\"https://matrix.to/#/%1\$s\">%2\$s</a>"
-
-        private const val MENTION_SPAN_TO_MD_TEMPLATE = "[%2\$s](https://matrix.to/#/%1\$s)"
-    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt
index 417417f439424341b29830d8d36995fdb61f9ff6..89d33f98d219a7d054d578e61bc9db4586064d11 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt
@@ -21,16 +21,20 @@ import androidx.lifecycle.LiveData
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
+import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.query.QueryStringValue
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
 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.GuestAccess
 import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent
 import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
 import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
 import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry
 import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
+import org.matrix.android.sdk.api.session.room.model.livelocation.BeaconInfo
+import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationBeaconContent
 import org.matrix.android.sdk.api.session.room.state.StateService
 import org.matrix.android.sdk.api.util.JsonDict
 import org.matrix.android.sdk.api.util.MimeTypes
@@ -186,4 +190,42 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
         }
         updateJoinRule(RoomJoinRules.RESTRICTED, null, allowEntries)
     }
+
+    override suspend fun stopLiveLocation(userId: String) {
+        getLiveLocationBeaconInfo(userId, true)?.let { beaconInfoStateEvent ->
+            beaconInfoStateEvent.getClearContent()?.toModel<LiveLocationBeaconContent>()?.let { content ->
+                val beaconContent = LiveLocationBeaconContent(
+                        unstableBeaconInfo = BeaconInfo(
+                                description = content.getBestBeaconInfo()?.description,
+                                timeout = content.getBestBeaconInfo()?.timeout,
+                                isLive = false,
+                        ),
+                        unstableTimestampAsMilliseconds = System.currentTimeMillis()
+                ).toContent()
+
+                beaconInfoStateEvent.stateKey?.let {
+                    sendStateEvent(
+                            eventType = EventType.STATE_ROOM_BEACON_INFO.first(),
+                            body = beaconContent,
+                            stateKey = it
+                    )
+                }
+            }
+        }
+    }
+
+    override suspend fun getLiveLocationBeaconInfo(userId: String, filterOnlyLive: Boolean): Event? {
+        return EventType.STATE_ROOM_BEACON_INFO
+                .mapNotNull {
+                    stateEventDataSource.getStateEvent(
+                            roomId = roomId,
+                            eventType = it,
+                            stateKey = QueryStringValue.Equals(userId)
+                    )
+                }
+                .firstOrNull { beaconInfoEvent ->
+                    !filterOnlyLive ||
+                            beaconInfoEvent.getClearContent()?.toModel<LiveLocationBeaconContent>()?.getBestBeaconInfo()?.isLive.orFalse()
+                }
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/StateEventDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/StateEventDataSource.kt
index 2114b9c5901c5cf5963a716e5442f2aa683cd327..42d6677409472eceb21143af01eabc79f5e8628d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/StateEventDataSource.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/StateEventDataSource.kt
@@ -32,7 +32,6 @@ import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
 import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields
 import org.matrix.android.sdk.internal.di.SessionDatabase
 import org.matrix.android.sdk.internal.query.QueryStringValueProcessor
-import org.matrix.android.sdk.internal.query.process
 import javax.inject.Inject
 
 internal class StateEventDataSource @Inject constructor(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/GraphUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/GraphUtils.kt
index b7e6548b54f8855a7e349b7243478b2e84006f9f..e3a215445db74897fe080aa22a32d705d8edb7a5 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/GraphUtils.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/GraphUtils.kt
@@ -18,16 +18,16 @@ package org.matrix.android.sdk.internal.session.room.summary
 
 import java.util.LinkedList
 
-data class GraphNode(
+internal data class GraphNode(
         val name: String
 )
 
-data class GraphEdge(
+internal data class GraphEdge(
         val source: GraphNode,
         val destination: GraphNode
 )
 
-class Graph {
+internal class Graph {
 
     private val adjacencyList: HashMap<GraphNode, ArrayList<GraphEdge>> = HashMap()
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt
index c9fc3c9575be324d9efcb63c07147fbc51ae6835..18a4f80547cb23e23914a686d466b8967bd0994c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt
@@ -26,6 +26,7 @@ import com.zhuinden.monarchy.Monarchy
 import io.realm.Realm
 import io.realm.RealmQuery
 import io.realm.kotlin.where
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.query.ActiveSpaceFilter
 import org.matrix.android.sdk.api.query.RoomCategoryFilter
 import org.matrix.android.sdk.api.query.isNormalized
@@ -42,6 +43,7 @@ import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotification
 import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams
 import org.matrix.android.sdk.api.util.Optional
 import org.matrix.android.sdk.api.util.toOptional
+import org.matrix.android.sdk.internal.database.RealmSessionProvider
 import org.matrix.android.sdk.internal.database.mapper.RoomSummaryMapper
 import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
 import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
@@ -55,8 +57,10 @@ import javax.inject.Inject
 
 internal class RoomSummaryDataSource @Inject constructor(
         @SessionDatabase private val monarchy: Monarchy,
+        private val realmSessionProvider: RealmSessionProvider,
         private val roomSummaryMapper: RoomSummaryMapper,
-        private val queryStringValueProcessor: QueryStringValueProcessor
+        private val queryStringValueProcessor: QueryStringValueProcessor,
+        private val coroutineDispatchers: MatrixCoroutineDispatchers
 ) {
 
     fun getRoomSummary(roomIdOrAlias: String): RoomSummary? {
@@ -219,14 +223,25 @@ internal class RoomSummaryDataSource @Inject constructor(
         return object : UpdatableLivePageResult {
             override val livePagedList: LiveData<PagedList<RoomSummary>> = mapped
 
-            override fun updateQuery(builder: (RoomSummaryQueryParams) -> RoomSummaryQueryParams) {
-                realmDataSourceFactory.updateQuery {
-                    roomSummariesQuery(it, builder.invoke(queryParams)).process(sortOrder)
-                }
-            }
-
             override val liveBoundaries: LiveData<ResultBoundaries>
                 get() = boundaries
+
+            override var queryParams: RoomSummaryQueryParams = queryParams
+                set(value) {
+                    field = value
+                    realmDataSourceFactory.updateQuery {
+                        roomSummariesQuery(it, value).process(sortOrder)
+                    }
+                }
+        }
+    }
+
+    fun getCountLive(queryParams: RoomSummaryQueryParams): LiveData<Int> {
+        val liveRooms = monarchy.findAllManagedWithChanges {
+            roomSummariesQuery(it, queryParams)
+        }
+        return Transformations.map(liveRooms) {
+            it.realmResults.where().count().toInt()
         }
     }
 
@@ -293,6 +308,7 @@ internal class RoomSummaryDataSource @Inject constructor(
             RoomCategoryFilter.ONLY_ROOMS              -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, false)
             RoomCategoryFilter.ONLY_WITH_NOTIFICATIONS -> query.greaterThan(RoomSummaryEntityFields.NOTIFICATION_COUNT, 0)
             RoomCategoryFilter.ALL                     -> Unit // nop
+            null                                       -> Unit
         }
 
         // Timber.w("VAL: activeSpaceId : ${queryParams.activeSpaceId}")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt
index 1c1d59fb3d23f119b0d5d87426adf1244b156bb1..3af579d0507bed867b1dfb4c448d7d11c604d333 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt
@@ -18,9 +18,11 @@ package org.matrix.android.sdk.internal.session.room.summary
 
 import io.realm.Realm
 import io.realm.kotlin.createObject
+import kotlinx.coroutines.runBlocking
 import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.content.EncryptionEventContent
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataTypes
 import org.matrix.android.sdk.api.session.room.model.Membership
@@ -39,7 +41,6 @@ import org.matrix.android.sdk.api.session.sync.model.RoomSyncSummary
 import org.matrix.android.sdk.api.session.sync.model.RoomSyncUnreadNotifications
 import org.matrix.android.sdk.internal.crypto.EventDecryptor
 import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService
-import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent
 import org.matrix.android.sdk.internal.database.mapper.ContentMapper
 import org.matrix.android.sdk.internal.database.mapper.asDomain
 import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
@@ -63,7 +64,6 @@ import org.matrix.android.sdk.internal.session.room.accountdata.RoomAccountDataD
 import org.matrix.android.sdk.internal.session.room.membership.RoomDisplayNameResolver
 import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
 import org.matrix.android.sdk.internal.session.room.relationship.RoomChildRelationInfo
-import org.matrix.android.sdk.internal.util.Normalizer
 import timber.log.Timber
 import javax.inject.Inject
 import kotlin.system.measureTimeMillis
@@ -74,8 +74,16 @@ internal class RoomSummaryUpdater @Inject constructor(
         private val roomAvatarResolver: RoomAvatarResolver,
         private val eventDecryptor: EventDecryptor,
         private val crossSigningService: DefaultCrossSigningService,
-        private val roomAccountDataDataSource: RoomAccountDataDataSource,
-        private val normalizer: Normalizer) {
+        private val roomAccountDataDataSource: RoomAccountDataDataSource
+) {
+
+    fun refreshLatestPreviewContent(realm: Realm, roomId: String) {
+        val roomSummaryEntity = RoomSummaryEntity.getOrNull(realm, roomId)
+        if (roomSummaryEntity != null) {
+            val latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
+            latestPreviewableEvent?.attemptToDecrypt()
+        }
+    }
 
     fun update(realm: Realm,
                roomId: String,
@@ -127,6 +135,7 @@ internal class RoomSummaryUpdater @Inject constructor(
         val lastActivityFromEvent = latestPreviewableEvent?.root?.originServerTs
         if (lastActivityFromEvent != null) {
             roomSummaryEntity.lastActivityTime = lastActivityFromEvent
+            latestPreviewableEvent.attemptToDecrypt()
         }
 
         roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 ||
@@ -160,16 +169,6 @@ internal class RoomSummaryUpdater @Inject constructor(
         }
         roomSummaryEntity.updateHasFailedSending()
 
-        val root = latestPreviewableEvent?.root
-        if (root?.type == EventType.ENCRYPTED && root.decryptionResultJson == null) {
-            Timber.v("Should decrypt ${latestPreviewableEvent.eventId}")
-            // mmm i want to decrypt now or is it ok to do it async?
-            tryOrNull {
-                eventDecryptor.decryptEvent(root.asDomain(), "")
-            }
-                    ?.let { root.setDecryptionResult(it) }
-        }
-
         if (updateMembers) {
             val otherRoomMembers = RoomMemberHelper(realm, roomId)
                     .queryActiveRoomMembersEvent()
@@ -186,6 +185,22 @@ internal class RoomSummaryUpdater @Inject constructor(
         }
     }
 
+    private fun TimelineEventEntity.attemptToDecrypt() {
+        when (val root = this.root) {
+            null -> {
+                Timber.v("Decryption skipped due to missing root event $eventId")
+            }
+            else -> {
+                if (root.type == EventType.ENCRYPTED && root.decryptionResultJson == null) {
+                    Timber.v("Should decrypt $eventId")
+                    tryOrNull {
+                        runBlocking { eventDecryptor.decryptEvent(root.asDomain(), "") }
+                    }?.let { root.setDecryptionResult(it) }
+                }
+            }
+        }
+    }
+
     private fun RoomSummaryEntity.updateHasFailedSending() {
         hasFailedSending = TimelineEventEntity.findAllInRoomWithSendStates(realm, roomId, SendState.HAS_FAILED_STATES).isNotEmpty()
     }
@@ -411,8 +426,6 @@ internal class RoomSummaryUpdater @Inject constructor(
                         realm.where(RoomSummaryEntity::class.java)
                                 .process(RoomSummaryEntityFields.MEMBERSHIP_STR, listOf(Membership.JOIN))
                                 .notEqualTo(RoomSummaryEntityFields.ROOM_TYPE, RoomType.SPACE)
-                                // also we do not count DM in here, because home space will already show them
-                                .equalTo(RoomSummaryEntityFields.IS_DIRECT, false)
                                 .contains(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, space.roomId)
                                 .findAll().forEach {
                                     highlightCount += it.highlightCount
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt
index 5967ae8d2edd2a11d7653fe52ea95565ccd75364..b65991347dd3693edab907d8c211243e3c72c544 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/DefaultThreadsService.kt
@@ -23,25 +23,25 @@ import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import io.realm.Realm
 import org.matrix.android.sdk.api.session.room.threads.ThreadsService
-import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
-import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
-import org.matrix.android.sdk.internal.database.helper.findAllLocalThreadNotificationsForRoomId
+import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary
+import org.matrix.android.sdk.internal.database.helper.enhanceWithEditions
 import org.matrix.android.sdk.internal.database.helper.findAllThreadsForRoomId
-import org.matrix.android.sdk.internal.database.helper.isUserParticipatingInThread
-import org.matrix.android.sdk.internal.database.helper.mapEventsWithEdition
+import org.matrix.android.sdk.internal.database.mapper.ThreadSummaryMapper
 import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
-import org.matrix.android.sdk.internal.database.model.EventEntity
-import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
-import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
 import org.matrix.android.sdk.internal.di.SessionDatabase
 import org.matrix.android.sdk.internal.di.UserId
-import org.matrix.android.sdk.internal.util.awaitTransaction
+import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadSummariesTask
+import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
 
 internal class DefaultThreadsService @AssistedInject constructor(
         @Assisted private val roomId: String,
         @UserId private val userId: String,
+        private val fetchThreadTimelineTask: FetchThreadTimelineTask,
+        private val fetchThreadSummariesTask: FetchThreadSummariesTask,
         @SessionDatabase private val monarchy: Monarchy,
         private val timelineEventMapper: TimelineEventMapper,
+        private val threadSummaryMapper: ThreadSummaryMapper
 ) : ThreadsService {
 
     @AssistedFactory
@@ -49,55 +49,40 @@ internal class DefaultThreadsService @AssistedInject constructor(
         fun create(roomId: String): DefaultThreadsService
     }
 
-    override fun getMarkedThreadNotificationsLive(): LiveData<List<TimelineEvent>> {
+    override fun getAllThreadSummariesLive(): LiveData<List<ThreadSummary>> {
         return monarchy.findAllMappedWithChanges(
-                { TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) },
-                { timelineEventMapper.map(it) }
+                { ThreadSummaryEntity.findAllThreadsForRoomId(it, roomId = roomId) },
+                {
+                    threadSummaryMapper.map(it)
+                }
         )
     }
 
-    override fun getMarkedThreadNotifications(): List<TimelineEvent> {
+    override fun getAllThreadSummaries(): List<ThreadSummary> {
         return monarchy.fetchAllMappedSync(
-                { TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) },
-                { timelineEventMapper.map(it) }
+                { ThreadSummaryEntity.findAllThreadsForRoomId(it, roomId = roomId) },
+                { threadSummaryMapper.map(it) }
         )
     }
 
-    override fun getAllThreadsLive(): LiveData<List<TimelineEvent>> {
-        return monarchy.findAllMappedWithChanges(
-                { TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) },
-                { timelineEventMapper.map(it) }
-        )
-    }
-
-    override fun getAllThreads(): List<TimelineEvent> {
-        return monarchy.fetchAllMappedSync(
-                { TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) },
-                { timelineEventMapper.map(it) }
-        )
-    }
-
-    override fun isUserParticipatingInThread(rootThreadEventId: String): Boolean {
+    override fun enhanceThreadWithEditions(threads: List<ThreadSummary>): List<ThreadSummary> {
         return Realm.getInstance(monarchy.realmConfiguration).use {
-            TimelineEventEntity.isUserParticipatingInThread(
-                    realm = it,
-                    roomId = roomId,
-                    rootThreadEventId = rootThreadEventId,
-                    senderId = userId)
+            threads.enhanceWithEditions(it, roomId)
         }
     }
 
-    override fun mapEventsWithEdition(threads: List<TimelineEvent>): List<TimelineEvent> {
-        return Realm.getInstance(monarchy.realmConfiguration).use {
-            threads.mapEventsWithEdition(it, roomId)
-        }
+    override suspend fun fetchThreadTimeline(rootThreadEventId: String, from: String, limit: Int) {
+        fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params(
+                roomId = roomId,
+                rootThreadEventId = rootThreadEventId,
+                from = from,
+                limit = limit
+        ))
     }
 
-    override suspend fun markThreadAsRead(rootThreadEventId: String) {
-        monarchy.awaitTransaction {
-            EventEntity.where(
-                    realm = it,
-                    eventId = rootThreadEventId).findFirst()?.threadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE
-        }
+    override suspend fun fetchThreadSummaries() {
+        fetchThreadSummariesTask.execute(FetchThreadSummariesTask.Params(
+                roomId = roomId
+        ))
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/local/DefaultThreadsLocalService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/local/DefaultThreadsLocalService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..3bc36fb2a804c8b9dfc0452a1f63d5a3e774dfbf
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/threads/local/DefaultThreadsLocalService.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2022 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.room.threads.local
+
+import androidx.lifecycle.LiveData
+import com.zhuinden.monarchy.Monarchy
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import io.realm.Realm
+import org.matrix.android.sdk.api.session.room.threads.local.ThreadsLocalService
+import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
+import org.matrix.android.sdk.internal.database.helper.findAllLocalThreadNotificationsForRoomId
+import org.matrix.android.sdk.internal.database.helper.findAllThreadsForRoomId
+import org.matrix.android.sdk.internal.database.helper.isUserParticipatingInThread
+import org.matrix.android.sdk.internal.database.helper.mapEventsWithEdition
+import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
+import org.matrix.android.sdk.internal.database.model.EventEntity
+import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
+import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.di.SessionDatabase
+import org.matrix.android.sdk.internal.di.UserId
+import org.matrix.android.sdk.internal.util.awaitTransaction
+
+internal class DefaultThreadsLocalService @AssistedInject constructor(
+        @Assisted private val roomId: String,
+        @UserId private val userId: String,
+        @SessionDatabase private val monarchy: Monarchy,
+        private val timelineEventMapper: TimelineEventMapper,
+) : ThreadsLocalService {
+
+    @AssistedFactory
+    interface Factory {
+        fun create(roomId: String): DefaultThreadsLocalService
+    }
+
+    override fun getMarkedThreadNotificationsLive(): LiveData<List<TimelineEvent>> {
+        return monarchy.findAllMappedWithChanges(
+                { TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) },
+                { timelineEventMapper.map(it) }
+        )
+    }
+
+    override fun getMarkedThreadNotifications(): List<TimelineEvent> {
+        return monarchy.fetchAllMappedSync(
+                { TimelineEventEntity.findAllLocalThreadNotificationsForRoomId(it, roomId = roomId) },
+                { timelineEventMapper.map(it) }
+        )
+    }
+
+    override fun getAllThreadsLive(): LiveData<List<TimelineEvent>> {
+        return monarchy.findAllMappedWithChanges(
+                { TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) },
+                { timelineEventMapper.map(it) }
+        )
+    }
+
+    override fun getAllThreads(): List<TimelineEvent> {
+        return monarchy.fetchAllMappedSync(
+                { TimelineEventEntity.findAllThreadsForRoomId(it, roomId = roomId) },
+                { timelineEventMapper.map(it) }
+        )
+    }
+
+    override fun isUserParticipatingInThread(rootThreadEventId: String): Boolean {
+        return Realm.getInstance(monarchy.realmConfiguration).use {
+            TimelineEventEntity.isUserParticipatingInThread(
+                    realm = it,
+                    roomId = roomId,
+                    rootThreadEventId = rootThreadEventId,
+                    senderId = userId)
+        }
+    }
+
+    override fun mapEventsWithEdition(threads: List<TimelineEvent>): List<TimelineEvent> {
+        return Realm.getInstance(monarchy.realmConfiguration).use {
+            threads.mapEventsWithEdition(it, roomId)
+        }
+    }
+
+    override suspend fun markThreadAsRead(rootThreadEventId: String) {
+        monarchy.awaitTransaction {
+            EventEntity.where(
+                    realm = it,
+                    eventId = rootThreadEventId).findFirst()?.threadNotificationState = ThreadNotificationState.NO_NEW_MESSAGE
+        }
+    }
+}
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 3dd4225b2c3fdee5e12c4062ff875f61d70294bf..5064ebf49b3d36696726277fef5c5a95309f5ca3 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
@@ -35,9 +35,10 @@ import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.session.room.timeline.Timeline
 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.internal.database.lightweight.LightweightSettingsStorage
+import org.matrix.android.sdk.api.settings.LightweightSettingsStorage
 import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
 import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
+import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
 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.SemaphoreCoroutineSequencer
@@ -58,6 +59,7 @@ internal class DefaultTimeline(private val roomId: String,
                                paginationTask: PaginationTask,
                                getEventTask: GetContextOfEventTask,
                                fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
+                               fetchThreadTimelineTask: FetchThreadTimelineTask,
                                timelineEventMapper: TimelineEventMapper,
                                timelineInput: TimelineInput,
                                threadsAwarenessHandler: ThreadsAwarenessHandler,
@@ -89,7 +91,9 @@ internal class DefaultTimeline(private val roomId: String,
             realm = backgroundRealm,
             eventDecryptor = eventDecryptor,
             paginationTask = paginationTask,
+            realmConfiguration = realmConfiguration,
             fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
+            fetchThreadTimelineTask = fetchThreadTimelineTask,
             getContextOfEventTask = getEventTask,
             timelineInput = timelineInput,
             timelineEventMapper = timelineEventMapper,
@@ -219,7 +223,15 @@ internal class DefaultTimeline(private val roomId: String,
         updateState(direction) {
             it.copy(loading = true)
         }
-        val loadMoreResult = strategy.loadMore(count, direction, fetchOnServerIfNeeded)
+        val loadMoreResult = try {
+            strategy.loadMore(count, direction, fetchOnServerIfNeeded)
+        } catch (throwable: Throwable) {
+            // Timeline could not be loaded with a (likely) permanent issue, such as the
+            // server now knowing the initialEventId, so we want to show an error message
+            // and possibly restart without initialEventId.
+            onTimelineFailure(throwable)
+            return false
+        }
         Timber.v("$baseLogMessage: result $loadMoreResult")
         val hasMoreToLoad = loadMoreResult != LoadMoreResult.REACHED_END
         updateState(direction) {
@@ -297,7 +309,13 @@ internal class DefaultTimeline(private val roomId: String,
         Timber.v("Post snapshot of ${snapshot.size} events")
         withContext(coroutineDispatchers.main) {
             listeners.forEach {
-                tryOrNull { it.onTimelineUpdated(snapshot) }
+                if (initialEventId != null && isFromThreadTimeline && snapshot.firstOrNull { it.eventId == initialEventId } == null) {
+                    // We are in a thread timeline with a permalink, post update timeline only when the appropriate message have been found
+                    tryOrNull { it.onTimelineUpdated(arrayListOf()) }
+                } else {
+                    // In all the other cases update timeline as expected
+                    tryOrNull { it.onTimelineUpdated(snapshot) }
+                }
             }
         }
     }
@@ -332,6 +350,14 @@ internal class DefaultTimeline(private val roomId: String,
         }
     }
 
+    private fun onTimelineFailure(throwable: Throwable) {
+        timelineScope.launch(coroutineDispatchers.main) {
+            listeners.forEach {
+                tryOrNull { it.onTimelineFailure(throwable) }
+            }
+        }
+    }
+
     private fun buildStrategy(mode: LoadTimelineStrategy.Mode): LoadTimelineStrategy {
         return LoadTimelineStrategy(
                 roomId = roomId,
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 8094fee504ff535d9b78aa152e8e7b2c0eb5c50d..826c9d7c48ef60800c13cdd3db5e355ba61d557d 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
@@ -26,11 +26,12 @@ import org.matrix.android.sdk.api.session.room.timeline.Timeline
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
 import org.matrix.android.sdk.api.session.room.timeline.TimelineService
 import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
+import org.matrix.android.sdk.api.settings.LightweightSettingsStorage
 import org.matrix.android.sdk.api.util.Optional
-import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
 import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
 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.room.relation.threads.FetchThreadTimelineTask
 import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler
 import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
 
@@ -42,6 +43,7 @@ internal class DefaultTimelineService @AssistedInject constructor(
         private val eventDecryptor: TimelineEventDecryptor,
         private val paginationTask: PaginationTask,
         private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
+        private val fetchThreadTimelineTask: FetchThreadTimelineTask,
         private val timelineEventMapper: TimelineEventMapper,
         private val loadRoomMembersTask: LoadRoomMembersTask,
         private val threadsAwarenessHandler: ThreadsAwarenessHandler,
@@ -64,10 +66,11 @@ internal class DefaultTimelineService @AssistedInject constructor(
                 realmConfiguration = monarchy.realmConfiguration,
                 coroutineDispatchers = coroutineDispatchers,
                 paginationTask = paginationTask,
+                fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
                 timelineEventMapper = timelineEventMapper,
                 timelineInput = timelineInput,
                 eventDecryptor = eventDecryptor,
-                fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
+                fetchThreadTimelineTask = fetchThreadTimelineTask,
                 loadRoomMembersTask = loadRoomMembersTask,
                 readReceiptHandler = readReceiptHandler,
                 getEventTask = contextOfEventTask,
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 9ede2f6562f260ffcd35c2ee5691dc3329463839..5bca5118b82e20478569e6a7500348a37ee4f28b 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
@@ -17,9 +17,9 @@
 package org.matrix.android.sdk.internal.session.room.timeline
 
 import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.internal.crypto.EventDecryptor
-import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
 import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
 import org.matrix.android.sdk.internal.network.executeRequest
 import org.matrix.android.sdk.internal.session.room.RoomAPI
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
index f332c4a35f608fd367b7c8d02b200ac331899ca9..ff986d04af15e43262112340518d49bff5c76c6d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
@@ -19,20 +19,30 @@ package org.matrix.android.sdk.internal.session.room.timeline
 import io.realm.OrderedCollectionChangeSet
 import io.realm.OrderedRealmCollectionChangeListener
 import io.realm.Realm
+import io.realm.RealmConfiguration
 import io.realm.RealmResults
+import io.realm.kotlin.createObject
 import kotlinx.coroutines.CompletableDeferred
 import org.matrix.android.sdk.api.extensions.orFalse
+import org.matrix.android.sdk.api.failure.Failure
+import org.matrix.android.sdk.api.failure.MatrixError
 import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.api.session.room.timeline.Timeline
 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.internal.database.lightweight.LightweightSettingsStorage
+import org.matrix.android.sdk.api.settings.LightweightSettingsStorage
+import org.matrix.android.sdk.internal.database.helper.addIfNecessary
 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.ChunkEntityFields
+import org.matrix.android.sdk.internal.database.model.RoomEntity
+import org.matrix.android.sdk.internal.database.model.deleteAndClearThreadEvents
 import org.matrix.android.sdk.internal.database.query.findAllIncludingEvents
+import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfThread
 import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
 import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
+import timber.log.Timber
 import java.util.concurrent.atomic.AtomicReference
 
 /**
@@ -76,6 +86,8 @@ internal class LoadTimelineStrategy(
             val realm: AtomicReference<Realm>,
             val eventDecryptor: TimelineEventDecryptor,
             val paginationTask: PaginationTask,
+            val realmConfiguration: RealmConfiguration,
+            val fetchThreadTimelineTask: FetchThreadTimelineTask,
             val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
             val getContextOfEventTask: GetContextOfEventTask,
             val timelineInput: TimelineInput,
@@ -90,7 +102,6 @@ internal class LoadTimelineStrategy(
     private var getContextLatch: CompletableDeferred<Unit>? = null
     private var chunkEntity: RealmResults<ChunkEntity>? = null
     private var timelineChunk: TimelineChunk? = null
-
     private val chunkEntityListener = OrderedRealmCollectionChangeListener { _: RealmResults<ChunkEntity>, changeSet: OrderedCollectionChangeSet ->
         // Can be call either when you open a permalink on an unknown event
         // or when there is a gap in the timeline.
@@ -170,6 +181,9 @@ internal class LoadTimelineStrategy(
         getContextLatch?.cancel()
         chunkEntity = null
         timelineChunk = null
+        if (mode is Mode.Thread) {
+            clearThreadChunkEntity(dependencies.realm.get(), mode.rootThreadEventId)
+        }
     }
 
     suspend fun loadMore(count: Int, direction: Timeline.Direction, fetchOnServerIfNeeded: Boolean = true): LoadMoreResult {
@@ -182,9 +196,16 @@ internal class LoadTimelineStrategy(
                 getContextLatch?.await()
                 getContextLatch = null
             } catch (failure: Throwable) {
+                if (failure is Failure.ServerError && failure.error.code in listOf(MatrixError.M_NOT_FOUND, MatrixError.M_FORBIDDEN)) {
+                    // This failure is likely permanent, so handle in DefaultTimeline to restart without eventId
+                    throw failure
+                }
                 return LoadMoreResult.FAILURE
             }
         }
+        if (mode is Mode.Thread) {
+            return timelineChunk?.loadMoreThread(count) ?: LoadMoreResult.FAILURE
+        }
         return timelineChunk?.loadMore(count, direction, fetchOnServerIfNeeded) ?: LoadMoreResult.FAILURE
     }
 
@@ -201,7 +222,7 @@ internal class LoadTimelineStrategy(
     }
 
     private fun buildSendingEvents(): List<TimelineEvent> {
-        return if (hasReachedLastForward()) {
+        return if (hasReachedLastForward() || mode is Mode.Thread) {
             sendingEventsDataSource.buildSendingEvents()
         } else {
             emptyList()
@@ -219,13 +240,47 @@ internal class LoadTimelineStrategy(
                 ChunkEntity.findAllIncludingEvents(realm, listOf(mode.originEventId))
             }
             is Mode.Thread    -> {
+                recreateThreadChunkEntity(realm, mode.rootThreadEventId)
                 ChunkEntity.where(realm, roomId)
-                        .equalTo(ChunkEntityFields.IS_LAST_FORWARD, true)
+                        .equalTo(ChunkEntityFields.ROOT_THREAD_EVENT_ID, mode.rootThreadEventId)
+                        .equalTo(ChunkEntityFields.IS_LAST_FORWARD_THREAD, true)
                         .findAll()
             }
         }
     }
 
+    /**
+     * Clear any existing thread chunk entity and create a new one, with the
+     * rootThreadEventId included
+     */
+    private fun recreateThreadChunkEntity(realm: Realm, rootThreadEventId: String) {
+        realm.executeTransaction {
+            // Lets delete the chunk and start a new one
+            ChunkEntity.findLastForwardChunkOfThread(it, roomId, rootThreadEventId)?.deleteAndClearThreadEvents()?.let {
+                Timber.i("###THREADS LoadTimelineStrategy [onStart] thread chunk cleared..")
+            }
+            val threadChunk = it.createObject<ChunkEntity>().apply {
+                Timber.i("###THREADS LoadTimelineStrategy [onStart] Created new thread chunk with rootThreadEventId: $rootThreadEventId")
+                this.rootThreadEventId = rootThreadEventId
+                this.isLastForwardThread = true
+            }
+            if (threadChunk.isValid) {
+                RoomEntity.where(it, roomId).findFirst()?.addIfNecessary(threadChunk)
+            }
+        }
+    }
+
+    /**
+     * Clear any existing thread chunk
+     */
+    private fun clearThreadChunkEntity(realm: Realm, rootThreadEventId: String) {
+        realm.executeTransaction {
+            ChunkEntity.findLastForwardChunkOfThread(it, roomId, rootThreadEventId)?.deleteAndClearThreadEvents()?.let {
+                Timber.i("###THREADS LoadTimelineStrategy [onStop] thread chunk cleared..")
+            }
+        }
+    }
+
     private fun hasReachedLastForward(): Boolean {
         return timelineChunk?.hasReachedLastForward().orFalse()
     }
@@ -237,8 +292,10 @@ internal class LoadTimelineStrategy(
                     timelineSettings = dependencies.timelineSettings,
                     roomId = roomId,
                     timelineId = timelineId,
+                    fetchThreadTimelineTask = dependencies.fetchThreadTimelineTask,
                     eventDecryptor = dependencies.eventDecryptor,
                     paginationTask = dependencies.paginationTask,
+                    realmConfiguration = dependencies.realmConfiguration,
                     fetchTokenAndPaginateTask = dependencies.fetchTokenAndPaginateTask,
                     timelineEventMapper = dependencies.timelineEventMapper,
                     uiEchoManager = uiEchoManager,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt
index 1262c09d974b1667baaa1c0291e59596857a5b3c..637267a9b17c3c5ff066998a130286e43cdd9a92 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/SendingEventsDataSource.kt
@@ -45,9 +45,11 @@ internal class RealmSendingEventsDataSource(
     private var frozenSendingTimelineEvents: RealmList<TimelineEventEntity>? = null
 
     private val sendingTimelineEventsListener = RealmChangeListener<RealmList<TimelineEventEntity>> { events ->
-        uiEchoManager.onSentEventsInDatabase(events.map { it.eventId })
-        updateFrozenResults(events)
-        onEventsUpdated(false)
+        if (events.isValid) {
+            uiEchoManager.onSentEventsInDatabase(events.map { it.eventId })
+            updateFrozenResults(events)
+            onEventsUpdated(false)
+        }
     }
 
     override fun start() {
@@ -55,6 +57,7 @@ internal class RealmSendingEventsDataSource(
         roomEntity = RoomEntity.where(safeRealm, roomId = roomId).findFirst()
         sendingTimelineEvents = roomEntity?.sendingTimelineEvents
         sendingTimelineEvents?.addChangeListener(sendingTimelineEventsListener)
+        updateFrozenResults(sendingTimelineEvents)
     }
 
     override fun stop() {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
index c0dc31fcf8a988fec4c068ccdd0afc05354cc8d0..4ead1d4ecba2805ca9e9622fd7dffdd1254b6cf0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.timeline
 
 import io.realm.OrderedCollectionChangeSet
 import io.realm.OrderedRealmCollectionChangeListener
+import io.realm.RealmConfiguration
 import io.realm.RealmObjectChangeListener
 import io.realm.RealmQuery
 import io.realm.RealmResults
@@ -29,13 +30,15 @@ import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.room.timeline.Timeline
 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.internal.database.lightweight.LightweightSettingsStorage
+import org.matrix.android.sdk.api.settings.LightweightSettingsStorage
 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.ChunkEntityFields
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
+import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadTimelineTask
+import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
 import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler
 import timber.log.Timber
 import java.util.Collections
@@ -50,8 +53,10 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
                              private val timelineSettings: TimelineSettings,
                              private val roomId: String,
                              private val timelineId: String,
+                             private val fetchThreadTimelineTask: FetchThreadTimelineTask,
                              private val eventDecryptor: TimelineEventDecryptor,
                              private val paginationTask: PaginationTask,
+                             private val realmConfiguration: RealmConfiguration,
                              private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
                              private val timelineEventMapper: TimelineEventMapper,
                              private val uiEchoManager: UIEchoManager? = null,
@@ -78,11 +83,15 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
             isLastBackward.set(chunkEntity.isLastBackward)
         }
         if (changeSet.isFieldChanged(ChunkEntityFields.NEXT_CHUNK.`$`)) {
-            nextChunk = createTimelineChunk(chunkEntity.nextChunk)
+            nextChunk = createTimelineChunk(chunkEntity.nextChunk).also {
+                it?.prevChunk = this
+            }
             nextChunkLatch?.complete(Unit)
         }
         if (changeSet.isFieldChanged(ChunkEntityFields.PREV_CHUNK.`$`)) {
-            prevChunk = createTimelineChunk(chunkEntity.prevChunk)
+            prevChunk = createTimelineChunk(chunkEntity.prevChunk).also {
+                it?.nextChunk = this
+            }
             prevChunkLatch?.complete(Unit)
         }
     }
@@ -141,29 +150,57 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
         val loadFromStorage = loadFromStorage(count, direction).also {
             logLoadedFromStorage(it, direction)
         }
+        if (loadFromStorage.numberOfEvents == 6) {
+            Timber.i("here")
+        }
 
         val offsetCount = count - loadFromStorage.numberOfEvents
 
-        return if (direction == Timeline.Direction.FORWARDS && isLastForward.get()) {
+        return if (offsetCount == 0) {
+            LoadMoreResult.SUCCESS
+        } else if (direction == Timeline.Direction.FORWARDS && isLastForward.get()) {
             LoadMoreResult.REACHED_END
         } else if (direction == Timeline.Direction.BACKWARDS && isLastBackward.get()) {
             LoadMoreResult.REACHED_END
         } else if (timelineSettings.isThreadTimeline() && loadFromStorage.threadReachedEnd) {
             LoadMoreResult.REACHED_END
-        } else if (offsetCount == 0) {
-            LoadMoreResult.SUCCESS
         } else {
             delegateLoadMore(fetchOnServerIfNeeded, offsetCount, direction)
         }
     }
 
+    /**
+     * This function will fetch more live thread timeline events using the /relations api. It will
+     * always fetch results, while we want our data to be up to dated.
+     */
+    suspend fun loadMoreThread(count: Int, direction: Timeline.Direction = Timeline.Direction.BACKWARDS): LoadMoreResult {
+        val rootThreadEventId = timelineSettings.rootThreadEventId ?: return LoadMoreResult.FAILURE
+        return if (direction == Timeline.Direction.BACKWARDS) {
+            try {
+                fetchThreadTimelineTask.execute(FetchThreadTimelineTask.Params(
+                        roomId,
+                        rootThreadEventId,
+                        chunkEntity.prevToken,
+                        count
+                )).toLoadMoreResult()
+            } catch (failure: Throwable) {
+                Timber.e(failure, "Failed to fetch thread timeline events from the server")
+                LoadMoreResult.FAILURE
+            }
+        } else {
+            LoadMoreResult.FAILURE
+        }
+    }
+
     private suspend fun delegateLoadMore(fetchFromServerIfNeeded: Boolean, offsetCount: Int, direction: Timeline.Direction): LoadMoreResult {
         return if (direction == Timeline.Direction.FORWARDS) {
             val nextChunkEntity = chunkEntity.nextChunk
             when {
                 nextChunkEntity != null -> {
                     if (nextChunk == null) {
-                        nextChunk = createTimelineChunk(nextChunkEntity)
+                        nextChunk = createTimelineChunk(nextChunkEntity).also {
+                            it?.prevChunk = this
+                        }
                     }
                     nextChunk?.loadMore(offsetCount, direction, fetchFromServerIfNeeded) ?: LoadMoreResult.FAILURE
                 }
@@ -179,7 +216,9 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
             when {
                 prevChunkEntity != null -> {
                     if (prevChunk == null) {
-                        prevChunk = createTimelineChunk(prevChunkEntity)
+                        prevChunk = createTimelineChunk(prevChunkEntity).also {
+                            it?.nextChunk = this
+                        }
                     }
                     prevChunk?.loadMore(offsetCount, direction, fetchFromServerIfNeeded) ?: LoadMoreResult.FAILURE
                 }
@@ -413,6 +452,14 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
         }
     }
 
+    private fun DefaultFetchThreadTimelineTask.Result.toLoadMoreResult(): LoadMoreResult {
+        return when (this) {
+            DefaultFetchThreadTimelineTask.Result.REACHED_END -> LoadMoreResult.REACHED_END
+            DefaultFetchThreadTimelineTask.Result.SHOULD_FETCH_MORE,
+            DefaultFetchThreadTimelineTask.Result.SUCCESS     -> LoadMoreResult.SUCCESS
+        }
+    }
+
     private fun getOffsetIndex(): Int {
         var offset = 0
         var currentNextChunk = nextChunk
@@ -454,6 +501,7 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
                 }
             }
         }
+
         if (insertions.isNotEmpty() || modifications.isNotEmpty()) {
             onBuiltEvents(true)
         }
@@ -487,6 +535,8 @@ internal class TimelineChunk(private val chunkEntity: ChunkEntity,
                 timelineId = timelineId,
                 eventDecryptor = eventDecryptor,
                 paginationTask = paginationTask,
+                realmConfiguration = realmConfiguration,
+                fetchThreadTimelineTask = fetchThreadTimelineTask,
                 fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
                 timelineEventMapper = timelineEventMapper,
                 uiEchoManager = uiEchoManager,
@@ -508,13 +558,18 @@ private fun RealmQuery<TimelineEventEntity>.offsets(
         count: Int,
         startDisplayIndex: Int
 ): RealmQuery<TimelineEventEntity> {
-    sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
-    if (direction == Timeline.Direction.BACKWARDS) {
+    return if (direction == Timeline.Direction.BACKWARDS) {
         lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex)
+        sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
+        limit(count.toLong())
     } else {
         greaterThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, startDisplayIndex)
+        // We need to sort ascending first so limit works in the right direction
+        sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)
+        limit(count.toLong())
+        // Result is expected to be sorted descending
+        sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
     }
-    return limit(count.toLong())
 }
 
 private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {
@@ -533,7 +588,6 @@ private fun ChunkEntity.sortedTimelineEvents(rootThreadEventId: String?): RealmR
                 .or()
                 .equalTo(TimelineEventEntityFields.ROOT.EVENT_ID, rootThreadEventId)
                 .endGroup()
-                .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
                 .findAll()
     }
 }
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 49a8a8b55a33279e3a5abd98c6707b7eaf96d84b..5c30dc20d9df8e44ef8dc366feb0633ea2fd7b0c 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
@@ -17,13 +17,13 @@ package org.matrix.android.sdk.internal.session.room.timeline
 
 import io.realm.Realm
 import io.realm.RealmConfiguration
+import kotlinx.coroutines.runBlocking
 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.NewSessionListener
 import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
 import org.matrix.android.sdk.api.session.events.model.toModel
-import org.matrix.android.sdk.internal.crypto.NewSessionListener
-import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
-import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
 import org.matrix.android.sdk.internal.database.mapper.asDomain
 import org.matrix.android.sdk.internal.database.model.EventEntity
 import org.matrix.android.sdk.internal.database.query.where
@@ -39,7 +39,6 @@ internal class TimelineEventDecryptor @Inject constructor(
         private val realmConfiguration: RealmConfiguration,
         private val cryptoService: CryptoService,
         private val threadsAwarenessHandler: ThreadsAwarenessHandler,
-        private val lightweightSettingsStorage: LightweightSettingsStorage
 ) {
 
     private val newSessionListener = object : NewSessionListener {
@@ -99,23 +98,30 @@ internal class TimelineEventDecryptor @Inject constructor(
         }
         executor?.execute {
             Realm.getInstance(realmConfiguration).use { realm ->
-                processDecryptRequest(request, realm)
+                try {
+                    runBlocking {
+                        processDecryptRequest(request, realm)
+                    }
+                } catch (e: InterruptedException) {
+                    Timber.i("Decryption got interrupted")
+                }
             }
         }
     }
 
     private fun threadAwareNonEncryptedEvents(request: DecryptionRequest, realm: Realm) {
         val event = request.event
-            realm.executeTransaction {
-                val eventId = event.eventId ?: return@executeTransaction
-                val eventEntity = EventEntity
-                        .where(it, eventId = eventId)
-                        .findFirst()
-                val decryptedEvent = eventEntity?.asDomain()
-                threadsAwarenessHandler.makeEventThreadAware(realm, event.roomId, decryptedEvent, eventEntity)
+        realm.executeTransaction {
+            val eventId = event.eventId ?: return@executeTransaction
+            val eventEntity = EventEntity
+                    .where(it, eventId = eventId)
+                    .findFirst()
+            val decryptedEvent = eventEntity?.asDomain()
+            threadsAwarenessHandler.makeEventThreadAware(realm, event.roomId, decryptedEvent, eventEntity)
         }
     }
-    private fun processDecryptRequest(request: DecryptionRequest, realm: Realm) {
+
+    private suspend fun processDecryptRequest(request: DecryptionRequest, realm: Realm) {
         val event = request.event
         val timelineId = request.timelineId
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt
index 6607e71bd9cb530bc96ef00b673b7e602ebaf174..d3f24a8568a4a8ba5107f20668890b826019932f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt
@@ -23,20 +23,22 @@ import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
 import org.matrix.android.sdk.api.session.room.send.SendState
+import org.matrix.android.sdk.api.settings.LightweightSettingsStorage
 import org.matrix.android.sdk.internal.database.helper.addIfNecessary
 import org.matrix.android.sdk.internal.database.helper.addStateEvent
 import org.matrix.android.sdk.internal.database.helper.addTimelineEvent
 import org.matrix.android.sdk.internal.database.helper.updateThreadSummaryIfNeeded
-import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
 import org.matrix.android.sdk.internal.database.mapper.toEntity
 import org.matrix.android.sdk.internal.database.model.ChunkEntity
 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.model.RoomEntity
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
+import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
 import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
 import org.matrix.android.sdk.internal.database.query.create
 import org.matrix.android.sdk.internal.database.query.find
+import org.matrix.android.sdk.internal.database.query.findAll
 import org.matrix.android.sdk.internal.database.query.where
 import org.matrix.android.sdk.internal.di.SessionDatabase
 import org.matrix.android.sdk.internal.di.UserId
@@ -49,10 +51,10 @@ import javax.inject.Inject
  * Insert Chunk in DB, and eventually link next and previous chunk in db.
  */
 internal class TokenChunkEventPersistor @Inject constructor(
-                                                            @SessionDatabase private val monarchy: Monarchy,
-                                                            @UserId private val userId: String,
-                                                            private val lightweightSettingsStorage: LightweightSettingsStorage,
-                                                            private val liveEventManager: Lazy<StreamEventsManager>) {
+        @SessionDatabase private val monarchy: Monarchy,
+        @UserId private val userId: String,
+        private val lightweightSettingsStorage: LightweightSettingsStorage,
+        private val liveEventManager: Lazy<StreamEventsManager>) {
 
     enum class Result {
         SHOULD_FETCH_MORE,
@@ -79,7 +81,8 @@ internal class TokenChunkEventPersistor @Inject constructor(
 
                     val existingChunk = ChunkEntity.find(realm, roomId, prevToken = prevToken, nextToken = nextToken)
                     if (existingChunk != null) {
-                        Timber.v("This chunk is already in the db, returns")
+                        Timber.v("This chunk is already in the db, checking if this might be caused by broken links")
+                        existingChunk.fixChunkLinks(realm, roomId, direction, prevToken, nextToken)
                         return@awaitTransaction
                     }
                     val prevChunk = ChunkEntity.find(realm, roomId, nextToken = prevToken)
@@ -88,8 +91,14 @@ internal class TokenChunkEventPersistor @Inject constructor(
                         this.nextChunk = nextChunk
                         this.prevChunk = prevChunk
                     }
-                    nextChunk?.prevChunk = currentChunk
-                    prevChunk?.nextChunk = currentChunk
+                    val allNextChunks = ChunkEntity.findAll(realm, roomId, prevToken = nextToken)
+                    val allPrevChunks = ChunkEntity.findAll(realm, roomId, nextToken = prevToken)
+                    allNextChunks?.forEach {
+                        it.prevChunk = currentChunk
+                    }
+                    allPrevChunks?.forEach {
+                        it.nextChunk = currentChunk
+                    }
                     if (receivedChunk.events.isEmpty() && !receivedChunk.hasMore()) {
                         handleReachEnd(roomId, direction, currentChunk)
                     } else {
@@ -108,6 +117,34 @@ internal class TokenChunkEventPersistor @Inject constructor(
         }
     }
 
+    private fun ChunkEntity.fixChunkLinks(
+            realm: Realm,
+            roomId: String,
+            direction: PaginationDirection,
+            prevToken: String?,
+            nextToken: String?,
+    ) {
+        if (direction == PaginationDirection.FORWARDS) {
+            val prevChunks = ChunkEntity.findAll(realm, roomId, nextToken = prevToken)
+            Timber.v("Found ${prevChunks?.size} prevChunks")
+            prevChunks?.forEach {
+                if (it.nextChunk != this) {
+                    Timber.i("Set nextChunk for ${it.identifier()} from ${it.nextChunk?.identifier()} to ${identifier()}")
+                    it.nextChunk = this
+                }
+            }
+        } else {
+            val nextChunks = ChunkEntity.findAll(realm, roomId, prevToken = nextToken)
+            Timber.v("Found ${nextChunks?.size} nextChunks")
+            nextChunks?.forEach {
+                if (it.prevChunk != this) {
+                    Timber.i("Set prevChunk for ${it.identifier()} from ${it.prevChunk?.identifier()} to ${identifier()}")
+                    it.prevChunk = this
+                }
+            }
+        }
+    }
+
     private fun handleReachEnd(roomId: String, direction: PaginationDirection, currentChunk: ChunkEntity) {
         Timber.v("Reach end of $roomId")
         if (direction == PaginationDirection.FORWARDS) {
@@ -145,9 +182,12 @@ internal class TokenChunkEventPersistor @Inject constructor(
                 if (event.eventId == null || event.senderId == null) {
                     return@forEach
                 }
-                // We check for the timeline event with this id
+                // We check for the timeline event with this id, but not in the thread chunk
                 val eventId = event.eventId
-                val existingTimelineEvent = TimelineEventEntity.where(realm, roomId, eventId).findFirst()
+                val existingTimelineEvent = TimelineEventEntity
+                        .where(realm, roomId, eventId)
+                        .equalTo(TimelineEventEntityFields.OWNED_BY_THREAD_CHUNK, false)
+                        .findFirst()
                 // If it exists, we want to stop here, just link the prevChunk
                 val existingChunk = existingTimelineEvent?.chunk?.firstOrNull()
                 if (existingChunk != null) {
@@ -173,7 +213,7 @@ internal class TokenChunkEventPersistor @Inject constructor(
                     return@processTimelineEvents
                 }
                 val ageLocalTs = event.unsignedData?.age?.let { now - it }
-                val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
+                var eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
                 if (event.type == EventType.STATE_ROOM_MEMBER && event.stateKey != null) {
                     val contentToUse = if (direction == PaginationDirection.BACKWARDS) {
                         event.prevContent
@@ -183,7 +223,11 @@ internal class TokenChunkEventPersistor @Inject constructor(
                     roomMemberContentsByUser[event.stateKey] = contentToUse.toModel<RoomMemberContent>()
                 }
                 liveEventManager.get().dispatchPaginatedEventReceived(event, roomId)
-                currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser)
+                currentChunk.addTimelineEvent(
+                        roomId = roomId,
+                        eventEntity = eventEntity,
+                        direction = direction,
+                        roomMemberContentsByUser = roomMemberContentsByUser)
                 if (lightweightSettingsStorage.areThreadMessagesEnabled()) {
                     eventEntity.rootThreadEventId?.let {
                         // This is a thread event
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/TypingBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/TypingBody.kt
index 973870bb47fbd3dc06af7746e5ac8fd923f55151..66913e0cbfbf629aecf19be9d548910c0789bd53 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/TypingBody.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/TypingBody.kt
@@ -20,7 +20,7 @@ import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
 
 @JsonClass(generateAdapter = true)
-data class TypingBody(
+internal data class TypingBody(
         // Required. Whether the user is typing or not. If false, the timeout key can be omitted.
         @Json(name = "typing")
         val typing: Boolean,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/TypingEventContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/TypingEventContent.kt
index 488d38d762de68f2a375b8649eeb089e89807170..5b01085648576d51749f0ca568001d8820833ef2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/TypingEventContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/typing/TypingEventContent.kt
@@ -20,7 +20,7 @@ import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
 
 @JsonClass(generateAdapter = true)
-data class TypingEventContent(
+internal data class TypingEventContent(
         @Json(name = "user_ids")
         val typingUserIds: List<String> = emptyList()
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/GetUploadsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/GetUploadsTask.kt
index 028c3e91933c1ecbd4bcc555909d9c5be6894995..7daf506c14db4cc0b5767818c7f71c7c1a861bbd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/GetUploadsTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/uploads/GetUploadsTask.kt
@@ -17,6 +17,7 @@
 package org.matrix.android.sdk.internal.session.room.uploads
 
 import com.zhuinden.monarchy.Monarchy
+import io.realm.Sort
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.toModel
@@ -75,6 +76,7 @@ internal class DefaultGetUploadsTask @Inject constructor(
             monarchy.doWithRealm { realm ->
                 eventsFromRealm = EventEntity.whereType(realm, EventType.ENCRYPTED, params.roomId)
                         .like(EventEntityFields.DECRYPTION_RESULT_JSON, TimelineEventFilter.DecryptedContent.URL)
+                        .sort(EventEntityFields.ORIGIN_SERVER_TS, Sort.DESCENDING)
                         .findAll()
                         .map { it.asDomain() }
                         // Exclude stickers
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt
index fad1840e51b07349427572f59e3744a321f63dc0..17dc90fdb0e09d32b206f3b2bb886143b9e930d8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt
@@ -34,7 +34,6 @@ import java.io.InputStream
 import java.io.ObjectInputStream
 import java.io.ObjectOutputStream
 import java.io.OutputStream
-import java.lang.IllegalArgumentException
 import java.math.BigInteger
 import java.security.KeyPairGenerator
 import java.security.KeyStore
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt
index c18055e0894f9f8f3e44ac64d2175347cd7a47b3..355039b22c704d8dce8038764f60b7e362a3cd0b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt
@@ -18,6 +18,8 @@ package org.matrix.android.sdk.internal.session.space
 
 import android.net.Uri
 import androidx.lifecycle.LiveData
+import kotlinx.coroutines.withContext
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.query.QueryStringValue
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
@@ -43,6 +45,7 @@ import org.matrix.android.sdk.api.session.space.SpaceService
 import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams
 import org.matrix.android.sdk.api.session.space.model.SpaceChildContent
 import org.matrix.android.sdk.api.session.space.model.SpaceParentContent
+import org.matrix.android.sdk.api.session.space.peeking.SpacePeekResult
 import org.matrix.android.sdk.internal.di.UserId
 import org.matrix.android.sdk.internal.session.room.RoomGetter
 import org.matrix.android.sdk.internal.session.room.SpaceGetter
@@ -51,7 +54,6 @@ import org.matrix.android.sdk.internal.session.room.membership.leaving.LeaveRoom
 import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
 import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
 import org.matrix.android.sdk.internal.session.space.peeking.PeekSpaceTask
-import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult
 import javax.inject.Inject
 
 internal class DefaultSpaceService @Inject constructor(
@@ -64,7 +66,8 @@ internal class DefaultSpaceService @Inject constructor(
         private val stateEventDataSource: StateEventDataSource,
         private val peekSpaceTask: PeekSpaceTask,
         private val resolveSpaceInfoTask: ResolveSpaceInfoTask,
-        private val leaveRoomTask: LeaveRoomTask
+        private val leaveRoomTask: LeaveRoomTask,
+        private val coroutineDispatchers: MatrixCoroutineDispatchers,
 ) : SpaceService {
 
     override suspend fun createSpace(params: CreateSpaceParams): String {
@@ -105,79 +108,118 @@ internal class DefaultSpaceService @Inject constructor(
         return roomSummaryDataSource.getSpaceSummaries(spaceSummaryQueryParams, sortOrder)
     }
 
-    override fun getRootSpaceSummaries(): List<RoomSummary> {
-        return roomSummaryDataSource.getRootSpaceSummaries()
+    override suspend fun getRootSpaceSummaries(): List<RoomSummary> {
+        return withContext(coroutineDispatchers.io) {
+            roomSummaryDataSource.getRootSpaceSummaries()
+        }
     }
 
     override suspend fun peekSpace(spaceId: String): SpacePeekResult {
         return peekSpaceTask.execute(PeekSpaceTask.Params(spaceId))
     }
 
-    override suspend fun querySpaceChildren(spaceId: String,
-                                            suggestedOnly: Boolean?,
-                                            limit: Int?,
-                                            from: String?,
-                                            knownStateList: List<Event>?): SpaceHierarchyData {
-        return resolveSpaceInfoTask.execute(
-                ResolveSpaceInfoTask.Params(
-                        spaceId = spaceId, limit = limit, maxDepth = 1, from = from, suggestedOnly = suggestedOnly
-                )
-        ).let { response ->
-            val spaceDesc = response.rooms?.firstOrNull { it.roomId == spaceId }
-            val root = RoomSummary(
-                    roomId = spaceDesc?.roomId ?: spaceId,
-                    roomType = spaceDesc?.roomType,
-                    name = spaceDesc?.name ?: "",
-                    displayName = spaceDesc?.name ?: "",
-                    topic = spaceDesc?.topic ?: "",
-                    joinedMembersCount = spaceDesc?.numJoinedMembers,
-                    avatarUrl = spaceDesc?.avatarUrl ?: "",
-                    encryptionEventTs = null,
-                    typingUsers = emptyList(),
-                    isEncrypted = false,
-                    flattenParentIds = emptyList(),
-                    canonicalAlias = spaceDesc?.canonicalAlias,
-                    joinRules = RoomJoinRules.PUBLIC.takeIf { spaceDesc?.worldReadable == true }
-            )
-            val children = response.rooms
-                    ?.filter { it.roomId != spaceId }
-                    ?.flatMap { childSummary ->
-                        (spaceDesc?.childrenState ?: knownStateList)
-                                ?.filter { it.stateKey == childSummary.roomId && it.type == EventType.STATE_SPACE_CHILD }
-                                ?.mapNotNull { childStateEv ->
-                                    // create a child entry for everytime this room is the child of a space
-                                    // beware that a room could appear then twice in this list
-                                    childStateEv.content.toModel<SpaceChildContent>()?.let { childStateEvContent ->
-                                        SpaceChildInfo(
-                                                childRoomId = childSummary.roomId,
-                                                isKnown = true,
-                                                roomType = childSummary.roomType,
-                                                name = childSummary.name,
-                                                topic = childSummary.topic,
-                                                avatarUrl = childSummary.avatarUrl,
-                                                order = childStateEvContent.order,
-//                                                        autoJoin = childStateEvContent.autoJoin ?: false,
-                                                viaServers = childStateEvContent.via.orEmpty(),
-                                                activeMemberCount = childSummary.numJoinedMembers,
-                                                parentRoomId = childStateEv.roomId,
-                                                suggested = childStateEvContent.suggested,
-                                                canonicalAlias = childSummary.canonicalAlias,
-                                                aliases = childSummary.aliases,
-                                                worldReadable = childSummary.worldReadable
-                                        )
-                                    }
-                                }.orEmpty()
-                    }
-                    .orEmpty()
-            SpaceHierarchyData(
-                    rootSummary = root,
-                    children = children,
-                    childrenState = spaceDesc?.childrenState.orEmpty(),
-                    nextToken = response.nextBatch
+    override suspend fun querySpaceChildren(
+            spaceId: String,
+            suggestedOnly: Boolean?,
+            limit: Int?,
+            from: String?,
+            knownStateList: List<Event>?
+    ): SpaceHierarchyData {
+        val spacesResponse = getSpacesResponse(spaceId, suggestedOnly, limit, from)
+        val spaceRootResponse = spacesResponse.getRoot(spaceId)
+        val spaceRoot = spaceRootResponse?.toRoomSummary() ?: createBlankRoomSummary(spaceId)
+        val spaceChildren = spacesResponse.rooms.mapSpaceChildren(spaceId, spaceRootResponse, knownStateList)
+
+        return SpaceHierarchyData(
+                rootSummary = spaceRoot,
+                children = spaceChildren,
+                childrenState = spaceRootResponse?.childrenState.orEmpty(),
+                nextToken = spacesResponse.nextBatch
+        )
+    }
+
+    private suspend fun getSpacesResponse(spaceId: String, suggestedOnly: Boolean?, limit: Int?, from: String?) =
+            resolveSpaceInfoTask.execute(
+                    ResolveSpaceInfoTask.Params(spaceId = spaceId, limit = limit, maxDepth = 1, from = from, suggestedOnly = suggestedOnly)
             )
-        }
+
+    private fun SpacesResponse.getRoot(spaceId: String) = rooms?.firstOrNull { it.roomId == spaceId }
+
+    private fun SpaceChildSummaryResponse.toRoomSummary() = RoomSummary(
+            roomId = roomId,
+            roomType = roomType,
+            name = name ?: "",
+            displayName = name ?: "",
+            topic = topic ?: "",
+            joinedMembersCount = numJoinedMembers,
+            avatarUrl = avatarUrl ?: "",
+            encryptionEventTs = null,
+            typingUsers = emptyList(),
+            isEncrypted = false,
+            flattenParentIds = emptyList(),
+            canonicalAlias = canonicalAlias,
+            joinRules = RoomJoinRules.PUBLIC.takeIf { isWorldReadable }
+    )
+
+    private fun createBlankRoomSummary(spaceId: String) = RoomSummary(
+            roomId = spaceId,
+            joinedMembersCount = null,
+            encryptionEventTs = null,
+            typingUsers = emptyList(),
+            isEncrypted = false,
+            flattenParentIds = emptyList(),
+            canonicalAlias = null,
+            joinRules = null
+    )
+
+    private fun List<SpaceChildSummaryResponse>?.mapSpaceChildren(
+            spaceId: String,
+            spaceRootResponse: SpaceChildSummaryResponse?,
+            knownStateList: List<Event>?,
+    ) = this?.filterIdIsNot(spaceId)
+            ?.toSpaceChildInfoList(spaceId, spaceRootResponse, knownStateList)
+            .orEmpty()
+
+    private fun List<SpaceChildSummaryResponse>.filterIdIsNot(spaceId: String) = filter { it.roomId != spaceId }
+
+    private fun List<SpaceChildSummaryResponse>.toSpaceChildInfoList(
+            spaceId: String,
+            rootRoomResponse: SpaceChildSummaryResponse?,
+            knownStateList: List<Event>?,
+    ) = flatMap { spaceChildSummary ->
+        (rootRoomResponse?.childrenState ?: knownStateList)
+                ?.filter { it.isChildOf(spaceChildSummary) }
+                ?.mapNotNull { childStateEvent -> childStateEvent.toSpaceChildInfo(spaceId, spaceChildSummary) }
+                .orEmpty()
     }
 
+    private fun Event.isChildOf(space: SpaceChildSummaryResponse) = stateKey == space.roomId && type == EventType.STATE_SPACE_CHILD
+
+    private fun Event.toSpaceChildInfo(spaceId: String, summary: SpaceChildSummaryResponse) = content.toModel<SpaceChildContent>()?.let { content ->
+        createSpaceChildInfo(spaceId, summary, content)
+    }
+
+    private fun createSpaceChildInfo(
+            spaceId: String,
+            summary: SpaceChildSummaryResponse,
+            content: SpaceChildContent
+    ) = SpaceChildInfo(
+            childRoomId = summary.roomId,
+            isKnown = true,
+            roomType = summary.roomType,
+            name = summary.name,
+            topic = summary.topic,
+            avatarUrl = summary.avatarUrl,
+            order = content.order,
+            viaServers = content.via.orEmpty(),
+            activeMemberCount = summary.numJoinedMembers,
+            parentRoomId = spaceId,
+            suggested = content.suggested,
+            canonicalAlias = summary.canonicalAlias,
+            aliases = summary.aliases,
+            worldReadable = summary.isWorldReadable
+    )
+
     override suspend fun joinSpace(spaceIdOrAlias: String,
                                    reason: String?,
                                    viaServers: List<String>): JoinSpaceResult {
@@ -192,10 +234,6 @@ internal class DefaultSpaceService @Inject constructor(
         leaveRoomTask.execute(LeaveRoomTask.Params(spaceId, reason))
     }
 
-//    override fun getSpaceParentsOfRoom(roomId: String): List<SpaceSummary> {
-//        return spaceSummaryDataSource.getParentsOfRoom(roomId)
-//    }
-
     override suspend fun setSpaceParent(childRoomId: String, parentSpaceId: String, canonical: Boolean, viaServers: List<String>) {
         // Should we perform some validation here?,
         // and if client want to bypass, it could use sendStateEvent directly?
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt
index 2a396d6ee7b444f1435a6751d784b9686021a2e1..d59ca06c2cda6fdf852c47cedb54d23bc6c81c1d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt
@@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.space
 import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
 import org.matrix.android.sdk.internal.network.executeRequest
 import org.matrix.android.sdk.internal.task.Task
+import retrofit2.HttpException
 import javax.inject.Inject
 
 internal interface ResolveSpaceInfoTask : Task<ResolveSpaceInfoTask.Params, SpacesResponse> {
@@ -28,7 +29,6 @@ internal interface ResolveSpaceInfoTask : Task<ResolveSpaceInfoTask.Params, Spac
             val maxDepth: Int?,
             val from: String?,
             val suggestedOnly: Boolean?
-//            val autoJoinOnly: Boolean?
     )
 }
 
@@ -36,14 +36,30 @@ internal class DefaultResolveSpaceInfoTask @Inject constructor(
         private val spaceApi: SpaceApi,
         private val globalErrorReceiver: GlobalErrorReceiver
 ) : ResolveSpaceInfoTask {
-    override suspend fun execute(params: ResolveSpaceInfoTask.Params): SpacesResponse {
-        return executeRequest(globalErrorReceiver) {
+
+    override suspend fun execute(params: ResolveSpaceInfoTask.Params) = executeRequest(globalErrorReceiver) {
+        try {
+            getSpaceHierarchy(params)
+        } catch (e: HttpException) {
+            getUnstableSpaceHierarchy(params)
+        }
+    }
+
+    private suspend fun getSpaceHierarchy(params: ResolveSpaceInfoTask.Params) =
             spaceApi.getSpaceHierarchy(
                     spaceId = params.spaceId,
                     suggestedOnly = params.suggestedOnly,
                     limit = params.limit,
                     maxDepth = params.maxDepth,
-                    from = params.from)
-        }
-    }
+                    from = params.from,
+            )
+
+    private suspend fun getUnstableSpaceHierarchy(params: ResolveSpaceInfoTask.Params) =
+            spaceApi.getSpaceHierarchyUnstable(
+                    spaceId = params.spaceId,
+                    suggestedOnly = params.suggestedOnly,
+                    limit = params.limit,
+                    maxDepth = params.maxDepth,
+                    from = params.from,
+            )
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt
index edd10bc2ef9040c2e4e91cd10a7466d5735ad900..fda9b4b5bc6db6418bb8e54b5d7242cd3452d4bc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt
@@ -31,11 +31,22 @@ internal interface SpaceApi {
      * @param from: Optional. Pagination token given to retrieve the next set of rooms.
      * Note that if a pagination token is provided, then the parameters given for suggested_only and max_depth must be the same.
      */
-    @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc2946/rooms/{roomId}/hierarchy")
+    @GET(NetworkConstants.URI_API_PREFIX_PATH_V1 + "rooms/{roomId}/hierarchy")
     suspend fun getSpaceHierarchy(
             @Path("roomId") spaceId: String,
             @Query("suggested_only") suggestedOnly: Boolean?,
             @Query("limit") limit: Int?,
             @Query("max_depth") maxDepth: Int?,
             @Query("from") from: String?): SpacesResponse
+
+    /**
+     * Unstable version of [getSpaceHierarchy]
+     */
+    @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc2946/rooms/{roomId}/hierarchy")
+    suspend fun getSpaceHierarchyUnstable(
+            @Path("roomId") spaceId: String,
+            @Query("suggested_only") suggestedOnly: Boolean?,
+            @Query("limit") limit: Int?,
+            @Query("max_depth") maxDepth: Int?,
+            @Query("from") from: String?): SpacesResponse
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceChildSummaryResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceChildSummaryResponse.kt
index e0f273d0c2510c8b838e4d11d3bbbfc686bd4c72..b6a9c73d36596323af4f8855b7ac259e4fbad87c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceChildSummaryResponse.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceChildSummaryResponse.kt
@@ -81,7 +81,7 @@ internal data class SpaceChildSummaryResponse(
          * Required. Whether the room may be viewed by guest users without joining.
          */
         @Json(name = "world_readable")
-        val worldReadable: Boolean = false,
+        val isWorldReadable: Boolean = false,
 
         /**
          * Required. Whether guest users may join the room and participate in it. If they can,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/PeekSpaceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/PeekSpaceTask.kt
index c45d4420ae476980b8be1e313b094ae81be08ff5..3647941998a0d70fddc32b19ca9c7b83e9bba0fe 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/PeekSpaceTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/PeekSpaceTask.kt
@@ -23,6 +23,11 @@ import org.matrix.android.sdk.api.session.room.model.RoomType
 import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
 import org.matrix.android.sdk.api.session.room.peeking.PeekResult
 import org.matrix.android.sdk.api.session.space.model.SpaceChildContent
+import org.matrix.android.sdk.api.session.space.peeking.ISpaceChild
+import org.matrix.android.sdk.api.session.space.peeking.SpaceChildPeekResult
+import org.matrix.android.sdk.api.session.space.peeking.SpacePeekResult
+import org.matrix.android.sdk.api.session.space.peeking.SpacePeekSummary
+import org.matrix.android.sdk.api.session.space.peeking.SpaceSubChildPeekResult
 import org.matrix.android.sdk.internal.session.room.peeking.PeekRoomTask
 import org.matrix.android.sdk.internal.session.room.peeking.ResolveRoomStateTask
 import org.matrix.android.sdk.internal.task.Task
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncEphemeralTemporaryStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncEphemeralTemporaryStore.kt
index 8c68e224dcde88756d958369c4af0aa5bd0d58a1..ef9f468c8645394af7b2e3dd50cc68cd60e0bb22 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncEphemeralTemporaryStore.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncEphemeralTemporaryStore.kt
@@ -21,8 +21,8 @@ import com.squareup.moshi.Moshi
 import okio.buffer
 import okio.source
 import org.matrix.android.sdk.api.session.sync.model.RoomSyncEphemeral
+import org.matrix.android.sdk.api.util.md5
 import org.matrix.android.sdk.internal.di.SessionFilesDirectory
-import org.matrix.android.sdk.internal.util.md5
 import timber.log.Timber
 import java.io.File
 import javax.inject.Inject
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncPresence.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncPresence.kt
index 18e17c7d139cf88e9070d527a925865777e67dd9..4f1fe43b7ddcc24dee4b7b7548055c8193f8ac04 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncPresence.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncPresence.kt
@@ -16,6 +16,8 @@
 
 package org.matrix.android.sdk.internal.session.sync
 
+import org.matrix.android.sdk.api.session.presence.model.PresenceEnum
+
 /**
  * For `set_presence` parameter in the /sync request
  *
@@ -24,8 +26,19 @@ package org.matrix.android.sdk.internal.session.sync
  * parameter is set to "offline" then the client is not marked as being online when it uses this API.
  * When set to "unavailable", the client is marked as being idle. One of: ["offline", "online", "unavailable"]
  */
-enum class SyncPresence(val value: String) {
+internal enum class SyncPresence(val value: String) {
     Offline("offline"),
     Online("online"),
-    Unavailable("unavailable")
+    Unavailable("unavailable");
+
+    companion object {
+        fun from(presenceEnum: PresenceEnum): SyncPresence {
+            return when (presenceEnum) {
+                PresenceEnum.ONLINE -> Online
+                PresenceEnum.OFFLINE -> Offline
+                PresenceEnum.UNAVAILABLE -> Unavailable
+            }
+        }
+        fun from(s: String?): SyncPresence? = values().find { it.value == s }
+    }
 }
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 f93da9705d0d58b91c2c893b150bb351f1b250b5..97850e81d32084ea8a5fac97043359aa46cc4694 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
@@ -26,7 +26,6 @@ import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse
 import org.matrix.android.sdk.api.session.sync.model.SyncResponse
 import org.matrix.android.sdk.internal.SessionManager
 import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
-import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
 import org.matrix.android.sdk.internal.di.SessionDatabase
 import org.matrix.android.sdk.internal.di.SessionId
 import org.matrix.android.sdk.internal.di.WorkManagerProvider
@@ -42,7 +41,6 @@ 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,10 +63,8 @@ internal class SyncResponseHandler @Inject constructor(
         private val aggregatorHandler: SyncResponsePostTreatmentAggregatorHandler,
         private val cryptoService: DefaultCryptoService,
         private val tokenStore: SyncTokenStore,
-        private val lightweightSettingsStorage: LightweightSettingsStorage,
         private val processEventForPushTask: ProcessEventForPushTask,
         private val pushRuleService: PushRuleService,
-        private val threadsAwarenessHandler: ThreadsAwarenessHandler,
         private val presenceSyncHandler: PresenceSyncHandler
 ) {
 
@@ -110,6 +106,7 @@ internal class SyncResponseHandler @Inject constructor(
 
         // Start one big transaction
         monarchy.awaitTransaction { realm ->
+            // IMPORTANT nothing should be suspend here as we are accessing the realm instance (thread local)
             measureTimeMillis {
                 Timber.v("Handle rooms")
                 reportSubtask(reporter, InitSyncStep.ImportingAccountRoom, 1, 0.7f) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregator.kt
index fe44531390bef1254433fe4c113c3038031ffda1..e9452c59fcde33acf54e064bae4d98f76eb7c228 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregator.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregator.kt
@@ -22,4 +22,7 @@ internal class SyncResponsePostTreatmentAggregator {
 
     // Map of roomId to directUserId
     val directChatsToCheck = mutableMapOf<String, String>()
+
+    // List of userIds to fetch and update at the end of incremental syncs
+    val userIdsToFetch = mutableListOf<String>()
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt
index b4da1a02cd918066ac3d32bcb73b18da45ad9bba..b56f89774983a8e453e79b4cc16aed6c6974080a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt
@@ -18,11 +18,14 @@ package org.matrix.android.sdk.internal.session.sync
 
 import android.os.SystemClock
 import okhttp3.ResponseBody
+import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.logger.LoggerTag
 import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.api.session.initsync.InitSyncStep
 import org.matrix.android.sdk.api.session.initsync.SyncStatusService
 import org.matrix.android.sdk.api.session.statistics.StatisticEvent
+import org.matrix.android.sdk.api.session.sync.InitialSyncStrategy
+import org.matrix.android.sdk.api.session.sync.initialSyncStrategy
 import org.matrix.android.sdk.api.session.sync.model.LazyRoomSyncEphemeral
 import org.matrix.android.sdk.api.session.sync.model.SyncResponse
 import org.matrix.android.sdk.internal.di.SessionFilesDirectory
@@ -104,7 +107,11 @@ internal class DefaultSyncTask @Inject constructor(
         val isInitialSync = token == null
         if (isInitialSync) {
             // We might want to get the user information in parallel too
-            userStore.createOrUpdate(userId)
+            val user = tryOrNull { session.getProfileAsUser(userId) }
+            userStore.createOrUpdate(
+                    userId = userId,
+                    displayName = user?.displayName,
+                    avatarUrl = user?.avatarUrl)
             defaultSyncStatusService.startRoot(InitSyncStep.ImportingAccount, 100)
         }
         // Maybe refresh the homeserver capabilities data we know
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 f299d3effa1b7b458fc2e57f7adfc599ab26e587..429f4985334d77e4f5fefedb72b4a38ecbf32002 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
@@ -18,16 +18,16 @@ 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.crypto.model.MXEventDecryptionResult
+import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.content.OlmEventContent
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.room.model.message.MessageContent
 import org.matrix.android.sdk.api.session.sync.model.SyncResponse
 import org.matrix.android.sdk.api.session.sync.model.ToDeviceSyncResponse
 import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
-import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
-import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
-import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent
 import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService
 import org.matrix.android.sdk.internal.session.initsync.ProgressReporter
 import timber.log.Timber
@@ -38,7 +38,7 @@ private val loggerTag = LoggerTag("CryptoSyncHandler", LoggerTag.CRYPTO)
 internal class CryptoSyncHandler @Inject constructor(private val cryptoService: DefaultCryptoService,
                                                      private val verificationService: DefaultVerificationService) {
 
-    fun handleToDevice(toDevice: ToDeviceSyncResponse, progressReporter: ProgressReporter? = null) {
+    suspend fun handleToDevice(toDevice: ToDeviceSyncResponse, progressReporter: ProgressReporter? = null) {
         val total = toDevice.events?.size ?: 0
         toDevice.events?.forEachIndexed { index, event ->
             progressReporter?.reportProgress(index * 100F / total)
@@ -66,7 +66,7 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService:
      * @param timelineId the timeline identifier
      * @return true if the event has been decrypted
      */
-    private fun decryptToDeviceEvent(event: Event, timelineId: String?): Boolean {
+    private suspend fun decryptToDeviceEvent(event: Event, timelineId: String?): Boolean {
         Timber.v("## CRYPTO | decryptToDeviceEvent")
         if (event.getClearType() == EventType.ENCRYPTED) {
             var result: MXEventDecryptionResult? = null
@@ -80,6 +80,8 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService:
                     it.identityKey() == senderKey
                 }?.deviceId ?: senderKey
                 Timber.e("## CRYPTO | Failed to decrypt to device event from ${event.senderId}|$deviceId reason:<${event.mCryptoError ?: exception}>")
+            } catch (failure: Throwable) {
+                Timber.e(failure, "## CRYPTO | Failed to decrypt to device event from ${event.senderId}")
             }
 
             if (null != result) {
@@ -91,7 +93,9 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService:
                 )
                 return true
             } else {
-                // should not happen
+                // Could happen for to device events
+                // None of the known session could decrypt the message
+                // In this case unwedging process might have been started (rate limited)
                 Timber.e("## CRYPTO | ERROR NULL DECRYPTION RESULT from ${event.senderId}")
             }
         }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/PresenceSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/PresenceSyncHandler.kt
index fe173a35c3855f2993006f091cbd70174cc6281c..6a7af1dda4a86fa3b005cff30a96cc0fe8c3fabb 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/PresenceSyncHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/PresenceSyncHandler.kt
@@ -17,6 +17,7 @@
 package org.matrix.android.sdk.internal.session.sync.handler
 
 import io.realm.Realm
+import org.matrix.android.sdk.api.MatrixConfiguration
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.getPresenceContent
 import org.matrix.android.sdk.api.session.sync.model.PresenceSyncResponse
@@ -27,7 +28,7 @@ import org.matrix.android.sdk.internal.database.query.updateDirectUserPresence
 import org.matrix.android.sdk.internal.database.query.updateUserPresence
 import javax.inject.Inject
 
-internal class PresenceSyncHandler @Inject constructor() {
+internal class PresenceSyncHandler @Inject constructor(private val matrixConfiguration: MatrixConfiguration) {
 
     fun handle(realm: Realm, presenceSyncResponse: PresenceSyncResponse?) {
         presenceSyncResponse?.events
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/SyncResponsePostTreatmentAggregatorHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/SyncResponsePostTreatmentAggregatorHandler.kt
index 1e0e87a450e4ecf9aeccbf3a2c898182920235d1..c638ed4f808deb3e61c3925bd9d3b68740f7ede4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/SyncResponsePostTreatmentAggregatorHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/SyncResponsePostTreatmentAggregatorHandler.kt
@@ -16,22 +16,32 @@
 
 package org.matrix.android.sdk.internal.session.sync.handler
 
+import com.zhuinden.monarchy.Monarchy
 import org.matrix.android.sdk.api.MatrixPatterns
+import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.api.session.user.model.User
+import org.matrix.android.sdk.internal.di.SessionDatabase
+import org.matrix.android.sdk.internal.session.profile.GetProfileInfoTask
 import org.matrix.android.sdk.internal.session.sync.RoomSyncEphemeralTemporaryStore
 import org.matrix.android.sdk.internal.session.sync.SyncResponsePostTreatmentAggregator
 import org.matrix.android.sdk.internal.session.sync.model.accountdata.toMutable
+import org.matrix.android.sdk.internal.session.user.UserEntityFactory
 import org.matrix.android.sdk.internal.session.user.accountdata.DirectChatsHelper
 import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask
+import org.matrix.android.sdk.internal.util.awaitTransaction
 import javax.inject.Inject
 
 internal class SyncResponsePostTreatmentAggregatorHandler @Inject constructor(
         private val directChatsHelper: DirectChatsHelper,
         private val ephemeralTemporaryStore: RoomSyncEphemeralTemporaryStore,
-        private val updateUserAccountDataTask: UpdateUserAccountDataTask
+        private val updateUserAccountDataTask: UpdateUserAccountDataTask,
+        private val getProfileInfoTask: GetProfileInfoTask,
+        @SessionDatabase private val monarchy: Monarchy,
 ) {
-    suspend fun handle(synResHaResponsePostTreatmentAggregator: SyncResponsePostTreatmentAggregator) {
-        cleanupEphemeralFiles(synResHaResponsePostTreatmentAggregator.ephemeralFilesToDelete)
-        updateDirectUserIds(synResHaResponsePostTreatmentAggregator.directChatsToCheck)
+    suspend fun handle(aggregator: SyncResponsePostTreatmentAggregator) {
+        cleanupEphemeralFiles(aggregator.ephemeralFilesToDelete)
+        updateDirectUserIds(aggregator.directChatsToCheck)
+        fetchAndUpdateUsers(aggregator.userIdsToFetch)
     }
 
     private fun cleanupEphemeralFiles(ephemeralFilesToDelete: List<String>) {
@@ -59,13 +69,33 @@ internal class SyncResponsePostTreatmentAggregatorHandler @Inject constructor(
                         }
 
                 // remove roomId from currentDirectUserId entry
-                hasUpdate = hasUpdate or(directChats[currentDirectUserId]?.remove(roomId) == true)
+                hasUpdate = hasUpdate or (directChats[currentDirectUserId]?.remove(roomId) == true)
                 // remove currentDirectUserId entry if there is no attached room anymore
-                hasUpdate = hasUpdate or(directChats.takeIf { it[currentDirectUserId].isNullOrEmpty() }?.remove(currentDirectUserId) != null)
+                hasUpdate = hasUpdate or (directChats.takeIf { it[currentDirectUserId].isNullOrEmpty() }?.remove(currentDirectUserId) != null)
             }
         }
         if (hasUpdate) {
             updateUserAccountDataTask.execute(UpdateUserAccountDataTask.DirectChatParams(directMessages = directChats))
         }
     }
+
+    private suspend fun fetchAndUpdateUsers(userIdsToFetch: List<String>) {
+        fetchUsers(userIdsToFetch)
+                .takeIf { it.isNotEmpty() }
+                ?.saveLocally()
+    }
+
+    private suspend fun fetchUsers(userIdsToFetch: List<String>) = userIdsToFetch.mapNotNull {
+        tryOrNull {
+            val profileJson = getProfileInfoTask.execute(GetProfileInfoTask.Params(it))
+            User.fromJson(it, profileJson)
+        }
+    }
+
+    private suspend fun List<User>.saveLocally() {
+        val userEntities = map { user -> UserEntityFactory.create(user) }
+        monarchy.awaitTransaction {
+            it.insertOrUpdate(userEntities)
+        }
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt
index 025ee329f8a0dd6731b67458760428e401a4d7ac..2c84f1e692f523406a8ca3abd41c25ab52ad00f6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt
@@ -33,7 +33,7 @@ import javax.inject.Inject
 // value : dict key $UserId
 //              value dict key ts
 //                    dict value ts value
-typealias ReadReceiptContent = Map<String, Map<String, Map<String, Map<String, Double>>>>
+internal typealias ReadReceiptContent = Map<String, Map<String, Map<String, Map<String, Double>>>>
 
 private const val READ_KEY = "m.read"
 private const val TIMESTAMP_KEY = "ts"
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 99e6521eb70b76656b477d0cedc15055b98d9d6d..afd8e1bb99a5b163d410181ae32e335bddd79678 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
@@ -19,25 +19,31 @@ package org.matrix.android.sdk.internal.session.sync.handler.room
 import dagger.Lazy
 import io.realm.Realm
 import io.realm.kotlin.createObject
+import kotlinx.coroutines.runBlocking
+import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
+import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
 import org.matrix.android.sdk.api.session.initsync.InitSyncStep
 import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
 import org.matrix.android.sdk.api.session.room.send.SendState
+import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummaryUpdateType
+import org.matrix.android.sdk.api.session.sync.InitialSyncStrategy
+import org.matrix.android.sdk.api.session.sync.initialSyncStrategy
 import org.matrix.android.sdk.api.session.sync.model.InvitedRoomSync
 import org.matrix.android.sdk.api.session.sync.model.LazyRoomSyncEphemeral
 import org.matrix.android.sdk.api.session.sync.model.RoomSync
 import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse
+import org.matrix.android.sdk.api.settings.LightweightSettingsStorage
 import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
-import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
-import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
 import org.matrix.android.sdk.internal.database.helper.addIfNecessary
 import org.matrix.android.sdk.internal.database.helper.addTimelineEvent
+import org.matrix.android.sdk.internal.database.helper.createOrUpdate
 import org.matrix.android.sdk.internal.database.helper.updateThreadSummaryIfNeeded
-import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
 import org.matrix.android.sdk.internal.database.mapper.asDomain
 import org.matrix.android.sdk.internal.database.mapper.toEntity
 import org.matrix.android.sdk.internal.database.model.ChunkEntity
@@ -46,10 +52,13 @@ 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.model.RoomEntity
 import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
+import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
 import org.matrix.android.sdk.internal.database.model.deleteOnCascade
+import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntity
 import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
 import org.matrix.android.sdk.internal.database.query.find
 import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
+import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfThread
 import org.matrix.android.sdk.internal.database.query.getOrCreate
 import org.matrix.android.sdk.internal.database.query.getOrNull
 import org.matrix.android.sdk.internal.database.query.where
@@ -67,9 +76,7 @@ import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater
 import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
 import org.matrix.android.sdk.internal.session.room.timeline.TimelineInput
 import org.matrix.android.sdk.internal.session.room.typing.TypingEventContent
-import org.matrix.android.sdk.internal.session.sync.InitialSyncStrategy
 import org.matrix.android.sdk.internal.session.sync.SyncResponsePostTreatmentAggregator
-import org.matrix.android.sdk.internal.session.sync.initialSyncStrategy
 import org.matrix.android.sdk.internal.session.sync.parsing.RoomSyncAccountDataHandler
 import org.matrix.android.sdk.internal.util.computeBestChunkSize
 import timber.log.Timber
@@ -84,6 +91,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                                                    private val threadsAwarenessHandler: ThreadsAwarenessHandler,
                                                    private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
                                                    @UserId private val userId: String,
+                                                   private val homeServerCapabilitiesService: HomeServerCapabilitiesService,
                                                    private val lightweightSettingsStorage: LightweightSettingsStorage,
                                                    private val timelineInput: TimelineInput,
                                                    private val liveEventService: Lazy<StreamEventsManager>) {
@@ -94,12 +102,11 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
         data class LEFT(val data: Map<String, RoomSync>) : HandlingStrategy()
     }
 
-    fun handle(realm: Realm,
-               roomsSyncResponse: RoomsSyncResponse,
-               isInitialSync: Boolean,
-               aggregator: SyncResponsePostTreatmentAggregator,
-               reporter: ProgressReporter? = null) {
-        Timber.v("Execute transaction from $this")
+    suspend fun handle(realm: Realm,
+                       roomsSyncResponse: RoomsSyncResponse,
+                       isInitialSync: Boolean,
+                       aggregator: SyncResponsePostTreatmentAggregator,
+                       reporter: ProgressReporter? = null) {
         handleRoomSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), isInitialSync, aggregator, reporter)
         handleRoomSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), isInitialSync, aggregator, reporter)
         handleRoomSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), isInitialSync, aggregator, reporter)
@@ -113,11 +120,11 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
     }
     // PRIVATE METHODS *****************************************************************************
 
-    private fun handleRoomSync(realm: Realm,
-                               handlingStrategy: HandlingStrategy,
-                               isInitialSync: Boolean,
-                               aggregator: SyncResponsePostTreatmentAggregator,
-                               reporter: ProgressReporter?) {
+    private suspend fun handleRoomSync(realm: Realm,
+                                       handlingStrategy: HandlingStrategy,
+                                       isInitialSync: Boolean,
+                                       aggregator: SyncResponsePostTreatmentAggregator,
+                                       reporter: ProgressReporter?) {
         val insertType = if (isInitialSync) {
             EventInsertType.INITIAL_SYNC
         } else {
@@ -150,11 +157,11 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
         realm.insertOrUpdate(rooms)
     }
 
-    private fun insertJoinRoomsFromInitSync(realm: Realm,
-                                            handlingStrategy: HandlingStrategy.JOINED,
-                                            syncLocalTimeStampMillis: Long,
-                                            aggregator: SyncResponsePostTreatmentAggregator,
-                                            reporter: ProgressReporter?) {
+    private suspend fun insertJoinRoomsFromInitSync(realm: Realm,
+                                                    handlingStrategy: HandlingStrategy.JOINED,
+                                                    syncLocalTimeStampMillis: Long,
+                                                    aggregator: SyncResponsePostTreatmentAggregator,
+                                                    reporter: ProgressReporter?) {
         val bestChunkSize = computeBestChunkSize(
                 listSize = handlingStrategy.data.keys.size,
                 limit = (initialSyncStrategy as? InitialSyncStrategy.Optimized)?.maxRoomsToInsert ?: Int.MAX_VALUE
@@ -192,13 +199,14 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
         }
     }
 
-    private fun handleJoinedRoom(realm: Realm,
-                                 roomId: String,
-                                 roomSync: RoomSync,
-                                 insertType: EventInsertType,
-                                 syncLocalTimestampMillis: Long,
-                                 aggregator: SyncResponsePostTreatmentAggregator): RoomEntity {
+    private suspend fun handleJoinedRoom(realm: Realm,
+                                         roomId: String,
+                                         roomSync: RoomSync,
+                                         insertType: EventInsertType,
+                                         syncLocalTimestampMillis: Long,
+                                         aggregator: SyncResponsePostTreatmentAggregator): RoomEntity {
         Timber.v("Handle join sync for room $roomId")
+        val isInitialSync = insertType == EventInsertType.INITIAL_SYNC
 
         val ephemeralResult = (roomSync.ephemeral as? LazyRoomSyncEphemeral.Parsed)
                 ?._roomSyncEphemeral
@@ -233,7 +241,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                 }
                 // Give info to crypto module
                 cryptoService.onStateEvent(roomId, event)
-                roomMemberEventHandler.handle(realm, roomId, event, aggregator)
+                roomMemberEventHandler.handle(realm, roomId, event, isInitialSync, aggregator)
             }
         }
         if (roomSync.timeline?.events?.isNotEmpty() == true) {
@@ -275,6 +283,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                                   insertType: EventInsertType,
                                   syncLocalTimestampMillis: Long): RoomEntity {
         Timber.v("Handle invited sync for room $roomId")
+        val isInitialSync = insertType == EventInsertType.INITIAL_SYNC
         val roomEntity = RoomEntity.getOrCreate(realm, roomId)
         roomEntity.membership = Membership.INVITE
         if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) {
@@ -288,7 +297,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                     eventId = eventEntity.eventId
                     root = eventEntity
                 }
-                roomMemberEventHandler.handle(realm, roomId, event)
+                roomMemberEventHandler.handle(realm, roomId, event, isInitialSync)
             }
         }
         val inviterEvent = roomSync.inviteState?.events?.lastOrNull {
@@ -304,6 +313,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                                roomSync: RoomSync,
                                insertType: EventInsertType,
                                syncLocalTimestampMillis: Long): RoomEntity {
+        val isInitialSync = insertType == EventInsertType.INITIAL_SYNC
         val roomEntity = RoomEntity.getOrCreate(realm, roomId)
         for (event in roomSync.state?.events.orEmpty()) {
             if (event.eventId == null || event.stateKey == null || event.type == null) {
@@ -315,7 +325,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                 eventId = event.eventId
                 root = eventEntity
             }
-            roomMemberEventHandler.handle(realm, roomId, event)
+            roomMemberEventHandler.handle(realm, roomId, event, isInitialSync)
         }
         for (event in roomSync.timeline?.events.orEmpty()) {
             if (event.eventId == null || event.senderId == null || event.type == null) {
@@ -329,7 +339,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                     root = eventEntity
                 }
                 if (event.type == EventType.STATE_ROOM_MEMBER) {
-                    roomMemberEventHandler.handle(realm, roomEntity.roomId, event)
+                    roomMemberEventHandler.handle(realm, roomEntity.roomId, event, isInitialSync)
                 }
             }
         }
@@ -343,15 +353,15 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
         return roomEntity
     }
 
-    private fun handleTimelineEvents(realm: Realm,
-                                     roomId: String,
-                                     roomEntity: RoomEntity,
-                                     eventList: List<Event>,
-                                     prevToken: String? = null,
-                                     isLimited: Boolean = true,
-                                     insertType: EventInsertType,
-                                     syncLocalTimestampMillis: Long,
-                                     aggregator: SyncResponsePostTreatmentAggregator): ChunkEntity {
+    private suspend fun handleTimelineEvents(realm: Realm,
+                                             roomId: String,
+                                             roomEntity: RoomEntity,
+                                             eventList: List<Event>,
+                                             prevToken: String? = null,
+                                             isLimited: Boolean = true,
+                                             insertType: EventInsertType,
+                                             syncLocalTimestampMillis: Long,
+                                             aggregator: SyncResponsePostTreatmentAggregator): ChunkEntity {
         val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId)
         if (isLimited && lastChunk != null) {
             lastChunk.deleteOnCascade(deleteStateEvents = false, canDeleteRoot = true)
@@ -373,13 +383,15 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                 continue
             }
 
-            eventIds.add(event.eventId)
-            liveEventService.get().dispatchLiveEventReceived(event, roomId, insertType == EventInsertType.INITIAL_SYNC)
-
             val isInitialSync = insertType == EventInsertType.INITIAL_SYNC
 
+            eventIds.add(event.eventId)
+            liveEventService.get().dispatchLiveEventReceived(event, roomId, isInitialSync)
+
             if (event.isEncrypted() && !isInitialSync) {
-                decryptIfNeeded(event, roomId)
+                runBlocking {
+                    decryptIfNeeded(event, roomId)
+                }
             }
             var contentToInject: String? = null
             if (!isInitialSync) {
@@ -394,9 +406,8 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                     root = eventEntity
                 }
                 if (event.type == EventType.STATE_ROOM_MEMBER) {
-                    val fixedContent = event.getFixedRoomMemberContent()
-                    roomMemberContentsByUser[event.stateKey] = fixedContent
-                    roomMemberEventHandler.handle(realm, roomEntity.roomId, event.stateKey, fixedContent, aggregator)
+                    roomMemberContentsByUser[event.stateKey] = event.getFixedRoomMemberContent()
+                    roomMemberEventHandler.handle(realm, roomEntity.roomId, event, isInitialSync, aggregator)
                 }
             }
 
@@ -406,11 +417,28 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                 rootStateEvent?.asDomain()?.getFixedRoomMemberContent()
             }
 
-            chunkEntity.addTimelineEvent(roomId, eventEntity, PaginationDirection.FORWARDS, roomMemberContentsByUser)
+            val timelineEventAdded = chunkEntity.addTimelineEvent(
+                    roomId = roomId,
+                    eventEntity = eventEntity,
+                    direction = PaginationDirection.FORWARDS,
+                    roomMemberContentsByUser = roomMemberContentsByUser)
             if (lightweightSettingsStorage.areThreadMessagesEnabled()) {
                 eventEntity.rootThreadEventId?.let {
                     // This is a thread event
                     optimizedThreadSummaryMap[it] = eventEntity
+                    // Add the same thread timeline event to Thread Chunk
+                    addToThreadChunkIfNeeded(realm, roomId, it, timelineEventAdded, roomEntity)
+                    if (homeServerCapabilitiesService.getHomeServerCapabilities().canUseThreading) {
+                        // Update thread summaries only if homeserver supports threading
+                        ThreadSummaryEntity.createOrUpdate(
+                                threadSummaryType = ThreadSummaryUpdateType.ADD,
+                                realm = realm,
+                                roomId = roomId,
+                                threadEventEntity = eventEntity,
+                                roomMemberContentsByUser = roomMemberContentsByUser,
+                                userId = userId,
+                                roomEntity = roomEntity)
+                    }
                 } ?: run {
                     // This is a normal event or a root thread one
                     optimizedThreadSummaryMap[eventEntity.eventId] = eventEntity
@@ -455,7 +483,29 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
         return chunkEntity
     }
 
-    private fun decryptIfNeeded(event: Event, roomId: String) {
+    /**
+     * Adds new event to the appropriate thread chunk. If the event is already in
+     * the thread timeline and /relations api, we should not added it
+     */
+    private fun addToThreadChunkIfNeeded(realm: Realm,
+                                         roomId: String,
+                                         threadId: String,
+                                         timelineEventEntity: TimelineEventEntity?,
+                                         roomEntity: RoomEntity) {
+        val eventId = timelineEventEntity?.eventId ?: return
+
+        ChunkEntity.findLastForwardChunkOfThread(realm, roomId, threadId)?.let { threadChunk ->
+            val existingEvent = threadChunk.timelineEvents.find(eventId)
+            if (existingEvent?.ownedByThreadChunk == true) {
+                Timber.i("###THREADS RoomSyncHandler event:${timelineEventEntity.eventId} already exists, do not add")
+                return@addToThreadChunkIfNeeded
+            }
+            threadChunk.timelineEvents.add(0, timelineEventEntity)
+            roomEntity.addIfNecessary(threadChunk)
+        }
+    }
+
+    private suspend fun decryptIfNeeded(event: Event, roomId: String) {
         try {
             // Event from sync does not have roomId, so add it to the event first
             val result = cryptoService.decryptEvent(event.copy(roomId = roomId), "")
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
index f3a1523955383185046dcae80ec8d93d7f635440..efc8e39a18d8873fab3926a35ea9ef0633aba034 100644
--- 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
@@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.sync.handler.room
 import com.zhuinden.monarchy.Monarchy
 import io.realm.Realm
 import io.realm.kotlin.where
+import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult
 import org.matrix.android.sdk.api.session.events.model.Content
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
@@ -32,11 +33,11 @@ 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.model.message.MessageType
+import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
 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.settings.LightweightSettingsStorage
 import org.matrix.android.sdk.api.util.JsonDict
-import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
-import org.matrix.android.sdk.internal.database.lightweight.LightweightSettingsStorage
 import org.matrix.android.sdk.internal.database.mapper.ContentMapper
 import org.matrix.android.sdk.internal.database.mapper.EventMapper
 import org.matrix.android.sdk.internal.database.mapper.asDomain
@@ -161,7 +162,7 @@ internal class ThreadsAwarenessHandler @Inject constructor(
                              eventEntity: EventEntity? = null): String? {
         event ?: return null
         roomId ?: return null
-        if (lightweightSettingsStorage.areThreadMessagesEnabled()) return null
+        if (lightweightSettingsStorage.areThreadMessagesEnabled() && !isReplyEvent(event)) return null
         handleRootThreadEventsIfNeeded(realm, roomId, eventEntity, event)
         if (!isThreadEvent(event)) return null
         val eventPayload = if (!event.isEncrypted()) {
@@ -170,8 +171,9 @@ internal class ThreadsAwarenessHandler @Inject constructor(
             event.mxDecryptionResult?.payload?.toMutableMap() ?: return null
         }
         val eventBody = event.getDecryptedTextSummary() ?: return null
+        val threadRelation = getRootThreadRelationContent(event)
         val eventIdToInject = getPreviousEventOrRoot(event) ?: run {
-            return@makeEventThreadAware injectFallbackIndicator(event, eventBody, eventEntity, eventPayload)
+            return@makeEventThreadAware injectFallbackIndicator(event, eventBody, eventEntity, eventPayload, threadRelation)
         }
         val eventToInject = getEventFromDB(realm, eventIdToInject)
         val eventToInjectBody = eventToInject?.getDecryptedTextSummary()
@@ -183,17 +185,19 @@ internal class ThreadsAwarenessHandler @Inject constructor(
                     roomId = roomId,
                     eventBody = eventBody,
                     eventToInject = eventToInject,
-                    eventToInjectBody = eventToInjectBody) ?: return null
+                    eventToInjectBody = eventToInjectBody,
+                    threadRelation = threadRelation) ?: return null
+
             // update the event
             contentForNonEncrypted = updateEventEntity(event, eventEntity, eventPayload, messageTextContent)
         } else {
-            contentForNonEncrypted = injectFallbackIndicator(event, eventBody, eventEntity, eventPayload)
+            contentForNonEncrypted = injectFallbackIndicator(event, eventBody, eventEntity, eventPayload, threadRelation)
         }
 
         // Now lets try to find relations for improved results, while some events may come with reverse order
         eventEntity?.let {
             // When eventEntity is not null means that we are not from within roomSyncHandler
-            handleEventsThatRelatesTo(realm, roomId, event, eventBody, false)
+            handleEventsThatRelatesTo(realm, roomId, event, eventBody, false, threadRelation)
         }
         return contentForNonEncrypted
     }
@@ -205,11 +209,16 @@ internal class ThreadsAwarenessHandler @Inject constructor(
      * @param event the current event received
      * @return The content to inject in the roomSyncHandler live events
      */
-    private fun handleRootThreadEventsIfNeeded(realm: Realm, roomId: String, eventEntity: EventEntity?, event: Event): String? {
+    private fun handleRootThreadEventsIfNeeded(
+            realm: Realm,
+            roomId: String,
+            eventEntity: EventEntity?,
+            event: Event
+    ): String? {
         if (!isThreadEvent(event) && cacheEventRootId.contains(eventEntity?.eventId)) {
             eventEntity?.let {
                 val eventBody = event.getDecryptedTextSummary() ?: return null
-                return handleEventsThatRelatesTo(realm, roomId, event, eventBody, true)
+                return handleEventsThatRelatesTo(realm, roomId, event, eventBody, true, null)
             }
         }
         return null
@@ -224,7 +233,14 @@ internal class ThreadsAwarenessHandler @Inject constructor(
      * @param isFromCache determines whether or not we already know this is root thread event
      * @return The content to inject in the roomSyncHandler live events
      */
-    private fun handleEventsThatRelatesTo(realm: Realm, roomId: String, event: Event, eventBody: String, isFromCache: Boolean): String? {
+    private fun handleEventsThatRelatesTo(
+            realm: Realm,
+            roomId: String,
+            event: Event,
+            eventBody: String,
+            isFromCache: Boolean,
+            threadRelation: RelationDefaultContent?
+    ): String? {
         event.eventId ?: return null
         val rootThreadEventId = if (isFromCache) event.eventId else event.getRootThreadEventId() ?: return null
         eventThatRelatesTo(realm, event.eventId, rootThreadEventId)?.forEach { eventEntityFound ->
@@ -236,7 +252,8 @@ internal class ThreadsAwarenessHandler @Inject constructor(
                     roomId = roomId,
                     eventBody = newEventBody,
                     eventToInject = event,
-                    eventToInjectBody = eventBody) ?: return null
+                    eventToInjectBody = eventBody,
+                    threadRelation = threadRelation) ?: return null
 
             return updateEventEntity(newEventFound, eventEntityFound, newEventPayload, messageTextContent)
         }
@@ -280,7 +297,9 @@ internal class ThreadsAwarenessHandler @Inject constructor(
     private fun injectEvent(roomId: String,
                             eventBody: String,
                             eventToInject: Event,
-                            eventToInjectBody: String): Content? {
+                            eventToInjectBody: String,
+                            threadRelation: RelationDefaultContent?
+    ): Content? {
         val eventToInjectId = eventToInject.eventId ?: return null
         val eventIdToInjectSenderId = eventToInject.senderId.orEmpty()
         val permalink = permalinkFactory.createPermalink(roomId, eventToInjectId, false)
@@ -293,6 +312,7 @@ internal class ThreadsAwarenessHandler @Inject constructor(
                 eventBody)
 
         return MessageTextContent(
+                relatesTo = threadRelation,
                 msgType = MessageType.MSGTYPE_TEXT,
                 format = MessageFormat.FORMAT_MATRIX_HTML,
                 body = eventBody,
@@ -306,12 +326,14 @@ internal class ThreadsAwarenessHandler @Inject constructor(
     private fun injectFallbackIndicator(event: Event,
                                         eventBody: String,
                                         eventEntity: EventEntity?,
-                                        eventPayload: MutableMap<String, Any>): String? {
+                                        eventPayload: MutableMap<String, Any>,
+                                        threadRelation: RelationDefaultContent?): String? {
         val replyFormatted = LocalEchoEventFactory.QUOTE_PATTERN.format(
                 "In reply to a thread",
                 eventBody)
 
         val messageTextContent = MessageTextContent(
+                relatesTo = threadRelation,
                 msgType = MessageType.MSGTYPE_TEXT,
                 format = MessageFormat.FORMAT_MATRIX_HTML,
                 body = eventBody,
@@ -332,7 +354,7 @@ internal class ThreadsAwarenessHandler @Inject constructor(
                 .findAll()
         cacheEventRootId.add(rootThreadEventId)
         return threadList.filter {
-            it.asDomain().getRelationContentForType(RelationType.IO_THREAD)?.inReplyTo?.eventId == currentEventId
+            it.asDomain().getRelationContentForType(RelationType.THREAD)?.inReplyTo?.eventId == currentEventId
         }
     }
 
@@ -350,7 +372,7 @@ internal class ThreadsAwarenessHandler @Inject constructor(
      * @param event
      */
     private fun isThreadEvent(event: Event): Boolean =
-            event.content.toModel<MessageRelationContent>()?.relatesTo?.type == RelationType.IO_THREAD
+            event.content.toModel<MessageRelationContent>()?.relatesTo?.type == RelationType.THREAD
 
     /**
      * Returns the root thread eventId or null otherwise
@@ -359,9 +381,22 @@ internal class ThreadsAwarenessHandler @Inject constructor(
     private fun getRootThreadEventId(event: Event): String? =
             event.content.toModel<MessageRelationContent>()?.relatesTo?.eventId
 
+    private fun getRootThreadRelationContent(event: Event): RelationDefaultContent? =
+            event.content.toModel<MessageRelationContent>()?.relatesTo
+
     private fun getPreviousEventOrRoot(event: Event): String? =
             event.content.toModel<MessageRelationContent>()?.relatesTo?.inReplyTo?.eventId
 
+    /**
+     * Returns if we should html inject the current event.
+     */
+    private fun isReplyEvent(event: Event): Boolean {
+        return isThreadEvent(event) && !isFallingBack(event) && getPreviousEventOrRoot(event) != null
+    }
+
+    private fun isFallingBack(event: Event): Boolean =
+            event.content.toModel<MessageRelationContent>()?.relatesTo?.isFallingBack == true
+
     @Suppress("UNCHECKED_CAST")
     private fun getValueFromPayload(payload: JsonDict?, key: String): String? {
         val content = payload?.get("content") as? JsonDict
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt
index 2460720adc68bc1fd4206b53b267463936f8c007..fc6a4e03d6410fb49e2884fef42a3459a70e791d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt
@@ -39,8 +39,8 @@ import org.matrix.android.sdk.api.session.sync.SyncState
 import org.matrix.android.sdk.api.session.sync.model.SyncResponse
 import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker
 import org.matrix.android.sdk.internal.session.call.ActiveCallHandler
-import org.matrix.android.sdk.internal.session.sync.SyncPresence
 import org.matrix.android.sdk.internal.session.sync.SyncTask
+import org.matrix.android.sdk.internal.settings.DefaultLightweightSettingsStorage
 import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver
 import org.matrix.android.sdk.internal.util.Debouncer
 import org.matrix.android.sdk.internal.util.createUIHandler
@@ -59,7 +59,8 @@ private val loggerTag = LoggerTag("SyncThread", LoggerTag.SYNC)
 internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
                                               private val networkConnectivityChecker: NetworkConnectivityChecker,
                                               private val backgroundDetectionObserver: BackgroundDetectionObserver,
-                                              private val activeCallHandler: ActiveCallHandler
+                                              private val activeCallHandler: ActiveCallHandler,
+                                              private val lightweightSettingsStorage: DefaultLightweightSettingsStorage
 ) : Thread("SyncThread"), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {
 
     private var state: SyncState = SyncState.Idle
@@ -104,10 +105,12 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
 
     fun pause() = synchronized(lock) {
         if (isStarted) {
-            Timber.tag(loggerTag.value).d("Pause sync...")
+            Timber.tag(loggerTag.value).d("Pause sync... Not cancelling incremental sync")
             isStarted = false
             retryNoNetworkTask?.cancel()
-            syncScope.coroutineContext.cancelChildren()
+            // Do not cancel the current incremental sync.
+            // Incremental sync can be long and it requires the user to wait for the treatment to end,
+            // else all is restarted from the beginning each time the user moves the app to foreground.
         }
     }
 
@@ -180,7 +183,8 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
                     else                            -> DEFAULT_LONG_POOL_TIMEOUT
                 }
                 Timber.tag(loggerTag.value).d("Execute sync request with timeout $timeout")
-                val params = SyncTask.Params(timeout, SyncPresence.Online, afterPause = afterPause)
+                val presence = lightweightSettingsStorage.getSyncPresenceStatus()
+                val params = SyncTask.Params(timeout, presence, afterPause = afterPause)
                 val sync = syncScope.launch {
                     previousSyncResponseHasToDevice = doSync(params)
                 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/DefaultLazyRoomSyncEphemeralJsonAdapter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/DefaultLazyRoomSyncEphemeralJsonAdapter.kt
index 012470a07683a7e7936d3bd77c44996cf678f401..c83f658bfeb552e97407b04e730f0a3028913396 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/DefaultLazyRoomSyncEphemeralJsonAdapter.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/DefaultLazyRoomSyncEphemeralJsonAdapter.kt
@@ -21,9 +21,9 @@ import com.squareup.moshi.JsonAdapter
 import com.squareup.moshi.JsonReader
 import com.squareup.moshi.JsonWriter
 import com.squareup.moshi.ToJson
+import org.matrix.android.sdk.api.session.sync.InitialSyncStrategy
 import org.matrix.android.sdk.api.session.sync.model.LazyRoomSyncEphemeral
 import org.matrix.android.sdk.api.session.sync.model.RoomSyncEphemeral
-import org.matrix.android.sdk.internal.session.sync.InitialSyncStrategy
 import org.matrix.android.sdk.internal.session.sync.RoomSyncEphemeralTemporaryStore
 import timber.log.Timber
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/InitialSyncResponseParser.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/InitialSyncResponseParser.kt
index f00cce2d5e8fa7299d1ced78ec1b06d98df350b1..de3269ca1e7afd88bb72eb48cff53c1adb099e63 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/InitialSyncResponseParser.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/InitialSyncResponseParser.kt
@@ -19,8 +19,8 @@ package org.matrix.android.sdk.internal.session.sync.parsing
 import com.squareup.moshi.Moshi
 import okio.buffer
 import okio.source
+import org.matrix.android.sdk.api.session.sync.InitialSyncStrategy
 import org.matrix.android.sdk.api.session.sync.model.SyncResponse
-import org.matrix.android.sdk.internal.session.sync.InitialSyncStrategy
 import org.matrix.android.sdk.internal.session.sync.RoomSyncEphemeralTemporaryStore
 import timber.log.Timber
 import java.io.File
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt
index 6205e3e4b16e2ef5c7f0f4aaabe7091140795aa2..5f62f40ab3f45ace08f439fe87966abd30d723a0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/DefaultTermsService.kt
@@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse
 import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.terms.GetTermsResponse
+import org.matrix.android.sdk.api.session.terms.TermsResponse
 import org.matrix.android.sdk.api.session.terms.TermsService
 import org.matrix.android.sdk.api.util.JsonDict
 import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsAPI.kt
index fb6aff5a9ed4aaa626d2ccb4631af0a9edfd2e12..1f117de67e529e7303d66cc83becba4da03be002 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsAPI.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/terms/TermsAPI.kt
@@ -16,6 +16,7 @@
 
 package org.matrix.android.sdk.internal.session.terms
 
+import org.matrix.android.sdk.api.session.terms.TermsResponse
 import org.matrix.android.sdk.api.util.JsonDict
 import org.matrix.android.sdk.api.util.emptyJsonDict
 import org.matrix.android.sdk.internal.network.HttpHeaders
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/DefaultUserService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/DefaultUserService.kt
index 52b8cc36892422ea50766d714dbf66c16c74d031..4ffc42e714ed1afa75d88832957f043c3090be5f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/DefaultUserService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/DefaultUserService.kt
@@ -18,7 +18,6 @@ package org.matrix.android.sdk.internal.session.user
 
 import androidx.lifecycle.LiveData
 import androidx.paging.PagedList
-import org.matrix.android.sdk.api.session.profile.ProfileService
 import org.matrix.android.sdk.api.session.user.UserService
 import org.matrix.android.sdk.api.session.user.model.User
 import org.matrix.android.sdk.api.util.Optional
@@ -37,16 +36,10 @@ internal class DefaultUserService @Inject constructor(private val userDataSource
     }
 
     override suspend fun resolveUser(userId: String): User {
-        val known = getUser(userId)
-        if (known != null) {
-            return known
-        } else {
+        return getUser(userId) ?: run {
             val params = GetProfileInfoTask.Params(userId)
-            val data = getProfileInfoTask.execute(params)
-            return User(
-                    userId,
-                    data[ProfileService.DISPLAY_NAME_KEY] as? String,
-                    data[ProfileService.AVATAR_URL_KEY] as? String)
+            val json = getProfileInfoTask.execute(params)
+            User.fromJson(userId, json)
         }
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserEntityFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserEntityFactory.kt
index 9a9458e84bdb68ffc1f1f4fece4589bdd9ce0984..46ea7547b03d2e2c21c9acc22270747aa2577a64 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserEntityFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserEntityFactory.kt
@@ -17,6 +17,7 @@
 package org.matrix.android.sdk.internal.session.user
 
 import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
+import org.matrix.android.sdk.api.session.user.model.User
 import org.matrix.android.sdk.internal.database.model.UserEntity
 
 internal object UserEntityFactory {
@@ -24,8 +25,16 @@ internal object UserEntityFactory {
     fun create(userId: String, roomMember: RoomMemberContent): UserEntity {
         return UserEntity(
                 userId = userId,
-                displayName = roomMember.displayName ?: "",
-                avatarUrl = roomMember.avatarUrl ?: ""
+                displayName = roomMember.displayName.orEmpty(),
+                avatarUrl = roomMember.avatarUrl.orEmpty()
+        )
+    }
+
+    fun create(user: User): UserEntity {
+        return UserEntity(
+                userId = user.userId,
+                displayName = user.displayName.orEmpty(),
+                avatarUrl = user.avatarUrl.orEmpty()
         )
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt
index cc5625b2554b3b4d56954d595cf81e58fdd69373..bbeff18c01302a49f67f77748673f56492c69f3e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt
@@ -21,7 +21,7 @@ import retrofit2.http.Body
 import retrofit2.http.PUT
 import retrofit2.http.Path
 
-interface AccountDataAPI {
+internal interface AccountDataAPI {
 
     /**
      * Set some account_data for the client.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt
index ddcac475ee6d2cae50c79281d9e61596ef88a17e..59c4dd671eb25e14ee771a7ca3ffa928773a51a0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt
@@ -23,12 +23,12 @@ import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
 import org.matrix.android.sdk.api.session.events.model.Content
 import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataEvent
 import org.matrix.android.sdk.api.util.Optional
+import org.matrix.android.sdk.api.util.awaitCallback
 import org.matrix.android.sdk.internal.di.SessionDatabase
 import org.matrix.android.sdk.internal.session.room.accountdata.RoomAccountDataDataSource
 import org.matrix.android.sdk.internal.session.sync.handler.UserAccountDataSyncHandler
 import org.matrix.android.sdk.internal.task.TaskExecutor
 import org.matrix.android.sdk.internal.task.configureWith
-import org.matrix.android.sdk.internal.util.awaitCallback
 import javax.inject.Inject
 
 internal class DefaultSessionAccountDataService @Inject constructor(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DirectChatsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DirectChatsHelper.kt
index c7b125b5d6d1b245dc77c19bf9fa1f0433410576..c4fbdc75ab0d14018417dd6d9542948fbd38fcea 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DirectChatsHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DirectChatsHelper.kt
@@ -24,8 +24,9 @@ import org.matrix.android.sdk.internal.di.SessionDatabase
 import org.matrix.android.sdk.internal.session.sync.model.accountdata.DirectMessagesContent
 import javax.inject.Inject
 
-internal class DirectChatsHelper @Inject constructor(@SessionDatabase
-                                                     private val realmConfiguration: RealmConfiguration) {
+internal class DirectChatsHelper @Inject constructor(
+        @SessionDatabase private val realmConfiguration: RealmConfiguration
+) {
 
     /**
      * @return a map of userId <-> list of roomId
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetPostAPIMediator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetPostAPIMediator.kt
index 07f7c7cb868f08a56b4b8ace5fdc4f06eef0f316..10b4f7f7d7d0c73e3d4d30f40b14d1793a22149f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetPostAPIMediator.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetPostAPIMediator.kt
@@ -25,7 +25,6 @@ import org.matrix.android.sdk.api.util.JsonDict
 import org.matrix.android.sdk.internal.util.createUIHandler
 import timber.log.Timber
 import java.lang.reflect.Type
-import java.util.HashMap
 import javax.inject.Inject
 
 internal class DefaultWidgetPostAPIMediator @Inject constructor(private val moshi: Moshi,
@@ -88,10 +87,10 @@ internal class DefaultWidgetPostAPIMediator @Inject constructor(private val mosh
     }
 
     /*
-   * *********************************************************************************************
-   * Message sending methods
-   * *********************************************************************************************
-   */
+     * *********************************************************************************************
+     * Message sending methods
+     * *********************************************************************************************
+     */
 
     /**
      * Send a boolean response
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt
index 9f5f91d917adbdc0e90f8528f5810abb2c0f84a8..e18117d2d7bded184ae2822cb54d24f8e81447ad 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt
@@ -42,7 +42,6 @@ import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
 import org.matrix.android.sdk.internal.session.user.accountdata.UserAccountDataDataSource
 import org.matrix.android.sdk.internal.session.widgets.helper.WidgetFactory
 import org.matrix.android.sdk.internal.session.widgets.helper.extractWidgetSequence
-import java.util.HashMap
 import javax.inject.Inject
 
 @SessionScope
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/settings/DefaultLightweightSettingsStorage.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/settings/DefaultLightweightSettingsStorage.kt
new file mode 100644
index 0000000000000000000000000000000000000000..284d5c99105b93a404168e2750b56af2ba98b7ac
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/settings/DefaultLightweightSettingsStorage.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2022 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.settings
+
+import android.content.Context
+import androidx.core.content.edit
+import androidx.preference.PreferenceManager
+import org.matrix.android.sdk.api.MatrixConfiguration
+import org.matrix.android.sdk.api.settings.LightweightSettingsStorage
+import org.matrix.android.sdk.internal.session.sync.SyncPresence
+import javax.inject.Inject
+
+/**
+ * The purpose of this class is to provide an alternative and lightweight way to store settings/data
+ * on the sdk without using the database. This should be used just for sdk/user preferences and
+ * not for large data sets
+ */
+internal class DefaultLightweightSettingsStorage @Inject constructor(
+        context: Context,
+        private val matrixConfiguration: MatrixConfiguration
+) : LightweightSettingsStorage {
+
+    private val sdkDefaultPrefs = PreferenceManager.getDefaultSharedPreferences(context.applicationContext)
+
+    override fun setThreadMessagesEnabled(enabled: Boolean) {
+        sdkDefaultPrefs.edit {
+            putBoolean(MATRIX_SDK_SETTINGS_THREAD_MESSAGES_ENABLED, enabled)
+        }
+    }
+
+    override fun areThreadMessagesEnabled(): Boolean {
+        return sdkDefaultPrefs.getBoolean(MATRIX_SDK_SETTINGS_THREAD_MESSAGES_ENABLED, matrixConfiguration.threadMessagesEnabledDefault)
+    }
+
+    /**
+     * Set the presence status sent on syncs when the application is in foreground.
+     *
+     * @param presence the presence status that should be sent on sync
+     */
+    internal fun setSyncPresenceStatus(presence: SyncPresence) {
+        sdkDefaultPrefs.edit {
+            putString(MATRIX_SDK_SETTINGS_FOREGROUND_PRESENCE_STATUS, presence.value)
+        }
+    }
+
+    /**
+     * Get the presence status that should be sent on syncs when the application is in foreground.
+     *
+     * @return the presence status that should be sent on sync
+     */
+    internal fun getSyncPresenceStatus(): SyncPresence {
+        val presenceString = sdkDefaultPrefs.getString(MATRIX_SDK_SETTINGS_FOREGROUND_PRESENCE_STATUS, SyncPresence.Online.value)
+        return SyncPresence.from(presenceString) ?: SyncPresence.Online
+    }
+
+    companion object {
+        const val MATRIX_SDK_SETTINGS_THREAD_MESSAGES_ENABLED = "MATRIX_SDK_SETTINGS_THREAD_MESSAGES_ENABLED"
+        private const val MATRIX_SDK_SETTINGS_FOREGROUND_PRESENCE_STATUS = "MATRIX_SDK_SETTINGS_FOREGROUND_PRESENCE_STATUS"
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/settings/SettingsModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/settings/SettingsModule.kt
new file mode 100644
index 0000000000000000000000000000000000000000..db57f695960758cd84bcf3217e1399846aff3af7
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/settings/SettingsModule.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2022 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.settings
+
+import dagger.Binds
+import dagger.Module
+import org.matrix.android.sdk.api.settings.LightweightSettingsStorage
+
+@Module
+internal abstract class SettingsModule {
+    @Binds
+    abstract fun bindLightweightSettingsStorage(storage: DefaultLightweightSettingsStorage): LightweightSettingsStorage
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/BackgroundDetectionObserver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/BackgroundDetectionObserver.kt
index 9c8b36a3ed000d5b56c179e00c2717b6372c5d23..2dd16d83750a26aecf2a9cd4c67b9082cce14736 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/BackgroundDetectionObserver.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/BackgroundDetectionObserver.kt
@@ -20,7 +20,7 @@ import androidx.lifecycle.DefaultLifecycleObserver
 import androidx.lifecycle.LifecycleOwner
 import timber.log.Timber
 
-interface BackgroundDetectionObserver : DefaultLifecycleObserver {
+internal interface BackgroundDetectionObserver : DefaultLifecycleObserver {
     val isInBackground: Boolean
 
     fun register(listener: Listener)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/CompatUtil.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/CompatUtil.kt
deleted file mode 100644
index 81d601f6f0cc4398d77133ee8f77fe812281aa61..0000000000000000000000000000000000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/CompatUtil.kt
+++ /dev/null
@@ -1,299 +0,0 @@
-/*
- * Copyright 2020 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.
- */
-
-@file:Suppress("DEPRECATION")
-
-package org.matrix.android.sdk.internal.util
-
-import android.content.Context
-import android.content.SharedPreferences
-import android.os.Build
-import android.preference.PreferenceManager
-import android.security.KeyPairGeneratorSpec
-import android.security.keystore.KeyGenParameterSpec
-import android.security.keystore.KeyProperties
-import android.util.Base64
-import androidx.core.content.edit
-import timber.log.Timber
-import java.io.IOException
-import java.io.InputStream
-import java.io.OutputStream
-import java.math.BigInteger
-import java.security.InvalidAlgorithmParameterException
-import java.security.InvalidKeyException
-import java.security.KeyPairGenerator
-import java.security.KeyStore
-import java.security.KeyStoreException
-import java.security.NoSuchAlgorithmException
-import java.security.NoSuchProviderException
-import java.security.PrivateKey
-import java.security.SecureRandom
-import java.security.UnrecoverableKeyException
-import java.security.cert.CertificateException
-import java.security.spec.AlgorithmParameterSpec
-import java.security.spec.RSAKeyGenParameterSpec
-import java.util.Calendar
-import javax.crypto.Cipher
-import javax.crypto.CipherInputStream
-import javax.crypto.CipherOutputStream
-import javax.crypto.IllegalBlockSizeException
-import javax.crypto.KeyGenerator
-import javax.crypto.NoSuchPaddingException
-import javax.crypto.SecretKey
-import javax.crypto.spec.GCMParameterSpec
-import javax.crypto.spec.IvParameterSpec
-import javax.crypto.spec.SecretKeySpec
-import javax.security.auth.x500.X500Principal
-
-object CompatUtil {
-    private val TAG = CompatUtil::class.java.simpleName
-    private const val ANDROID_KEY_STORE_PROVIDER = "AndroidKeyStore"
-    private const val AES_GCM_CIPHER_TYPE = "AES/GCM/NoPadding"
-    private const val AES_GCM_KEY_SIZE_IN_BITS = 128
-    private const val AES_GCM_IV_LENGTH = 12
-    private const val AES_LOCAL_PROTECTION_KEY_ALIAS = "aes_local_protection"
-
-    private const val RSA_WRAP_LOCAL_PROTECTION_KEY_ALIAS = "rsa_wrap_local_protection"
-    private const val RSA_WRAP_CIPHER_TYPE = "RSA/NONE/PKCS1Padding"
-    private const val AES_WRAPPED_PROTECTION_KEY_SHARED_PREFERENCE = "aes_wrapped_local_protection"
-
-    private const val SHARED_KEY_ANDROID_VERSION_WHEN_KEY_HAS_BEEN_GENERATED = "android_version_when_key_has_been_generated"
-
-    private var sSecretKeyAndVersion: SecretKeyAndVersion? = null
-
-    /**
-     * Returns the unique SecureRandom instance shared for all local storage encryption operations.
-     */
-    private val prng: SecureRandom by lazy(LazyThreadSafetyMode.NONE) { SecureRandom() }
-
-    /**
-     * Returns the AES key used for local storage encryption/decryption with AES/GCM.
-     * The key is created if it does not exist already in the keystore.
-     * From Marshmallow, this key is generated and operated directly from the android keystore.
-     * From KitKat and before Marshmallow, this key is stored in the application shared preferences
-     * wrapped by a RSA key generated and operated directly from the android keystore.
-     *
-     * @param context the context holding the application shared preferences
-     */
-    @Synchronized
-    @Throws(KeyStoreException::class,
-            CertificateException::class,
-            NoSuchAlgorithmException::class,
-            IOException::class,
-            NoSuchProviderException::class,
-            InvalidAlgorithmParameterException::class,
-            NoSuchPaddingException::class,
-            InvalidKeyException::class,
-            IllegalBlockSizeException::class,
-            UnrecoverableKeyException::class)
-    private fun getAesGcmLocalProtectionKey(context: Context): SecretKeyAndVersion {
-        if (sSecretKeyAndVersion == null) {
-            val keyStore = KeyStore.getInstance(ANDROID_KEY_STORE_PROVIDER)
-            keyStore.load(null)
-
-            Timber.i(TAG, "Loading local protection key")
-
-            var key: SecretKey?
-
-            val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
-            // Get the version of Android when the key has been generated, default to the current version of the system. In this case, the
-            // key will be generated
-            val androidVersionWhenTheKeyHasBeenGenerated = sharedPreferences
-                    .getInt(SHARED_KEY_ANDROID_VERSION_WHEN_KEY_HAS_BEEN_GENERATED, Build.VERSION.SDK_INT)
-
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-                if (keyStore.containsAlias(AES_LOCAL_PROTECTION_KEY_ALIAS)) {
-                    Timber.i(TAG, "AES local protection key found in keystore")
-                    key = keyStore.getKey(AES_LOCAL_PROTECTION_KEY_ALIAS, null) as SecretKey
-                } else {
-                    // Check if a key has been created on version < M (in case of OS upgrade)
-                    key = readKeyApiL(sharedPreferences, keyStore)
-
-                    if (key == null) {
-                        Timber.i(TAG, "Generating AES key with keystore")
-                        val generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE_PROVIDER)
-                        generator.init(
-                                KeyGenParameterSpec.Builder(AES_LOCAL_PROTECTION_KEY_ALIAS,
-                                        KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
-                                        .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
-                                        .setKeySize(AES_GCM_KEY_SIZE_IN_BITS)
-                                        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
-                                        .build())
-                        key = generator.generateKey()
-
-                        sharedPreferences.edit {
-                            putInt(SHARED_KEY_ANDROID_VERSION_WHEN_KEY_HAS_BEEN_GENERATED, Build.VERSION.SDK_INT)
-                        }
-                    }
-                }
-            } else {
-                key = readKeyApiL(sharedPreferences, keyStore)
-
-                if (key == null) {
-                    Timber.i(TAG, "Generating RSA key pair with keystore")
-                    val generator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, ANDROID_KEY_STORE_PROVIDER)
-                    val start = Calendar.getInstance()
-                    val end = Calendar.getInstance()
-                    end.add(Calendar.YEAR, 10)
-
-                    generator.initialize(
-                            KeyPairGeneratorSpec.Builder(context)
-                                    .setAlgorithmParameterSpec(RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4))
-                                    .setAlias(RSA_WRAP_LOCAL_PROTECTION_KEY_ALIAS)
-                                    .setSubject(X500Principal("CN=matrix-android-sdk"))
-                                    .setStartDate(start.time)
-                                    .setEndDate(end.time)
-                                    .setSerialNumber(BigInteger.ONE)
-                                    .build())
-                    val keyPair = generator.generateKeyPair()
-
-                    Timber.i(TAG, "Generating wrapped AES key")
-
-                    val aesKeyRaw = ByteArray(AES_GCM_KEY_SIZE_IN_BITS / java.lang.Byte.SIZE)
-                    prng.nextBytes(aesKeyRaw)
-                    key = SecretKeySpec(aesKeyRaw, "AES")
-
-                    val cipher = Cipher.getInstance(RSA_WRAP_CIPHER_TYPE)
-                    cipher.init(Cipher.WRAP_MODE, keyPair.public)
-                    val wrappedAesKey = cipher.wrap(key)
-
-                    sharedPreferences.edit {
-                        putString(AES_WRAPPED_PROTECTION_KEY_SHARED_PREFERENCE, Base64.encodeToString(wrappedAesKey, 0))
-                        putInt(SHARED_KEY_ANDROID_VERSION_WHEN_KEY_HAS_BEEN_GENERATED, Build.VERSION.SDK_INT)
-                    }
-                }
-            }
-
-            sSecretKeyAndVersion = SecretKeyAndVersion(key!!, androidVersionWhenTheKeyHasBeenGenerated)
-        }
-
-        return sSecretKeyAndVersion!!
-    }
-
-    /**
-     * Read the key, which may have been stored when the OS was < M
-     *
-     * @param sharedPreferences shared pref
-     * @param keyStore          key store
-     * @return the key if it exists or null
-     */
-    @Throws(KeyStoreException::class,
-            NoSuchPaddingException::class,
-            NoSuchAlgorithmException::class,
-            InvalidKeyException::class,
-            UnrecoverableKeyException::class)
-    private fun readKeyApiL(sharedPreferences: SharedPreferences, keyStore: KeyStore): SecretKey? {
-        val wrappedAesKeyString = sharedPreferences.getString(AES_WRAPPED_PROTECTION_KEY_SHARED_PREFERENCE, null)
-        if (wrappedAesKeyString != null && keyStore.containsAlias(RSA_WRAP_LOCAL_PROTECTION_KEY_ALIAS)) {
-            Timber.i(TAG, "RSA + wrapped AES local protection keys found in keystore")
-            val privateKey = keyStore.getKey(RSA_WRAP_LOCAL_PROTECTION_KEY_ALIAS, null) as PrivateKey
-            val wrappedAesKey = Base64.decode(wrappedAesKeyString, 0)
-            val cipher = Cipher.getInstance(RSA_WRAP_CIPHER_TYPE)
-            cipher.init(Cipher.UNWRAP_MODE, privateKey)
-            return cipher.unwrap(wrappedAesKey, "AES", Cipher.SECRET_KEY) as SecretKey
-        }
-
-        // Key does not exist
-        return null
-    }
-
-    /**
-     * Create a CipherOutputStream instance.
-     * Before Kitkat, this method will return out as local storage encryption is not implemented for
-     * devices before KitKat.
-     *
-     * @param out     the output stream
-     * @param context the context holding the application shared preferences
-     */
-    @Throws(IOException::class,
-            CertificateException::class,
-            NoSuchAlgorithmException::class,
-            UnrecoverableKeyException::class,
-            InvalidKeyException::class,
-            InvalidAlgorithmParameterException::class,
-            NoSuchPaddingException::class,
-            NoSuchProviderException::class,
-            KeyStoreException::class,
-            IllegalBlockSizeException::class)
-    fun createCipherOutputStream(out: OutputStream, context: Context): OutputStream? {
-        val keyAndVersion = getAesGcmLocalProtectionKey(context)
-
-        val cipher = Cipher.getInstance(AES_GCM_CIPHER_TYPE)
-        val iv: ByteArray
-
-        if (keyAndVersion.androidVersionWhenTheKeyHasBeenGenerated >= Build.VERSION_CODES.M) {
-            cipher.init(Cipher.ENCRYPT_MODE, keyAndVersion.secretKey)
-            iv = cipher.iv
-        } else {
-            iv = ByteArray(AES_GCM_IV_LENGTH)
-            prng.nextBytes(iv)
-            cipher.init(Cipher.ENCRYPT_MODE, keyAndVersion.secretKey, IvParameterSpec(iv))
-        }
-
-        if (iv.size != AES_GCM_IV_LENGTH) {
-            Timber.e(TAG, "Invalid IV length ${iv.size}")
-            return null
-        }
-
-        out.write(iv.size)
-        out.write(iv)
-
-        return CipherOutputStream(out, cipher)
-    }
-
-    /**
-     * Create a CipherInputStream instance.
-     * Warning, if inputStream is not an encrypted stream, it's up to the caller to close and reopen inputStream, because the stream has been read.
-     *
-     * @param inputStream the input stream
-     * @param context     the context holding the application shared preferences
-     * @return inputStream, or the created InputStream, or null if the InputStream inputStream does not contain encrypted data
-     */
-    @Throws(NoSuchPaddingException::class,
-            NoSuchAlgorithmException::class,
-            CertificateException::class,
-            InvalidKeyException::class,
-            KeyStoreException::class,
-            UnrecoverableKeyException::class,
-            IllegalBlockSizeException::class,
-            NoSuchProviderException::class,
-            InvalidAlgorithmParameterException::class,
-            IOException::class)
-    fun createCipherInputStream(inputStream: InputStream, context: Context): InputStream? {
-        val ivLen = inputStream.read()
-        if (ivLen != AES_GCM_IV_LENGTH) {
-            Timber.e(TAG, "Invalid IV length $ivLen")
-            return null
-        }
-
-        val iv = ByteArray(AES_GCM_IV_LENGTH)
-        inputStream.read(iv)
-
-        val cipher = Cipher.getInstance(AES_GCM_CIPHER_TYPE)
-
-        val keyAndVersion = getAesGcmLocalProtectionKey(context)
-
-        val spec: AlgorithmParameterSpec = if (keyAndVersion.androidVersionWhenTheKeyHasBeenGenerated >= Build.VERSION_CODES.M) {
-            GCMParameterSpec(AES_GCM_KEY_SIZE_IN_BITS, iv)
-        } else {
-            IvParameterSpec(iv)
-        }
-
-        cipher.init(Cipher.DECRYPT_MODE, keyAndVersion.secretKey, spec)
-
-        return CipherInputStream(inputStream, cipher)
-    }
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FileSaver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FileSaver.kt
index fb5e3a5774103f4466082d1c6472d8a72f5e19be..3fcf35c127cec270d04dc42c73e592cbf7543d04 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FileSaver.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FileSaver.kt
@@ -24,7 +24,7 @@ import java.io.InputStream
  * Save an input stream to a file with Okio
  */
 @WorkerThread
-fun writeToFile(inputStream: InputStream, outputFile: File) {
+internal fun writeToFile(inputStream: InputStream, outputFile: File) {
     // Ensure the parent folder exists, else it will crash
     outputFile.parentFile?.mkdirs()
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/JsonCanonicalizer.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/JsonCanonicalizer.kt
index a34b91a70bbc0b458642ddfafb63813c4b0c679d..5994cbcf93fca7ac35a32f0609438f81dbc9de9c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/JsonCanonicalizer.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/JsonCanonicalizer.kt
@@ -28,7 +28,7 @@ import java.util.TreeSet
  * Build canonical Json
  * Doc: https://matrix.org/docs/spec/appendices.html#canonical-json
  */
-object JsonCanonicalizer {
+internal object JsonCanonicalizer {
 
     fun <T> getCanonicalJson(type: Class<T>, o: T): String {
         val adapter = MoshiProvider.providesMoshi().adapter<T>(type)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/LiveDataUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/LiveDataUtils.kt
deleted file mode 100644
index 80c3b8322654780e0981618cfd49b63794415c76..0000000000000000000000000000000000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/LiveDataUtils.kt
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright 2020 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.util
-
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.MediatorLiveData
-
-object LiveDataUtils {
-
-    fun <FIRST, SECOND, OUT> combine(firstSource: LiveData<FIRST>,
-                                     secondSource: LiveData<SECOND>,
-                                     mapper: (FIRST, SECOND) -> OUT): LiveData<OUT> {
-        return MediatorLiveData<OUT>().apply {
-            var firstValue: FIRST? = null
-            var secondValue: SECOND? = null
-
-            val valueDispatcher = {
-                firstValue?.let { safeFirst ->
-                    secondValue?.let { safeSecond ->
-                        val mappedValue = mapper(safeFirst, safeSecond)
-                        postValue(mappedValue)
-                    }
-                }
-            }
-
-            addSource(firstSource) {
-                firstValue = it
-                valueDispatcher()
-            }
-
-            addSource(secondSource) {
-                secondValue = it
-                valueDispatcher()
-            }
-        }
-    }
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Monarchy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Monarchy.kt
index afe77d76d551137c05a9ad2d5f8d69930f078b13..6152eacae55b7ad723835e7b1e12b188725638c4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Monarchy.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Monarchy.kt
@@ -26,7 +26,7 @@ internal suspend fun <T> Monarchy.awaitTransaction(transaction: suspend (realm:
     return awaitTransaction(realmConfiguration, transaction)
 }
 
-fun <T : RealmModel> Monarchy.fetchCopied(query: (Realm) -> T?): T? {
+internal fun <T : RealmModel> Monarchy.fetchCopied(query: (Realm) -> T?): T? {
     val ref = AtomicReference<T>()
     doWithRealm { realm ->
         val result = query.invoke(realm)?.let {
@@ -37,7 +37,7 @@ fun <T : RealmModel> Monarchy.fetchCopied(query: (Realm) -> T?): T? {
     return ref.get()
 }
 
-fun <U, T : RealmModel> Monarchy.fetchCopyMap(query: (Realm) -> T?, map: (T, realm: Realm) -> U): U? {
+internal fun <U, T : RealmModel> Monarchy.fetchCopyMap(query: (Realm) -> T?, map: (T, realm: Realm) -> U): U? {
     val ref = AtomicReference<U?>()
     doWithRealm { realm ->
         val result = query.invoke(realm)?.let {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Normalizer.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Normalizer.kt
index 0e9c885394a1b43db82bb56bf72b64841b6f7896..05105211277f230e32d9eb3febbdbe7f84fbdff4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Normalizer.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Normalizer.kt
@@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.util
 import java.text.Normalizer
 import javax.inject.Inject
 
-class Normalizer @Inject constructor() {
+internal class Normalizer @Inject constructor() {
 
     fun normalize(input: String): String {
         return Normalizer.normalize(input.lowercase(), Normalizer.Form.NFD)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/database/RealmMigrator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/database/RealmMigrator.kt
index 15e82f3cc0f570c62c607106a6c021d66d8da143..f22f0810a146c31176cb1d034286b9eed623fdb7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/database/RealmMigrator.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/database/RealmMigrator.kt
@@ -20,7 +20,7 @@ import io.realm.DynamicRealm
 import io.realm.RealmObjectSchema
 import timber.log.Timber
 
-abstract class RealmMigrator(private val realm: DynamicRealm,
+internal abstract class RealmMigrator(private val realm: DynamicRealm,
                              private val targetSchemaVersion: Int) {
     fun perform() {
         Timber.d("Migrate ${realm.configuration.realmFileName} to $targetSchemaVersion")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/SessionWorkerParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/SessionWorkerParams.kt
index c6c038d2d245e14c2b3f9bbb863e9adcd0059160..de36b85660f0d81766280fe335b0c0b3a9eb8489 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/SessionWorkerParams.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/SessionWorkerParams.kt
@@ -20,7 +20,7 @@ package org.matrix.android.sdk.internal.worker
  * Note about the Worker usage:
  * The workers we chain, or when using the append strategy, should never return Result.Failure(), else the chain will be broken forever
  */
-interface SessionWorkerParams {
+internal interface SessionWorkerParams {
     val sessionId: String
 
     /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/androidsdk/crypto/data/MXDeviceInfo.java b/matrix-sdk-android/src/main/java/org/matrix/androidsdk/crypto/data/MXDeviceInfo.java
index 1014ceda0e56a5988cc4abaa84cb24d0c9588683..4612b8d6ff2625572bea999741fbe8c84792c2a6 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/androidsdk/crypto/data/MXDeviceInfo.java
+++ b/matrix-sdk-android/src/main/java/org/matrix/androidsdk/crypto/data/MXDeviceInfo.java
@@ -20,10 +20,9 @@ import java.io.Serializable;
 import java.util.List;
 import java.util.Map;
 
-/*
- * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose
+/**
+ * <b>IMPORTANT:</b> This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose
  */
-
 public class MXDeviceInfo implements Serializable {
     private static final long serialVersionUID = 20129670646382964L;
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/androidsdk/crypto/data/MXOlmInboundGroupSession2.java b/matrix-sdk-android/src/main/java/org/matrix/androidsdk/crypto/data/MXOlmInboundGroupSession2.java
index 7277a86e9ac954d227dfcde929139ce6e29ed9ca..c6a8c1443c20089224f4ed554beba523350b8664 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/androidsdk/crypto/data/MXOlmInboundGroupSession2.java
+++ b/matrix-sdk-android/src/main/java/org/matrix/androidsdk/crypto/data/MXOlmInboundGroupSession2.java
@@ -23,11 +23,9 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
-/*
- * IMPORTANT: This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose
- */
-
 /**
+ * <b>IMPORTANT:</b> This class is imported from Riot-Android to be able to perform a migration. Do not use it for any other purpose
+ *
  * This class adds more context to a OLMInboundGroupSession object.
  * This allows additional checks. The class implements NSCoding so that the context can be stored.
  */
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt
index 96655b849dffb144c59d0e15768e6e16b4caa834..088e16095031cd085e492900a755ae4647fffffe 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/auth/data/VersionsKtTest.kt
@@ -19,6 +19,7 @@ package org.matrix.android.sdk.api.auth.data
 import org.amshove.kluent.shouldBe
 import org.junit.Test
 import org.matrix.android.sdk.internal.auth.version.Versions
+import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreads
 import org.matrix.android.sdk.internal.auth.version.isSupportedBySdk
 
 class VersionsKtTest {
@@ -53,5 +54,20 @@ class VersionsKtTest {
         Versions(supportedVersions = listOf("r0.5.0", "r0.6.0")).isSupportedBySdk() shouldBe true
         Versions(supportedVersions = listOf("r0.5.0", "r0.6.1")).isSupportedBySdk() shouldBe true
         Versions(supportedVersions = listOf("r0.6.0")).isSupportedBySdk() shouldBe true
+        Versions(supportedVersions = listOf("v1.6.0")).isSupportedBySdk() shouldBe true
+    }
+
+    @Test
+    fun doesServerSupportThreads() {
+        Versions(supportedVersions = listOf("r0.6.0")).doesServerSupportThreads() shouldBe false
+        Versions(supportedVersions = listOf("r0.9.1")).doesServerSupportThreads() shouldBe false
+        Versions(supportedVersions = listOf("v1.2.0")).doesServerSupportThreads() shouldBe false
+        Versions(supportedVersions = listOf("v1.3.0")).doesServerSupportThreads() shouldBe true
+        Versions(supportedVersions = listOf("v1.3.1")).doesServerSupportThreads() shouldBe true
+        Versions(supportedVersions = listOf("v1.5.1")).doesServerSupportThreads() shouldBe true
+        Versions(supportedVersions = listOf("r0.6.0"), unstableFeatures = mapOf("org.matrix.msc3440.stable" to true)).doesServerSupportThreads() shouldBe true
+        Versions(supportedVersions = listOf("v1.2.1"), unstableFeatures = mapOf("org.matrix.msc3440.stable" to true)).doesServerSupportThreads() shouldBe true
+        Versions(supportedVersions = listOf("r0.6.0"), unstableFeatures = mapOf("org.matrix.msc3440.stable" to false)).doesServerSupportThreads() shouldBe false
+        Versions(supportedVersions = listOf("v1.4.0"), unstableFeatures = mapOf("org.matrix.msc3440.stable" to false)).doesServerSupportThreads() shouldBe true
     }
 }
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/RecoveryKeyTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/RecoveryKeyTest.kt
index 4e4548b19728892235824ddfd743bba568bac73e..d4c9da2986e782882e18071a29558cef3657d374 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/RecoveryKeyTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/keysbackup/util/RecoveryKeyTest.kt
@@ -22,6 +22,9 @@ import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.Test
 import org.matrix.android.sdk.MatrixTest
+import org.matrix.android.sdk.api.session.crypto.keysbackup.computeRecoveryKey
+import org.matrix.android.sdk.api.session.crypto.keysbackup.extractCurveKeyFromRecoveryKey
+import org.matrix.android.sdk.api.session.crypto.keysbackup.isValidRecoveryKey
 
 class RecoveryKeyTest : MatrixTest {
 
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/store/db/HelperTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/store/db/HelperTest.kt
index b50d0581b0ce485348c837623c1189856da8e096..39b3c5c731cc6e4236edbf7b81d2cb504ccf8374 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/store/db/HelperTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/store/db/HelperTest.kt
@@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.crypto.store.db
 import org.junit.Assert.assertEquals
 import org.junit.Test
 import org.matrix.android.sdk.MatrixTest
-import org.matrix.android.sdk.internal.util.md5
+import org.matrix.android.sdk.api.util.md5
 
 class HelperTest : MatrixTest {
 
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt
index c8be0f54871b23e29b0fdd0ce95c89157ea43d9d..31fd86fe65ecf172a167ac2febfb193973c511ae 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt
@@ -16,7 +16,8 @@
 
 package org.matrix.android.sdk.internal.session.pushers
 
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
 import org.amshove.kluent.internal.assertFailsWith
 import org.amshove.kluent.shouldBeEqualTo
 import org.junit.Test
@@ -39,6 +40,7 @@ private val A_JSON_PUSHER = JsonPusher(
         data = JsonPusherData(brand = "Element")
 )
 
+@ExperimentalCoroutinesApi
 class DefaultAddPusherTaskTest {
 
     private val pushersAPI = FakePushersAPI()
@@ -55,7 +57,7 @@ class DefaultAddPusherTaskTest {
     fun `given no persisted pusher when adding Pusher then updates api and inserts result with Registered state`() {
         monarchy.givenWhereReturns<PusherEntity>(result = null)
 
-        runBlocking { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
+        runTest { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
 
         pushersAPI.verifySetPusher(A_JSON_PUSHER)
         monarchy.verifyInsertOrUpdate<PusherEntity> {
@@ -70,7 +72,7 @@ class DefaultAddPusherTaskTest {
         val realmResult = PusherEntity(appDisplayName = null)
         monarchy.givenWhereReturns(result = realmResult)
 
-        runBlocking { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
+        runTest { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
 
         pushersAPI.verifySetPusher(A_JSON_PUSHER)
 
@@ -85,7 +87,7 @@ class DefaultAddPusherTaskTest {
         pushersAPI.givenSetPusherErrors(SocketException())
 
         assertFailsWith<SocketException> {
-            runBlocking { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
+            runTest { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
         }
 
         realmResult.state shouldBeEqualTo PusherState.FAILED_TO_REGISTER
@@ -97,7 +99,7 @@ class DefaultAddPusherTaskTest {
         pushersAPI.givenSetPusherErrors(SocketException())
 
         assertFailsWith<SocketException> {
-            runBlocking { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
+            runTest { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
         }
     }
 }
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/space/DefaultResolveSpaceInfoTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/space/DefaultResolveSpaceInfoTaskTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7203f89629049dd2e97195163c9f03ced3565885
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/space/DefaultResolveSpaceInfoTaskTest.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.space
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import okhttp3.ResponseBody.Companion.toResponseBody
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.Test
+import org.matrix.android.sdk.test.fakes.FakeGlobalErrorReceiver
+import org.matrix.android.sdk.test.fakes.FakeSpaceApi
+import org.matrix.android.sdk.test.fixtures.SpacesResponseFixture.aSpacesResponse
+import retrofit2.HttpException
+import retrofit2.Response
+
+@ExperimentalCoroutinesApi
+internal class DefaultResolveSpaceInfoTaskTest {
+
+    private val spaceApi = FakeSpaceApi()
+    private val globalErrorReceiver = FakeGlobalErrorReceiver()
+    private val resolveSpaceInfoTask = DefaultResolveSpaceInfoTask(spaceApi.instance, globalErrorReceiver)
+
+    @Test
+    fun `given stable endpoint works, when execute, then return stable api data`() = runTest {
+        spaceApi.givenStableEndpointReturns(response)
+
+        val result = resolveSpaceInfoTask.execute(spaceApi.params)
+
+        result shouldBeEqualTo response
+    }
+
+    @Test
+    fun `given stable endpoint fails, when execute, then fallback to unstable endpoint`() = runTest {
+        spaceApi.givenStableEndpointThrows(httpException)
+        spaceApi.givenUnstableEndpointReturns(response)
+
+        val result = resolveSpaceInfoTask.execute(spaceApi.params)
+
+        result shouldBeEqualTo response
+    }
+
+    companion object {
+        private val response = aSpacesResponse()
+        private val httpException = HttpException(Response.error<SpacesResponse>(500, "".toResponseBody()))
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/task/CoroutineSequencersTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/task/CoroutineSequencersTest.kt
index 0abca8bee3dc47d7f4b29f9830699b16a8846738..149b964fd25a09f832732887a6f0334ccc2593fa 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/task/CoroutineSequencersTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/task/CoroutineSequencersTest.kt
@@ -21,7 +21,7 @@ import kotlinx.coroutines.asCoroutineDispatcher
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.joinAll
 import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
 import org.junit.Assert.assertEquals
 import org.junit.Test
 import org.matrix.android.sdk.MatrixTest
@@ -51,7 +51,7 @@ class CoroutineSequencersTest : MatrixTest {
                             .also { results.add(it) }
                 }
         )
-        runBlocking {
+        runTest {
             jobs.joinAll()
         }
         assertEquals(3, results.size)
@@ -81,7 +81,7 @@ class CoroutineSequencersTest : MatrixTest {
                             .also { results.add(it) }
                 }
         )
-        runBlocking {
+        runTest {
             jobs.joinAll()
         }
         assertEquals(3, results.size)
@@ -109,7 +109,7 @@ class CoroutineSequencersTest : MatrixTest {
         )
         // We are canceling the second job
         jobs[1].cancel()
-        runBlocking {
+        runTest {
             jobs.joinAll()
         }
         assertEquals(2, results.size)
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSpaceApi.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSpaceApi.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d4fc9867913965da8cc6ec101f71d5df7dcd9491
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSpaceApi.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.test.fakes
+
+import io.mockk.coEvery
+import io.mockk.mockk
+import org.matrix.android.sdk.internal.session.space.SpaceApi
+import org.matrix.android.sdk.internal.session.space.SpacesResponse
+import org.matrix.android.sdk.test.fixtures.ResolveSpaceInfoTaskParamsFixture
+
+internal class FakeSpaceApi {
+
+    val instance: SpaceApi = mockk()
+    val params = ResolveSpaceInfoTaskParamsFixture.aResolveSpaceInfoTaskParams()
+
+    fun givenStableEndpointReturns(response: SpacesResponse) {
+        coEvery { instance.getSpaceHierarchy(params.spaceId, params.suggestedOnly, params.limit, params.maxDepth, params.from) } returns response
+    }
+
+    fun givenStableEndpointThrows(throwable: Throwable) {
+        coEvery { instance.getSpaceHierarchy(params.spaceId, params.suggestedOnly, params.limit, params.maxDepth, params.from) } throws throwable
+    }
+
+    fun givenUnstableEndpointReturns(response: SpacesResponse) {
+        coEvery { instance.getSpaceHierarchyUnstable(params.spaceId, params.suggestedOnly, params.limit, params.maxDepth, params.from) } returns response
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/ResolveSpaceInfoTaskParamsFixture.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/ResolveSpaceInfoTaskParamsFixture.kt
new file mode 100644
index 0000000000000000000000000000000000000000..28f8c3637dc8157cae5b31aaf13947f3106062c2
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/ResolveSpaceInfoTaskParamsFixture.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.test.fixtures
+
+import org.matrix.android.sdk.internal.session.space.ResolveSpaceInfoTask
+
+internal object ResolveSpaceInfoTaskParamsFixture {
+    fun aResolveSpaceInfoTaskParams(
+            spaceId: String = "",
+            limit: Int? = null,
+            maxDepth: Int? = null,
+            from: String? = null,
+            suggestedOnly: Boolean? = null,
+    ) = ResolveSpaceInfoTask.Params(
+            spaceId,
+            limit,
+            maxDepth,
+            from,
+            suggestedOnly,
+    )
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/SpacesResponseFixture.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/SpacesResponseFixture.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0a08331114455e454e461a0d337fe9bdb33bba0b
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/SpacesResponseFixture.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.test.fixtures
+
+import org.matrix.android.sdk.internal.session.space.SpaceChildSummaryResponse
+import org.matrix.android.sdk.internal.session.space.SpacesResponse
+
+internal object SpacesResponseFixture {
+    fun aSpacesResponse(
+            nextBatch: String? = null,
+            rooms: List<SpaceChildSummaryResponse>? = null,
+    ) = SpacesResponse(
+            nextBatch,
+            rooms,
+    )
+}