From 05a767daeb77bae55aa03adee0a433656350e2ea Mon Sep 17 00:00:00 2001
From: Benoit Marty <benoit@matrix.org>
Date: Tue, 13 Sep 2022 10:32:01 +0200
Subject: [PATCH] Import v1.4.36 from Element Android

---
 dependencies.gradle                           |   5 +-
 matrix-sdk-android/build.gradle               |   4 +-
 .../sdk/internal/crypto/CryptoStoreHelper.kt  |   2 +
 .../sdk/internal/crypto/E2eeSanityTests.kt    |   4 +-
 .../crypto/crosssigning/XSigningTest.kt       |   2 +-
 .../internal/crypto/verification/SASTest.kt   |   4 +-
 .../sdk/api/session/crypto/CryptoService.kt   |  27 +-
 .../sdk/api/session/events/model/EventType.kt |   3 +
 .../sdk/api/session/identity/ThreePid.kt      |   4 +
 .../room/model/create/CreateRoomParams.kt     |  27 +-
 .../room/model/create/CreateRoomStateEvent.kt |   2 +
 .../internal/crypto/DefaultCryptoService.kt   |  31 +-
 .../DefaultCrossSigningService.kt             |   5 +
 .../crypto/crosssigning/UpdateTrustWorker.kt  |   2 +
 .../internal/crypto/store/IMXCryptoStore.kt   |   4 +
 .../crypto/store/db/RealmCryptoStore.kt       |  32 +-
 .../MyDeviceLastSeenInfoEntityMapper.kt       |  33 ++
 .../internal/crypto/tasks/SendEventTask.kt    |  16 +
 .../database/RealmSessionStoreMigration.kt    |   4 +-
 .../database/migration/MigrateSessionTo036.kt |  33 ++
 .../database/model/LocalRoomSummaryEntity.kt  |  39 ++
 .../database/model/SessionRealmModule.kt      |   1 +
 .../query/LocalRoomSummaryEntityQueries.kt    |  31 ++
 .../android/sdk/internal/di/MoshiProvider.kt  |   8 +
 .../android/sdk/internal/di/NetworkModule.kt  |   2 +-
 .../interceptors/FormattedJsonHttpLogger.kt   |  12 +-
 .../sdk/internal/session/SessionComponent.kt  |   3 +
 .../sdk/internal/session/room/RoomModule.kt   |  10 +
 .../create/CreateLocalRoomStateEventsTask.kt  | 299 ++++++++++++
 .../room/create/CreateLocalRoomTask.kt        | 128 ++---
 .../session/room/create/CreateRoomBody.kt     |   9 +-
 .../create/CreateRoomFromLocalRoomTask.kt     | 149 ++++++
 .../session/room/create/CreateRoomTask.kt     |   5 +-
 .../LocalRoomThirdPartyInviteContent.kt       |  34 ++
 .../room/delete/DeleteLocalRoomTask.kt        |   4 +
 .../room/membership/RoomMemberEventHandler.kt |   3 +-
 .../membership/threepid/ThreePidInviteBody.kt |   8 +
 .../session/room/state/SendStateTask.kt       |  46 +-
 .../room/summary/RoomSummaryUpdater.kt        |  14 +-
 .../session/sync/SyncResponseHandler.kt       |  26 +-
 .../SyncResponsePostTreatmentAggregator.kt    |   7 +-
 ...cResponsePostTreatmentAggregatorHandler.kt |  58 ++-
 .../session/sync/handler/UpdateUserWorker.kt  |  99 ++++
 .../sync/handler/room/RoomSyncHandler.kt      |  17 +-
 .../internal/worker/MatrixWorkerFactory.kt    |   3 +
 .../interceptors/FormattedJsonHttpLogger.kt   |  30 --
 .../MyDeviceLastSeenInfoEntityMapperTest.kt   |  52 ++
 ...faultCreateLocalRoomStateEventsTaskTest.kt | 462 ++++++++++++++++++
 .../DefaultCreateRoomFromLocalRoomTaskTest.kt | 158 ++++++
 ...faultGetActiveBeaconInfoForUserTaskTest.kt |   3 +-
 .../android/sdk/test/fakes/FakeMonarchy.kt    |   8 +-
 .../test/fakes/FakeStateEventDataSource.kt    |   6 +-
 52 files changed, 1729 insertions(+), 249 deletions(-)
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/MyDeviceLastSeenInfoEntityMapper.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo036.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/LocalRoomSummaryEntity.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LocalRoomSummaryEntityQueries.kt
 rename matrix-sdk-android/src/{debug => main}/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt (84%)
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomStateEventsTask.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomFromLocalRoomTask.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/LocalRoomThirdPartyInviteContent.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UpdateUserWorker.kt
 delete mode 100644 matrix-sdk-android/src/release/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt
 create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/MyDeviceLastSeenInfoEntityMapperTest.kt
 create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/create/DefaultCreateLocalRoomStateEventsTaskTest.kt
 create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/create/DefaultCreateRoomFromLocalRoomTaskTest.kt

diff --git a/dependencies.gradle b/dependencies.gradle
index 5083dd45..3759763f 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -22,7 +22,7 @@ def markwon = "4.6.2"
 def moshi = "1.13.0"
 def lifecycle = "2.5.1"
 def flowBinding = "1.2.0"
-def flipper = "0.157.0"
+def flipper = "0.163.0"
 def epoxy = "4.6.2"
 def mavericks = "2.7.0"
 def glide = "4.13.2"
@@ -85,6 +85,8 @@ ext.libs = [
                 'material'                : "com.google.android.material:material:1.6.1",
                 'appdistributionApi'      : "com.google.firebase:firebase-appdistribution-api-ktx:$appDistribution",
                 'appdistribution'         : "com.google.firebase:firebase-appdistribution:$appDistribution",
+                // Phone number https://github.com/google/libphonenumber
+                'phonenumber'             : "com.googlecode.libphonenumber:libphonenumber:8.12.54"
         ],
         dagger      : [
                 'dagger'                  : "com.google.dagger:dagger:$dagger",
@@ -104,6 +106,7 @@ ext.libs = [
                 'moshi'                  : "com.squareup.moshi:moshi:$moshi",
                 'moshiKt'                : "com.squareup.moshi:moshi-kotlin:$moshi",
                 'moshiKotlin'            : "com.squareup.moshi:moshi-kotlin-codegen:$moshi",
+                'moshiAdapters'          : "com.squareup.moshi:moshi-adapters:$moshi",
                 'retrofit'               : "com.squareup.retrofit2:retrofit:$retrofit",
                 'retrofitMoshi'          : "com.squareup.retrofit2:converter-moshi:$retrofit"
         ],
diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle
index bcd829ce..9dbc5f42 100644
--- a/matrix-sdk-android/build.gradle
+++ b/matrix-sdk-android/build.gradle
@@ -166,6 +166,7 @@ dependencies {
     implementation 'com.squareup.okhttp3:logging-interceptor'
 
     implementation libs.squareup.moshi
+    implementation libs.squareup.moshiAdapters
     kapt libs.squareup.moshiKotlin
 
     api "com.atlassian.commonmark:commonmark:0.13.0"
@@ -201,8 +202,7 @@ dependencies {
     // Exif data handling
     implementation libs.apache.commonsImaging
 
-    // Phone number https://github.com/google/libphonenumber
-    implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.54'
+    implementation libs.google.phonenumber
 
     testImplementation libs.tests.junit
     // Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt
index ba1afd47..48cfbebe 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/CryptoStoreHelper.kt
@@ -21,6 +21,7 @@ import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStore
 import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule
 import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper
+import org.matrix.android.sdk.internal.crypto.store.db.mapper.MyDeviceLastSeenInfoEntityMapper
 import org.matrix.android.sdk.internal.di.MoshiProvider
 import org.matrix.android.sdk.internal.util.time.DefaultClock
 import kotlin.random.Random
@@ -37,6 +38,7 @@ internal class CryptoStoreHelper {
                 userId = "userId_" + Random.nextInt(),
                 deviceId = "deviceId_sample",
                 clock = DefaultClock(),
+                myDeviceLastSeenInfoEntityMapper = MyDeviceLastSeenInfoEntityMapper()
         )
     }
 }
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
index 251c13cc..f8832954 100644
--- 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
@@ -676,8 +676,8 @@ class E2eeSanityTests : InstrumentedTest {
         assertEquals("Decimal code should have matched", oldCode, newCode)
 
         // Assert that devices are verified
-        val newDeviceFromOldPov: CryptoDeviceInfo? = aliceSession.cryptoService().getDeviceInfo(aliceSession.myUserId, aliceNewSession.sessionParams.deviceId)
-        val oldDeviceFromNewPov: CryptoDeviceInfo? = aliceSession.cryptoService().getDeviceInfo(aliceSession.myUserId, aliceSession.sessionParams.deviceId)
+        val newDeviceFromOldPov: CryptoDeviceInfo? = aliceSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceNewSession.sessionParams.deviceId)
+        val oldDeviceFromNewPov: CryptoDeviceInfo? = aliceSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceSession.sessionParams.deviceId)
 
         Assert.assertTrue("new device should be verified from old point of view", newDeviceFromOldPov!!.isVerified)
         Assert.assertTrue("old device should be verified from new point of view", oldDeviceFromNewPov!!.isVerified)
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 8cb38ddc..ef3fdfee 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
@@ -193,7 +193,7 @@ class XSigningTest : InstrumentedTest {
             fail("Bob should see the new device")
         }
 
-        val bobSecondDevicePOVFirstDevice = bobSession.cryptoService().getDeviceInfo(bobUserId, bobSecondDeviceId)
+        val bobSecondDevicePOVFirstDevice = bobSession.cryptoService().getCryptoDeviceInfo(bobUserId, bobSecondDeviceId)
         assertNotNull("Bob Second device should be known and persisted from first", bobSecondDevicePOVFirstDevice)
 
         // Manually mark it as trusted from first session
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 c2e74abc..1bffbeee 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
@@ -521,9 +521,9 @@ class SASTest : InstrumentedTest {
         testHelper.await(bobSASLatch)
 
         // Assert that devices are verified
-        val bobDeviceInfoFromAlicePOV: CryptoDeviceInfo? = aliceSession.cryptoService().getDeviceInfo(bobUserId, bobDeviceId)
+        val bobDeviceInfoFromAlicePOV: CryptoDeviceInfo? = aliceSession.cryptoService().getCryptoDeviceInfo(bobUserId, bobDeviceId)
         val aliceDeviceInfoFromBobPOV: CryptoDeviceInfo? =
-                bobSession.cryptoService().getDeviceInfo(aliceSession.myUserId, aliceSession.cryptoService().getMyDevice().deviceId)
+                bobSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceSession.cryptoService().getMyDevice().deviceId)
 
         assertTrue("alice device should be verified from bob point of view", aliceDeviceInfoFromBobPOV!!.isVerified)
         assertTrue("bob device should be verified from alice point of view", bobDeviceInfoFromAlicePOV!!.isVerified)
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 a5e05f69..e0e662c7 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
@@ -40,6 +40,7 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationServic
 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.content.RoomKeyWithHeldContent
