diff --git a/CHANGES.md b/CHANGES.md
index 30fb194c5e1a802761f919a62aed257f445108e3..22ed75abf2e9a698b654cee9f27b3d1a1bfaa9bd 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,18 @@
 Please also refer to the Changelog of Element Android: https://github.com/vector-im/element-android/blob/main/CHANGES.md
 
+Changes in Element v1.4.32 (2022-08-10)
+=======================================
+
+Imported from Element 1.4.32. (https://github.com/vector-im/element-android/releases/tag/v1.4.32)
+
+Changes in Element v1.4.30 (2022-07-29)
+=======================================
+
+SDK API changes ⚠️
+------------------
+- Communities/Groups are removed completely ([#5733](https://github.com/vector-im/element-android/issues/5733))
+- SDK - The SpaceFilter is query parameter is no longer nullable, use SpaceFilter.NoFilter instead ([#6666](https://github.com/vector-im/element-android/issues/6666))
+
 Changes in Matrix-SDK 1.4.27 (2022-07-20)
 ===================================================
 
diff --git a/dependencies.gradle b/dependencies.gradle
index db9278b9757695e11478cb61a1bbae54046c6e33..93a62a548ed30ba09328c73d4e43369e7997db11 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -13,15 +13,16 @@ ext.versions = [
 def gradle = "7.1.3"
 // Ref: https://kotlinlang.org/releases.html
 def kotlin = "1.6.21"
-def kotlinCoroutines = "1.6.3"
+def kotlinCoroutines = "1.6.4"
 def dagger = "2.42"
+def appDistribution = "16.0.0-beta03"
 def retrofit = "2.9.0"
 def arrow = "0.8.2"
 def markwon = "4.6.2"
 def moshi = "1.13.0"
-def lifecycle = "2.4.1"
+def lifecycle = "2.5.1"
 def flowBinding = "1.2.0"
-def flipper = "0.151.1"
+def flipper = "0.156.0"
 def epoxy = "4.6.2"
 def mavericks = "2.7.0"
 def glide = "4.13.2"
@@ -29,7 +30,7 @@ def bigImageViewer = "1.8.1"
 def jjwt = "0.11.5"
 def vanniktechEmoji = "0.15.0"
 
-def fragment = "1.4.1"
+def fragment = "1.5.1"
 
 // Testing
 def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819
@@ -49,9 +50,7 @@ ext.libs = [
                 'coroutinesTest'          : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines"
         ],
         androidx    : [
-                'annotation'              : "androidx.annotation:annotation:1.4.0",
-                'activity'                : "androidx.activity:activity:1.4.0",
-                'annotations'             : "androidx.annotation:annotation:1.3.0",
+                'activity'                : "androidx.activity:activity:1.5.1",
                 'appCompat'               : "androidx.appcompat:appcompat:1.4.2",
                 'biometric'               : "androidx.biometric:biometric:1.1.0",
                 'core'                    : "androidx.core:core-ktx:1.8.0",
@@ -83,7 +82,9 @@ ext.libs = [
                 'transition'              : "androidx.transition:transition:1.2.0",
         ],
         google      : [
-                'material'                : "com.google.android.material:material:1.6.1"
+                '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",
         ],
         dagger      : [
                 'dagger'                  : "com.google.dagger:dagger:$dagger",
@@ -96,6 +97,9 @@ ext.libs = [
                 'flipper'                 : "com.facebook.flipper:flipper:$flipper",
                 'flipperNetworkPlugin'    : "com.facebook.flipper:flipper-network-plugin:$flipper",
         ],
+        element     : [
+                'opusencoder'             : "io.element.android:opusencoder:1.0.4",
+        ],
         squareup    : [
                 'moshi'                  : "com.squareup.moshi:moshi:$moshi",
                 'moshiKt'                : "com.squareup.moshi:moshi-kotlin:$moshi",
diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle
index b785c7f50b71d941d22ebd9d8beabd29e73c1a07..d5972ed8461eff2b78f6deb8fbd20440b306bb6f 100644
--- a/dependencies_groups.gradle
+++ b/dependencies_groups.gradle
@@ -74,6 +74,7 @@ ext.groups = [
                         'com.github.javaparser',
                         'com.github.piasy',
                         'com.github.shyiko.klob',
+                        'com.github.rubensousa',
                         'com.google',
                         'com.google.android',
                         'com.google.api.grpc',
@@ -124,6 +125,8 @@ ext.groups = [
                         'commons-logging',
                         'info.picocli',
                         'io.arrow-kt',
+                        'io.element.android',
+                        'io.github.davidburstrom.contester',
                         'io.github.detekt.sarif4k',
                         'io.github.microutils',
                         'io.github.reactivecircus.flowbinding',
diff --git a/gradle.properties b/gradle.properties
index a5d0668154e016749568e6804dd059f1e31ba4bf..2dd5d3213fe9fda8191d3ffcc7885f6aee37e35a 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -26,7 +26,7 @@ vector.httpLogLevel=NONE
 # Ref: https://github.com/vanniktech/gradle-maven-publish-plugin
 GROUP=org.matrix.android
 POM_ARTIFACT_ID=matrix-android-sdk2
-VERSION_NAME=1.4.27
+VERSION_NAME=1.4.32
 
 POM_PACKAGING=aar
 
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index e1e0c8dc42bfac53afdfb5d12df6da8b4f96f93c..924f117d9a3ca0a3b524ec5a466d9744356ef40d 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionSha256Sum=e6d864e3b5bc05cc62041842b306383fc1fefcec359e70cebb1d470a6094ca82
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip
+distributionSha256Sum=db9c8211ed63f61f60292c69e80d89196f9eb36665e369e7f00ac4cc841c2219
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip
 zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
+zipStorePath=wrapper/dists
\ No newline at end of file
diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle
index a7cb1cad2fc016d84f240d069491f57bf544ef66..e57154f6a8ad006db3e7934f236cba66e850b73f 100644
--- a/matrix-sdk-android/build.gradle
+++ b/matrix-sdk-android/build.gradle
@@ -19,7 +19,7 @@ buildscript {
         }
     }
     dependencies {
-        classpath "io.realm:realm-gradle-plugin:10.9.0"
+        classpath "io.realm:realm-gradle-plugin:10.11.0"
     }
 }
 
@@ -202,7 +202,7 @@ dependencies {
     implementation libs.apache.commonsImaging
 
     // Phone number https://github.com/google/libphonenumber
-    implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.51'
+    implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.53'
 
     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/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
index 7dafe33935f0c84baec58743f05208b1d4d9266e..a78953caacc87a7cfa27534f64b9d104876ff597 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
@@ -18,12 +18,15 @@ package org.matrix.android.sdk.common
 
 import android.content.Context
 import android.net.Uri
+import android.util.Log
 import androidx.lifecycle.Observer
 import androidx.test.internal.runner.junit4.statement.UiThreadStatement
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
 import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
@@ -38,7 +41,10 @@ import org.matrix.android.sdk.api.auth.registration.RegistrationResult
 import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.getRoomSummary
 import org.matrix.android.sdk.api.session.room.Room
+import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure
+import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.api.session.room.model.message.MessageContent
 import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.api.session.room.timeline.Timeline
@@ -47,6 +53,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
 import org.matrix.android.sdk.api.session.sync.SyncState
 import timber.log.Timber
 import java.util.UUID
+import java.util.concurrent.CancellationException
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
 
@@ -54,7 +61,7 @@ import java.util.concurrent.TimeUnit
  * This class exposes methods to be used in common cases
  * Registration, login, Sync, Sending messages...
  */
-class CommonTestHelper private constructor(context: Context) {
+class CommonTestHelper internal constructor(context: Context) {
 
     companion object {
         internal fun runSessionTest(context: Context, autoSignoutOnClose: Boolean = true, block: (CommonTestHelper) -> Unit) {
@@ -241,6 +248,37 @@ class CommonTestHelper private constructor(context: Context) {
         return sentEvents
     }
 
+    fun waitForAndAcceptInviteInRoom(otherSession: Session, roomID: String) {
+        waitWithLatch { latch ->
+            retryPeriodicallyWithLatch(latch) {
+                val roomSummary = otherSession.getRoomSummary(roomID)
+                (roomSummary != null && roomSummary.membership == Membership.INVITE).also {
+                    if (it) {
+                        Log.v("# TEST", "${otherSession.myUserId} can see the invite")
+                    }
+                }
+            }
+        }
+
+        // not sure why it's taking so long :/
+        runBlockingTest(90_000) {
+            Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $roomID")
+            try {
+                otherSession.roomService().joinRoom(roomID)
+            } catch (ex: JoinRoomFailure.JoinedWithTimeout) {
+                // it's ok we will wait after
+            }
+        }
+
+        Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...")
+        waitWithLatch {
+            retryPeriodicallyWithLatch(it) {
+                val roomSummary = otherSession.getRoomSummary(roomID)
+                roomSummary != null && roomSummary.membership == Membership.JOIN
+            }
+        }
+    }
+
     /**
      * Reply in a thread
      * @param room the room where to send the messages
@@ -285,6 +323,8 @@ class CommonTestHelper private constructor(context: Context) {
         )
         assertNotNull(session)
         return session.also {
+            // most of the test was created pre-MSC3061 so ensure compatibility
+            it.cryptoService().enableShareKeyOnInvite(false)
             trackedSessions.add(session)
         }
     }
@@ -428,16 +468,26 @@ class CommonTestHelper private constructor(context: Context) {
      * @param latch
      * @throws InterruptedException
      */
-    fun await(latch: CountDownLatch, timeout: Long? = TestConstants.timeOutMillis) {
+    fun await(latch: CountDownLatch, timeout: Long? = TestConstants.timeOutMillis, job: Job? = null) {
         assertTrue(
                 "Timed out after " + timeout + "ms waiting for something to happen. See stacktrace for cause.",
-                latch.await(timeout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS)
+                latch.await(timeout ?: TestConstants.timeOutMillis, TimeUnit.MILLISECONDS).also {
+                    if (!it) {
+                        // cancel job on timeout
+                        job?.cancel("Await timeout")
+                    }
+                }
         )
     }
 
     suspend fun retryPeriodicallyWithLatch(latch: CountDownLatch, condition: (() -> Boolean)) {
         while (true) {
-            delay(1000)
+            try {
+                delay(1000)
+            } catch (ex: CancellationException) {
+                // the job was canceled, just stop
+                return
+            }
             if (condition()) {
                 latch.countDown()
                 return
@@ -447,10 +497,10 @@ class CommonTestHelper private constructor(context: Context) {
 
     fun waitWithLatch(timeout: Long? = TestConstants.timeOutMillis, dispatcher: CoroutineDispatcher = Dispatchers.Main, block: suspend (CountDownLatch) -> Unit) {
         val latch = CountDownLatch(1)
-        coroutineScope.launch(dispatcher) {
+        val job = coroutineScope.launch(dispatcher) {
             block(latch)
         }
-        await(latch, timeout)
+        await(latch, timeout, job)
     }
 
     fun <T> runBlockingTest(timeout: Long = TestConstants.timeOutMillis, block: suspend () -> T): T {
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt
index 5fd86d4fdb87ec58d9d6481ab97138d8cdf79dde..f36bfb6210e164e8e4c82316bbd4b70601ff3776 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt
@@ -53,6 +53,7 @@ import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.getRoom
 import org.matrix.android.sdk.api.session.room.Room
 import org.matrix.android.sdk.api.session.room.model.Membership
+import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
 import org.matrix.android.sdk.api.session.room.model.RoomSummary
 import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
 import org.matrix.android.sdk.api.session.room.model.message.MessageContent
@@ -76,11 +77,14 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
     /**
      * @return alice session
      */
-    fun doE2ETestWithAliceInARoom(encryptedRoom: Boolean = true): CryptoTestData {
+    fun doE2ETestWithAliceInARoom(encryptedRoom: Boolean = true, roomHistoryVisibility: RoomHistoryVisibility? = null): CryptoTestData {
         val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
 
         val roomId = testHelper.runBlockingTest {
-            aliceSession.roomService().createRoom(CreateRoomParams().apply { name = "MyRoom" })
+            aliceSession.roomService().createRoom(CreateRoomParams().apply {
+                historyVisibility = roomHistoryVisibility
+                name = "MyRoom"
+            })
         }
         if (encryptedRoom) {
             testHelper.waitWithLatch { latch ->
@@ -104,8 +108,8 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
     /**
      * @return alice and bob sessions
      */
-    fun doE2ETestWithAliceAndBobInARoom(encryptedRoom: Boolean = true): CryptoTestData {
-        val cryptoTestData = doE2ETestWithAliceInARoom(encryptedRoom)
+    fun doE2ETestWithAliceAndBobInARoom(encryptedRoom: Boolean = true, roomHistoryVisibility: RoomHistoryVisibility? = null): CryptoTestData {
+        val cryptoTestData = doE2ETestWithAliceInARoom(encryptedRoom, roomHistoryVisibility)
         val aliceSession = cryptoTestData.firstSession
         val aliceRoomId = cryptoTestData.roomId
 
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2EShareKeysConfigTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2EShareKeysConfigTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..32d63a1934241c23e99f0a6bdb7a54436923914b
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2EShareKeysConfigTest.kt
@@ -0,0 +1,298 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.crypto
+
+import android.util.Log
+import androidx.test.filters.LargeTest
+import org.amshove.kluent.internal.assertEquals
+import org.junit.Assert
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.junit.runners.MethodSorters
+import org.matrix.android.sdk.InstrumentedTest
+import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersion
+import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult
+import org.matrix.android.sdk.api.session.crypto.keysbackup.MegolmBackupCreationInfo
+import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
+import org.matrix.android.sdk.api.session.getRoom
+import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
+import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
+import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+import org.matrix.android.sdk.common.CommonTestHelper
+import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
+import org.matrix.android.sdk.common.CryptoTestData
+import org.matrix.android.sdk.common.SessionTestParams
+import org.matrix.android.sdk.common.TestConstants
+import org.matrix.android.sdk.common.TestMatrixCallback
+
+@RunWith(JUnit4::class)
+@FixMethodOrder(MethodSorters.JVM)
+@LargeTest
+class E2EShareKeysConfigTest : InstrumentedTest {
+
+    @Test
+    fun msc3061ShouldBeDisabledByDefault() = runCryptoTest(context()) { _, commonTestHelper ->
+        val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = false))
+        Assert.assertFalse("MSC3061 is lab and should be disabled by default", aliceSession.cryptoService().isShareKeysOnInviteEnabled())
+    }
+
+    @Test
+    fun ensureKeysAreNotSharedIfOptionDisabled() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
+        val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true))
+        aliceSession.cryptoService().enableShareKeyOnInvite(false)
+        val roomId = commonTestHelper.runBlockingTest {
+            aliceSession.roomService().createRoom(CreateRoomParams().apply {
+                historyVisibility = RoomHistoryVisibility.SHARED
+                name = "MyRoom"
+                enableEncryption()
+            })
+        }
+
+        commonTestHelper.waitWithLatch { latch ->
+            commonTestHelper.retryPeriodicallyWithLatch(latch) {
+                aliceSession.roomService().getRoomSummary(roomId)?.isEncrypted == true
+            }
+        }
+        val roomAlice = aliceSession.roomService().getRoom(roomId)!!
+
+        // send some messages
+        val withSession1 = commonTestHelper.sendTextMessage(roomAlice, "Hello", 1)
+        aliceSession.cryptoService().discardOutboundSession(roomId)
+        val withSession2 = commonTestHelper.sendTextMessage(roomAlice, "World", 1)
+
+        // Create bob account
+        val bobSession = commonTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams(withInitialSync = true))
+
+        // Let alice invite bob
+        commonTestHelper.runBlockingTest {
+            roomAlice.membershipService().invite(bobSession.myUserId)
+        }
+
+        commonTestHelper.waitForAndAcceptInviteInRoom(bobSession, roomId)
+
+        // Bob has join but should not be able to decrypt history
+        cryptoTestHelper.ensureCannotDecrypt(
+                withSession1.map { it.eventId } + withSession2.map { it.eventId },
+                bobSession,
+                roomId
+        )
+
+        // We don't need bob anymore
+        commonTestHelper.signOutAndClose(bobSession)
+
+        // Now let's enable history key sharing on alice side
+        aliceSession.cryptoService().enableShareKeyOnInvite(true)
+
+        // let's add a new message first
+        val afterFlagOn = commonTestHelper.sendTextMessage(roomAlice, "After", 1)
+
+        // Worth nothing to check that the session was rotated
+        Assert.assertNotEquals(
+                "Session should have been rotated",
+                withSession2.first().root.content?.get("session_id")!!,
+                afterFlagOn.first().root.content?.get("session_id")!!
+        )
+
+        // Invite a new user
+        val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true))
+
+        // Let alice invite sam
+        commonTestHelper.runBlockingTest {
+            roomAlice.membershipService().invite(samSession.myUserId)
+        }
+
+        commonTestHelper.waitForAndAcceptInviteInRoom(samSession, roomId)
+
+        // Sam shouldn't be able to decrypt messages with the first session, but should decrypt the one with 3rd session
+        cryptoTestHelper.ensureCannotDecrypt(
+                withSession1.map { it.eventId } + withSession2.map { it.eventId },
+                samSession,
+                roomId
+        )
+
+        cryptoTestHelper.ensureCanDecrypt(
+                afterFlagOn.map { it.eventId },
+                samSession,
+                roomId,
+                afterFlagOn.map { it.root.getClearContent()?.get("body") as String })
+    }
+
+    @Test
+    fun ifSharingDisabledOnAliceSideBobShouldNotShareAliceHistoty() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
+        val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(roomHistoryVisibility = RoomHistoryVisibility.SHARED)
+        val aliceSession = testData.firstSession.also {
+            it.cryptoService().enableShareKeyOnInvite(false)
+        }
+        val bobSession = testData.secondSession!!.also {
+            it.cryptoService().enableShareKeyOnInvite(true)
+        }
+
+        val (fromAliceNotSharable, fromBobSharable, samSession) = commonAliceAndBobSendMessages(commonTestHelper, aliceSession, testData, bobSession)
+
+        // Bob should have shared history keys to sam.
+        // But has alice hasn't enabled sharing, bob shouldn't send her sessions
+        cryptoTestHelper.ensureCannotDecrypt(
+                fromAliceNotSharable.map { it.eventId },
+                samSession,
+                testData.roomId
+        )
+
+        cryptoTestHelper.ensureCanDecrypt(
+                fromBobSharable.map { it.eventId },
+                samSession,
+                testData.roomId,
+                fromBobSharable.map { it.root.getClearContent()?.get("body") as String })
+    }
+
+    @Test
+    fun ifSharingEnabledOnAliceSideBobShouldShareAliceHistoty() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
+        val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(roomHistoryVisibility = RoomHistoryVisibility.SHARED)
+        val aliceSession = testData.firstSession.also {
+            it.cryptoService().enableShareKeyOnInvite(true)
+        }
+        val bobSession = testData.secondSession!!.also {
+            it.cryptoService().enableShareKeyOnInvite(true)
+        }
+
+        val (fromAliceNotSharable, fromBobSharable, samSession) = commonAliceAndBobSendMessages(commonTestHelper, aliceSession, testData, bobSession)
+
+        cryptoTestHelper.ensureCanDecrypt(
+                fromAliceNotSharable.map { it.eventId },
+                samSession,
+                testData.roomId,
+                fromAliceNotSharable.map { it.root.getClearContent()?.get("body") as String })
+
+        cryptoTestHelper.ensureCanDecrypt(
+                fromBobSharable.map { it.eventId },
+                samSession,
+                testData.roomId,
+                fromBobSharable.map { it.root.getClearContent()?.get("body") as String })
+    }
+
+    private fun commonAliceAndBobSendMessages(commonTestHelper: CommonTestHelper, aliceSession: Session, testData: CryptoTestData, bobSession: Session): Triple<List<TimelineEvent>, List<TimelineEvent>, Session> {
+        val fromAliceNotSharable = commonTestHelper.sendTextMessage(aliceSession.getRoom(testData.roomId)!!, "Hello from alice", 1)
+        val fromBobSharable = commonTestHelper.sendTextMessage(bobSession.getRoom(testData.roomId)!!, "Hello from bob", 1)
+
+        // Now let bob invite Sam
+        // Invite a new user
+        val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true))
+
+        // Let bob invite sam
+        commonTestHelper.runBlockingTest {
+            bobSession.getRoom(testData.roomId)!!.membershipService().invite(samSession.myUserId)
+        }
+
+        commonTestHelper.waitForAndAcceptInviteInRoom(samSession, testData.roomId)
+        return Triple(fromAliceNotSharable, fromBobSharable, samSession)
+    }
+
+    // test flag on backup is correct
+
+    @Test
+    fun testBackupFlagIsCorrect() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
+        val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true))
+        aliceSession.cryptoService().enableShareKeyOnInvite(false)
+        val roomId = commonTestHelper.runBlockingTest {
+            aliceSession.roomService().createRoom(CreateRoomParams().apply {
+                historyVisibility = RoomHistoryVisibility.SHARED
+                name = "MyRoom"
+                enableEncryption()
+            })
+        }
+
+        commonTestHelper.waitWithLatch { latch ->
+            commonTestHelper.retryPeriodicallyWithLatch(latch) {
+                aliceSession.roomService().getRoomSummary(roomId)?.isEncrypted == true
+            }
+        }
+        val roomAlice = aliceSession.roomService().getRoom(roomId)!!
+
+        // send some messages
+        val notSharableMessage = commonTestHelper.sendTextMessage(roomAlice, "Hello", 1)
+        aliceSession.cryptoService().enableShareKeyOnInvite(true)
+        val sharableMessage = commonTestHelper.sendTextMessage(roomAlice, "World", 1)
+
+        Log.v("#E2E TEST", "Create and start key backup for bob ...")
+        val keysBackupService = aliceSession.cryptoService().keysBackupService()
+        val keyBackupPassword = "FooBarBaz"
+        val megolmBackupCreationInfo = commonTestHelper.doSync<MegolmBackupCreationInfo> {
+            keysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it)
+        }
+        val version = commonTestHelper.doSync<KeysVersion> {
+            keysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it)
+        }
+
+        commonTestHelper.waitWithLatch { latch ->
+            keysBackupService.backupAllGroupSessions(
+                    null,
+                    TestMatrixCallback(latch, true)
+            )
+        }
+
+        // signout
+        commonTestHelper.signOutAndClose(aliceSession)
+
+        val newAliceSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true))
+        newAliceSession.cryptoService().enableShareKeyOnInvite(true)
+
+        newAliceSession.cryptoService().keysBackupService().let { kbs ->
+            val keyVersionResult = commonTestHelper.doSync<KeysVersionResult?> {
+                kbs.getVersion(version.version, it)
+            }
+
+            val importedResult = commonTestHelper.doSync<ImportRoomKeysResult> {
+                kbs.restoreKeyBackupWithPassword(
+                        keyVersionResult!!,
+                        keyBackupPassword,
+                        null,
+                        null,
+                        null,
+                        it
+                )
+            }
+
+            assertEquals(2, importedResult.totalNumberOfKeys)
+        }
+
+        // Now let's invite sam
+        // Invite a new user
+        val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true))
+
+        // Let alice invite sam
+        commonTestHelper.runBlockingTest {
+            newAliceSession.getRoom(roomId)!!.membershipService().invite(samSession.myUserId)
+        }
+
+        commonTestHelper.waitForAndAcceptInviteInRoom(samSession, roomId)
+
+        // Sam shouldn't be able to decrypt messages with the first session, but should decrypt the one with 3rd session
+        cryptoTestHelper.ensureCannotDecrypt(
+                notSharableMessage.map { it.eventId },
+                samSession,
+                roomId
+        )
+
+        cryptoTestHelper.ensureCanDecrypt(
+                sharableMessage.map { it.eventId },
+                samSession,
+                roomId,
+                sharableMessage.map { it.root.getClearContent()?.get("body") as String })
+    }
+}
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 5a61eee7fe30c238ddfd410e318c9a901999a980..251c13ccbf4a2dd4142cb4599518c31940bed670 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
@@ -23,7 +23,6 @@ import org.amshove.kluent.fail
 import org.amshove.kluent.internal.assertEquals
 import org.junit.Assert
 import org.junit.FixMethodOrder
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -49,9 +48,7 @@ import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventCon
 import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.getRoom
-import org.matrix.android.sdk.api.session.getRoomSummary
 import org.matrix.android.sdk.api.session.room.Room
-import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure
 import org.matrix.android.sdk.api.session.room.getTimelineEvent
 import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.api.session.room.model.message.MessageContent
@@ -67,10 +64,10 @@ import org.matrix.android.sdk.common.TestMatrixCallback
 import org.matrix.android.sdk.mustFail
 import java.util.concurrent.CountDownLatch
 
+// @Ignore("This test fails with an unhandled exception thrown from a coroutine which terminates the entire test run.")
 @RunWith(JUnit4::class)
 @FixMethodOrder(MethodSorters.JVM)
 @LargeTest
-@Ignore("This test fails with an unhandled exception thrown from a coroutine which terminates the entire test run.")
 class E2eeSanityTests : InstrumentedTest {
 
     @get:Rule val rule = RetryTestRule(3)
@@ -115,7 +112,7 @@ class E2eeSanityTests : InstrumentedTest {
 
         // All user should accept invite
         otherAccounts.forEach { otherSession ->
-            waitForAndAcceptInviteInRoom(testHelper, otherSession, e2eRoomID)
+            testHelper.waitForAndAcceptInviteInRoom(otherSession, e2eRoomID)
             Log.v("#E2E TEST", "${otherSession.myUserId} joined room $e2eRoomID")
         }
 
@@ -156,7 +153,7 @@ class E2eeSanityTests : InstrumentedTest {
         }
 
         newAccount.forEach {
-            waitForAndAcceptInviteInRoom(testHelper, it, e2eRoomID)
+            testHelper.waitForAndAcceptInviteInRoom(it, e2eRoomID)
         }
 
         ensureMembersHaveJoined(testHelper, aliceSession, newAccount, e2eRoomID)
@@ -740,37 +737,6 @@ class E2eeSanityTests : InstrumentedTest {
         }
     }
 
-    private fun waitForAndAcceptInviteInRoom(testHelper: CommonTestHelper, otherSession: Session, e2eRoomID: String) {
-        testHelper.waitWithLatch { latch ->
-            testHelper.retryPeriodicallyWithLatch(latch) {
-                val roomSummary = otherSession.getRoomSummary(e2eRoomID)
-                (roomSummary != null && roomSummary.membership == Membership.INVITE).also {
-                    if (it) {
-                        Log.v("#E2E TEST", "${otherSession.myUserId} can see the invite from alice")
-                    }
-                }
-            }
-        }
-
-        // not sure why it's taking so long :/
-        testHelper.runBlockingTest(90_000) {
-            Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID")
-            try {
-                otherSession.roomService().joinRoom(e2eRoomID)
-            } catch (ex: JoinRoomFailure.JoinedWithTimeout) {
-                // it's ok we will wait after
-            }
-        }
-
-        Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...")
-        testHelper.waitWithLatch {
-            testHelper.retryPeriodicallyWithLatch(it) {
-                val roomSummary = otherSession.getRoomSummary(e2eRoomID)
-                roomSummary != null && roomSummary.membership == Membership.JOIN
-            }
-        }
-    }
-
     private fun ensureIsDecrypted(testHelper: CommonTestHelper, sentEventIds: List<String>, session: Session, e2eRoomID: String) {
         testHelper.waitWithLatch { latch ->
             sentEventIds.forEach { sentEventId ->
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..32a95008b1ac5017ff1790dc6442f09041795e66
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt
@@ -0,0 +1,424 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.crypto
+
+import android.util.Log
+import androidx.test.filters.LargeTest
+import org.amshove.kluent.internal.assertEquals
+import org.amshove.kluent.internal.assertNotEquals
+import org.junit.Assert
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.junit.runners.MethodSorters
+import org.matrix.android.sdk.InstrumentedTest
+import org.matrix.android.sdk.api.query.QueryStringValue
+import org.matrix.android.sdk.api.session.Session
+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.getRoom
+import org.matrix.android.sdk.api.session.room.Room
+import org.matrix.android.sdk.api.session.room.failure.JoinRoomFailure
+import org.matrix.android.sdk.api.session.room.model.Membership
+import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
+import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent
+import org.matrix.android.sdk.api.session.room.model.shouldShareHistory
+import org.matrix.android.sdk.common.CommonTestHelper
+import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
+import org.matrix.android.sdk.common.CryptoTestHelper
+import org.matrix.android.sdk.common.SessionTestParams
+
+@RunWith(JUnit4::class)
+@FixMethodOrder(MethodSorters.JVM)
+@LargeTest
+class E2eeShareKeysHistoryTest : InstrumentedTest {
+
+    @Test
+    fun testShareMessagesHistoryWithRoomWorldReadable() {
+        testShareHistoryWithRoomVisibility(RoomHistoryVisibility.WORLD_READABLE)
+    }
+
+    @Test
+    fun testShareMessagesHistoryWithRoomShared() {
+        testShareHistoryWithRoomVisibility(RoomHistoryVisibility.SHARED)
+    }
+
+    @Test
+    fun testShareMessagesHistoryWithRoomJoined() {
+        testShareHistoryWithRoomVisibility(RoomHistoryVisibility.JOINED)
+    }
+
+    @Test
+    fun testShareMessagesHistoryWithRoomInvited() {
+        testShareHistoryWithRoomVisibility(RoomHistoryVisibility.INVITED)
+    }
+
+    /**
+     * In this test we create a room and test that new members
+     * can decrypt history when the room visibility is
+     * RoomHistoryVisibility.SHARED or RoomHistoryVisibility.WORLD_READABLE.
+     * We should not be able to view messages/decrypt otherwise
+     */
+    private fun testShareHistoryWithRoomVisibility(roomHistoryVisibility: RoomHistoryVisibility? = null) =
+            runCryptoTest(context()) { cryptoTestHelper, testHelper ->
+                val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true, roomHistoryVisibility)
+
+                val e2eRoomID = cryptoTestData.roomId
+
+                // Alice
+                val aliceSession = cryptoTestData.firstSession.also {
+                    it.cryptoService().enableShareKeyOnInvite(true)
+                }
+                val aliceRoomPOV = aliceSession.roomService().getRoom(e2eRoomID)!!
+
+                // Bob
+                val bobSession = cryptoTestData.secondSession!!.also {
+                    it.cryptoService().enableShareKeyOnInvite(true)
+                }
+                val bobRoomPOV = bobSession.roomService().getRoom(e2eRoomID)!!
+
+                assertEquals(bobRoomPOV.roomSummary()?.joinedMembersCount, 2)
+                Log.v("#E2E TEST", "Alice and Bob are in roomId: $e2eRoomID")
+
+                val aliceMessageId: String? = sendMessageInRoom(aliceRoomPOV, "Hello Bob, I am Alice!", testHelper)
+                Assert.assertTrue("Message should be sent", aliceMessageId != null)
+                Log.v("#E2E TEST", "Alice sent message to roomId: $e2eRoomID")
+
+                // Bob should be able to decrypt the message
+                testHelper.waitWithLatch { latch ->
+                    testHelper.retryPeriodicallyWithLatch(latch) {
+                        val timelineEvent = bobSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!)
+                        (timelineEvent != null &&
+                                timelineEvent.isEncrypted() &&
+                                timelineEvent.root.getClearType() == EventType.MESSAGE).also {
+                            if (it) {
+                                Log.v("#E2E TEST", "Bob can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}")
+                            }
+                        }
+                    }
+                }
+
+                // Create a new user
+                val arisSession = testHelper.createAccount("aris", SessionTestParams(true)).also {
+                    it.cryptoService().enableShareKeyOnInvite(true)
+                }
+                Log.v("#E2E TEST", "Aris user created")
+
+                // Alice invites new user to the room
+                testHelper.runBlockingTest {
+                    Log.v("#E2E TEST", "Alice invites ${arisSession.myUserId}")
+                    aliceRoomPOV.membershipService().invite(arisSession.myUserId)
+                }
+
+                waitForAndAcceptInviteInRoom(arisSession, e2eRoomID, testHelper)
+
+                ensureMembersHaveJoined(aliceSession, arrayListOf(arisSession), e2eRoomID, testHelper)
+                Log.v("#E2E TEST", "Aris has joined roomId: $e2eRoomID")
+
+                when (roomHistoryVisibility) {
+                    RoomHistoryVisibility.WORLD_READABLE,
+                    RoomHistoryVisibility.SHARED,
+                    null
+                    -> {
+                        // Aris should be able to decrypt the message
+                        testHelper.waitWithLatch { latch ->
+                            testHelper.retryPeriodicallyWithLatch(latch) {
+                                val timelineEvent = arisSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!)
+                                (timelineEvent != null &&
+                                        timelineEvent.isEncrypted() &&
+                                        timelineEvent.root.getClearType() == EventType.MESSAGE
+                                        ).also {
+                                            if (it) {
+                                                Log.v("#E2E TEST", "Aris can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}")
+                                            }
+                                        }
+                            }
+                        }
+                    }
+                    RoomHistoryVisibility.INVITED,
+                    RoomHistoryVisibility.JOINED -> {
+                        // Aris should not even be able to get the message
+                        testHelper.waitWithLatch { latch ->
+                            testHelper.retryPeriodicallyWithLatch(latch) {
+                                val timelineEvent = arisSession.roomService().getRoom(e2eRoomID)
+                                        ?.timelineService()
+                                        ?.getTimelineEvent(aliceMessageId!!)
+                                timelineEvent == null
+                            }
+                        }
+                    }
+                }
+
+                testHelper.signOutAndClose(arisSession)
+                cryptoTestData.cleanUp(testHelper)
+            }
+
+    @Test
+    fun testNeedsRotationFromWorldReadableToShared() {
+        testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("shared"))
+    }
+
+    @Test
+    fun testNeedsRotationFromWorldReadableToInvited() {
+        testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("invited"))
+    }
+
+    @Test
+    fun testNeedsRotationFromWorldReadableToJoined() {
+        testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("joined"))
+    }
+
+    @Test
+    fun testNeedsRotationFromSharedToWorldReadable() {
+        testRotationDueToVisibilityChange(RoomHistoryVisibility.SHARED, RoomHistoryVisibilityContent("world_readable"))
+    }
+
+    @Test
+    fun testNeedsRotationFromSharedToInvited() {
+        testRotationDueToVisibilityChange(RoomHistoryVisibility.SHARED, RoomHistoryVisibilityContent("invited"))
+    }
+
+    @Test
+    fun testNeedsRotationFromSharedToJoined() {
+        testRotationDueToVisibilityChange(RoomHistoryVisibility.SHARED, RoomHistoryVisibilityContent("joined"))
+    }
+
+    @Test
+    fun testNeedsRotationFromInvitedToShared() {
+        testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("shared"))
+    }
+
+    @Test
+    fun testNeedsRotationFromInvitedToWorldReadable() {
+        testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("world_readable"))
+    }
+
+    @Test
+    fun testNeedsRotationFromInvitedToJoined() {
+        testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("joined"))
+    }
+
+    @Test
+    fun testNeedsRotationFromJoinedToShared() {
+        testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("shared"))
+    }
+
+    @Test
+    fun testNeedsRotationFromJoinedToInvited() {
+        testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("invited"))
+    }
+
+    @Test
+    fun testNeedsRotationFromJoinedToWorldReadable() {
+        testRotationDueToVisibilityChange(RoomHistoryVisibility.WORLD_READABLE, RoomHistoryVisibilityContent("world_readable"))
+    }
+
+    /**
+     * In this test we will test that a rotation is needed when
+     * When the room's history visibility setting changes to world_readable or shared
+     * from invited or joined, or changes to invited or joined from world_readable or shared,
+     * senders that support this flag must rotate their megolm sessions.
+     */
+    private fun testRotationDueToVisibilityChange(
+            initRoomHistoryVisibility: RoomHistoryVisibility,
+            nextRoomHistoryVisibility: RoomHistoryVisibilityContent
+    ) {
+        val testHelper = CommonTestHelper(context())
+        val cryptoTestHelper = CryptoTestHelper(testHelper)
+
+        val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true, initRoomHistoryVisibility)
+        val e2eRoomID = cryptoTestData.roomId
+
+        // Alice
+        val aliceSession = cryptoTestData.firstSession.also {
+            it.cryptoService().enableShareKeyOnInvite(true)
+        }
+        val aliceRoomPOV = aliceSession.roomService().getRoom(e2eRoomID)!!
+//        val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting
+
+        // Bob
+        val bobSession = cryptoTestData.secondSession!!
+
+        val bobRoomPOV = bobSession.roomService().getRoom(e2eRoomID)!!
+
+        assertEquals(bobRoomPOV.roomSummary()?.joinedMembersCount, 2)
+        Log.v("#E2E TEST ROTATION", "Alice and Bob are in roomId: $e2eRoomID")
+
+        val aliceMessageId: String? = sendMessageInRoom(aliceRoomPOV, "Hello Bob, I am Alice!", testHelper)
+        Assert.assertTrue("Message should be sent", aliceMessageId != null)
+        Log.v("#E2E TEST ROTATION", "Alice sent message to roomId: $e2eRoomID")
+
+        // Bob should be able to decrypt the message
+        var firstAliceMessageMegolmSessionId: String? = null
+        val bobRoomPov = bobSession.roomService().getRoom(e2eRoomID)
+        testHelper.waitWithLatch { latch ->
+            testHelper.retryPeriodicallyWithLatch(latch) {
+                val timelineEvent = bobRoomPov
+                        ?.timelineService()
+                        ?.getTimelineEvent(aliceMessageId!!)
+                (timelineEvent != null &&
+                        timelineEvent.isEncrypted() &&
+                        timelineEvent.root.getClearType() == EventType.MESSAGE).also {
+                    if (it) {
+                        firstAliceMessageMegolmSessionId = timelineEvent?.root?.content?.get("session_id") as? String
+                        Log.v(
+                                "#E2E TEST",
+                                "Bob can decrypt the message (sid:$firstAliceMessageMegolmSessionId): ${timelineEvent?.root?.getDecryptedTextSummary()}"
+                        )
+                    }
+                }
+            }
+        }
+
+        Assert.assertNotNull("megolm session id can't be null", firstAliceMessageMegolmSessionId)
+
+        var secondAliceMessageSessionId: String? = null
+        sendMessageInRoom(aliceRoomPOV, "Other msg", testHelper)?.let { secondMessage ->
+            testHelper.waitWithLatch { latch ->
+                testHelper.retryPeriodicallyWithLatch(latch) {
+                    val timelineEvent = bobRoomPov
+                            ?.timelineService()
+                            ?.getTimelineEvent(secondMessage)
+                    (timelineEvent != null &&
+                            timelineEvent.isEncrypted() &&
+                            timelineEvent.root.getClearType() == EventType.MESSAGE).also {
+                        if (it) {
+                            secondAliceMessageSessionId = timelineEvent?.root?.content?.get("session_id") as? String
+                            Log.v(
+                                    "#E2E TEST",
+                                    "Bob can decrypt the message (sid:$secondAliceMessageSessionId): ${timelineEvent?.root?.getDecryptedTextSummary()}"
+                            )
+                        }
+                    }
+                }
+            }
+        }
+        assertEquals("No rotation needed session should be the same", firstAliceMessageMegolmSessionId, secondAliceMessageSessionId)
+        Log.v("#E2E TEST ROTATION", "No rotation needed yet")
+
+        // Let's change the room history visibility
+        testHelper.runBlockingTest {
+            aliceRoomPOV.stateService()
+                    .sendStateEvent(
+                            eventType = EventType.STATE_ROOM_HISTORY_VISIBILITY,
+                            stateKey = "",
+                            body = RoomHistoryVisibilityContent(
+                                    historyVisibilityStr = nextRoomHistoryVisibility.historyVisibilityStr
+                            ).toContent()
+                    )
+        }
+
+        // ensure that the state did synced down
+        testHelper.waitWithLatch { latch ->
+            testHelper.retryPeriodicallyWithLatch(latch) {
+                aliceRoomPOV.stateService().getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty)?.content
+                        ?.toModel<RoomHistoryVisibilityContent>()?.historyVisibility == nextRoomHistoryVisibility.historyVisibility
+            }
+        }
+
+        testHelper.waitWithLatch { latch ->
+            testHelper.retryPeriodicallyWithLatch(latch) {
+                val roomVisibility = aliceSession.getRoom(e2eRoomID)!!
+                        .stateService()
+                        .getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty)
+                        ?.content
+                        ?.toModel<RoomHistoryVisibilityContent>()
+                Log.v("#E2E TEST ROTATION", "Room visibility changed from: ${initRoomHistoryVisibility.name} to: ${roomVisibility?.historyVisibility?.name}")
+                roomVisibility?.historyVisibility == nextRoomHistoryVisibility.historyVisibility
+            }
+        }
+
+        var aliceThirdMessageSessionId: String? = null
+        sendMessageInRoom(aliceRoomPOV, "Message after visibility change", testHelper)?.let { thirdMessage ->
+            testHelper.waitWithLatch { latch ->
+                testHelper.retryPeriodicallyWithLatch(latch) {
+                    val timelineEvent = bobRoomPov
+                            ?.timelineService()
+                            ?.getTimelineEvent(thirdMessage)
+                    (timelineEvent != null &&
+                            timelineEvent.isEncrypted() &&
+                            timelineEvent.root.getClearType() == EventType.MESSAGE).also {
+                        if (it) {
+                            aliceThirdMessageSessionId = timelineEvent?.root?.content?.get("session_id") as? String
+                        }
+                    }
+                }
+            }
+        }
+
+        when {
+            initRoomHistoryVisibility.shouldShareHistory() == nextRoomHistoryVisibility.historyVisibility?.shouldShareHistory() -> {
+                assertEquals("Session shouldn't have been rotated", secondAliceMessageSessionId, aliceThirdMessageSessionId)
+                Log.v("#E2E TEST ROTATION", "Rotation is not needed")
+            }
+            initRoomHistoryVisibility.shouldShareHistory() != nextRoomHistoryVisibility.historyVisibility!!.shouldShareHistory() -> {
+                assertNotEquals("Session should have been rotated", secondAliceMessageSessionId, aliceThirdMessageSessionId)
+                Log.v("#E2E TEST ROTATION", "Rotation is needed!")
+            }
+        }
+
+        cryptoTestData.cleanUp(testHelper)
+    }
+
+    private fun sendMessageInRoom(aliceRoomPOV: Room, text: String, testHelper: CommonTestHelper): String? {
+        return testHelper.sendTextMessage(aliceRoomPOV, text, 1).firstOrNull()?.eventId
+    }
+
+    private fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String, testHelper: CommonTestHelper) {
+        testHelper.waitWithLatch { latch ->
+            testHelper.retryPeriodicallyWithLatch(latch) {
+                otherAccounts.map {
+                    aliceSession.roomService().getRoomMember(it.myUserId, e2eRoomID)?.membership
+                }.all {
+                    it == Membership.JOIN
+                }
+            }
+        }
+    }
+
+    private fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String, testHelper: CommonTestHelper) {
+        testHelper.waitWithLatch { latch ->
+            testHelper.retryPeriodicallyWithLatch(latch) {
+                val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID)
+                (roomSummary != null && roomSummary.membership == Membership.INVITE).also {
+                    if (it) {
+                        Log.v("#E2E TEST", "${otherSession.myUserId} can see the invite from alice")
+                    }
+                }
+            }
+        }
+
+        testHelper.runBlockingTest(60_000) {
+            Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID")
+            try {
+                otherSession.roomService().joinRoom(e2eRoomID)
+            } catch (ex: JoinRoomFailure.JoinedWithTimeout) {
+                // it's ok we will wait after
+            }
+        }
+
+        Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...")
+        testHelper.waitWithLatch {
+            testHelper.retryPeriodicallyWithLatch(it) {
+                val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID)
+                roomSummary != null && roomSummary.membership == Membership.JOIN
+            }
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt
index e37ae5be86ffb8a2354a17aeb78244093c8f1d29..e8e7b1d708444b9589884837a559a8e0ca442e9f 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt
@@ -72,7 +72,7 @@ class PreShareKeysTest : InstrumentedTest {
         assertNotNull("Bob should have received and decrypted a room key event from alice", bobInboundForAlice)
         assertEquals("Wrong room", e2eRoomID, bobInboundForAlice!!.roomId)
 
-        val megolmSessionId = bobInboundForAlice.olmInboundGroupSession!!.sessionIdentifier()
+        val megolmSessionId = bobInboundForAlice.session.sessionIdentifier()
 
         assertEquals("Wrong session", aliceOutboundSessionInRoom, megolmSessionId)
 
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt
index e8a474a54a5d9cd60a9eef8e2be89194571768d1..5fe737618411831e27d93b4baa6c465b4b11a8d6 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt
@@ -21,7 +21,6 @@ import org.amshove.kluent.shouldBe
 import org.junit.Assert
 import org.junit.Before
 import org.junit.FixMethodOrder
-import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -60,7 +59,6 @@ import kotlin.coroutines.resume
  */
 @RunWith(AndroidJUnit4::class)
 @FixMethodOrder(MethodSorters.JVM)
-@Ignore
 class UnwedgingTest : InstrumentedTest {
 
     private lateinit var messagesReceivedByBob: List<TimelineEvent>
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt
index ae420a09b38e59414d3cc90dd223a62369cafc22..0aac4297e4265114ac8b405218e7fcf424b444f1 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt
@@ -21,7 +21,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import org.junit.Assert
 import org.junit.FixMethodOrder
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -47,7 +46,6 @@ import org.matrix.android.sdk.mustFail
 @RunWith(AndroidJUnit4::class)
 @FixMethodOrder(MethodSorters.JVM)
 @LargeTest
-@Ignore
 class WithHeldTests : InstrumentedTest {
 
     @get:Rule val rule = RetryTestRule(3)
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt
index 45fdb9e1e30f07b4e5b512243b850ede447d284a..cf201611a0c0aa02d591a99d72fab3c13d273a92 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt
@@ -19,14 +19,14 @@ package org.matrix.android.sdk.internal.crypto.keysbackup
 import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.common.CommonTestHelper
 import org.matrix.android.sdk.common.CryptoTestData
-import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
+import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper
 
 /**
  * Data class to store result of [KeysBackupTestHelper.createKeysBackupScenarioWithPassword]
  */
 internal data class KeysBackupScenarioData(
         val cryptoTestData: CryptoTestData,
-        val aliceKeys: List<OlmInboundGroupSessionWrapper2>,
+        val aliceKeys: List<MXInboundMegolmSessionWrapper>,
         val prepareKeysBackupDataResult: PrepareKeysBackupDataResult,
         val aliceSession2: Session
 ) {
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt
index fb498e0de5dfde0e47a872738d7ea5fe32b7b111..2439119f01c8eb1ddcd0abd8cc91d103cf2903fe 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt
@@ -24,7 +24,6 @@ import org.junit.Assert.assertNotNull
 import org.junit.Assert.assertNull
 import org.junit.Assert.assertTrue
 import org.junit.FixMethodOrder
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -56,7 +55,6 @@ import java.util.concurrent.CountDownLatch
 @RunWith(AndroidJUnit4::class)
 @FixMethodOrder(MethodSorters.JVM)
 @LargeTest
-@Ignore
 class KeysBackupTest : InstrumentedTest {
 
     @get:Rule val rule = RetryTestRule(3)
@@ -301,7 +299,7 @@ class KeysBackupTest : InstrumentedTest {
         val keyBackupCreationInfo = keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup).megolmBackupCreationInfo
 
         // - Check encryptGroupSession() returns stg
-        val keyBackupData = keysBackup.encryptGroupSession(session)
+        val keyBackupData = testHelper.runBlockingTest { keysBackup.encryptGroupSession(session) }
         assertNotNull(keyBackupData)
         assertNotNull(keyBackupData!!.sessionData)
 
@@ -312,7 +310,7 @@ class KeysBackupTest : InstrumentedTest {
         val sessionData = keysBackup
                 .decryptKeyBackupData(
                         keyBackupData,
-                        session.olmInboundGroupSession!!.sessionIdentifier(),
+                        session.safeSessionId!!,
                         cryptoTestData.roomId,
                         decryption!!
                 )
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt
index 38f94c51039cb9a57c5af236c6f56233259b187a..2cc2b506b925322552f247fbb6f002c0e889fbbd 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt
@@ -187,7 +187,7 @@ internal class KeysBackupTestHelper(
         // - Alice must have the same keys on both devices
         for (aliceKey1 in testData.aliceKeys) {
             val aliceKey2 = (testData.aliceSession2.cryptoService().keysBackupService() as DefaultKeysBackupService).store
-                    .getInboundGroupSession(aliceKey1.olmInboundGroupSession!!.sessionIdentifier(), aliceKey1.senderKey!!)
+                    .getInboundGroupSession(aliceKey1.safeSessionId!!, aliceKey1.senderKey!!)
             Assert.assertNotNull(aliceKey2)
             assertKeysEquals(aliceKey1.exportKeys(), aliceKey2!!.exportKeys())
         }
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt
index 3dbf206e089c12815ecf89dd0ce9c8ebab86e306..2e5c69b04874855432b3fb6f79f8c45cc12c5088 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineForwardPaginationTest.kt
@@ -22,6 +22,7 @@ import org.amshove.kluent.internal.assertEquals
 import org.amshove.kluent.shouldBeFalse
 import org.amshove.kluent.shouldBeTrue
 import org.junit.FixMethodOrder
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
@@ -52,6 +53,7 @@ class TimelineForwardPaginationTest : InstrumentedTest {
      * This test ensure that if we click to permalink, we will be able to go back to the live
      */
     @Test
+    @Ignore("Ignoring this test until it's fixed since it blocks the CI.")
     fun forwardPaginationTest() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
         val numberOfMessagesToSend = 90
         val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false)
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt
index 38136ff5cee79fd5576e8063884218fca948c4ce..2cd579df24dfec8056199cf1c48cc539656b6d3d 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt
@@ -56,19 +56,17 @@ class SpaceCreationTest : InstrumentedTest {
         val roomName = "My Space"
         val topic = "A public space for test"
         var spaceId: String = ""
-        commonTestHelper.waitWithLatch {
+        commonTestHelper.runBlockingTest {
             spaceId = session.spaceService().createSpace(roomName, topic, null, true)
-            // wait a bit to let the summary update it self :/
-            it.countDown()
         }
-        Thread.sleep(4_000)
 
-        val syncedSpace = session.spaceService().getSpace(spaceId)
         commonTestHelper.waitWithLatch {
             commonTestHelper.retryPeriodicallyWithLatch(it) {
-                syncedSpace?.asRoom()?.roomSummary()?.name != null
+                session.spaceService().getSpace(spaceId)?.asRoom()?.roomSummary()?.name != null
             }
         }
+
+        val syncedSpace = session.spaceService().getSpace(spaceId)
         assertEquals("Room name should be set", roomName, syncedSpace?.asRoom()?.roomSummary()?.name)
         assertEquals("Room topic should be set", topic, syncedSpace?.asRoom()?.roomSummary()?.topic)
         // assertEquals(topic, syncedSpace.asRoom().roomSummary()?., "Room topic should be set")
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt
index 63ca963479f2c0edb135aac507a861d700d90bb4..18645fd6d9ce82cf5323a03a56db9d17140a56f2 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt
@@ -20,7 +20,6 @@ import android.util.Log
 import androidx.lifecycle.Observer
 import kotlinx.coroutines.runBlocking
 import org.junit.Assert.assertEquals
-import org.junit.Assert.assertNotNull
 import org.junit.Assert.assertTrue
 import org.junit.FixMethodOrder
 import org.junit.Ignore
@@ -62,47 +61,40 @@ class SpaceHierarchyTest : InstrumentedTest {
         val spaceName = "My Space"
         val topic = "A public space for test"
         var spaceId = ""
-        commonTestHelper.waitWithLatch {
+        commonTestHelper.runBlockingTest {
             spaceId = session.spaceService().createSpace(spaceName, topic, null, true)
-            it.countDown()
         }
 
         val syncedSpace = session.spaceService().getSpace(spaceId)
 
         var roomId = ""
-        commonTestHelper.waitWithLatch {
+        commonTestHelper.runBlockingTest {
             roomId = session.roomService().createRoom(CreateRoomParams().apply { name = "General" })
-            it.countDown()
         }
 
         val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
 
-        commonTestHelper.waitWithLatch {
+        commonTestHelper.runBlockingTest {
             syncedSpace!!.addChildren(roomId, viaServers, null, true)
-            it.countDown()
         }
 
-        commonTestHelper.waitWithLatch {
+        commonTestHelper.runBlockingTest {
             session.spaceService().setSpaceParent(roomId, spaceId, true, viaServers)
-            it.countDown()
         }
 
-        Thread.sleep(9000)
-
-        val parents = session.getRoom(roomId)?.roomSummary()?.spaceParents
-        val canonicalParents = session.getRoom(roomId)?.roomSummary()?.spaceParents?.filter { it.canonical == true }
-
-        parents?.forEach {
-            Log.d("## TEST", "parent : $it")
+        commonTestHelper.waitWithLatch { latch ->
+            commonTestHelper.retryPeriodicallyWithLatch(latch) {
+                val parents = session.getRoom(roomId)?.roomSummary()?.spaceParents
+                val canonicalParents = session.getRoom(roomId)?.roomSummary()?.spaceParents?.filter { it.canonical == true }
+                parents?.forEach {
+                    Log.d("## TEST", "parent : $it")
+                }
+                parents?.size == 1 &&
+                        parents.first().roomSummary?.name == spaceName &&
+                        canonicalParents?.size == 1 &&
+                        canonicalParents.first().roomSummary?.name == spaceName
+            }
         }
-
-        assertNotNull(parents)
-        assertEquals(1, parents!!.size)
-        assertEquals(spaceName, parents.first().roomSummary?.name)
-
-        assertNotNull(canonicalParents)
-        assertEquals(1, canonicalParents!!.size)
-        assertEquals(spaceName, canonicalParents.first().roomSummary?.name)
     }
 
 //    @Test
@@ -173,52 +165,55 @@ class SpaceHierarchyTest : InstrumentedTest {
 //    }
 
     @Test
-    fun testFilteringBySpace() = CommonTestHelper.runSessionTest(context()) { commonTestHelper ->
+    fun testFilteringBySpace() = runSessionTest(context()) { commonTestHelper ->
         val session = commonTestHelper.createAccount("John", SessionTestParams(true))
 
         val spaceAInfo = createPublicSpace(
-                session, "SpaceA", listOf(
-                Triple("A1", true /*auto-join*/, true/*canonical*/),
-                Triple("A2", true, true)
-        )
+                commonTestHelper,
+                session, "SpaceA",
+                listOf(
+                        Triple("A1", true /*auto-join*/, true/*canonical*/),
+                        Triple("A2", true, true)
+                )
         )
 
         /* val spaceBInfo = */ createPublicSpace(
-            session, "SpaceB", listOf(
-            Triple("B1", true /*auto-join*/, true/*canonical*/),
-            Triple("B2", true, true),
-            Triple("B3", true, true)
-    )
+            commonTestHelper,
+            session, "SpaceB",
+            listOf(
+                    Triple("B1", true /*auto-join*/, true/*canonical*/),
+                    Triple("B2", true, true),
+                    Triple("B3", true, true)
+            )
     )
 
         val spaceCInfo = createPublicSpace(
-                session, "SpaceC", listOf(
-                Triple("C1", true /*auto-join*/, true/*canonical*/),
-                Triple("C2", true, true)
-        )
+                commonTestHelper,
+                session, "SpaceC",
+                listOf(
+                        Triple("C1", true /*auto-join*/, true/*canonical*/),
+                        Triple("C2", true, true)
+                )
         )
 
         // add C as a subspace of A
         val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId)
         val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
-        commonTestHelper.waitWithLatch {
+        commonTestHelper.runBlockingTest {
             spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
             session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers)
-            it.countDown()
         }
 
         // Create orphan rooms
 
         var orphan1 = ""
-        commonTestHelper.waitWithLatch {
+        commonTestHelper.runBlockingTest {
             orphan1 = session.roomService().createRoom(CreateRoomParams().apply { name = "O1" })
-            it.countDown()
         }
 
         var orphan2 = ""
-        commonTestHelper.waitWithLatch {
+        commonTestHelper.runBlockingTest {
             orphan2 = session.roomService().createRoom(CreateRoomParams().apply { name = "O2" })
-            it.countDown()
         }
 
         val allRooms = session.roomService().getRoomSummaries(roomSummaryQueryParams { excludeType = listOf(RoomType.SPACE) })
@@ -240,10 +235,9 @@ class SpaceHierarchyTest : InstrumentedTest {
         assertTrue("A1 should be a grand child of A", aChildren.any { it.name == "C2" })
 
         // Add a non canonical child and check that it does not appear as orphan
-        commonTestHelper.waitWithLatch {
+        commonTestHelper.runBlockingTest {
             val a3 = session.roomService().createRoom(CreateRoomParams().apply { name = "A3" })
             spaceA!!.addChildren(a3, viaServers, null, false)
-            it.countDown()
         }
 
         Thread.sleep(6_000)
@@ -255,37 +249,39 @@ class SpaceHierarchyTest : InstrumentedTest {
 
     @Test
     @Ignore("This test will be ignored until it is fixed")
-    fun testBreakCycle() = CommonTestHelper.runSessionTest(context()) { commonTestHelper ->
+    fun testBreakCycle() = runSessionTest(context()) { commonTestHelper ->
         val session = commonTestHelper.createAccount("John", SessionTestParams(true))
 
         val spaceAInfo = createPublicSpace(
-                session, "SpaceA", listOf(
-                Triple("A1", true /*auto-join*/, true/*canonical*/),
-                Triple("A2", true, true)
-        )
+                commonTestHelper,
+                session, "SpaceA",
+                listOf(
+                        Triple("A1", true /*auto-join*/, true/*canonical*/),
+                        Triple("A2", true, true)
+                )
         )
 
         val spaceCInfo = createPublicSpace(
-                session, "SpaceC", listOf(
-                Triple("C1", true /*auto-join*/, true/*canonical*/),
-                Triple("C2", true, true)
-        )
+                commonTestHelper,
+                session, "SpaceC",
+                listOf(
+                        Triple("C1", true /*auto-join*/, true/*canonical*/),
+                        Triple("C2", true, true)
+                )
         )
 
         // add C as a subspace of A
         val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId)
         val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
-        commonTestHelper.waitWithLatch {
+        commonTestHelper.runBlockingTest {
             spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
             session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers)
-            it.countDown()
         }
 
         // add back A as subspace of C
-        commonTestHelper.waitWithLatch {
+        commonTestHelper.runBlockingTest {
             val spaceC = session.spaceService().getSpace(spaceCInfo.spaceId)
             spaceC!!.addChildren(spaceAInfo.spaceId, viaServers, null, true)
-            it.countDown()
         }
 
         // A -> C -> A
@@ -300,37 +296,46 @@ class SpaceHierarchyTest : InstrumentedTest {
     }
 
     @Test
-    fun testLiveFlatChildren() = CommonTestHelper.runSessionTest(context()) { commonTestHelper ->
+    fun testLiveFlatChildren() = runSessionTest(context()) { commonTestHelper ->
         val session = commonTestHelper.createAccount("John", SessionTestParams(true))
 
         val spaceAInfo = createPublicSpace(
-                session, "SpaceA", listOf(
-                Triple("A1", true /*auto-join*/, true/*canonical*/),
-                Triple("A2", true, true)
-        )
+                commonTestHelper,
+                session,
+                "SpaceA",
+                listOf(
+                        Triple("A1", true /*auto-join*/, true/*canonical*/),
+                        Triple("A2", true, true)
+                )
         )
 
         val spaceBInfo = createPublicSpace(
-                session, "SpaceB", listOf(
-                Triple("B1", true /*auto-join*/, true/*canonical*/),
-                Triple("B2", true, true),
-                Triple("B3", true, true)
-        )
+                commonTestHelper,
+                session,
+                "SpaceB",
+                listOf(
+                        Triple("B1", true /*auto-join*/, true/*canonical*/),
+                        Triple("B2", true, true),
+                        Triple("B3", true, true)
+                )
         )
 
         // add B as a subspace of A
         val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId)
         val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
-        runBlocking {
+        commonTestHelper.runBlockingTest {
             spaceA!!.addChildren(spaceBInfo.spaceId, viaServers, null, true)
             session.spaceService().setSpaceParent(spaceBInfo.spaceId, spaceAInfo.spaceId, true, viaServers)
         }
 
         val spaceCInfo = createPublicSpace(
-                session, "SpaceC", listOf(
-                Triple("C1", true /*auto-join*/, true/*canonical*/),
-                Triple("C2", true, true)
-        )
+                commonTestHelper,
+                session,
+                "SpaceC",
+                listOf(
+                        Triple("C1", true /*auto-join*/, true/*canonical*/),
+                        Triple("C2", true, true)
+                )
         )
 
         commonTestHelper.waitWithLatch { latch ->
@@ -348,13 +353,13 @@ class SpaceHierarchyTest : InstrumentedTest {
                 }
             }
 
+            flatAChildren.observeForever(childObserver)
+
             // add C as subspace of B
             val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId)
             spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
 
             // C1 and C2 should be in flatten child of A now
-
-            flatAChildren.observeForever(childObserver)
         }
 
         // Test part one of the rooms
@@ -374,10 +379,10 @@ class SpaceHierarchyTest : InstrumentedTest {
                 }
             }
 
-            // part from b room
-            session.roomService().leaveRoom(bRoomId)
             // The room should have disapear from flat children
             flatAChildren.observeForever(childObserver)
+            // part from b room
+            session.roomService().leaveRoom(bRoomId)
         }
         commonTestHelper.signOutAndClose(session)
     }
@@ -388,6 +393,7 @@ class SpaceHierarchyTest : InstrumentedTest {
     )
 
     private fun createPublicSpace(
+            commonTestHelper: CommonTestHelper,
             session: Session,
             spaceName: String,
             childInfo: List<Triple<String, Boolean, Boolean?>>
@@ -395,29 +401,27 @@ class SpaceHierarchyTest : InstrumentedTest {
     ): TestSpaceCreationResult {
         var spaceId = ""
         var roomIds: List<String> = emptyList()
-        runSessionTest(context()) { commonTestHelper ->
-            commonTestHelper.waitWithLatch { latch ->
-                spaceId = session.spaceService().createSpace(spaceName, "Test Topic", null, true)
-                val syncedSpace = session.spaceService().getSpace(spaceId)
-                val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
-
-                roomIds = childInfo.map { entry ->
-                    session.roomService().createRoom(CreateRoomParams().apply { name = entry.first })
-                }
-                roomIds.forEachIndexed { index, roomId ->
-                    syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second)
-                    val canonical = childInfo[index].third
-                    if (canonical != null) {
-                        session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers)
-                    }
+        commonTestHelper.runBlockingTest {
+            spaceId = session.spaceService().createSpace(spaceName, "Test Topic", null, true)
+            val syncedSpace = session.spaceService().getSpace(spaceId)
+            val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
+
+            roomIds = childInfo.map { entry ->
+                session.roomService().createRoom(CreateRoomParams().apply { name = entry.first })
+            }
+            roomIds.forEachIndexed { index, roomId ->
+                syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second)
+                val canonical = childInfo[index].third
+                if (canonical != null) {
+                    session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers)
                 }
-                latch.countDown()
             }
         }
         return TestSpaceCreationResult(spaceId, roomIds)
     }
 
     private fun createPrivateSpace(
+            commonTestHelper: CommonTestHelper,
             session: Session,
             spaceName: String,
             childInfo: List<Triple<String, Boolean, Boolean?>>
@@ -425,34 +429,31 @@ class SpaceHierarchyTest : InstrumentedTest {
     ): TestSpaceCreationResult {
         var spaceId = ""
         var roomIds: List<String> = emptyList()
-        runSessionTest(context()) { commonTestHelper ->
-            commonTestHelper.waitWithLatch { latch ->
-                spaceId = session.spaceService().createSpace(spaceName, "My Private Space", null, false)
-                val syncedSpace = session.spaceService().getSpace(spaceId)
-                val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
-                roomIds =
-                        childInfo.map { entry ->
-                            val homeServerCapabilities = session
-                                    .homeServerCapabilitiesService()
-                                    .getHomeServerCapabilities()
-                            session.roomService().createRoom(CreateRoomParams().apply {
-                                name = entry.first
-                                this.featurePreset = RestrictedRoomPreset(
-                                        homeServerCapabilities,
-                                        listOf(
-                                                RoomJoinRulesAllowEntry.restrictedToRoom(spaceId)
-                                        )
-                                )
-                            })
-                        }
-                roomIds.forEachIndexed { index, roomId ->
-                    syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second)
-                    val canonical = childInfo[index].third
-                    if (canonical != null) {
-                        session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers)
+        commonTestHelper.runBlockingTest {
+            spaceId = session.spaceService().createSpace(spaceName, "My Private Space", null, false)
+            val syncedSpace = session.spaceService().getSpace(spaceId)
+            val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
+            roomIds =
+                    childInfo.map { entry ->
+                        val homeServerCapabilities = session
+                                .homeServerCapabilitiesService()
+                                .getHomeServerCapabilities()
+                        session.roomService().createRoom(CreateRoomParams().apply {
+                            name = entry.first
+                            this.featurePreset = RestrictedRoomPreset(
+                                    homeServerCapabilities,
+                                    listOf(
+                                            RoomJoinRulesAllowEntry.restrictedToRoom(spaceId)
+                                    )
+                            )
+                        })
                     }
+            roomIds.forEachIndexed { index, roomId ->
+                syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second)
+                val canonical = childInfo[index].third
+                if (canonical != null) {
+                    session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers)
                 }
-                latch.countDown()
             }
         }
         return TestSpaceCreationResult(spaceId, roomIds)
@@ -463,25 +464,31 @@ class SpaceHierarchyTest : InstrumentedTest {
         val session = commonTestHelper.createAccount("John", SessionTestParams(true))
 
         /* val spaceAInfo = */ createPublicSpace(
-            session, "SpaceA", listOf(
-            Triple("A1", true /*auto-join*/, true/*canonical*/),
-            Triple("A2", true, true)
-    )
+            commonTestHelper,
+            session, "SpaceA",
+            listOf(
+                    Triple("A1", true /*auto-join*/, true/*canonical*/),
+                    Triple("A2", true, true)
+            )
     )
 
         val spaceBInfo = createPublicSpace(
-                session, "SpaceB", listOf(
-                Triple("B1", true /*auto-join*/, true/*canonical*/),
-                Triple("B2", true, true),
-                Triple("B3", true, true)
-        )
+                commonTestHelper,
+                session, "SpaceB",
+                listOf(
+                        Triple("B1", true /*auto-join*/, true/*canonical*/),
+                        Triple("B2", true, true),
+                        Triple("B3", true, true)
+                )
         )
 
         val spaceCInfo = createPublicSpace(
-                session, "SpaceC", listOf(
-                Triple("C1", true /*auto-join*/, true/*canonical*/),
-                Triple("C2", true, true)
-        )
+                commonTestHelper,
+                session, "SpaceC",
+                listOf(
+                        Triple("C1", true /*auto-join*/, true/*canonical*/),
+                        Triple("C2", true, true)
+                )
         )
 
         val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
@@ -490,7 +497,6 @@ class SpaceHierarchyTest : InstrumentedTest {
         runBlocking {
             val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId)
             spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
-            Thread.sleep(6_000)
         }
 
 //        Thread.sleep(4_000)
@@ -501,11 +507,12 @@ class SpaceHierarchyTest : InstrumentedTest {
         //   + C
         //     + c1, c2
 
-        val rootSpaces = commonTestHelper.runBlockingTest {
-            session.spaceService().getRootSpaceSummaries()
+        commonTestHelper.waitWithLatch { latch ->
+            commonTestHelper.retryPeriodicallyWithLatch(latch) {
+                val rootSpaces = commonTestHelper.runBlockingTest { session.spaceService().getRootSpaceSummaries() }
+                rootSpaces.size == 2
+            }
         }
-
-        assertEquals("Unexpected number of root spaces ${rootSpaces.map { it.name }}", 2, rootSpaces.size)
     }
 
     @Test
@@ -514,10 +521,12 @@ class SpaceHierarchyTest : InstrumentedTest {
         val bobSession = commonTestHelper.createAccount("Bib", SessionTestParams(true))
 
         val spaceAInfo = createPrivateSpace(
-                aliceSession, "Private Space A", listOf(
-                Triple("General", true /*suggested*/, true/*canonical*/),
-                Triple("Random", true, true)
-        )
+                commonTestHelper,
+                aliceSession, "Private Space A",
+                listOf(
+                        Triple("General", true /*suggested*/, true/*canonical*/),
+                        Triple("Random", true, true)
+                )
         )
 
         commonTestHelper.runBlockingTest {
@@ -529,10 +538,9 @@ class SpaceHierarchyTest : InstrumentedTest {
         }
 
         var bobRoomId = ""
-        commonTestHelper.waitWithLatch {
+        commonTestHelper.runBlockingTest {
             bobRoomId = bobSession.roomService().createRoom(CreateRoomParams().apply { name = "A Bob Room" })
             bobSession.getRoom(bobRoomId)!!.membershipService().invite(aliceSession.myUserId)
-            it.countDown()
         }
 
         commonTestHelper.runBlockingTest {
@@ -545,9 +553,8 @@ class SpaceHierarchyTest : InstrumentedTest {
             }
         }
 
-        commonTestHelper.waitWithLatch {
+        commonTestHelper.runBlockingTest {
             bobSession.spaceService().setSpaceParent(bobRoomId, spaceAInfo.spaceId, false, listOf(bobSession.sessionParams.homeServerHost ?: ""))
-            it.countDown()
         }
 
         commonTestHelper.waitWithLatch { latch ->
@@ -603,4 +610,82 @@ class SpaceHierarchyTest : InstrumentedTest {
             }
         }
     }
+
+    @Test
+    fun testDirectParentNames() = runSessionTest(context()) { commonTestHelper ->
+        val aliceSession = commonTestHelper.createAccount("Alice", SessionTestParams(true))
+
+        val spaceAInfo = createPublicSpace(
+                commonTestHelper,
+                aliceSession, "SpaceA",
+                listOf(
+                        Triple("A1", true /*auto-join*/, true/*canonical*/),
+                        Triple("A2", true, true)
+                )
+        )
+
+        val spaceBInfo = createPublicSpace(
+                commonTestHelper,
+                aliceSession, "SpaceB",
+                listOf(
+                        Triple("B1", true /*auto-join*/, true/*canonical*/),
+                        Triple("B2", true, true),
+                        Triple("B3", true, true)
+                )
+        )
+
+        // also add B1 in space A
+
+        val B1roomId = spaceBInfo.roomIds.first()
+        val viaServers = listOf(aliceSession.sessionParams.homeServerHost ?: "")
+
+        val spaceA = aliceSession.spaceService().getSpace(spaceAInfo.spaceId)
+        val spaceB = aliceSession.spaceService().getSpace(spaceBInfo.spaceId)
+        commonTestHelper.runBlockingTest {
+            spaceA!!.addChildren(B1roomId, viaServers, null, true)
+        }
+
+        commonTestHelper.waitWithLatch { latch ->
+            commonTestHelper.retryPeriodicallyWithLatch(latch) {
+                val roomSummary = aliceSession.getRoomSummary(B1roomId)
+                roomSummary != null &&
+                        roomSummary.directParentNames.size == 2 &&
+                        roomSummary.directParentNames.contains(spaceA!!.spaceSummary()!!.name) &&
+                        roomSummary.directParentNames.contains(spaceB!!.spaceSummary()!!.name)
+            }
+        }
+
+        commonTestHelper.waitWithLatch { latch ->
+            commonTestHelper.retryPeriodicallyWithLatch(latch) {
+                val roomSummary = aliceSession.getRoomSummary(spaceAInfo.roomIds.first())
+                roomSummary != null &&
+                        roomSummary.directParentNames.size == 1 &&
+                        roomSummary.directParentNames.contains(spaceA!!.spaceSummary()!!.name)
+            }
+        }
+
+        val newAName = "FooBar"
+        commonTestHelper.runBlockingTest {
+            spaceA!!.asRoom().stateService().updateName(newAName)
+        }
+
+        commonTestHelper.waitWithLatch { latch ->
+            commonTestHelper.retryPeriodicallyWithLatch(latch) {
+                val roomSummary = aliceSession.getRoomSummary(B1roomId)
+                roomSummary != null &&
+                        roomSummary.directParentNames.size == 2 &&
+                        roomSummary.directParentNames.contains(newAName) &&
+                        roomSummary.directParentNames.contains(spaceB!!.spaceSummary()!!.name)
+            }
+        }
+
+        commonTestHelper.waitWithLatch { latch ->
+            commonTestHelper.retryPeriodicallyWithLatch(latch) {
+                val roomSummary = aliceSession.getRoomSummary(spaceAInfo.roomIds.first())
+                roomSummary != null &&
+                        roomSummary.directParentNames.size == 1 &&
+                        roomSummary.directParentNames.contains(newAName)
+            }
+        }
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt
index 82f39806c0392e6359496804106fede495ba17da..bae4b06a058a5667cf0032ddf79e9311fe65b870 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt
@@ -156,6 +156,20 @@ object MatrixPatterns {
         return matrixId?.substringAfter(":", missingDelimiterValue = "")?.takeIf { it.isNotEmpty() }
     }
 
+    /**
+     * Extract user name from a matrix id.
+     *
+     * @param matrixId
+     * @return null if the input is not a valid matrixId
+     */
+    fun extractUserNameFromId(matrixId: String): String? {
+        return if (isUserId(matrixId)) {
+            matrixId.removePrefix("@").substringBefore(":", missingDelimiterValue = "")
+        } else {
+            null
+        }
+    }
+
     /**
      * Orders which are not strings, or do not consist solely of ascii characters in the range \x20 (space) to \x7E (~),
      * or consist of more than 50 characters, are forbidden and the field should be ignored if received.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/LoginType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/LoginType.kt
new file mode 100644
index 0000000000000000000000000000000000000000..627a8256799d455b80a8afa75b9bc3485b334289
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/LoginType.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.auth
+
+enum class LoginType {
+    PASSWORD,
+    SSO,
+    UNSUPPORTED,
+    CUSTOM,
+    DIRECT,
+    UNKNOWN;
+
+    companion object {
+
+        fun fromName(name: String) = when (name) {
+            PASSWORD.name -> PASSWORD
+            SSO.name -> SSO
+            UNSUPPORTED.name -> UNSUPPORTED
+            CUSTOM.name -> CUSTOM
+            DIRECT.name -> DIRECT
+            else -> UNKNOWN
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SessionParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SessionParams.kt
index e3815231d93dd04d2ac4f8422d90976fc07d54c7..de227631ed9d58f8e3e270f2e248ab0820438336 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SessionParams.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SessionParams.kt
@@ -16,6 +16,8 @@
 
 package org.matrix.android.sdk.api.auth.data
 
+import org.matrix.android.sdk.api.auth.LoginType
+
 /**
  * This data class holds necessary data to open a session.
  * You don't have to manually instantiate it.
@@ -34,7 +36,12 @@ data class SessionParams(
         /**
          * Set to false if the current token is not valid anymore. Application should not have to use this info.
          */
-        val isTokenValid: Boolean
+        val isTokenValid: Boolean,
+
+        /**
+         * The authentication method that was used to create the session.
+         */
+        val loginType: LoginType,
 ) {
     /*
      * Shortcuts. Usually the application should only need to use these shortcuts
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt
index 9507ddda65726c55a513db0bbd707b224389e242..015cb6a1a28bfc409aa7ee625aefd71447d70de0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/crypto/MXCryptoConfig.kt
@@ -38,4 +38,5 @@ data class MXCryptoConfig constructor(
          * You can limit request only to your sessions by turning this setting to `true`
          */
         val limitRoomKeyRequestsToMyDevices: Boolean = false,
-)
+
+        )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt
index d3cc8fc8e48a94a394bebac28f15598a830edd5e..68b931b33c21f6a6652b72628eea1e36044aedb8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt
@@ -62,7 +62,10 @@ fun Throwable.isUsernameInUse() = this is Failure.ServerError &&
         error.code == MatrixError.M_USER_IN_USE
 
 fun Throwable.isInvalidUsername() = this is Failure.ServerError &&
-        error.code == MatrixError.M_INVALID_USERNAME
+        (error.code == MatrixError.M_INVALID_USERNAME || usernameContainsNonAsciiCharacters())
+
+private fun Failure.ServerError.usernameContainsNonAsciiCharacters() = error.code == MatrixError.M_UNKNOWN &&
+        error.message == "Query parameter \'username\' must be ascii"
 
 fun Throwable.isInvalidPassword() = this is Failure.ServerError &&
         error.code == MatrixError.M_FORBIDDEN &&
@@ -86,6 +89,10 @@ fun Throwable.isInvalidUIAAuth() = this is Failure.ServerError &&
 fun Throwable.isHomeserverUnavailable() = this is Failure.NetworkConnection &&
         this.ioException is UnknownHostException
 
+fun Throwable.isMissingEmailVerification() = this is Failure.ServerError &&
+        error.code == MatrixError.M_UNAUTHORIZED &&
+        error.message == "Unable to get validated threepid"
+
 /**
  * Try to convert to a RegistrationFlowResponse. Return null in the cases it's not possible
  */
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/network/ApiPath.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/network/ApiPath.kt
index baf33a59c57775190fa0d480a8b4a5c17aa154a8..f5e5628566a4486d721e94750de51fd9bb164f20 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/network/ApiPath.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/network/ApiPath.kt
@@ -162,7 +162,7 @@ enum class ApiPath(val path: String, val method: String) {
     KICK_USER(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/kick", "POST"),
     REDACT_EVENT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/redact/{eventId}/{txnId}", "PUT"),
     REPORT_CONTENT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/report/{eventId}", "POST"),
-    GET_ALIASES(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc2432/rooms/{roomId}/aliases", "GET"),
+    GET_ALIASES(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/aliases", "GET"),
     SEND_TYPING_STATE(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/typing/{userId}", "PUT"),
     PUT_TAG(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/rooms/{roomId}/tags/{tag}", "PUT"),
     DELETE_TAG(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/rooms/{roomId}/tags/{tag}", "DELETE"),
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/SpaceFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/SpaceFilter.kt
index 6383412ffbdf714fa4b2ad8d8391508fc28b223f..ccefd5855fcb22e7eb27fd3f90133a317649aba7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/SpaceFilter.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/SpaceFilter.kt
@@ -35,9 +35,19 @@ sealed interface SpaceFilter {
      * Used to get all the rooms that do not have the provided space in their parent hierarchy.
      */
     data class ExcludeSpace(val spaceId: String) : SpaceFilter
+
+    /**
+     * Used to apply no filtering to the space.
+     */
+    object NoFilter : SpaceFilter
 }
 
 /**
  * Return a [SpaceFilter.ActiveSpace] if the String is not null, or [SpaceFilter.OrphanRooms].
  */
 fun String?.toActiveSpaceOrOrphanRooms(): SpaceFilter = this?.let { SpaceFilter.ActiveSpace(it) } ?: SpaceFilter.OrphanRooms
+
+/**
+ * Return a [SpaceFilter.ActiveSpace] if the String is not null, or [SpaceFilter.NoFilter].
+ */
+fun String?.toActiveSpaceOrNoFilter(): SpaceFilter = this?.let { SpaceFilter.ActiveSpace(it) } ?: SpaceFilter.NoFilter
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtils.kt
index bd2a1078b244702923b86f1e54715005cba9cead..e701e0f3ba0c4505c73a9633a6bbf78515b84e7e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtils.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtils.kt
@@ -180,11 +180,11 @@ class SecretStoringUtils @Inject constructor(
             is KeyStore.PrivateKeyEntry -> keyEntry.certificate.publicKey
             else -> throw IllegalStateException("Unknown KeyEntry type.")
         }
-        val cipherMode = when {
+        val cipherAlgorithm = when {
             buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> AES_MODE
             else -> RSA_MODE
         }
-        val cipher = Cipher.getInstance(cipherMode)
+        val cipher = Cipher.getInstance(cipherAlgorithm)
         cipher.init(Cipher.ENCRYPT_MODE, key)
         return cipher
     }
@@ -204,13 +204,17 @@ class SecretStoringUtils @Inject constructor(
                     .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                     .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                     .setKeySize(128)
+                    .setUserAuthenticationRequired(keyNeedsUserAuthentication)
                     .apply {
-                        setUserAuthenticationRequired(keyNeedsUserAuthentication)
-                        if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.N) {
-                            setInvalidatedByBiometricEnrollment(true)
+                        if (keyNeedsUserAuthentication) {
+                            buildVersionSdkIntProvider.whenAtLeast(Build.VERSION_CODES.N) {
+                                setInvalidatedByBiometricEnrollment(true)
+                            }
+                            buildVersionSdkIntProvider.whenAtLeast(Build.VERSION_CODES.P) {
+                                setUnlockedDeviceRequired(true)
+                            }
                         }
                     }
-                    .setUserAuthenticationRequired(keyNeedsUserAuthentication)
                     .build()
             generator.init(keyGenSpec)
             return generator.generateKey()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt
index 1b01239de5222f1b0deca3a3660c862ec987cf81..63c1c25130de509574070607bada8df944f3f667 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt
@@ -33,7 +33,6 @@ import org.matrix.android.sdk.api.session.crypto.CryptoService
 import org.matrix.android.sdk.api.session.events.EventService
 import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker
 import org.matrix.android.sdk.api.session.file.FileService
-import org.matrix.android.sdk.api.session.group.GroupService
 import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
 import org.matrix.android.sdk.api.session.identity.IdentityService
 import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService
@@ -154,11 +153,6 @@ interface Session {
      */
     fun roomDirectoryService(): RoomDirectoryService
 
-    /**
-     * Returns the GroupService associated with the session.
-     */
-    fun groupService(): GroupService
-
     /**
      * Returns the UserService associated with the session.
      */
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 638da1180498472bb9e936248dcf045611c0a9b9..a5e05f69e07ec2c270151917b6ac05dd7a0ca0ee 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.internal.crypto.model.SessionInfo
 
 interface CryptoService {
 
@@ -84,6 +85,20 @@ interface CryptoService {
 
     fun isKeyGossipingEnabled(): Boolean
 
+    /**
+     * As per MSC3061.
+     * If true will make it possible to share part of e2ee room history
+     * on invite depending on the room visibility setting.
+     */
+    fun enableShareKeyOnInvite(enable: Boolean)
+
+    /**
+     * As per MSC3061.
+     * If true will make it possible to share part of e2ee room history
+     * on invite depending on the room visibility setting.
+     */
+    fun isShareKeysOnInviteEnabled(): Boolean
+
     fun setRoomUnBlacklistUnverifiedDevices(roomId: String)
 
     fun getDeviceTrackingStatus(userId: String): Int
@@ -176,4 +191,9 @@ interface CryptoService {
      * send, in order to speed up sending of the message.
      */
     fun prepareToEncrypt(roomId: String, callback: MatrixCallback<Unit>)
+
+    /**
+     * Share all inbound sessions of the last chunk messages to the provided userId devices.
+     */
+    suspend fun sendSharedHistoryKeys(roomId: String, userId: String, sessionInfoSet: Set<SessionInfo>?)
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/ForwardedRoomKeyContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/ForwardedRoomKeyContent.kt
index 3df4ef7c9a068296184d464105896e1441cdfdd0..664cd00e94347efd7e875001ad14b904197f0159 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/ForwardedRoomKeyContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/ForwardedRoomKeyContent.kt
@@ -69,5 +69,11 @@ data class ForwardedRoomKeyContent(
          * private part of this key unless they have done device verification.
          */
         @Json(name = "sender_claimed_ed25519_key")
-        val senderClaimedEd25519Key: String? = null
+        val senderClaimedEd25519Key: String? = null,
+
+        /**
+         * MSC3061 Identifies keys that were sent when the room's visibility setting was set to world_readable or shared.
+         */
+        @Json(name = "org.matrix.msc3061.shared_history")
+        val sharedHistory: Boolean? = false,
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
index 7f9ab4c6dd61faa8434ee462819570167dde7e09..59dc6c434d00bfd083f2ac905da2051fd4ebf359 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
@@ -202,7 +202,7 @@ data class Event(
      * It will return a decrypted text message or an empty string otherwise.
      */
     fun getDecryptedTextSummary(): String? {
-        if (isRedacted()) return "Message Deleted"
+        if (isRedacted()) return "Message removed"
         val text = getDecryptedValue() ?: run {
             if (isPoll()) {
                 return getPollQuestion() ?: "created a poll."
@@ -371,6 +371,8 @@ fun Event.isPoll(): Boolean = getClearType() in EventType.POLL_START || getClear
 
 fun Event.isSticker(): Boolean = getClearType() == EventType.STICKER
 
+fun Event.isLiveLocation(): Boolean = getClearType() in EventType.STATE_ROOM_BEACON_INFO
+
 fun Event.getRelationContent(): RelationDefaultContent? {
     return if (isEncrypted()) {
         content.toModel<EncryptedEventContent>()?.relatesTo
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 fa3a9f6acd6cd19cd1296f8056ff04bfbc97c125..8fdbba21c5fed6dd7fc7b0813a73372b2bebf4bf 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
@@ -87,7 +87,10 @@ object EventType {
     // Key share events
     const val ROOM_KEY_REQUEST = "m.room_key_request"
     const val FORWARDED_ROOM_KEY = "m.forwarded_room_key"
-    const val ROOM_KEY_WITHHELD = "org.matrix.room_key.withheld"
+    val ROOM_KEY_WITHHELD = StableUnstableId(
+            stable = "m.room_key.withheld",
+            unstable = "org.matrix.room_key.withheld"
+    )
 
     const val REQUEST_SECRET = "m.secret.request"
     const val SEND_SECRET = "m.secret.send"
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/StableUnstableId.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/StableUnstableId.kt
new file mode 100644
index 0000000000000000000000000000000000000000..c68a9e47f970318725048536b2c934d63baa3807
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/StableUnstableId.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.events.model
+
+data class StableUnstableId(
+        val stable: String,
+        val unstable: String,
+) {
+    val values = listOf(stable, unstable)
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/RoomKeyContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/RoomKeyContent.kt
index 0830a566ab86d6bc301a5f94004f93b3bf369819..5b18d29ea0a73cc0f164660bc74700fcedac055f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/RoomKeyContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/content/RoomKeyContent.kt
@@ -38,5 +38,12 @@ data class RoomKeyContent(
 
         // should be a Long but it is sometimes a double
         @Json(name = "chain_index")
-        val chainIndex: Any? = null
+        val chainIndex: Any? = null,
+
+        /**
+         * MSC3061 Identifies keys that were sent when the room's visibility setting was set to world_readable or shared.
+         */
+        @Json(name = "org.matrix.msc3061.shared_history")
+        val sharedHistory: Boolean? = false
+
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/Group.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/Group.kt
deleted file mode 100644
index 25c69e5025cf306c988d7fbcf24830f61c479388..0000000000000000000000000000000000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/Group.kt
+++ /dev/null
@@ -1,31 +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.api.session.group
-
-/**
- * This interface defines methods to interact within a group.
- */
-interface Group {
-    val groupId: String
-
-    /**
-     * This methods allows you to refresh data about this group. It will be reflected on the GroupSummary.
-     * The SDK also takes care of refreshing group data every hour.
-     * @return a Cancelable to be able to cancel requests.
-     */
-    suspend fun fetchGroupData()
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/GroupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/GroupService.kt
deleted file mode 100644
index 1968af222aa1c6c44912302ad0561f8f3433b94a..0000000000000000000000000000000000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/GroupService.kt
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright 2020 The Matrix.org Foundation C.I.C.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.matrix.android.sdk.api.session.group
-
-import androidx.lifecycle.LiveData
-import org.matrix.android.sdk.api.session.group.model.GroupSummary
-
-/**
- * This interface defines methods to get groups. It's implemented at the session level.
- */
-interface GroupService {
-    /**
-     * Get a group from a groupId.
-     * @param groupId the groupId to look for.
-     * @return the group with groupId or null
-     */
-    fun getGroup(groupId: String): Group?
-
-    /**
-     * Get a groupSummary from a groupId.
-     * @param groupId the groupId to look for.
-     * @return the groupSummary with groupId or null
-     */
-    fun getGroupSummary(groupId: String): GroupSummary?
-
-    /**
-     * Get a list of group summaries. This list is a snapshot of the data.
-     * @return the list of [GroupSummary]
-     */
-    fun getGroupSummaries(groupSummaryQueryParams: GroupSummaryQueryParams): List<GroupSummary>
-
-    /**
-     * Get a live list of group summaries. This list is refreshed as soon as the data changes.
-     * @return the [LiveData] of [GroupSummary]
-     */
-    fun getGroupSummariesLive(groupSummaryQueryParams: GroupSummaryQueryParams): LiveData<List<GroupSummary>>
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/GroupSummaryQueryParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/GroupSummaryQueryParams.kt
deleted file mode 100644
index 5104b3ee5371a5b0e53c2a892f82e408e27dcaed..0000000000000000000000000000000000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/GroupSummaryQueryParams.kt
+++ /dev/null
@@ -1,44 +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.api.session.group
-
-import org.matrix.android.sdk.api.query.QueryStringValue
-import org.matrix.android.sdk.api.session.room.model.Membership
-
-fun groupSummaryQueryParams(init: (GroupSummaryQueryParams.Builder.() -> Unit) = {}): GroupSummaryQueryParams {
-    return GroupSummaryQueryParams.Builder().apply(init).build()
-}
-
-/**
- * This class can be used to filter group summaries.
- */
-data class GroupSummaryQueryParams(
-        val displayName: QueryStringValue,
-        val memberships: List<Membership>
-) {
-
-    class Builder {
-
-        var displayName: QueryStringValue = QueryStringValue.IsNotEmpty
-        var memberships: List<Membership> = Membership.all()
-
-        fun build() = GroupSummaryQueryParams(
-                displayName = displayName,
-                memberships = memberships
-        )
-    }
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/model/GroupSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/model/GroupSummary.kt
deleted file mode 100644
index ef50fce82ff3fd087a3cd37ad1eee6406fb82d3e..0000000000000000000000000000000000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/group/model/GroupSummary.kt
+++ /dev/null
@@ -1,33 +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.api.session.group.model
-
-import org.matrix.android.sdk.api.session.room.model.Membership
-
-/**
- * This class holds some data of a group.
- * It can be retrieved through [org.matrix.android.sdk.api.session.group.GroupService]
- */
-data class GroupSummary(
-        val groupId: String,
-        val membership: Membership,
-        val displayName: String = "",
-        val shortDescription: String = "",
-        val avatarUrl: String = "",
-        val roomIds: List<String> = emptyList(),
-        val userIds: List<String> = emptyList()
-)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkData.kt
index e8d9c89b547539423f7c8ea9bf48d7726d7b24ea..fc46c9211762fc9c964d8412d06ff75c0396f954 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkData.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkData.kt
@@ -54,7 +54,5 @@ sealed class PermalinkData {
 
     data class UserLink(val userId: String) : PermalinkData()
 
-    data class GroupLink(val groupId: String) : PermalinkData()
-
-    data class FallbackLink(val uri: Uri) : PermalinkData()
+    data class FallbackLink(val uri: Uri, val isLegacyGroupLink: Boolean = false) : PermalinkData()
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkParser.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkParser.kt
index 0168b7ac3ab6e9679916da9ecf416a0fdf5205f4..3dccc3fbf2228e02081c485846a486d9a268d203 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkParser.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkParser.kt
@@ -61,27 +61,29 @@ object PermalinkParser {
         val params = safeFragment
                 .split(MatrixPatterns.SEP_REGEX)
                 .filter { it.isNotEmpty() }
-                .map { URLDecoder.decode(it, "UTF-8") }
                 .take(2)
 
+        val decodedParams = params
+                .map { URLDecoder.decode(it, "UTF-8") }
+
         val identifier = params.getOrNull(0)
-        val extraParameter = params.getOrNull(1)
+        val decodedIdentifier = decodedParams.getOrNull(0)
+        val extraParameter = decodedParams.getOrNull(1)
         return when {
-            identifier.isNullOrEmpty() -> PermalinkData.FallbackLink(uri)
-            MatrixPatterns.isUserId(identifier) -> PermalinkData.UserLink(userId = identifier)
-            MatrixPatterns.isGroupId(identifier) -> PermalinkData.GroupLink(groupId = identifier)
-            MatrixPatterns.isRoomId(identifier) -> {
-                handleRoomIdCase(fragment, identifier, matrixToUri, extraParameter, viaQueryParameters)
+            identifier.isNullOrEmpty() || decodedIdentifier.isNullOrEmpty() -> PermalinkData.FallbackLink(uri)
+            MatrixPatterns.isUserId(decodedIdentifier) -> PermalinkData.UserLink(userId = decodedIdentifier)
+            MatrixPatterns.isRoomId(decodedIdentifier) -> {
+                handleRoomIdCase(fragment, decodedIdentifier, matrixToUri, extraParameter, viaQueryParameters)
             }
-            MatrixPatterns.isRoomAlias(identifier) -> {
+            MatrixPatterns.isRoomAlias(decodedIdentifier) -> {
                 PermalinkData.RoomLink(
-                        roomIdOrAlias = identifier,
+                        roomIdOrAlias = decodedIdentifier,
                         isRoomAlias = true,
                         eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) },
                         viaParameters = viaQueryParameters
                 )
             }
-            else -> PermalinkData.FallbackLink(uri)
+            else -> PermalinkData.FallbackLink(uri, MatrixPatterns.isGroupId(identifier))
         }
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt
index 5dfb8961e3ce0fb5a6630c025cde96ca49a64500..ad8106c9c10dd8a179d1e9d2aa86f02def0417a0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomService.kt
@@ -40,6 +40,18 @@ interface RoomService {
      */
     suspend fun createRoom(createRoomParams: CreateRoomParams): String
 
+    /**
+     * Create a room locally.
+     * This room will not be synchronized with the server and will not come back from the sync, so all the events related to this room will be generated
+     * locally.
+     */
+    suspend fun createLocalRoom(createRoomParams: CreateRoomParams): String
+
+    /**
+     * Delete a local room with all its related events.
+     */
+    suspend fun deleteLocalRoom(roomId: String)
+
     /**
      * Create a direct room asynchronously. This is a facility method to create a direct room with the necessary parameters.
      */
@@ -231,14 +243,11 @@ interface RoomService {
      * @param queryParams The filter to use
      * @param pagedListConfig The paged list configuration (page size, initial load, prefetch distance...)
      * @param sortOrder defines how to sort the results
-     * @param getFlattenParents When true, the list of known parents and grand parents summaries will be resolved.
-     * This can have significant impact on performance, better be used only on manageable list (filtered by displayName, ..).
      */
     fun getFilteredPagedRoomSummariesLive(
             queryParams: RoomSummaryQueryParams,
             pagedListConfig: PagedList.Config = defaultPagedListConfig,
             sortOrder: RoomSortOrder = RoomSortOrder.ACTIVITY,
-            getFlattenParents: Boolean = false,
     ): UpdatableLivePageResult
 
     /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt
index 3d943473e4ebb34aa161bf5847be9cda0088da1c..60963ef25a4da2e1a530cbb16bd24c34699c3456 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSummaryQueryParams.kt
@@ -86,11 +86,7 @@ data class RoomSummaryQueryParams(
         /**
          * Used to filter room using the current space.
          */
-        val spaceFilter: SpaceFilter?,
-        /**
-         * Used to filter room using the current group.
-         */
-        val activeGroupId: String? = null
+        val spaceFilter: SpaceFilter,
 ) {
 
     /**
@@ -105,8 +101,7 @@ data class RoomSummaryQueryParams(
         var roomTagQueryFilter: RoomTagQueryFilter? = null
         var excludeType: List<String?>? = listOf(RoomType.SPACE)
         var includeType: List<String?>? = null
-        var spaceFilter: SpaceFilter? = null
-        var activeGroupId: String? = null
+        var spaceFilter: SpaceFilter = SpaceFilter.NoFilter
 
         fun build() = RoomSummaryQueryParams(
                 displayName = displayName,
@@ -117,7 +112,6 @@ data class RoomSummaryQueryParams(
                 excludeType = excludeType,
                 includeType = includeType,
                 spaceFilter = spaceFilter,
-                activeGroupId = activeGroupId
         )
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt
index ada3dc85d78a66291a9cd7e89af6fb66335ad30c..cd8acbccccb3eeecc82f5d9fddd62c4ef783c68b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt
@@ -16,7 +16,6 @@
 
 package org.matrix.android.sdk.api.session.room.location
 
-import androidx.annotation.MainThread
 import androidx.lifecycle.LiveData
 import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
 import org.matrix.android.sdk.api.util.Cancelable
@@ -48,9 +47,10 @@ interface LocationSharingService {
     /**
      * Starts sharing live location in the room.
      * @param timeoutMillis timeout of the live in milliseconds
+     * @param description description of the live for text fallback
      * @return the result of the update of the live
      */
-    suspend fun startLiveLocationShare(timeoutMillis: Long): UpdateLiveLocationShareResult
+    suspend fun startLiveLocationShare(timeoutMillis: Long, description: String): UpdateLiveLocationShareResult
 
     /**
      * Stops sharing live location in the room.
@@ -58,16 +58,21 @@ interface LocationSharingService {
      */
     suspend fun stopLiveLocationShare(): UpdateLiveLocationShareResult
 
+    /**
+     * Redact (delete) the live associated to the given beacon info event id.
+     * @param beaconInfoEventId event id of the initial beacon info state event
+     * @param reason Optional reason string
+     */
+    suspend fun redactLiveLocationShare(beaconInfoEventId: String, reason: String?)
+
     /**
      * Returns a LiveData on the list of current running live location shares.
      */
-    @MainThread
     fun getRunningLiveLocationShareSummaries(): LiveData<List<LiveLocationShareAggregatedSummary>>
 
     /**
      * Returns a LiveData on the live location share summary with the given eventId.
      * @param beaconInfoEventId event id of the initial beacon info state event
      */
-    @MainThread
     fun getLiveLocationShareSummary(beaconInfoEventId: String): LiveData<Optional<LiveLocationShareAggregatedSummary>>
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/MembershipService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/MembershipService.kt
index e7ac69be74a5f284721b230ae5580d3234465c9c..144cfeb3b81b6dd57bc50313f9c55efb45ee8cd2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/MembershipService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/members/MembershipService.kt
@@ -30,6 +30,20 @@ interface MembershipService {
      */
     suspend fun loadRoomMembersIfNeeded()
 
+    /**
+     * All the room members can be not loaded, for instance after an initial sync.
+     * All the members will be loaded when calling [loadRoomMembersIfNeeded], or when sending an encrypted
+     * event to the room.
+     * The fun let the app know if all the members have been loaded for this room.
+     * @return true if all the members are loaded, or false elsewhere.
+     */
+    suspend fun areAllMembersLoaded(): Boolean
+
+    /**
+     * Live version for [areAllMembersLoaded].
+     */
+    fun areAllMembersLoadedLive(): LiveData<Boolean>
+
     /**
      * Return the roomMember with userId or null.
      * @param userId the userId param to look for
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomHistoryVisibility.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomHistoryVisibility.kt
index 06069f264620619b0149a92da44b7adb1f7aef22..2b0ea1d8fbcae85b219881de691a14186da248be 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomHistoryVisibility.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomHistoryVisibility.kt
@@ -48,3 +48,9 @@ enum class RoomHistoryVisibility {
      */
     @Json(name = "joined") JOINED
 }
+
+/**
+ * Room history should be shared only if room visibility is world_readable or shared.
+ */
+internal fun RoomHistoryVisibility.shouldShareHistory() =
+        this == RoomHistoryVisibility.WORLD_READABLE || this == RoomHistoryVisibility.SHARED
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt
index 1ab23b7a11953b64a104a283c505aa6a1695ed48..ff4977491fc8524a35fdfc0d1b6dece901fdb1de 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt
@@ -164,9 +164,9 @@ data class RoomSummary(
          */
         val spaceChildren: List<SpaceChildInfo>? = null,
         /**
-         * List of all the space parents. Will be empty by default, you have to explicitly request it.
+         * The names of the room's direct space parents if any.
          */
-        val flattenParents: List<RoomSummary> = emptyList(),
+        val directParentNames: List<String> = emptyList(),
         /**
          * List of all the space parent Ids.
          */
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/VersioningState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/VersioningState.kt
index 2e1668ebbba7f8a7dad45d9c0073ec8d6e0a6823..8cfe3da03187a29220a3cf2696193997d2540865 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/VersioningState.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/VersioningState.kt
@@ -33,5 +33,7 @@ enum class VersioningState {
     /**
      * The room has been upgraded, and the new room has been joined.
      */
-    UPGRADED_ROOM_JOINED,
+    UPGRADED_ROOM_JOINED;
+
+    fun isUpgraded() = this != NONE
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupRooms.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/localecho/RoomLocalEcho.kt
similarity index 58%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupRooms.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/localecho/RoomLocalEcho.kt
index 9e5d18225b3f06659748755d6272c1b56357bfdd..7ef0d63924ddba419eeb183a28871108c5f2fc28 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupRooms.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/localecho/RoomLocalEcho.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 The Matrix.org Foundation C.I.C.
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,15 +14,18 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.session.group.model
+package org.matrix.android.sdk.api.session.room.model.localecho
 
-import com.squareup.moshi.Json
-import com.squareup.moshi.JsonClass
+import java.util.UUID
 
-@JsonClass(generateAdapter = true)
-internal data class GroupRooms(
+object RoomLocalEcho {
 
-        @Json(name = "total_room_count_estimate") val totalRoomCountEstimate: Int? = null,
-        @Json(name = "chunk") val rooms: List<GroupRoom> = emptyList()
+    private const val PREFIX = "!local."
 
-)
+    /**
+     * Tell whether the provider room id is a local id.
+     */
+    fun isLocalEchoId(roomId: String) = roomId.startsWith(PREFIX)
+
+    internal fun createLocalEchoId() = "${PREFIX}${UUID.randomUUID()}"
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt
index 9d8c8a13bd7aafaa44df96aff0c645550ba3fd40..d391abf1e61528cc3768335b0d36aab8a550489b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt
@@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.RelationType
 import org.matrix.android.sdk.api.session.events.model.getRelationContent
 import org.matrix.android.sdk.api.session.events.model.isEdition
+import org.matrix.android.sdk.api.session.events.model.isLiveLocation
 import org.matrix.android.sdk.api.session.events.model.isPoll
 import org.matrix.android.sdk.api.session.events.model.isReply
 import org.matrix.android.sdk.api.session.events.model.isSticker
@@ -165,6 +166,10 @@ fun TimelineEvent.isSticker(): Boolean {
     return root.isSticker()
 }
 
+fun TimelineEvent.isLiveLocation(): Boolean {
+    return root.isLiveLocation()
+}
+
 /**
  * Returns whether or not the event is a root thread event.
  */
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/InitialSyncStep.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/InitialSyncStep.kt
index 407585b003c448b3eb1b246d11df009982b37efd..c4a3638ac495cf775500d6642e443680bd8763ba 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/InitialSyncStep.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/InitialSyncStep.kt
@@ -22,7 +22,6 @@ enum class InitialSyncStep {
     ImportingAccount,
     ImportingAccountCrypto,
     ImportingAccountRoom,
-    ImportingAccountGroups,
     ImportingAccountData,
     ImportingAccountJoinedRooms,
     ImportingAccountInvitedRooms,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/SyncService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/SyncService.kt
index 5b2bf651af891090d6776600d19dc16ddd29282a..71f7ab8494b6be34bd67c0e4629e6667b58a1675 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/SyncService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/SyncService.kt
@@ -60,9 +60,9 @@ interface SyncService {
     fun getSyncStateLive(): LiveData<SyncState>
 
     /**
-     * Get the [SyncRequestState] as a LiveData.
+     * Get the [SyncRequestState] as a SharedFlow.
      */
-    fun getSyncRequestStateLive(): LiveData<SyncRequestState>
+    fun getSyncRequestStateFlow(): SharedFlow<SyncRequestState>
 
     /**
      * This method returns a flow of SyncResponse. New value will be pushed through the sync thread.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/GroupSyncProfile.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/GroupSyncProfile.kt
deleted file mode 100644
index 581e6824ee5a41db133ba961aed381b5e10322a4..0000000000000000000000000000000000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/GroupSyncProfile.kt
+++ /dev/null
@@ -1,33 +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.api.session.sync.model
-
-import com.squareup.moshi.Json
-import com.squareup.moshi.JsonClass
-
-@JsonClass(generateAdapter = true)
-data class GroupSyncProfile(
-        /**
-         * The name of the group, if any. May be nil.
-         */
-        @Json(name = "name") val name: String? = null,
-
-        /**
-         * The URL for the group's avatar. May be nil.
-         */
-        @Json(name = "avatar_url") val avatarUrl: String? = null
-)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/GroupsSyncResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/GroupsSyncResponse.kt
deleted file mode 100644
index fd8710bbda4f4524a0d7e8dc27aa22b87c448f8a..0000000000000000000000000000000000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/GroupsSyncResponse.kt
+++ /dev/null
@@ -1,38 +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.api.session.sync.model
-
-import com.squareup.moshi.Json
-import com.squareup.moshi.JsonClass
-
-@JsonClass(generateAdapter = true)
-data class GroupsSyncResponse(
-        /**
-         * Joined groups: An array of groups ids.
-         */
-        @Json(name = "join") val join: Map<String, Any> = emptyMap(),
-
-        /**
-         * Invitations. The groups that the user has been invited to: keys are groups ids.
-         */
-        @Json(name = "invite") val invite: Map<String, InvitedGroupSync> = emptyMap(),
-
-        /**
-         * Left groups. An array of groups ids: the groups that the user has left or been banned from.
-         */
-        @Json(name = "leave") val leave: Map<String, Any> = emptyMap()
-)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/InvitedGroupSync.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/InvitedGroupSync.kt
deleted file mode 100644
index d41df9f0f62b67030c82ff2519494bc02c95ff89..0000000000000000000000000000000000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/InvitedGroupSync.kt
+++ /dev/null
@@ -1,33 +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.api.session.sync.model
-
-import com.squareup.moshi.Json
-import com.squareup.moshi.JsonClass
-
-@JsonClass(generateAdapter = true)
-data class InvitedGroupSync(
-        /**
-         * The identifier of the inviter.
-         */
-        @Json(name = "inviter") val inviter: String? = null,
-
-        /**
-         * The group profile.
-         */
-        @Json(name = "profile") val profile: GroupSyncProfile? = null
-)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/SyncResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/SyncResponse.kt
index c70964a513e3e5c361e4b2571a0cb37689657ee5..382d8a1740421f0ed9e07c8ec4d13c6ed67f793a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/SyncResponse.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/SyncResponse.kt
@@ -65,10 +65,4 @@ data class SyncResponse(
          */
         @Json(name = "org.matrix.msc2732.device_unused_fallback_key_types")
         val deviceUnusedFallbackKeyTypes: List<String>? = null,
-
-        /**
-         * List of groups.
-         */
-        @Json(name = "groups") val groups: GroupsSyncResponse? = null
-
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetType.kt
index ee098f9bf2e064ad9f71d6a9959d7a3bf8c28b8e..f02fe4f9de82a9f0c279cf8e270203fd46b7d22f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetType.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetType.kt
@@ -28,7 +28,8 @@ private val DEFINED_TYPES by lazy {
             WidgetType.StickerPicker,
             WidgetType.Grafana,
             WidgetType.Custom,
-            WidgetType.IntegrationManager
+            WidgetType.IntegrationManager,
+            WidgetType.ElementCall,
     )
 }
 
@@ -47,6 +48,7 @@ sealed class WidgetType(open val preferred: String, open val legacy: String = pr
     object Grafana : WidgetType("m.grafana")
     object Custom : WidgetType("m.custom")
     object IntegrationManager : WidgetType("m.integration_manager")
+    object ElementCall : WidgetType("io.element.call")
     data class Fallback(override val preferred: String) : WidgetType(preferred)
 
     fun matches(type: String): Boolean {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/BuildVersionSdkIntProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/BuildVersionSdkIntProvider.kt
index b7ea187ec508b7453f821973187eea8aa0b0e7e5..900a2e237f175da98dfd44f7ccae9745c54e0a10 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/BuildVersionSdkIntProvider.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/BuildVersionSdkIntProvider.kt
@@ -21,4 +21,14 @@ interface BuildVersionSdkIntProvider {
      * Return the current version of the Android SDK.
      */
     fun get(): Int
+
+    /**
+     * Checks the if the current OS version is equal or greater than [version].
+     * @return A `non-null` result if true, `null` otherwise.
+     */
+    fun <T> whenAtLeast(version: Int, result: () -> T): T? {
+        return if (get() >= version) {
+            result()
+        } else null
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt
index 26dd31dc2d4f214a12bbd9ef40121abfff635dee..974f1cfcbefa793fb0c6cfd675b6b0c842d3dde5 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MatrixItem.kt
@@ -18,7 +18,6 @@ package org.matrix.android.sdk.api.util
 
 import org.matrix.android.sdk.BuildConfig
 import org.matrix.android.sdk.api.extensions.tryOrNull
-import org.matrix.android.sdk.api.session.group.model.GroupSummary
 import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
 import org.matrix.android.sdk.api.session.room.model.RoomSummary
 import org.matrix.android.sdk.api.session.room.model.RoomType
@@ -113,19 +112,6 @@ sealed class MatrixItem(
         override fun updateAvatar(newAvatar: String?) = copy(avatarUrl = newAvatar)
     }
 
-    data class GroupItem(
-            override val id: String,
-            override val displayName: String? = null,
-            override val avatarUrl: String? = null
-    ) :
-            MatrixItem(id, displayName, avatarUrl) {
-        init {
-            if (BuildConfig.DEBUG) checkId()
-        }
-
-        override fun updateAvatar(newAvatar: String?) = copy(avatarUrl = newAvatar)
-    }
-
     protected fun checkId() {
         if (!id.startsWith(getIdPrefix())) {
             error("Wrong usage of MatrixItem: check the id $id should start with ${getIdPrefix()}")
@@ -144,7 +130,6 @@ sealed class MatrixItem(
         is RoomItem,
         is EveryoneInRoomItem -> '!'
         is RoomAliasItem -> '#'
-        is GroupItem -> '+'
     }
 
     fun firstLetterOfDisplayName(): String {
@@ -196,8 +181,6 @@ sealed class MatrixItem(
 
 fun User.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)
 
-fun GroupSummary.toMatrixItem() = MatrixItem.GroupItem(groupId, displayName, avatarUrl)
-
 fun RoomSummary.toMatrixItem() = if (roomType == RoomType.SPACE) {
     MatrixItem.SpaceItem(roomId, displayName, avatarUrl)
 } else {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt
index ef47775f1bec34033b2e42ab9207ed67de510c06..5ec0dedadf51b32352def7c4f146277e5f09bcae 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/MimeTypes.kt
@@ -33,9 +33,15 @@ object MimeTypes {
 
     const val Ogg = "audio/ogg"
 
+    const val PlainText = "text/plain"
+
     fun String?.normalizeMimeType() = if (this == BadJpg) Jpeg else this
 
     fun String?.isMimeTypeImage() = this?.startsWith("image/").orFalse()
     fun String?.isMimeTypeVideo() = this?.startsWith("video/").orFalse()
     fun String?.isMimeTypeAudio() = this?.startsWith("audio/").orFalse()
+    fun String?.isMimeTypeApplication() = this?.startsWith("application/").orFalse()
+    fun String?.isMimeTypeFile() = this?.startsWith("file/").orFalse()
+    fun String?.isMimeTypeText() = this?.startsWith("text/").orFalse()
+    fun String?.isMimeTypeAny() = this?.startsWith("*/").orFalse()
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/StringOrderUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/StringOrderUtils.kt
index 83c858594167aa658d166b268c1830dc76fc4152..1de0a360342eb536522654d07fa9f52bc23d409a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/StringOrderUtils.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/StringOrderUtils.kt
@@ -76,7 +76,7 @@ object StringOrderUtils {
     }
 
     fun stringToBase(x: String, alphabet: CharArray): BigInteger {
-        if (x.isEmpty()) throw IllegalArgumentException()
+        require(x.isNotEmpty())
         val len = alphabet.size.toBigInteger()
         var result = BigInteger("0")
         x.reversed().forEachIndexed { index, c ->
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthModule.kt
index ddb70be906b15b8e84d872eab2ab9edc9fb6ef23..463692e574cf767a9837150a643c9ffa9094e4fb 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthModule.kt
@@ -83,6 +83,9 @@ internal abstract class AuthModule {
     @Binds
     abstract fun bindSessionCreator(creator: DefaultSessionCreator): SessionCreator
 
+    @Binds
+    abstract fun bindSessionParamsCreator(creator: DefaultSessionParamsCreator): SessionParamsCreator
+
     @Binds
     abstract fun bindDirectLoginTask(task: DefaultDirectLoginTask): DirectLoginTask
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt
index 9d6b018a672cc113b3cf5e92820ba9d41b3f92b0..446f9318479fc16d0898f84d23b6c034b68645e3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt
@@ -22,6 +22,7 @@ import okhttp3.OkHttpClient
 import org.matrix.android.sdk.api.MatrixPatterns
 import org.matrix.android.sdk.api.MatrixPatterns.getServerName
 import org.matrix.android.sdk.api.auth.AuthenticationService
+import org.matrix.android.sdk.api.auth.LoginType
 import org.matrix.android.sdk.api.auth.data.Credentials
 import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
 import org.matrix.android.sdk.api.auth.data.LoginFlowResult
@@ -361,7 +362,7 @@ internal class DefaultAuthenticationService @Inject constructor(
             homeServerConnectionConfig: HomeServerConnectionConfig,
             credentials: Credentials
     ): Session {
-        return sessionCreator.createSession(credentials, homeServerConnectionConfig)
+        return sessionCreator.createSession(credentials, homeServerConnectionConfig, LoginType.SSO)
     }
 
     override suspend fun getWellKnownData(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/SessionCreator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/SessionCreator.kt
index ba01146a4a7e450e18bf6f62f63193f6fe679110..7dbb11c7fd04d8b8aaa51cf6ab2f2d2f785802fe 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/SessionCreator.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/SessionCreator.kt
@@ -16,69 +16,41 @@
 
 package org.matrix.android.sdk.internal.auth
 
-import android.net.Uri
+import org.matrix.android.sdk.api.auth.LoginType
 import org.matrix.android.sdk.api.auth.data.Credentials
 import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
-import org.matrix.android.sdk.api.auth.data.SessionParams
-import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.internal.SessionManager
-import timber.log.Timber
 import javax.inject.Inject
 
 internal interface SessionCreator {
-    suspend fun createSession(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session
+
+    suspend fun createSession(
+            credentials: Credentials,
+            homeServerConnectionConfig: HomeServerConnectionConfig,
+            loginType: LoginType,
+    ): Session
 }
 
 internal class DefaultSessionCreator @Inject constructor(
         private val sessionParamsStore: SessionParamsStore,
         private val sessionManager: SessionManager,
         private val pendingSessionStore: PendingSessionStore,
-        private val isValidClientServerApiTask: IsValidClientServerApiTask
+        private val sessionParamsCreator: SessionParamsCreator,
 ) : SessionCreator {
 
     /**
      * Credentials can affect the homeServerConnectionConfig, override homeserver url and/or
      * identity server url if provided in the credentials.
      */
-    override suspend fun createSession(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session {
+    override suspend fun createSession(
+            credentials: Credentials,
+            homeServerConnectionConfig: HomeServerConnectionConfig,
+            loginType: LoginType,
+    ): Session {
         // We can cleanup the pending session params
         pendingSessionStore.delete()
-
-        val overriddenUrl = credentials.discoveryInformation?.homeServer?.baseURL
-                // remove trailing "/"
-                ?.trim { it == '/' }
-                ?.takeIf { it.isNotBlank() }
-                // It can be the same value, so in this case, do not check again the validity
-                ?.takeIf { it != homeServerConnectionConfig.homeServerUriBase.toString() }
-                ?.also { Timber.d("Overriding homeserver url to $it (will check if valid)") }
-                ?.let { Uri.parse(it) }
-                ?.takeIf {
-                    // Validate the URL, if the configuration is wrong server side, do not override
-                    tryOrNull {
-                        isValidClientServerApiTask.execute(
-                                IsValidClientServerApiTask.Params(
-                                        homeServerConnectionConfig.copy(homeServerUriBase = it)
-                                )
-                        )
-                                .also { Timber.d("Overriding homeserver url: $it") }
-                    } ?: true // In case of other error (no network, etc.), consider it is valid...
-                }
-
-        val sessionParams = SessionParams(
-                credentials = credentials,
-                homeServerConnectionConfig = homeServerConnectionConfig.copy(
-                        homeServerUriBase = overriddenUrl ?: homeServerConnectionConfig.homeServerUriBase,
-                        identityServerUri = credentials.discoveryInformation?.identityServer?.baseURL
-                                // remove trailing "/"
-                                ?.trim { it == '/' }
-                                ?.takeIf { it.isNotBlank() }
-                                ?.also { Timber.d("Overriding identity server url to $it") }
-                                ?.let { Uri.parse(it) }
-                                ?: homeServerConnectionConfig.identityServerUri
-                ),
-                isTokenValid = true)
-
+        val sessionParams = sessionParamsCreator.create(credentials, homeServerConnectionConfig, loginType)
         sessionParamsStore.save(sessionParams)
         return sessionManager.getOrCreateSession(sessionParams)
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/SessionParamsCreator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/SessionParamsCreator.kt
new file mode 100644
index 0000000000000000000000000000000000000000..31ed9a1e85fafaa585b3ae355db0d454457f75a5
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/SessionParamsCreator.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.auth
+
+import android.net.Uri
+import org.matrix.android.sdk.api.auth.LoginType
+import org.matrix.android.sdk.api.auth.data.Credentials
+import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
+import org.matrix.android.sdk.api.auth.data.SessionParams
+import org.matrix.android.sdk.api.extensions.tryOrNull
+import timber.log.Timber
+import javax.inject.Inject
+
+internal interface SessionParamsCreator {
+
+    suspend fun create(
+            credentials: Credentials,
+            homeServerConnectionConfig: HomeServerConnectionConfig,
+            loginType: LoginType,
+    ): SessionParams
+}
+
+internal class DefaultSessionParamsCreator @Inject constructor(
+        private val isValidClientServerApiTask: IsValidClientServerApiTask
+) : SessionParamsCreator {
+
+    override suspend fun create(
+            credentials: Credentials,
+            homeServerConnectionConfig: HomeServerConnectionConfig,
+            loginType: LoginType,
+    ) = SessionParams(
+            credentials = credentials,
+            homeServerConnectionConfig = homeServerConnectionConfig.overrideWithCredentials(credentials),
+            isTokenValid = true,
+            loginType = loginType,
+    )
+
+    private suspend fun HomeServerConnectionConfig.overrideWithCredentials(credentials: Credentials) = copy(
+            homeServerUriBase = credentials.getHomeServerUri(this) ?: homeServerUriBase,
+            identityServerUri = credentials.getIdentityServerUri() ?: identityServerUri
+    )
+
+    private suspend fun Credentials.getHomeServerUri(homeServerConnectionConfig: HomeServerConnectionConfig) =
+            discoveryInformation?.homeServer?.baseURL
+                    ?.trim { it == '/' }
+                    ?.takeIf { it.isNotBlank() }
+                    // It can be the same value, so in this case, do not check again the validity
+                    ?.takeIf { it != homeServerConnectionConfig.homeServerUriBase.toString() }
+                    ?.also { Timber.d("Overriding homeserver url to $it (will check if valid)") }
+                    ?.let { Uri.parse(it) }
+                    ?.takeIf { validateUri(it, homeServerConnectionConfig) }
+
+    private suspend fun validateUri(uri: Uri, homeServerConnectionConfig: HomeServerConnectionConfig) =
+            // Validate the URL, if the configuration is wrong server side, do not override
+            tryOrNull {
+                performClientServerApiValidation(uri, homeServerConnectionConfig)
+            } ?: true // In case of other error (no network, etc.), consider it is valid...
+
+    private suspend fun performClientServerApiValidation(uri: Uri, homeServerConnectionConfig: HomeServerConnectionConfig) =
+            isValidClientServerApiTask.execute(
+                    IsValidClientServerApiTask.Params(homeServerConnectionConfig.copy(homeServerUriBase = uri))
+            ).also { Timber.d("Overriding homeserver url: $it") }
+
+    private fun Credentials.getIdentityServerUri() = discoveryInformation?.identityServer?.baseURL
+            ?.trim { it == '/' }
+            ?.takeIf { it.isNotBlank() }
+            ?.also { Timber.d("Overriding identity server url to $it") }
+            ?.let { Uri.parse(it) }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/AuthRealmMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/AuthRealmMigration.kt
index 88c6d04ee67f6f50c222241b6eb209c63be74c3c..c5b8eae3ff07f5c788b21363e71787db7b55c558 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/AuthRealmMigration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/AuthRealmMigration.kt
@@ -17,15 +17,18 @@
 package org.matrix.android.sdk.internal.auth.db
 
 import io.realm.DynamicRealm
-import io.realm.RealmMigration
 import org.matrix.android.sdk.internal.auth.db.migration.MigrateAuthTo001
 import org.matrix.android.sdk.internal.auth.db.migration.MigrateAuthTo002
 import org.matrix.android.sdk.internal.auth.db.migration.MigrateAuthTo003
 import org.matrix.android.sdk.internal.auth.db.migration.MigrateAuthTo004
-import timber.log.Timber
+import org.matrix.android.sdk.internal.auth.db.migration.MigrateAuthTo005
+import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
 import javax.inject.Inject
 
-internal class AuthRealmMigration @Inject constructor() : RealmMigration {
+internal class AuthRealmMigration @Inject constructor() : MatrixRealmMigration(
+        dbName = "Auth",
+        schemaVersion = 5L,
+) {
     /**
      * Forces all AuthRealmMigration instances to be equal.
      * Avoids Realm throwing when multiple instances of the migration are set.
@@ -33,14 +36,11 @@ internal class AuthRealmMigration @Inject constructor() : RealmMigration {
     override fun equals(other: Any?) = other is AuthRealmMigration
     override fun hashCode() = 4000
 
-    val schemaVersion = 4L
-
-    override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
-        Timber.d("Migrating Auth Realm from $oldVersion to $newVersion")
-
+    override fun doMigrate(realm: DynamicRealm, oldVersion: Long) {
         if (oldVersion < 1) MigrateAuthTo001(realm).perform()
         if (oldVersion < 2) MigrateAuthTo002(realm).perform()
         if (oldVersion < 3) MigrateAuthTo003(realm).perform()
         if (oldVersion < 4) MigrateAuthTo004(realm).perform()
+        if (oldVersion < 5) MigrateAuthTo005(realm).perform()
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/SessionParamsEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/SessionParamsEntity.kt
index ba1ab8147b766a06b64425305703b4a1645b2ff0..f6c883cac0e97287db9d97e0b8edf3133f8e6b21 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/SessionParamsEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/SessionParamsEntity.kt
@@ -26,5 +26,6 @@ internal open class SessionParamsEntity(
         var homeServerConnectionConfigJson: String = "",
         // Set to false when the token is invalid and the user has been soft logged out
         // In case of hard logout, this object is deleted from DB
-        var isTokenValid: Boolean = true
+        var isTokenValid: Boolean = true,
+        var loginType: String = "",
 ) : RealmObject()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/SessionParamsMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/SessionParamsMapper.kt
index 86929b1afe7f7a9f93ff25b316139eedfd2773e9..23923bf2677001357ab24c47179135736bc8c6ca 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/SessionParamsMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/SessionParamsMapper.kt
@@ -17,6 +17,7 @@
 package org.matrix.android.sdk.internal.auth.db
 
 import com.squareup.moshi.Moshi
+import org.matrix.android.sdk.api.auth.LoginType
 import org.matrix.android.sdk.api.auth.data.Credentials
 import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
 import org.matrix.android.sdk.api.auth.data.SessionParams
@@ -37,7 +38,7 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {
         if (credentials == null || homeServerConnectionConfig == null) {
             return null
         }
-        return SessionParams(credentials, homeServerConnectionConfig, entity.isTokenValid)
+        return SessionParams(credentials, homeServerConnectionConfig, entity.isTokenValid, LoginType.fromName(entity.loginType))
     }
 
     fun map(sessionParams: SessionParams?): SessionParamsEntity? {
@@ -54,7 +55,8 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {
                 sessionParams.userId,
                 credentialsJson,
                 homeServerConnectionConfigJson,
-                sessionParams.isTokenValid
+                sessionParams.isTokenValid,
+                sessionParams.loginType.name,
         )
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo005.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo005.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2cf1b62a4c4a9a2be519df32105f563262b3976b
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo005.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.auth.db.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.api.auth.LoginType
+import org.matrix.android.sdk.internal.auth.db.SessionParamsEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+import timber.log.Timber
+
+internal class MigrateAuthTo005(realm: DynamicRealm) : RealmMigrator(realm, 5) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        Timber.d("Update SessionParamsEntity to add LoginType")
+
+        realm.schema.get("SessionParamsEntity")
+                ?.addField(SessionParamsEntityFields.LOGIN_TYPE, String::class.java)
+                ?.setRequired(SessionParamsEntityFields.LOGIN_TYPE, true)
+                ?.transform { it.set(SessionParamsEntityFields.LOGIN_TYPE, LoginType.UNKNOWN.name) }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt
index 656a4f671bd46488bedd98cebaca32363a8f3fca..468e998407cac4e38d06ebf256221053cf0e80fd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt
@@ -17,6 +17,7 @@
 package org.matrix.android.sdk.internal.auth.login
 
 import android.util.Patterns
+import org.matrix.android.sdk.api.auth.LoginType
 import org.matrix.android.sdk.api.auth.login.LoginProfileInfo
 import org.matrix.android.sdk.api.auth.login.LoginWizard
 import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
@@ -78,7 +79,7 @@ internal class DefaultLoginWizard(
             authAPI.login(loginParams)
         }
 
-        return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig)
+        return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig, LoginType.PASSWORD)
     }
 
     /**
@@ -92,7 +93,7 @@ internal class DefaultLoginWizard(
             authAPI.login(loginParams)
         }
 
-        return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig)
+        return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig, LoginType.SSO)
     }
 
     override suspend fun loginCustom(data: JsonDict): Session {
@@ -100,7 +101,7 @@ internal class DefaultLoginWizard(
             authAPI.login(data)
         }
 
-        return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig)
+        return sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig, LoginType.CUSTOM)
     }
 
     override suspend fun resetPassword(email: String) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DirectLoginTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DirectLoginTask.kt
index c9311867c8c6bffee491b9ae835fdc5a4b586cca..af421057561fa0a6a990cfcd57fd33c2335373a9 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DirectLoginTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DirectLoginTask.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.auth.login
 
 import dagger.Lazy
 import okhttp3.OkHttpClient
+import org.matrix.android.sdk.api.auth.LoginType
 import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
 import org.matrix.android.sdk.api.failure.Failure
 import org.matrix.android.sdk.api.session.Session
@@ -77,7 +78,7 @@ internal class DefaultDirectLoginTask @Inject constructor(
             }
         }
 
-        return sessionCreator.createSession(credentials, params.homeServerConnectionConfig)
+        return sessionCreator.createSession(credentials, params.homeServerConnectionConfig, LoginType.DIRECT)
     }
 
     private fun buildClient(homeServerConnectionConfig: HomeServerConnectionConfig): OkHttpClient {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt
index d6ec0297b4a88e900805f0ec15e7c0c8466305a3..46ebbb7b7171f7881b1b87e300b58124e74d8017 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt
@@ -17,6 +17,7 @@
 package org.matrix.android.sdk.internal.auth.registration
 
 import kotlinx.coroutines.delay
+import org.matrix.android.sdk.api.auth.LoginType
 import org.matrix.android.sdk.api.auth.data.Credentials
 import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
 import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
@@ -64,7 +65,7 @@ internal class DefaultRegistrationWizard(
 
     override suspend fun getRegistrationFlow(): RegistrationResult {
         val params = RegistrationParams()
-        return performRegistrationRequest(params)
+        return performRegistrationRequest(params, LoginType.PASSWORD)
     }
 
     override suspend fun createAccount(
@@ -77,7 +78,7 @@ internal class DefaultRegistrationWizard(
                 password = password,
                 initialDeviceDisplayName = initialDeviceDisplayName
         )
-        return performRegistrationRequest(params)
+        return performRegistrationRequest(params, LoginType.PASSWORD)
                 .also {
                     pendingSessionData = pendingSessionData.copy(isRegistrationStarted = true)
                             .also { pendingSessionStore.savePendingSessionData(it) }
@@ -89,7 +90,7 @@ internal class DefaultRegistrationWizard(
                 ?: throw IllegalStateException("developer error, call createAccount() method first")
 
         val params = RegistrationParams(auth = AuthParams.createForCaptcha(safeSession, response))
-        return performRegistrationRequest(params)
+        return performRegistrationRequest(params, LoginType.PASSWORD)
     }
 
     override suspend fun acceptTerms(): RegistrationResult {
@@ -97,7 +98,7 @@ internal class DefaultRegistrationWizard(
                 ?: throw IllegalStateException("developer error, call createAccount() method first")
 
         val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.TERMS, session = safeSession))
-        return performRegistrationRequest(params)
+        return performRegistrationRequest(params, LoginType.PASSWORD)
     }
 
     override suspend fun addThreePid(threePid: RegisterThreePid): RegistrationResult {
@@ -151,14 +152,14 @@ internal class DefaultRegistrationWizard(
                 .also { pendingSessionStore.savePendingSessionData(it) }
 
         // and send the sid a first time
-        return performRegistrationRequest(params)
+        return performRegistrationRequest(params, LoginType.PASSWORD)
     }
 
     override suspend fun checkIfEmailHasBeenValidated(delayMillis: Long): RegistrationResult {
         val safeParam = pendingSessionData.currentThreePidData?.registrationParams
                 ?: throw IllegalStateException("developer error, no pending three pid")
 
-        return performRegistrationRequest(safeParam, delayMillis)
+        return performRegistrationRequest(safeParam, LoginType.PASSWORD, delayMillis)
     }
 
     override suspend fun handleValidateThreePid(code: String): RegistrationResult {
@@ -179,7 +180,7 @@ internal class DefaultRegistrationWizard(
         if (validationResponse.isSuccess()) {
             // The entered code is correct
             // Same than validate email
-            return performRegistrationRequest(registrationParams, 3_000)
+            return performRegistrationRequest(registrationParams, LoginType.PASSWORD, 3_000)
         } else {
             // The code is not correct
             throw Failure.SuccessError
@@ -191,7 +192,7 @@ internal class DefaultRegistrationWizard(
                 ?: throw IllegalStateException("developer error, call createAccount() method first")
 
         val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.DUMMY, session = safeSession))
-        return performRegistrationRequest(params)
+        return performRegistrationRequest(params, LoginType.PASSWORD)
     }
 
     override suspend fun registrationCustom(
@@ -204,25 +205,28 @@ internal class DefaultRegistrationWizard(
         mutableParams["session"] = safeSession
 
         val params = RegistrationCustomParams(auth = mutableParams)
-        return performRegistrationOtherRequest(params)
+        return performRegistrationOtherRequest(LoginType.CUSTOM, params)
     }
 
     private suspend fun performRegistrationRequest(
             registrationParams: RegistrationParams,
+            loginType: LoginType,
             delayMillis: Long = 0
     ): RegistrationResult {
         delay(delayMillis)
-        return register { registerTask.execute(RegisterTask.Params(registrationParams)) }
+        return register(loginType) { registerTask.execute(RegisterTask.Params(registrationParams)) }
     }
 
     private suspend fun performRegistrationOtherRequest(
-            registrationCustomParams: RegistrationCustomParams
+            loginType: LoginType,
+            registrationCustomParams: RegistrationCustomParams,
     ): RegistrationResult {
-        return register { registerCustomTask.execute(RegisterCustomTask.Params(registrationCustomParams)) }
+        return register(loginType) { registerCustomTask.execute(RegisterCustomTask.Params(registrationCustomParams)) }
     }
 
     private suspend fun register(
-            execute: suspend () -> Credentials
+            loginType: LoginType,
+            execute: suspend () -> Credentials,
     ): RegistrationResult {
         val credentials = try {
             execute.invoke()
@@ -237,8 +241,7 @@ internal class DefaultRegistrationWizard(
             }
         }
 
-        val session =
-                sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig)
+        val session = sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig, loginType)
         return RegistrationResult.Success(session)
     }
 
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 e0bcde2296369032d1140b01a51e0cfa831f1a55..35c066dea897ec316294d960b04f4f10b3423fdf 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
@@ -71,6 +71,7 @@ import org.matrix.android.sdk.api.session.room.model.Membership
 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.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.internal.crypto.actions.MegolmSessionDataImporter
 import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
@@ -81,6 +82,7 @@ import org.matrix.android.sdk.internal.crypto.algorithms.olm.MXOlmEncryptionFact
 import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService
 import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
 import org.matrix.android.sdk.internal.crypto.model.MXKey.Companion.KEY_SIGNED_CURVE_25519_TYPE
+import org.matrix.android.sdk.internal.crypto.model.SessionInfo
 import org.matrix.android.sdk.internal.crypto.model.toRest
 import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
@@ -818,7 +820,7 @@ internal class DefaultCryptoService @Inject constructor(
                 EventType.SEND_SECRET -> {
                     onSecretSendReceived(event)
                 }
-                EventType.ROOM_KEY_WITHHELD -> {
+                in EventType.ROOM_KEY_WITHHELD.values -> {
                     onKeyWithHeldReceived(event)
                 }
                 else -> {
@@ -867,7 +869,7 @@ internal class DefaultCryptoService @Inject constructor(
                 senderKey = withHeldContent.senderKey,
                 fromDevice = withHeldContent.fromDevice,
                 event = Event(
-                        type = EventType.ROOM_KEY_WITHHELD,
+                        type = EventType.ROOM_KEY_WITHHELD.stable,
                         senderId = senderId,
                         content = event.getClearContent()
                 )
@@ -963,8 +965,12 @@ internal class DefaultCryptoService @Inject constructor(
     private fun onRoomHistoryVisibilityEvent(roomId: String, event: Event) {
         if (!event.isStateEvent()) return
         val eventContent = event.content.toModel<RoomHistoryVisibilityContent>()
-        eventContent?.historyVisibility?.let {
-            cryptoStore.setShouldEncryptForInvitedMembers(roomId, it != RoomHistoryVisibility.JOINED)
+        val historyVisibility = eventContent?.historyVisibility
+        if (historyVisibility == null) {
+            cryptoStore.setShouldShareHistory(roomId, false)
+        } else {
+            cryptoStore.setShouldEncryptForInvitedMembers(roomId, historyVisibility != RoomHistoryVisibility.JOINED)
+            cryptoStore.setShouldShareHistory(roomId, historyVisibility.shouldShareHistory())
         }
     }
 
@@ -1111,6 +1117,10 @@ internal class DefaultCryptoService @Inject constructor(
 
     override fun isKeyGossipingEnabled() = cryptoStore.isKeyGossipingEnabled()
 
+    override fun isShareKeysOnInviteEnabled() = cryptoStore.isShareKeysOnInviteEnabled()
+
+    override fun enableShareKeyOnInvite(enable: Boolean) = cryptoStore.enableShareKeyOnInvite(enable)
+
     /**
      * Tells whether the client should ever send encrypted messages to unverified devices.
      * The default value is false.
@@ -1335,6 +1345,30 @@ internal class DefaultCryptoService @Inject constructor(
         }
     }
 
+    override suspend fun sendSharedHistoryKeys(roomId: String, userId: String, sessionInfoSet: Set<SessionInfo>?) {
+        deviceListManager.downloadKeys(listOf(userId), false)
+        val userDevices = cryptoStore.getUserDeviceList(userId)
+        val sessionToShare = sessionInfoSet.orEmpty().mapNotNull { sessionInfo ->
+            // Get inbound session from sessionId and sessionKey
+            withContext(coroutineDispatchers.crypto) {
+                olmDevice.getInboundGroupSession(
+                        sessionId = sessionInfo.sessionId,
+                        senderKey = sessionInfo.senderKey,
+                        roomId = roomId
+                ).takeIf { it.wrapper.sessionData.sharedHistory }
+            }
+        }
+
+        userDevices?.forEach { deviceInfo ->
+            // Lets share the provided inbound sessions for every user device
+            sessionToShare.forEach { inboundGroupSession ->
+                val encryptor = roomEncryptorsStore.get(roomId)
+                encryptor?.shareHistoryKeysWithDevice(inboundGroupSession, deviceInfo)
+                Timber.i("## CRYPTO | Sharing inbound session")
+            }
+        }
+    }
+
     /* ==========================================================================================
      * For test only
      * ========================================================================================== */
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt
index c1d04eb22ba482fe4c7a8791233ef7730d478ad3..bc3309132a8cac906aa638d73efa8618d30fe9a2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt
@@ -18,6 +18,8 @@ package org.matrix.android.sdk.internal.crypto
 
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
 import kotlinx.coroutines.withContext
 import org.matrix.android.sdk.api.MatrixCallback
 import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
@@ -68,6 +70,7 @@ internal class EventDecryptor @Inject constructor(
             val senderKey: String?
     )
 
+    private val wedgedMutex = Mutex()
     private val wedgedDevices = mutableListOf<WedgedDeviceInfo>()
 
     /**
@@ -151,11 +154,13 @@ internal class EventDecryptor @Inject constructor(
         }
     }
 
-    private fun markOlmSessionForUnwedging(senderId: String, senderKey: String) {
-        val info = WedgedDeviceInfo(senderId, senderKey)
-        if (!wedgedDevices.contains(info)) {
-            Timber.tag(loggerTag.value).d("Marking device from $senderId key:$senderKey as wedged")
-            wedgedDevices.add(info)
+    private suspend fun markOlmSessionForUnwedging(senderId: String, senderKey: String) {
+        wedgedMutex.withLock {
+            val info = WedgedDeviceInfo(senderId, senderKey)
+            if (!wedgedDevices.contains(info)) {
+                Timber.tag(loggerTag.value).d("Marking device from $senderId key:$senderKey as wedged")
+                wedgedDevices.add(info)
+            }
         }
     }
 
@@ -167,15 +172,17 @@ internal class EventDecryptor @Inject constructor(
         Timber.tag(loggerTag.value).v("Unwedging:  ${wedgedDevices.size} are wedged")
         // get the one that should be retried according to rate limit
         val now = clock.epochMillis()
-        val toUnwedge = wedgedDevices.filter {
-            val lastForcedDate = lastNewSessionForcedDates[it] ?: 0
-            if (now - lastForcedDate < DefaultCryptoService.CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
-                Timber.tag(loggerTag.value).d("Unwedging, New session for $it already forced with device at $lastForcedDate")
-                return@filter false
+        val toUnwedge = wedgedMutex.withLock {
+            wedgedDevices.filter {
+                val lastForcedDate = lastNewSessionForcedDates[it] ?: 0
+                if (now - lastForcedDate < DefaultCryptoService.CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
+                    Timber.tag(loggerTag.value).d("Unwedging, New session for $it already forced with device at $lastForcedDate")
+                    return@filter false
+                }
+                // let's already mark that we tried now
+                lastNewSessionForcedDates[it] = now
+                true
             }
-            // let's already mark that we tried now
-            lastNewSessionForcedDates[it] = now
-            true
         }
 
         if (toUnwedge.isEmpty()) {
@@ -230,6 +237,15 @@ internal class EventDecryptor @Inject constructor(
                                     withContext(coroutineDispatchers.io) {
                                         sendToDeviceTask.executeRetry(sendToDeviceParams, remainingRetry = SEND_TO_DEVICE_RETRY_COUNT)
                                     }
+
+                                    deviceList.values.flatten().forEach { deviceInfo ->
+                                        wedgedMutex.withLock {
+                                            wedgedDevices.removeAll {
+                                                it.senderKey == deviceInfo.identityKey() &&
+                                                        it.userId == deviceInfo.userId
+                                            }
+                                        }
+                                    }
                                 } catch (failure: Throwable) {
                                     deviceList.flatMap { it.value }.joinToString { it.shortDebugString() }.let {
                                         Timber.tag(loggerTag.value).e(failure, "## Failed to unwedge devices: $it}")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt
index e4d322cadd3a148c840f33235b4ae905de57c0ae..39dfb72149047bd09ca80ca7182d3512f6064612 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt
@@ -21,17 +21,14 @@ import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.sync.Mutex
 import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
-import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.logger.LoggerTag
-import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
+import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import timber.log.Timber
-import java.util.Timer
-import java.util.TimerTask
 import javax.inject.Inject
 
 internal data class InboundGroupSessionHolder(
-        val wrapper: OlmInboundGroupSessionWrapper2,
+        val wrapper: MXInboundMegolmSessionWrapper,
         val mutex: Mutex = Mutex()
 )
 
@@ -57,18 +54,13 @@ internal class InboundGroupSessionStore @Inject constructor(
             if (oldValue != null) {
                 cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
                     Timber.tag(loggerTag.value).v("## Inbound: entryRemoved ${oldValue.wrapper.roomId}-${oldValue.wrapper.senderKey}")
-                    store.storeInboundGroupSessions(listOf(oldValue).map { it.wrapper })
-                    oldValue.wrapper.olmInboundGroupSession?.releaseSession()
+                    // store.storeInboundGroupSessions(listOf(oldValue).map { it.wrapper })
+                    oldValue.wrapper.session.releaseSession()
                 }
             }
         }
     }
 
-    private val timer = Timer()
-    private var timerTask: TimerTask? = null
-
-    private val dirtySession = mutableListOf<OlmInboundGroupSessionWrapper2>()
-
     @Synchronized
     fun clear() {
         sessionCache.evictAll()
@@ -90,12 +82,11 @@ internal class InboundGroupSessionStore @Inject constructor(
     @Synchronized
     fun replaceGroupSession(old: InboundGroupSessionHolder, new: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
         Timber.tag(loggerTag.value).v("## Replacing outdated session ${old.wrapper.roomId}-${old.wrapper.senderKey}")
-        dirtySession.remove(old.wrapper)
         store.removeInboundGroupSession(sessionId, senderKey)
         sessionCache.remove(CacheKey(sessionId, senderKey))
 
         // release removed session
-        old.wrapper.olmInboundGroupSession?.releaseSession()
+        old.wrapper.session.releaseSession()
 
         internalStoreGroupSession(new, sessionId, senderKey)
     }
@@ -107,33 +98,14 @@ internal class InboundGroupSessionStore @Inject constructor(
 
     private fun internalStoreGroupSession(holder: InboundGroupSessionHolder, sessionId: String, senderKey: String) {
         Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession mark as dirty ${holder.wrapper.roomId}-${holder.wrapper.senderKey}")
-        // We want to batch this a bit for performances
-        dirtySession.add(holder.wrapper)
 
         if (sessionCache[CacheKey(sessionId, senderKey)] == null) {
             // first time seen, put it in memory cache while waiting for batch insert
             // If it's already known, no need to update cache it's already there
             sessionCache.put(CacheKey(sessionId, senderKey), holder)
         }
-
-        timerTask?.cancel()
-        timerTask = object : TimerTask() {
-            override fun run() {
-                batchSave()
-            }
-        }
-        timer.schedule(timerTask!!, 300)
-    }
-
-    @Synchronized
-    private fun batchSave() {
-        val toSave = mutableListOf<OlmInboundGroupSessionWrapper2>().apply { addAll(dirtySession) }
-        dirtySession.clear()
         cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
-            Timber.tag(loggerTag.value).v("## Inbound: getInboundGroupSession batching save of ${toSave.size}")
-            tryOrNull {
-                store.storeInboundGroupSessions(toSave)
-            }
+            store.storeInboundGroupSessions(listOf(holder.wrapper))
         }
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingKeyRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingKeyRequestManager.kt
index 7f36224daec5f66fef7c29588b130428ac5b1f7d..729b4481e42c55b24fae2968de9c2507756bab60 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingKeyRequestManager.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingKeyRequestManager.kt
@@ -315,7 +315,7 @@ internal class IncomingKeyRequestManager @Inject constructor(
         )
 
         val params = SendToDeviceTask.Params(
-                EventType.ROOM_KEY_WITHHELD,
+                EventType.ROOM_KEY_WITHHELD.stable,
                 MXUsersDevicesMap<Any>().apply {
                     setObject(request.requestingUserId, request.requestingDeviceId, withHeldContent)
                 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt
index 24b6fd166f316866affb67f718b8e68bd1bf37c7..96ccba51dc10094967157ff4267884a9d35975dc 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt
@@ -27,7 +27,8 @@ import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE
 import org.matrix.android.sdk.api.util.JsonDict
 import org.matrix.android.sdk.internal.crypto.algorithms.megolm.MXOutboundSessionInfo
 import org.matrix.android.sdk.internal.crypto.algorithms.megolm.SharedWithHelper
-import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
+import org.matrix.android.sdk.internal.crypto.model.InboundGroupSessionData
+import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper
 import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import org.matrix.android.sdk.internal.di.MoshiProvider
@@ -38,6 +39,7 @@ import org.matrix.android.sdk.internal.util.convertToUTF8
 import org.matrix.android.sdk.internal.util.time.Clock
 import org.matrix.olm.OlmAccount
 import org.matrix.olm.OlmException
+import org.matrix.olm.OlmInboundGroupSession
 import org.matrix.olm.OlmMessage
 import org.matrix.olm.OlmOutboundGroupSession
 import org.matrix.olm.OlmSession
@@ -514,8 +516,9 @@ internal class MXOlmDevice @Inject constructor(
             return MXOutboundSessionInfo(
                     sessionId = sessionId,
                     sharedWithHelper = SharedWithHelper(roomId, sessionId, store),
-                    clock,
-                    restoredOutboundGroupSession.creationTime
+                    clock = clock,
+                    creationTime = restoredOutboundGroupSession.creationTime,
+                    sharedHistory = restoredOutboundGroupSession.sharedHistory
             )
         }
         return null
@@ -598,6 +601,7 @@ internal class MXOlmDevice @Inject constructor(
      * @param forwardingCurve25519KeyChain Devices involved in forwarding this session to us.
      * @param keysClaimed Other keys the sender claims.
      * @param exportFormat true if the megolm keys are in export format
+     * @param sharedHistory MSC3061, this key is sharable on invite
      * @return true if the operation succeeds.
      */
     fun addInboundGroupSession(
@@ -607,31 +611,39 @@ internal class MXOlmDevice @Inject constructor(
             senderKey: String,
             forwardingCurve25519KeyChain: List<String>,
             keysClaimed: Map<String, String>,
-            exportFormat: Boolean
+            exportFormat: Boolean,
+            sharedHistory: Boolean
     ): AddSessionResult {
-        val candidateSession = OlmInboundGroupSessionWrapper2(sessionKey, exportFormat)
+        val candidateSession = tryOrNull("Failed to create inbound session in room $roomId") {
+            if (exportFormat) {
+                OlmInboundGroupSession.importSession(sessionKey)
+            } else {
+                OlmInboundGroupSession(sessionKey)
+            }
+        }
+
         val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) }
         val existingSession = existingSessionHolder?.wrapper
         // If we have an existing one we should check if the new one is not better
         if (existingSession != null) {
             Timber.tag(loggerTag.value).d("## addInboundGroupSession() check if known session is better than candidate session")
             try {
-                val existingFirstKnown = existingSession.firstKnownIndex ?: return AddSessionResult.NotImported.also {
+                val existingFirstKnown = tryOrNull { existingSession.session.firstKnownIndex } ?: return AddSessionResult.NotImported.also {
                     // This is quite unexpected, could throw if native was released?
                     Timber.tag(loggerTag.value).e("## addInboundGroupSession() null firstKnownIndex on existing session")
-                    candidateSession.olmInboundGroupSession?.releaseSession()
+                    candidateSession?.releaseSession()
                     // Probably should discard it?
                 }
-                val newKnownFirstIndex = candidateSession.firstKnownIndex
+                val newKnownFirstIndex = tryOrNull("Failed to get candidate first known index") { candidateSession?.firstKnownIndex }
                 // If our existing session is better we keep it
                 if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) {
                     Timber.tag(loggerTag.value).d("## addInboundGroupSession() : ignore session our is better $senderKey/$sessionId")
-                    candidateSession.olmInboundGroupSession?.releaseSession()
+                    candidateSession?.releaseSession()
                     return AddSessionResult.NotImportedHigherIndex(newKnownFirstIndex.toInt())
                 }
             } catch (failure: Throwable) {
                 Timber.tag(loggerTag.value).e("## addInboundGroupSession() Failed to add inbound: ${failure.localizedMessage}")
-                candidateSession.olmInboundGroupSession?.releaseSession()
+                candidateSession?.releaseSession()
                 return AddSessionResult.NotImported
             }
         }
@@ -639,36 +651,42 @@ internal class MXOlmDevice @Inject constructor(
         Timber.tag(loggerTag.value).d("## addInboundGroupSession() : Candidate session should be added $senderKey/$sessionId")
 
         // sanity check on the new session
-        val candidateOlmInboundSession = candidateSession.olmInboundGroupSession
-        if (null == candidateOlmInboundSession) {
+        if (null == candidateSession) {
             Timber.tag(loggerTag.value).e("## addInboundGroupSession : invalid session <null>")
             return AddSessionResult.NotImported
         }
 
         try {
-            if (candidateOlmInboundSession.sessionIdentifier() != sessionId) {
+            if (candidateSession.sessionIdentifier() != sessionId) {
                 Timber.tag(loggerTag.value).e("## addInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
-                candidateOlmInboundSession.releaseSession()
+                candidateSession.releaseSession()
                 return AddSessionResult.NotImported
             }
         } catch (e: Throwable) {
-            candidateOlmInboundSession.releaseSession()
+            candidateSession.releaseSession()
             Timber.tag(loggerTag.value).e(e, "## addInboundGroupSession : sessionIdentifier() failed")
             return AddSessionResult.NotImported
         }
 
-        candidateSession.senderKey = senderKey
-        candidateSession.roomId = roomId
-        candidateSession.keysClaimed = keysClaimed
-        candidateSession.forwardingCurve25519KeyChain = forwardingCurve25519KeyChain
+        val candidateSessionData = InboundGroupSessionData(
+                senderKey = senderKey,
+                roomId = roomId,
+                keysClaimed = keysClaimed,
+                forwardingCurve25519KeyChain = forwardingCurve25519KeyChain,
+                sharedHistory = sharedHistory,
+        )
 
+        val wrapper = MXInboundMegolmSessionWrapper(
+                candidateSession,
+                candidateSessionData
+        )
         if (existingSession != null) {
-            inboundGroupSessionStore.replaceGroupSession(existingSessionHolder, InboundGroupSessionHolder(candidateSession), sessionId, senderKey)
+            inboundGroupSessionStore.replaceGroupSession(existingSessionHolder, InboundGroupSessionHolder(wrapper), sessionId, senderKey)
         } else {
-            inboundGroupSessionStore.storeInBoundGroupSession(InboundGroupSessionHolder(candidateSession), sessionId, senderKey)
+            inboundGroupSessionStore.storeInBoundGroupSession(InboundGroupSessionHolder(wrapper), sessionId, senderKey)
         }
 
-        return AddSessionResult.Imported(candidateSession.firstKnownIndex?.toInt() ?: 0)
+        return AddSessionResult.Imported(candidateSession.firstKnownIndex.toInt())
     }
 
     /**
@@ -677,41 +695,22 @@ internal class MXOlmDevice @Inject constructor(
      * @param megolmSessionsData the megolm sessions data
      * @return the successfully imported sessions.
      */
-    fun importInboundGroupSessions(megolmSessionsData: List<MegolmSessionData>): List<OlmInboundGroupSessionWrapper2> {
-        val sessions = ArrayList<OlmInboundGroupSessionWrapper2>(megolmSessionsData.size)
+    fun importInboundGroupSessions(megolmSessionsData: List<MegolmSessionData>): List<MXInboundMegolmSessionWrapper> {
+        val sessions = ArrayList<MXInboundMegolmSessionWrapper>(megolmSessionsData.size)
 
         for (megolmSessionData in megolmSessionsData) {
             val sessionId = megolmSessionData.sessionId ?: continue
             val senderKey = megolmSessionData.senderKey ?: continue
             val roomId = megolmSessionData.roomId
 
-            var candidateSessionToImport: OlmInboundGroupSessionWrapper2? = null
-
-            try {
-                candidateSessionToImport = OlmInboundGroupSessionWrapper2(megolmSessionData)
-            } catch (e: Exception) {
-                Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
-            }
-
-            // sanity check
-            if (candidateSessionToImport?.olmInboundGroupSession == null) {
-                Timber.tag(loggerTag.value).e("## importInboundGroupSession : invalid session")
-                continue
-            }
-
-            val candidateOlmInboundGroupSession = candidateSessionToImport.olmInboundGroupSession
-            try {
-                if (candidateOlmInboundGroupSession?.sessionIdentifier() != sessionId) {
-                    Timber.tag(loggerTag.value).e("## importInboundGroupSession : ERROR: Mismatched group session ID from senderKey: $senderKey")
-                    candidateOlmInboundGroupSession?.releaseSession()
-                    continue
-                }
-            } catch (e: Exception) {
-                Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession : sessionIdentifier() failed")
-                candidateOlmInboundGroupSession?.releaseSession()
+            val candidateSessionToImport = try {
+                MXInboundMegolmSessionWrapper.newFromMegolmData(megolmSessionData, true)
+            } catch (e: Throwable) {
+                Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession() : Failed to import session $senderKey/$sessionId")
                 continue
             }
 
+            val candidateOlmInboundGroupSession = candidateSessionToImport.session
             val existingSessionHolder = tryOrNull { getInboundGroupSession(sessionId, senderKey, roomId) }
             val existingSession = existingSessionHolder?.wrapper
 
@@ -721,16 +720,16 @@ internal class MXOlmDevice @Inject constructor(
                 sessions.add(candidateSessionToImport)
             } else {
                 Timber.tag(loggerTag.value).e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
-                val existingFirstKnown = tryOrNull { existingSession.firstKnownIndex }
-                val candidateFirstKnownIndex = tryOrNull { candidateSessionToImport.firstKnownIndex }
+                val existingFirstKnown = tryOrNull { existingSession.session.firstKnownIndex }
+                val candidateFirstKnownIndex = tryOrNull { candidateSessionToImport.session.firstKnownIndex }
 
                 if (existingFirstKnown == null || candidateFirstKnownIndex == null) {
                     // should not happen?
-                    candidateSessionToImport.olmInboundGroupSession?.releaseSession()
+                    candidateSessionToImport.session.releaseSession()
                     Timber.tag(loggerTag.value)
                             .w("## importInboundGroupSession() : Can't check session null index $existingFirstKnown/$candidateFirstKnownIndex")
                 } else {
-                    if (existingFirstKnown <= candidateSessionToImport.firstKnownIndex!!) {
+                    if (existingFirstKnown <= candidateFirstKnownIndex) {
                         // Ignore this, keep existing
                         candidateOlmInboundGroupSession.releaseSession()
                     } else {
@@ -774,8 +773,7 @@ internal class MXOlmDevice @Inject constructor(
     ): OlmDecryptionResult {
         val sessionHolder = getInboundGroupSession(sessionId, senderKey, roomId)
         val wrapper = sessionHolder.wrapper
-        val inboundGroupSession = wrapper.olmInboundGroupSession
-                ?: throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, "Session is null")
+        val inboundGroupSession = wrapper.session
         if (roomId != wrapper.roomId) {
             // Check that the room id matches the original one for the session. This stops
             // the HS pretending a message was targeting a different room.
@@ -810,7 +808,6 @@ internal class MXOlmDevice @Inject constructor(
             }
             replayAttackMap[messageIndexKey] = eventId
         }
-        inboundGroupSessionStore.storeInBoundGroupSession(sessionHolder, sessionId, senderKey)
         val payload = try {
             val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
             val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage)
@@ -822,9 +819,9 @@ internal class MXOlmDevice @Inject constructor(
 
         return OlmDecryptionResult(
                 payload,
-                wrapper.keysClaimed,
+                wrapper.sessionData.keysClaimed,
                 senderKey,
-                wrapper.forwardingCurve25519KeyChain
+                wrapper.sessionData.forwardingCurve25519KeyChain
         )
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MegolmSessionData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MegolmSessionData.kt
index f6bc9a9148af14285a23d32ed0d32645eddf92f2..ca0bdc8a0ebafd989793ed12c35de9b3cae80d1f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MegolmSessionData.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MegolmSessionData.kt
@@ -69,5 +69,13 @@ internal data class MegolmSessionData(
          * Devices which forwarded this session to us (normally empty).
          */
         @Json(name = "forwarding_curve25519_key_chain")
-        val forwardingCurve25519KeyChain: List<String>? = null
+        val forwardingCurve25519KeyChain: List<String>? = null,
+
+        /**
+         * Flag that indicates whether or not the current inboundSession will be shared to
+         * invited users to decrypt past messages.
+         */
+        // When this feature lands in spec name = shared_history should be used
+        @Json(name = "org.matrix.msc3061.shared_history")
+        val sharedHistory: Boolean = false,
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingKeyRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingKeyRequestManager.kt
index 6b22cc09d6e8ff37f3ec348b81168a032faa9546..810699d933953860fe3e53e9b9f90516b7e22c1a 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingKeyRequestManager.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingKeyRequestManager.kt
@@ -437,7 +437,10 @@ internal class OutgoingKeyRequestManager @Inject constructor(
         if (perSessionBackupQueryRateLimiter.tryFromBackupIfPossible(sessionId, roomId)) {
             // let's see what's the index
             val knownIndex = tryOrNull {
-                inboundGroupSessionStore.getInboundGroupSession(sessionId, request.requestBody?.senderKey ?: "")?.wrapper?.firstKnownIndex
+                inboundGroupSessionStore.getInboundGroupSession(sessionId, request.requestBody?.senderKey ?: "")
+                        ?.wrapper
+                        ?.session
+                        ?.firstKnownIndex
             }
             if (knownIndex != null && knownIndex <= request.fromIndex) {
                 // we found the key in backup with good enough index, so we can just mark as cancelled, no need to send request
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt
index f6ab96aee68033c95460b670ecc194b403003e89..a624b92a198c89b40ae2e9271609bcb9293e14d7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/MegolmSessionDataImporter.kt
@@ -84,8 +84,9 @@ internal class MegolmSessionDataImporter @Inject constructor(
                             megolmSessionData.senderKey ?: "",
                             tryOrNull {
                                 olmInboundGroupSessionWrappers
-                                        .firstOrNull { it.olmInboundGroupSession?.sessionIdentifier() == megolmSessionData.sessionId }
-                                        ?.firstKnownIndex?.toInt()
+                                        .firstOrNull { it.session.sessionIdentifier() == megolmSessionData.sessionId }
+                                        ?.session?.firstKnownIndex
+                                        ?.toInt()
                             } ?: 0
                     )
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXEncrypting.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXEncrypting.kt
index 73ce5a5004b10ca227b8d50362fe6881546dbb69..1454f5b4868d2f38f9c8a866d0401851abcd49e6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXEncrypting.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXEncrypting.kt
@@ -16,7 +16,9 @@
 
 package org.matrix.android.sdk.internal.crypto.algorithms
 
+import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
 import org.matrix.android.sdk.api.session.events.model.Content
+import org.matrix.android.sdk.internal.crypto.InboundGroupSessionHolder
 
 /**
  * An interface for encrypting data.
@@ -32,4 +34,6 @@ internal interface IMXEncrypting {
      * @return the encrypted content
      */
     suspend fun encryptEventContent(eventContent: Content, eventType: String, userIds: List<String>): Content
+
+    suspend fun shareHistoryKeysWithDevice(inboundSessionWrapper: InboundGroupSessionHolder, deviceInfo: CryptoDeviceInfo) {}
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt
index 141d6f74cd0b23525356af59790b459dbeceba3e..410b74e19f1d6b155875241bff96d50e79e584c7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt
@@ -17,6 +17,7 @@
 package org.matrix.android.sdk.internal.crypto.algorithms.megolm
 
 import dagger.Lazy
+import org.matrix.android.sdk.api.MatrixConfiguration
 import org.matrix.android.sdk.api.logger.LoggerTag
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
 import org.matrix.android.sdk.api.session.crypto.NewSessionListener
@@ -41,6 +42,7 @@ internal class MXMegolmDecryption(
         private val olmDevice: MXOlmDevice,
         private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
         private val cryptoStore: IMXCryptoStore,
+        private val matrixConfiguration: MatrixConfiguration,
         private val liveEventManager: Lazy<StreamEventsManager>
 ) : IMXDecrypting {
 
@@ -240,13 +242,14 @@ internal class MXMegolmDecryption(
 
         Timber.tag(loggerTag.value).i("onRoomKeyEvent addInboundGroupSession ${roomKeyContent.sessionId}")
         val addSessionResult = olmDevice.addInboundGroupSession(
-                roomKeyContent.sessionId,
-                roomKeyContent.sessionKey,
-                roomKeyContent.roomId,
-                senderKey,
-                forwardingCurve25519KeyChain,
-                keysClaimed,
-                exportFormat
+                sessionId = roomKeyContent.sessionId,
+                sessionKey = roomKeyContent.sessionKey,
+                roomId = roomKeyContent.roomId,
+                senderKey = senderKey,
+                forwardingCurve25519KeyChain = forwardingCurve25519KeyChain,
+                keysClaimed = keysClaimed,
+                exportFormat = exportFormat,
+                sharedHistory = roomKeyContent.getSharedKey()
         )
 
         when (addSessionResult) {
@@ -296,6 +299,14 @@ internal class MXMegolmDecryption(
         }
     }
 
+    /**
+     * Returns boolean shared key flag, if enabled with respect to matrix configuration.
+     */
+    private fun RoomKeyContent.getSharedKey(): Boolean {
+        if (!cryptoStore.isShareKeysOnInviteEnabled()) return false
+        return sharedHistory ?: false
+    }
+
     /**
      * Check if the some messages can be decrypted with a new session.
      *
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt
index 81a6fb28c06e4fc30bdc24c7b5f62fbc5995c1c0..38edbb7430a2bd932dd86c855a237cc4482ad6d1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt
@@ -17,6 +17,7 @@
 package org.matrix.android.sdk.internal.crypto.algorithms.megolm
 
 import dagger.Lazy
+import org.matrix.android.sdk.api.MatrixConfiguration
 import org.matrix.android.sdk.internal.crypto.MXOlmDevice
 import org.matrix.android.sdk.internal.crypto.OutgoingKeyRequestManager
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
@@ -27,6 +28,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor(
         private val olmDevice: MXOlmDevice,
         private val outgoingKeyRequestManager: OutgoingKeyRequestManager,
         private val cryptoStore: IMXCryptoStore,
+        private val matrixConfiguration: MatrixConfiguration,
         private val eventsManager: Lazy<StreamEventsManager>
 ) {
 
@@ -35,6 +37,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor(
                 olmDevice,
                 outgoingKeyRequestManager,
                 cryptoStore,
+                matrixConfiguration,
                 eventsManager
         )
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt
index 7bfbae6edf5ab194de33dec1054e3fef5b3c2d2b..771b5f9a626e6eaf17322a2cb87638f9ac34f850 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt
@@ -32,6 +32,7 @@ import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
 import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
 import org.matrix.android.sdk.internal.crypto.DeviceListManager
+import org.matrix.android.sdk.internal.crypto.InboundGroupSessionHolder
 import org.matrix.android.sdk.internal.crypto.MXOlmDevice
 import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
 import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
@@ -151,14 +152,27 @@ internal class MXMegolmEncryption(
                 "ed25519" to olmDevice.deviceEd25519Key!!
         )
 
+        val sharedHistory = cryptoStore.shouldShareHistory(roomId)
+        Timber.tag(loggerTag.value).v("prepareNewSessionInRoom() as sharedHistory $sharedHistory")
         olmDevice.addInboundGroupSession(
-                sessionId!!, olmDevice.getSessionKey(sessionId)!!, roomId, olmDevice.deviceCurve25519Key!!,
-                emptyList(), keysClaimedMap, false
+                sessionId = sessionId!!,
+                sessionKey = olmDevice.getSessionKey(sessionId)!!,
+                roomId = roomId,
+                senderKey = olmDevice.deviceCurve25519Key!!,
+                forwardingCurve25519KeyChain = emptyList(),
+                keysClaimed = keysClaimedMap,
+                exportFormat = false,
+                sharedHistory = sharedHistory
         )
 
         defaultKeysBackupService.maybeBackupKeys()
 
-        return MXOutboundSessionInfo(sessionId, SharedWithHelper(roomId, sessionId, cryptoStore), clock)
+        return MXOutboundSessionInfo(
+                sessionId = sessionId,
+                sharedWithHelper = SharedWithHelper(roomId, sessionId, cryptoStore),
+                clock = clock,
+                sharedHistory = sharedHistory
+        )
     }
 
     /**
@@ -172,6 +186,8 @@ internal class MXMegolmEncryption(
         if (session == null ||
                 // Need to make a brand new session?
                 session.needsRotation(sessionRotationPeriodMsgs, sessionRotationPeriodMs) ||
+                // Is there a room history visibility change since the last outboundSession
+                cryptoStore.shouldShareHistory(roomId) != session.sharedHistory ||
                 // Determine if we have shared with anyone we shouldn't have
                 session.sharedWithTooManyDevices(devicesInRoom)) {
             Timber.tag(loggerTag.value).d("roomId:$roomId Starting new megolm session because we need to rotate.")
@@ -231,26 +247,29 @@ internal class MXMegolmEncryption(
     /**
      * Share the device keys of a an user.
      *
-     * @param session the session info
+     * @param sessionInfo the session info
      * @param devicesByUser the devices map
      */
     private suspend fun shareUserDevicesKey(
-            session: MXOutboundSessionInfo,
+            sessionInfo: MXOutboundSessionInfo,
             devicesByUser: Map<String, List<CryptoDeviceInfo>>
     ) {
-        val sessionKey = olmDevice.getSessionKey(session.sessionId)
-        val chainIndex = olmDevice.getMessageIndex(session.sessionId)
-
-        val submap = HashMap<String, Any>()
-        submap["algorithm"] = MXCRYPTO_ALGORITHM_MEGOLM
-        submap["room_id"] = roomId
-        submap["session_id"] = session.sessionId
-        submap["session_key"] = sessionKey!!
-        submap["chain_index"] = chainIndex
-
-        val payload = HashMap<String, Any>()
-        payload["type"] = EventType.ROOM_KEY
-        payload["content"] = submap
+        val sessionKey = olmDevice.getSessionKey(sessionInfo.sessionId) ?: return Unit.also {
+            Timber.tag(loggerTag.value).v("shareUserDevicesKey() Failed to share session, failed to export")
+        }
+        val chainIndex = olmDevice.getMessageIndex(sessionInfo.sessionId)
+
+        val payload = mapOf(
+                "type" to EventType.ROOM_KEY,
+                "content" to mapOf(
+                        "algorithm" to MXCRYPTO_ALGORITHM_MEGOLM,
+                        "room_id" to roomId,
+                        "session_id" to sessionInfo.sessionId,
+                        "session_key" to sessionKey,
+                        "chain_index" to chainIndex,
+                        "org.matrix.msc3061.shared_history" to sessionInfo.sharedHistory
+                )
+        )
 
         var t0 = clock.epochMillis()
         Timber.tag(loggerTag.value).v("shareUserDevicesKey() : starts")
@@ -292,7 +311,7 @@ internal class MXMegolmEncryption(
         // for dead devices on every message.
         for ((_, devicesToShareWith) in devicesByUser) {
             for (deviceInfo in devicesToShareWith) {
-                session.sharedWithHelper.markedSessionAsShared(deviceInfo, chainIndex)
+                sessionInfo.sharedWithHelper.markedSessionAsShared(deviceInfo, chainIndex)
                 // XXX is it needed to add it to the audit trail?
                 // For now decided that no, we are more interested by forward trail
             }
@@ -300,8 +319,8 @@ internal class MXMegolmEncryption(
 
         if (haveTargets) {
             t0 = clock.epochMillis()
-            Timber.tag(loggerTag.value).i("shareUserDevicesKey() ${session.sessionId} : has target")
-            Timber.tag(loggerTag.value).d("sending to device room key for ${session.sessionId} to ${contentMap.toDebugString()}")
+            Timber.tag(loggerTag.value).i("shareUserDevicesKey() ${sessionInfo.sessionId} : has target")
+            Timber.tag(loggerTag.value).d("sending to device room key for ${sessionInfo.sessionId} to ${contentMap.toDebugString()}")
             val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap)
             try {
                 withContext(coroutineDispatchers.io) {
@@ -310,7 +329,7 @@ internal class MXMegolmEncryption(
                 Timber.tag(loggerTag.value).i("shareUserDevicesKey() : sendToDevice succeeds after ${clock.epochMillis() - t0} ms")
             } catch (failure: Throwable) {
                 // What to do here...
-                Timber.tag(loggerTag.value).e("shareUserDevicesKey() : Failed to share <${session.sessionId}>")
+                Timber.tag(loggerTag.value).e("shareUserDevicesKey() : Failed to share <${sessionInfo.sessionId}>")
             }
         } else {
             Timber.tag(loggerTag.value).i("shareUserDevicesKey() : no need to share key")
@@ -320,7 +339,7 @@ internal class MXMegolmEncryption(
             // XXX offload?, as they won't read the message anyhow?
             notifyKeyWithHeld(
                     noOlmToNotify,
-                    session.sessionId,
+                    sessionInfo.sessionId,
                     olmDevice.deviceCurve25519Key,
                     WithHeldCode.NO_OLM
             )
@@ -346,7 +365,7 @@ internal class MXMegolmEncryption(
                 fromDevice = myDeviceId
         )
         val params = SendToDeviceTask.Params(
-                EventType.ROOM_KEY_WITHHELD,
+                EventType.ROOM_KEY_WITHHELD.stable,
                 MXUsersDevicesMap<Any>().apply {
                     targets.forEach {
                         setObject(it.userId, it.deviceId, withHeldContent)
@@ -514,6 +533,51 @@ internal class MXMegolmEncryption(
         }
     }
 
+    @Throws
+    override suspend fun shareHistoryKeysWithDevice(inboundSessionWrapper: InboundGroupSessionHolder, deviceInfo: CryptoDeviceInfo) {
+        require(inboundSessionWrapper.wrapper.sessionData.sharedHistory) { "This key can't be shared" }
+        Timber.tag(loggerTag.value).i("process shareHistoryKeys for ${inboundSessionWrapper.wrapper.safeSessionId} to ${deviceInfo.shortDebugString()}")
+        val userId = deviceInfo.userId
+        val deviceId = deviceInfo.deviceId
+        val devicesByUser = mapOf(userId to listOf(deviceInfo))
+        val usersDeviceMap = try {
+            ensureOlmSessionsForDevicesAction.handle(devicesByUser)
+        } catch (failure: Throwable) {
+            Timber.tag(loggerTag.value).i(failure, "process shareHistoryKeys failed to ensure olm")
+            // process anyway?
+            null
+        }
+        val olmSessionResult = usersDeviceMap?.getObject(userId, deviceId)
+        if (olmSessionResult?.sessionId == null) {
+            Timber.tag(loggerTag.value).w("shareHistoryKeys: no session with this device, probably because there were no one-time keys")
+            return
+        }
+
+        val export = inboundSessionWrapper.mutex.withLock {
+            inboundSessionWrapper.wrapper.exportKeys()
+        } ?: return Unit.also {
+            Timber.tag(loggerTag.value).e("shareHistoryKeys: failed to export group session ${inboundSessionWrapper.wrapper.safeSessionId}")
+        }
+
+        val payloadJson = mapOf(
+                "type" to EventType.FORWARDED_ROOM_KEY,
+                "content" to export
+        )
+
+        val encodedPayload =
+                withContext(coroutineDispatchers.computation) {
+                    messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
+                }
+        val sendToDeviceMap = MXUsersDevicesMap<Any>()
+        sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
+        Timber.tag(loggerTag.value)
+                .d("shareHistoryKeys() : sending session ${inboundSessionWrapper.wrapper.safeSessionId} to ${deviceInfo.shortDebugString()}")
+        val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
+        withContext(coroutineDispatchers.io) {
+            sendToDeviceTask.execute(sendToDeviceParams)
+        }
+    }
+
     data class DeviceInRoomInfo(
             val allowedDevices: MXUsersDevicesMap<CryptoDeviceInfo> = MXUsersDevicesMap(),
             val withHeldDevices: MXUsersDevicesMap<WithHeldCode> = MXUsersDevicesMap()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt
index 28d925d8fd5cf3690af003798442a82b00da9483..e0caa0d9a5900c494d40b06d7db1835f132a5c07 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXOutboundSessionInfo.kt
@@ -28,6 +28,7 @@ internal class MXOutboundSessionInfo(
         private val clock: Clock,
         // When the session was created
         private val creationTime: Long = clock.epochMillis(),
+        val sharedHistory: Boolean = false
 ) {
 
     // Number of times this session has been used
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt
index 5eaa106af3cc732c38953816dd1c94f505aa97ca..8691c087798bcb415a4162cb3745604f90c4200b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt
@@ -24,8 +24,10 @@ import androidx.annotation.WorkerThread
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.withLock
 import kotlinx.coroutines.withContext
 import org.matrix.android.sdk.api.MatrixCallback
+import org.matrix.android.sdk.api.MatrixConfiguration
 import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.auth.data.Credentials
 import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
@@ -50,6 +52,7 @@ import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult
 import org.matrix.android.sdk.api.session.crypto.model.ImportRoomKeysResult
 import org.matrix.android.sdk.api.util.awaitCallback
 import org.matrix.android.sdk.api.util.fromBase64
+import org.matrix.android.sdk.internal.crypto.InboundGroupSessionStore
 import org.matrix.android.sdk.internal.crypto.MXOlmDevice
 import org.matrix.android.sdk.internal.crypto.MegolmSessionData
 import org.matrix.android.sdk.internal.crypto.ObjectSigner
@@ -71,7 +74,7 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetRoomSessionsDa
 import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetSessionsDataTask
 import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreSessionsDataTask
 import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.UpdateKeysBackupVersionTask
-import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
+import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
 import org.matrix.android.sdk.internal.di.MoshiProvider
@@ -118,6 +121,8 @@ internal class DefaultKeysBackupService @Inject constructor(
         private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask,
         // Task executor
         private val taskExecutor: TaskExecutor,
+        private val matrixConfiguration: MatrixConfiguration,
+        private val inboundGroupSessionStore: InboundGroupSessionStore,
         private val coroutineDispatchers: MatrixCoroutineDispatchers,
         private val cryptoCoroutineScope: CoroutineScope
 ) : KeysBackupService {
@@ -1316,7 +1321,7 @@ internal class DefaultKeysBackupService @Inject constructor(
 
                 olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper ->
                     val roomId = olmInboundGroupSessionWrapper.roomId ?: return@forEach
-                    val olmInboundGroupSession = olmInboundGroupSessionWrapper.olmInboundGroupSession ?: return@forEach
+                    val olmInboundGroupSession = olmInboundGroupSessionWrapper.session
 
                     try {
                         encryptGroupSession(olmInboundGroupSessionWrapper)
@@ -1344,6 +1349,8 @@ internal class DefaultKeysBackupService @Inject constructor(
 
                                         // Mark keys as backed up
                                         cryptoStore.markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers)
+                                        // we can release the sessions now
+                                        olmInboundGroupSessionWrappers.onEach { it.session.releaseSession() }
 
                                         if (olmInboundGroupSessionWrappers.size < KEY_BACKUP_SEND_KEYS_MAX_COUNT) {
                                             Timber.v("backupKeys: All keys have been backed up")
@@ -1405,19 +1412,29 @@ internal class DefaultKeysBackupService @Inject constructor(
 
     @VisibleForTesting
     @WorkerThread
-    fun encryptGroupSession(olmInboundGroupSessionWrapper: OlmInboundGroupSessionWrapper2): KeyBackupData? {
+    suspend fun encryptGroupSession(olmInboundGroupSessionWrapper: MXInboundMegolmSessionWrapper): KeyBackupData? {
+        olmInboundGroupSessionWrapper.safeSessionId ?: return null
+        olmInboundGroupSessionWrapper.senderKey ?: return null
         // Gather information for each key
-        val device = olmInboundGroupSessionWrapper.senderKey?.let { cryptoStore.deviceWithIdentityKey(it) }
+        val device = cryptoStore.deviceWithIdentityKey(olmInboundGroupSessionWrapper.senderKey)
 
         // Build the m.megolm_backup.v1.curve25519-aes-sha2 data as defined at
         // https://github.com/uhoreg/matrix-doc/blob/e2e_backup/proposals/1219-storing-megolm-keys-serverside.md#mmegolm_backupv1curve25519-aes-sha2-key-format
-        val sessionData = olmInboundGroupSessionWrapper.exportKeys() ?: return null
+        val sessionData = inboundGroupSessionStore
+                .getInboundGroupSession(olmInboundGroupSessionWrapper.safeSessionId, olmInboundGroupSessionWrapper.senderKey)
+                ?.let {
+                    withContext(coroutineDispatchers.computation) {
+                        it.mutex.withLock { it.wrapper.exportKeys() }
+                    }
+                }
+                ?: return null
         val sessionBackupData = mapOf(
                 "algorithm" to sessionData.algorithm,
                 "sender_key" to sessionData.senderKey,
                 "sender_claimed_keys" to sessionData.senderClaimedKeys,
                 "forwarding_curve25519_key_chain" to (sessionData.forwardingCurve25519KeyChain.orEmpty()),
-                "session_key" to sessionData.sessionKey
+                "session_key" to sessionData.sessionKey,
+                "org.matrix.msc3061.shared_history" to sessionData.sharedHistory
         )
 
         val json = MoshiProvider.providesMoshi()
@@ -1425,7 +1442,9 @@ internal class DefaultKeysBackupService @Inject constructor(
                 .toJson(sessionBackupData)
 
         val encryptedSessionBackupData = try {
-            backupOlmPkEncryption?.encrypt(json)
+            withContext(coroutineDispatchers.computation) {
+                backupOlmPkEncryption?.encrypt(json)
+            }
         } catch (e: OlmException) {
             Timber.e(e, "OlmException")
             null
@@ -1435,14 +1454,14 @@ internal class DefaultKeysBackupService @Inject constructor(
         // Build backup data for that key
         return KeyBackupData(
                 firstMessageIndex = try {
-                    olmInboundGroupSessionWrapper.olmInboundGroupSession?.firstKnownIndex ?: 0
+                    olmInboundGroupSessionWrapper.session.firstKnownIndex
                 } catch (e: OlmException) {
                     Timber.e(e, "OlmException")
                     0L
                 },
-                forwardedCount = olmInboundGroupSessionWrapper.forwardingCurve25519KeyChain.orEmpty().size,
+                forwardedCount = olmInboundGroupSessionWrapper.sessionData.forwardingCurve25519KeyChain.orEmpty().size,
                 isVerified = device?.isVerified == true,
-
+                sharedHistory = olmInboundGroupSessionWrapper.getSharedKey(),
                 sessionData = mapOf(
                         "ciphertext" to encryptedSessionBackupData.mCipherText,
                         "mac" to encryptedSessionBackupData.mMac,
@@ -1451,6 +1470,14 @@ internal class DefaultKeysBackupService @Inject constructor(
         )
     }
 
+    /**
+     * Returns boolean shared key flag, if enabled with respect to matrix configuration.
+     */
+    private fun MXInboundMegolmSessionWrapper.getSharedKey(): Boolean {
+        if (!cryptoStore.isShareKeysOnInviteEnabled()) return false
+        return sessionData.sharedHistory
+    }
+
     @VisibleForTesting
     @WorkerThread
     fun decryptKeyBackupData(keyBackupData: KeyBackupData, sessionId: String, roomId: String, decryption: OlmPkDecryption): MegolmSessionData? {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeyBackupData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeyBackupData.kt
index 5c3d0c12b0ec16ac06ee87fb658f566e68c52524..1817b18e2ac6d82452509bf36977a6a1734b2cb2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeyBackupData.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/rest/KeyBackupData.kt
@@ -50,5 +50,12 @@ internal data class KeyBackupData(
          * Algorithm-dependent data.
          */
         @Json(name = "session_data")
-        val sessionData: JsonDict
+        val sessionData: JsonDict,
+
+        /**
+         * Flag that indicates whether or not the current inboundSession will be shared to
+         * invited users to decrypt past messages.
+         */
+        @Json(name = "org.matrix.msc3061.shared_history")
+        val sharedHistory: Boolean = false
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/InboundGroupSessionData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/InboundGroupSessionData.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2ce36aa209ba7f90d922652b431f7890187f429d
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/InboundGroupSessionData.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.crypto.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class InboundGroupSessionData(
+
+        /** The room in which this session is used. */
+        @Json(name = "room_id")
+        var roomId: String? = null,
+
+        /** The base64-encoded curve25519 key of the sender. */
+        @Json(name = "sender_key")
+        var senderKey: String? = null,
+
+        /** Other keys the sender claims. */
+        @Json(name = "keys_claimed")
+        var keysClaimed: Map<String, String>? = null,
+
+        /** Devices which forwarded this session to us (normally emty). */
+        @Json(name = "forwarding_curve25519_key_chain")
+        var forwardingCurve25519KeyChain: List<String>? = emptyList(),
+
+        /** Not yet used, will be in backup v2
+        val untrusted?: Boolean = false */
+
+        /**
+         * Flag that indicates whether or not the current inboundSession will be shared to
+         * invited users to decrypt past messages.
+         */
+        @Json(name = "shared_history")
+        val sharedHistory: Boolean = false,
+
+        )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXInboundMegolmSessionWrapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXInboundMegolmSessionWrapper.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2772b3483594e1e58205386a6e6526ac27d34bbd
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXInboundMegolmSessionWrapper.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.crypto.model
+
+import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
+import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.internal.crypto.MegolmSessionData
+import org.matrix.olm.OlmInboundGroupSession
+import timber.log.Timber
+
+data class MXInboundMegolmSessionWrapper(
+        // olm object
+        val session: OlmInboundGroupSession,
+        // data about the session
+        val sessionData: InboundGroupSessionData
+) {
+    // shortcut
+    val roomId = sessionData.roomId
+    val senderKey = sessionData.senderKey
+    val safeSessionId = tryOrNull("Fail to get megolm session Id") { session.sessionIdentifier() }
+
+    /**
+     * Export the inbound group session keys.
+     * @param index the index to export. If null, the first known index will be used
+     * @return the inbound group session as MegolmSessionData if the operation succeeds
+     */
+    internal fun exportKeys(index: Long? = null): MegolmSessionData? {
+        return try {
+            val keysClaimed = sessionData.keysClaimed ?: return null
+            val wantedIndex = index ?: session.firstKnownIndex
+
+            MegolmSessionData(
+                    senderClaimedEd25519Key = sessionData.keysClaimed?.get("ed25519"),
+                    forwardingCurve25519KeyChain = sessionData.forwardingCurve25519KeyChain?.toList().orEmpty(),
+                    sessionKey = session.export(wantedIndex),
+                    senderClaimedKeys = keysClaimed,
+                    roomId = sessionData.roomId,
+                    sessionId = session.sessionIdentifier(),
+                    senderKey = senderKey,
+                    algorithm = MXCRYPTO_ALGORITHM_MEGOLM,
+                    sharedHistory = sessionData.sharedHistory
+            )
+        } catch (e: Exception) {
+            Timber.e(e, "## Failed to export megolm : sessionID ${tryOrNull { session.sessionIdentifier() }} failed")
+            null
+        }
+    }
+
+    companion object {
+
+        /**
+         * @exportFormat true if the megolm keys are in export format
+         *    (ie, they lack an ed25519 signature)
+         */
+        @Throws
+        internal fun newFromMegolmData(megolmSessionData: MegolmSessionData, exportFormat: Boolean): MXInboundMegolmSessionWrapper {
+            val exportedKey = megolmSessionData.sessionKey ?: throw IllegalArgumentException("key data not found")
+            val inboundSession = if (exportFormat) {
+                OlmInboundGroupSession.importSession(exportedKey)
+            } else {
+                OlmInboundGroupSession(exportedKey)
+            }
+                    .also {
+                        if (it.sessionIdentifier() != megolmSessionData.sessionId) {
+                            it.releaseSession()
+                            throw IllegalStateException("Mismatched group session Id")
+                        }
+                    }
+            val data = InboundGroupSessionData(
+                    roomId = megolmSessionData.roomId,
+                    senderKey = megolmSessionData.senderKey,
+                    keysClaimed = megolmSessionData.senderClaimedKeys,
+                    forwardingCurve25519KeyChain = megolmSessionData.forwardingCurve25519KeyChain,
+                    sharedHistory = megolmSessionData.sharedHistory,
+            )
+
+            return MXInboundMegolmSessionWrapper(
+                    inboundSession,
+                    data
+            )
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper2.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper2.kt
index 289c169d6d142ddfaee52badaed4f97f551c0ec4..600fcb10033ba5e26a51e8fb93135e5fef909c5c 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper2.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OlmInboundGroupSessionWrapper2.kt
@@ -26,6 +26,8 @@ import java.io.Serializable
  * This class adds more context to a OlmInboundGroupSession object.
  * This allows additional checks. The class implements Serializable so that the context can be stored.
  */
+// Note used anymore, just for database migration
+// Deprecated("Use MXInboundMegolmSessionWrapper")
 internal class OlmInboundGroupSessionWrapper2 : Serializable {
 
     // The associated olm inbound group session.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OutboundGroupSessionWrapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OutboundGroupSessionWrapper.kt
index 4ac87f44cef04359dfb5e1e64b4ead7947ae4d73..5a6d1f4bc12fe9934e57dc153ce9adfda912a509 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OutboundGroupSessionWrapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/OutboundGroupSessionWrapper.kt
@@ -20,5 +20,9 @@ import org.matrix.olm.OlmOutboundGroupSession
 
 internal data class OutboundGroupSessionWrapper(
         val outboundGroupSession: OlmOutboundGroupSession,
-        val creationTime: Long
+        val creationTime: Long,
+        /**
+         * As per MSC 3061, declares if this key could be shared when inviting a new user to the room.
+         */
+        val sharedHistory: Boolean = false
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupUsers.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/SessionInfo.kt
similarity index 53%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupUsers.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/SessionInfo.kt
index 9dd1339157ae8967fd0e1137726cb14b756e2fad..b3a2ba4dfe7273128089083d6bf147e4a4caab06 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupUsers.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/SessionInfo.kt
@@ -1,11 +1,11 @@
 /*
- * Copyright 2020 The Matrix.org Foundation C.I.C.
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
- * http://www.apache.org/licenses/LICENSE-2.0
+ *     http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
@@ -14,13 +14,9 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.session.group.model
+package org.matrix.android.sdk.internal.crypto.model
 
-import com.squareup.moshi.Json
-import com.squareup.moshi.JsonClass
-
-@JsonClass(generateAdapter = true)
-internal data class GroupUsers(
-        @Json(name = "total_user_count_estimate") val totalUserCountEstimate: Int,
-        @Json(name = "chunk") val users: List<GroupUser> = emptyList()
+data class SessionInfo(
+        val sessionId: String,
+        val senderKey: String
 )
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 b5b8d8e97446d08d5cf5494738824f3066f0dd49..0413fc730c71305974b78130106d2bb057f8e870 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
@@ -35,7 +35,7 @@ import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
 import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
 import org.matrix.android.sdk.api.util.Optional
-import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
+import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper
 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.db.model.KeysBackupDataEntity
@@ -64,7 +64,15 @@ internal interface IMXCryptoStore {
      *
      * @return the list of all known group sessions, to export them.
      */
-    fun getInboundGroupSessions(): List<OlmInboundGroupSessionWrapper2>
+    fun getInboundGroupSessions(): List<MXInboundMegolmSessionWrapper>
+
+    /**
+     * Retrieve the known inbound group sessions for the specified room.
+     *
+     * @param roomId The roomId that the sessions will be returned
+     * @return the list of all known group sessions, for the provided roomId
+     */
+    fun getInboundGroupSessions(roomId: String): List<MXInboundMegolmSessionWrapper>
 
     /**
      * @return true to unilaterally blacklist all unverified devices.
@@ -90,6 +98,20 @@ internal interface IMXCryptoStore {
 
     fun isKeyGossipingEnabled(): Boolean
 
+    /**
+     * As per MSC3061.
+     * If true will make it possible to share part of e2ee room history
+     * on invite depending on the room visibility setting.
+     */
+    fun enableShareKeyOnInvite(enable: Boolean)
+
+    /**
+     * As per MSC3061.
+     * If true will make it possible to share part of e2ee room history
+     * on invite depending on the room visibility setting.
+     */
+    fun isShareKeysOnInviteEnabled(): Boolean
+
     /**
      * Provides the rooms ids list in which the messages are not encrypted for the unverified devices.
      *
@@ -250,6 +272,17 @@ internal interface IMXCryptoStore {
 
     fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean)
 
+    fun shouldShareHistory(roomId: String): Boolean
+
+    /**
+     * Sets a boolean flag that will determine whether or not room history (existing inbound sessions)
+     * will be shared to new user invites.
+     *
+     * @param roomId the room id
+     * @param shouldShareHistory The boolean flag
+     */
+    fun setShouldShareHistory(roomId: String, shouldShareHistory: Boolean)
+
     /**
      * Store a session between the logged-in user and another device.
      *
@@ -290,7 +323,7 @@ internal interface IMXCryptoStore {
      *
      * @param sessions the inbound group sessions to store.
      */
-    fun storeInboundGroupSessions(sessions: List<OlmInboundGroupSessionWrapper2>)
+    fun storeInboundGroupSessions(sessions: List<MXInboundMegolmSessionWrapper>)
 
     /**
      * Retrieve an inbound group session.
@@ -299,7 +332,17 @@ internal interface IMXCryptoStore {
      * @param senderKey the base64-encoded curve25519 key of the sender.
      * @return an inbound group session.
      */
-    fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2?
+    fun getInboundGroupSession(sessionId: String, senderKey: String): MXInboundMegolmSessionWrapper?
+
+    /**
+     * Retrieve an inbound group session, filtering shared history.
+     *
+     * @param sessionId the session identifier.
+     * @param senderKey the base64-encoded curve25519 key of the sender.
+     * @param sharedHistory filter inbound session with respect to shared history field
+     * @return an inbound group session.
+     */
+    fun getInboundGroupSession(sessionId: String, senderKey: String, sharedHistory: Boolean): MXInboundMegolmSessionWrapper?
 
     /**
      * Get the current outbound group session for this encrypted room.
@@ -333,7 +376,7 @@ internal interface IMXCryptoStore {
      *
      * @param olmInboundGroupSessionWrappers the sessions
      */
-    fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List<OlmInboundGroupSessionWrapper2>)
+    fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List<MXInboundMegolmSessionWrapper>)
 
     /**
      * Retrieve inbound group sessions that are not yet backed up.
@@ -341,7 +384,7 @@ internal interface IMXCryptoStore {
      * @param limit the maximum number of sessions to return.
      * @return an array of non backed up inbound group sessions.
      */
-    fun inboundGroupSessionsToBackup(limit: Int): List<OlmInboundGroupSessionWrapper2>
+    fun inboundGroupSessionsToBackup(limit: Int): List<MXInboundMegolmSessionWrapper>
 
     /**
      * Number of stored inbound group sessions.
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 028d8f73f9556bcdb8002704e8d107b3e36d8e84..f5468634cbf8854beb0551635b71193ee0868c8f 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
@@ -50,7 +50,7 @@ import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldCo
 import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
 import org.matrix.android.sdk.api.util.Optional
 import org.matrix.android.sdk.api.util.toOptional
-import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
+import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper
 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
@@ -657,12 +657,28 @@ internal class RealmCryptoStore @Inject constructor(
                 ?: false
     }
 
+    override fun shouldShareHistory(roomId: String): Boolean {
+        if (!isShareKeysOnInviteEnabled()) return false
+        return doWithRealm(realmConfiguration) {
+            CryptoRoomEntity.getById(it, roomId)?.shouldShareHistory
+        }
+                ?: false
+    }
+
     override fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean) {
         doRealmTransaction(realmConfiguration) {
             CryptoRoomEntity.getOrCreate(it, roomId).shouldEncryptForInvitedMembers = shouldEncryptForInvitedMembers
         }
     }
 
+    override fun setShouldShareHistory(roomId: String, shouldShareHistory: Boolean) {
+        Timber.tag(loggerTag.value)
+                .v("setShouldShareHistory for room $roomId is $shouldShareHistory")
+        doRealmTransaction(realmConfiguration) {
+            CryptoRoomEntity.getOrCreate(it, roomId).shouldShareHistory = shouldShareHistory
+        }
+    }
+
     override fun storeSession(olmSessionWrapper: OlmSessionWrapper, deviceKey: String) {
         var sessionIdentifier: String? = null
 
@@ -727,54 +743,61 @@ internal class RealmCryptoStore @Inject constructor(
         }
     }
 
-    override fun storeInboundGroupSessions(sessions: List<OlmInboundGroupSessionWrapper2>) {
+    override fun storeInboundGroupSessions(sessions: List<MXInboundMegolmSessionWrapper>) {
         if (sessions.isEmpty()) {
             return
         }
 
         doRealmTransaction(realmConfiguration) { realm ->
-            sessions.forEach { session ->
-                var sessionIdentifier: String? = null
+            sessions.forEach { wrapper ->
 
-                try {
-                    sessionIdentifier = session.olmInboundGroupSession?.sessionIdentifier()
+                val sessionIdentifier = try {
+                    wrapper.session.sessionIdentifier()
                 } catch (e: OlmException) {
                     Timber.e(e, "## storeInboundGroupSession() : sessionIdentifier failed")
+                    return@forEach
                 }
 
-                if (sessionIdentifier != null) {
-                    val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, session.senderKey)
+//                    val shouldShareHistory = session.roomId?.let { roomId ->
+//                        CryptoRoomEntity.getById(realm, roomId)?.shouldShareHistory
+//                    } ?: false
+                val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionIdentifier, wrapper.sessionData.senderKey)
 
-                    val existing = realm.where<OlmInboundGroupSessionEntity>()
-                            .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
-                            .findFirst()
+                val existing = realm.where<OlmInboundGroupSessionEntity>()
+                        .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
+                        .findFirst()
 
-                    if (existing != null) {
-                        // we want to keep the existing backup status
-                        existing.putInboundGroupSession(session)
-                    } else {
-                        val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply {
-                            primaryKey = key
-                            sessionId = sessionIdentifier
-                            senderKey = session.senderKey
-                            putInboundGroupSession(session)
-                        }
-
-                        realm.insertOrUpdate(realmOlmInboundGroupSession)
-                    }
+                val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply {
+                    primaryKey = key
+                    store(wrapper)
+                    backedUp = existing?.backedUp ?: false
                 }
+
+                Timber.v("## CRYPTO | shouldShareHistory: ${wrapper.sessionData.sharedHistory} for $key")
+                realm.insertOrUpdate(realmOlmInboundGroupSession)
             }
         }
     }
 
-    override fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2? {
+    override fun getInboundGroupSession(sessionId: String, senderKey: String): MXInboundMegolmSessionWrapper? {
         val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey)
 
+        return doWithRealm(realmConfiguration) { realm ->
+            realm.where<OlmInboundGroupSessionEntity>()
+                    .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
+                    .findFirst()
+                    ?.toModel()
+        }
+    }
+
+    override fun getInboundGroupSession(sessionId: String, senderKey: String, sharedHistory: Boolean): MXInboundMegolmSessionWrapper? {
+        val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey)
         return doWithRealm(realmConfiguration) {
             it.where<OlmInboundGroupSessionEntity>()
+                    .equalTo(OlmInboundGroupSessionEntityFields.SHARED_HISTORY, sharedHistory)
                     .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key)
                     .findFirst()
-                    ?.getInboundGroupSession()
+                    ?.toModel()
         }
     }
 
@@ -786,7 +809,8 @@ internal class RealmCryptoStore @Inject constructor(
                         entity.getOutboundGroupSession()?.let {
                             OutboundGroupSessionWrapper(
                                     it,
-                                    entity.creationTime ?: 0
+                                    entity.creationTime ?: 0,
+                                    entity.shouldShareHistory
                             )
                         }
                     }
@@ -806,6 +830,8 @@ internal class RealmCryptoStore @Inject constructor(
                 if (outboundGroupSession != null) {
                     val info = realm.createObject(OutboundGroupSessionInfoEntity::class.java).apply {
                         creationTime = clock.epochMillis()
+                        // Store the room history visibility on the outbound session creation
+                        shouldShareHistory = entity.shouldShareHistory
                         putOutboundGroupSession(outboundGroupSession)
                     }
                     entity.outboundSessionInfo = info
@@ -814,17 +840,32 @@ internal class RealmCryptoStore @Inject constructor(
         }
     }
 
+//    override fun needsRotationDueToVisibilityChange(roomId: String): Boolean {
+//        return doWithRealm(realmConfiguration) { realm ->
+//            CryptoRoomEntity.getById(realm, roomId)?.let { entity ->
+//                entity.shouldShareHistory != entity.outboundSessionInfo?.shouldShareHistory
+//            }
+//        } ?: false
+//    }
+
     /**
      * Note: the result will be only use to export all the keys and not to use the OlmInboundGroupSessionWrapper2,
      * so there is no need to use or update `inboundGroupSessionToRelease` for native memory management.
      */
-    override fun getInboundGroupSessions(): List<OlmInboundGroupSessionWrapper2> {
-        return doWithRealm(realmConfiguration) {
-            it.where<OlmInboundGroupSessionEntity>()
+    override fun getInboundGroupSessions(): List<MXInboundMegolmSessionWrapper> {
+        return doWithRealm(realmConfiguration) { realm ->
+            realm.where<OlmInboundGroupSessionEntity>()
                     .findAll()
-                    .mapNotNull { inboundGroupSessionEntity ->
-                        inboundGroupSessionEntity.getInboundGroupSession()
-                    }
+                    .mapNotNull { it.toModel() }
+        }
+    }
+
+    override fun getInboundGroupSessions(roomId: String): List<MXInboundMegolmSessionWrapper> {
+        return doWithRealm(realmConfiguration) { realm ->
+            realm.where<OlmInboundGroupSessionEntity>()
+                    .equalTo(OlmInboundGroupSessionEntityFields.ROOM_ID, roomId)
+                    .findAll()
+                    .mapNotNull { it.toModel() }
         }
     }
 
@@ -885,7 +926,7 @@ internal class RealmCryptoStore @Inject constructor(
         }
     }
 
-    override fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List<OlmInboundGroupSessionWrapper2>) {
+    override fun markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers: List<MXInboundMegolmSessionWrapper>) {
         if (olmInboundGroupSessionWrappers.isEmpty()) {
             return
         }
@@ -893,10 +934,13 @@ internal class RealmCryptoStore @Inject constructor(
         doRealmTransaction(realmConfiguration) { realm ->
             olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper ->
                 try {
-                    val sessionIdentifier = olmInboundGroupSessionWrapper.olmInboundGroupSession?.sessionIdentifier()
+                    val sessionIdentifier =
+                            tryOrNull("Failed to get session identifier") {
+                                olmInboundGroupSessionWrapper.session.sessionIdentifier()
+                            } ?: return@forEach
                     val key = OlmInboundGroupSessionEntity.createPrimaryKey(
                             sessionIdentifier,
-                            olmInboundGroupSessionWrapper.senderKey
+                            olmInboundGroupSessionWrapper.sessionData.senderKey
                     )
 
                     val existing = realm.where<OlmInboundGroupSessionEntity>()
@@ -909,9 +953,7 @@ internal class RealmCryptoStore @Inject constructor(
                         // ... might be in cache but not yet persisted, create a record to persist backedup state
                         val realmOlmInboundGroupSession = OlmInboundGroupSessionEntity().apply {
                             primaryKey = key
-                            sessionId = sessionIdentifier
-                            senderKey = olmInboundGroupSessionWrapper.senderKey
-                            putInboundGroupSession(olmInboundGroupSessionWrapper)
+                            store(olmInboundGroupSessionWrapper)
                             backedUp = true
                         }
 
@@ -924,15 +966,13 @@ internal class RealmCryptoStore @Inject constructor(
         }
     }
 
-    override fun inboundGroupSessionsToBackup(limit: Int): List<OlmInboundGroupSessionWrapper2> {
+    override fun inboundGroupSessionsToBackup(limit: Int): List<MXInboundMegolmSessionWrapper> {
         return doWithRealm(realmConfiguration) {
             it.where<OlmInboundGroupSessionEntity>()
                     .equalTo(OlmInboundGroupSessionEntityFields.BACKED_UP, false)
                     .limit(limit.toLong())
                     .findAll()
-                    .mapNotNull { inboundGroupSession ->
-                        inboundGroupSession.getInboundGroupSession()
-                    }
+                    .mapNotNull { it.toModel() }
         }
     }
 
@@ -973,6 +1013,18 @@ internal class RealmCryptoStore @Inject constructor(
         } ?: false
     }
 
+    override fun isShareKeysOnInviteEnabled(): Boolean {
+        return doWithRealm(realmConfiguration) {
+            it.where<CryptoMetadataEntity>().findFirst()?.enableKeyForwardingOnInvite
+        } ?: false
+    }
+
+    override fun enableShareKeyOnInvite(enable: Boolean) {
+        doRealmTransaction(realmConfiguration) {
+            it.where<CryptoMetadataEntity>().findFirst()?.enableKeyForwardingOnInvite = enable
+        }
+    }
+
     override fun setDeviceKeysUploaded(uploaded: Boolean) {
         doRealmTransaction(realmConfiguration) {
             it.where<CryptoMetadataEntity>().findFirst()?.deviceKeysSentToServer = uploaded
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt
index 02c2a27decd9ae64f27246599e66d233ecf0724b..c36d572da63024fe55622e1a283ca9c029d76b0b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt
@@ -17,7 +17,6 @@
 package org.matrix.android.sdk.internal.crypto.store.db
 
 import io.realm.DynamicRealm
-import io.realm.RealmMigration
 import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo001Legacy
 import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo002Legacy
 import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo003RiotX
@@ -34,13 +33,23 @@ import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo
 import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo014
 import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo015
 import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo016
+import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo017
+import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
 import org.matrix.android.sdk.internal.util.time.Clock
-import timber.log.Timber
 import javax.inject.Inject
 
+/**
+ * Schema version history:
+ *  0, 1, 2: legacy Riot-Android;
+ *  3: migrate to RiotX schema;
+ *  4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6).
+ */
 internal class RealmCryptoStoreMigration @Inject constructor(
         private val clock: Clock,
-) : RealmMigration {
+) : MatrixRealmMigration(
+        dbName = "Crypto",
+        schemaVersion = 17L,
+) {
     /**
      * Forces all RealmCryptoStoreMigration instances to be equal.
      * Avoids Realm throwing when multiple instances of the migration are set.
@@ -48,14 +57,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(
     override fun equals(other: Any?) = other is RealmCryptoStoreMigration
     override fun hashCode() = 5000
 
-    // 0, 1, 2: legacy Riot-Android
-    // 3: migrate to RiotX schema
-    // 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6)
-    val schemaVersion = 16L
-
-    override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
-        Timber.d("Migrating Realm Crypto from $oldVersion to $newVersion")
-
+    override fun doMigrate(realm: DynamicRealm, oldVersion: Long) {
         if (oldVersion < 1) MigrateCryptoTo001Legacy(realm).perform()
         if (oldVersion < 2) MigrateCryptoTo002Legacy(realm).perform()
         if (oldVersion < 3) MigrateCryptoTo003RiotX(realm).perform()
@@ -72,5 +74,6 @@ internal class RealmCryptoStoreMigration @Inject constructor(
         if (oldVersion < 14) MigrateCryptoTo014(realm).perform()
         if (oldVersion < 15) MigrateCryptoTo015(realm).perform()
         if (oldVersion < 16) MigrateCryptoTo016(realm).perform()
+        if (oldVersion < 17) MigrateCryptoTo017(realm).perform()
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo017.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo017.kt
new file mode 100644
index 0000000000000000000000000000000000000000..8904c412cd8324b9efee3251596b2f8fe40991d5
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo017.kt
@@ -0,0 +1,101 @@
+/*
+ * 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.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.internal.crypto.model.InboundGroupSessionData
+import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm
+import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMetadataEntityFields
+import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFields
+import org.matrix.android.sdk.internal.crypto.store.db.model.OlmInboundGroupSessionEntityFields
+import org.matrix.android.sdk.internal.crypto.store.db.model.OutboundGroupSessionInfoEntityFields
+import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm
+import org.matrix.android.sdk.internal.di.MoshiProvider
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+import timber.log.Timber
+
+/**
+ * Version 17L enhance OlmInboundGroupSessionEntity to support shared history for MSC3061.
+ * Also migrates how megolm session are stored to avoid additional serialized frozen class.
+ */
+internal class MigrateCryptoTo017(realm: DynamicRealm) : RealmMigrator(realm, 17) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.get("CryptoRoomEntity")
+                ?.addField(CryptoRoomEntityFields.SHOULD_SHARE_HISTORY, Boolean::class.java)?.transform {
+                    // We don't have access to the session database to check for the state here and set the good value.
+                    // But for now as it's behind a lab flag, will set to false and force initial sync when enabled
+                    it.setBoolean(CryptoRoomEntityFields.SHOULD_SHARE_HISTORY, false)
+                }
+
+        realm.schema.get("OutboundGroupSessionInfoEntity")
+                ?.addField(OutboundGroupSessionInfoEntityFields.SHOULD_SHARE_HISTORY, Boolean::class.java)?.transform {
+                    // We don't have access to the session database to check for the state here and set the good value.
+                    // But for now as it's behind a lab flag, will set to false and force initial sync when enabled
+                    it.setBoolean(OutboundGroupSessionInfoEntityFields.SHOULD_SHARE_HISTORY, false)
+                }
+
+        realm.schema.get("CryptoMetadataEntity")
+                ?.addField(CryptoMetadataEntityFields.ENABLE_KEY_FORWARDING_ON_INVITE, Boolean::class.java)
+                ?.transform { obj ->
+                    // default to false
+                    obj.setBoolean(CryptoMetadataEntityFields.ENABLE_KEY_FORWARDING_ON_INVITE, false)
+                }
+
+        val moshiAdapter = MoshiProvider.providesMoshi().adapter(InboundGroupSessionData::class.java)
+
+        realm.schema.get("OlmInboundGroupSessionEntity")
+                ?.addField(OlmInboundGroupSessionEntityFields.SHARED_HISTORY, Boolean::class.java)
+                ?.addField(OlmInboundGroupSessionEntityFields.ROOM_ID, String::class.java)
+                ?.addField(OlmInboundGroupSessionEntityFields.INBOUND_GROUP_SESSION_DATA_JSON, String::class.java)
+                ?.addField(OlmInboundGroupSessionEntityFields.SERIALIZED_OLM_INBOUND_GROUP_SESSION, String::class.java)
+                ?.transform { dynamicObject ->
+                    try {
+                        // we want to convert the old wrapper frozen class into a
+                        // map of sessionData & the pickled session herself
+                        dynamicObject.getString(OlmInboundGroupSessionEntityFields.OLM_INBOUND_GROUP_SESSION_DATA)?.let { oldData ->
+                            val oldWrapper = tryOrNull("Failed to convert megolm inbound group data") {
+                                @Suppress("DEPRECATION")
+                                deserializeFromRealm<org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2?>(oldData)
+                            }
+                            val groupSession = oldWrapper?.olmInboundGroupSession
+                                    ?: return@transform Unit.also {
+                                        Timber.w("Failed to migrate megolm session, no olmInboundGroupSession")
+                                    }
+                            // now convert to new data
+                            val data = InboundGroupSessionData(
+                                    senderKey = oldWrapper.senderKey,
+                                    roomId = oldWrapper.roomId,
+                                    keysClaimed = oldWrapper.keysClaimed,
+                                    forwardingCurve25519KeyChain = oldWrapper.forwardingCurve25519KeyChain,
+                                    sharedHistory = false,
+                            )
+
+                            dynamicObject.setString(OlmInboundGroupSessionEntityFields.INBOUND_GROUP_SESSION_DATA_JSON, moshiAdapter.toJson(data))
+                            dynamicObject.setString(OlmInboundGroupSessionEntityFields.SERIALIZED_OLM_INBOUND_GROUP_SESSION, serializeForRealm(groupSession))
+
+                            // denormalized fields
+                            dynamicObject.setString(OlmInboundGroupSessionEntityFields.ROOM_ID, oldWrapper.roomId)
+                            dynamicObject.setBoolean(OlmInboundGroupSessionEntityFields.SHARED_HISTORY, false)
+                        }
+                    } catch (failure: Throwable) {
+                        Timber.e(failure, "Failed to migrate megolm session")
+                    }
+                }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMetadataEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMetadataEntity.kt
index 63ed0e537e45eaddd479b3223e43c19178a2b3f3..88708f824eaa41e3cc0f3a2530510beecc0ca2c7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMetadataEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMetadataEntity.kt
@@ -35,6 +35,11 @@ internal open class CryptoMetadataEntity(
         var globalBlacklistUnverifiedDevices: Boolean = false,
         // setting to enable or disable key gossiping
         var globalEnableKeyGossiping: Boolean = true,
+
+        // MSC3061: Sharing room keys for past messages
+        // If set to true key history will be shared to invited users with respect to room setting
+        var enableKeyForwardingOnInvite: Boolean = false,
+
         // The keys backup version currently used. Null means no backup.
         var backupVersion: String? = null,
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoRoomEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoRoomEntity.kt
index 114a596964fa7228ed15a5db8490f164b321a906..be575861632fc669e97a60d37e17a4e19ea07e73 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoRoomEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoRoomEntity.kt
@@ -24,6 +24,8 @@ internal open class CryptoRoomEntity(
         var algorithm: String? = null,
         var shouldEncryptForInvitedMembers: Boolean? = null,
         var blacklistUnverifiedDevices: Boolean = false,
+        // Determines whether or not room history should be shared on new member invites
+        var shouldShareHistory: Boolean = false,
         // Store the current outbound session for this room,
         // to avoid re-create and re-share at each startup (if rotation not needed..)
         // This is specific to megolm but not sure how to model it better
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt
index a4f6c279acb964fd1697847a0edc7c9f3b545831..62ab73e379e9158238f9bc734ca878ae6de11321 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt
@@ -18,9 +18,12 @@ package org.matrix.android.sdk.internal.crypto.store.db.model
 
 import io.realm.RealmObject
 import io.realm.annotations.PrimaryKey
-import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
+import org.matrix.android.sdk.internal.crypto.model.InboundGroupSessionData
+import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper
 import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm
 import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm
+import org.matrix.android.sdk.internal.di.MoshiProvider
+import org.matrix.olm.OlmInboundGroupSession
 import timber.log.Timber
 
 internal fun OlmInboundGroupSessionEntity.Companion.createPrimaryKey(sessionId: String?, senderKey: String?) = "$sessionId|$senderKey"
@@ -28,27 +31,83 @@ internal fun OlmInboundGroupSessionEntity.Companion.createPrimaryKey(sessionId:
 internal open class OlmInboundGroupSessionEntity(
         // Combined value to build a primary key
         @PrimaryKey var primaryKey: String? = null,
+
+        // denormalization for faster querying (these fields are in the inboundGroupSessionDataJson)
         var sessionId: String? = null,
         var senderKey: String? = null,
-        // olmInboundGroupSessionData contains Json
+        var roomId: String? = null,
+
+        // Deprecated, used for migration / olmInboundGroupSessionData contains Json
+        // keep it in case of problem to have a chance to recover
         var olmInboundGroupSessionData: String? = null,
+
+        // Stores the session data in an extensible format
+        // to allow to store data not yet supported for later use
+        var inboundGroupSessionDataJson: String? = null,
+
+        // The pickled session
+        var serializedOlmInboundGroupSession: String? = null,
+
+        // Flag that indicates whether or not the current inboundSession will be shared to
+        // invited users to decrypt past messages
+        var sharedHistory: Boolean = false,
         // Indicate if the key has been backed up to the homeserver
         var backedUp: Boolean = false
 ) :
         RealmObject() {
 
-    fun getInboundGroupSession(): OlmInboundGroupSessionWrapper2? {
+    fun store(wrapper: MXInboundMegolmSessionWrapper) {
+        this.serializedOlmInboundGroupSession = serializeForRealm(wrapper.session)
+        this.inboundGroupSessionDataJson = adapter.toJson(wrapper.sessionData)
+        this.roomId = wrapper.sessionData.roomId
+        this.senderKey = wrapper.sessionData.senderKey
+        this.sessionId = wrapper.session.sessionIdentifier()
+        this.sharedHistory = wrapper.sessionData.sharedHistory
+    }
+//    fun getInboundGroupSession(): OlmInboundGroupSessionWrapper2? {
+//        return try {
+//            deserializeFromRealm<OlmInboundGroupSessionWrapper2?>(olmInboundGroupSessionData)
+//        } catch (failure: Throwable) {
+//            Timber.e(failure, "## Deserialization failure")
+//            return null
+//        }
+//    }
+//
+//    fun putInboundGroupSession(olmInboundGroupSessionWrapper: OlmInboundGroupSessionWrapper2?) {
+//        olmInboundGroupSessionData = serializeForRealm(olmInboundGroupSessionWrapper)
+//    }
+
+    fun getOlmGroupSession(): OlmInboundGroupSession? {
         return try {
-            deserializeFromRealm<OlmInboundGroupSessionWrapper2?>(olmInboundGroupSessionData)
+            deserializeFromRealm(serializedOlmInboundGroupSession)
         } catch (failure: Throwable) {
             Timber.e(failure, "## Deserialization failure")
             return null
         }
     }
 
-    fun putInboundGroupSession(olmInboundGroupSessionWrapper: OlmInboundGroupSessionWrapper2?) {
-        olmInboundGroupSessionData = serializeForRealm(olmInboundGroupSessionWrapper)
+    fun getData(): InboundGroupSessionData? {
+        return try {
+            inboundGroupSessionDataJson?.let {
+                adapter.fromJson(it)
+            }
+        } catch (failure: Throwable) {
+            Timber.e(failure, "## Deserialization failure")
+            return null
+        }
     }
 
-    companion object
+    fun toModel(): MXInboundMegolmSessionWrapper? {
+        val data = getData() ?: return null
+        val session = getOlmGroupSession() ?: return null
+        return MXInboundMegolmSessionWrapper(
+                session = session,
+                sessionData = data
+        )
+    }
+
+    companion object {
+        private val adapter = MoshiProvider.providesMoshi()
+                .adapter(InboundGroupSessionData::class.java)
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutboundGroupSessionInfoEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutboundGroupSessionInfoEntity.kt
index d50db78415dad5128b9d85b1f0d4cd92915e91d0..2ebd550201fb1a5bcacbd851f236835d6399a06e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutboundGroupSessionInfoEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutboundGroupSessionInfoEntity.kt
@@ -24,7 +24,8 @@ import timber.log.Timber
 
 internal open class OutboundGroupSessionInfoEntity(
         var serializedOutboundSessionData: String? = null,
-        var creationTime: Long? = null
+        var creationTime: Long? = null,
+        var shouldShareHistory: Boolean = false
 ) : RealmObject() {
 
     fun getOutboundGroupSession(): OlmOutboundGroupSession? {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingKeyRequestEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingKeyRequestEntity.kt
index 854d148b76c66be956c91c95de583bcacd83f6c9..b10e7501d6a7066e5f5e641129e583a501f39819 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingKeyRequestEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/OutgoingKeyRequestEntity.kt
@@ -117,7 +117,7 @@ internal open class OutgoingKeyRequestEntity(
 
     private fun eventToResult(event: Event): RequestResult? {
         return when (event.getClearType()) {
-            EventType.ROOM_KEY_WITHHELD -> {
+            in EventType.ROOM_KEY_WITHHELD.values -> {
                 event.content.toModel<RoomKeyWithHeldContent>()?.code?.let {
                     RequestResult.Failure(it)
                 }
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 fbd9d245d9c1493b29b5f76d357f0e5e27b69ac3..bb14b417dd3020c89aa00037cd144239fb1d2e15 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
@@ -15,7 +15,6 @@
  */
 package org.matrix.android.sdk.internal.crypto.tasks
 
-import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
@@ -48,8 +47,12 @@ internal class DefaultSendEventTask @Inject constructor(
             params.event.roomId
                     ?.takeIf { params.encrypt }
                     ?.let { roomId ->
-                        tryOrNull {
+                        try {
                             loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId))
+                        } catch (failure: Throwable) {
+                            // send any way?
+                            // the result is that some users won't probably be able to decrypt :/
+                            Timber.w(failure, "SendEvent: failed to load members in room ${params.event.roomId}")
                         }
                     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt
index 821663bcff1f8255576454bb39e4a4ccc33dc9d3..8a805a55888b86e54356868a7176705445acb462 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt
@@ -63,7 +63,7 @@ internal class VerificationMessageProcessor @Inject constructor(
         // the message should be ignored by the receiver.
 
         if (!VerificationService.isValidRequest(event.ageLocalTs, clock.epochMillis())) return Unit.also {
-            Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated age:$event.ageLocalTs ms")
+            Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated age:${event.ageLocalTs} ms")
         }
 
         Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt
index 690ac122687c5835e38b7c673cfbd8a2a1ca3f16..5b1a4752f1464e8a3b6cf2bb03f3f7b337e987c0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/DefaultQrCodeVerificationTransaction.kt
@@ -84,7 +84,7 @@ internal class DefaultQrCodeVerificationTransaction(
         // Perform some checks
         if (otherQrCodeData.transactionId != transactionId) {
             Timber.d("## Verification QR: Invalid transaction actual ${otherQrCodeData.transactionId} expected:$transactionId")
-            cancel(CancelCode.QrCodeInvalid)
+            cancel(CancelCode.UnknownTransaction)
             return
         }
 
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 665567bf2a4d3ebdf10ee4d3f8bf3e6b3157e0ce..b733aa6fc052ffa1767d58b69f8ebaaf7ba60f88 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
@@ -17,7 +17,6 @@
 package org.matrix.android.sdk.internal.database
 
 import io.realm.DynamicRealm
-import io.realm.RealmMigration
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo001
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo002
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo003
@@ -49,13 +48,20 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo028
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo029
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo030
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo031
+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.util.Normalizer
-import timber.log.Timber
+import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
 import javax.inject.Inject
 
 internal class RealmSessionStoreMigration @Inject constructor(
         private val normalizer: Normalizer
-) : RealmMigration {
+) : MatrixRealmMigration(
+        dbName = "Session",
+        schemaVersion = 35L,
+) {
     /**
      * Forces all RealmSessionStoreMigration instances to be equal.
      * Avoids Realm throwing when multiple instances of the migration are set.
@@ -63,11 +69,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
     override fun equals(other: Any?) = other is RealmSessionStoreMigration
     override fun hashCode() = 1000
 
-    val schemaVersion = 31L
-
-    override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
-        Timber.d("Migrating Realm Session from $oldVersion to $newVersion")
-
+    override fun doMigrate(realm: DynamicRealm, oldVersion: Long) {
         if (oldVersion < 1) MigrateSessionTo001(realm).perform()
         if (oldVersion < 2) MigrateSessionTo002(realm).perform()
         if (oldVersion < 3) MigrateSessionTo003(realm).perform()
@@ -99,5 +101,9 @@ internal class RealmSessionStoreMigration @Inject constructor(
         if (oldVersion < 29) MigrateSessionTo029(realm).perform()
         if (oldVersion < 30) MigrateSessionTo030(realm).perform()
         if (oldVersion < 31) MigrateSessionTo031(realm).perform()
+        if (oldVersion < 32) MigrateSessionTo032(realm).perform()
+        if (oldVersion < 33) MigrateSessionTo033(realm).perform()
+        if (oldVersion < 34) MigrateSessionTo034(realm).perform()
+        if (oldVersion < 35) MigrateSessionTo035(realm).perform()
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt
index 234caec970df92d5d201e0fdec4146615642d151..221abe0df50d6048bc58fe014f46d26a51185a49 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ChunkEntityHelper.kt
@@ -18,7 +18,11 @@ package org.matrix.android.sdk.internal.database.helper
 
 import io.realm.Realm
 import io.realm.kotlin.createObject
+import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent
+import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
+import org.matrix.android.sdk.internal.crypto.model.SessionInfo
+import org.matrix.android.sdk.internal.database.mapper.asDomain
 import org.matrix.android.sdk.internal.database.model.ChunkEntity
 import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields
 import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
@@ -31,6 +35,7 @@ import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFie
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
 import org.matrix.android.sdk.internal.database.query.find
+import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
 import org.matrix.android.sdk.internal.database.query.getOrCreate
 import org.matrix.android.sdk.internal.database.query.where
 import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
@@ -180,3 +185,12 @@ internal fun ChunkEntity.isMoreRecentThan(chunkToCheck: ChunkEntity): Boolean {
     // We don't know, so we assume it's false
     return false
 }
+
+internal fun ChunkEntity.Companion.findLatestSessionInfo(realm: Realm, roomId: String): Set<SessionInfo>? =
+        ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)?.timelineEvents?.mapNotNull { timelineEvent ->
+            timelineEvent?.root?.asDomain()?.content?.toModel<EncryptedEventContent>()?.let { content ->
+                content.sessionId ?: return@mapNotNull null
+                content.senderKey ?: return@mapNotNull null
+                SessionInfo(content.sessionId, content.senderKey)
+            }
+        }?.toSet()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/GroupSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/GroupSummaryMapper.kt
deleted file mode 100644
index 13c3a796c4d1d797097b17f0c991e05b72895f18..0000000000000000000000000000000000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/GroupSummaryMapper.kt
+++ /dev/null
@@ -1,39 +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.database.mapper
-
-import org.matrix.android.sdk.api.session.group.model.GroupSummary
-import org.matrix.android.sdk.internal.database.model.GroupSummaryEntity
-
-internal object GroupSummaryMapper {
-
-    fun map(groupSummaryEntity: GroupSummaryEntity): GroupSummary {
-        return GroupSummary(
-                groupSummaryEntity.groupId,
-                groupSummaryEntity.membership,
-                groupSummaryEntity.displayName,
-                groupSummaryEntity.shortDescription,
-                groupSummaryEntity.avatarUrl,
-                groupSummaryEntity.roomIds.toList(),
-                groupSummaryEntity.userIds.toList()
-        )
-    }
-}
-
-internal fun GroupSummaryEntity.asDomain(): GroupSummary {
-    return GroupSummaryMapper.map(this)
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt
index 735cfe411ce67b2b695652b3fd35e5dc78b93cf5..72b0f7a0432022af7cf36dfd4bec4a6309fc1937 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt
@@ -106,6 +106,7 @@ internal class RoomSummaryMapper @Inject constructor(
                             worldReadable = it.childSummaryEntity?.joinRules == RoomJoinRules.PUBLIC
                     )
                 },
+                directParentNames = roomSummaryEntity.directParentNames.toList(),
                 flattenParentIds = roomSummaryEntity.flattenParentIds?.split("|") ?: emptyList(),
                 roomEncryptionAlgorithm = when (val alg = roomSummaryEntity.e2eAlgorithm) {
                     // I should probably use #hasEncryptorClassForAlgorithm but it says it supports
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo010.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo010.kt
index aae80423ac5118ab2dde48a47fbfa8b10a0dadf5..27423a9dca9033bee7e1987fb9a53271e39c01f0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo010.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo010.kt
@@ -49,7 +49,7 @@ internal class MigrateSessionTo010(realm: DynamicRealm) : RealmMigrator(realm, 1
         realm.schema.get("RoomSummaryEntity")
                 ?.addField(RoomSummaryEntityFields.ROOM_TYPE, String::class.java)
                 ?.addField(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, String::class.java)
-                ?.addField(RoomSummaryEntityFields.GROUP_IDS, String::class.java)
+                ?.addField("groupIds", String::class.java)
                 ?.transform { obj ->
 
                     val creationEvent = realm.where("CurrentStateEventEntity")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroup.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo032.kt
similarity index 51%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroup.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo032.kt
index 9c37d4db6c9ff2fafbc13159f7eef14d944dd083..1506b8c8b38498c1ea062474448f59437aff844c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroup.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo032.kt
@@ -1,11 +1,11 @@
 /*
- * Copyright 2020 The Matrix.org Foundation C.I.C.
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
- * http://www.apache.org/licenses/LICENSE-2.0
+ *     http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
@@ -14,17 +14,15 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.session.group
+package org.matrix.android.sdk.internal.database.migration
 
-import org.matrix.android.sdk.api.session.group.Group
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
 
-internal class DefaultGroup(
-        override val groupId: String,
-        private val getGroupDataTask: GetGroupDataTask
-) : Group {
+internal class MigrateSessionTo032(realm: DynamicRealm) : RealmMigrator(realm, 32) {
 
-    override suspend fun fetchGroupData() {
-        val params = GetGroupDataTask.Params.FetchWithIds(listOf(groupId))
-        getGroupDataTask.execute(params)
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.get("RoomSummaryEntity")
+                ?.removeField("groupIds")
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo033.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo033.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0e3a8599c594383faf8997a3e4e5ad7d04e2c4df
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo033.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.livelocation.LiveLocationShareAggregatedSummaryEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+/**
+ * Migrating to:
+ * Live location sharing aggregated summary: adding new field relatedEventIds.
+ */
+internal class MigrateSessionTo033(realm: DynamicRealm) : RealmMigrator(realm, 33) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.get("LiveLocationShareAggregatedSummaryEntity")
+                ?.addRealmListField(LiveLocationShareAggregatedSummaryEntityFields.RELATED_EVENT_IDS.`$`, String::class.java)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo034.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo034.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b23e84706f40e63298aa730b1439b036a1130521
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo034.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.livelocation.LiveLocationShareAggregatedSummaryEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+/**
+ * Migrating to:
+ * Live location sharing aggregated summary: adding new field startOfLiveTimestampMillis.
+ */
+internal class MigrateSessionTo034(realm: DynamicRealm) : RealmMigrator(realm, 34) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.get("LiveLocationShareAggregatedSummaryEntity")
+                ?.addField(LiveLocationShareAggregatedSummaryEntityFields.START_OF_LIVE_TIMESTAMP_MILLIS, Long::class.java)
+                ?.setNullable(LiveLocationShareAggregatedSummaryEntityFields.START_OF_LIVE_TIMESTAMP_MILLIS, true)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo035.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo035.kt
new file mode 100644
index 0000000000000000000000000000000000000000..5b3c95b4a2c36d8e29062ebb0af6cdb0189d678f
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo035.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.database.migration
+
+import io.realm.DynamicRealm
+import io.realm.RealmList
+import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+internal class MigrateSessionTo035(realm: DynamicRealm) : RealmMigrator(realm, 35) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.get("RoomSummaryEntity")
+                ?.addRealmListField(RoomSummaryEntityFields.DIRECT_PARENT_NAMES.`$`, String::class.java)
+                ?.transform { it.setList(RoomSummaryEntityFields.DIRECT_PARENT_NAMES.`$`, RealmList("")) }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/GroupEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/GroupEntity.kt
deleted file mode 100644
index 0120bb91d3ef2edcd5a8ee49b093aed01fb94234..0000000000000000000000000000000000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/GroupEntity.kt
+++ /dev/null
@@ -1,40 +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.database.model
-
-import io.realm.RealmObject
-import io.realm.annotations.PrimaryKey
-import org.matrix.android.sdk.api.session.room.model.Membership
-
-/**
- * This class is used to store group info (groupId and membership) from the sync response.
- * Then GetGroupDataTask is called regularly to fetch group information from the homeserver.
- */
-internal open class GroupEntity(@PrimaryKey var groupId: String = "") :
-        RealmObject() {
-
-    private var membershipStr: String = Membership.NONE.name
-    var membership: Membership
-        get() {
-            return Membership.valueOf(membershipStr)
-        }
-        set(value) {
-            membershipStr = value.name
-        }
-
-    companion object
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/GroupSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/GroupSummaryEntity.kt
deleted file mode 100644
index d96514855976cf05b19a1b87534fdc742f4bd83f..0000000000000000000000000000000000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/GroupSummaryEntity.kt
+++ /dev/null
@@ -1,43 +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.database.model
-
-import io.realm.RealmList
-import io.realm.RealmObject
-import io.realm.annotations.PrimaryKey
-import org.matrix.android.sdk.api.session.room.model.Membership
-
-internal open class GroupSummaryEntity(
-        @PrimaryKey var groupId: String = "",
-        var displayName: String = "",
-        var shortDescription: String = "",
-        var avatarUrl: String = "",
-        var roomIds: RealmList<String> = RealmList(),
-        var userIds: RealmList<String> = RealmList()
-) : RealmObject() {
-
-    private var membershipStr: String = Membership.NONE.name
-    var membership: Membership
-        get() {
-            return Membership.valueOf(membershipStr)
-        }
-        set(value) {
-            membershipStr = value.name
-        }
-
-    companion object
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt
index cd755590be0f253a296559d7f04f596b10694812..471bec59afe7d9bb9737e97b3b014dd6e4a36aca 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt
@@ -34,7 +34,8 @@ internal open class RoomSummaryEntity(
         @PrimaryKey var roomId: String = "",
         var roomType: String? = null,
         var parents: RealmList<SpaceParentSummaryEntity> = RealmList(),
-        var children: RealmList<SpaceChildSummaryEntity> = RealmList()
+        var children: RealmList<SpaceChildSummaryEntity> = RealmList(),
+        var directParentNames: RealmList<String> = RealmList(),
 ) : RealmObject() {
 
     private var displayName: String? = ""
@@ -240,11 +241,6 @@ internal open class RoomSummaryEntity(
             if (value != field) field = value
         }
 
-    var groupIds: String? = null
-        set(value) {
-            if (value != field) field = value
-        }
-
     @Index
     private var membershipStr: String = Membership.NONE.name
 
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 890c2300f8bd52852e64b439a424caa5353399d9..d131589dd142afe06a8dbaf10daff74df9cbb66f 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
@@ -32,8 +32,6 @@ import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntit
             EventInsertEntity::class,
             TimelineEventEntity::class,
             FilterEntity::class,
-            GroupEntity::class,
-            GroupSummaryEntity::class,
             ReadReceiptEntity::class,
             RoomEntity::class,
             RoomSummaryEntity::class,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/livelocation/LiveLocationShareAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/livelocation/LiveLocationShareAggregatedSummaryEntity.kt
index c5df8e933838d2eaf43b068aa9ccdac1405c662a..ca793ffd8e2ffc962a384715120d1c2fb0fecb56 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/livelocation/LiveLocationShareAggregatedSummaryEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/livelocation/LiveLocationShareAggregatedSummaryEntity.kt
@@ -16,6 +16,7 @@
 
 package org.matrix.android.sdk.internal.database.model.livelocation
 
+import io.realm.RealmList
 import io.realm.RealmObject
 import io.realm.annotations.PrimaryKey
 
@@ -29,6 +30,11 @@ internal open class LiveLocationShareAggregatedSummaryEntity(
         @PrimaryKey
         var eventId: String = "",
 
+        /**
+         * List of event ids used to compute the aggregated summary data.
+         */
+        var relatedEventIds: RealmList<String> = RealmList(),
+
         var roomId: String = "",
 
         var userId: String = "",
@@ -38,6 +44,8 @@ internal open class LiveLocationShareAggregatedSummaryEntity(
          */
         var isActive: Boolean? = null,
 
+        var startOfLiveTimestampMillis: Long? = null,
+
         var endOfLiveTimestampMillis: Long? = null,
 
         /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/CurrentStateEventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/CurrentStateEventEntityQueries.kt
index e0dbf2eee82cb7baf4e3c2909093ed015534ee9e..e17d07c584454190724366c86f8ffb656eeb5e14 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/CurrentStateEventEntityQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/CurrentStateEventEntityQueries.kt
@@ -23,13 +23,20 @@ import io.realm.kotlin.createObject
 import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
 import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields
 
+internal fun CurrentStateEventEntity.Companion.whereRoomId(
+        realm: Realm,
+        roomId: String
+): RealmQuery<CurrentStateEventEntity> {
+    return realm.where(CurrentStateEventEntity::class.java)
+            .equalTo(CurrentStateEventEntityFields.ROOM_ID, roomId)
+}
+
 internal fun CurrentStateEventEntity.Companion.whereType(
         realm: Realm,
         roomId: String,
         type: String
 ): RealmQuery<CurrentStateEventEntity> {
-    return realm.where(CurrentStateEventEntity::class.java)
-            .equalTo(CurrentStateEventEntityFields.ROOM_ID, roomId)
+    return whereRoomId(realm = realm, roomId = roomId)
             .equalTo(CurrentStateEventEntityFields.TYPE, type)
 }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt
index 6caa832110ccee65587a7420a8b5f97f796b0d7c..1c19c21de29601e43fe06981c946798eae28dbbd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt
@@ -23,6 +23,11 @@ import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEnt
 import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntityFields
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
 
+internal fun EventAnnotationsSummaryEntity.Companion.where(realm: Realm, eventId: String): RealmQuery<EventAnnotationsSummaryEntity> {
+    return realm.where<EventAnnotationsSummaryEntity>()
+            .equalTo(EventAnnotationsSummaryEntityFields.EVENT_ID, eventId)
+}
+
 internal fun EventAnnotationsSummaryEntity.Companion.where(realm: Realm, roomId: String, eventId: String): RealmQuery<EventAnnotationsSummaryEntity> {
     return realm.where<EventAnnotationsSummaryEntity>()
             .equalTo(EventAnnotationsSummaryEntityFields.ROOM_ID, roomId)
@@ -44,3 +49,7 @@ internal fun EventAnnotationsSummaryEntity.Companion.getOrCreate(realm: Realm, r
     return EventAnnotationsSummaryEntity.where(realm, roomId, eventId).findFirst()
             ?: EventAnnotationsSummaryEntity.create(realm, roomId, eventId)
 }
+
+internal fun EventAnnotationsSummaryEntity.Companion.get(realm: Realm, eventId: String): EventAnnotationsSummaryEntity? {
+    return EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/GroupEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/GroupEntityQueries.kt
deleted file mode 100644
index 020592d1dd3708b267d8c44b6309c314bc305e7b..0000000000000000000000000000000000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/GroupEntityQueries.kt
+++ /dev/null
@@ -1,34 +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.database.query
-
-import io.realm.Realm
-import io.realm.RealmQuery
-import io.realm.kotlin.where
-import org.matrix.android.sdk.api.session.room.model.Membership
-import org.matrix.android.sdk.internal.database.model.GroupEntity
-import org.matrix.android.sdk.internal.database.model.GroupEntityFields
-import org.matrix.android.sdk.internal.query.process
-
-internal fun GroupEntity.Companion.where(realm: Realm, groupId: String): RealmQuery<GroupEntity> {
-    return realm.where<GroupEntity>()
-            .equalTo(GroupEntityFields.GROUP_ID, groupId)
-}
-
-internal fun GroupEntity.Companion.where(realm: Realm, memberships: List<Membership>): RealmQuery<GroupEntity> {
-    return realm.where<GroupEntity>().process(GroupEntityFields.MEMBERSHIP_STR, memberships)
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/GroupSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/GroupSummaryEntityQueries.kt
deleted file mode 100644
index 8131598d95b2de54c0f8a771304c9fce8c5b447a..0000000000000000000000000000000000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/GroupSummaryEntityQueries.kt
+++ /dev/null
@@ -1,41 +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.database.query
-
-import io.realm.Realm
-import io.realm.RealmQuery
-import io.realm.kotlin.createObject
-import io.realm.kotlin.where
-import org.matrix.android.sdk.internal.database.model.GroupSummaryEntity
-import org.matrix.android.sdk.internal.database.model.GroupSummaryEntityFields
-
-internal fun GroupSummaryEntity.Companion.where(realm: Realm, groupId: String? = null): RealmQuery<GroupSummaryEntity> {
-    val query = realm.where<GroupSummaryEntity>()
-    if (groupId != null) {
-        query.equalTo(GroupSummaryEntityFields.GROUP_ID, groupId)
-    }
-    return query
-}
-
-internal fun GroupSummaryEntity.Companion.where(realm: Realm, groupIds: List<String>): RealmQuery<GroupSummaryEntity> {
-    return realm.where<GroupSummaryEntity>()
-            .`in`(GroupSummaryEntityFields.GROUP_ID, groupIds.toTypedArray())
-}
-
-internal fun GroupSummaryEntity.Companion.getOrCreate(realm: Realm, groupId: String): GroupSummaryEntity {
-    return where(realm, groupId).findFirst() ?: realm.createObject(groupId)
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt
index d69f251f6ffbf769227e29b722d0ce93a52fd72c..a1179ccdce2f6d6a8f4bd94b21abc2c98912e365 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt
@@ -23,6 +23,14 @@ import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEnt
 import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
 import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields
 
+internal fun LiveLocationShareAggregatedSummaryEntity.Companion.where(
+        realm: Realm,
+        eventId: String,
+): RealmQuery<LiveLocationShareAggregatedSummaryEntity> {
+    return realm.where<LiveLocationShareAggregatedSummaryEntity>()
+            .equalTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, eventId)
+}
+
 internal fun LiveLocationShareAggregatedSummaryEntity.Companion.where(
         realm: Realm,
         roomId: String,
@@ -72,17 +80,26 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.get(
     return LiveLocationShareAggregatedSummaryEntity.where(realm, roomId, eventId).findFirst()
 }
 
+internal fun LiveLocationShareAggregatedSummaryEntity.Companion.get(
+        realm: Realm,
+        eventId: String,
+): LiveLocationShareAggregatedSummaryEntity? {
+    return LiveLocationShareAggregatedSummaryEntity.where(realm, eventId).findFirst()
+}
+
 internal fun LiveLocationShareAggregatedSummaryEntity.Companion.findActiveLiveInRoomForUser(
         realm: Realm,
         roomId: String,
         userId: String,
         ignoredEventId: String,
+        startOfLiveTimestampThreshold: Long,
 ): List<LiveLocationShareAggregatedSummaryEntity> {
     return LiveLocationShareAggregatedSummaryEntity
             .whereRoomId(realm, roomId = roomId)
             .equalTo(LiveLocationShareAggregatedSummaryEntityFields.USER_ID, userId)
             .equalTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true)
             .notEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, ignoredEventId)
+            .lessThan(LiveLocationShareAggregatedSummaryEntityFields.START_OF_LIVE_TIMESTAMP_MILLIS, startOfLiveTimestampThreshold)
             .findAll()
             .toList()
 }
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 b5b46a3f5ae5b89c82364f2765ce9ddd84fed033..113e780e5ce56a1c7be53c02b5e91a3d9b4023de 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
@@ -21,6 +21,7 @@ import com.squareup.moshi.Moshi
 import dagger.Module
 import dagger.Provides
 import okhttp3.ConnectionSpec
+import okhttp3.Dispatcher
 import okhttp3.OkHttpClient
 import okhttp3.Protocol
 import okhttp3.logging.HttpLoggingInterceptor
@@ -73,7 +74,9 @@ internal object NetworkModule {
             apiInterceptor: ApiInterceptor
     ): OkHttpClient {
         val spec = ConnectionSpec.Builder(matrixConfiguration.connectionSpec).build()
-
+        val dispatcher = Dispatcher().apply {
+            maxRequestsPerHost = 20
+        }
         return OkHttpClient.Builder()
                 // workaround for #4669
                 .protocols(listOf(Protocol.HTTP_1_1))
@@ -94,6 +97,7 @@ internal object NetworkModule {
                         addInterceptor(curlLoggingInterceptor)
                     }
                 }
+                .dispatcher(dispatcher)
                 .connectionSpecs(Collections.singletonList(spec))
                 .applyMatrixConfiguration(matrixConfiguration)
                 .build()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/RealmExtensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/RealmExtensions.kt
index 00cbe0aa85ebb394c02e205be79ad84583557721..c6ea2bc7bd512a47fc635592164457b35caa2f68 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/RealmExtensions.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/RealmExtensions.kt
@@ -20,6 +20,7 @@ import io.realm.RealmList
 import io.realm.RealmObject
 import io.realm.RealmObjectSchema
 import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
+import org.matrix.android.sdk.internal.util.fatalError
 
 internal fun RealmObject.assertIsManaged() {
     check(isManaged) { "${javaClass.simpleName} entity should be managed to use this function" }
@@ -27,10 +28,19 @@ internal fun RealmObject.assertIsManaged() {
 
 /**
  * Clear a RealmList by deleting all its items calling the provided lambda.
+ * The lambda is supposed to delete the item, which means that after this operation, the list will be empty.
  */
 internal fun <T> RealmList<T>.clearWith(delete: (T) -> Unit) {
-    while (!isEmpty()) {
-        first()?.let { delete.invoke(it) }
+    map { item ->
+        // Create a lambda for all items of the list
+        { delete(item) }
+    }.forEach { lambda ->
+        // Then invoke all the lambda
+        lambda.invoke()
+    }
+
+    if (isNotEmpty()) {
+        fatalError("`clearWith` MUST delete all elements of the RealmList")
     }
 }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/DefaultLegacySessionImporter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/DefaultLegacySessionImporter.kt
index 56d9cc21439ca6ab0081b53d02a1f2048f410f76..7d52d9b2bfdb353b76bc64f58ada383446edf84e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/DefaultLegacySessionImporter.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/legacy/DefaultLegacySessionImporter.kt
@@ -20,6 +20,7 @@ import android.content.Context
 import io.realm.Realm
 import io.realm.RealmConfiguration
 import kotlinx.coroutines.runBlocking
+import org.matrix.android.sdk.api.auth.LoginType
 import org.matrix.android.sdk.api.auth.data.Credentials
 import org.matrix.android.sdk.api.auth.data.DiscoveryInformation
 import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
@@ -145,7 +146,8 @@ internal class DefaultLegacySessionImporter @Inject constructor(
                         forceUsageTlsVersions = legacyConfig.forceUsageOfTlsVersions()
                 ),
                 // If token is not valid, this boolean will be updated later
-                isTokenValid = true
+                isTokenValid = true,
+                loginType = LoginType.UNKNOWN,
         )
 
         Timber.d("Migration: save session")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkCallbackStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkCallbackStrategy.kt
index f75fb01746609097d9c4bb9a6136ae6a95a76282..90d2719e250277a021135dcfb6339fcca957acf7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkCallbackStrategy.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkCallbackStrategy.kt
@@ -70,7 +70,15 @@ internal class PreferredNetworkCallbackStrategy @Inject constructor(context: Con
 
     override fun register(hasChanged: () -> Unit) {
         hasChangedCallback = hasChanged
-        conn.registerDefaultNetworkCallback(networkCallback)
+        // Add a try catch for safety
+        // XXX: It happens when running all tests in CI, at some points we reach a limit here causing TooManyRequestsException
+        // and crashing the sync thread. We might have problem here, would need some investigation
+        // for now adding a catch to allow CI to continue running
+        try {
+            conn.registerDefaultNetworkCallback(networkCallback)
+        } catch (t: Throwable) {
+            Timber.e(t, "Unable to register default network callback")
+        }
     }
 
     override fun unregister() {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GlobalRealmMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GlobalRealmMigration.kt
index a9dfd47b5a46a42ff94443d3d6aa00b51904956b..575afa84944b18ff780d089376fe2cae5d8b92a5 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GlobalRealmMigration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/raw/GlobalRealmMigration.kt
@@ -17,12 +17,14 @@
 package org.matrix.android.sdk.internal.raw
 
 import io.realm.DynamicRealm
-import io.realm.RealmMigration
 import org.matrix.android.sdk.internal.raw.migration.MigrateGlobalTo001
-import timber.log.Timber
+import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
 import javax.inject.Inject
 
-internal class GlobalRealmMigration @Inject constructor() : RealmMigration {
+internal class GlobalRealmMigration @Inject constructor() : MatrixRealmMigration(
+        dbName = "Global",
+        schemaVersion = 1L,
+) {
     /**
      * Forces all GlobalRealmMigration instances to be equal.
      * Avoids Realm throwing when multiple instances of the migration are set.
@@ -30,11 +32,7 @@ internal class GlobalRealmMigration @Inject constructor() : RealmMigration {
     override fun equals(other: Any?) = other is GlobalRealmMigration
     override fun hashCode() = 2000
 
-    val schemaVersion = 1L
-
-    override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
-        Timber.d("Migrating Global Realm from $oldVersion to $newVersion")
-
+    override fun doMigrate(realm: DynamicRealm, oldVersion: Long) {
         if (oldVersion < 1) MigrateGlobalTo001(realm).perform()
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt
index 7c50a0ff84ede28af924cbcf0c1856206b8e46fa..57db187bdc8047a9dc7eddf36dedcaf4f6cd2cf1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt
@@ -41,7 +41,6 @@ import org.matrix.android.sdk.api.session.crypto.CryptoService
 import org.matrix.android.sdk.api.session.events.EventService
 import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker
 import org.matrix.android.sdk.api.session.file.FileService
-import org.matrix.android.sdk.api.session.group.GroupService
 import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
 import org.matrix.android.sdk.api.session.identity.IdentityService
 import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService
@@ -97,7 +96,6 @@ internal class DefaultSession @Inject constructor(
         private val sessionListeners: SessionListeners,
         private val roomService: Lazy<RoomService>,
         private val roomDirectoryService: Lazy<RoomDirectoryService>,
-        private val groupService: Lazy<GroupService>,
         private val userService: Lazy<UserService>,
         private val filterService: Lazy<FilterService>,
         private val federationService: Lazy<FederationService>,
@@ -209,7 +207,6 @@ internal class DefaultSession @Inject constructor(
     override fun homeServerCapabilitiesService(): HomeServerCapabilitiesService = homeServerCapabilitiesService.get()
     override fun roomService(): RoomService = roomService.get()
     override fun roomDirectoryService(): RoomDirectoryService = roomDirectoryService.get()
-    override fun groupService(): GroupService = groupService.get()
     override fun userService(): UserService = userService.get()
     override fun signOutService(): SignOutService = signOutService.get()
     override fun filterService(): FilterService = filterService.get()
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 d3cae3ac2d84cc85bb474608f9daa910408fdd36..a79f35bcb648add749ce68b4e32f890019801b78 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
@@ -35,8 +35,6 @@ import org.matrix.android.sdk.internal.session.content.ContentModule
 import org.matrix.android.sdk.internal.session.content.UploadContentWorker
 import org.matrix.android.sdk.internal.session.contentscanner.ContentScannerModule
 import org.matrix.android.sdk.internal.session.filter.FilterModule
-import org.matrix.android.sdk.internal.session.group.GetGroupDataWorker
-import org.matrix.android.sdk.internal.session.group.GroupModule
 import org.matrix.android.sdk.internal.session.homeserver.HomeServerCapabilitiesModule
 import org.matrix.android.sdk.internal.session.identity.IdentityModule
 import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManagerModule
@@ -74,10 +72,8 @@ import org.matrix.android.sdk.internal.util.system.SystemModule
             SyncModule::class,
             HomeServerCapabilitiesModule::class,
             SignOutModule::class,
-            GroupModule::class,
             UserModule::class,
             FilterModule::class,
-            GroupModule::class,
             ContentModule::class,
             CacheModule::class,
             MediaModule::class,
@@ -124,8 +120,6 @@ internal interface SessionComponent {
 
     fun inject(worker: RedactEventWorker)
 
-    fun inject(worker: GetGroupDataWorker)
-
     fun inject(worker: UploadContentWorker)
 
     fun inject(worker: SyncWorker)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt
index f8a52f0b7ed5ac51f4473a32f9a6b607fde41d7b..b9f56cbc9f1af3df96572d12c534c506bfa29314 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt
@@ -88,6 +88,7 @@ import org.matrix.android.sdk.internal.session.room.EventRelationsAggregationPro
 import org.matrix.android.sdk.internal.session.room.aggregation.poll.DefaultPollAggregationProcessor
 import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollAggregationProcessor
 import org.matrix.android.sdk.internal.session.room.create.RoomCreateEventProcessor
+import org.matrix.android.sdk.internal.session.room.location.LiveLocationShareRedactionEventProcessor
 import org.matrix.android.sdk.internal.session.room.prune.RedactionEventProcessor
 import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
 import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessorCoroutine
@@ -321,6 +322,10 @@ internal abstract class SessionModule {
     @IntoSet
     abstract fun bindEventRedactionProcessor(processor: RedactionEventProcessor): EventInsertLiveProcessor
 
+    @Binds
+    @IntoSet
+    abstract fun bindLiveLocationShareRedactionEventProcessor(processor: LiveLocationShareRedactionEventProcessor): EventInsertLiveProcessor
+
     @Binds
     @IntoSet
     abstract fun bindEventRelationsAggregationProcessor(processor: EventRelationsAggregationProcessor): EventInsertLiveProcessor
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/displayname/DisplayNameResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/displayname/DisplayNameResolver.kt
index 76d956f9a5cc914b95379d100c4113223826bc01..3b0799438eb2e5f1437f0d8b58f52bc537e02ff6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/displayname/DisplayNameResolver.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/displayname/DisplayNameResolver.kt
@@ -25,7 +25,7 @@ internal class DisplayNameResolver @Inject constructor(
         private val matrixConfiguration: MatrixConfiguration
 ) {
     fun getBestName(matrixItem: MatrixItem): String {
-        return if (matrixItem is MatrixItem.GroupItem || matrixItem is MatrixItem.RoomAliasItem) {
+        return if (matrixItem is MatrixItem.RoomAliasItem) {
             // Best name is the id, and we keep the displayName of the room for the case we need the first letter
             matrixItem.id
         } else {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroupService.kt
deleted file mode 100644
index 9334d09377228be0ff3bd5d0050a9fd4499bbd4c..0000000000000000000000000000000000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/DefaultGroupService.kt
+++ /dev/null
@@ -1,80 +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.session.group
-
-import androidx.lifecycle.LiveData
-import com.zhuinden.monarchy.Monarchy
-import io.realm.Realm
-import io.realm.RealmQuery
-import org.matrix.android.sdk.api.session.group.Group
-import org.matrix.android.sdk.api.session.group.GroupService
-import org.matrix.android.sdk.api.session.group.GroupSummaryQueryParams
-import org.matrix.android.sdk.api.session.group.model.GroupSummary
-import org.matrix.android.sdk.internal.database.mapper.asDomain
-import org.matrix.android.sdk.internal.database.model.GroupEntity
-import org.matrix.android.sdk.internal.database.model.GroupSummaryEntity
-import org.matrix.android.sdk.internal.database.model.GroupSummaryEntityFields
-import org.matrix.android.sdk.internal.database.query.where
-import org.matrix.android.sdk.internal.di.SessionDatabase
-import org.matrix.android.sdk.internal.query.QueryStringValueProcessor
-import org.matrix.android.sdk.internal.query.process
-import org.matrix.android.sdk.internal.util.fetchCopyMap
-import javax.inject.Inject
-
-internal class DefaultGroupService @Inject constructor(
-        @SessionDatabase private val monarchy: Monarchy,
-        private val groupFactory: GroupFactory,
-        private val queryStringValueProcessor: QueryStringValueProcessor,
-) : GroupService {
-
-    override fun getGroup(groupId: String): Group? {
-        return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
-            GroupEntity.where(realm, groupId).findFirst()?.let {
-                groupFactory.create(groupId)
-            }
-        }
-    }
-
-    override fun getGroupSummary(groupId: String): GroupSummary? {
-        return monarchy.fetchCopyMap(
-                { realm -> GroupSummaryEntity.where(realm, groupId).findFirst() },
-                { it, _ -> it.asDomain() }
-        )
-    }
-
-    override fun getGroupSummaries(groupSummaryQueryParams: GroupSummaryQueryParams): List<GroupSummary> {
-        return monarchy.fetchAllMappedSync(
-                { groupSummariesQuery(it, groupSummaryQueryParams) },
-                { it.asDomain() }
-        )
-    }
-
-    override fun getGroupSummariesLive(groupSummaryQueryParams: GroupSummaryQueryParams): LiveData<List<GroupSummary>> {
-        return monarchy.findAllMappedWithChanges(
-                { groupSummariesQuery(it, groupSummaryQueryParams) },
-                { it.asDomain() }
-        )
-    }
-
-    private fun groupSummariesQuery(realm: Realm, queryParams: GroupSummaryQueryParams): RealmQuery<GroupSummaryEntity> {
-        return with(queryStringValueProcessor) {
-            GroupSummaryEntity.where(realm)
-                    .process(GroupSummaryEntityFields.DISPLAY_NAME, queryParams.displayName)
-                    .process(GroupSummaryEntityFields.MEMBERSHIP_STR, queryParams.memberships)
-        }
-    }
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GetGroupDataTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GetGroupDataTask.kt
deleted file mode 100644
index 235291d0617251bc28925e872fc7e555f9c09e1e..0000000000000000000000000000000000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GetGroupDataTask.kt
+++ /dev/null
@@ -1,110 +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.session.group
-
-import com.zhuinden.monarchy.Monarchy
-import org.matrix.android.sdk.api.session.room.model.Membership
-import org.matrix.android.sdk.internal.database.model.GroupEntity
-import org.matrix.android.sdk.internal.database.model.GroupSummaryEntity
-import org.matrix.android.sdk.internal.database.query.getOrCreate
-import org.matrix.android.sdk.internal.database.query.where
-import org.matrix.android.sdk.internal.di.SessionDatabase
-import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
-import org.matrix.android.sdk.internal.network.executeRequest
-import org.matrix.android.sdk.internal.session.group.model.GroupRooms
-import org.matrix.android.sdk.internal.session.group.model.GroupSummaryResponse
-import org.matrix.android.sdk.internal.session.group.model.GroupUsers
-import org.matrix.android.sdk.internal.task.Task
-import org.matrix.android.sdk.internal.util.awaitTransaction
-import timber.log.Timber
-import javax.inject.Inject
-
-internal interface GetGroupDataTask : Task<GetGroupDataTask.Params, Unit> {
-    sealed class Params {
-        object FetchAllActive : Params()
-        data class FetchWithIds(val groupIds: List<String>) : Params()
-    }
-}
-
-internal class DefaultGetGroupDataTask @Inject constructor(
-        private val groupAPI: GroupAPI,
-        @SessionDatabase private val monarchy: Monarchy,
-        private val globalErrorReceiver: GlobalErrorReceiver
-) : GetGroupDataTask {
-
-    private data class GroupData(
-            val groupId: String,
-            val groupSummary: GroupSummaryResponse,
-            val groupRooms: GroupRooms,
-            val groupUsers: GroupUsers
-    )
-
-    override suspend fun execute(params: GetGroupDataTask.Params) {
-        val groupIds = when (params) {
-            is GetGroupDataTask.Params.FetchAllActive -> {
-                getActiveGroupIds()
-            }
-            is GetGroupDataTask.Params.FetchWithIds -> {
-                params.groupIds
-            }
-        }
-        Timber.v("Fetch data for group with ids: ${groupIds.joinToString(";")}")
-        val data = groupIds.map { groupId ->
-            val groupSummary = executeRequest(globalErrorReceiver) {
-                groupAPI.getSummary(groupId)
-            }
-            val groupRooms = executeRequest(globalErrorReceiver) {
-                groupAPI.getRooms(groupId)
-            }
-            val groupUsers = executeRequest(globalErrorReceiver) {
-                groupAPI.getUsers(groupId)
-            }
-            GroupData(groupId, groupSummary, groupRooms, groupUsers)
-        }
-        insertInDb(data)
-    }
-
-    private fun getActiveGroupIds(): List<String> {
-        return monarchy.fetchAllMappedSync(
-                { realm ->
-                    GroupEntity.where(realm, Membership.activeMemberships())
-                },
-                { it.groupId }
-        )
-    }
-
-    private suspend fun insertInDb(groupDataList: List<GroupData>) {
-        monarchy
-                .awaitTransaction { realm ->
-                    groupDataList.forEach { groupData ->
-
-                        val groupSummaryEntity = GroupSummaryEntity.getOrCreate(realm, groupData.groupId)
-
-                        groupSummaryEntity.avatarUrl = groupData.groupSummary.profile?.avatarUrl ?: ""
-                        val name = groupData.groupSummary.profile?.name
-                        groupSummaryEntity.displayName = if (name.isNullOrEmpty()) groupData.groupId else name
-                        groupSummaryEntity.shortDescription = groupData.groupSummary.profile?.shortDescription ?: ""
-
-                        groupSummaryEntity.roomIds.clear()
-                        groupData.groupRooms.rooms.mapTo(groupSummaryEntity.roomIds) { it.roomId }
-
-                        groupSummaryEntity.userIds.clear()
-                        groupData.groupUsers.users.mapTo(groupSummaryEntity.userIds) { it.userId }
-                    }
-                }
-    }
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GetGroupDataWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GetGroupDataWorker.kt
deleted file mode 100644
index 21582cb4bed7b2867768c73e768b44b469d08f96..0000000000000000000000000000000000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GetGroupDataWorker.kt
+++ /dev/null
@@ -1,59 +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.session.group
-
-import android.content.Context
-import androidx.work.WorkerParameters
-import com.squareup.moshi.JsonClass
-import org.matrix.android.sdk.internal.SessionManager
-import org.matrix.android.sdk.internal.session.SessionComponent
-import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
-import org.matrix.android.sdk.internal.worker.SessionWorkerParams
-import javax.inject.Inject
-
-/**
- * Possible previous worker: None.
- * Possible next worker    : None.
- */
-internal class GetGroupDataWorker(context: Context, params: WorkerParameters, sessionManager: SessionManager) :
-        SessionSafeCoroutineWorker<GetGroupDataWorker.Params>(context, params, sessionManager, Params::class.java) {
-
-    @JsonClass(generateAdapter = true)
-    internal data class Params(
-            override val sessionId: String,
-            override val lastFailureMessage: String? = null
-    ) : SessionWorkerParams
-
-    @Inject lateinit var getGroupDataTask: GetGroupDataTask
-
-    override fun injectWith(injector: SessionComponent) {
-        injector.inject(this)
-    }
-
-    override suspend fun doSafeWork(params: Params): Result {
-        return runCatching {
-            getGroupDataTask.execute(GetGroupDataTask.Params.FetchAllActive)
-        }.fold(
-                { Result.success() },
-                { Result.retry() }
-        )
-    }
-
-    override fun buildErrorParams(params: Params, message: String): Params {
-        return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
-    }
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupAPI.kt
deleted file mode 100644
index c9d25b910441388d331af54a81c1da767d16a5b6..0000000000000000000000000000000000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupAPI.kt
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright 2020 The Matrix.org Foundation C.I.C.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.matrix.android.sdk.internal.session.group
-
-import org.matrix.android.sdk.internal.network.NetworkConstants
-import org.matrix.android.sdk.internal.session.group.model.GroupRooms
-import org.matrix.android.sdk.internal.session.group.model.GroupSummaryResponse
-import org.matrix.android.sdk.internal.session.group.model.GroupUsers
-import retrofit2.http.GET
-import retrofit2.http.Path
-
-internal interface GroupAPI {
-
-    /**
-     * Request a group summary.
-     *
-     * @param groupId the group id
-     */
-    @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "groups/{groupId}/summary")
-    suspend fun getSummary(@Path("groupId") groupId: String): GroupSummaryResponse
-
-    /**
-     * Request the rooms list.
-     *
-     * @param groupId the group id
-     */
-    @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "groups/{groupId}/rooms")
-    suspend fun getRooms(@Path("groupId") groupId: String): GroupRooms
-
-    /**
-     * Request the users list.
-     *
-     * @param groupId the group id
-     */
-    @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "groups/{groupId}/users")
-    suspend fun getUsers(@Path("groupId") groupId: String): GroupUsers
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupFactory.kt
deleted file mode 100644
index 653d2a69331615b24ba985dfaa85ca6ffb11d6ce..0000000000000000000000000000000000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupFactory.kt
+++ /dev/null
@@ -1,37 +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.session.group
-
-import org.matrix.android.sdk.api.session.group.Group
-import org.matrix.android.sdk.internal.session.SessionScope
-import javax.inject.Inject
-
-internal interface GroupFactory {
-    fun create(groupId: String): Group
-}
-
-@SessionScope
-internal class DefaultGroupFactory @Inject constructor(private val getGroupDataTask: GetGroupDataTask) :
-        GroupFactory {
-
-    override fun create(groupId: String): Group {
-        return DefaultGroup(
-                groupId = groupId,
-                getGroupDataTask = getGroupDataTask
-        )
-    }
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupModule.kt
deleted file mode 100644
index 4dd61aa91410115403a2a7c13e6e188af0341796..0000000000000000000000000000000000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/GroupModule.kt
+++ /dev/null
@@ -1,47 +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.session.group
-
-import dagger.Binds
-import dagger.Module
-import dagger.Provides
-import org.matrix.android.sdk.api.session.group.GroupService
-import org.matrix.android.sdk.internal.session.SessionScope
-import retrofit2.Retrofit
-
-@Module
-internal abstract class GroupModule {
-
-    @Module
-    companion object {
-        @Provides
-        @JvmStatic
-        @SessionScope
-        fun providesGroupAPI(retrofit: Retrofit): GroupAPI {
-            return retrofit.create(GroupAPI::class.java)
-        }
-    }
-
-    @Binds
-    abstract fun bindGroupFactory(factory: DefaultGroupFactory): GroupFactory
-
-    @Binds
-    abstract fun bindGetGroupDataTask(task: DefaultGetGroupDataTask): GetGroupDataTask
-
-    @Binds
-    abstract fun bindGroupService(service: DefaultGroupService): GroupService
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupProfile.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupProfile.kt
deleted file mode 100644
index 30eeb74b2e28d60236f3159b8045735331425462..0000000000000000000000000000000000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupProfile.kt
+++ /dev/null
@@ -1,49 +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.session.group.model
-
-import com.squareup.moshi.Json
-import com.squareup.moshi.JsonClass
-
-/**
- * This class represents a community profile in the server responses.
- */
-@JsonClass(generateAdapter = true)
-internal data class GroupProfile(
-
-        @Json(name = "short_description") val shortDescription: String? = null,
-
-        /**
-         * Tell whether the group is public.
-         */
-        @Json(name = "is_public") val isPublic: Boolean? = null,
-
-        /**
-         * The URL for the group's avatar. May be nil.
-         */
-        @Json(name = "avatar_url") val avatarUrl: String? = null,
-
-        /**
-         * The group's name.
-         */
-        @Json(name = "name") val name: String? = null,
-
-        /**
-         * The optional HTML formatted string used to described the group.
-         */
-        @Json(name = "long_description") val longDescription: String? = null
-)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupRoom.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupRoom.kt
deleted file mode 100644
index 86e64f6797841b399b59467c13f496c583b03c0c..0000000000000000000000000000000000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupRoom.kt
+++ /dev/null
@@ -1,35 +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.session.group.model
-
-import com.squareup.moshi.Json
-import com.squareup.moshi.JsonClass
-
-@JsonClass(generateAdapter = true)
-internal data class GroupRoom(
-
-        @Json(name = "aliases") val aliases: List<String> = emptyList(),
-        @Json(name = "canonical_alias") val canonicalAlias: String? = null,
-        @Json(name = "name") val name: String? = null,
-        @Json(name = "num_joined_members") val numJoinedMembers: Int = 0,
-        @Json(name = "room_id") val roomId: String,
-        @Json(name = "topic") val topic: String? = null,
-        @Json(name = "world_readable") val worldReadable: Boolean = false,
-        @Json(name = "guest_can_join") val guestCanJoin: Boolean = false,
-        @Json(name = "avatar_url") val avatarUrl: String? = null
-
-)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryResponse.kt
deleted file mode 100644
index bf287e982c09b4cfcbd7918d385a52d2dcb67b7b..0000000000000000000000000000000000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryResponse.kt
+++ /dev/null
@@ -1,46 +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.session.group.model
-
-import com.squareup.moshi.Json
-import com.squareup.moshi.JsonClass
-
-/**
- * This class represents the summary of a community in the server response.
- */
-@JsonClass(generateAdapter = true)
-internal data class GroupSummaryResponse(
-        /**
-         * The group profile.
-         */
-        @Json(name = "profile") val profile: GroupProfile? = null,
-
-        /**
-         * The group users.
-         */
-        @Json(name = "users_section") val usersSection: GroupSummaryUsersSection? = null,
-
-        /**
-         * The current user status.
-         */
-        @Json(name = "user") val user: GroupSummaryUser? = null,
-
-        /**
-         * The rooms linked to the community.
-         */
-        @Json(name = "rooms_section") val roomsSection: GroupSummaryRoomsSection? = null
-)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryRoomsSection.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryRoomsSection.kt
deleted file mode 100644
index 87d07167ce2aad46469c1cc8bb95a54b1346b182..0000000000000000000000000000000000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryRoomsSection.kt
+++ /dev/null
@@ -1,34 +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.session.group.model
-
-import com.squareup.moshi.Json
-import com.squareup.moshi.JsonClass
-
-/**
- * This class represents the community rooms in a group summary response.
- */
-@JsonClass(generateAdapter = true)
-internal data class GroupSummaryRoomsSection(
-
-        @Json(name = "total_room_count_estimate") val totalRoomCountEstimate: Int? = null,
-
-        @Json(name = "rooms") val rooms: List<String> = emptyList()
-
-        // TODO Check the meaning and the usage of these categories. This dictionary is empty FTM.
-        // public Map<Object, Object> categories;
-)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryUser.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryUser.kt
deleted file mode 100644
index 121ae6fd7d6833df5e2452169c9f970ea97c91ec..0000000000000000000000000000000000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryUser.kt
+++ /dev/null
@@ -1,37 +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.session.group.model
-
-import com.squareup.moshi.Json
-import com.squareup.moshi.JsonClass
-
-/**
- * This class represents the current user status in a group summary response.
- */
-@JsonClass(generateAdapter = true)
-internal data class GroupSummaryUser(
-
-        /**
-         * The current user membership in this community.
-         */
-        @Json(name = "membership") val membership: String? = null,
-
-        /**
-         * Tell whether the user published this community on his profile.
-         */
-        @Json(name = "is_publicised") val isPublicised: Boolean? = null
-)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryUsersSection.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryUsersSection.kt
deleted file mode 100644
index 63608c582a66db0d2f7a96899999235583e747e3..0000000000000000000000000000000000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupSummaryUsersSection.kt
+++ /dev/null
@@ -1,35 +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.session.group.model
-
-import com.squareup.moshi.Json
-import com.squareup.moshi.JsonClass
-
-/**
- * This class represents the community members in a group summary response.
- */
-
-@JsonClass(generateAdapter = true)
-internal data class GroupSummaryUsersSection(
-
-        @Json(name = "total_user_count_estimate") val totalUserCountEstimate: Int,
-
-        @Json(name = "users") val users: List<String> = emptyList()
-
-        // TODO Check the meaning and the usage of these roles. This dictionary is empty FTM.
-        // public Map<Object, Object> roles;
-)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupUser.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupUser.kt
deleted file mode 100644
index a54c66535eb4049a3e4cbd07165a6932664f48f4..0000000000000000000000000000000000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/group/model/GroupUser.kt
+++ /dev/null
@@ -1,29 +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.session.group.model
-
-import com.squareup.moshi.Json
-import com.squareup.moshi.JsonClass
-
-@JsonClass(generateAdapter = true)
-internal data class GroupUser(
-        @Json(name = "display_name") val displayName: String = "",
-        @Json(name = "user_id") val userId: String,
-        @Json(name = "is_privileged") val isPrivileged: Boolean = false,
-        @Json(name = "avatar_url") val avatarUrl: String? = "",
-        @Json(name = "is_public") val isPublic: Boolean = false
-)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/RealmIdentityStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/RealmIdentityStoreMigration.kt
index e731f9f3472e473b810bba0c3b9be2bb05d02a30..4756b41f4c8feb5ac2102ea2448da23742aa2260 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/RealmIdentityStoreMigration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/db/RealmIdentityStoreMigration.kt
@@ -17,12 +17,14 @@
 package org.matrix.android.sdk.internal.session.identity.db
 
 import io.realm.DynamicRealm
-import io.realm.RealmMigration
 import org.matrix.android.sdk.internal.session.identity.db.migration.MigrateIdentityTo001
-import timber.log.Timber
+import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
 import javax.inject.Inject
 
-internal class RealmIdentityStoreMigration @Inject constructor() : RealmMigration {
+internal class RealmIdentityStoreMigration @Inject constructor() : MatrixRealmMigration(
+        dbName = "Identity",
+        schemaVersion = 1L,
+) {
     /**
      * Forces all RealmIdentityStoreMigration instances to be equal.
      * Avoids Realm throwing when multiple instances of the migration are set.
@@ -30,11 +32,7 @@ internal class RealmIdentityStoreMigration @Inject constructor() : RealmMigratio
     override fun equals(other: Any?) = other is RealmIdentityStoreMigration
     override fun hashCode() = 3000
 
-    val schemaVersion = 1L
-
-    override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
-        Timber.d("Migrating Realm Identity from $oldVersion to $newVersion")
-
+    override fun doMigrate(realm: DynamicRealm, oldVersion: Long) {
         if (oldVersion < 1) MigrateIdentityTo001(realm).perform()
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt
index 8e20199135a336d6a43e5edbf670370cc1e12706..3ecd47787f647c42c59898fb90a7fc1c09c8a2c0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt
@@ -97,7 +97,6 @@ internal class PermalinkFactory @Inject constructor(
             url.startsWith(MATRIX_TO_URL_BASE) -> url.substring(MATRIX_TO_URL_BASE.length)
             clientBaseUrl != null && url.startsWith(clientBaseUrl) -> {
                 when (PermalinkParser.parse(url)) {
-                    is PermalinkData.GroupLink -> url.substring(clientBaseUrl.length + GROUP_PATH.length)
                     is PermalinkData.RoomLink -> url.substring(clientBaseUrl.length + ROOM_PATH.length)
                     is PermalinkData.UserLink -> url.substring(clientBaseUrl.length + USER_PATH.length)
                     else -> null
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt
index 5e6d052443d0c54b0742eaf3f12692bf51592d74..989bcaee4474ad2b8bf33f944ed95d33e0c4f2f8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt
@@ -43,7 +43,9 @@ import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFie
 import org.matrix.android.sdk.internal.di.SessionDatabase
 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.create.CreateLocalRoomTask
 import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask
+import org.matrix.android.sdk.internal.session.room.delete.DeleteLocalRoomTask
 import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource
 import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
 import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask
@@ -60,6 +62,8 @@ import javax.inject.Inject
 internal class DefaultRoomService @Inject constructor(
         @SessionDatabase private val monarchy: Monarchy,
         private val createRoomTask: CreateRoomTask,
+        private val createLocalRoomTask: CreateLocalRoomTask,
+        private val deleteLocalRoomTask: DeleteLocalRoomTask,
         private val joinRoomTask: JoinRoomTask,
         private val markAllRoomsReadTask: MarkAllRoomsReadTask,
         private val updateBreadcrumbsTask: UpdateBreadcrumbsTask,
@@ -78,6 +82,14 @@ internal class DefaultRoomService @Inject constructor(
         return createRoomTask.executeRetry(createRoomParams, 3)
     }
 
+    override suspend fun createLocalRoom(createRoomParams: CreateRoomParams): String {
+        return createLocalRoomTask.execute(createRoomParams)
+    }
+
+    override suspend fun deleteLocalRoom(roomId: String) {
+        deleteLocalRoomTask.execute(DeleteLocalRoomTask.Params(roomId))
+    }
+
     override fun getRoom(roomId: String): Room? {
         return roomGetter.getRoom(roomId)
     }
@@ -140,9 +152,8 @@ internal class DefaultRoomService @Inject constructor(
             queryParams: RoomSummaryQueryParams,
             pagedListConfig: PagedList.Config,
             sortOrder: RoomSortOrder,
-            getFlattenParents: Boolean
     ): UpdatableLivePageResult {
-        return roomSummaryDataSource.getUpdatablePagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder, getFlattenParents)
+        return roomSummaryDataSource.getUpdatablePagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder)
     }
 
     override fun getRoomCountLive(queryParams: RoomSummaryQueryParams): LiveData<Int> {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
index ac2880de69946da33fc1ddb58db47ce03a2d33f2..9bcb7b8e4c7fba65c7171c1b16c95ca0bfaa6fac 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
@@ -377,7 +377,7 @@ internal interface RoomAPI {
      * Get a list of aliases maintained by the local server for the given room.
      * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-rooms-roomid-aliases
      */
-    @GET(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc2432/rooms/{roomId}/aliases")
+    @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/aliases")
     suspend fun getAliases(@Path("roomId") roomId: String): GetAliasesResponse
 
     /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomDataSource.kt
new file mode 100644
index 0000000000000000000000000000000000000000..bcbc53f95e2e9ad6a087466eb9f8819027b16aa5
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomDataSource.kt
@@ -0,0 +1,55 @@
+/*
+ * 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
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.Transformations
+import com.zhuinden.monarchy.Monarchy
+import io.realm.Realm
+import org.matrix.android.sdk.api.extensions.orFalse
+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.query.where
+import org.matrix.android.sdk.internal.di.SessionDatabase
+import javax.inject.Inject
+
+internal class RoomDataSource @Inject constructor(
+        @SessionDatabase private val monarchy: Monarchy,
+) {
+    fun getRoomMembersLoadStatus(roomId: String): RoomMembersLoadStatusType {
+        var result: RoomMembersLoadStatusType?
+        Realm.getInstance(monarchy.realmConfiguration).use {
+            result = RoomEntity.where(it, roomId).findFirst()?.membersLoadStatus
+        }
+        return result ?: RoomMembersLoadStatusType.NONE
+    }
+
+    fun getRoomMembersLoadStatusLive(roomId: String): LiveData<Boolean> {
+        val liveData = monarchy.findAllMappedWithChanges(
+                {
+                    RoomEntity.where(it, roomId)
+                },
+                {
+                    it.membersLoadStatus == RoomMembersLoadStatusType.LOADED
+                }
+        )
+
+        return Transformations.map(liveData) { results ->
+            results.firstOrNull().orFalse()
+        }
+    }
+}
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 c4d37d124b2f2d664762205f25742d79ba315dd2..d01324a35f91b57cb69596653fdf972d77db838b 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,8 +43,12 @@ 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.CreateLocalRoomTask
 import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask
+import org.matrix.android.sdk.internal.session.room.create.DefaultCreateLocalRoomTask
 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
 import org.matrix.android.sdk.internal.session.room.directory.DefaultGetPublicRoomTask
 import org.matrix.android.sdk.internal.session.room.directory.DefaultGetRoomDirectoryVisibilityTask
 import org.matrix.android.sdk.internal.session.room.directory.DefaultSetRoomDirectoryVisibilityTask
@@ -54,11 +58,13 @@ import org.matrix.android.sdk.internal.session.room.directory.SetRoomDirectoryVi
 import org.matrix.android.sdk.internal.session.room.location.CheckIfExistingActiveLiveTask
 import org.matrix.android.sdk.internal.session.room.location.DefaultCheckIfExistingActiveLiveTask
 import org.matrix.android.sdk.internal.session.room.location.DefaultGetActiveBeaconInfoForUserTask
+import org.matrix.android.sdk.internal.session.room.location.DefaultRedactLiveLocationShareTask
 import org.matrix.android.sdk.internal.session.room.location.DefaultSendLiveLocationTask
 import org.matrix.android.sdk.internal.session.room.location.DefaultSendStaticLocationTask
 import org.matrix.android.sdk.internal.session.room.location.DefaultStartLiveLocationShareTask
 import org.matrix.android.sdk.internal.session.room.location.DefaultStopLiveLocationShareTask
 import org.matrix.android.sdk.internal.session.room.location.GetActiveBeaconInfoForUserTask
+import org.matrix.android.sdk.internal.session.room.location.RedactLiveLocationShareTask
 import org.matrix.android.sdk.internal.session.room.location.SendLiveLocationTask
 import org.matrix.android.sdk.internal.session.room.location.SendStaticLocationTask
 import org.matrix.android.sdk.internal.session.room.location.StartLiveLocationShareTask
@@ -204,6 +210,12 @@ internal abstract class RoomModule {
     @Binds
     abstract fun bindCreateRoomTask(task: DefaultCreateRoomTask): CreateRoomTask
 
+    @Binds
+    abstract fun bindCreateLocalRoomTask(task: DefaultCreateLocalRoomTask): CreateLocalRoomTask
+
+    @Binds
+    abstract fun bindDeleteLocalRoomTask(task: DefaultDeleteLocalRoomTask): DeleteLocalRoomTask
+
     @Binds
     abstract fun bindGetPublicRoomTask(task: DefaultGetPublicRoomTask): GetPublicRoomTask
 
@@ -329,4 +341,7 @@ internal abstract class RoomModule {
 
     @Binds
     abstract fun bindCheckIfExistingActiveLiveTask(task: DefaultCheckIfExistingActiveLiveTask): CheckIfExistingActiveLiveTask
+
+    @Binds
+    abstract fun bindRedactLiveLocationShareTask(task: DefaultRedactLiveLocationShareTask): RedactLiveLocationShareTask
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt
index 921749122b3e511dcf7246ab05f3fc9161d75d03..510c20497b1aee8e1605f776447c563271f091b3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.aggregation.livelocation
 
 import androidx.work.ExistingWorkPolicy
 import io.realm.Realm
+import io.realm.RealmList
 import org.matrix.android.sdk.api.extensions.orTrue
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.toContent
@@ -73,16 +74,22 @@ internal class LiveLocationAggregationProcessor @Inject constructor(
                 eventId = targetEventId
         )
 
+        if (!isLive && !event.eventId.isNullOrEmpty()) {
+            // in this case, the received event is a new state event related to the previous one
+            addRelatedEventId(event.eventId, aggregatedSummary)
+        }
+
         // remote event can stay with isLive == true while the local summary is no more active
         val isActive = aggregatedSummary.isActive.orTrue() && isLive
         val endOfLiveTimestampMillis = content.getBestTimestampMillis()?.let { it + (content.timeout ?: 0) }
         Timber.d("updating summary of id=$targetEventId with isActive=$isActive and endTimestamp=$endOfLiveTimestampMillis")
 
+        aggregatedSummary.startOfLiveTimestampMillis = content.getBestTimestampMillis()
         aggregatedSummary.endOfLiveTimestampMillis = endOfLiveTimestampMillis
         aggregatedSummary.isActive = isActive
         aggregatedSummary.userId = event.senderId
 
-        deactivateAllPreviousBeacons(realm, roomId, event.senderId, targetEventId)
+        deactivateAllPreviousBeacons(realm, roomId, event.senderId, targetEventId, content.getBestTimestampMillis() ?: 0)
 
         if (isActive) {
             scheduleDeactivationAfterTimeout(targetEventId, roomId, endOfLiveTimestampMillis)
@@ -144,6 +151,11 @@ internal class LiveLocationAggregationProcessor @Inject constructor(
                 roomId = roomId,
                 eventId = relatedEventId
         )
+
+        if (!event.eventId.isNullOrEmpty()) {
+            addRelatedEventId(event.eventId, aggregatedSummary)
+        }
+
         val updatedLocationTimestamp = content.getBestTimestampMillis() ?: 0
         val currentLocationTimestamp = ContentMapper
                 .map(aggregatedSummary.lastLocationContent)
@@ -160,13 +172,31 @@ internal class LiveLocationAggregationProcessor @Inject constructor(
         }
     }
 
-    private fun deactivateAllPreviousBeacons(realm: Realm, roomId: String, userId: String, currentEventId: String) {
+    private fun addRelatedEventId(
+            eventId: String,
+            aggregatedSummary: LiveLocationShareAggregatedSummaryEntity
+    ) {
+        Timber.d("adding related event id $eventId to summary of id ${aggregatedSummary.eventId}")
+        val updatedEventIds = aggregatedSummary.relatedEventIds.toMutableList().also {
+            it.add(eventId)
+        }
+        aggregatedSummary.relatedEventIds = RealmList(*updatedEventIds.toTypedArray())
+    }
+
+    private fun deactivateAllPreviousBeacons(
+            realm: Realm,
+            roomId: String,
+            userId: String,
+            currentEventId: String,
+            currentEventTimestamp: Long
+    ) {
         LiveLocationShareAggregatedSummaryEntity
                 .findActiveLiveInRoomForUser(
                         realm = realm,
                         roomId = roomId,
                         userId = userId,
-                        ignoredEventId = currentEventId
+                        ignoredEventId = currentEventId,
+                        startOfLiveTimestampThreshold = currentEventTimestamp
                 )
                 .forEach { it.isActive = false }
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomLocalAliasesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomLocalAliasesTask.kt
index 1ff4156ed3e857b2439c007af34ca91018383587..62681c89d87ee90b67fdee2c05121adf51010ea0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomLocalAliasesTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/GetRoomLocalAliasesTask.kt
@@ -34,7 +34,6 @@ internal class DefaultGetRoomLocalAliasesTask @Inject constructor(
 ) : GetRoomLocalAliasesTask {
 
     override suspend fun execute(params: GetRoomLocalAliasesTask.Params): List<String> {
-        // We do not check for "org.matrix.msc2432", so the API may be missing
         val response = executeRequest(globalErrorReceiver) {
             roomAPI.getAliases(roomId = params.roomId)
         }
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
new file mode 100644
index 0000000000000000000000000000000000000000..d57491a4c86f22b92fd167518074cba4a54140db
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt
@@ -0,0 +1,267 @@
+/*
+ * 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.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.database.awaitNotEmptyResult
+import org.matrix.android.sdk.internal.database.helper.addTimelineEvent
+import org.matrix.android.sdk.internal.database.mapper.asDomain
+import org.matrix.android.sdk.internal.database.mapper.toEntity
+import org.matrix.android.sdk.internal.database.model.ChunkEntity
+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.RoomEntity
+import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType
+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.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
+import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
+import org.matrix.android.sdk.internal.task.Task
+import org.matrix.android.sdk.internal.util.awaitTransaction
+import org.matrix.android.sdk.internal.util.time.Clock
+import java.util.concurrent.TimeUnit
+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 clock: Clock,
+) : CreateLocalRoomTask {
+
+    override suspend fun execute(params: CreateRoomParams): String {
+        val createRoomBody = createRoomBodyBuilder.build(params.withDefault())
+        val roomId = RoomLocalEcho.createLocalEchoId()
+        monarchy.awaitTransaction { realm ->
+            createLocalRoomEntity(realm, roomId, createRoomBody)
+            createLocalRoomSummaryEntity(realm, roomId, createRoomBody)
+        }
+
+        // Wait for room to be created in DB
+        try {
+            awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm ->
+                realm.where(RoomSummaryEntity::class.java)
+                        .equalTo(RoomSummaryEntityFields.ROOM_ID, roomId)
+                        .equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name)
+            }
+        } catch (exception: TimeoutCancellationException) {
+            throw CreateRoomFailure.CreatedWithTimeout(roomId)
+        }
+
+        return roomId
+    }
+
+    /**
+     * Create a local room entity from the given room creation params.
+     * This will also generate and store in database the chunk and the events related to the room params in order to retrieve and display the local room.
+     */
+    private suspend fun createLocalRoomEntity(realm: Realm, roomId: String, createRoomBody: CreateRoomBody) {
+        RoomEntity.getOrCreate(realm, roomId).apply {
+            membership = Membership.JOIN
+            chunks.add(createLocalRoomChunk(realm, roomId, createRoomBody))
+            membersLoadStatus = RoomMembersLoadStatusType.LOADED
+        }
+    }
+
+    private fun createLocalRoomSummaryEntity(realm: Realm, roomId: String, createRoomBody: CreateRoomBody) {
+        val otherUserId = createRoomBody.getDirectUserId()
+        if (otherUserId != null) {
+            RoomSummaryEntity.getOrCreate(realm, roomId).apply {
+                isDirect = true
+                directUserId = otherUserId
+            }
+        }
+        roomSummaryUpdater.update(
+                realm = realm,
+                roomId = roomId,
+                membership = Membership.JOIN,
+                roomSummary = RoomSyncSummary(
+                        heroes = createRoomBody.invitedUserIds.orEmpty().take(5),
+                        joinedMembersCount = 1,
+                        invitedMembersCount = createRoomBody.invitedUserIds?.size ?: 0
+                ),
+                updateMembers = !createRoomBody.invitedUserIds.isNullOrEmpty()
+        )
+    }
+
+    /**
+     * Create a single chunk containing the necessary events to display the local room.
+     *
+     * @param realm the current instance of realm
+     * @param roomId the id of the local room
+     * @param createRoomBody the room creation params
+     *
+     * @return a chunk entity
+     */
+    private suspend fun createLocalRoomChunk(realm: Realm, roomId: String, createRoomBody: CreateRoomBody): ChunkEntity {
+        val chunkEntity = realm.createObject<ChunkEntity>().apply {
+            isLastBackward = true
+            isLastForward = true
+        }
+
+        val eventList = createLocalRoomEvents(createRoomBody)
+        val roomMemberContentsByUser = HashMap<String, RoomMemberContent?>()
+
+        for (event in eventList) {
+            if (event.eventId == null || event.senderId == null || event.type == null) {
+                continue
+            }
+
+            val now = clock.epochMillis()
+            val eventEntity = event.toEntity(roomId, SendState.SYNCED, now).copyToRealmOrIgnore(realm, EventInsertType.INCREMENTAL_SYNC)
+            if (event.stateKey != null) {
+                CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
+                    eventId = event.eventId
+                    root = eventEntity
+                }
+                if (event.type == EventType.STATE_ROOM_MEMBER) {
+                    roomMemberContentsByUser[event.stateKey] = event.getFixedRoomMemberContent()
+                    roomMemberEventHandler.handle(realm, roomId, event, false)
+                }
+            }
+
+            roomMemberContentsByUser.getOrPut(event.senderId) {
+                // If we don't have any new state on this user, get it from db
+                val rootStateEvent = CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root
+                rootStateEvent?.asDomain()?.getFixedRoomMemberContent()
+            }
+
+            chunkEntity.addTimelineEvent(
+                    roomId = roomId,
+                    eventEntity = eventEntity,
+                    direction = PaginationDirection.FORWARDS,
+                    roomMemberContentsByUser = roomMemberContentsByUser
+            )
+        }
+
+        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 cffa632768ca7f2e3fbc02645fa56c8f721877a8..b326c3618c46ab54d53037c05c3a4ed406258f97 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
@@ -120,3 +120,20 @@ internal data class CreateRoomBody(
         @Json(name = "room_version")
         val roomVersion: String?
 )
+
+/**
+ * Tells if the created room can be a direct chat one.
+ *
+ * @return true if it is a direct chat
+ */
+private fun CreateRoomBody.isDirect(): Boolean {
+    return preset == CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT && isDirect == true
+}
+
+internal fun CreateRoomBody.getDirectUserId(): String? {
+    return if (isDirect()) {
+        invitedUserIds?.firstOrNull()
+                ?: invite3pids?.firstOrNull()?.address
+                ?: throw IllegalStateException("You can't create a direct room without an invitedUser")
+    } else null
+}
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 6dd2c91048519738626cd6e85fc5db4e2ff72aca..d76640573fc27916bae112cc71249e482cba167c 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
@@ -62,11 +62,6 @@ internal class DefaultCreateRoomTask @Inject constructor(
 ) : CreateRoomTask {
 
     override suspend fun execute(params: CreateRoomParams): String {
-        val otherUserId = if (params.isDirect()) {
-            params.getFirstInvitedUserId()
-                    ?: throw IllegalStateException("You can't create a direct room without an invitedUser")
-        } else null
-
         if (params.preset == CreateRoomPreset.PRESET_PUBLIC_CHAT) {
             try {
                 aliasAvailabilityChecker.check(params.roomAliasName)
@@ -111,14 +106,13 @@ internal class DefaultCreateRoomTask @Inject constructor(
             RoomSummaryEntity.where(it, roomId).findFirst()?.lastActivityTime = clock.epochMillis()
         }
 
-        if (otherUserId != null) {
-            handleDirectChatCreation(roomId, otherUserId)
-        }
+        handleDirectChatCreation(roomId, createRoomBody.getDirectUserId())
         setReadMarkers(roomId)
         return roomId
     }
 
-    private suspend fun handleDirectChatCreation(roomId: String, otherUserId: String) {
+    private suspend fun handleDirectChatCreation(roomId: String, otherUserId: String?) {
+        otherUserId ?: return // This is not a direct room
         monarchy.awaitTransaction { realm ->
             RoomSummaryEntity.where(realm, roomId).findFirst()?.apply {
                 this.directUserId = otherUserId
@@ -133,21 +127,4 @@ internal class DefaultCreateRoomTask @Inject constructor(
         val setReadMarkerParams = SetReadMarkersTask.Params(roomId, forceReadReceipt = true, forceReadMarker = true)
         return readMarkersTask.execute(setReadMarkerParams)
     }
-
-    /**
-     * Tells if the created room can be a direct chat one.
-     *
-     * @return true if it is a direct chat
-     */
-    private fun CreateRoomParams.isDirect(): Boolean {
-        return preset == CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT &&
-                isDirect == true
-    }
-
-    /**
-     * @return the first invited user id
-     */
-    private fun CreateRoomParams.getFirstInvitedUserId(): String? {
-        return invitedUserIds.firstOrNull() ?: invite3pids.firstOrNull()?.value
-    }
 }
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
new file mode 100644
index 0000000000000000000000000000000000000000..936c94e520f6be63319ef5446ff643045bbd9b1d
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/delete/DeleteLocalRoomTask.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.delete
+
+import com.zhuinden.monarchy.Monarchy
+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.RoomEntity
+import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
+import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
+import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
+import org.matrix.android.sdk.internal.database.model.deleteOnCascade
+import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.database.query.whereRoomId
+import org.matrix.android.sdk.internal.di.SessionDatabase
+import org.matrix.android.sdk.internal.session.room.delete.DeleteLocalRoomTask.Params
+import org.matrix.android.sdk.internal.task.Task
+import org.matrix.android.sdk.internal.util.awaitTransaction
+import timber.log.Timber
+import javax.inject.Inject
+
+internal interface DeleteLocalRoomTask : Task<Params, Unit> {
+    data class Params(val roomId: String)
+}
+
+internal class DefaultDeleteLocalRoomTask @Inject constructor(
+        @SessionDatabase private val monarchy: Monarchy,
+) : DeleteLocalRoomTask {
+
+    override suspend fun execute(params: Params) {
+        val roomId = params.roomId
+
+        if (RoomLocalEcho.isLocalEchoId(roomId)) {
+            monarchy.awaitTransaction { realm ->
+                Timber.i("## DeleteLocalRoomTask - delete local room id $roomId")
+                RoomMemberSummaryEntity.where(realm, roomId = roomId).findAll()
+                        ?.also { Timber.i("## DeleteLocalRoomTask - RoomMemberSummaryEntity - delete ${it.size} entries") }
+                        ?.deleteAllFromRealm()
+                CurrentStateEventEntity.whereRoomId(realm, roomId = roomId).findAll()
+                        ?.also { Timber.i("## DeleteLocalRoomTask - CurrentStateEventEntity - delete ${it.size} entries") }
+                        ?.deleteAllFromRealm()
+                EventEntity.whereRoomId(realm, roomId = roomId).findAll()
+                        ?.also { Timber.i("## DeleteLocalRoomTask - EventEntity - delete ${it.size} entries") }
+                        ?.deleteAllFromRealm()
+                TimelineEventEntity.whereRoomId(realm, roomId = roomId).findAll()
+                        ?.also { Timber.i("## DeleteLocalRoomTask - TimelineEventEntity - delete ${it.size} entries") }
+                        ?.forEach { it.deleteOnCascade(true) }
+                ChunkEntity.where(realm, roomId = roomId).findAll()
+                        ?.also { Timber.i("## DeleteLocalRoomTask - ChunkEntity - delete ${it.size} entries") }
+                        ?.forEach { it.deleteOnCascade(deleteStateEvents = true, canDeleteRoot = true) }
+                RoomSummaryEntity.where(realm, roomId = roomId).findAll()
+                        ?.also { Timber.i("## DeleteLocalRoomTask - RoomSummaryEntity - delete ${it.size} entries") }
+                        ?.deleteAllFromRealm()
+                RoomEntity.where(realm, roomId = roomId).findAll()
+                        ?.also { Timber.i("## DeleteLocalRoomTask - RoomEntity - 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/location/DefaultLocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt
index 20320cad230919b30ddfe0225e18999b69e4b265..60312071d7010f7859e6bcb915202d306d65a0cd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt
@@ -42,6 +42,7 @@ internal class DefaultLocationSharingService @AssistedInject constructor(
         private val startLiveLocationShareTask: StartLiveLocationShareTask,
         private val stopLiveLocationShareTask: StopLiveLocationShareTask,
         private val checkIfExistingActiveLiveTask: CheckIfExistingActiveLiveTask,
+        private val redactLiveLocationShareTask: RedactLiveLocationShareTask,
         private val liveLocationShareAggregatedSummaryMapper: LiveLocationShareAggregatedSummaryMapper,
 ) : LocationSharingService {
 
@@ -72,7 +73,7 @@ internal class DefaultLocationSharingService @AssistedInject constructor(
         return sendLiveLocationTask.execute(params)
     }
 
-    override suspend fun startLiveLocationShare(timeoutMillis: Long): UpdateLiveLocationShareResult {
+    override suspend fun startLiveLocationShare(timeoutMillis: Long, description: String): UpdateLiveLocationShareResult {
         // Ensure to stop any active live before starting a new one
         if (checkIfExistingActiveLive()) {
             val result = stopLiveLocationShare()
@@ -82,7 +83,8 @@ internal class DefaultLocationSharingService @AssistedInject constructor(
         }
         val params = StartLiveLocationShareTask.Params(
                 roomId = roomId,
-                timeoutMillis = timeoutMillis
+                timeoutMillis = timeoutMillis,
+                description = description
         )
         return startLiveLocationShareTask.execute(params)
     }
@@ -101,6 +103,15 @@ internal class DefaultLocationSharingService @AssistedInject constructor(
         return stopLiveLocationShareTask.execute(params)
     }
 
+    override suspend fun redactLiveLocationShare(beaconInfoEventId: String, reason: String?) {
+        val params = RedactLiveLocationShareTask.Params(
+                roomId = roomId,
+                beaconInfoEventId = beaconInfoEventId,
+                reason = reason
+        )
+        return redactLiveLocationShareTask.execute(params)
+    }
+
     override fun getRunningLiveLocationShareSummaries(): LiveData<List<LiveLocationShareAggregatedSummary>> {
         return monarchy.findAllMappedWithChanges(
                 { LiveLocationShareAggregatedSummaryEntity.findRunningLiveInRoom(it, roomId = roomId) },
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt
new file mode 100644
index 0000000000000000000000000000000000000000..fa3479ed3c3155d625a499ff55f8d89890e92cc8
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.location
+
+import io.realm.Realm
+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.internal.database.model.EventAnnotationsSummaryEntity
+import org.matrix.android.sdk.internal.database.model.EventEntity
+import org.matrix.android.sdk.internal.database.model.EventInsertType
+import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
+import org.matrix.android.sdk.internal.database.query.get
+import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
+import timber.log.Timber
+import javax.inject.Inject
+
+/**
+ * Listens to the database for the insertion of any redaction event.
+ * Delete specifically the aggregated summary related to a redacted live location share event.
+ */
+internal class LiveLocationShareRedactionEventProcessor @Inject constructor() : EventInsertLiveProcessor {
+
+    override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean {
+        return eventType == EventType.REDACTION && insertType != EventInsertType.LOCAL_ECHO
+    }
+
+    override suspend fun process(realm: Realm, event: Event) {
+        if (event.redacts.isNullOrBlank() || LocalEcho.isLocalEchoId(event.eventId.orEmpty())) {
+            return
+        }
+
+        val redactedEvent = EventEntity.where(realm, eventId = event.redacts).findFirst()
+                ?: return
+
+        if (redactedEvent.type in EventType.STATE_ROOM_BEACON_INFO) {
+            val liveSummary = LiveLocationShareAggregatedSummaryEntity.get(realm, eventId = redactedEvent.eventId)
+
+            if (liveSummary != null) {
+                Timber.d("deleting live summary with id: ${liveSummary.eventId}")
+                liveSummary.deleteFromRealm()
+                val annotationsSummary = EventAnnotationsSummaryEntity.get(realm, eventId = redactedEvent.eventId)
+                if (annotationsSummary != null) {
+                    Timber.d("deleting annotation summary with id: ${annotationsSummary.eventId}")
+                    annotationsSummary.deleteFromRealm()
+                }
+            }
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/RedactLiveLocationShareTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/RedactLiveLocationShareTask.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ac855b81e7ce701192f60eefe1313177bdbd5027
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/RedactLiveLocationShareTask.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.location
+
+import io.realm.RealmConfiguration
+import org.matrix.android.sdk.internal.database.awaitTransaction
+import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
+import org.matrix.android.sdk.internal.database.query.get
+import org.matrix.android.sdk.internal.di.SessionDatabase
+import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
+import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
+import org.matrix.android.sdk.internal.task.Task
+import timber.log.Timber
+import javax.inject.Inject
+
+internal interface RedactLiveLocationShareTask : Task<RedactLiveLocationShareTask.Params, Unit> {
+    data class Params(
+            val roomId: String,
+            val beaconInfoEventId: String,
+            val reason: String?
+    )
+}
+
+internal class DefaultRedactLiveLocationShareTask @Inject constructor(
+        @SessionDatabase private val realmConfiguration: RealmConfiguration,
+        private val localEchoEventFactory: LocalEchoEventFactory,
+        private val eventSenderProcessor: EventSenderProcessor,
+) : RedactLiveLocationShareTask {
+
+    override suspend fun execute(params: RedactLiveLocationShareTask.Params) {
+        val relatedEventIds = getRelatedEventIdsOfLive(params.beaconInfoEventId)
+        Timber.d("beacon with id ${params.beaconInfoEventId} has related event ids: ${relatedEventIds.joinToString(", ")}")
+
+        postRedactionWithLocalEcho(
+                eventId = params.beaconInfoEventId,
+                roomId = params.roomId,
+                reason = params.reason
+        )
+        relatedEventIds.forEach { eventId ->
+            postRedactionWithLocalEcho(
+                    eventId = eventId,
+                    roomId = params.roomId,
+                    reason = params.reason
+            )
+        }
+    }
+
+    private suspend fun getRelatedEventIdsOfLive(beaconInfoEventId: String): List<String> {
+        return awaitTransaction(realmConfiguration) { realm ->
+            val aggregatedSummaryEntity = LiveLocationShareAggregatedSummaryEntity.get(
+                    realm = realm,
+                    eventId = beaconInfoEventId
+            )
+            aggregatedSummaryEntity?.relatedEventIds?.toList() ?: emptyList()
+        }
+    }
+
+    private fun postRedactionWithLocalEcho(eventId: String, roomId: String, reason: String?) {
+        Timber.d("posting redaction for event of id $eventId")
+        val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, eventId, reason)
+        localEchoEventFactory.createLocalEcho(redactionEcho)
+        eventSenderProcessor.postRedaction(redactionEcho, reason)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt
index b943c279778b1ef043b7bc4222ee306fb791ac90..79019e47658123085b9abb3bcf632170251812a7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt
@@ -30,6 +30,7 @@ internal interface StartLiveLocationShareTask : Task<StartLiveLocationShareTask.
     data class Params(
             val roomId: String,
             val timeoutMillis: Long,
+            val description: String,
     )
 }
 
@@ -41,6 +42,7 @@ internal class DefaultStartLiveLocationShareTask @Inject constructor(
 
     override suspend fun execute(params: StartLiveLocationShareTask.Params): UpdateLiveLocationShareResult {
         val beaconContent = MessageBeaconInfoContent(
+                body = params.description,
                 timeout = params.timeoutMillis,
                 isLive = true,
                 unstableTimestampMillis = clock.epochMillis()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt
index ef89ca33a723f71b52f60b82479573ad505fb40d..20708b38142f65c87df88e257d861cd783ffce69 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt
@@ -17,24 +17,31 @@
 package org.matrix.android.sdk.internal.session.room.membership
 
 import androidx.lifecycle.LiveData
+import com.otaliastudios.opengl.core.use
 import com.zhuinden.monarchy.Monarchy
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import io.realm.Realm
 import io.realm.RealmQuery
+import org.matrix.android.sdk.api.MatrixConfiguration
+import org.matrix.android.sdk.api.session.crypto.CryptoService
 import org.matrix.android.sdk.api.session.identity.ThreePid
 import org.matrix.android.sdk.api.session.room.members.MembershipService
 import org.matrix.android.sdk.api.session.room.members.RoomMemberQueryParams
 import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
+import org.matrix.android.sdk.internal.database.helper.findLatestSessionInfo
 import org.matrix.android.sdk.internal.database.mapper.asDomain
+import org.matrix.android.sdk.internal.database.model.ChunkEntity
 import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
 import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
+import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType
 import org.matrix.android.sdk.internal.di.SessionDatabase
 import org.matrix.android.sdk.internal.di.UserId
 import org.matrix.android.sdk.internal.query.QueryStringValueProcessor
 import org.matrix.android.sdk.internal.query.process
+import org.matrix.android.sdk.internal.session.room.RoomDataSource
 import org.matrix.android.sdk.internal.session.room.membership.admin.MembershipAdminTask
 import org.matrix.android.sdk.internal.session.room.membership.joining.InviteTask
 import org.matrix.android.sdk.internal.session.room.membership.threepid.InviteThreePidTask
@@ -47,8 +54,11 @@ internal class DefaultMembershipService @AssistedInject constructor(
         private val inviteTask: InviteTask,
         private val inviteThreePidTask: InviteThreePidTask,
         private val membershipAdminTask: MembershipAdminTask,
+        private val roomDataSource: RoomDataSource,
+        private val cryptoService: CryptoService,
         @UserId
         private val userId: String,
+        private val matrixConfiguration: MatrixConfiguration,
         private val queryStringValueProcessor: QueryStringValueProcessor
 ) : MembershipService {
 
@@ -62,6 +72,15 @@ internal class DefaultMembershipService @AssistedInject constructor(
         loadRoomMembersTask.execute(params)
     }
 
+    override suspend fun areAllMembersLoaded(): Boolean {
+        val status = roomDataSource.getRoomMembersLoadStatus(roomId)
+        return status == RoomMembersLoadStatusType.LOADED
+    }
+
+    override fun areAllMembersLoadedLive(): LiveData<Boolean> {
+        return roomDataSource.getRoomMembersLoadStatusLive(roomId)
+    }
+
     override fun getRoomMember(userId: String): RoomMemberSummary? {
         val roomMemberEntity = monarchy.fetchCopied {
             RoomMemberHelper(it, roomId).getLastRoomMember(userId)
@@ -127,10 +146,20 @@ internal class DefaultMembershipService @AssistedInject constructor(
     }
 
     override suspend fun invite(userId: String, reason: String?) {
+        sendShareHistoryKeysIfNeeded(userId)
         val params = InviteTask.Params(roomId, userId, reason)
         inviteTask.execute(params)
     }
 
+    private suspend fun sendShareHistoryKeysIfNeeded(userId: String) {
+        if (!cryptoService.isShareKeysOnInviteEnabled()) return
+        // TODO not sure it's the right way to get the latest messages in a room
+        val sessionInfo = Realm.getInstance(monarchy.realmConfiguration).use {
+            ChunkEntity.findLatestSessionInfo(it, roomId)
+        }
+        cryptoService.sendSharedHistoryKeys(roomId, userId, sessionInfo)
+    }
+
     override suspend fun invite3pid(threePid: ThreePid) {
         val params = InviteThreePidTask.Params(roomId, threePid)
         return inviteThreePidTask.execute(params)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt
index 7052eb23e247c99c4d195a0850588e3fdf85f334..c02049f40d9813022c4461f50aa1580d99848820 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt
@@ -17,7 +17,6 @@
 package org.matrix.android.sdk.internal.session.room.membership
 
 import com.zhuinden.monarchy.Monarchy
-import io.realm.Realm
 import io.realm.kotlin.createObject
 import kotlinx.coroutines.TimeoutCancellationException
 import org.matrix.android.sdk.api.session.room.model.Membership
@@ -38,6 +37,7 @@ import org.matrix.android.sdk.internal.di.SessionDatabase
 import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
 import org.matrix.android.sdk.internal.network.executeRequest
 import org.matrix.android.sdk.internal.session.room.RoomAPI
+import org.matrix.android.sdk.internal.session.room.RoomDataSource
 import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater
 import org.matrix.android.sdk.internal.session.sync.SyncTokenStore
 import org.matrix.android.sdk.internal.task.Task
@@ -58,6 +58,7 @@ internal interface LoadRoomMembersTask : Task<LoadRoomMembersTask.Params, Unit>
 internal class DefaultLoadRoomMembersTask @Inject constructor(
         private val roomAPI: RoomAPI,
         @SessionDatabase private val monarchy: Monarchy,
+        private val roomDataSource: RoomDataSource,
         private val syncTokenStore: SyncTokenStore,
         private val roomSummaryUpdater: RoomSummaryUpdater,
         private val roomMemberEventHandler: RoomMemberEventHandler,
@@ -68,7 +69,7 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(
 ) : LoadRoomMembersTask {
 
     override suspend fun execute(params: LoadRoomMembersTask.Params) {
-        when (getRoomMembersLoadStatus(params.roomId)) {
+        when (roomDataSource.getRoomMembersLoadStatus(params.roomId)) {
             RoomMembersLoadStatusType.NONE -> doRequest(params)
             RoomMembersLoadStatusType.LOADING -> waitPreviousRequestToFinish(params)
             RoomMembersLoadStatusType.LOADED -> Unit
@@ -142,14 +143,6 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(
         }
     }
 
-    private fun getRoomMembersLoadStatus(roomId: String): RoomMembersLoadStatusType {
-        var result: RoomMembersLoadStatusType?
-        Realm.getInstance(monarchy.realmConfiguration).use {
-            result = RoomEntity.where(it, roomId).findFirst()?.membersLoadStatus
-        }
-        return result ?: RoomMembersLoadStatusType.NONE
-    }
-
     private suspend fun setRoomMembersLoadStatus(roomId: String, status: RoomMembersLoadStatusType) {
         monarchy.awaitTransaction { realm ->
             val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt
index e33fbb56b19d72b3124d880c6dd15cb9ede3b0a3..cc86679cbc79c8cbf2f0c5495420487a8f4dd0d7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt
@@ -74,6 +74,8 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr
             when (typeToPrune) {
                 EventType.ENCRYPTED,
                 EventType.MESSAGE,
+                in EventType.STATE_ROOM_BEACON_INFO,
+                in EventType.BEACON_LOCATION_DATA,
                 in EventType.POLL_START -> {
                     Timber.d("REDACTION for message ${eventToPrune.eventId}")
                     val unsignedData = EventMapper.map(eventToPrune).unsignedData
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
index f52500de1b58638ba3d0c26021007653e7e10d78..4fbc91e9ec2d788036ce1ac3c5ca92d7af261458 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
@@ -20,6 +20,7 @@ import android.content.Context
 import android.graphics.Bitmap
 import android.media.MediaMetadataRetriever
 import androidx.exifinterface.media.ExifInterface
+import org.matrix.android.sdk.api.extensions.ensureNotEmpty
 import org.matrix.android.sdk.api.session.content.ContentAttachmentData
 import org.matrix.android.sdk.api.session.events.model.Content
 import org.matrix.android.sdk.api.session.events.model.Event
@@ -700,6 +701,7 @@ internal class LocalEchoEventFactory @Inject constructor(
             MessageType.MSGTYPE_AUDIO -> return TextContent("sent an audio file.")
             MessageType.MSGTYPE_IMAGE -> return TextContent("sent an image.")
             MessageType.MSGTYPE_VIDEO -> return TextContent("sent a video.")
+            MessageType.MSGTYPE_BEACON_INFO -> return TextContent(content.body.ensureNotEmpty() ?: "Live location")
             MessageType.MSGTYPE_POLL_START -> {
                 return TextContent((content as? MessagePollContent)?.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "")
             }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorThread.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorThread.kt
index c5b13043d7c5f49399ba4d18c624b5cb8c589494..51107c96557bd6737287e1d67f5b6fd1b8c6a02b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorThread.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorThread.kt
@@ -119,7 +119,7 @@ internal class EventSenderProcessorThread @Inject constructor(
 
     override fun cancel(eventId: String, roomId: String) {
         (currentTask as? SendEventQueuedTask)
-                ?.takeIf { it -> it.event.eventId == eventId && it.event.roomId == roomId }
+                ?.takeIf { it.event.eventId == eventId && it.event.roomId == roomId }
                 ?.cancel()
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt
index cb7dc270e8c47085e6501631d0a30a77738ccc56..82fc94df7c8fcdb80714d5e684a70e47b54ebd63 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryDataSource.kt
@@ -200,14 +200,13 @@ internal class RoomSummaryDataSource @Inject constructor(
             queryParams: RoomSummaryQueryParams,
             pagedListConfig: PagedList.Config,
             sortOrder: RoomSortOrder,
-            getFlattenedParents: Boolean = false
     ): UpdatableLivePageResult {
         val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
             roomSummariesQuery(realm, queryParams).process(sortOrder)
         }
         val dataSourceFactory = realmDataSourceFactory.map {
             roomSummaryMapper.map(it)
-        }.map { if (getFlattenedParents) it.getWithParents() else it }
+        }
 
         val boundaries = MutableLiveData(ResultBoundaries())
 
@@ -246,13 +245,6 @@ internal class RoomSummaryDataSource @Inject constructor(
         }
     }
 
-    private fun RoomSummary.getWithParents(): RoomSummary {
-        val parents = flattenParentIds.mapNotNull { parentId ->
-            getRoomSummary(parentId)
-        }
-        return copy(flattenParents = parents)
-    }
-
     fun getCountLive(queryParams: RoomSummaryQueryParams): LiveData<Int> {
         val liveRooms = monarchy.findAllManagedWithChanges {
             roomSummariesQuery(it, queryParams)
@@ -325,12 +317,9 @@ internal class RoomSummaryDataSource @Inject constructor(
             is SpaceFilter.ExcludeSpace -> {
                 query.not().contains(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, queryParams.spaceFilter.spaceId)
             }
-            null -> Unit // nop
+            SpaceFilter.NoFilter -> Unit // nop
         }
 
-        queryParams.activeGroupId?.let { activeGroupId ->
-            query.contains(RoomSummaryEntityFields.GROUP_IDS, activeGroupId)
-        }
         return query
     }
 
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 e4afe7aa49dadee1905ead0a64c499e3a1dcd33f..7e064a84ecf039a60b475b7f7104950be70d01ae 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
@@ -44,7 +44,6 @@ import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningSe
 import org.matrix.android.sdk.internal.database.mapper.ContentMapper
 import org.matrix.android.sdk.internal.database.mapper.asDomain
 import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
-import org.matrix.android.sdk.internal.database.model.GroupSummaryEntity
 import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
 import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
 import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
@@ -224,6 +223,7 @@ internal class RoomSummaryUpdater @Inject constructor(
                     .sort(RoomSummaryEntityFields.ROOM_ID)
                     .findAll().map {
                         it.flattenParentIds = null
+                        it.directParentNames.clear()
                         it to emptyList<RoomSummaryEntity>().toMutableSet()
                     }
                     .toMap()
@@ -351,39 +351,29 @@ internal class RoomSummaryUpdater @Inject constructor(
             }
 
             val acyclicGraph = graph.withoutEdges(backEdges)
-//            Timber.v("## SPACES: acyclicGraph $acyclicGraph")
             val flattenSpaceParents = acyclicGraph.flattenDestination().map {
                 it.key.name to it.value.map { it.name }
             }.toMap()
-//            Timber.v("## SPACES: flattenSpaceParents ${flattenSpaceParents.map { it.key.name to it.value.map { it.name } }.joinToString("\n") {
-//                it.first + ": [" + it.second.joinToString(",") + "]"
-//            }}")
-
-//            Timber.v("## SPACES: lookup map ${lookupMap.map { it.key.name to it.value.map { it.name } }.toMap()}")
 
             lookupMap.entries
                     .filter { it.key.roomType == RoomType.SPACE && it.key.membership == Membership.JOIN }
                     .forEach { entry ->
                         val parent = RoomSummaryEntity.where(realm, entry.key.roomId).findFirst()
                         if (parent != null) {
-//                            Timber.v("## SPACES: check hierarchy of ${parent.name} id ${parent.roomId}")
-//                            Timber.v("## SPACES: flat known parents of ${parent.name} are ${flattenSpaceParents[parent.roomId]}")
                             val flattenParentsIds = (flattenSpaceParents[parent.roomId] ?: emptyList()) + listOf(parent.roomId)
-//                            Timber.v("## SPACES: flatten known parents of children of ${parent.name} are ${flattenParentsIds}")
+
                             entry.value.forEach { child ->
                                 RoomSummaryEntity.where(realm, child.roomId).findFirst()?.let { childSum ->
+                                    childSum.directParentNames.add(parent.displayName())
 
-//                                    Timber.w("## SPACES: ${childSum.name} is ${childSum.roomId} fc: ${childSum.flattenParentIds}")
-//                                    var allParents = childSum.flattenParentIds ?: ""
-                                    if (childSum.flattenParentIds == null) childSum.flattenParentIds = ""
+                                    if (childSum.flattenParentIds == null) {
+                                        childSum.flattenParentIds = ""
+                                    }
                                     flattenParentsIds.forEach {
                                         if (childSum.flattenParentIds?.contains(it) != true) {
                                             childSum.flattenParentIds += "|$it"
                                         }
                                     }
-//                                    childSum.flattenParentIds = "$allParents|"
-
-//                                    Timber.v("## SPACES: flatten of ${childSum.name} is ${childSum.flattenParentIds}")
                                 }
                             }
                         }
@@ -438,38 +428,6 @@ internal class RoomSummaryUpdater @Inject constructor(
                         space.notificationCount = notificationCount
                     }
             // xxx invites??
-
-            // LEGACY GROUPS
-            // lets mark rooms that belongs to groups
-            val existingGroups = GroupSummaryEntity.where(realm).findAll()
-
-            // For rooms
-            realm.where(RoomSummaryEntity::class.java)
-                    .process(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.activeMemberships())
-                    .equalTo(RoomSummaryEntityFields.IS_DIRECT, false)
-                    .findAll().forEach { room ->
-                        val belongsTo = existingGroups.filter { it.roomIds.contains(room.roomId) }
-                        room.groupIds = if (belongsTo.isEmpty()) {
-                            null
-                        } else {
-                            "|${belongsTo.joinToString("|")}|"
-                        }
-                    }
-
-            // For DMS
-            realm.where(RoomSummaryEntity::class.java)
-                    .process(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.activeMemberships())
-                    .equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
-                    .findAll().forEach { room ->
-                        val belongsTo = existingGroups.filter {
-                            it.userIds.intersect(room.otherMemberIds).isNotEmpty()
-                        }
-                        room.groupIds = if (belongsTo.isEmpty()) {
-                            null
-                        } else {
-                            "|${belongsTo.joinToString("|")}|"
-                        }
-                    }
         }.also {
             Timber.v("## SPACES: Finish checking room hierarchy in $it ms")
         }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/DefaultSyncService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/DefaultSyncService.kt
index 37869b88f9d2688a0a53aba849998bf60aad173d..691dd7b20d31add110d5f2a251108ee7e44ab989 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/DefaultSyncService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/DefaultSyncService.kt
@@ -16,8 +16,6 @@
 
 package org.matrix.android.sdk.internal.session.sync
 
-import androidx.lifecycle.LiveData
-import org.matrix.android.sdk.api.session.sync.SyncRequestState
 import org.matrix.android.sdk.api.session.sync.SyncService
 import org.matrix.android.sdk.internal.di.SessionId
 import org.matrix.android.sdk.internal.di.WorkManagerProvider
@@ -75,9 +73,7 @@ internal class DefaultSyncService @Inject constructor(
 
     override fun getSyncState() = getSyncThread().currentState()
 
-    override fun getSyncRequestStateLive(): LiveData<SyncRequestState> {
-        return syncRequestStateTracker.syncRequestState
-    }
+    override fun getSyncRequestStateFlow() = syncRequestStateTracker.syncRequestState
 
     override fun hasAlreadySynced(): Boolean {
         return syncTokenStore.getLastToken() != null
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncRequestStateTracker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncRequestStateTracker.kt
index bcc5fcf9abe0d059e76f993e7a24352301ba3e30..03ce8cb3f2dc4054b3dc84c8cd2a7d45e2df831f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncRequestStateTracker.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncRequestStateTracker.kt
@@ -16,23 +16,26 @@
 
 package org.matrix.android.sdk.internal.session.sync
 
-import androidx.lifecycle.MutableLiveData
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.launch
 import org.matrix.android.sdk.api.session.sync.InitialSyncStep
 import org.matrix.android.sdk.api.session.sync.SyncRequestState
 import org.matrix.android.sdk.internal.session.SessionScope
 import javax.inject.Inject
 
 @SessionScope
-internal class SyncRequestStateTracker @Inject constructor() :
-        ProgressReporter {
+internal class SyncRequestStateTracker @Inject constructor(
+        private val coroutineScope: CoroutineScope
+) : ProgressReporter {
 
-    val syncRequestState = MutableLiveData<SyncRequestState>()
+    val syncRequestState = MutableSharedFlow<SyncRequestState>()
 
     private var rootTask: TaskInfo? = null
 
     // Only to be used for incremental sync
     fun setSyncRequestState(newSyncRequestState: SyncRequestState.IncrementalSyncRequestState) {
-        syncRequestState.postValue(newSyncRequestState)
+        emitSyncState(newSyncRequestState)
     }
 
     /**
@@ -42,7 +45,9 @@ internal class SyncRequestStateTracker @Inject constructor() :
             initialSyncStep: InitialSyncStep,
             totalProgress: Int
     ) {
-        endAll()
+        if (rootTask != null) {
+            endAll()
+        }
         rootTask = TaskInfo(initialSyncStep, totalProgress, null, 1F)
         reportProgress(0F)
     }
@@ -71,7 +76,7 @@ internal class SyncRequestStateTracker @Inject constructor() :
                 // Update the progress of the leaf and all its parents
                 leaf.setProgress(progress)
                 // Then update the live data using leaf wording and root progress
-                syncRequestState.postValue(SyncRequestState.InitialSyncProgressing(leaf.initialSyncStep, root.currentProgress.toInt()))
+                emitSyncState(SyncRequestState.InitialSyncProgressing(leaf.initialSyncStep, root.currentProgress.toInt()))
             }
         }
     }
@@ -86,13 +91,19 @@ internal class SyncRequestStateTracker @Inject constructor() :
                 // And close it
                 endedTask.parent.child = null
             } else {
-                syncRequestState.postValue(SyncRequestState.Idle)
+                emitSyncState(SyncRequestState.Idle)
             }
         }
     }
 
     fun endAll() {
         rootTask = null
-        syncRequestState.postValue(SyncRequestState.Idle)
+        emitSyncState(SyncRequestState.Idle)
+    }
+
+    private fun emitSyncState(state: SyncRequestState) {
+        coroutineScope.launch {
+            syncRequestState.emit(state)
+        }
     }
 }
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 9e5302222ab3571e9c7964ce58752538874eb223..392c73bd835aadf4b316bcba5ab76950a758cf7c 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
@@ -16,47 +16,36 @@
 
 package org.matrix.android.sdk.internal.session.sync
 
-import androidx.work.ExistingPeriodicWorkPolicy
 import com.zhuinden.monarchy.Monarchy
 import org.matrix.android.sdk.api.session.pushrules.PushRuleService
 import org.matrix.android.sdk.api.session.pushrules.RuleScope
 import org.matrix.android.sdk.api.session.sync.InitialSyncStep
-import org.matrix.android.sdk.api.session.sync.model.GroupsSyncResponse
 import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse
 import org.matrix.android.sdk.api.session.sync.model.SyncResponse
 import org.matrix.android.sdk.internal.SessionManager
 import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
 import org.matrix.android.sdk.internal.di.SessionDatabase
 import org.matrix.android.sdk.internal.di.SessionId
-import org.matrix.android.sdk.internal.di.WorkManagerProvider
 import org.matrix.android.sdk.internal.session.SessionListeners
 import org.matrix.android.sdk.internal.session.dispatchTo
-import org.matrix.android.sdk.internal.session.group.GetGroupDataWorker
 import org.matrix.android.sdk.internal.session.pushrules.ProcessEventForPushTask
 import org.matrix.android.sdk.internal.session.sync.handler.CryptoSyncHandler
-import org.matrix.android.sdk.internal.session.sync.handler.GroupSyncHandler
 import org.matrix.android.sdk.internal.session.sync.handler.PresenceSyncHandler
 import org.matrix.android.sdk.internal.session.sync.handler.SyncResponsePostTreatmentAggregatorHandler
 import org.matrix.android.sdk.internal.session.sync.handler.UserAccountDataSyncHandler
 import org.matrix.android.sdk.internal.session.sync.handler.room.RoomSyncHandler
 import org.matrix.android.sdk.internal.util.awaitTransaction
-import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
 import timber.log.Timber
-import java.util.concurrent.TimeUnit
 import javax.inject.Inject
 import kotlin.system.measureTimeMillis
 
-private const val GET_GROUP_DATA_WORKER = "GET_GROUP_DATA_WORKER"
-
 internal class SyncResponseHandler @Inject constructor(
         @SessionDatabase private val monarchy: Monarchy,
         @SessionId private val sessionId: String,
         private val sessionManager: SessionManager,
         private val sessionListeners: SessionListeners,
-        private val workManagerProvider: WorkManagerProvider,
         private val roomSyncHandler: RoomSyncHandler,
         private val userAccountDataSyncHandler: UserAccountDataSyncHandler,
-        private val groupSyncHandler: GroupSyncHandler,
         private val cryptoSyncHandler: CryptoSyncHandler,
         private val aggregatorHandler: SyncResponsePostTreatmentAggregatorHandler,
         private val cryptoService: DefaultCryptoService,
@@ -109,7 +98,7 @@ internal class SyncResponseHandler @Inject constructor(
             // IMPORTANT nothing should be suspend here as we are accessing the realm instance (thread local)
             measureTimeMillis {
                 Timber.v("Handle rooms")
-                reportSubtask(reporter, InitialSyncStep.ImportingAccountRoom, 1, 0.7f) {
+                reportSubtask(reporter, InitialSyncStep.ImportingAccountRoom, 1, 0.8f) {
                     if (syncResponse.rooms != null) {
                         roomSyncHandler.handle(realm, syncResponse.rooms, isInitialSync, aggregator, reporter)
                     }
@@ -118,17 +107,6 @@ internal class SyncResponseHandler @Inject constructor(
                 Timber.v("Finish handling rooms in $it ms")
             }
 
-            measureTimeMillis {
-                reportSubtask(reporter, InitialSyncStep.ImportingAccountGroups, 1, 0.1f) {
-                    Timber.v("Handle groups")
-                    if (syncResponse.groups != null) {
-                        groupSyncHandler.handle(realm, syncResponse.groups, reporter)
-                    }
-                }
-            }.also {
-                Timber.v("Finish handling groups in $it ms")
-            }
-
             measureTimeMillis {
                 reportSubtask(reporter, InitialSyncStep.ImportingAccountData, 1, 0.1f) {
                     Timber.v("Handle accountData")
@@ -155,9 +133,6 @@ internal class SyncResponseHandler @Inject constructor(
             userAccountDataSyncHandler.synchronizeWithServerIfNeeded(it.invite)
             dispatchInvitedRoom(it)
         }
-        syncResponse.groups?.let {
-            scheduleGroupDataFetchingIfNeeded(it)
-        }
 
         Timber.v("On sync completed")
         cryptoSyncHandler.onSyncCompleted(syncResponse)
@@ -177,31 +152,6 @@ internal class SyncResponseHandler @Inject constructor(
         }
     }
 
-    /**
-     * At the moment we don't get any group data through the sync, so we poll where every hour.
-     * You can also force to refetch group data using [Group] API.
-     */
-    private fun scheduleGroupDataFetchingIfNeeded(groupsSyncResponse: GroupsSyncResponse) {
-        val groupIds = ArrayList<String>()
-        groupIds.addAll(groupsSyncResponse.join.keys)
-        groupIds.addAll(groupsSyncResponse.invite.keys)
-        if (groupIds.isEmpty()) {
-            Timber.v("No new groups to fetch data for.")
-            return
-        }
-        Timber.v("There are ${groupIds.size} new groups to fetch data for.")
-        val getGroupDataWorkerParams = GetGroupDataWorker.Params(sessionId)
-        val workData = WorkerParamsFactory.toData(getGroupDataWorkerParams)
-
-        val getGroupWork = workManagerProvider.matrixPeriodicWorkRequestBuilder<GetGroupDataWorker>(1, TimeUnit.HOURS)
-                .setInputData(workData)
-                .setConstraints(WorkManagerProvider.workConstraints)
-                .build()
-
-        workManagerProvider.workManager
-                .enqueueUniquePeriodicWork(GET_GROUP_DATA_WORKER, ExistingPeriodicWorkPolicy.REPLACE, getGroupWork)
-    }
-
     private suspend fun checkPushRules(roomsSyncResponse: RoomsSyncResponse, isInitialSync: Boolean) {
         Timber.v("[PushRules] --> checkPushRules")
         if (isInitialSync) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/GroupSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/GroupSyncHandler.kt
deleted file mode 100644
index 1983d9f433576e1ff5bd124e33b2a0b54c0de460..0000000000000000000000000000000000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/GroupSyncHandler.kt
+++ /dev/null
@@ -1,104 +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.session.sync.handler
-
-import io.realm.Realm
-import org.matrix.android.sdk.api.session.room.model.Membership
-import org.matrix.android.sdk.api.session.sync.InitialSyncStep
-import org.matrix.android.sdk.api.session.sync.model.GroupsSyncResponse
-import org.matrix.android.sdk.api.session.sync.model.InvitedGroupSync
-import org.matrix.android.sdk.internal.database.model.GroupEntity
-import org.matrix.android.sdk.internal.database.model.GroupSummaryEntity
-import org.matrix.android.sdk.internal.database.query.getOrCreate
-import org.matrix.android.sdk.internal.database.query.where
-import org.matrix.android.sdk.internal.session.sync.ProgressReporter
-import org.matrix.android.sdk.internal.session.sync.mapWithProgress
-import javax.inject.Inject
-
-internal class GroupSyncHandler @Inject constructor() {
-
-    sealed class HandlingStrategy {
-        data class JOINED(val data: Map<String, Any>) : HandlingStrategy()
-        data class INVITED(val data: Map<String, InvitedGroupSync>) : HandlingStrategy()
-        data class LEFT(val data: Map<String, Any>) : HandlingStrategy()
-    }
-
-    fun handle(
-            realm: Realm,
-            roomsSyncResponse: GroupsSyncResponse,
-            reporter: ProgressReporter? = null
-    ) {
-        handleGroupSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), reporter)
-        handleGroupSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), reporter)
-        handleGroupSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), reporter)
-    }
-
-    // PRIVATE METHODS *****************************************************************************
-
-    private fun handleGroupSync(realm: Realm, handlingStrategy: HandlingStrategy, reporter: ProgressReporter?) {
-        val groups = when (handlingStrategy) {
-            is HandlingStrategy.JOINED ->
-                handlingStrategy.data.mapWithProgress(reporter, InitialSyncStep.ImportingAccountGroups, 0.6f) {
-                    handleJoinedGroup(realm, it.key)
-                }
-
-            is HandlingStrategy.INVITED ->
-                handlingStrategy.data.mapWithProgress(reporter, InitialSyncStep.ImportingAccountGroups, 0.3f) {
-                    handleInvitedGroup(realm, it.key)
-                }
-
-            is HandlingStrategy.LEFT ->
-                handlingStrategy.data.mapWithProgress(reporter, InitialSyncStep.ImportingAccountGroups, 0.1f) {
-                    handleLeftGroup(realm, it.key)
-                }
-        }
-        realm.insertOrUpdate(groups)
-    }
-
-    private fun handleJoinedGroup(
-            realm: Realm,
-            groupId: String
-    ): GroupEntity {
-        val groupEntity = GroupEntity.where(realm, groupId).findFirst() ?: GroupEntity(groupId)
-        val groupSummaryEntity = GroupSummaryEntity.getOrCreate(realm, groupId)
-        groupEntity.membership = Membership.JOIN
-        groupSummaryEntity.membership = Membership.JOIN
-        return groupEntity
-    }
-
-    private fun handleInvitedGroup(
-            realm: Realm,
-            groupId: String
-    ): GroupEntity {
-        val groupEntity = GroupEntity.where(realm, groupId).findFirst() ?: GroupEntity(groupId)
-        val groupSummaryEntity = GroupSummaryEntity.getOrCreate(realm, groupId)
-        groupEntity.membership = Membership.INVITE
-        groupSummaryEntity.membership = Membership.INVITE
-        return groupEntity
-    }
-
-    private fun handleLeftGroup(
-            realm: Realm,
-            groupId: String
-    ): GroupEntity {
-        val groupEntity = GroupEntity.where(realm, groupId).findFirst() ?: GroupEntity(groupId)
-        val groupSummaryEntity = GroupSummaryEntity.getOrCreate(realm, groupId)
-        groupEntity.membership = Membership.LEAVE
-        groupSummaryEntity.membership = Membership.LEAVE
-        return groupEntity
-    }
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/database/MatrixRealmMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/database/MatrixRealmMigration.kt
new file mode 100644
index 0000000000000000000000000000000000000000..4dff466de222f671d6ba6ce49e22050b058a17d8
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/database/MatrixRealmMigration.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.util.database
+
+import io.realm.DynamicRealm
+import io.realm.RealmMigration
+import timber.log.Timber
+import kotlin.system.measureTimeMillis
+
+internal abstract class MatrixRealmMigration(
+        private val dbName: String,
+        val schemaVersion: Long,
+) : RealmMigration {
+    final override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
+        Timber.d("Migrating Realm $dbName from $oldVersion to $newVersion")
+        val duration = measureTimeMillis {
+            doMigrate(realm, oldVersion)
+        }
+        Timber.d("Migrating Realm $dbName from $oldVersion to $newVersion took $duration ms.")
+    }
+
+    abstract fun doMigrate(realm: DynamicRealm, oldVersion: Long)
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/database/RealmMigrator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/database/RealmMigrator.kt
index 8da1bed97e0c5385e9aae4133fb278c16b078816..1992d3b918c1b5917e9be0d663257ef95ed98e55 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/database/RealmMigrator.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/database/RealmMigrator.kt
@@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.util.database
 import io.realm.DynamicRealm
 import io.realm.RealmObjectSchema
 import timber.log.Timber
+import kotlin.system.measureTimeMillis
 
 internal abstract class RealmMigrator(
         private val realm: DynamicRealm,
@@ -26,7 +27,10 @@ internal abstract class RealmMigrator(
 ) {
     fun perform() {
         Timber.d("Migrate ${realm.configuration.realmFileName} to $targetSchemaVersion")
-        doMigrate(realm)
+        val duration = measureTimeMillis {
+            doMigrate(realm)
+        }
+        Timber.d("Migrate ${realm.configuration.realmFileName} to $targetSchemaVersion took $duration ms.")
     }
 
     abstract fun doMigrate(realm: DynamicRealm)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/fatal.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/fatal.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7ed807d7cc7ef5644b568c9dae6e1b22a355a9a1
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/fatal.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.util
+
+import org.matrix.android.sdk.BuildConfig
+import timber.log.Timber
+
+/**
+ * Throws in debug, only log in production.
+ * As this method does not always throw, next statement should be a return.
+ */
+internal fun fatalError(message: String) {
+    if (BuildConfig.DEBUG) {
+        error(message)
+    } else {
+        Timber.e(message)
+    }
+}
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 52146ef484b81746b89c66be779d260bbd1e1ef7..83f9532870217c7d0a98703c4d321fd0b9168911 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
@@ -25,7 +25,6 @@ import org.matrix.android.sdk.internal.SessionManager
 import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker
 import org.matrix.android.sdk.internal.di.MatrixScope
 import org.matrix.android.sdk.internal.session.content.UploadContentWorker
-import org.matrix.android.sdk.internal.session.group.GetGroupDataWorker
 import org.matrix.android.sdk.internal.session.pushers.AddPusherWorker
 import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.DeactivateLiveLocationShareWorker
 import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker
@@ -53,8 +52,6 @@ internal class MatrixWorkerFactory @Inject constructor(private val sessionManage
                 CheckFactoryWorker(appContext, workerParameters, true)
             AddPusherWorker::class.java.name ->
                 AddPusherWorker(appContext, workerParameters, sessionManager)
-            GetGroupDataWorker::class.java.name ->
-                GetGroupDataWorker(appContext, workerParameters, sessionManager)
             MultipleEventSendingDispatcherWorker::class.java.name ->
                 MultipleEventSendingDispatcherWorker(appContext, workerParameters, sessionManager)
             RedactEventWorker::class.java.name ->
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/MatrixPatternsTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/MatrixPatternsTest.kt
index 0d0450adc2b9aab323a55c97a3627d0ddb8fbd81..ec587e0536ecea8af9978995e81c8127c69f909d 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/MatrixPatternsTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/MatrixPatternsTest.kt
@@ -35,6 +35,23 @@ class MatrixPatternsTest {
             MatrixPatterns.isUserId(input) shouldBeEqualTo expected
         }
     }
+
+    @Test
+    fun `given matrix id cases, when extracting userName, then returns expected`() {
+        val cases = listOf(
+                MatrixIdCase("foobar", userName = null),
+                MatrixIdCase("@foobar", userName = null),
+                MatrixIdCase("foobar@matrix.org", userName = null),
+                MatrixIdCase("@foobar: matrix.org", userName = null),
+                MatrixIdCase("foobar:matrix.org", userName = null),
+                MatrixIdCase("@foobar:matrix.org", userName = "foobar"),
+        )
+
+        cases.forEach { (input, expected) ->
+            MatrixPatterns.extractUserNameFromId(input) shouldBeEqualTo expected
+        }
+    }
 }
 
 private data class UserIdCase(val input: String, val isUserId: Boolean)
+private data class MatrixIdCase(val input: String, val userName: String?)
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo005Test.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo005Test.kt
new file mode 100644
index 0000000000000000000000000000000000000000..95b226411b28983a45cc75c86f91de3306158f18
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/auth/db/migration/MigrateAuthTo005Test.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.auth.db.migration
+
+import org.junit.Test
+import org.matrix.android.sdk.test.fakes.internal.auth.db.migration.Fake005MigrationRealm
+
+class MigrateAuthTo005Test {
+
+    private val fakeRealm = Fake005MigrationRealm()
+    private val migrator = MigrateAuthTo005(fakeRealm.instance)
+
+    @Test
+    fun `when doMigrate, then LoginType field added`() {
+        migrator.doMigrate(fakeRealm.instance)
+
+        fakeRealm.verifyLoginTypeAdded()
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/auth/login/LoginTypeTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/auth/login/LoginTypeTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..495302acb25c4318e8f65b1d5991c24b3a94d923
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/auth/login/LoginTypeTest.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.auth.login
+
+import org.amshove.kluent.shouldBeEqualTo
+import org.amshove.kluent.shouldNotBeEqualTo
+import org.junit.Test
+import org.matrix.android.sdk.api.auth.LoginType
+
+class LoginTypeTest {
+
+    @Test
+    fun `when getting type fromName, then map correctly`() {
+        LoginType.fromName(LoginType.PASSWORD.name) shouldBeEqualTo LoginType.PASSWORD
+        LoginType.fromName(LoginType.SSO.name) shouldBeEqualTo LoginType.SSO
+        LoginType.fromName(LoginType.UNSUPPORTED.name) shouldBeEqualTo LoginType.UNSUPPORTED
+        LoginType.fromName(LoginType.CUSTOM.name) shouldBeEqualTo LoginType.CUSTOM
+        LoginType.fromName(LoginType.DIRECT.name) shouldBeEqualTo LoginType.DIRECT
+        LoginType.fromName(LoginType.UNKNOWN.name) shouldBeEqualTo LoginType.UNKNOWN
+    }
+
+    @Test // The failure of this test means that an existing type has not been correctly added to fromValue
+    fun `given non-unknown type name, when getting type fromName, then type is not UNKNOWN`() {
+        val types = LoginType.values()
+
+        types.forEach { type ->
+            if (type != LoginType.UNKNOWN) {
+                LoginType.fromName(type.name) shouldNotBeEqualTo LoginType.UNKNOWN
+            }
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt
index 933087af2b6027fb264ed584973dd882095fc85a..25d441ef5c2e901ed128dad0afdca3de97666843 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.aggregation.livelocation
 
 import androidx.work.ExistingWorkPolicy
 import org.amshove.kluent.shouldBeEqualTo
+import org.amshove.kluent.shouldContain
 import org.junit.Test
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.UnsignedData
@@ -35,6 +36,7 @@ import org.matrix.android.sdk.test.fakes.FakeWorkManagerProvider
 import org.matrix.android.sdk.test.fakes.givenEqualTo
 import org.matrix.android.sdk.test.fakes.givenFindAll
 import org.matrix.android.sdk.test.fakes.givenFindFirst
+import org.matrix.android.sdk.test.fakes.givenLessThan
 import org.matrix.android.sdk.test.fakes.givenNotEqualTo
 
 private const val A_SESSION_ID = "session_id"
@@ -182,6 +184,7 @@ internal class LiveLocationAggregationProcessorTest {
         aggregatedEntity.roomId shouldBeEqualTo A_ROOM_ID
         aggregatedEntity.userId shouldBeEqualTo A_SENDER_ID
         aggregatedEntity.isActive shouldBeEqualTo true
+        aggregatedEntity.startOfLiveTimestampMillis shouldBeEqualTo A_TIMESTAMP
         aggregatedEntity.endOfLiveTimestampMillis shouldBeEqualTo A_TIMESTAMP + A_TIMEOUT_MILLIS
         aggregatedEntity.lastLocationContent shouldBeEqualTo null
         previousEntities.forEach { entity ->
@@ -199,9 +202,10 @@ internal class LiveLocationAggregationProcessorTest {
                 age = 123,
                 replacesState = AN_EVENT_ID
         )
+        val stateEventId = "state-event-id"
         val event = Event(
                 senderId = A_SENDER_ID,
-                eventId = "",
+                eventId = stateEventId,
                 unsignedData = unsignedData
         )
         val beaconInfo = MessageBeaconInfoContent(
@@ -237,6 +241,7 @@ internal class LiveLocationAggregationProcessorTest {
         aggregatedEntity.roomId shouldBeEqualTo A_ROOM_ID
         aggregatedEntity.userId shouldBeEqualTo A_SENDER_ID
         aggregatedEntity.isActive shouldBeEqualTo false
+        aggregatedEntity.relatedEventIds shouldContain stateEventId
         aggregatedEntity.endOfLiveTimestampMillis shouldBeEqualTo A_TIMESTAMP + A_TIMEOUT_MILLIS
         aggregatedEntity.lastLocationContent shouldBeEqualTo null
         previousEntities.forEach { entity ->
@@ -324,7 +329,7 @@ internal class LiveLocationAggregationProcessorTest {
         val lastBeaconLocationContent = MessageBeaconLocationDataContent(
                 unstableTimestampMillis = A_TIMESTAMP
         )
-        givenLastSummaryQueryReturns(
+        val aggregatedEntity = givenLastSummaryQueryReturns(
                 eventId = AN_EVENT_ID,
                 roomId = A_ROOM_ID,
                 beaconLocationContent = lastBeaconLocationContent
@@ -340,6 +345,7 @@ internal class LiveLocationAggregationProcessorTest {
         )
 
         result shouldBeEqualTo false
+        aggregatedEntity.relatedEventIds shouldContain AN_EVENT_ID
     }
 
     @Test
@@ -353,7 +359,7 @@ internal class LiveLocationAggregationProcessorTest {
         val lastBeaconLocationContent = MessageBeaconLocationDataContent(
                 unstableTimestampMillis = A_TIMESTAMP - 60_000
         )
-        val entity = givenLastSummaryQueryReturns(
+        val aggregatedEntity = givenLastSummaryQueryReturns(
                 eventId = AN_EVENT_ID,
                 roomId = A_ROOM_ID,
                 beaconLocationContent = lastBeaconLocationContent
@@ -369,7 +375,8 @@ internal class LiveLocationAggregationProcessorTest {
         )
 
         result shouldBeEqualTo true
-        val savedLocationData = ContentMapper.map(entity.lastLocationContent).toModel<MessageBeaconLocationDataContent>()
+        aggregatedEntity.relatedEventIds shouldContain AN_EVENT_ID
+        val savedLocationData = ContentMapper.map(aggregatedEntity.lastLocationContent).toModel<MessageBeaconLocationDataContent>()
         savedLocationData?.getBestTimestampMillis() shouldBeEqualTo A_TIMESTAMP
         savedLocationData?.getBestLocationInfo()?.geoUri shouldBeEqualTo A_GEO_URI
     }
@@ -399,6 +406,7 @@ internal class LiveLocationAggregationProcessorTest {
                 .givenNotEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, AN_EVENT_ID)
                 .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.USER_ID, A_SENDER_ID)
                 .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true)
+                .givenLessThan(LiveLocationShareAggregatedSummaryEntityFields.START_OF_LIVE_TIMESTAMP_MILLIS, A_TIMESTAMP)
                 .givenFindAll(summaryList)
         return summaryList
     }
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt
index de9120653160a866563253f94ae3d8e9794bea71..a01f51604ca6600a562a7135d187f40c9ef69b1b 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt
@@ -22,8 +22,10 @@ import androidx.lifecycle.Transformations
 import io.mockk.coEvery
 import io.mockk.coVerify
 import io.mockk.every
+import io.mockk.just
 import io.mockk.mockk
 import io.mockk.mockkStatic
+import io.mockk.runs
 import io.mockk.slot
 import io.mockk.unmockkAll
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -51,6 +53,8 @@ private const val A_LATITUDE = 1.4
 private const val A_LONGITUDE = 40.0
 private const val AN_UNCERTAINTY = 5.0
 private const val A_TIMEOUT = 15_000L
+private const val A_DESCRIPTION = "description"
+private const val A_REASON = "reason"
 
 @ExperimentalCoroutinesApi
 internal class DefaultLocationSharingServiceTest {
@@ -61,6 +65,7 @@ internal class DefaultLocationSharingServiceTest {
     private val startLiveLocationShareTask = mockk<StartLiveLocationShareTask>()
     private val stopLiveLocationShareTask = mockk<StopLiveLocationShareTask>()
     private val checkIfExistingActiveLiveTask = mockk<CheckIfExistingActiveLiveTask>()
+    private val redactLiveLocationShareTask = mockk<RedactLiveLocationShareTask>()
     private val fakeLiveLocationShareAggregatedSummaryMapper = mockk<LiveLocationShareAggregatedSummaryMapper>()
 
     private val defaultLocationSharingService = DefaultLocationSharingService(
@@ -71,6 +76,7 @@ internal class DefaultLocationSharingServiceTest {
             startLiveLocationShareTask = startLiveLocationShareTask,
             stopLiveLocationShareTask = stopLiveLocationShareTask,
             checkIfExistingActiveLiveTask = checkIfExistingActiveLiveTask,
+            redactLiveLocationShareTask = redactLiveLocationShareTask,
             liveLocationShareAggregatedSummaryMapper = fakeLiveLocationShareAggregatedSummaryMapper
     )
 
@@ -137,7 +143,7 @@ internal class DefaultLocationSharingServiceTest {
         coEvery { stopLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success("stopped-event-id")
         coEvery { startLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
 
-        val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT)
+        val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT, A_DESCRIPTION)
 
         result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
         val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params(
@@ -150,7 +156,8 @@ internal class DefaultLocationSharingServiceTest {
         coVerify { stopLiveLocationShareTask.execute(expectedStopParams) }
         val expectedStartParams = StartLiveLocationShareTask.Params(
                 roomId = A_ROOM_ID,
-                timeoutMillis = A_TIMEOUT
+                timeoutMillis = A_TIMEOUT,
+                description = A_DESCRIPTION
         )
         coVerify { startLiveLocationShareTask.execute(expectedStartParams) }
     }
@@ -161,7 +168,7 @@ internal class DefaultLocationSharingServiceTest {
         val error = Throwable()
         coEvery { stopLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Failure(error)
 
-        val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT)
+        val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT, A_DESCRIPTION)
 
         result shouldBeEqualTo UpdateLiveLocationShareResult.Failure(error)
         val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params(
@@ -179,7 +186,7 @@ internal class DefaultLocationSharingServiceTest {
         coEvery { checkIfExistingActiveLiveTask.execute(any()) } returns false
         coEvery { startLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
 
-        val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT)
+        val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT, A_DESCRIPTION)
 
         result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
         val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params(
@@ -188,7 +195,8 @@ internal class DefaultLocationSharingServiceTest {
         coVerify { checkIfExistingActiveLiveTask.execute(expectedCheckExistingParams) }
         val expectedStartParams = StartLiveLocationShareTask.Params(
                 roomId = A_ROOM_ID,
-                timeoutMillis = A_TIMEOUT
+                timeoutMillis = A_TIMEOUT,
+                description = A_DESCRIPTION
         )
         coVerify { startLiveLocationShareTask.execute(expectedStartParams) }
     }
@@ -206,6 +214,20 @@ internal class DefaultLocationSharingServiceTest {
         coVerify { stopLiveLocationShareTask.execute(expectedParams) }
     }
 
+    @Test
+    fun `live location share can be redacted`() = runTest {
+        coEvery { redactLiveLocationShareTask.execute(any()) } just runs
+
+        defaultLocationSharingService.redactLiveLocationShare(beaconInfoEventId = AN_EVENT_ID, reason = A_REASON)
+
+        val expectedParams = RedactLiveLocationShareTask.Params(
+                roomId = A_ROOM_ID,
+                beaconInfoEventId = AN_EVENT_ID,
+                reason = A_REASON
+        )
+        coVerify { redactLiveLocationShareTask.execute(expectedParams) }
+    }
+
     @Test
     fun `livedata of live summaries is correctly computed`() {
         val entity = LiveLocationShareAggregatedSummaryEntity()
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultRedactLiveLocationShareTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultRedactLiveLocationShareTaskTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b8618d1a791cb5340dde97e4d540c498415a9d45
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultRedactLiveLocationShareTaskTest.kt
@@ -0,0 +1,126 @@
+/*
+ * 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.location
+
+import io.mockk.unmockkAll
+import io.realm.RealmList
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Test
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
+import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields
+import org.matrix.android.sdk.test.fakes.FakeEventSenderProcessor
+import org.matrix.android.sdk.test.fakes.FakeLocalEchoEventFactory
+import org.matrix.android.sdk.test.fakes.FakeRealm
+import org.matrix.android.sdk.test.fakes.FakeRealmConfiguration
+import org.matrix.android.sdk.test.fakes.givenEqualTo
+import org.matrix.android.sdk.test.fakes.givenFindFirst
+
+private const val A_ROOM_ID = "room-id"
+private const val AN_EVENT_ID = "event-id"
+private const val AN_EVENT_ID_1 = "event-id-1"
+private const val AN_EVENT_ID_2 = "event-id-2"
+private const val AN_EVENT_ID_3 = "event-id-3"
+private const val A_REASON = "reason"
+
+@ExperimentalCoroutinesApi
+class DefaultRedactLiveLocationShareTaskTest {
+
+    private val fakeRealmConfiguration = FakeRealmConfiguration()
+    private val fakeLocalEchoEventFactory = FakeLocalEchoEventFactory()
+    private val fakeEventSenderProcessor = FakeEventSenderProcessor()
+    private val fakeRealm = FakeRealm()
+
+    private val defaultRedactLiveLocationShareTask = DefaultRedactLiveLocationShareTask(
+            realmConfiguration = fakeRealmConfiguration.instance,
+            localEchoEventFactory = fakeLocalEchoEventFactory.instance,
+            eventSenderProcessor = fakeEventSenderProcessor
+    )
+
+    @After
+    fun tearDown() {
+        unmockkAll()
+    }
+
+    @Test
+    fun `given parameters when redacting then post redact events and related and creates redact local echos`() = runTest {
+        val params = createParams()
+        val relatedEventIds = listOf(AN_EVENT_ID_1, AN_EVENT_ID_2, AN_EVENT_ID_3)
+        val aggregatedSummaryEntity = createSummary(relatedEventIds)
+        givenSummaryForId(AN_EVENT_ID, aggregatedSummaryEntity)
+        fakeRealmConfiguration.givenAwaitTransaction<List<String>>(fakeRealm.instance)
+        val redactEvents = givenCreateRedactEventWithLocalEcho(relatedEventIds + AN_EVENT_ID)
+        givenPostRedaction(redactEvents)
+
+        defaultRedactLiveLocationShareTask.execute(params)
+
+        verifyCreateRedactEventForEventIds(relatedEventIds + AN_EVENT_ID)
+        verifyCreateLocalEchoForEvents(redactEvents)
+    }
+
+    private fun createParams() = RedactLiveLocationShareTask.Params(
+            roomId = A_ROOM_ID,
+            beaconInfoEventId = AN_EVENT_ID,
+            reason = A_REASON
+    )
+
+    private fun createSummary(relatedEventIds: List<String>): LiveLocationShareAggregatedSummaryEntity {
+        return LiveLocationShareAggregatedSummaryEntity(
+                eventId = AN_EVENT_ID,
+                relatedEventIds = RealmList(*relatedEventIds.toTypedArray()),
+        )
+    }
+
+    private fun givenSummaryForId(eventId: String, aggregatedSummaryEntity: LiveLocationShareAggregatedSummaryEntity) {
+        fakeRealm.givenWhere<LiveLocationShareAggregatedSummaryEntity>()
+                .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, eventId)
+                .givenFindFirst(aggregatedSummaryEntity)
+    }
+
+    private fun givenCreateRedactEventWithLocalEcho(eventIds: List<String>): List<Event> {
+        return eventIds.map { eventId ->
+            fakeLocalEchoEventFactory.givenCreateRedactEvent(
+                    eventId = eventId,
+                    withLocalEcho = true
+            )
+        }
+    }
+
+    private fun givenPostRedaction(redactEvents: List<Event>) {
+        redactEvents.forEach {
+            fakeEventSenderProcessor.givenPostRedaction(event = it, reason = A_REASON)
+        }
+    }
+
+    private fun verifyCreateRedactEventForEventIds(eventIds: List<String>) {
+        eventIds.forEach { eventId ->
+            fakeLocalEchoEventFactory.verifyCreateRedactEvent(
+                    roomId = A_ROOM_ID,
+                    eventId = eventId,
+                    reason = A_REASON
+            )
+        }
+    }
+
+    private fun verifyCreateLocalEchoForEvents(events: List<Event>) {
+        events.forEach { redactionEvent ->
+            fakeLocalEchoEventFactory.verifyCreateLocalEcho(redactionEvent)
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt
index 909ba5d048eb93e39e130a9f59c33dd9fb5c2c01..aa8826243fcf7050fe934f4421f27cf6833bc4be 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt
@@ -34,6 +34,7 @@ import org.matrix.android.sdk.test.fakes.FakeSendStateTask
 private const val A_USER_ID = "user-id"
 private const val A_ROOM_ID = "room-id"
 private const val AN_EVENT_ID = "event-id"
+private const val A_DESCRIPTION = "description"
 private const val A_TIMEOUT = 15_000L
 private const val AN_EPOCH = 1655210176L
 
@@ -58,7 +59,8 @@ internal class DefaultStartLiveLocationShareTaskTest {
     fun `given parameters and no error when calling the task then result is success`() = runTest {
         val params = StartLiveLocationShareTask.Params(
                 roomId = A_ROOM_ID,
-                timeoutMillis = A_TIMEOUT
+                timeoutMillis = A_TIMEOUT,
+                description = A_DESCRIPTION
         )
         fakeClock.givenEpoch(AN_EPOCH)
         fakeSendStateTask.givenExecuteRetryReturns(AN_EVENT_ID)
@@ -67,6 +69,7 @@ internal class DefaultStartLiveLocationShareTaskTest {
 
         result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
         val expectedBeaconContent = MessageBeaconInfoContent(
+                body = A_DESCRIPTION,
                 timeout = params.timeoutMillis,
                 isLive = true,
                 unstableTimestampMillis = AN_EPOCH
@@ -87,7 +90,8 @@ internal class DefaultStartLiveLocationShareTaskTest {
     fun `given parameters and an empty returned event id when calling the task then result is failure`() = runTest {
         val params = StartLiveLocationShareTask.Params(
                 roomId = A_ROOM_ID,
-                timeoutMillis = A_TIMEOUT
+                timeoutMillis = A_TIMEOUT,
+                description = A_DESCRIPTION
         )
         fakeClock.givenEpoch(AN_EPOCH)
         fakeSendStateTask.givenExecuteRetryReturns("")
@@ -101,7 +105,8 @@ internal class DefaultStartLiveLocationShareTaskTest {
     fun `given parameters and error during event sending when calling the task then result is failure`() = runTest {
         val params = StartLiveLocationShareTask.Params(
                 roomId = A_ROOM_ID,
-                timeoutMillis = A_TIMEOUT
+                timeoutMillis = A_TIMEOUT,
+                description = A_DESCRIPTION
         )
         fakeClock.givenEpoch(AN_EPOCH)
         val error = Throwable()
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..24d9c30039f497188ae6bf583e62a9ae7d70d387
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.location
+
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.amshove.kluent.shouldBe
+import org.junit.Test
+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.internal.database.model.EventAnnotationsSummaryEntity
+import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntityFields
+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.livelocation.LiveLocationShareAggregatedSummaryEntity
+import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields
+import org.matrix.android.sdk.test.fakes.FakeRealm
+import org.matrix.android.sdk.test.fakes.givenDelete
+import org.matrix.android.sdk.test.fakes.givenEqualTo
+import org.matrix.android.sdk.test.fakes.givenFindFirst
+
+private const val AN_EVENT_ID = "event-id"
+private const val A_REDACTED_EVENT_ID = "redacted-event-id"
+
+@ExperimentalCoroutinesApi
+class LiveLocationShareRedactionEventProcessorTest {
+
+    private val liveLocationShareRedactionEventProcessor = LiveLocationShareRedactionEventProcessor()
+    private val fakeRealm = FakeRealm()
+
+    @Test
+    fun `given an event when checking if it should be processed then only event of type REDACTED is processed`() {
+        val eventId = AN_EVENT_ID
+        val eventType = EventType.REDACTION
+        val insertType = EventInsertType.INCREMENTAL_SYNC
+
+        val result = liveLocationShareRedactionEventProcessor.shouldProcess(
+                eventId = eventId,
+                eventType = eventType,
+                insertType = insertType
+        )
+
+        result shouldBe true
+    }
+
+    @Test
+    fun `given an event when checking if it should be processed then local echo is not processed`() {
+        val eventId = AN_EVENT_ID
+        val eventType = EventType.REDACTION
+        val insertType = EventInsertType.LOCAL_ECHO
+
+        val result = liveLocationShareRedactionEventProcessor.shouldProcess(
+                eventId = eventId,
+                eventType = eventType,
+                insertType = insertType
+        )
+
+        result shouldBe false
+    }
+
+    @Test
+    fun `given a redacted live location share event when processing it then related summaries are deleted from database`() = runTest {
+        val event = Event(eventId = AN_EVENT_ID, redacts = A_REDACTED_EVENT_ID)
+        val redactedEventEntity = EventEntity(eventId = A_REDACTED_EVENT_ID, type = EventType.STATE_ROOM_BEACON_INFO.first())
+        fakeRealm.givenWhere<EventEntity>()
+                .givenEqualTo(EventEntityFields.EVENT_ID, A_REDACTED_EVENT_ID)
+                .givenFindFirst(redactedEventEntity)
+        val liveSummary = mockk<LiveLocationShareAggregatedSummaryEntity>()
+        every { liveSummary.eventId } returns A_REDACTED_EVENT_ID
+        liveSummary.givenDelete()
+        fakeRealm.givenWhere<LiveLocationShareAggregatedSummaryEntity>()
+                .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, A_REDACTED_EVENT_ID)
+                .givenFindFirst(liveSummary)
+        val annotationsSummary = mockk<EventAnnotationsSummaryEntity>()
+        every { annotationsSummary.eventId } returns A_REDACTED_EVENT_ID
+        annotationsSummary.givenDelete()
+        fakeRealm.givenWhere<EventAnnotationsSummaryEntity>()
+                .givenEqualTo(EventAnnotationsSummaryEntityFields.EVENT_ID, A_REDACTED_EVENT_ID)
+                .givenFindFirst(annotationsSummary)
+
+        liveLocationShareRedactionEventProcessor.process(fakeRealm.instance, event = event)
+
+        verify {
+            liveSummary.deleteFromRealm()
+            annotationsSummary.deleteFromRealm()
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventSenderProcessor.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventSenderProcessor.kt
index fbdcf5bfd7cf6f0bf6ba97d0e726afb35c0af6b1..db04b8b8cba730d77d782e142a0d23d845234216 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventSenderProcessor.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventSenderProcessor.kt
@@ -27,4 +27,8 @@ internal class FakeEventSenderProcessor : EventSenderProcessor by mockk() {
     fun givenPostEventReturns(event: Event, cancelable: Cancelable) {
         every { postEvent(event) } returns cancelable
     }
+
+    fun givenPostRedaction(event: Event, reason: String?) {
+        every { postRedaction(event, reason) } returns mockk()
+    }
 }
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeLocalEchoEventFactory.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeLocalEchoEventFactory.kt
index 50ec85f14a2e186c3033c3f3f508111157ef5c60..f484e32149d3e45bdd851b371854c8ffbce1a5a9 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeLocalEchoEventFactory.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeLocalEchoEventFactory.kt
@@ -46,6 +46,24 @@ internal class FakeLocalEchoEventFactory {
         return event
     }
 
+    fun verifyCreateStaticLocationEvent(
+            roomId: String,
+            latitude: Double,
+            longitude: Double,
+            uncertainty: Double?,
+            isUserLocation: Boolean
+    ) {
+        verify {
+            instance.createStaticLocationEvent(
+                    roomId = roomId,
+                    latitude = latitude,
+                    longitude = longitude,
+                    uncertainty = uncertainty,
+                    isUserLocation = isUserLocation
+            )
+        }
+    }
+
     fun givenCreateLiveLocationEvent(withLocalEcho: Boolean): Event {
         val event = Event()
         every {
@@ -64,38 +82,50 @@ internal class FakeLocalEchoEventFactory {
         return event
     }
 
-    fun verifyCreateStaticLocationEvent(
+    fun verifyCreateLiveLocationEvent(
             roomId: String,
+            beaconInfoEventId: String,
             latitude: Double,
             longitude: Double,
-            uncertainty: Double?,
-            isUserLocation: Boolean
+            uncertainty: Double?
     ) {
         verify {
-            instance.createStaticLocationEvent(
+            instance.createLiveLocationEvent(
                     roomId = roomId,
+                    beaconInfoEventId = beaconInfoEventId,
                     latitude = latitude,
                     longitude = longitude,
-                    uncertainty = uncertainty,
-                    isUserLocation = isUserLocation
+                    uncertainty = uncertainty
             )
         }
     }
 
-    fun verifyCreateLiveLocationEvent(
+    fun givenCreateRedactEvent(eventId: String, withLocalEcho: Boolean): Event {
+        val event = Event()
+        every {
+            instance.createRedactEvent(
+                    roomId = any(),
+                    eventId = eventId,
+                    reason = any()
+            )
+        } returns event
+
+        if (withLocalEcho) {
+            every { instance.createLocalEcho(event) } just runs
+        }
+        return event
+    }
+
+    fun verifyCreateRedactEvent(
             roomId: String,
-            beaconInfoEventId: String,
-            latitude: Double,
-            longitude: Double,
-            uncertainty: Double?
+            eventId: String,
+            reason: String?
     ) {
         verify {
-            instance.createLiveLocationEvent(
+            instance.createRedactEvent(
                     roomId = roomId,
-                    beaconInfoEventId = beaconInfoEventId,
-                    latitude = latitude,
-                    longitude = longitude,
-                    uncertainty = uncertainty
+                    eventId = eventId,
+                    reason = reason
             )
         }
     }
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt
index 0ebff8727889dd42832674f3e5ab2bbacfa39843..afdcf111f897c13f52ce2fb000d3428830afb6bb 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt
@@ -18,10 +18,13 @@ package org.matrix.android.sdk.test.fakes
 
 import io.mockk.MockKVerificationScope
 import io.mockk.every
+import io.mockk.just
 import io.mockk.mockk
+import io.mockk.runs
 import io.mockk.verify
 import io.realm.Realm
 import io.realm.RealmModel
+import io.realm.RealmObject
 import io.realm.RealmQuery
 import io.realm.RealmResults
 import io.realm.kotlin.where
@@ -97,3 +100,18 @@ inline fun <reified T : RealmModel> RealmQuery<T>.givenIsNotNull(
     every { isNotNull(fieldName) } returns this
     return this
 }
+
+inline fun <reified T : RealmModel> RealmQuery<T>.givenLessThan(
+        fieldName: String,
+        value: Long
+): RealmQuery<T> {
+    every { lessThan(fieldName, value) } returns this
+    return this
+}
+
+/**
+ * Should be called on a mocked RealmObject and not on a real RealmObject so that the underlying final method is mocked.
+ */
+fun RealmObject.givenDelete() {
+    every { deleteFromRealm() } just runs
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt
new file mode 100644
index 0000000000000000000000000000000000000000..15a9823c79daddcddd0a6c1b72cfc8201a95be2f
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.test.fakes
+
+import io.mockk.coEvery
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.slot
+import io.realm.Realm
+import io.realm.RealmConfiguration
+import org.matrix.android.sdk.internal.database.awaitTransaction
+
+internal class FakeRealmConfiguration {
+
+    init {
+        mockkStatic("org.matrix.android.sdk.internal.database.AsyncTransactionKt")
+    }
+
+    val instance = mockk<RealmConfiguration>()
+
+    fun <T> givenAwaitTransaction(realm: Realm) {
+        val transaction = slot<suspend (Realm) -> T>()
+        coEvery { awaitTransaction(instance, capture(transaction)) } coAnswers {
+            secondArg<suspend (Realm) -> T>().invoke(realm)
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/api/FakeSession.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/api/FakeSession.kt
new file mode 100644
index 0000000000000000000000000000000000000000..df22455fb1deff44dd5f6009cc1f5799f731a123
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/api/FakeSession.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.test.fakes.api
+
+import io.mockk.mockk
+import org.matrix.android.sdk.api.session.Session
+
+class FakeSession {
+
+    val instance: Session = mockk()
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakeSessionManager.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakeSessionManager.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2232ad9b4f2ad172e28467bb76aad2b6d7aa9710
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakeSessionManager.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.test.fakes.internal
+
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import org.amshove.kluent.shouldBeEqualTo
+import org.matrix.android.sdk.api.auth.data.SessionParams
+import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.internal.SessionManager
+import org.matrix.android.sdk.test.fakes.api.FakeSession
+
+internal class FakeSessionManager {
+
+    val instance: SessionManager = mockk()
+
+    init {
+        every { instance.getOrCreateSession(any()) } returns fakeSession.instance
+    }
+
+    fun assertSessionCreatedWithParams(session: Session, sessionParams: SessionParams) {
+        verify { instance.getOrCreateSession(sessionParams) }
+
+        session shouldBeEqualTo fakeSession.instance
+    }
+
+    companion object {
+        private val fakeSession = FakeSession()
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/FakeIsValidClientServerApiTask.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/FakeIsValidClientServerApiTask.kt
new file mode 100644
index 0000000000000000000000000000000000000000..40681748c1e5fff9a109aec64de30fbff1a0affd
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/FakeIsValidClientServerApiTask.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.test.fakes.internal.auth
+
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.mockk
+import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
+import org.matrix.android.sdk.internal.auth.IsValidClientServerApiTask
+import org.matrix.android.sdk.internal.auth.IsValidClientServerApiTask.Params
+
+internal class FakeIsValidClientServerApiTask {
+
+    init {
+        coEvery { instance.execute(any()) } returns true
+    }
+
+    val instance: IsValidClientServerApiTask = mockk()
+
+    fun givenValidationFails() {
+        coEvery { instance.execute(any()) } returns false
+    }
+
+    fun verifyExecutionWithConfig(config: HomeServerConnectionConfig) {
+        coVerify { instance.execute(Params(config)) }
+    }
+
+    fun verifyNoExecution() {
+        coVerify(inverse = true) { instance.execute(any()) }
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/FakePendingSessionStore.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/FakePendingSessionStore.kt
new file mode 100644
index 0000000000000000000000000000000000000000..8a18b75ca2914f2ce82fda47f94a38f0d0bdd1e2
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/FakePendingSessionStore.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.test.fakes.internal.auth
+
+import io.mockk.coJustRun
+import io.mockk.coVerify
+import io.mockk.mockk
+import org.matrix.android.sdk.internal.auth.PendingSessionStore
+
+internal class FakePendingSessionStore {
+
+    val instance: PendingSessionStore = mockk()
+
+    init {
+        coJustRun { instance.delete() }
+    }
+
+    fun verifyPendingSessionDataCleared() {
+        coVerify { instance.delete() }
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/FakeSessionParamsCreator.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/FakeSessionParamsCreator.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f64e5a451d2ed4121464202a78602755b985b56b
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/FakeSessionParamsCreator.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.matrix.android.sdk.test.fakes.internal.auth
+
+import android.net.Uri
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import org.matrix.android.sdk.api.auth.LoginType
+import org.matrix.android.sdk.api.auth.data.Credentials
+import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
+import org.matrix.android.sdk.internal.auth.SessionParamsCreator
+import org.matrix.android.sdk.test.fixtures.SessionParamsFixture.aSessionParams
+
+internal class FakeSessionParamsCreator {
+
+    val instance: SessionParamsCreator = mockk()
+
+    init {
+        mockkStatic(Uri::class)
+        every { Uri.parse(any()) } returns mockk()
+        coEvery { instance.create(any(), any(), any()) } returns sessionParams
+    }
+
+    fun verifyCreatedWithParameters(
+            credentials: Credentials,
+            homeServerConnectionConfig: HomeServerConnectionConfig,
+            loginType: LoginType,
+    ) {
+        coVerify { instance.create(credentials, homeServerConnectionConfig, loginType) }
+    }
+
+    companion object {
+        val sessionParams = aSessionParams()
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/FakeSessionParamsStore.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/FakeSessionParamsStore.kt
new file mode 100644
index 0000000000000000000000000000000000000000..22e8a32a326565cfda94347066312455e5b4c56e
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/FakeSessionParamsStore.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.test.fakes.internal.auth
+
+import io.mockk.coJustRun
+import io.mockk.coVerify
+import io.mockk.mockk
+import org.matrix.android.sdk.api.auth.data.SessionParams
+import org.matrix.android.sdk.internal.auth.SessionParamsStore
+
+internal class FakeSessionParamsStore {
+
+    val instance: SessionParamsStore = mockk()
+
+    init {
+        coJustRun { instance.save(any()) }
+    }
+
+    fun verifyParamsSaved(sessionParams: SessionParams) {
+        coVerify { instance.save(sessionParams) }
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/db/migration/Fake005MigrationRealm.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/db/migration/Fake005MigrationRealm.kt
new file mode 100644
index 0000000000000000000000000000000000000000..13fd4a972c8e7ff069add95ac02d6f793dc9e90c
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/db/migration/Fake005MigrationRealm.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.test.fakes.internal.auth.db.migration
+
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verifyOrder
+import io.realm.DynamicRealm
+import io.realm.RealmObjectSchema
+import io.realm.RealmSchema
+import org.matrix.android.sdk.internal.auth.db.SessionParamsEntityFields
+
+class Fake005MigrationRealm {
+
+    val instance: DynamicRealm = mockk()
+
+    private val schema: RealmSchema = mockk()
+    private val objectSchema: RealmObjectSchema = mockk()
+
+    init {
+        every { instance.schema } returns schema
+        every { schema.get("SessionParamsEntity") } returns objectSchema
+        every { objectSchema.addField(any(), any()) } returns objectSchema
+        every { objectSchema.setRequired(any(), any()) } returns objectSchema
+        every { objectSchema.transform(any()) } returns objectSchema
+    }
+
+    fun verifyLoginTypeAdded() {
+        verifyLoginTypeFieldAddedAndTransformed()
+    }
+
+    private fun verifyLoginTypeFieldAddedAndTransformed() {
+        verifyOrder {
+            objectSchema["SessionParamsEntity"]
+            objectSchema.addField(SessionParamsEntityFields.LOGIN_TYPE, String::class.java)
+            objectSchema.setRequired(SessionParamsEntityFields.LOGIN_TYPE, true)
+            objectSchema.transform(any())
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/db/sessionparams/FakeCredentialsJsonAdapter.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/db/sessionparams/FakeCredentialsJsonAdapter.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f1cb4071fdb2db65159f5e84c1f231b5199db4dd
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/db/sessionparams/FakeCredentialsJsonAdapter.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.test.fakes.internal.auth.db.sessionparams
+
+import com.squareup.moshi.JsonAdapter
+import io.mockk.every
+import io.mockk.mockk
+import org.matrix.android.sdk.api.auth.data.Credentials
+import org.matrix.android.sdk.test.fakes.internal.auth.db.sessionparams.FakeSessionParamsMapperMoshi.Companion.sessionParams
+import org.matrix.android.sdk.test.fakes.internal.auth.db.sessionparams.FakeSessionParamsMapperMoshi.Companion.sessionParamsEntity
+import org.matrix.android.sdk.test.fixtures.CredentialsFixture.aCredentials
+
+internal class FakeCredentialsJsonAdapter {
+
+    val instance: JsonAdapter<Credentials> = mockk()
+
+    init {
+        every { instance.fromJson(sessionParamsEntity.credentialsJson) } returns credentials
+        every { instance.toJson(sessionParams.credentials) } returns CREDENTIALS_JSON
+    }
+
+    fun givenNullDeserialization() {
+        every { instance.fromJson(sessionParamsEntity.credentialsJson) } returns null
+    }
+
+    fun givenNullSerialization() {
+        every { instance.toJson(credentials) } returns null
+    }
+
+    companion object {
+        val credentials = aCredentials()
+        const val CREDENTIALS_JSON = "credentials_json"
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/db/sessionparams/FakeHomeServerConnectionConfigJsonAdapter.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/db/sessionparams/FakeHomeServerConnectionConfigJsonAdapter.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f85d6e27781fc7872ee82bfcc6af03f8a8d7f027
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/db/sessionparams/FakeHomeServerConnectionConfigJsonAdapter.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.test.fakes.internal.auth.db.sessionparams
+
+import com.squareup.moshi.JsonAdapter
+import io.mockk.every
+import io.mockk.mockk
+import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
+import org.matrix.android.sdk.test.fakes.internal.auth.db.sessionparams.FakeSessionParamsMapperMoshi.Companion.sessionParams
+import org.matrix.android.sdk.test.fakes.internal.auth.db.sessionparams.FakeSessionParamsMapperMoshi.Companion.sessionParamsEntity
+
+internal class FakeHomeServerConnectionConfigJsonAdapter {
+
+    val instance: JsonAdapter<HomeServerConnectionConfig> = mockk()
+
+    init {
+        every { instance.fromJson(sessionParamsEntity.homeServerConnectionConfigJson) } returns homeServerConnectionConfig
+        every { instance.toJson(sessionParams.homeServerConnectionConfig) } returns HOME_SERVER_CONNECTION_CONFIG_JSON
+    }
+
+    fun givenNullDeserialization() {
+        every { instance.fromJson(sessionParamsEntity.credentialsJson) } returns null
+    }
+
+    fun givenNullSerialization() {
+        every { instance.toJson(homeServerConnectionConfig) } returns null
+    }
+
+    companion object {
+        val homeServerConnectionConfig = HomeServerConnectionConfig.Builder().withHomeServerUri("homeserver").build()
+        const val HOME_SERVER_CONNECTION_CONFIG_JSON = "home_server_connection_config_json"
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/db/sessionparams/FakeSessionParamsMapperMoshi.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/db/sessionparams/FakeSessionParamsMapperMoshi.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ed0ddb11793e37a58f187032027763ba40535fda
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/auth/db/sessionparams/FakeSessionParamsMapperMoshi.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.test.fakes.internal.auth.db.sessionparams
+
+import android.net.Uri
+import com.squareup.moshi.Moshi
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import org.amshove.kluent.shouldBeEqualTo
+import org.amshove.kluent.shouldBeNull
+import org.matrix.android.sdk.api.auth.LoginType
+import org.matrix.android.sdk.api.auth.data.Credentials
+import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
+import org.matrix.android.sdk.api.auth.data.SessionParams
+import org.matrix.android.sdk.api.auth.data.sessionId
+import org.matrix.android.sdk.internal.auth.db.SessionParamsEntity
+import org.matrix.android.sdk.test.fakes.internal.auth.db.sessionparams.FakeCredentialsJsonAdapter.Companion.CREDENTIALS_JSON
+import org.matrix.android.sdk.test.fakes.internal.auth.db.sessionparams.FakeCredentialsJsonAdapter.Companion.credentials
+import org.matrix.android.sdk.test.fakes.internal.auth.db.sessionparams.FakeHomeServerConnectionConfigJsonAdapter.Companion.HOME_SERVER_CONNECTION_CONFIG_JSON
+import org.matrix.android.sdk.test.fakes.internal.auth.db.sessionparams.FakeHomeServerConnectionConfigJsonAdapter.Companion.homeServerConnectionConfig
+import org.matrix.android.sdk.test.fixtures.SessionParamsEntityFixture.aSessionParamsEntity
+import org.matrix.android.sdk.test.fixtures.SessionParamsFixture.aSessionParams
+
+internal class FakeSessionParamsMapperMoshi {
+
+    val instance: Moshi = mockk()
+    private val credentialsJsonAdapter = FakeCredentialsJsonAdapter()
+    private val homeServerConnectionConfigAdapter = FakeHomeServerConnectionConfigJsonAdapter()
+
+    init {
+        mockkStatic(Uri::class)
+        every { Uri.parse(any()) } returns mockk()
+        every { instance.adapter(Credentials::class.java) } returns credentialsJsonAdapter.instance
+        every { instance.adapter(HomeServerConnectionConfig::class.java) } returns homeServerConnectionConfigAdapter.instance
+    }
+
+    fun assertSessionParamsWasMappedSuccessfully(sessionParams: SessionParams?) {
+        sessionParams shouldBeEqualTo SessionParams(
+                credentials,
+                homeServerConnectionConfig,
+                sessionParamsEntity.isTokenValid,
+                LoginType.fromName(sessionParamsEntity.loginType)
+        )
+    }
+
+    fun assertSessionParamsIsNull(sessionParams: SessionParams?) {
+        sessionParams.shouldBeNull()
+    }
+
+    fun assertSessionParamsEntityWasMappedSuccessfully(sessionParamsEntity: SessionParamsEntity?) {
+        sessionParamsEntity shouldBeEqualTo SessionParamsEntity(
+                sessionParams.credentials.sessionId(),
+                sessionParams.userId,
+                CREDENTIALS_JSON,
+                HOME_SERVER_CONNECTION_CONFIG_JSON,
+                sessionParams.isTokenValid,
+                sessionParams.loginType.name,
+        )
+    }
+
+    fun assertSessionParamsEntityIsNull(sessionParamsEntity: SessionParamsEntity?) {
+        sessionParamsEntity.shouldBeNull()
+    }
+
+    companion object {
+        val sessionParams = aSessionParams()
+        val sessionParamsEntity = aSessionParamsEntity()
+        val nullSessionParams: SessionParams? = null
+        val nullSessionParamsEntity: SessionParamsEntity? = null
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/CredentialsFixture.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/CredentialsFixture.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2e7b36ff638646b5783c7484f18e43c0956e49fb
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/CredentialsFixture.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.test.fixtures
+
+import org.matrix.android.sdk.api.auth.data.Credentials
+import org.matrix.android.sdk.api.auth.data.DiscoveryInformation
+
+object CredentialsFixture {
+    fun aCredentials(
+            userId: String = "",
+            accessToken: String = "",
+            refreshToken: String? = null,
+            homeServer: String? = null,
+            deviceId: String? = null,
+            discoveryInformation: DiscoveryInformation? = null,
+    ) = Credentials(
+            userId,
+            accessToken,
+            refreshToken,
+            homeServer,
+            deviceId,
+            discoveryInformation,
+    )
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/DiscoveryInformationFixture.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/DiscoveryInformationFixture.kt
new file mode 100644
index 0000000000000000000000000000000000000000..c929a27d23dbeba334a3806ff11ce311950cb554
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/DiscoveryInformationFixture.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.test.fixtures
+
+import org.matrix.android.sdk.api.auth.data.DiscoveryInformation
+import org.matrix.android.sdk.api.auth.data.WellKnownBaseConfig
+
+object DiscoveryInformationFixture {
+    fun aDiscoveryInformation(
+            homeServer: WellKnownBaseConfig? = null,
+            identityServer: WellKnownBaseConfig? = null,
+    ) = DiscoveryInformation(
+            homeServer,
+            identityServer
+    )
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/SessionParamsEntityFixture.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/SessionParamsEntityFixture.kt
new file mode 100644
index 0000000000000000000000000000000000000000..bbea232a223a334e4ff7251d9dd3d35b83bfcbf1
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/SessionParamsEntityFixture.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.test.fixtures
+
+import org.matrix.android.sdk.internal.auth.db.SessionParamsEntity
+
+internal object SessionParamsEntityFixture {
+    fun aSessionParamsEntity(
+            sessionId: String = "",
+            userId: String = "",
+            credentialsJson: String = "",
+            homeServerConnectionConfigJson: String = "",
+            isTokenValid: Boolean = true,
+            loginType: String = "",
+    ) = SessionParamsEntity(
+            sessionId,
+            userId,
+            credentialsJson,
+            homeServerConnectionConfigJson,
+            isTokenValid,
+            loginType,
+    )
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/SessionParamsFixture.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/SessionParamsFixture.kt
new file mode 100644
index 0000000000000000000000000000000000000000..5cbbe1a47a20153036bb03fd1796683bca9725e3
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/SessionParamsFixture.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.test.fixtures
+
+import org.matrix.android.sdk.api.auth.LoginType
+import org.matrix.android.sdk.api.auth.data.Credentials
+import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
+import org.matrix.android.sdk.api.auth.data.SessionParams
+import org.matrix.android.sdk.test.fixtures.CredentialsFixture.aCredentials
+
+object SessionParamsFixture {
+    fun aSessionParams(
+            credentials: Credentials = aCredentials(),
+            homeServerConnectionConfig: HomeServerConnectionConfig = HomeServerConnectionConfig.Builder().withHomeServerUri("homeserver").build(),
+            isTokenValid: Boolean = false,
+            loginType: LoginType = LoginType.UNKNOWN,
+    ) = SessionParams(
+            credentials,
+            homeServerConnectionConfig,
+            isTokenValid,
+            loginType,
+    )
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/WellKnownBaseConfigFixture.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/WellKnownBaseConfigFixture.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a33308dbd68040ac4120ba37efdf5a6b79927be1
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/WellKnownBaseConfigFixture.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.test.fixtures
+
+import org.matrix.android.sdk.api.auth.data.WellKnownBaseConfig
+
+object WellKnownBaseConfigFixture {
+    fun aWellKnownBaseConfig(
+            baseUrl: String? = null,
+    ) = WellKnownBaseConfig(
+            baseUrl,
+    )
+}