+import org.matrix.android.sdk.api.util.Optional
 import org.matrix.android.sdk.internal.crypto.model.SessionInfo
 
 interface CryptoService {
@@ -113,7 +114,19 @@ interface CryptoService {
 
     fun setRoomBlacklistUnverifiedDevices(roomId: String)
 
-    fun getDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo?
+    fun getCryptoDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo?
+
+    fun getCryptoDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>)
+
+    fun getCryptoDeviceInfo(userId: String): List<CryptoDeviceInfo>
+
+    fun getLiveCryptoDeviceInfo(): LiveData<List<CryptoDeviceInfo>>
+
+    fun getLiveCryptoDeviceInfoWithId(deviceId: String): LiveData<Optional<CryptoDeviceInfo>>
+
+    fun getLiveCryptoDeviceInfo(userId: String): LiveData<List<CryptoDeviceInfo>>
+
+    fun getLiveCryptoDeviceInfo(userIds: List<String>): LiveData<List<CryptoDeviceInfo>>
 
     fun requestRoomKeyForEvent(event: Event)
 
@@ -127,9 +140,9 @@ interface CryptoService {
 
     fun getMyDevicesInfo(): List<DeviceInfo>
 
-    fun getLiveMyDevicesInfo(): LiveData<List<DeviceInfo>>
+    fun getMyDevicesInfoLive(): LiveData<List<DeviceInfo>>
 
-    fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>)
+    fun getMyDevicesInfoLive(deviceId: String): LiveData<Optional<DeviceInfo>>
 
     fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int
 
@@ -156,14 +169,6 @@ interface CryptoService {
 
     fun downloadKeys(userIds: List<String>, forceDownload: Boolean, callback: MatrixCallback<MXUsersDevicesMap<CryptoDeviceInfo>>)
 
-    fun getCryptoDeviceInfo(userId: String): List<CryptoDeviceInfo>
-
-    fun getLiveCryptoDeviceInfo(): LiveData<List<CryptoDeviceInfo>>
-
-    fun getLiveCryptoDeviceInfo(userId: String): LiveData<List<CryptoDeviceInfo>>
-
-    fun getLiveCryptoDeviceInfo(userIds: List<String>): LiveData<List<CryptoDeviceInfo>>
-
     fun addNewSessionListener(newSessionListener: NewSessionListener)
     fun removeSessionListener(listener: NewSessionListener)
 
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 8fdbba21..84c25776 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
@@ -70,6 +70,9 @@ object EventType {
     const val STATE_ROOM_ENCRYPTION = "m.room.encryption"
     const val STATE_ROOM_SERVER_ACL = "m.room.server_acl"
 
+    // This type is for local purposes, it should never be processed by the server
+    const val LOCAL_STATE_ROOM_THIRD_PARTY_INVITE = "local.room.third_party_invite"
+
     // Call Events
     const val CALL_INVITE = "m.call.invite"
     const val CALL_CANDIDATES = "m.call.candidates"
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/ThreePid.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/ThreePid.kt
index 6bcf5768..24748f88 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/ThreePid.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/identity/ThreePid.kt
@@ -18,10 +18,14 @@ package org.matrix.android.sdk.api.session.identity
 
 import com.google.i18n.phonenumbers.NumberParseException
 import com.google.i18n.phonenumbers.PhoneNumberUtil
+import com.squareup.moshi.JsonClass
 import org.matrix.android.sdk.internal.session.profile.ThirdPartyIdentifier
 
 sealed class ThreePid(open val value: String) {
+    @JsonClass(generateAdapter = true)
     data class Email(val email: String) : ThreePid(email)
+
+    @JsonClass(generateAdapter = true)
     data class Msisdn(val msisdn: String) : ThreePid(msisdn)
 }
 
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 b7b0cc89..d6eb7b30 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,13 +17,16 @@
 package org.matrix.android.sdk.api.session.room.model.create
 
 import android.net.Uri
+import com.squareup.moshi.JsonClass
 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.di.MoshiProvider
 
+@JsonClass(generateAdapter = true)
 open class CreateRoomParams {
     /**
      * A public visibility indicates that the room will be shown in the published room list.
@@ -61,12 +64,12 @@ open class CreateRoomParams {
      * A list of user IDs to invite to the room.
      * This will tell the server to invite everyone in the list to the newly created room.
      */
-    val invitedUserIds = mutableListOf<String>()
+    var invitedUserIds = mutableListOf<String>()
 
     /**
      * A list of objects representing third party IDs to invite into the room.
      */
-    val invite3pids = mutableListOf<ThreePid>()
+    var invite3pids = mutableListOf<ThreePid>()
 
     /**
      * Initial Guest Access.
@@ -99,14 +102,14 @@ open class CreateRoomParams {
      * The server will clobber the following keys: creator.
      * Future versions of the specification may allow the server to clobber other keys.
      */
-    val creationContent = mutableMapOf<String, Any>()
+    var creationContent = mutableMapOf<String, Any>()
 
     /**
      * A list of state events to set in the new room. This allows the user to override the default state events
      * set in the new room. The expected format of the state events are an object with type, state_key and content keys set.
      * Takes precedence over events set by preset, but gets overridden by name and topic keys.
      */
-    val initialStates = mutableListOf<CreateRoomStateEvent>()
+    var initialStates = mutableListOf<CreateRoomStateEvent>()
 
     /**
      * Set to true to disable federation of this room.
@@ -151,7 +154,7 @@ open class CreateRoomParams {
      * Supported value: MXCRYPTO_ALGORITHM_MEGOLM.
      */
     var algorithm: String? = null
-        private set
+        internal set
 
     var historyVisibility: RoomHistoryVisibility? = null
 
@@ -161,10 +164,18 @@ open class CreateRoomParams {
 
     var roomVersion: String? = null
 
-    var featurePreset: RoomFeaturePreset? = null
+    @Transient var featurePreset: RoomFeaturePreset? = null
 
     companion object {
-        private const val CREATION_CONTENT_KEY_M_FEDERATE = "m.federate"
-        private const val CREATION_CONTENT_KEY_ROOM_TYPE = "type"
+        internal const val CREATION_CONTENT_KEY_M_FEDERATE = "m.federate"
+        internal const val CREATION_CONTENT_KEY_ROOM_TYPE = "type"
+
+        fun fromJson(json: String?): CreateRoomParams? {
+            return json?.let { MoshiProvider.providesMoshi().adapter(CreateRoomParams::class.java).fromJson(it) }
+        }
     }
 }
+
+internal fun CreateRoomParams.toJSONString(): String {
+    return MoshiProvider.providesMoshi().adapter(CreateRoomParams::class.java).toJson(this)
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomStateEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomStateEvent.kt
index fcfdc3e3..d89c72c5 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomStateEvent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomStateEvent.kt
@@ -16,8 +16,10 @@
 
 package org.matrix.android.sdk.api.session.room.model.create
 
+import com.squareup.moshi.JsonClass
 import org.matrix.android.sdk.api.session.events.model.Content
 
+@JsonClass(generateAdapter = true)
 data class CreateRoomStateEvent(
         /**
          * Required. The type of event to send.
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 35c066de..8dd7c309 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
@@ -73,6 +73,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityConten
 import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
 import org.matrix.android.sdk.api.session.room.model.shouldShareHistory
 import org.matrix.android.sdk.api.session.sync.model.SyncResponse
+import org.matrix.android.sdk.api.util.Optional
 import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter
 import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
 import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
@@ -273,21 +274,16 @@ internal class DefaultCryptoService @Inject constructor(
                 .executeBy(taskExecutor)
     }
 
-    override fun getLiveMyDevicesInfo(): LiveData<List<DeviceInfo>> {
+    override fun getMyDevicesInfoLive(): LiveData<List<DeviceInfo>> {
         return cryptoStore.getLiveMyDevicesInfo()
     }
 
-    override fun getMyDevicesInfo(): List<DeviceInfo> {
-        return cryptoStore.getMyDevicesInfo()
+    override fun getMyDevicesInfoLive(deviceId: String): LiveData<Optional<DeviceInfo>> {
+        return cryptoStore.getLiveMyDevicesInfo(deviceId)
     }
 
-    override fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>) {
-        getDeviceInfoTask
-                .configureWith(GetDeviceInfoTask.Params(deviceId)) {
-                    this.executionThread = TaskThread.CRYPTO
-                    this.callback = callback
-                }
-                .executeBy(taskExecutor)
+    override fun getMyDevicesInfo(): List<DeviceInfo> {
+        return cryptoStore.getMyDevicesInfo()
     }
 
     override fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int {
@@ -513,7 +509,7 @@ internal class DefaultCryptoService @Inject constructor(
      * @param userId the user id
      * @param deviceId the device id
      */
-    override fun getDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? {
+    override fun getCryptoDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo? {
         return if (userId.isNotEmpty() && !deviceId.isNullOrEmpty()) {
             cryptoStore.getUserDevice(userId, deviceId)
         } else {
@@ -521,6 +517,15 @@ internal class DefaultCryptoService @Inject constructor(
         }
     }
 
+    override fun getCryptoDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>) {
+        getDeviceInfoTask
+                .configureWith(GetDeviceInfoTask.Params(deviceId)) {
+                    this.executionThread = TaskThread.CRYPTO
+                    this.callback = callback
+                }
+                .executeBy(taskExecutor)
+    }
+
     override fun getCryptoDeviceInfo(userId: String): List<CryptoDeviceInfo> {
         return cryptoStore.getUserDeviceList(userId).orEmpty()
     }
@@ -529,6 +534,10 @@ internal class DefaultCryptoService @Inject constructor(
         return cryptoStore.getLiveDeviceList()
     }
 
+    override fun getLiveCryptoDeviceInfoWithId(deviceId: String): LiveData<Optional<CryptoDeviceInfo>> {
+        return cryptoStore.getLiveDeviceWithId(deviceId)
+    }
+
     override fun getLiveCryptoDeviceInfo(userId: String): LiveData<List<CryptoDeviceInfo>> {
         return cryptoStore.getLiveDeviceList(userId)
     }
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 e466def1..d405bdce 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
@@ -779,6 +779,11 @@ internal class DefaultCrossSigningService @Inject constructor(
 
     override fun onUsersDeviceUpdate(userIds: List<String>) {
         Timber.d("## CrossSigning - onUsersDeviceUpdate for users: ${userIds.logLimit()}")
+        checkTrustAndAffectedRoomShields(userIds)
+    }
+
+    fun checkTrustAndAffectedRoomShields(userIds: List<String>) {
+        Timber.d("## CrossSigning - checkTrustAndAffectedRoomShields for users: ${userIds.logLimit()}")
         val workerParams = UpdateTrustWorker.Params(
                 sessionId = sessionId,
                 filename = updateTrustWorkerDataRepository.createParam(userIds)
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 f1dc060e..6d845ec5 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
@@ -207,6 +207,7 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
     private suspend fun updateTrustStep2(userList: List<String>, myCrossSigningInfo: MXCrossSigningInfo?) {
         Timber.d("## CrossSigning - Updating shields for impacted rooms...")
         awaitTransaction(sessionRealmConfiguration) { sessionRealm ->
+            Timber.d("## CrossSigning - Updating shields for impacted rooms - in transaction")
             Realm.getInstance(cryptoRealmConfiguration).use { cryptoRealm ->
                 sessionRealm.where(RoomMemberSummaryEntity::class.java)
                         .`in`(RoomMemberSummaryEntityFields.USER_ID, userList.toTypedArray())
@@ -239,6 +240,7 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
                         }
             }
         }
+        Timber.d("## CrossSigning - Updating shields for impacted rooms - END")
     }
 
     private fun getCrossSigningInfo(cryptoRealm: Realm, userId: String): MXCrossSigningInfo? {
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 0413fc73..56eba252 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
@@ -238,10 +238,14 @@ internal interface IMXCryptoStore {
     // TODO temp
     fun getLiveDeviceList(): LiveData<List<CryptoDeviceInfo>>
 
+    fun getLiveDeviceWithId(deviceId: String): LiveData<Optional<CryptoDeviceInfo>>
+
     fun getMyDevicesInfo(): List<DeviceInfo>
 
     fun getLiveMyDevicesInfo(): LiveData<List<DeviceInfo>>
 
+    fun getLiveMyDevicesInfo(deviceId: String): LiveData<Optional<DeviceInfo>>
+
     fun saveMyDevicesInfo(info: List<DeviceInfo>)
 
     /**
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 f5468634..3b8fa4ca 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
@@ -55,6 +55,7 @@ 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.store.IMXCryptoStore
 import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper
+import org.matrix.android.sdk.internal.crypto.store.db.mapper.MyDeviceLastSeenInfoEntityMapper
 import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntity
 import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntityFields
 import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailMapper
@@ -68,6 +69,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity
 import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields
 import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
 import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity
+import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntityFields
 import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntity
 import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields
 import org.matrix.android.sdk.internal.crypto.store.db.model.OlmSessionEntity
@@ -112,6 +114,7 @@ internal class RealmCryptoStore @Inject constructor(
         @UserId private val userId: String,
         @DeviceId private val deviceId: String?,
         private val clock: Clock,
+        private val myDeviceLastSeenInfoEntityMapper: MyDeviceLastSeenInfoEntityMapper,
 ) : IMXCryptoStore {
 
     /* ==========================================================================================
@@ -578,6 +581,12 @@ internal class RealmCryptoStore @Inject constructor(
         }
     }
 
+    override fun getLiveDeviceWithId(deviceId: String): LiveData<Optional<CryptoDeviceInfo>> {
+        return Transformations.map(getLiveDeviceList()) { devices ->
+            devices.firstOrNull { it.deviceId == deviceId }.toOptional()
+        }
+    }
+
     override fun getMyDevicesInfo(): List<DeviceInfo> {
         return monarchy.fetchAllCopiedSync {
             it.where<MyDeviceLastSeenInfoEntity>()
@@ -596,17 +605,24 @@ internal class RealmCryptoStore @Inject constructor(
                 { realm: Realm ->
                     realm.where<MyDeviceLastSeenInfoEntity>()
                 },
-                { entity ->
-                    DeviceInfo(
-                            deviceId = entity.deviceId,
-                            lastSeenIp = entity.lastSeenIp,
-                            lastSeenTs = entity.lastSeenTs,
-                            displayName = entity.displayName
-                    )
-                }
+                { entity -> myDeviceLastSeenInfoEntityMapper.map(entity) }
         )
     }
 
+    override fun getLiveMyDevicesInfo(deviceId: String): LiveData<Optional<DeviceInfo>> {
+        val liveData = monarchy.findAllMappedWithChanges(
+                { realm: Realm ->
+                    realm.where<MyDeviceLastSeenInfoEntity>()
+                            .equalTo(MyDeviceLastSeenInfoEntityFields.DEVICE_ID, deviceId)
+                },
+                { entity -> myDeviceLastSeenInfoEntityMapper.map(entity) }
+        )
+
+        return Transformations.map(liveData) {
+            it.firstOrNull().toOptional()
+        }
+    }
+
     override fun saveMyDevicesInfo(info: List<DeviceInfo>) {
         val entities = info.map {
             MyDeviceLastSeenInfoEntity(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/MyDeviceLastSeenInfoEntityMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/MyDeviceLastSeenInfoEntityMapper.kt
new file mode 100644
index 00000000..38a7569a
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/MyDeviceLastSeenInfoEntityMapper.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.crypto.store.db.mapper
+
+import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
+import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity
+import javax.inject.Inject
+
+internal class MyDeviceLastSeenInfoEntityMapper @Inject constructor() {
+
+    fun map(entity: MyDeviceLastSeenInfoEntity): DeviceInfo {
+        return DeviceInfo(
+                deviceId = entity.deviceId,
+                lastSeenIp = entity.lastSeenIp,
+                lastSeenTs = entity.lastSeenTs,
+                displayName = entity.displayName
+        )
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt
index bb14b417..405757e3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt
@@ -16,10 +16,12 @@
 package org.matrix.android.sdk.internal.crypto.tasks
 
 import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
 import org.matrix.android.sdk.api.session.room.send.SendState
 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
+import org.matrix.android.sdk.internal.session.room.create.CreateRoomFromLocalRoomTask
 import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
 import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
 import org.matrix.android.sdk.internal.task.Task
@@ -37,12 +39,17 @@ internal class DefaultSendEventTask @Inject constructor(
         private val localEchoRepository: LocalEchoRepository,
         private val encryptEventTask: EncryptEventTask,
         private val loadRoomMembersTask: LoadRoomMembersTask,
+        private val createRoomFromLocalRoomTask: CreateRoomFromLocalRoomTask,
         private val roomAPI: RoomAPI,
         private val globalErrorReceiver: GlobalErrorReceiver
 ) : SendEventTask {
 
     override suspend fun execute(params: SendEventTask.Params): String {
         try {
+            if (params.event.isLocalRoomEvent) {
+                return createRoomAndSendEvent(params)
+            }
+
             // Make sure to load all members in the room before sending the event.
             params.event.roomId
                     ?.takeIf { params.encrypt }
@@ -78,6 +85,12 @@ internal class DefaultSendEventTask @Inject constructor(
         }
     }
 
+    private suspend fun createRoomAndSendEvent(params: SendEventTask.Params): String {
+        val roomId = createRoomFromLocalRoomTask.execute(CreateRoomFromLocalRoomTask.Params(params.event.roomId.orEmpty()))
+        Timber.d("State event: convert local room (${params.event.roomId}) to existing room ($roomId) before sending the event.")
+        return execute(params.copy(event = params.event.copy(roomId = roomId)))
+    }
+
     @Throws
     private suspend fun handleEncryption(params: SendEventTask.Params): Event {
         if (params.encrypt && !params.event.isEncrypted()) {
@@ -91,4 +104,7 @@ internal class DefaultSendEventTask @Inject constructor(
         }
         return params.event
     }
+
+    private val Event.isLocalRoomEvent
+        get() = RoomLocalEcho.isLocalEchoId(roomId.orEmpty())
 }
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 b733aa6f..0b118638 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
@@ -52,6 +52,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo032
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo033
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo034
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo035
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo036
 import org.matrix.android.sdk.internal.util.Normalizer
 import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
 import javax.inject.Inject
@@ -60,7 +61,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
         private val normalizer: Normalizer
 ) : MatrixRealmMigration(
         dbName = "Session",
-        schemaVersion = 35L,
+        schemaVersion = 36L,
 ) {
     /**
      * Forces all RealmSessionStoreMigration instances to be equal.
@@ -105,5 +106,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
         if (oldVersion < 33) MigrateSessionTo033(realm).perform()
         if (oldVersion < 34) MigrateSessionTo034(realm).perform()
         if (oldVersion < 35) MigrateSessionTo035(realm).perform()
+        if (oldVersion < 36) MigrateSessionTo036(realm).perform()
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo036.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo036.kt
new file mode 100644
index 00000000..efcb181e
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo036.kt
@@ -0,0 +1,33 @@
+/*
+ * 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 org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+internal class MigrateSessionTo036(realm: DynamicRealm) : RealmMigrator(realm, 36) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.create("LocalRoomSummaryEntity")
+                .addField(LocalRoomSummaryEntityFields.ROOM_ID, String::class.java)
+                .addPrimaryKey(LocalRoomSummaryEntityFields.ROOM_ID)
+                .setRequired(LocalRoomSummaryEntityFields.ROOM_ID, true)
+                .addField(LocalRoomSummaryEntityFields.CREATE_ROOM_PARAMS_STR, String::class.java)
+                .addRealmObjectField(LocalRoomSummaryEntityFields.ROOM_SUMMARY_ENTITY.`$`, realm.schema.get("RoomSummaryEntity")!!)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/LocalRoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/LocalRoomSummaryEntity.kt
new file mode 100644
index 00000000..fd8331e9
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/LocalRoomSummaryEntity.kt
@@ -0,0 +1,39 @@
+/*
+ * 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
+
+import io.realm.RealmObject
+import io.realm.annotations.PrimaryKey
+import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
+import org.matrix.android.sdk.api.session.room.model.create.toJSONString
+
+internal open class LocalRoomSummaryEntity(
+        @PrimaryKey var roomId: String = "",
+        var roomSummaryEntity: RoomSummaryEntity? = null,
+        private var createRoomParamsStr: String? = null
+) : RealmObject() {
+
+    var createRoomParams: CreateRoomParams?
+        get() {
+            return CreateRoomParams.fromJson(createRoomParamsStr)
+        }
+        set(value) {
+            createRoomParamsStr = value?.toJSONString()
+        }
+
+    companion object
+}
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 d131589d..b222bcb7 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
@@ -35,6 +35,7 @@ import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntit
             ReadReceiptEntity::class,
             RoomEntity::class,
             RoomSummaryEntity::class,
+            LocalRoomSummaryEntity::class,
             RoomTagEntity::class,
             SyncEntity::class,
             PendingThreePidEntity::class,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LocalRoomSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LocalRoomSummaryEntityQueries.kt
new file mode 100644
index 00000000..527350be
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LocalRoomSummaryEntityQueries.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.RealmQuery
+import io.realm.kotlin.where
+import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity
+import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntityFields
+
+internal fun LocalRoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery<LocalRoomSummaryEntity> {
+    val query = realm.where<LocalRoomSummaryEntity>()
+    if (roomId != null) {
+        query.equalTo(LocalRoomSummaryEntityFields.ROOM_ID, roomId)
+    }
+    return query
+}
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 8f007f22..0a737d5e 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
@@ -17,6 +17,8 @@
 package org.matrix.android.sdk.internal.di
 
 import com.squareup.moshi.Moshi
+import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory
+import org.matrix.android.sdk.api.session.identity.ThreePid
 import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageDefaultContent
@@ -60,6 +62,12 @@ internal object MoshiProvider {
                             .registerSubtype(MessagePollResponseContent::class.java, MessageType.MSGTYPE_POLL_RESPONSE)
             )
             .add(SerializeNulls.JSON_ADAPTER_FACTORY)
+            .add(
+                    PolymorphicJsonAdapterFactory.of(ThreePid::class.java, "type")
+                            .withSubtype(ThreePid.Email::class.java, "email")
+                            .withSubtype(ThreePid.Msisdn::class.java, "msisdn")
+                            .withDefaultValue(null)
+            )
             .build()
 
     fun providesMoshi(): Moshi {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt
index 113e780e..cb2088a1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt
@@ -42,7 +42,7 @@ internal object NetworkModule {
     @Provides
     @JvmStatic
     fun providesHttpLoggingInterceptor(): HttpLoggingInterceptor {
-        val logger = FormattedJsonHttpLogger()
+        val logger = FormattedJsonHttpLogger(BuildConfig.OKHTTP_LOGGING_LEVEL)
         val interceptor = HttpLoggingInterceptor(logger)
         interceptor.level = BuildConfig.OKHTTP_LOGGING_LEVEL
         return interceptor
diff --git a/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt
similarity index 84%
rename from matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt
index 2661bd1f..4e052553 100644
--- a/matrix-sdk-android/src/debug/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt
@@ -23,15 +23,17 @@ import org.json.JSONException
 import org.json.JSONObject
 import timber.log.Timber
 
-internal class FormattedJsonHttpLogger : HttpLoggingInterceptor.Logger {
+internal class FormattedJsonHttpLogger(
+        private val level: HttpLoggingInterceptor.Level
+) : HttpLoggingInterceptor.Logger {
 
     companion object {
         private const val INDENT_SPACE = 2
     }
 
     /**
-     * Log the message and try to log it again as a JSON formatted string
-     * Note: it can consume a lot of memory but it is only in DEBUG mode
+     * Log the message and try to log it again as a JSON formatted string.
+     * Note: it can consume a lot of memory but it is only in DEBUG mode.
      *
      * @param message
      */
@@ -39,6 +41,10 @@ internal class FormattedJsonHttpLogger : HttpLoggingInterceptor.Logger {
     override fun log(@NonNull message: String) {
         Timber.v(message)
 
+        // Try to log formatted Json only if there is a chance that [message] contains Json.
+        // It can be only the case if we log the bodies of Http requests.
+        if (level != HttpLoggingInterceptor.Level.BODY) return
+
         if (message.startsWith("{")) {
             // JSON Detected
             try {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt
index a79f35bc..a7572035 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt
@@ -55,6 +55,7 @@ import org.matrix.android.sdk.internal.session.space.SpaceModule
 import org.matrix.android.sdk.internal.session.sync.SyncModule
 import org.matrix.android.sdk.internal.session.sync.SyncTask
 import org.matrix.android.sdk.internal.session.sync.SyncTokenStore
+import org.matrix.android.sdk.internal.session.sync.handler.UpdateUserWorker
 import org.matrix.android.sdk.internal.session.sync.job.SyncWorker
 import org.matrix.android.sdk.internal.session.terms.TermsModule
 import org.matrix.android.sdk.internal.session.thirdparty.ThirdPartyModule
@@ -128,6 +129,8 @@ internal interface SessionComponent {
 
     fun inject(worker: UpdateTrustWorker)
 
+    fun inject(worker: UpdateUserWorker)
+
     fun inject(worker: DeactivateLiveLocationShareWorker)
 
     @Component.Factory
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 d01324a3..1475b672 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
@@ -43,9 +43,13 @@ import org.matrix.android.sdk.internal.session.room.alias.DefaultGetRoomLocalAli
 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.GetRoomLocalAliasesTask
+import org.matrix.android.sdk.internal.session.room.create.CreateLocalRoomStateEventsTask
 import org.matrix.android.sdk.internal.session.room.create.CreateLocalRoomTask
+import org.matrix.android.sdk.internal.session.room.create.CreateRoomFromLocalRoomTask
 import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask
+import org.matrix.android.sdk.internal.session.room.create.DefaultCreateLocalRoomStateEventsTask
 import org.matrix.android.sdk.internal.session.room.create.DefaultCreateLocalRoomTask
+import org.matrix.android.sdk.internal.session.room.create.DefaultCreateRoomFromLocalRoomTask
 import org.matrix.android.sdk.internal.session.room.create.DefaultCreateRoomTask
 import org.matrix.android.sdk.internal.session.room.delete.DefaultDeleteLocalRoomTask
 import org.matrix.android.sdk.internal.session.room.delete.DeleteLocalRoomTask
@@ -213,6 +217,12 @@ internal abstract class RoomModule {
     @Binds
     abstract fun bindCreateLocalRoomTask(task: DefaultCreateLocalRoomTask): CreateLocalRoomTask
 
+    @Binds
+    abstract fun bindCreateLocalRoomStateEventsTask(task: DefaultCreateLocalRoomStateEventsTask): CreateLocalRoomStateEventsTask
+
+    @Binds
+    abstract fun bindCreateRoomFromLocalRoomTask(task: DefaultCreateRoomFromLocalRoomTask): CreateRoomFromLocalRoomTask
+
     @Binds
     abstract fun bindDeleteLocalRoomTask(task: DefaultDeleteLocalRoomTask): DeleteLocalRoomTask
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomStateEventsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomStateEventsTask.kt
new file mode 100644
index 00000000..a9ff4970
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomStateEventsTask.kt
@@ -0,0 +1,299 @@
+/*
+ * 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.create
+
+import org.matrix.android.sdk.api.MatrixPatterns.getServerName
+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.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.LocalEcho
+import org.matrix.android.sdk.api.session.events.model.toContent
+import org.matrix.android.sdk.api.session.room.model.GuestAccess
+import org.matrix.android.sdk.api.session.room.model.Membership
+import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
+import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent
+import org.matrix.android.sdk.api.session.room.model.RoomGuestAccessContent
+import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
+import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent
+import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
+import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
+import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
+import org.matrix.android.sdk.api.session.room.model.RoomNameContent
+import org.matrix.android.sdk.api.session.room.model.RoomThirdPartyInviteContent
+import org.matrix.android.sdk.api.session.room.model.RoomTopicContent
+import org.matrix.android.sdk.api.session.room.model.banOrDefault
+import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
+import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset
+import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
+import org.matrix.android.sdk.api.session.room.model.eventsDefaultOrDefault
+import org.matrix.android.sdk.api.session.room.model.inviteOrDefault
+import org.matrix.android.sdk.api.session.room.model.kickOrDefault
+import org.matrix.android.sdk.api.session.room.model.redactOrDefault
+import org.matrix.android.sdk.api.session.room.model.stateDefaultOrDefault
+import org.matrix.android.sdk.api.session.room.model.usersDefaultOrDefault
+import org.matrix.android.sdk.api.session.user.UserService
+import org.matrix.android.sdk.internal.di.UserId
+import org.matrix.android.sdk.internal.session.room.create.CreateLocalRoomStateEventsTask.Params
+import org.matrix.android.sdk.internal.session.room.membership.threepid.toThreePid
+import org.matrix.android.sdk.internal.task.Task
+import org.matrix.android.sdk.internal.util.time.Clock
+import javax.inject.Inject
+
+/**
+ * Generate a list of local state events from the given [CreateRoomBody].
+ * The states events are generated according to the given configuration and following the matrix specification.
+ * This list reflects as much as possible a list of state events related to a real room configured and got from the server.
+ *
+ * Ref: https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3createroom
+ */
+internal interface CreateLocalRoomStateEventsTask : Task<Params, List<Event>> {
+    data class Params(val createRoomBody: CreateRoomBody)
+}
+
+internal class DefaultCreateLocalRoomStateEventsTask @Inject constructor(
+        @UserId private val myUserId: String,
+        private val userService: UserService,
+        private val clock: Clock,
+) : CreateLocalRoomStateEventsTask {
+
+    private lateinit var createRoomBody: CreateRoomBody
+
+    override suspend fun execute(params: Params): List<Event> {
+        createRoomBody = params.createRoomBody
+
+        // Build the list of the state events following the priorities from the matrix specification
+        // Changing the order of the events might break the correct display of the room on the client side
+        return buildList {
+            createRoomCreateEvent()
+            createRoomMemberEvents(listOf(myUserId))
+            createRoomPowerLevelsEvent()
+            createRoomAliasEvent()
+            createRoomPresetEvents()
+            createRoomInitialStateEvents()
+            createRoomNameAndTopicStateEvents()
+            createRoomMemberEvents(createRoomBody.invitedUserIds.orEmpty())
+            createRoomThreePidEvents()
+            createRoomDefaultEvents()
+        }
+    }
+
+    /**
+     * Generate the create state event related to this room.
+     */
+    private fun MutableList<Event>.createRoomCreateEvent() {
+        val roomCreateEvent = createLocalStateEvent(
+                type = EventType.STATE_ROOM_CREATE,
+                content = RoomCreateContent(
+                        creator = myUserId,
+                        roomVersion = createRoomBody.roomVersion,
+                        type = (createRoomBody.creationContent as? Map<*, *>)?.get(CreateRoomParams.CREATION_CONTENT_KEY_ROOM_TYPE) as? String
+
+                ).toContent(),
+        )
+        add(roomCreateEvent)
+    }
+
+    /**
+     * Generate the create state event related to the power levels using the given overridden values or the default values according to the specification.
+     * Ref: https://spec.matrix.org/latest/client-server-api/#mroompower_levels
+     */
+    private fun MutableList<Event>.createRoomPowerLevelsEvent() {
+        val powerLevelsContent = createLocalStateEvent(
+                type = EventType.STATE_ROOM_POWER_LEVELS,
+                content = (createRoomBody.powerLevelContentOverride ?: PowerLevelsContent()).let {
+                    it.copy(
+                            ban = it.banOrDefault(),
+                            eventsDefault = it.eventsDefaultOrDefault(),
+                            invite = it.inviteOrDefault(),
+                            kick = it.kickOrDefault(),
+                            redact = it.redactOrDefault(),
+                            stateDefault = it.stateDefaultOrDefault(),
+                            usersDefault = it.usersDefaultOrDefault(),
+                    )
+                }.toContent(),
+        )
+        add(powerLevelsContent)
+    }
+
+    /**
+     * Generate the local room member state events related to the given user ids, if any.
+     */
+    private suspend fun MutableList<Event>.createRoomMemberEvents(userIds: List<String>) {
+        val memberEvents = userIds
+                .mapNotNull { tryOrNull { userService.resolveUser(it) } }
+                .map { user ->
+                    createLocalStateEvent(
+                            type = EventType.STATE_ROOM_MEMBER,
+                            content = RoomMemberContent(
+                                    isDirect = createRoomBody.isDirect.takeUnless { user.userId == myUserId }.orFalse(),
+                                    membership = if (user.userId == myUserId) Membership.JOIN else Membership.INVITE,
+                                    displayName = user.displayName,
+                                    avatarUrl = user.avatarUrl
+                            ).toContent(),
+                            stateKey = user.userId
+                    )
+                }
+        addAll(memberEvents)
+    }
+
+    /**
+     * Generate the local state events related to the given third party invites, if any.
+     */
+    private fun MutableList<Event>.createRoomThreePidEvents() {
+        createRoomBody.invite3pids.orEmpty().forEach { body ->
+            val localThirdPartyInviteEvent = createLocalStateEvent(
+                    type = EventType.LOCAL_STATE_ROOM_THIRD_PARTY_INVITE,
+                    content = LocalRoomThirdPartyInviteContent(
+                            isDirect = createRoomBody.isDirect.orFalse(),
+                            membership = Membership.INVITE,
+                            displayName = body.address,
+                            thirdPartyInvite = body.toThreePid()
+                    ).toContent(),
+            )
+            val thirdPartyInviteEvent = createLocalStateEvent(
+                    type = EventType.STATE_ROOM_THIRD_PARTY_INVITE,
+                    content = RoomThirdPartyInviteContent(
+                            displayName = body.address,
+                            keyValidityUrl = null,
+                            publicKey = null,
+                            publicKeys = null
+                    ).toContent(),
+            )
+            add(localThirdPartyInviteEvent)
+            add(thirdPartyInviteEvent)
+        }
+    }
+
+    /**
+     * Generate the local state event related to the given alias, if any.
+     */
+    fun MutableList<Event>.createRoomAliasEvent() {
+        if (createRoomBody.roomAliasName != null) {
+            val canonicalAliasContent = createLocalStateEvent(
+                    type = EventType.STATE_ROOM_CANONICAL_ALIAS,
+                    content = RoomCanonicalAliasContent(
+                            canonicalAlias = "${createRoomBody.roomAliasName}:${myUserId.getServerName()}"
+                    ).toContent(),
+            )
+            add(canonicalAliasContent)
+        }
+    }
+
+    /**
+     * Generate the local state events related to the given [CreateRoomPreset].
+     * Ref: https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3createroom
+     */
+    private fun MutableList<Event>.createRoomPresetEvents() {
+        val preset = createRoomBody.preset ?: return
+
+        var joinRules: RoomJoinRules? = null
+        var historyVisibility: RoomHistoryVisibility? = null
+        var guestAccess: GuestAccess? = null
+        when (preset) {
+            CreateRoomPreset.PRESET_PRIVATE_CHAT,
+            CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT -> {
+                joinRules = RoomJoinRules.INVITE
+                historyVisibility = RoomHistoryVisibility.SHARED
+                guestAccess = GuestAccess.CanJoin
+            }
+            CreateRoomPreset.PRESET_PUBLIC_CHAT -> {
+                joinRules = RoomJoinRules.PUBLIC
+                historyVisibility = RoomHistoryVisibility.SHARED
+                guestAccess = GuestAccess.Forbidden
+            }
+        }
+
+        add(createLocalStateEvent(EventType.STATE_ROOM_JOIN_RULES, RoomJoinRulesContent(joinRules.value).toContent()))
+        add(createLocalStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, RoomHistoryVisibilityContent(historyVisibility.value).toContent()))
+        add(createLocalStateEvent(EventType.STATE_ROOM_GUEST_ACCESS, RoomGuestAccessContent(guestAccess.value).toContent()))
+    }
+
+    /**
+     * Generate the local state events related to the given initial states, if any.
+     * The given initial state events override the potential existing ones of the same type.
+     */
+    private fun MutableList<Event>.createRoomInitialStateEvents() {
+        val initialStates = createRoomBody.initialStates ?: return
+
+        val initialStateEvents = initialStates.map { createLocalStateEvent(it.type, it.content, it.stateKey) }
+        // Erase existing events of the same type
+        removeAll { event -> event.type in initialStateEvents.map { it.type } }
+        // Add the initial state events to the list
+        addAll(initialStateEvents)
+    }
+
+    /**
+     * Generate the local events related to the given room name and topic, if any.
+     */
+    private fun MutableList<Event>.createRoomNameAndTopicStateEvents() {
+        if (createRoomBody.name != null) {
+            add(createLocalStateEvent(EventType.STATE_ROOM_NAME, RoomNameContent(createRoomBody.name).toContent()))
+        }
+        if (createRoomBody.topic != null) {
+            add(createLocalStateEvent(EventType.STATE_ROOM_TOPIC, RoomTopicContent(createRoomBody.topic).toContent()))
+        }
+    }
+
+    /**
+     * Generate the local events which have not been set and are in that case provided by the server with default values.
+     * Default events:
+     * - m.room.history_visibility (https://spec.matrix.org/latest/client-server-api/#server-behaviour-5)
+     * - m.room.guest_access (https://spec.matrix.org/latest/client-server-api/#mroomguest_access)
+     */
+    private fun MutableList<Event>.createRoomDefaultEvents() {
+        // HistoryVisibility
+        if (none { it.type == EventType.STATE_ROOM_HISTORY_VISIBILITY }) {
+            add(
+                    createLocalStateEvent(
+                            type = EventType.STATE_ROOM_HISTORY_VISIBILITY,
+                            content = RoomHistoryVisibilityContent(RoomHistoryVisibility.SHARED.value).toContent(),
+                    )
+            )
+        }
+        // GuestAccess
+        if (none { it.type == EventType.STATE_ROOM_GUEST_ACCESS }) {
+            add(
+                    createLocalStateEvent(
+                            type = EventType.STATE_ROOM_GUEST_ACCESS,
+                            content = RoomGuestAccessContent(GuestAccess.Forbidden.value).toContent(),
+                    )
+            )
+        }
+    }
+
+    /**
+     * Generate a local state event from the given parameters.
+     *
+     * @param type the event type, see [EventType]
+     * @param content the content of the event
+     * @param stateKey the stateKey, if any
+     *
+     * @return a local state event
+     */
+    private fun createLocalStateEvent(type: String?, content: Content?, stateKey: String? = ""): Event {
+        return Event(
+                type = type,
+                senderId = myUserId,
+                stateKey = stateKey,
+                content = content,
+                originServerTs = clock.epochMillis(),
+                eventId = LocalEcho.createLocalEchoId()
+        )
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
index d57491a4..03c2b2a4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
@@ -21,26 +21,15 @@ import io.realm.Realm
 import io.realm.RealmConfiguration
 import io.realm.kotlin.createObject
 import kotlinx.coroutines.TimeoutCancellationException
-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.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.LocalEcho
-import org.matrix.android.sdk.api.session.events.model.toContent
 import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure
-import org.matrix.android.sdk.api.session.room.model.GuestAccess
 import org.matrix.android.sdk.api.session.room.model.Membership
-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.api.session.room.model.RoomMemberContent
 import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
-import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
 import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
 import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.api.session.sync.model.RoomSyncSummary
-import org.matrix.android.sdk.api.session.user.UserService
-import org.matrix.android.sdk.api.session.user.model.User
+import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
 import org.matrix.android.sdk.internal.database.awaitNotEmptyResult
 import org.matrix.android.sdk.internal.database.helper.addTimelineEvent
 import org.matrix.android.sdk.internal.database.mapper.asDomain
@@ -48,6 +37,7 @@ 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.CurrentStateEventEntity
 import org.matrix.android.sdk.internal.database.model.EventInsertType
+import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity
 import org.matrix.android.sdk.internal.database.model.RoomEntity
 import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType
 import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
@@ -56,7 +46,6 @@ 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.di.SessionDatabase
-import org.matrix.android.sdk.internal.di.UserId
 import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent
 import org.matrix.android.sdk.internal.session.room.membership.RoomMemberEventHandler
 import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater
@@ -70,22 +59,22 @@ import javax.inject.Inject
 internal interface CreateLocalRoomTask : Task<CreateRoomParams, String>
 
 internal class DefaultCreateLocalRoomTask @Inject constructor(
-        @UserId private val userId: String,
         @SessionDatabase private val monarchy: Monarchy,
         private val roomMemberEventHandler: RoomMemberEventHandler,
         private val roomSummaryUpdater: RoomSummaryUpdater,
         @SessionDatabase private val realmConfiguration: RealmConfiguration,
         private val createRoomBodyBuilder: CreateRoomBodyBuilder,
-        private val userService: UserService,
+        private val cryptoService: DefaultCryptoService,
         private val clock: Clock,
+        private val createLocalRoomStateEventsTask: CreateLocalRoomStateEventsTask,
 ) : CreateLocalRoomTask {
 
     override suspend fun execute(params: CreateRoomParams): String {
-        val createRoomBody = createRoomBodyBuilder.build(params.withDefault())
+        val createRoomBody = createRoomBodyBuilder.build(params)
         val roomId = RoomLocalEcho.createLocalEchoId()
         monarchy.awaitTransaction { realm ->
             createLocalRoomEntity(realm, roomId, createRoomBody)
-            createLocalRoomSummaryEntity(realm, roomId, createRoomBody)
+            createLocalRoomSummaryEntity(realm, roomId, params, createRoomBody)
         }
 
         // Wait for room to be created in DB
@@ -114,14 +103,29 @@ internal class DefaultCreateLocalRoomTask @Inject constructor(
         }
     }
 
-    private fun createLocalRoomSummaryEntity(realm: Realm, roomId: String, createRoomBody: CreateRoomBody) {
-        val otherUserId = createRoomBody.getDirectUserId()
-        if (otherUserId != null) {
-            RoomSummaryEntity.getOrCreate(realm, roomId).apply {
+    private fun createLocalRoomSummaryEntity(realm: Realm, roomId: String, createRoomParams: CreateRoomParams, createRoomBody: CreateRoomBody) {
+        // Create the room summary entity
+        val roomSummaryEntity = realm.createObject<RoomSummaryEntity>(roomId).apply {
+            val otherUserId = createRoomBody.getDirectUserId()
+            if (otherUserId != null) {
                 isDirect = true
                 directUserId = otherUserId
             }
         }
+
+        // Update the createRoomParams from the potential feature preset before saving
+        createRoomParams.featurePreset?.let { featurePreset ->
+            featurePreset.updateRoomParams(createRoomParams)
+            createRoomParams.initialStates.addAll(featurePreset.setupInitialStates().orEmpty())
+        }
+
+        // Create a LocalRoomSummaryEntity decorated by the related RoomSummaryEntity and the updated CreateRoomParams
+        realm.createObject<LocalRoomSummaryEntity>(roomId).also {
+            it.roomSummaryEntity = roomSummaryEntity
+            it.createRoomParams = createRoomParams
+        }
+
+        // Update the RoomSummaryEntity by simulating a fake sync response
         roomSummaryUpdater.update(
                 realm = realm,
                 roomId = roomId,
@@ -150,7 +154,7 @@ internal class DefaultCreateLocalRoomTask @Inject constructor(
             isLastForward = true
         }
 
-        val eventList = createLocalRoomEvents(createRoomBody)
+        val eventList = createLocalRoomStateEventsTask.execute(CreateLocalRoomStateEventsTask.Params(createRoomBody))
         val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
 
         for (event in eventList) {
@@ -169,6 +173,9 @@ internal class DefaultCreateLocalRoomTask @Inject constructor(
                     roomMemberContentsByUser[event.stateKey] = event.getFixedRoomMemberContent()
                     roomMemberEventHandler.handle(realm, roomId, event, false)
                 }
+
+                // Give info to crypto module
+                cryptoService.onStateEvent(roomId, event)
             }
 
             roomMemberContentsByUser.getOrPut(event.senderId) {
@@ -187,81 +194,4 @@ internal class DefaultCreateLocalRoomTask @Inject constructor(
 
         return chunkEntity
     }
-
-    /**
-     * Build the list of the events related to the room creation params.
-     *
-     * @param createRoomBody the room creation params
-     *
-     * @return the list of events
-     */
-    private suspend fun createLocalRoomEvents(createRoomBody: CreateRoomBody): List<Event> {
-        val myUser = userService.getUser(userId) ?: User(userId)
-        val invitedUsers = createRoomBody.invitedUserIds.orEmpty()
-                .mapNotNull { tryOrNull { userService.resolveUser(it) } }
-
-        val createRoomEvent = createLocalEvent(
-                type = EventType.STATE_ROOM_CREATE,
-                content = RoomCreateContent(
-                        creator = userId
-                ).toContent()
-        )
-        val myRoomMemberEvent = createLocalEvent(
-                type = EventType.STATE_ROOM_MEMBER,
-                content = RoomMemberContent(
-                        membership = Membership.JOIN,
-                        displayName = myUser.displayName,
-                        avatarUrl = myUser.avatarUrl
-                ).toContent(),
-                stateKey = userId
-        )
-        val roomMemberEvents = invitedUsers.map {
-            createLocalEvent(
-                    type = EventType.STATE_ROOM_MEMBER,
-                    content = RoomMemberContent(
-                            isDirect = createRoomBody.isDirect.orFalse(),
-                            membership = Membership.INVITE,
-                            displayName = it.displayName,
-                            avatarUrl = it.avatarUrl
-                    ).toContent(),
-                    stateKey = it.userId
-            )
-        }
-
-        return buildList {
-            add(createRoomEvent)
-            add(myRoomMemberEvent)
-            addAll(createRoomBody.initialStates.orEmpty().map { createLocalEvent(it.type, it.content, it.stateKey) })
-            addAll(roomMemberEvents)
-        }
-    }
-
-    /**
-     * Generate a local event from the given parameters.
-     *
-     * @param type the event type, see [EventType]
-     * @param content the content of the Event
-     * @param stateKey the stateKey, if any
-     *
-     * @return a fake event
-     */
-    private fun createLocalEvent(type: String?, content: Content?, stateKey: String? = ""): Event {
-        return Event(
-                type = type,
-                senderId = userId,
-                stateKey = stateKey,
-                content = content,
-                originServerTs = clock.epochMillis(),
-                eventId = LocalEcho.createLocalEchoId()
-        )
-    }
-
-    /**
-     * Setup default values to the CreateRoomParams as the room is created locally (the default values will not be defined by the server).
-     */
-    private fun CreateRoomParams.withDefault() = this.apply {
-        if (visibility == null) visibility = RoomDirectoryVisibility.PRIVATE
-        if (historyVisibility == null) historyVisibility = RoomHistoryVisibility.SHARED
-        if (guestAccess == null) guestAccess = GuestAccess.Forbidden
-    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt
index b326c361..17e1aba6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBody.kt
@@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.events.model.Event
 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.create.CreateRoomPreset
+import org.matrix.android.sdk.internal.di.MoshiProvider
 import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody
 
 /**
@@ -119,7 +120,13 @@ internal data class CreateRoomBody(
          */
         @Json(name = "room_version")
         val roomVersion: String?
-)
+) {
+    companion object {
+        fun fromJson(json: String?): CreateRoomBody? {
+            return json?.let { MoshiProvider.providesMoshi().adapter(CreateRoomBody::class.java).fromJson(it) }
+        }
+    }
+}
 
 /**
  * Tells if the created room can be a direct chat one.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomFromLocalRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomFromLocalRoomTask.kt
new file mode 100644
index 00000000..02538a5c
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomFromLocalRoomTask.kt
@@ -0,0 +1,149 @@
+/*
+ * 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.create
+
+import com.zhuinden.monarchy.Monarchy
+import io.realm.kotlin.where
+import kotlinx.coroutines.TimeoutCancellationException
+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.failure.CreateRoomFailure
+import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
+import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent
+import org.matrix.android.sdk.api.session.room.send.SendState
+import org.matrix.android.sdk.internal.database.awaitNotEmptyResult
+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.EventEntity
+import org.matrix.android.sdk.internal.database.model.EventEntityFields
+import org.matrix.android.sdk.internal.database.model.EventInsertType
+import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity
+import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntityFields
+import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
+import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
+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.whereRoomId
+import org.matrix.android.sdk.internal.di.SessionDatabase
+import org.matrix.android.sdk.internal.di.UserId
+import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
+import org.matrix.android.sdk.internal.task.Task
+import org.matrix.android.sdk.internal.util.awaitTransaction
+import org.matrix.android.sdk.internal.util.time.Clock
+import java.util.UUID
+import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+
+/**
+ * Create a room on the server from a local room.
+ * The configuration of the local room will be use to configure the new room.
+ * The potential local room members will also be invited to this new room.
+ *
+ * A local tombstone event will be created to indicate that the local room has been replacing by the new one.
+ */
+internal interface CreateRoomFromLocalRoomTask : Task<CreateRoomFromLocalRoomTask.Params, String> {
+    data class Params(val localRoomId: String)
+}
+
+internal class DefaultCreateRoomFromLocalRoomTask @Inject constructor(
+        @UserId private val userId: String,
+        @SessionDatabase private val monarchy: Monarchy,
+        private val createRoomTask: CreateRoomTask,
+        private val stateEventDataSource: StateEventDataSource,
+        private val clock: Clock,
+) : CreateRoomFromLocalRoomTask {
+
+    private val realmConfiguration
+        get() = monarchy.realmConfiguration
+
+    override suspend fun execute(params: CreateRoomFromLocalRoomTask.Params): String {
+        val replacementRoomId = stateEventDataSource.getStateEvent(params.localRoomId, EventType.STATE_ROOM_TOMBSTONE, QueryStringValue.IsEmpty)
+                ?.content.toModel<RoomTombstoneContent>()
+                ?.replacementRoomId
+
+        if (replacementRoomId != null) {
+            return replacementRoomId
+        }
+
+        var createRoomParams: CreateRoomParams? = null
+        var isEncrypted = false
+        monarchy.doWithRealm { realm ->
+            realm.where<LocalRoomSummaryEntity>()
+                    .equalTo(LocalRoomSummaryEntityFields.ROOM_ID, params.localRoomId)
+                    .findFirst()
+                    ?.let {
+                        createRoomParams = it.createRoomParams
+                        isEncrypted = it.roomSummaryEntity?.isEncrypted.orFalse()
+                    }
+        }
+        val roomId = createRoomTask.execute(createRoomParams!!)
+
+        try {
+            // Wait for all the room events before triggering the replacement room
+            awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm ->
+                realm.where(RoomSummaryEntity::class.java)
+                        .equalTo(RoomSummaryEntityFields.ROOM_ID, roomId)
+                        .equalTo(RoomSummaryEntityFields.INVITED_MEMBERS_COUNT, createRoomParams?.invitedUserIds?.size ?: 0)
+            }
+            awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm ->
+                EventEntity.whereRoomId(realm, roomId)
+                        .equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_HISTORY_VISIBILITY)
+            }
+            if (isEncrypted) {
+                awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm ->
+                    EventEntity.whereRoomId(realm, roomId)
+                            .equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_ENCRYPTION)
+                }
+            }
+        } catch (exception: TimeoutCancellationException) {
+            throw CreateRoomFailure.CreatedWithTimeout(roomId)
+        }
+
+        createTombstoneEvent(params, roomId)
+        return roomId
+    }
+
+    /**
+     * Create a Tombstone event to indicate that the local room has been replaced by a new one.
+     */
+    private suspend fun createTombstoneEvent(params: CreateRoomFromLocalRoomTask.Params, roomId: String) {
+        val now = clock.epochMillis()
+        val event = Event(
+                type = EventType.STATE_ROOM_TOMBSTONE,
+                senderId = userId,
+                originServerTs = now,
+                stateKey = "",
+                eventId = UUID.randomUUID().toString(),
+                content = RoomTombstoneContent(
+                        replacementRoomId = roomId
+                ).toContent()
+        )
+        monarchy.awaitTransaction { realm ->
+            val eventEntity = event.toEntity(params.localRoomId, SendState.SYNCED, now).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC)
+            if (event.stateKey != null && event.type != null && event.eventId != null) {
+                CurrentStateEventEntity.getOrCreate(realm, params.localRoomId, event.stateKey, event.type).apply {
+                    eventId = event.eventId
+                    root = eventEntity
+                }
+            }
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt
index d7664057..e558d34f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt
@@ -54,8 +54,7 @@ internal class DefaultCreateRoomTask @Inject constructor(
         private val directChatsHelper: DirectChatsHelper,
         private val updateUserAccountDataTask: UpdateUserAccountDataTask,
         private val readMarkersTask: SetReadMarkersTask,
-        @SessionDatabase
-        private val realmConfiguration: RealmConfiguration,
+        @SessionDatabase private val realmConfiguration: RealmConfiguration,
         private val createRoomBodyBuilder: CreateRoomBodyBuilder,
         private val globalErrorReceiver: GlobalErrorReceiver,
         private val clock: Clock,
@@ -71,7 +70,6 @@ internal class DefaultCreateRoomTask @Inject constructor(
         }
 
         val createRoomBody = createRoomBodyBuilder.build(params)
-
         val createRoomResponse = try {
             executeRequest(globalErrorReceiver) {
                 roomAPI.createRoom(createRoomBody)
@@ -90,6 +88,7 @@ internal class DefaultCreateRoomTask @Inject constructor(
             }
             throw throwable
         }
+
         val roomId = createRoomResponse.roomId
         // Wait for room to come back from the sync (but it can maybe be in the DB if the sync response is received before)
         try {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/LocalRoomThirdPartyInviteContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/LocalRoomThirdPartyInviteContent.kt
new file mode 100644
index 00000000..617ed353
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/LocalRoomThirdPartyInviteContent.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.create
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.session.identity.ThreePid
+import org.matrix.android.sdk.api.session.room.model.Membership
+
+/**
+ * Class representing the EventType.LOCAL_STATE_ROOM_THIRD_PARTY_INVITE state event content
+ * This class is only used to store the third party invite data of a local room.
+ */
+@JsonClass(generateAdapter = true)
+internal data class LocalRoomThirdPartyInviteContent(
+        @Json(name = "membership") val membership: Membership,
+        @Json(name = "displayname") val displayName: String? = null,
+        @Json(name = "is_direct") val isDirect: Boolean = false,
+        @Json(name = "third_party_invite") val thirdPartyInvite: ThreePid? = null,
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/delete/DeleteLocalRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/delete/DeleteLocalRoomTask.kt
index 936c94e5..49951d2d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/delete/DeleteLocalRoomTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/delete/DeleteLocalRoomTask.kt
@@ -21,6 +21,7 @@ import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
 import org.matrix.android.sdk.internal.database.model.ChunkEntity
 import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
 import org.matrix.android.sdk.internal.database.model.EventEntity
+import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity
 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.RoomSummaryEntity
@@ -70,6 +71,9 @@ internal class DefaultDeleteLocalRoomTask @Inject constructor(
                 RoomEntity.where(realm, roomId = roomId).findAll()
                         ?.also { Timber.i("## DeleteLocalRoomTask - RoomEntity - delete ${it.size} entries") }
                         ?.deleteAllFromRealm()
+                LocalRoomSummaryEntity.where(realm, roomId = roomId).findAll()
+                        ?.also { Timber.i("## DeleteLocalRoomTask - LocalRoomSummaryEntity - delete ${it.size} entries") }
+                        ?.deleteAllFromRealm()
             }
         } else {
             Timber.i("## DeleteLocalRoomTask - Failed to remove room with id $roomId: not a local room")
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 fd655252..cb7bbf07 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
@@ -140,7 +140,8 @@ internal class RoomMemberEventHandler @Inject constructor(
             val previousDisplayName = prevContent?.get("displayname") as? String
             val previousAvatar = prevContent?.get("avatar_url") as? String
 
-            if (previousDisplayName != roomMember.displayName || previousAvatar != roomMember.avatarUrl) {
+            if ((previousDisplayName != null && previousDisplayName != roomMember.displayName) ||
+                    (previousAvatar != null && previousAvatar != roomMember.avatarUrl)) {
                 aggregator.userIdsToFetch.add(eventUserId)
             }
         }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/threepid/ThreePidInviteBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/threepid/ThreePidInviteBody.kt
index 3141c052..d7b78fae 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/threepid/ThreePidInviteBody.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/threepid/ThreePidInviteBody.kt
@@ -18,6 +18,8 @@ package org.matrix.android.sdk.internal.session.room.membership.threepid
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.session.identity.ThreePid
+import org.matrix.android.sdk.internal.auth.data.ThreePidMedium
 
 @JsonClass(generateAdapter = true)
 internal data class ThreePidInviteBody(
@@ -43,3 +45,9 @@ internal data class ThreePidInviteBody(
         @Json(name = "address")
         val address: String
 )
+
+internal fun ThreePidInviteBody.toThreePid() = when (medium) {
+    ThreePidMedium.EMAIL -> ThreePid.Email(address)
+    ThreePidMedium.MSISDN -> ThreePid.Msisdn(address)
+    else -> null
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SendStateTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SendStateTask.kt
index 59c9de29..ecc452ed 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SendStateTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SendStateTask.kt
@@ -16,10 +16,12 @@
 
 package org.matrix.android.sdk.internal.session.room.state
 
+import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
 import org.matrix.android.sdk.api.util.JsonDict
 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
+import org.matrix.android.sdk.internal.session.room.create.CreateRoomFromLocalRoomTask
 import org.matrix.android.sdk.internal.task.Task
 import timber.log.Timber
 import javax.inject.Inject
@@ -35,28 +37,40 @@ internal interface SendStateTask : Task<SendStateTask.Params, String> {
 
 internal class DefaultSendStateTask @Inject constructor(
         private val roomAPI: RoomAPI,
-        private val globalErrorReceiver: GlobalErrorReceiver
+        private val globalErrorReceiver: GlobalErrorReceiver,
+        private val createRoomFromLocalRoomTask: CreateRoomFromLocalRoomTask,
 ) : SendStateTask {
 
     override suspend fun execute(params: SendStateTask.Params): String {
         return executeRequest(globalErrorReceiver) {
-            val response = if (params.stateKey.isEmpty()) {
-                roomAPI.sendStateEvent(
-                        roomId = params.roomId,
-                        stateEventType = params.eventType,
-                        params = params.body
-                )
+            if (RoomLocalEcho.isLocalEchoId(params.roomId)) {
+                // Room is local, so create a real one and send the event to this new room
+                createRoomAndSendEvent(params)
             } else {
-                roomAPI.sendStateEvent(
-                        roomId = params.roomId,
-                        stateEventType = params.eventType,
-                        stateKey = params.stateKey,
-                        params = params.body
-                )
-            }
-            response.eventId.also {
-                Timber.d("State event: $it just sent in room ${params.roomId}")
+                val response = if (params.stateKey.isEmpty()) {
+                    roomAPI.sendStateEvent(
+                            roomId = params.roomId,
+                            stateEventType = params.eventType,
+                            params = params.body
+                    )
+                } else {
+                    roomAPI.sendStateEvent(
+                            roomId = params.roomId,
+                            stateEventType = params.eventType,
+                            stateKey = params.stateKey,
+                            params = params.body
+                    )
+                }
+                response.eventId.also {
+                    Timber.d("State event: $it just sent in room ${params.roomId}")
+                }
             }
         }
     }
+
+    private suspend fun createRoomAndSendEvent(params: SendStateTask.Params): String {
+        val roomId = createRoomFromLocalRoomTask.execute(CreateRoomFromLocalRoomTask.Params(params.roomId))
+        Timber.d("State event: convert local room (${params.roomId}) to existing room ($roomId) before sending the event.")
+        return execute(params.copy(roomId = roomId))
+    }
 }
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 7e064a84..6979d428 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
@@ -63,6 +63,7 @@ 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.session.sync.SyncResponsePostTreatmentAggregator
 import timber.log.Timber
 import javax.inject.Inject
 import kotlin.system.measureTimeMillis
@@ -91,7 +92,8 @@ internal class RoomSummaryUpdater @Inject constructor(
             roomSummary: RoomSyncSummary? = null,
             unreadNotifications: RoomSyncUnreadNotifications? = null,
             updateMembers: Boolean = false,
-            inviterId: String? = null
+            inviterId: String? = null,
+            aggregator: SyncResponsePostTreatmentAggregator? = null
     ) {
         val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId)
         if (roomSummary != null) {
@@ -180,8 +182,14 @@ internal class RoomSummaryUpdater @Inject constructor(
             roomSummaryEntity.otherMemberIds.clear()
             roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers)
             if (roomSummaryEntity.isEncrypted && otherRoomMembers.isNotEmpty()) {
-                // mmm maybe we could only refresh shield instead of checking trust also?
-                crossSigningService.onUsersDeviceUpdate(otherRoomMembers)
+                if (aggregator == null) {
+                    // Do it now
+                    // mmm maybe we could only refresh shield instead of checking trust also?
+                    crossSigningService.checkTrustAndAffectedRoomShields(otherRoomMembers)
+                } else {
+                    // Schedule it
+                    aggregator.userIdsForCheckingTrustAndAffectedRoomShields.addAll(otherRoomMembers)
+                }
             }
         }
     }
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 392c73bd..05216d1d 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
@@ -126,21 +126,33 @@ internal class SyncResponseHandler @Inject constructor(
         }
 
         // Everything else we need to do outside the transaction
-        aggregatorHandler.handle(aggregator)
+        measureTimeMillis {
+            aggregatorHandler.handle(aggregator)
+        }.also {
+            Timber.v("Aggregator management took $it ms")
+        }
 
-        syncResponse.rooms?.let {
-            checkPushRules(it, isInitialSync)
-            userAccountDataSyncHandler.synchronizeWithServerIfNeeded(it.invite)
-            dispatchInvitedRoom(it)
+        measureTimeMillis {
+            syncResponse.rooms?.let {
+                checkPushRules(it, isInitialSync)
+                userAccountDataSyncHandler.synchronizeWithServerIfNeeded(it.invite)
+                dispatchInvitedRoom(it)
+            }
+        }.also {
+            Timber.v("SyncResponse.rooms post treatment took $it ms")
         }
 
-        Timber.v("On sync completed")
-        cryptoSyncHandler.onSyncCompleted(syncResponse)
+        measureTimeMillis {
+            cryptoSyncHandler.onSyncCompleted(syncResponse)
+        }.also {
+            Timber.v("cryptoSyncHandler.onSyncCompleted took $it ms")
+        }
 
         // post sync stuffs
         monarchy.writeAsync {
             roomSyncHandler.postSyncSpaceHierarchyHandle(it)
         }
+        Timber.v("On sync completed")
     }
 
     private fun dispatchInvitedRoom(roomsSyncResponse: RoomsSyncResponse) {
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 e9452c59..2b7f936f 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
@@ -23,6 +23,9 @@ 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>()
+    // Set of userIds to fetch and update at the end of incremental syncs
+    val userIdsToFetch = mutableSetOf<String>()
+
+    // Set of users to call `crossSigningService.checkTrustAndAffectedRoomShields` once per sync
+    val userIdsForCheckingTrustAndAffectedRoomShields = mutableSetOf<String>()
 }
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 c638ed4f..c749f77f 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,32 +16,39 @@
 
 package org.matrix.android.sdk.internal.session.sync.handler
 
-import com.zhuinden.monarchy.Monarchy
+import androidx.work.BackoffPolicy
+import androidx.work.ExistingWorkPolicy
 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.crypto.crosssigning.DefaultCrossSigningService
+import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker
+import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorkerDataRepository
+import org.matrix.android.sdk.internal.di.SessionId
+import org.matrix.android.sdk.internal.di.WorkManagerProvider
 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 org.matrix.android.sdk.internal.util.logLimit
+import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
+import timber.log.Timber
+import java.util.concurrent.TimeUnit
 import javax.inject.Inject
 
 internal class SyncResponsePostTreatmentAggregatorHandler @Inject constructor(
         private val directChatsHelper: DirectChatsHelper,
         private val ephemeralTemporaryStore: RoomSyncEphemeralTemporaryStore,
         private val updateUserAccountDataTask: UpdateUserAccountDataTask,
-        private val getProfileInfoTask: GetProfileInfoTask,
-        @SessionDatabase private val monarchy: Monarchy,
+        private val crossSigningService: DefaultCrossSigningService,
+        private val updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository,
+        private val workManagerProvider: WorkManagerProvider,
+        @SessionId private val sessionId: String,
 ) {
     suspend fun handle(aggregator: SyncResponsePostTreatmentAggregator) {
         cleanupEphemeralFiles(aggregator.ephemeralFilesToDelete)
         updateDirectUserIds(aggregator.directChatsToCheck)
         fetchAndUpdateUsers(aggregator.userIdsToFetch)
+        handleUserIdsForCheckingTrustAndAffectedRoomShields(aggregator.userIdsForCheckingTrustAndAffectedRoomShields)
     }
 
     private fun cleanupEphemeralFiles(ephemeralFilesToDelete: List<String>) {
@@ -79,23 +86,26 @@ internal class SyncResponsePostTreatmentAggregatorHandler @Inject constructor(
         }
     }
 
-    private suspend fun fetchAndUpdateUsers(userIdsToFetch: List<String>) {
-        fetchUsers(userIdsToFetch)
-                .takeIf { it.isNotEmpty() }
-                ?.saveLocally()
-    }
+    private fun fetchAndUpdateUsers(userIdsToFetch: Collection<String>) {
+        if (userIdsToFetch.isEmpty()) return
+        Timber.d("## Configure Worker to update users: ${userIdsToFetch.logLimit()}")
+        val workerParams = UpdateTrustWorker.Params(
+                sessionId = sessionId,
+                filename = updateTrustWorkerDataRepository.createParam(userIdsToFetch.toList())
+        )
+        val workerData = WorkerParamsFactory.toData(workerParams)
 
-    private suspend fun fetchUsers(userIdsToFetch: List<String>) = userIdsToFetch.mapNotNull {
-        tryOrNull {
-            val profileJson = getProfileInfoTask.execute(GetProfileInfoTask.Params(it))
-            User.fromJson(it, profileJson)
-        }
+        val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<UpdateUserWorker>()
+                .setInputData(workerData)
+                .setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY_MILLIS, TimeUnit.MILLISECONDS)
+                .build()
+
+        workManagerProvider.workManager
+                .beginUniqueWork("USER_UPDATE_QUEUE", ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
+                .enqueue()
     }
 
-    private suspend fun List<User>.saveLocally() {
-        val userEntities = map { user -> UserEntityFactory.create(user) }
-        monarchy.awaitTransaction {
-            it.insertOrUpdate(userEntities)
-        }
+    private fun handleUserIdsForCheckingTrustAndAffectedRoomShields(userIdsWithDeviceUpdate: Iterable<String>) {
+        crossSigningService.checkTrustAndAffectedRoomShields(userIdsWithDeviceUpdate.toList())
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UpdateUserWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UpdateUserWorker.kt
new file mode 100644
index 00000000..1f840a82
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UpdateUserWorker.kt
@@ -0,0 +1,99 @@
+/*
+ * 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.sync.handler
+
+import android.content.Context
+import androidx.work.WorkerParameters
+import com.zhuinden.monarchy.Monarchy
+import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.api.session.user.model.User
+import org.matrix.android.sdk.internal.SessionManager
+import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker
+import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorkerDataRepository
+import org.matrix.android.sdk.internal.di.SessionDatabase
+import org.matrix.android.sdk.internal.session.SessionComponent
+import org.matrix.android.sdk.internal.session.profile.GetProfileInfoTask
+import org.matrix.android.sdk.internal.session.user.UserEntityFactory
+import org.matrix.android.sdk.internal.util.awaitTransaction
+import org.matrix.android.sdk.internal.util.logLimit
+import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
+import timber.log.Timber
+import javax.inject.Inject
+
+/**
+ * Note: We reuse the same type [UpdateTrustWorker.Params], since the input data are the same.
+ */
+internal class UpdateUserWorker(context: Context, params: WorkerParameters, sessionManager: SessionManager) :
+        SessionSafeCoroutineWorker<UpdateTrustWorker.Params>(context, params, sessionManager, UpdateTrustWorker.Params::class.java) {
+
+    @SessionDatabase
+    @Inject lateinit var monarchy: Monarchy
+    @Inject lateinit var updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository
+    @Inject lateinit var getProfileInfoTask: GetProfileInfoTask
+
+    override fun injectWith(injector: SessionComponent) {
+        injector.inject(this)
+    }
+
+    override suspend fun doSafeWork(params: UpdateTrustWorker.Params): Result {
+        val userList = params.filename
+                ?.let { updateTrustWorkerDataRepository.getParam(it) }
+                ?.userIds
+                ?: params.updatedUserIds.orEmpty()
+
+        // List should not be empty, but let's avoid go further in case of empty list
+        if (userList.isNotEmpty()) {
+            Timber.v("## UpdateUserWorker - updating users: ${userList.logLimit()}")
+            fetchAndUpdateUsers(userList)
+        }
+
+        cleanup(params)
+        return Result.success()
+    }
+
+    private suspend fun fetchAndUpdateUsers(userIdsToFetch: Collection<String>) {
+        fetchUsers(userIdsToFetch)
+                .takeIf { it.isNotEmpty() }
+                ?.saveLocally()
+    }
+
+    private suspend fun fetchUsers(userIdsToFetch: Collection<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) }
+        Timber.d("## saveLocally()")
+        monarchy.awaitTransaction {
+            Timber.d("## saveLocally() - in transaction")
+            it.insertOrUpdate(userEntities)
+        }
+        Timber.d("## saveLocally() - END")
+    }
+
+    private fun cleanup(params: UpdateTrustWorker.Params) {
+        params.filename
+                ?.let { updateTrustWorkerDataRepository.delete(it) }
+    }
+
+    override fun buildErrorParams(params: UpdateTrustWorker.Params, message: String): UpdateTrustWorker.Params {
+        return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
+    }
+}
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 30e1ec66..bc91ca20 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
@@ -154,12 +154,12 @@ internal class RoomSyncHandler @Inject constructor(
             }
             is HandlingStrategy.INVITED ->
                 handlingStrategy.data.mapWithProgress(reporter, InitialSyncStep.ImportingAccountInvitedRooms, 0.1f) {
-                    handleInvitedRoom(realm, it.key, it.value, insertType, syncLocalTimeStampMillis)
+                    handleInvitedRoom(realm, it.key, it.value, insertType, syncLocalTimeStampMillis, aggregator)
                 }
 
             is HandlingStrategy.LEFT -> {
                 handlingStrategy.data.mapWithProgress(reporter, InitialSyncStep.ImportingAccountLeftRooms, 0.3f) {
-                    handleLeftRoom(realm, it.key, it.value, insertType, syncLocalTimeStampMillis)
+                    handleLeftRoom(realm, it.key, it.value, insertType, syncLocalTimeStampMillis, aggregator)
                 }
             }
         }
@@ -285,7 +285,8 @@ internal class RoomSyncHandler @Inject constructor(
                 Membership.JOIN,
                 roomSync.summary,
                 roomSync.unreadNotifications,
-                updateMembers = hasRoomMember
+                updateMembers = hasRoomMember,
+                aggregator = aggregator
         )
         return roomEntity
     }
@@ -295,7 +296,8 @@ internal class RoomSyncHandler @Inject constructor(
             roomId: String,
             roomSync: InvitedRoomSync,
             insertType: EventInsertType,
-            syncLocalTimestampMillis: Long
+            syncLocalTimestampMillis: Long,
+            aggregator: SyncResponsePostTreatmentAggregator
     ): RoomEntity {
         Timber.v("Handle invited sync for room $roomId")
         val isInitialSync = insertType == EventInsertType.INITIAL_SYNC
@@ -319,7 +321,7 @@ internal class RoomSyncHandler @Inject constructor(
             it.type == EventType.STATE_ROOM_MEMBER
         }
         roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.INVITE)
-        roomSummaryUpdater.update(realm, roomId, Membership.INVITE, updateMembers = true, inviterId = inviterEvent?.senderId)
+        roomSummaryUpdater.update(realm, roomId, Membership.INVITE, updateMembers = true, inviterId = inviterEvent?.senderId, aggregator = aggregator)
         return roomEntity
     }
 
@@ -328,7 +330,8 @@ internal class RoomSyncHandler @Inject constructor(
             roomId: String,
             roomSync: RoomSync,
             insertType: EventInsertType,
-            syncLocalTimestampMillis: Long
+            syncLocalTimestampMillis: Long,
+            aggregator: SyncResponsePostTreatmentAggregator
     ): RoomEntity {
         val isInitialSync = insertType == EventInsertType.INITIAL_SYNC
         val roomEntity = RoomEntity.getOrCreate(realm, roomId)
@@ -366,7 +369,7 @@ internal class RoomSyncHandler @Inject constructor(
         roomEntity.chunks.clearWith { it.deleteOnCascade(deleteStateEvents = true, canDeleteRoot = true) }
         roomTypingUsersHandler.handle(realm, roomId, null)
         roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.LEAVE)
-        roomSummaryUpdater.update(realm, roomId, membership, roomSync.summary, roomSync.unreadNotifications)
+        roomSummaryUpdater.update(realm, roomId, membership, roomSync.summary, roomSync.unreadNotifications, aggregator = aggregator)
         return roomEntity
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/MatrixWorkerFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/MatrixWorkerFactory.kt
index 83f95328..80bbbb79 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/MatrixWorkerFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/MatrixWorkerFactory.kt
@@ -30,6 +30,7 @@ import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.Dea
 import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker
 import org.matrix.android.sdk.internal.session.room.send.RedactEventWorker
 import org.matrix.android.sdk.internal.session.room.send.SendEventWorker
+import org.matrix.android.sdk.internal.session.sync.handler.UpdateUserWorker
 import org.matrix.android.sdk.internal.session.sync.job.SyncWorker
 import timber.log.Timber
 import javax.inject.Inject
@@ -62,6 +63,8 @@ internal class MatrixWorkerFactory @Inject constructor(private val sessionManage
                 SyncWorker(appContext, workerParameters, sessionManager)
             UpdateTrustWorker::class.java.name ->
                 UpdateTrustWorker(appContext, workerParameters, sessionManager)
+            UpdateUserWorker::class.java.name ->
+                UpdateUserWorker(appContext, workerParameters, sessionManager)
             UploadContentWorker::class.java.name ->
                 UploadContentWorker(appContext, workerParameters, sessionManager)
             DeactivateLiveLocationShareWorker::class.java.name ->
diff --git a/matrix-sdk-android/src/release/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt b/matrix-sdk-android/src/release/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt
deleted file mode 100644
index a815cec3..00000000
--- a/matrix-sdk-android/src/release/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt
+++ /dev/null
@@ -1,30 +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.interceptors
-
-import androidx.annotation.NonNull
-import okhttp3.logging.HttpLoggingInterceptor
-
-/**
- * No op logger
- */
-internal class FormattedJsonHttpLogger : HttpLoggingInterceptor.Logger {
-
-    @Synchronized
-    override fun log(@NonNull message: String) {
-    }
-}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/MyDeviceLastSeenInfoEntityMapperTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/MyDeviceLastSeenInfoEntityMapperTest.kt
new file mode 100644
index 00000000..a27f430e
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/MyDeviceLastSeenInfoEntityMapperTest.kt
@@ -0,0 +1,52 @@
+/*
+ * 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.crypto.store.db.mapper
+
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.Test
+import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
+import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntity
+
+private const val A_DEVICE_ID = "device-id"
+private const val AN_IP_ADDRESS = "ip-address"
+private const val A_TIMESTAMP = 123L
+private const val A_DISPLAY_NAME = "display-name"
+
+class MyDeviceLastSeenInfoEntityMapperTest {
+
+    private val myDeviceLastSeenInfoEntityMapper = MyDeviceLastSeenInfoEntityMapper()
+
+    @Test
+    fun `given an entity when mapping to model then all fields are correctly mapped`() {
+        val entity = MyDeviceLastSeenInfoEntity(
+                deviceId = A_DEVICE_ID,
+                lastSeenIp = AN_IP_ADDRESS,
+                lastSeenTs = A_TIMESTAMP,
+                displayName = A_DISPLAY_NAME
+        )
+        val expectedDeviceInfo = DeviceInfo(
+                deviceId = A_DEVICE_ID,
+                lastSeenIp = AN_IP_ADDRESS,
+                lastSeenTs = A_TIMESTAMP,
+                displayName = A_DISPLAY_NAME
+        )
+
+        val deviceInfo = myDeviceLastSeenInfoEntityMapper.map(entity)
+
+        deviceInfo shouldBeEqualTo expectedDeviceInfo
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/create/DefaultCreateLocalRoomStateEventsTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/create/DefaultCreateLocalRoomStateEventsTaskTest.kt
new file mode 100644
index 00000000..1c2cf293
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/create/DefaultCreateLocalRoomStateEventsTaskTest.kt
@@ -0,0 +1,462 @@
+/*
+ * 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.create
+
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.unmockkAll
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.amshove.kluent.shouldBeEqualTo
+import org.amshove.kluent.shouldNotBeNull
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.matrix.android.sdk.api.MatrixPatterns.getServerName
+import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
+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.content.EncryptionEventContent
+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.identity.ThreePid
+import org.matrix.android.sdk.api.session.room.model.GuestAccess
+import org.matrix.android.sdk.api.session.room.model.Membership
+import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
+import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent
+import org.matrix.android.sdk.api.session.room.model.RoomGuestAccessContent
+import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
+import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent
+import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
+import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
+import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
+import org.matrix.android.sdk.api.session.room.model.RoomNameContent
+import org.matrix.android.sdk.api.session.room.model.RoomThirdPartyInviteContent
+import org.matrix.android.sdk.api.session.room.model.RoomTopicContent
+import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset
+import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
+import org.matrix.android.sdk.api.session.room.powerlevels.Role
+import org.matrix.android.sdk.api.session.user.UserService
+import org.matrix.android.sdk.api.session.user.model.User
+import org.matrix.android.sdk.internal.session.profile.ThirdPartyIdentifier.Companion.MEDIUM_EMAIL
+import org.matrix.android.sdk.internal.session.profile.ThirdPartyIdentifier.Companion.MEDIUM_MSISDN
+import org.matrix.android.sdk.internal.session.room.membership.threepid.ThreePidInviteBody
+import org.matrix.android.sdk.internal.session.room.membership.threepid.toThreePid
+import org.matrix.android.sdk.internal.util.time.DefaultClock
+
+private const val MY_USER_ID = "my-user-id"
+private const val MY_USER_DISPLAY_NAME = "my-user-display-name"
+private const val MY_USER_AVATAR = "my-user-avatar"
+
+@ExperimentalCoroutinesApi
+internal class DefaultCreateLocalRoomStateEventsTaskTest {
+
+    private val clock = DefaultClock()
+    private val userService = mockk<UserService>()
+
+    private val defaultCreateLocalRoomStateEventsTask = DefaultCreateLocalRoomStateEventsTask(
+            myUserId = MY_USER_ID,
+            userService = userService,
+            clock = clock
+    )
+
+    lateinit var createRoomBody: CreateRoomBody
+
+    @Before
+    fun setup() {
+        createRoomBody = mockk {
+            every { roomVersion } returns null
+            every { creationContent } returns null
+            every { roomAliasName } returns null
+            every { topic } returns null
+            every { name } returns null
+            every { powerLevelContentOverride } returns null
+            every { initialStates } returns null
+            every { invite3pids } returns null
+            every { preset } returns null
+            every { isDirect } returns null
+            every { invitedUserIds } returns null
+        }
+        coEvery { userService.resolveUser(any()) } answers { User(firstArg()) }
+    }
+
+    @After
+    fun tearDown() {
+        unmockkAll()
+    }
+
+    @Test
+    fun `given a CreateRoomBody when execute then the resulting list of events contains the correct room create state event`() = runTest {
+        // Given
+        val aRoomVersion = "a_room_version"
+
+        every { createRoomBody.roomVersion } returns aRoomVersion
+
+        // When
+        val params = CreateLocalRoomStateEventsTask.Params(createRoomBody)
+        val result = defaultCreateLocalRoomStateEventsTask.execute(params)
+
+        // Then
+        val roomCreateEvent = result.find { it.type == EventType.STATE_ROOM_CREATE }
+        val roomCreateContent = roomCreateEvent?.content.toModel<RoomCreateContent>()
+
+        roomCreateContent?.creator shouldBeEqualTo MY_USER_ID
+        roomCreateContent?.roomVersion shouldBeEqualTo aRoomVersion
+    }
+
+    @Test
+    fun `given a CreateRoomBody when execute then the resulting list of events contains the correct name and topic state events`() = runTest {
+        // Given
+        val aRoomName = "a_room_name"
+        val aRoomTopic = "a_room_topic"
+
+        every { createRoomBody.name } returns aRoomName
+        every { createRoomBody.topic } returns aRoomTopic
+
+        // When
+        val params = CreateLocalRoomStateEventsTask.Params(createRoomBody)
+        val result = defaultCreateLocalRoomStateEventsTask.execute(params)
+
+        // Then
+        val roomNameEvent = result.find { it.type == EventType.STATE_ROOM_NAME }
+        val roomTopicEvent = result.find { it.type == EventType.STATE_ROOM_TOPIC }
+
+        roomNameEvent?.content.toModel<RoomNameContent>()?.name shouldBeEqualTo aRoomName
+        roomTopicEvent?.content.toModel<RoomTopicContent>()?.topic shouldBeEqualTo aRoomTopic
+    }
+
+    @Test
+    fun `given a CreateRoomBody when execute then the resulting list of events contains the correct room member events`() = runTest {
+        // Given
+        data class RoomMember(val user: User, val membership: Membership)
+
+        val aRoomMemberList: List<RoomMember> = listOf(
+                RoomMember(User(MY_USER_ID, MY_USER_DISPLAY_NAME, MY_USER_AVATAR), Membership.JOIN),
+                RoomMember(User("userA_id", "userA_display_name", "userA_avatar"), Membership.INVITE),
+                RoomMember(User("userB_id", "userB_display_name", "userB_avatar"), Membership.INVITE)
+        )
+
+        every { createRoomBody.invitedUserIds } returns aRoomMemberList.filter { it.membership == Membership.INVITE }.map { it.user.userId }
+        coEvery { userService.resolveUser(any()) } answers {
+            aRoomMemberList.map { it.user }.find { it.userId == firstArg() } ?: User(firstArg())
+        }
+
+        // When
+        val params = CreateLocalRoomStateEventsTask.Params(createRoomBody)
+        val result = defaultCreateLocalRoomStateEventsTask.execute(params)
+
+        // Then
+        val roomMemberEvents = result.filter { it.type == EventType.STATE_ROOM_MEMBER }
+
+        roomMemberEvents.map { it.stateKey } shouldBeEqualTo aRoomMemberList.map { it.user.userId }
+        roomMemberEvents.forEach { event ->
+            val roomMemberContent = event.content.toModel<RoomMemberContent>()
+            val roomMember = aRoomMemberList.find { it.user.userId == event.stateKey }
+
+            roomMember.shouldNotBeNull()
+            roomMemberContent?.avatarUrl shouldBeEqualTo roomMember.user.avatarUrl
+            roomMemberContent?.displayName shouldBeEqualTo roomMember.user.displayName
+            roomMemberContent?.membership shouldBeEqualTo roomMember.membership
+        }
+    }
+
+    @Test
+    fun `given a CreateRoomBody when execute then the resulting list of events contains the correct power levels event`() = runTest {
+        // Given
+        val aPowerLevelsContent = PowerLevelsContent(
+                ban = 1,
+                kick = 2,
+                invite = 3,
+                redact = 4,
+                eventsDefault = 5,
+                events = null,
+                usersDefault = 6,
+                users = null,
+                stateDefault = 7,
+                notifications = null
+        )
+
+        every { createRoomBody.powerLevelContentOverride } returns aPowerLevelsContent
+
+        // When
+        val params = CreateLocalRoomStateEventsTask.Params(createRoomBody)
+        val result = defaultCreateLocalRoomStateEventsTask.execute(params)
+
+        // Then
+        val roomPowerLevelsEvent = result.find { it.type == EventType.STATE_ROOM_POWER_LEVELS }
+        roomPowerLevelsEvent?.content.toModel<PowerLevelsContent>() shouldBeEqualTo aPowerLevelsContent
+    }
+
+    @Test
+    fun `given a CreateRoomBody when execute then the resulting list of events contains the correct canonical alias event`() = runTest {
+        // Given
+        val aRoomAlias = "a_room_alias"
+        val expectedCanonicalAlias = "$aRoomAlias:${MY_USER_ID.getServerName()}"
+
+        every { createRoomBody.roomAliasName } returns aRoomAlias
+
+        // When
+        val params = CreateLocalRoomStateEventsTask.Params(createRoomBody)
+        val result = defaultCreateLocalRoomStateEventsTask.execute(params)
+
+        // Then
+        val roomPowerLevelsEvent = result.find { it.type == EventType.STATE_ROOM_CANONICAL_ALIAS }
+        roomPowerLevelsEvent?.content.toModel<RoomCanonicalAliasContent>()?.canonicalAlias shouldBeEqualTo expectedCanonicalAlias
+    }
+
+    @Test
+    fun `given a CreateRoomBody when execute then the resulting list of events contains the correct preset related events`() = runTest {
+        data class ExpectedResult(val joinRules: RoomJoinRules, val historyVisibility: RoomHistoryVisibility, val guestAccess: GuestAccess)
+        data class Case(val preset: CreateRoomPreset, val expectedResult: ExpectedResult)
+
+        CreateRoomPreset.values().forEach { aRoomPreset ->
+            // Given
+            val case = when (aRoomPreset) {
+                CreateRoomPreset.PRESET_PRIVATE_CHAT -> Case(
+                        CreateRoomPreset.PRESET_PRIVATE_CHAT,
+                        ExpectedResult(RoomJoinRules.INVITE, RoomHistoryVisibility.SHARED, GuestAccess.CanJoin)
+                )
+                CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT -> Case(
+                        CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT,
+                        ExpectedResult(RoomJoinRules.INVITE, RoomHistoryVisibility.SHARED, GuestAccess.CanJoin)
+                )
+                CreateRoomPreset.PRESET_PUBLIC_CHAT -> Case(
+                        CreateRoomPreset.PRESET_PUBLIC_CHAT,
+                        ExpectedResult(RoomJoinRules.PUBLIC, RoomHistoryVisibility.SHARED, GuestAccess.Forbidden)
+                )
+            }
+            every { createRoomBody.preset } returns case.preset
+
+            // When
+            val params = CreateLocalRoomStateEventsTask.Params(createRoomBody)
+            val result = defaultCreateLocalRoomStateEventsTask.execute(params)
+
+            // Then
+            result.find { it.type == EventType.STATE_ROOM_JOIN_RULES }
+                    ?.content.toModel<RoomJoinRulesContent>()
+                    ?.joinRules shouldBeEqualTo case.expectedResult.joinRules
+            result.find { it.type == EventType.STATE_ROOM_HISTORY_VISIBILITY }
+                    ?.content.toModel<RoomHistoryVisibilityContent>()
+                    ?.historyVisibility shouldBeEqualTo case.expectedResult.historyVisibility
+            result.find { it.type == EventType.STATE_ROOM_GUEST_ACCESS }
+                    ?.content.toModel<RoomGuestAccessContent>()
+                    ?.guestAccess shouldBeEqualTo case.expectedResult.guestAccess
+        }
+    }
+
+    @Test
+    fun `given a CreateRoomBody when execute then the resulting list of events contains the initial state events`() = runTest {
+        // Given
+        val aListOfInitialStateEvents = listOf(
+                Event(
+                        type = EventType.STATE_ROOM_ENCRYPTION,
+                        stateKey = "",
+                        content = EncryptionEventContent(MXCRYPTO_ALGORITHM_MEGOLM).toContent()
+                ),
+                Event(
+                        type = "a_custom_type",
+                        content = mapOf("a_custom_map_to_integer" to 42),
+                        stateKey = "a_state_key"
+                ),
+                Event(
+                        type = "another_custom_type",
+                        content = mapOf("a_custom_map_to_boolean" to false),
+                        stateKey = "another_state_key"
+                )
+        )
+
+        every { createRoomBody.initialStates } returns aListOfInitialStateEvents
+
+        // When
+        val params = CreateLocalRoomStateEventsTask.Params(createRoomBody)
+        val result = defaultCreateLocalRoomStateEventsTask.execute(params)
+
+        // Then
+        aListOfInitialStateEvents.forEach { expected ->
+            val found = result.find { it.type == expected.type }
+            found.shouldNotBeNull()
+            found.content shouldBeEqualTo expected.content
+            found.stateKey shouldBeEqualTo expected.stateKey
+        }
+    }
+
+    @Test
+    fun `given a CreateRoomBody when execute then the resulting list of events contains the correct third party invite events`() = runTest {
+        // Given
+        val aListOfThreePids = listOf(
+                ThreePid.Email("bob@matrix.org"),
+                ThreePid.Msisdn("+11111111111"),
+                ThreePid.Email("alice@matrix.org"),
+                ThreePid.Msisdn("+22222222222"),
+        )
+        val aListOf3pids = aListOfThreePids.mapIndexed { index, threePid ->
+            ThreePidInviteBody(
+                    idServer = "an_id_server_$index",
+                    idAccessToken = "an_id_access_token_$index",
+                    medium = when (threePid) {
+                        is ThreePid.Email -> MEDIUM_EMAIL
+                        is ThreePid.Msisdn -> MEDIUM_MSISDN
+                    },
+                    address = threePid.value
+            )
+        }
+        every { createRoomBody.invite3pids } returns aListOf3pids
+
+        // When
+        val params = CreateLocalRoomStateEventsTask.Params(createRoomBody)
+        val result = defaultCreateLocalRoomStateEventsTask.execute(params)
+
+        // Then
+        val thirdPartyInviteEvents = result.filter { it.type == EventType.STATE_ROOM_THIRD_PARTY_INVITE }
+        val thirdPartyInviteContents = thirdPartyInviteEvents.map { it.content.toModel<RoomThirdPartyInviteContent>() }
+        val localThirdPartyInviteEvents = result.filter { it.type == EventType.LOCAL_STATE_ROOM_THIRD_PARTY_INVITE }
+        val localThirdPartyInviteContents = localThirdPartyInviteEvents.map { it.content.toModel<LocalRoomThirdPartyInviteContent>() }
+
+        thirdPartyInviteEvents.size shouldBeEqualTo aListOf3pids.size
+        localThirdPartyInviteEvents.size shouldBeEqualTo aListOf3pids.size
+
+        aListOf3pids.forEach { expected ->
+            thirdPartyInviteContents.find { it?.displayName == expected.address }.shouldNotBeNull()
+
+            val localThirdPartyInviteContent = localThirdPartyInviteContents.find { it?.thirdPartyInvite == expected.toThreePid() }
+            localThirdPartyInviteContent.shouldNotBeNull()
+            localThirdPartyInviteContent.membership shouldBeEqualTo Membership.INVITE
+            localThirdPartyInviteContent.isDirect shouldBeEqualTo createRoomBody.isDirect.orFalse()
+            localThirdPartyInviteContent.displayName shouldBeEqualTo expected.address
+        }
+    }
+
+    @Test
+    fun `given a CreateRoomBody with default values when execute then the resulting list of events is correct`() = runTest {
+        // Given
+        // map of expected event types to occurrences
+        val expectedEventTypes = mapOf(
+                EventType.STATE_ROOM_CREATE to 1,
+                EventType.STATE_ROOM_POWER_LEVELS to 1,
+                EventType.STATE_ROOM_MEMBER to 1,
+                EventType.STATE_ROOM_GUEST_ACCESS to 1,
+                EventType.STATE_ROOM_HISTORY_VISIBILITY to 1,
+        )
+        coEvery { userService.resolveUser(any()) } answers {
+            if (firstArg<String>() == MY_USER_ID) User(MY_USER_ID, MY_USER_DISPLAY_NAME, MY_USER_AVATAR) else User(firstArg())
+        }
+
+        // When
+        val params = CreateLocalRoomStateEventsTask.Params(createRoomBody)
+        val result = defaultCreateLocalRoomStateEventsTask.execute(params)
+
+        // Then
+        result.size shouldBeEqualTo expectedEventTypes.values.sum()
+        result.map { it.type }.toSet() shouldBeEqualTo expectedEventTypes.keys
+
+        // Room create
+        result.find { it.type == EventType.STATE_ROOM_CREATE }.shouldNotBeNull()
+        // Room member
+        result.singleOrNull { it.type == EventType.STATE_ROOM_MEMBER }?.stateKey shouldBeEqualTo MY_USER_ID
+        // Power levels
+        val powerLevelsContent = result.find { it.type == EventType.STATE_ROOM_POWER_LEVELS }?.content.toModel<PowerLevelsContent>()
+        powerLevelsContent.shouldNotBeNull()
+        powerLevelsContent.ban shouldBeEqualTo Role.Moderator.value
+        powerLevelsContent.kick shouldBeEqualTo Role.Moderator.value
+        powerLevelsContent.invite shouldBeEqualTo Role.Moderator.value
+        powerLevelsContent.redact shouldBeEqualTo Role.Moderator.value
+        powerLevelsContent.eventsDefault shouldBeEqualTo Role.Default.value
+        powerLevelsContent.usersDefault shouldBeEqualTo Role.Default.value
+        powerLevelsContent.stateDefault shouldBeEqualTo Role.Moderator.value
+        // Guest access
+        result.find { it.type == EventType.STATE_ROOM_GUEST_ACCESS }
+                ?.content.toModel<RoomGuestAccessContent>()?.guestAccess shouldBeEqualTo GuestAccess.Forbidden
+        // History visibility
+        result.find { it.type == EventType.STATE_ROOM_HISTORY_VISIBILITY }
+                ?.content.toModel<RoomHistoryVisibilityContent>()?.historyVisibility shouldBeEqualTo RoomHistoryVisibility.SHARED
+    }
+
+    @Test
+    fun `given a CreateRoomBody when execute then the resulting list of events is correctly ordered with the right values`() = runTest {
+        // Given
+        val expectedIsDirect = true
+        val expectedHistoryVisibility = RoomHistoryVisibility.WORLD_READABLE
+
+        every { createRoomBody.roomVersion } returns "a_room_version"
+        every { createRoomBody.roomAliasName } returns "a_room_alias_name"
+        every { createRoomBody.name } returns "a_name"
+        every { createRoomBody.topic } returns "a_topic"
+        every { createRoomBody.powerLevelContentOverride } returns PowerLevelsContent(
+                ban = 1,
+                kick = 2,
+                invite = 3,
+                redact = 4,
+                eventsDefault = 5,
+                events = null,
+                usersDefault = 6,
+                users = null,
+                stateDefault = 7,
+                notifications = null
+        )
+        every { createRoomBody.invite3pids } returns listOf(
+                ThreePidInviteBody(
+                        idServer = "an_id_server",
+                        idAccessToken = "an_id_access_token",
+                        medium = MEDIUM_EMAIL,
+                        address = "an_email@example.org"
+                )
+        )
+        every { createRoomBody.preset } returns CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT
+        every { createRoomBody.initialStates } returns listOf(
+                Event(type = "a_custom_type", stateKey = ""),
+                // override the value from the preset
+                Event(
+                        type = EventType.STATE_ROOM_HISTORY_VISIBILITY,
+                        stateKey = "",
+                        content = RoomHistoryVisibilityContent(expectedHistoryVisibility.value).toContent()
+                )
+        )
+        every { createRoomBody.isDirect } returns expectedIsDirect
+        every { createRoomBody.invitedUserIds } returns listOf("a_user_id")
+
+        val orderedExpectedEventType = listOf(
+                EventType.STATE_ROOM_CREATE,
+                EventType.STATE_ROOM_MEMBER,
+                EventType.STATE_ROOM_POWER_LEVELS,
+                EventType.STATE_ROOM_CANONICAL_ALIAS,
+                EventType.STATE_ROOM_JOIN_RULES,
+                EventType.STATE_ROOM_GUEST_ACCESS,
+                "a_custom_type",
+                EventType.STATE_ROOM_HISTORY_VISIBILITY,
+                EventType.STATE_ROOM_NAME,
+                EventType.STATE_ROOM_TOPIC,
+                EventType.STATE_ROOM_MEMBER,
+                EventType.LOCAL_STATE_ROOM_THIRD_PARTY_INVITE,
+                EventType.STATE_ROOM_THIRD_PARTY_INVITE,
+        )
+
+        // When
+        val params = CreateLocalRoomStateEventsTask.Params(createRoomBody)
+        val result = defaultCreateLocalRoomStateEventsTask.execute(params)
+
+        // Then
+        result.map { it.type } shouldBeEqualTo orderedExpectedEventType
+        result.find { it.type == EventType.STATE_ROOM_HISTORY_VISIBILITY }
+                ?.content.toModel<RoomHistoryVisibilityContent>()?.historyVisibility shouldBeEqualTo expectedHistoryVisibility
+        result.lastOrNull { it.type == EventType.STATE_ROOM_MEMBER }
+                ?.content.toModel<RoomMemberContent>()?.isDirect shouldBeEqualTo expectedIsDirect
+        result.lastOrNull { it.type == EventType.LOCAL_STATE_ROOM_THIRD_PARTY_INVITE }
+                ?.content.toModel<LocalRoomThirdPartyInviteContent>()?.isDirect shouldBeEqualTo expectedIsDirect
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/create/DefaultCreateRoomFromLocalRoomTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/create/DefaultCreateRoomFromLocalRoomTaskTest.kt
new file mode 100644
index 00000000..d3732363
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/create/DefaultCreateRoomFromLocalRoomTaskTest.kt
@@ -0,0 +1,158 @@
+/*
+ * 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.create
+
+import io.mockk.coEvery
+import io.mockk.coJustRun
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.unmockkAll
+import io.realm.kotlin.where
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+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.create.CreateRoomParams
+import org.matrix.android.sdk.api.session.room.model.tombstone.RoomTombstoneContent
+import org.matrix.android.sdk.internal.database.awaitNotEmptyResult
+import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
+import org.matrix.android.sdk.internal.database.model.EventEntity
+import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntity
+import org.matrix.android.sdk.internal.database.model.LocalRoomSummaryEntityFields
+import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
+import org.matrix.android.sdk.internal.database.query.getOrCreate
+import org.matrix.android.sdk.internal.util.time.DefaultClock
+import org.matrix.android.sdk.test.fakes.FakeMonarchy
+import org.matrix.android.sdk.test.fakes.FakeStateEventDataSource
+
+private const val A_LOCAL_ROOM_ID = "local.a-local-room-id"
+private const val AN_EXISTING_ROOM_ID = "an-existing-room-id"
+private const val A_ROOM_ID = "a-room-id"
+private const val MY_USER_ID = "my-user-id"
+
+@ExperimentalCoroutinesApi
+internal class DefaultCreateRoomFromLocalRoomTaskTest {
+
+    private val fakeMonarchy = FakeMonarchy()
+    private val clock = DefaultClock()
+    private val createRoomTask = mockk<CreateRoomTask>()
+    private val fakeStateEventDataSource = FakeStateEventDataSource()
+
+    private val defaultCreateRoomFromLocalRoomTask = DefaultCreateRoomFromLocalRoomTask(
+            userId = MY_USER_ID,
+            monarchy = fakeMonarchy.instance,
+            createRoomTask = createRoomTask,
+            stateEventDataSource = fakeStateEventDataSource.instance,
+            clock = clock
+    )
+
+    @Before
+    fun setup() {
+        mockkStatic("org.matrix.android.sdk.internal.database.RealmQueryLatchKt")
+        coJustRun { awaitNotEmptyResult<Any>(realmConfiguration = any(), timeoutMillis = any(), builder = any()) }
+
+        mockkStatic("org.matrix.android.sdk.internal.database.query.EventEntityQueriesKt")
+        coEvery { any<EventEntity>().copyToRealmOrIgnore(fakeMonarchy.fakeRealm.instance, any()) } answers { firstArg() }
+
+        mockkStatic("org.matrix.android.sdk.internal.database.query.CurrentStateEventEntityQueriesKt")
+        every { CurrentStateEventEntity.getOrCreate(fakeMonarchy.fakeRealm.instance, any(), any(), any()) } answers {
+            CurrentStateEventEntity(roomId = arg(2), stateKey = arg(3), type = arg(4))
+        }
+    }
+
+    @After
+    fun tearDown() {
+        unmockkAll()
+    }
+
+    @Test
+    fun `given a local room id when execute then the existing room id is kept`() = runTest {
+        // Given
+        givenATombstoneEvent(
+                Event(
+                        roomId = A_LOCAL_ROOM_ID,
+                        type = EventType.STATE_ROOM_TOMBSTONE,
+                        stateKey = "",
+                        content = RoomTombstoneContent(replacementRoomId = AN_EXISTING_ROOM_ID).toContent()
+                )
+        )
+
+        // When
+        val params = CreateRoomFromLocalRoomTask.Params(A_LOCAL_ROOM_ID)
+        val result = defaultCreateRoomFromLocalRoomTask.execute(params)
+
+        // Then
+        verifyTombstoneEvent(AN_EXISTING_ROOM_ID)
+        result shouldBeEqualTo AN_EXISTING_ROOM_ID
+    }
+
+    @Test
+    fun `given a local room id when execute then it is correctly executed`() = runTest {
+        // Given
+        val aCreateRoomParams = mockk<CreateRoomParams>()
+        val aLocalRoomSummaryEntity = mockk<LocalRoomSummaryEntity> {
+            every { roomSummaryEntity } returns mockk(relaxed = true)
+            every { createRoomParams } returns aCreateRoomParams
+        }
+        givenATombstoneEvent(null)
+        givenALocalRoomSummaryEntity(aLocalRoomSummaryEntity)
+
+        coEvery { createRoomTask.execute(any()) } returns A_ROOM_ID
+
+        // When
+        val params = CreateRoomFromLocalRoomTask.Params(A_LOCAL_ROOM_ID)
+        val result = defaultCreateRoomFromLocalRoomTask.execute(params)
+
+        // Then
+        verifyTombstoneEvent(null)
+        // CreateRoomTask has been called with the initial CreateRoomParams
+        coVerify { createRoomTask.execute(aCreateRoomParams) }
+        // The resulting roomId matches the roomId returned by the createRoomTask
+        result shouldBeEqualTo A_ROOM_ID
+        // A tombstone state event has been created
+        coVerify { CurrentStateEventEntity.getOrCreate(realm = any(), roomId = A_LOCAL_ROOM_ID, stateKey = any(), type = EventType.STATE_ROOM_TOMBSTONE) }
+    }
+
+    private fun givenATombstoneEvent(event: Event?) {
+        fakeStateEventDataSource.givenGetStateEventReturns(event)
+    }
+
+    private fun givenALocalRoomSummaryEntity(localRoomSummaryEntity: LocalRoomSummaryEntity) {
+        every {
+            fakeMonarchy.fakeRealm.instance
+                    .where<LocalRoomSummaryEntity>()
+                    .equalTo(LocalRoomSummaryEntityFields.ROOM_ID, A_LOCAL_ROOM_ID)
+                    .findFirst()
+        } returns localRoomSummaryEntity
+    }
+
+    private fun verifyTombstoneEvent(expectedRoomId: String?) {
+        fakeStateEventDataSource.verifyGetStateEvent(A_LOCAL_ROOM_ID, EventType.STATE_ROOM_TOMBSTONE, QueryStringValue.IsEmpty)
+        fakeStateEventDataSource.instance.getStateEvent(A_LOCAL_ROOM_ID, EventType.STATE_ROOM_TOMBSTONE, QueryStringValue.IsEmpty)
+                ?.content.toModel<RoomTombstoneContent>()
+                ?.replacementRoomId shouldBeEqualTo expectedRoomId
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt
index 588bfaa9..d51ed773 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt
@@ -22,6 +22,7 @@ import kotlinx.coroutines.test.runTest
 import org.amshove.kluent.shouldBeEqualTo
 import org.junit.After
 import org.junit.Test
+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
@@ -69,7 +70,7 @@ class DefaultGetActiveBeaconInfoForUserTaskTest {
         fakeStateEventDataSource.verifyGetStateEvent(
                 roomId = params.roomId,
                 eventType = EventType.STATE_ROOM_BEACON_INFO.first(),
-                stateKey = A_USER_ID
+                stateKey = QueryStringValue.Equals(A_USER_ID)
         )
     }
 }
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt
index d77084fe..2d501f12 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt
@@ -33,7 +33,7 @@ import org.matrix.android.sdk.internal.util.awaitTransaction
 internal class FakeMonarchy {
 
     val instance = mockk<Monarchy>()
-    private val fakeRealm = FakeRealm()
+    val fakeRealm = FakeRealm()
 
     init {
         mockkStatic("org.matrix.android.sdk.internal.util.MonarchyKt")
@@ -42,6 +42,12 @@ internal class FakeMonarchy {
         } coAnswers {
             secondArg<suspend (Realm) -> Any>().invoke(fakeRealm.instance)
         }
+        coEvery {
+            instance.doWithRealm(any())
+        } coAnswers {
+            firstArg<Monarchy.RealmBlock>().doWithRealm(fakeRealm.instance)
+        }
+        every { instance.realmConfiguration } returns mockk()
     }
 
     inline fun <reified T : RealmModel> givenWhere(): RealmQuery<T> {
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeStateEventDataSource.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeStateEventDataSource.kt
index ca03316f..ebb2a1d7 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeStateEventDataSource.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeStateEventDataSource.kt
@@ -19,7 +19,7 @@ package org.matrix.android.sdk.test.fakes
 import io.mockk.every
 import io.mockk.mockk
 import io.mockk.verify
-import org.matrix.android.sdk.api.query.QueryStringValue
+import org.matrix.android.sdk.api.query.QueryStateEventValue
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
 
@@ -37,12 +37,12 @@ internal class FakeStateEventDataSource {
         } returns event
     }
 
-    fun verifyGetStateEvent(roomId: String, eventType: String, stateKey: String) {
+    fun verifyGetStateEvent(roomId: String, eventType: String, stateKey: QueryStateEventValue) {
         verify {
             instance.getStateEvent(
                     roomId = roomId,
                     eventType = eventType,
-                    stateKey = QueryStringValue.Equals(stateKey)
+                    stateKey = stateKey
             )
         }
     }
-- 
GitLab