diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 61a9130cd9669c3843e6445dfe1fee2d493869bc..fb7f4a8a465d42b4a0390d464b83b99e8465bba7 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
   <component name="CompilerConfiguration">
-    <bytecodeTargetLevel target="1.8" />
+    <bytecodeTargetLevel target="11" />
   </component>
 </project>
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index d5d35ec44f10991b508f6454a85204a276726364..860da66a5ea990f8e3c36fcdb0e2e05dacadf880 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
-  <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
     <output url="file://$PROJECT_DIR$/build/classes" />
   </component>
   <component name="ProjectType">
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
index 7f68460d8b38ac04e3a3224d7c79ef719b1991a9..e497da999824b5c5629039f2e74794dd96b6c419 100644
--- a/.idea/runConfigurations.xml
+++ b/.idea/runConfigurations.xml
@@ -3,6 +3,7 @@
   <component name="RunConfigurationProducerService">
     <option name="ignoredProducers">
       <set>
+        <option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
         <option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
         <option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
         <option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
diff --git a/build.gradle b/build.gradle
index affee31588a15c069a98159df4f174244dd32f73..22f247b6a7d440171c5033c0e9a34f013be83074 100644
--- a/build.gradle
+++ b/build.gradle
@@ -2,8 +2,8 @@
 
 buildscript {
     // Ref: https://kotlinlang.org/releases.html
-    ext.kotlin_version = '1.4.32'
-    ext.kotlin_coroutines_version = "1.4.2"
+    ext.kotlin_version = '1.5.10'
+    ext.kotlin_coroutines_version = "1.5.0"
     repositories {
         google()
         jcenter()
@@ -12,7 +12,7 @@ buildscript {
         }
     }
     dependencies {
-        classpath 'com.android.tools.build:gradle:4.1.3'
+        classpath 'com.android.tools.build:gradle:4.2.1'
         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
 
         // NOTE: Do not place your application dependencies here; they belong
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 517ae0d4cec4df8ed790c8a1c549bab7e37dfa89..e1e2fd2c75e69ac5748b9c767a2b01d8e45f8b30 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=3db89524a3981819ff28c3f979236c1274a726e146ced0c8a2020417f9bc0782
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-all.zip
+distributionSha256Sum=13bf8d3cf8eeeb5770d19741a59bde9bd966dd78d17f1bbad787a05ef19d1c2d
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle
index 046846e5c140ca94a00f8a0b2f4a851f6d958644..973d09bec877e75eb0ad1a62ce8ab8da6412336f 100644
--- a/matrix-sdk-android/build.gradle
+++ b/matrix-sdk-android/build.gradle
@@ -6,13 +6,10 @@ apply plugin: 'realm-android'
 
 buildscript {
     repositories {
-        // mavenCentral()
-        //noinspection GrDeprecatedAPIUsage
-        jcenter()
+        mavenCentral()
     }
     dependencies {
-        // Stick to this version until https://github.com/realm/realm-java/issues/7402 is fixed
-        classpath "io.realm:realm-gradle-plugin:10.3.1"
+        classpath "io.realm:realm-gradle-plugin:10.5.0"
     }
 }
 
@@ -24,7 +21,7 @@ android {
         minSdkVersion 21
         targetSdkVersion 30
         versionCode 1
-        versionName "1.1.5"
+        versionName "1.1.8"
         // Multidex is useful for tests
         multiDexEnabled true
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -115,7 +112,7 @@ dependencies {
     def lifecycle_version = '2.2.0'
     def arch_version = '2.1.0'
     def markwon_version = '3.1.0'
-    def daggerVersion = '2.33'
+    def daggerVersion = '2.35.1'
     def work_version = '2.5.0'
     def retrofit_version = '2.9.0'
 
@@ -123,7 +120,7 @@ dependencies {
     implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
     implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
 
-    implementation "androidx.appcompat:appcompat:1.2.0"
+    implementation "androidx.appcompat:appcompat:1.3.0"
     implementation "androidx.core:core-ktx:1.3.2"
 
     implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
@@ -168,8 +165,11 @@ dependencies {
     implementation 'com.jakewharton.timber:timber:4.7.1'
     implementation 'com.facebook.stetho:stetho-okhttp3:1.6.0'
 
+    // Video compression
+    implementation 'com.otaliastudios:transcoder:0.10.3'
+
     // Phone number https://github.com/google/libphonenumber
-    implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.21'
+    implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.23'
 
     testImplementation 'junit:junit:4.13.2'
     testImplementation 'org.robolectric:robolectric:4.5.1'
@@ -187,7 +187,7 @@ dependencies {
     androidTestImplementation 'androidx.test:rules:1.3.0'
     androidTestImplementation 'androidx.test.ext:junit:1.1.2'
     androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
-    androidTestImplementation 'org.amshove.kluent:kluent-android:1.61'
+    androidTestImplementation 'org.amshove.kluent:kluent-android:1.65'
     androidTestImplementation 'io.mockk:mockk-android:1.11.0'
     androidTestImplementation "androidx.arch.core:core-testing:$arch_version"
     androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
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 5815b23c0642867848b77f76efd6ca65e1f54e81..da176491c696bb8105cfc5f3afe4ee4dabb6b576 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
@@ -66,8 +66,8 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
     fun doE2ETestWithAliceInARoom(encryptedRoom: Boolean = true): CryptoTestData {
         val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
 
-        val roomId = mTestHelper.doSync<String> {
-            aliceSession.createRoom(CreateRoomParams().apply { name = "MyRoom" }, it)
+        val roomId = mTestHelper.runBlockingTest {
+            aliceSession.createRoom(CreateRoomParams().apply { name = "MyRoom" })
         }
 
         if (encryptedRoom) {
@@ -135,7 +135,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
             bobRoomSummariesLive.observeForever(roomJoinedObserver)
         }
 
-        mTestHelper.doSync<Unit> { bobSession.joinRoom(aliceRoomId, callback = it) }
+        mTestHelper.runBlockingTest { bobSession.joinRoom(aliceRoomId) }
 
         mTestHelper.await(lock)
 
@@ -176,8 +176,8 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
             room.invite(samSession.myUserId, null)
         }
 
-        mTestHelper.doSync<Unit> {
-            samSession.joinRoom(room.roomId, null, emptyList(), it)
+        mTestHelper.runBlockingTest {
+            samSession.joinRoom(room.roomId, null, emptyList())
         }
 
         return samSession
@@ -256,8 +256,8 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
     }
 
     fun createDM(alice: Session, bob: Session): String {
-        val roomId = mTestHelper.doSync<String> {
-            alice.createDirectRoom(bob.myUserId, it)
+        val roomId = mTestHelper.runBlockingTest {
+            alice.createDirectRoom(bob.myUserId)
         }
 
         mTestHelper.waitWithLatch { latch ->
@@ -300,7 +300,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
                 bobRoomSummariesLive.observeForever(newRoomObserver)
             }
 
-            mTestHelper.doSync<Unit> { bob.joinRoom(roomId, callback = it) }
+            mTestHelper.runBlockingTest { bob.joinRoom(roomId) }
         }
 
         return roomId
@@ -398,8 +398,8 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
         val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
         aliceSession.cryptoService().setWarnOnUnknownDevices(false)
 
-        val roomId = mTestHelper.doSync<String> {
-            aliceSession.createRoom(CreateRoomParams().apply { name = "MyRoom" }, it)
+        val roomId = mTestHelper.runBlockingTest {
+            aliceSession.createRoom(CreateRoomParams().apply { name = "MyRoom" })
         }
         val room = aliceSession.getRoom(roomId)!!
 
@@ -412,7 +412,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
             val session = mTestHelper.createAccount("User_$index", defaultSessionParams)
             mTestHelper.runBlockingTest(timeout = 600_000) { room.invite(session.myUserId, null) }
             println("TEST -> " + session.myUserId + " invited")
-            mTestHelper.doSync<Unit> { session.joinRoom(room.roomId, null, emptyList(), it) }
+            mTestHelper.runBlockingTest { session.joinRoom(room.roomId, null, emptyList()) }
             println("TEST -> " + session.myUserId + " joined")
             sessions.add(session)
         }
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt
index 33de3456309f95d9b399341d4c20ebe905f4900e..1d05e655afd8943ea7ff6f18dfb64fcad7d30a80 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt
@@ -26,6 +26,7 @@ import org.matrix.android.sdk.internal.di.MatrixModule
 import org.matrix.android.sdk.internal.di.MatrixScope
 import org.matrix.android.sdk.internal.di.NetworkModule
 import org.matrix.android.sdk.internal.raw.RawModule
+import org.matrix.android.sdk.internal.util.system.SystemModule
 
 @Component(modules = [
     TestModule::class,
@@ -33,6 +34,7 @@ import org.matrix.android.sdk.internal.raw.RawModule
     NetworkModule::class,
     AuthModule::class,
     RawModule::class,
+    SystemModule::class,
     TestNetworkModule::class
 ])
 @MatrixScope
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 122584142e57fc5c03a6bca2d5eac3452150d287..a2566c14149a3034e0f63bfa5f24a2eab28cc67d 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
@@ -18,6 +18,8 @@ package org.matrix.android.sdk.internal.crypto
 
 import android.util.Log
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -29,8 +31,6 @@ import org.matrix.android.sdk.common.CommonTestHelper
 import org.matrix.android.sdk.common.CryptoTestHelper
 import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
 import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyContent
-import kotlin.test.assertEquals
-import kotlin.test.assertNotNull
 
 @RunWith(AndroidJUnit4::class)
 @FixMethodOrder(MethodSorters.JVM)
@@ -54,7 +54,7 @@ class PreShareKeysTest : InstrumentedTest {
                     && it.getClearType() == EventType.ROOM_KEY
         }
 
-        assertEquals(0, preShareCount, "Bob should not have receive any key from alice at this point")
+        assertEquals("Bob should not have receive any key from alice at this point", 0, preShareCount)
         Log.d("#Test", "Room Key Received from alice $preShareCount")
 
         // Force presharing of new outbound key
@@ -78,14 +78,14 @@ class PreShareKeysTest : InstrumentedTest {
         }
 
         val content = latest?.getClearContent().toModel<RoomKeyContent>()
-        assertNotNull(content, "Bob should have received and decrypted a room key event from alice")
-        assertEquals(e2eRoomID, content.roomId, "Wrong room")
+        assertNotNull("Bob should have received and decrypted a room key event from alice", content)
+        assertEquals("Wrong room", e2eRoomID, content!!.roomId)
         val megolmSessionId = content.sessionId!!
 
         val sharedIndex = aliceSession.cryptoService().getSharedWithInfo(e2eRoomID, megolmSessionId)
                 .getObject(bobSession.myUserId, bobSession.sessionParams.deviceId)
 
-        assertEquals(0, sharedIndex, "The session received by bob should match what alice sent")
+        assertEquals("The session received by bob should match what alice sent", 0, sharedIndex)
 
         // Just send a real message as test
         val sentEvent = mTestHelper.sendTextMessage(aliceSession.getRoom(e2eRoomID)!!, "Allo", 1).first()
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt
index e6b364f3fb3d9cf0017963f5ec7b3344c9253114..40659cef11a4a955e886a6eaeefe14c8f6e2724f 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt
@@ -71,13 +71,12 @@ class KeyShareTests : InstrumentedTest {
         val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
 
         // Create an encrypted room and add a message
-        val roomId = mTestHelper.doSync<String> {
+        val roomId = mTestHelper.runBlockingTest {
             aliceSession.createRoom(
                     CreateRoomParams().apply {
                         visibility = RoomDirectoryVisibility.PRIVATE
                         enableEncryption()
-                    },
-                    it
+                    }
             )
         }
         val room = aliceSession.getRoom(roomId)
@@ -332,13 +331,12 @@ class KeyShareTests : InstrumentedTest {
         }
 
         // Create an encrypted room and send a couple of messages
-        val roomId = mTestHelper.doSync<String> {
+        val roomId = mTestHelper.runBlockingTest {
             aliceSession.createRoom(
                     CreateRoomParams().apply {
                         visibility = RoomDirectoryVisibility.PRIVATE
                         enableEncryption()
-                    },
-                    it
+                    }
             )
         }
         val roomAlicePov = aliceSession.getRoom(roomId)
@@ -371,8 +369,8 @@ class KeyShareTests : InstrumentedTest {
             roomAlicePov.invite(bobSession.myUserId, null)
         }
 
-        mTestHelper.doSync<Unit> {
-            bobSession.joinRoom(roomAlicePov.roomId, null, emptyList(), it)
+        mTestHelper.runBlockingTest {
+            bobSession.joinRoom(roomAlicePov.roomId, null, emptyList())
         }
 
         // we want to discard alice outbound session
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeTest.kt
index ee604fc9abcc8b03a86509604f8ac03055f23256..76bf6dc0408aa82365e5c840cb195eb46e118536 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/QrCodeTest.kt
@@ -226,12 +226,12 @@ class QrCodeTest : InstrumentedTest {
 
     private fun checkHeader(byteArray: ByteArray) {
         // MATRIX
-        byteArray[0] shouldBeEqualTo 'M'.toByte()
-        byteArray[1] shouldBeEqualTo 'A'.toByte()
-        byteArray[2] shouldBeEqualTo 'T'.toByte()
-        byteArray[3] shouldBeEqualTo 'R'.toByte()
-        byteArray[4] shouldBeEqualTo 'I'.toByte()
-        byteArray[5] shouldBeEqualTo 'X'.toByte()
+        byteArray[0] shouldBeEqualTo 'M'.code.toByte()
+        byteArray[1] shouldBeEqualTo 'A'.code.toByte()
+        byteArray[2] shouldBeEqualTo 'T'.code.toByte()
+        byteArray[3] shouldBeEqualTo 'R'.code.toByte()
+        byteArray[4] shouldBeEqualTo 'I'.code.toByte()
+        byteArray[5] shouldBeEqualTo 'X'.code.toByte()
 
         // Version
         byteArray[6] shouldBeEqualTo 2
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtilsTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtilsTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7ee6caed0da5c0859b5ec27d3736e9096d69feba
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtilsTest.kt
@@ -0,0 +1,184 @@
+/*
+ * Copyright (c) 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.securestorage
+
+import android.os.Build
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.matrix.android.sdk.InstrumentedTest
+import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64
+import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding
+import java.io.ByteArrayOutputStream
+import java.util.UUID
+
+@RunWith(AndroidJUnit4::class)
+@FixMethodOrder(MethodSorters.JVM)
+class SecretStoringUtilsTest : InstrumentedTest {
+
+    private val buildVersionSdkIntProvider = TestBuildVersionSdkIntProvider()
+    private val secretStoringUtils = SecretStoringUtils(context(), buildVersionSdkIntProvider)
+
+    companion object {
+        const val TEST_STR = "This is something I want to store safely!"
+    }
+
+    @Test
+    fun testStringNominalCaseApi21() {
+        val alias = generateAlias()
+        buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP
+        // Encrypt
+        val encrypted = secretStoringUtils.securelyStoreString(TEST_STR, alias)
+        // Decrypt
+        val decrypted = secretStoringUtils.loadSecureSecret(encrypted, alias)
+        decrypted shouldBeEqualTo TEST_STR
+        secretStoringUtils.safeDeleteKey(alias)
+    }
+
+    @Test
+    fun testStringNominalCaseApi23() {
+        val alias = generateAlias()
+        buildVersionSdkIntProvider.value = Build.VERSION_CODES.M
+        // Encrypt
+        val encrypted = secretStoringUtils.securelyStoreString(TEST_STR, alias)
+        // Decrypt
+        val decrypted = secretStoringUtils.loadSecureSecret(encrypted, alias)
+        decrypted shouldBeEqualTo TEST_STR
+        secretStoringUtils.safeDeleteKey(alias)
+    }
+
+    @Test
+    fun testStringNominalCaseApi30() {
+        val alias = generateAlias()
+        buildVersionSdkIntProvider.value = Build.VERSION_CODES.R
+        // Encrypt
+        val encrypted = secretStoringUtils.securelyStoreString(TEST_STR, alias)
+        // Decrypt
+        val decrypted = secretStoringUtils.loadSecureSecret(encrypted, alias)
+        decrypted shouldBeEqualTo TEST_STR
+        secretStoringUtils.safeDeleteKey(alias)
+    }
+
+    @Test
+    fun testStringMigration21_23() {
+        val alias = generateAlias()
+        buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP
+        // Encrypt
+        val encrypted = secretStoringUtils.securelyStoreString(TEST_STR, alias)
+
+        // Simulate a system upgrade
+        buildVersionSdkIntProvider.value = Build.VERSION_CODES.M
+
+        // Decrypt
+        val decrypted = secretStoringUtils.loadSecureSecret(encrypted, alias)
+        decrypted shouldBeEqualTo TEST_STR
+        secretStoringUtils.safeDeleteKey(alias)
+    }
+
+    @Test
+    fun testObjectNominalCaseApi21() {
+        val alias = generateAlias()
+        buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP
+
+        // Encrypt
+        val encrypted = ByteArrayOutputStream().also { outputStream ->
+            outputStream.use {
+                secretStoringUtils.securelyStoreObject(TEST_STR, alias, it)
+            }
+        }
+                .toByteArray()
+                .toBase64NoPadding()
+        // Decrypt
+        val decrypted = encrypted.fromBase64().inputStream().use {
+            secretStoringUtils.loadSecureSecret<String>(it, alias)
+        }
+        decrypted shouldBeEqualTo TEST_STR
+        secretStoringUtils.safeDeleteKey(alias)
+    }
+
+    @Test
+    fun testObjectNominalCaseApi23() {
+        val alias = generateAlias()
+        buildVersionSdkIntProvider.value = Build.VERSION_CODES.M
+
+        // Encrypt
+        val encrypted = ByteArrayOutputStream().also { outputStream ->
+            outputStream.use {
+                secretStoringUtils.securelyStoreObject(TEST_STR, alias, it)
+            }
+        }
+                .toByteArray()
+                .toBase64NoPadding()
+        // Decrypt
+        val decrypted = encrypted.fromBase64().inputStream().use {
+            secretStoringUtils.loadSecureSecret<String>(it, alias)
+        }
+        decrypted shouldBeEqualTo TEST_STR
+        secretStoringUtils.safeDeleteKey(alias)
+    }
+
+    @Test
+    fun testObjectNominalCaseApi30() {
+        val alias = generateAlias()
+        buildVersionSdkIntProvider.value = Build.VERSION_CODES.R
+
+        // Encrypt
+        val encrypted = ByteArrayOutputStream().also { outputStream ->
+            outputStream.use {
+                secretStoringUtils.securelyStoreObject(TEST_STR, alias, it)
+            }
+        }
+                .toByteArray()
+                .toBase64NoPadding()
+        // Decrypt
+        val decrypted = encrypted.fromBase64().inputStream().use {
+            secretStoringUtils.loadSecureSecret<String>(it, alias)
+        }
+        decrypted shouldBeEqualTo TEST_STR
+        secretStoringUtils.safeDeleteKey(alias)
+    }
+
+    @Test
+    fun testObjectMigration21_23() {
+        val alias = generateAlias()
+        buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP
+
+        // Encrypt
+        val encrypted = ByteArrayOutputStream().also { outputStream ->
+            outputStream.use {
+                secretStoringUtils.securelyStoreObject(TEST_STR, alias, it)
+            }
+        }
+                .toByteArray()
+                .toBase64NoPadding()
+
+        // Simulate a system upgrade
+        buildVersionSdkIntProvider.value = Build.VERSION_CODES.M
+
+        // Decrypt
+        val decrypted = encrypted.fromBase64().inputStream().use {
+            secretStoringUtils.loadSecureSecret<String>(it, alias)
+        }
+        decrypted shouldBeEqualTo TEST_STR
+        secretStoringUtils.safeDeleteKey(alias)
+    }
+
+    private fun generateAlias() = UUID.randomUUID().toString()
+}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/TestBuildVersionSdkIntProvider.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/TestBuildVersionSdkIntProvider.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b08c88fb246da80217ee2c84e079053e56382319
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/TestBuildVersionSdkIntProvider.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.securestorage
+
+import org.matrix.android.sdk.internal.util.system.BuildVersionSdkIntProvider
+
+class TestBuildVersionSdkIntProvider : BuildVersionSdkIntProvider {
+    var value: Int = 0
+
+    override fun get() = value
+}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt
index ff07cf1d1d4ab473b389cc8e6d2509f818b5dae9..ace48cef77d365a4c03450e226caee540629984f 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt
@@ -16,6 +16,7 @@
 
 package org.matrix.android.sdk.session.room.timeline
 
+import org.junit.Assert.fail
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -29,7 +30,6 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
 import org.matrix.android.sdk.common.CommonTestHelper
 import org.matrix.android.sdk.common.CryptoTestHelper
 import java.util.concurrent.CountDownLatch
-import kotlin.test.fail
 
 @RunWith(JUnit4::class)
 @FixMethodOrder(MethodSorters.JVM)
@@ -80,6 +80,7 @@ class TimelineWithManyMembersTest : InstrumentedTest {
                                     return@createEventListener true
                                 } else {
                                     fail("User " + session.myUserId + " decrypted as " + body + " CryptoError: " + it.root.mCryptoError)
+                                    false
                                 }
                             } ?: return@createEventListener 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
new file mode 100644
index 0000000000000000000000000000000000000000..a1744a0dae2fabf3069df7b54001ab11e1219d78
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt
@@ -0,0 +1,222 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.session.space
+
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+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.ActiveSpaceFilter
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
+import org.matrix.android.sdk.api.session.room.model.GuestAccess
+import org.matrix.android.sdk.api.session.room.model.Membership
+import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
+import org.matrix.android.sdk.api.session.room.model.RoomGuestAccessContent
+import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
+import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent
+import org.matrix.android.sdk.api.session.room.model.RoomType
+import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
+import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset
+import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
+import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
+import org.matrix.android.sdk.api.session.space.JoinSpaceResult
+import org.matrix.android.sdk.common.CommonTestHelper
+import org.matrix.android.sdk.common.SessionTestParams
+
+@RunWith(JUnit4::class)
+@FixMethodOrder(MethodSorters.JVM)
+class SpaceCreationTest : InstrumentedTest {
+
+    private val commonTestHelper = CommonTestHelper(context())
+
+    @Test
+    fun createSimplePublicSpace() {
+        val session = commonTestHelper.createAccount("Hubble", SessionTestParams(true))
+        val roomName = "My Space"
+        val topic = "A public space for test"
+        var spaceId: String = ""
+        commonTestHelper.waitWithLatch {
+            GlobalScope.launch {
+                spaceId = session.spaceService().createSpace(roomName, topic, null, true)
+                // wait a bit to let the summary update it self :/
+                it.countDown()
+            }
+        }
+
+        val syncedSpace = session.spaceService().getSpace(spaceId)
+        commonTestHelper.waitWithLatch {
+            commonTestHelper.retryPeriodicallyWithLatch(it) {
+                syncedSpace?.asRoom()?.roomSummary()?.name != null
+            }
+        }
+        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")
+
+        assertNotNull("Space should be found by Id", syncedSpace)
+        val creationEvent = syncedSpace!!.asRoom().getStateEvent(EventType.STATE_ROOM_CREATE)
+        val createContent = creationEvent?.content.toModel<RoomCreateContent>()
+        assertEquals("Room type should be space", RoomType.SPACE, createContent?.type)
+
+        var powerLevelsContent: PowerLevelsContent? = null
+        commonTestHelper.waitWithLatch { latch ->
+            commonTestHelper.retryPeriodicallyWithLatch(latch) {
+                val toModel = syncedSpace.asRoom().getStateEvent(EventType.STATE_ROOM_POWER_LEVELS)?.content.toModel<PowerLevelsContent>()
+                powerLevelsContent = toModel
+                toModel != null
+            }
+        }
+        assertEquals("Space-rooms should be created with a power level for events_default of 100", 100, powerLevelsContent?.eventsDefault)
+
+        val guestAccess = syncedSpace.asRoom().getStateEvent(EventType.STATE_ROOM_GUEST_ACCESS)?.content
+                ?.toModel<RoomGuestAccessContent>()?.guestAccess
+
+        assertEquals("Public space room should be peekable by guest", GuestAccess.CanJoin, guestAccess)
+
+        val historyVisibility = syncedSpace.asRoom().getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY)?.content
+                ?.toModel<RoomHistoryVisibilityContent>()?.historyVisibility
+
+        assertEquals("Public space room should be world readable", RoomHistoryVisibility.WORLD_READABLE, historyVisibility)
+
+        commonTestHelper.signOutAndClose(session)
+    }
+
+    @Test
+    fun testJoinSimplePublicSpace() {
+        val aliceSession = commonTestHelper.createAccount("alice", SessionTestParams(true))
+        val bobSession = commonTestHelper.createAccount("bob", SessionTestParams(true))
+
+        val roomName = "My Space"
+        val topic = "A public space for test"
+        val spaceId: String
+        runBlocking {
+            spaceId = aliceSession.spaceService().createSpace(roomName, topic, null, true)
+            // wait a bit to let the summary update it self :/
+            delay(400)
+        }
+
+        // Try to join from bob, it's a public space no need to invite
+
+        val joinResult: JoinSpaceResult
+        runBlocking {
+            joinResult = bobSession.spaceService().joinSpace(spaceId)
+        }
+
+        assertEquals(JoinSpaceResult.Success, joinResult)
+
+        val spaceBobPov = bobSession.spaceService().getSpace(spaceId)
+        assertEquals("Room name should be set", roomName, spaceBobPov?.asRoom()?.roomSummary()?.name)
+        assertEquals("Room topic should be set", topic, spaceBobPov?.asRoom()?.roomSummary()?.topic)
+
+        commonTestHelper.signOutAndClose(aliceSession)
+        commonTestHelper.signOutAndClose(bobSession)
+    }
+
+    @Test
+    fun testSimplePublicSpaceWithChildren() {
+        val aliceSession = commonTestHelper.createAccount("alice", SessionTestParams(true))
+        val bobSession = commonTestHelper.createAccount("bob", SessionTestParams(true))
+
+        val roomName = "My Space"
+        val topic = "A public space for test"
+
+        val spaceId: String = runBlocking { aliceSession.spaceService().createSpace(roomName, topic, null, true) }
+        val syncedSpace = aliceSession.spaceService().getSpace(spaceId)
+
+        // create a room
+        var firstChild: String? = null
+        commonTestHelper.waitWithLatch {
+            GlobalScope.launch {
+                firstChild = aliceSession.createRoom(CreateRoomParams().apply {
+                    this.name = "FirstRoom"
+                    this.topic = "Description of first room"
+                    this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT
+                })
+                it.countDown()
+            }
+        }
+
+        commonTestHelper.waitWithLatch {
+            GlobalScope.launch {
+                syncedSpace?.addChildren(firstChild!!, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "a", true, suggested = true)
+                it.countDown()
+            }
+        }
+
+        var secondChild: String? = null
+        commonTestHelper.waitWithLatch {
+            GlobalScope.launch {
+                secondChild = aliceSession.createRoom(CreateRoomParams().apply {
+                    this.name = "SecondRoom"
+                    this.topic = "Description of second room"
+                    this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT
+                })
+                it.countDown()
+            }
+        }
+
+        commonTestHelper.waitWithLatch {
+            GlobalScope.launch {
+                syncedSpace?.addChildren(secondChild!!, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "b", false, suggested = true)
+                it.countDown()
+            }
+        }
+
+        // Try to join from bob, it's a public space no need to invite
+        var joinResult: JoinSpaceResult? = null
+        commonTestHelper.waitWithLatch {
+            GlobalScope.launch {
+                joinResult = bobSession.spaceService().joinSpace(spaceId)
+                // wait a bit to let the summary update it self :/
+                it.countDown()
+            }
+        }
+
+        assertEquals(JoinSpaceResult.Success, joinResult)
+
+        val spaceBobPov = bobSession.spaceService().getSpace(spaceId)
+        assertEquals("Room name should be set", roomName, spaceBobPov?.asRoom()?.roomSummary()?.name)
+        assertEquals("Room topic should be set", topic, spaceBobPov?.asRoom()?.roomSummary()?.topic)
+
+        // check if bob has joined automatically the first room
+
+        val bobMembershipFirstRoom = bobSession.getRoomSummary(firstChild!!)?.membership
+        assertEquals("Bob should have joined this room", Membership.JOIN, bobMembershipFirstRoom)
+        RoomSummaryQueryParams.Builder()
+
+        val childCount = bobSession.getRoomSummaries(
+                roomSummaryQueryParams {
+                    activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(spaceId)
+                }
+        ).size
+
+        assertEquals("Unexpected number of joined children", 1, childCount)
+
+        commonTestHelper.signOutAndClose(aliceSession)
+        commonTestHelper.signOutAndClose(bobSession)
+    }
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..521b5805bd81f116aac86364095ac9fa1d292526
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt
@@ -0,0 +1,472 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.session.space
+
+import android.util.Log
+import androidx.lifecycle.Observer
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+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.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.ActiveSpaceFilter
+import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.api.session.room.model.RoomSummary
+import org.matrix.android.sdk.api.session.room.model.RoomType
+import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
+import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
+import org.matrix.android.sdk.common.CommonTestHelper
+import org.matrix.android.sdk.common.SessionTestParams
+
+@RunWith(JUnit4::class)
+@FixMethodOrder(MethodSorters.JVM)
+class SpaceHierarchyTest : InstrumentedTest {
+
+    private val commonTestHelper = CommonTestHelper(context())
+
+    @Test
+    fun createCanonicalChildRelation() {
+        val session = commonTestHelper.createAccount("John", SessionTestParams(true))
+        val spaceName = "My Space"
+        val topic = "A public space for test"
+        var spaceId: String = ""
+        commonTestHelper.waitWithLatch {
+            GlobalScope.launch {
+                spaceId = session.spaceService().createSpace(spaceName, topic, null, true)
+                it.countDown()
+            }
+        }
+
+        val syncedSpace = session.spaceService().getSpace(spaceId)
+
+        var roomId: String = ""
+        commonTestHelper.waitWithLatch {
+            GlobalScope.launch {
+                roomId = session.createRoom(CreateRoomParams().apply { name = "General" })
+                it.countDown()
+            }
+        }
+
+        val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
+
+        commonTestHelper.waitWithLatch {
+            GlobalScope.launch {
+                syncedSpace!!.addChildren(roomId, viaServers, null, true)
+                it.countDown()
+            }
+        }
+
+        commonTestHelper.waitWithLatch {
+            GlobalScope.launch {
+                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")
+        }
+
+        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
+//    fun testCreateChildRelations() {
+//        val session = commonTestHelper.createAccount("Jhon", SessionTestParams(true))
+//        val spaceName = "My Space"
+//        val topic = "A public space for test"
+//        Log.d("## TEST", "Before")
+//
+//        var spaceId = ""
+//        commonTestHelper.waitWithLatch {
+//            GlobalScope.launch {
+//                spaceId = session.spaceService().createSpace(spaceName, topic, null, true)
+//                it.countDown()
+//            }
+//        }
+//
+//        Log.d("## TEST", "created space $spaceId ${Thread.currentThread()}")
+//        val syncedSpace = session.spaceService().getSpace(spaceId)
+//
+//        val children = listOf("General" to true /*canonical*/, "Random" to false)
+//
+// //        val roomIdList = children.map {
+// //            runBlocking {
+// //                session.createRoom(CreateRoomParams().apply { name = it.first })
+// //            } to it.second
+// //        }
+//        val roomIdList = mutableListOf<Pair<String, Boolean>>()
+//        commonTestHelper.waitWithLatch {
+//            GlobalScope.launch {
+//                children.forEach {
+//                    val rID = session.createRoom(CreateRoomParams().apply { name = it.first })
+//                    roomIdList.add(rID to it.second)
+//                }
+//                it.countDown()
+//            }
+//        }
+//
+//        val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
+//
+//        commonTestHelper.waitWithLatch {
+//            GlobalScope.launch {
+//                roomIdList.forEach { entry ->
+//                    syncedSpace!!.addChildren(entry.first, viaServers, null, true)
+//                }
+//                it.countDown()
+//            }
+//        }
+//
+//        commonTestHelper.waitWithLatch {
+//            GlobalScope.launch {
+//                roomIdList.forEach {
+//                    session.spaceService().setSpaceParent(it.first, spaceId, it.second, viaServers)
+//                }
+//                it.countDown()
+//            }
+//        }
+//
+//        roomIdList.forEach {
+//            val parents = session.getRoom(it.first)?.roomSummary()?.spaceParents
+//            val canonicalParents = session.getRoom(it.first)?.roomSummary()?.spaceParents?.filter { it.canonical == true }
+//
+//            assertNotNull(parents)
+//            assertEquals("Unexpected number of parent", 1, parents!!.size)
+//            assertEquals("Unexpected parent name", spaceName, parents.first().roomSummary?.name)
+//            assertEquals("Parent of ${it.first} should be canonical ${it.second}", if (it.second) 1 else 0, canonicalParents?.size ?: 0)
+//        }
+//    }
+
+    @Test
+    fun testFilteringBySpace() {
+        val session = commonTestHelper.createAccount("John", SessionTestParams(true))
+
+        val spaceAInfo = createPublicSpace(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)
+        ))
+
+        val spaceCInfo = createPublicSpace(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 {
+            GlobalScope.launch {
+                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 {
+            GlobalScope.launch {
+                orphan1 = session.createRoom(CreateRoomParams().apply { name = "O1" })
+                it.countDown()
+            }
+        }
+
+        var orphan2 = ""
+        commonTestHelper.waitWithLatch {
+            GlobalScope.launch {
+                orphan2 = session.createRoom(CreateRoomParams().apply { name = "O2" })
+                it.countDown()
+            }
+        }
+
+        val allRooms = session.getRoomSummaries(roomSummaryQueryParams { excludeType = listOf(RoomType.SPACE) })
+
+        assertEquals("Unexpected number of rooms", 9, allRooms.size)
+
+        val orphans = session.getFlattenRoomSummaryChildrenOf(null)
+
+        assertEquals("Unexpected number of orphan rooms", 2, orphans.size)
+        assertTrue("O1 should be an orphan", orphans.any { it.roomId == orphan1 })
+        assertTrue("O2 should be an orphan ${orphans.map { it.name }}", orphans.any { it.roomId == orphan2 })
+
+        val aChildren = session.getFlattenRoomSummaryChildrenOf(spaceAInfo.spaceId)
+
+        assertEquals("Unexpected number of flatten child rooms", 4, aChildren.size)
+        assertTrue("A1 should be a child of A", aChildren.any { it.name == "A1" })
+        assertTrue("A2 should be a child of A", aChildren.any { it.name == "A2" })
+        assertTrue("CA should be a grand child of A", aChildren.any { it.name == "C1" })
+        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 {
+            GlobalScope.launch {
+                val a3 = session.createRoom(CreateRoomParams().apply { name = "A3" })
+                spaceA!!.addChildren(a3, viaServers, null, false)
+                it.countDown()
+            }
+        }
+
+        Thread.sleep(2_000)
+        val orphansUpdate = session.getRoomSummaries(roomSummaryQueryParams {
+            activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(null)
+        })
+        assertEquals("Unexpected number of orphan rooms ${orphansUpdate.map { it.name }}", 2, orphansUpdate.size)
+    }
+
+    @Test
+    fun testBreakCycle() {
+        val session = commonTestHelper.createAccount("John", SessionTestParams(true))
+
+        val spaceAInfo = createPublicSpace(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)
+        ))
+
+        // add C as a subspace of A
+        val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId)
+        val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
+        commonTestHelper.waitWithLatch {
+            GlobalScope.launch {
+                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 {
+            GlobalScope.launch {
+                val spaceC = session.spaceService().getSpace(spaceCInfo.spaceId)
+                spaceC!!.addChildren(spaceAInfo.spaceId, viaServers, null, true)
+                it.countDown()
+            }
+        }
+
+        Thread.sleep(1000)
+
+        // A -> C -> A
+
+        val aChildren = session.getFlattenRoomSummaryChildrenOf(spaceAInfo.spaceId)
+
+        assertEquals("Unexpected number of flatten child rooms ${aChildren.map { it.name }}", 4, aChildren.size)
+        assertTrue("A1 should be a child of A", aChildren.any { it.name == "A1" })
+        assertTrue("A2 should be a child of A", aChildren.any { it.name == "A2" })
+        assertTrue("CA should be a grand child of A", aChildren.any { it.name == "C1" })
+        assertTrue("A1 should be a grand child of A", aChildren.any { it.name == "C2" })
+    }
+
+    @Test
+    fun testLiveFlatChildren() {
+        val session = commonTestHelper.createAccount("John", SessionTestParams(true))
+
+        val spaceAInfo = createPublicSpace(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)
+        ))
+
+        // add B as a subspace of A
+        val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId)
+        val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
+        runBlocking {
+            spaceA!!.addChildren(spaceBInfo.spaceId, viaServers, null, true)
+            session.spaceService().setSpaceParent(spaceBInfo.spaceId, spaceAInfo.spaceId, true, viaServers)
+        }
+
+        val flatAChildren = runBlocking(Dispatchers.Main) {
+            session.getFlattenRoomSummaryChildrenOfLive(spaceAInfo.spaceId)
+        }
+
+        commonTestHelper.waitWithLatch { latch ->
+
+            val childObserver = object : Observer<List<RoomSummary>> {
+                override fun onChanged(children: List<RoomSummary>?) {
+//                    Log.d("## TEST", "Space A flat children update : ${children?.map { it.name }}")
+                    System.out.println("## TEST | Space A flat children update : ${children?.map { it.name }}")
+                    if (children?.any { it.name == "C1" } == true && children.any { it.name == "C2" }) {
+                        // B1 has been added live!
+                        latch.countDown()
+                        flatAChildren.removeObserver(this)
+                    }
+                }
+            }
+
+            val spaceCInfo = createPublicSpace(session, "SpaceC", listOf(
+                    Triple("C1", true /*auto-join*/, true/*canonical*/),
+                    Triple("C2", true, true)
+            ))
+
+            // add C as subspace of B
+            runBlocking {
+                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
+
+            GlobalScope.launch(Dispatchers.Main) { flatAChildren.observeForever(childObserver) }
+        }
+
+        // Test part one of the rooms
+
+        val bRoomId = spaceBInfo.roomIds.first()
+        val bRoom = session.getRoom(bRoomId)
+
+        commonTestHelper.waitWithLatch { latch ->
+
+            val childObserver = object : Observer<List<RoomSummary>> {
+                override fun onChanged(children: List<RoomSummary>?) {
+                    System.out.println("## TEST | Space A flat children update : ${children?.map { it.name }}")
+                    if (children?.any { it.roomId == bRoomId } == false) {
+                        // B1 has been added live!
+                        latch.countDown()
+                        flatAChildren.removeObserver(this)
+                    }
+                }
+            }
+
+            // part from b room
+            runBlocking {
+                bRoom!!.leave(null)
+            }
+            // The room should have disapear from flat children
+            GlobalScope.launch(Dispatchers.Main) { flatAChildren.observeForever(childObserver) }
+        }
+    }
+
+    data class TestSpaceCreationResult(
+            val spaceId: String,
+            val roomIds: List<String>
+    )
+
+    private fun createPublicSpace(session: Session,
+                                  spaceName: String,
+                                  childInfo: List<Triple<String, Boolean, Boolean?>>
+            /** Name, auto-join, canonical*/
+    ): TestSpaceCreationResult {
+        var spaceId = ""
+        commonTestHelper.waitWithLatch {
+            GlobalScope.launch {
+                spaceId = session.spaceService().createSpace(spaceName, "Test Topic", null, true)
+                it.countDown()
+            }
+        }
+
+        val syncedSpace = session.spaceService().getSpace(spaceId)
+        val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
+
+        val roomIds =
+                childInfo.map { entry ->
+                    var roomId = ""
+                    commonTestHelper.waitWithLatch {
+                        GlobalScope.launch {
+                            roomId = session.createRoom(CreateRoomParams().apply { name = entry.first })
+                            it.countDown()
+                        }
+                    }
+                    roomId
+                }
+
+        roomIds.forEachIndexed { index, roomId ->
+            runBlocking {
+                syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second)
+                val canonical = childInfo[index].third
+                if (canonical != null) {
+                    session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers)
+                }
+            }
+        }
+        return TestSpaceCreationResult(spaceId, roomIds)
+    }
+
+    @Test
+    fun testRootSpaces() {
+        val session = commonTestHelper.createAccount("John", SessionTestParams(true))
+
+        val spaceAInfo = createPublicSpace(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)
+        ))
+
+        val spaceCInfo = createPublicSpace(session, "SpaceC", listOf(
+                Triple("C1", true /*auto-join*/, true/*canonical*/),
+                Triple("C2", true, true)
+        ))
+
+        val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
+
+        // add C as subspace of B
+        runBlocking {
+            val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId)
+            spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
+        }
+
+        Thread.sleep(2000)
+        // + A
+        //   a1, a2
+        // + B
+        //  b1, b2, b3
+        //   + C
+        //     + c1, c2
+
+        val rootSpaces = session.spaceService().getRootSpaceSummaries()
+
+        assertEquals("Unexpected number of root spaces ${rootSpaces.map { it.name }}", 2, rootSpaces.size)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt
index a7f5163774517dbe0ff588741568028f171a65e9..5e359172431564f5c880c20a9e480f2d50604674 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt
@@ -51,11 +51,15 @@ interface AuthenticationService {
 
     /**
      * Return a LoginWizard, to login to the homeserver. The login flow has to be retrieved first.
+     *
+     * See [LoginWizard] for more details
      */
     fun getLoginWizard(): LoginWizard
 
     /**
      * Return a RegistrationWizard, to create an matrix account on the homeserver. The login flow has to be retrieved first.
+     *
+     * See [RegistrationWizard] for more details.
      */
     fun getRegistrationWizard(): RegistrationWizard
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt
index f1f9ba3916b6e5718309a179068d01629230b50e..7d1407c0d81a9186c7cb912ef0a21d251611f316 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt
@@ -16,12 +16,10 @@
 
 package org.matrix.android.sdk.api.auth.data
 
-sealed class LoginFlowResult {
-    data class Success(
-            val supportedLoginTypes: List<String>,
-            val ssoIdentityProviders: List<SsoIdentityProvider>?,
-            val isLoginAndRegistrationSupported: Boolean,
-            val homeServerUrl: String,
-            val isOutdatedHomeserver: Boolean
-    ) : LoginFlowResult()
-}
+data class LoginFlowResult(
+        val supportedLoginTypes: List<String>,
+        val ssoIdentityProviders: List<SsoIdentityProvider>?,
+        val isLoginAndRegistrationSupported: Boolean,
+        val homeServerUrl: String,
+        val isOutdatedHomeserver: Boolean
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt
index cfaf74ce240f4ee8c9bf4650b9302815a92a641c..64b3e180aac660ca16dbf6679f69b227d10988dd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt
@@ -48,7 +48,7 @@ data class SsoIdentityProvider(
          */
         @Json(name = "brand") val brand: String?
 
-) : Parcelable {
+) : Parcelable, Comparable<SsoIdentityProvider> {
 
     companion object {
         const val BRAND_GOOGLE = "org.matrix.google"
@@ -58,4 +58,25 @@ data class SsoIdentityProvider(
         const val BRAND_TWITTER = "org.matrix.twitter"
         const val BRAND_GITLAB = "org.matrix.gitlab"
     }
+
+    override fun compareTo(other: SsoIdentityProvider): Int {
+        return other.toPriority().compareTo(toPriority())
+    }
+
+    private fun toPriority(): Int {
+        return when (brand) {
+            // We are on Android, so user is more likely to have a Google account
+            BRAND_GOOGLE   -> 5
+            // Facebook is also an important SSO provider
+            BRAND_FACEBOOK -> 4
+            // Twitter is more for professionals
+            BRAND_TWITTER  -> 3
+            // Here it's very for techie people
+            BRAND_GITHUB,
+            BRAND_GITLAB   -> 2
+            // And finally, if the account has been created with an iPhone...
+            BRAND_APPLE    -> 1
+            else           -> 0
+        }
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginProfileInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginProfileInfo.kt
new file mode 100644
index 0000000000000000000000000000000000000000..288a6d12326a0aef323d6ed9f79e0bf6016a5c25
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginProfileInfo.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.auth.login
+
+data class LoginProfileInfo(
+        val matrixId: String,
+        val displayName: String?,
+        val fullAvatarUrl: String?
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt
index 9c96cba40c7710ef6edf79ec60e7ba2763ed0f41..a2a93738370e8a79737e3a741f8b9eda48413b05 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt
@@ -17,34 +17,51 @@
 package org.matrix.android.sdk.api.auth.login
 
 import org.matrix.android.sdk.api.session.Session
-import org.matrix.android.sdk.api.util.Cancelable
 
+/**
+ * Set of methods to be able to login to an existing account on a homeserver.
+ *
+ * More documentation can be found in the file https://github.com/vector-im/element-android/blob/main/docs/signin.md
+ */
 interface LoginWizard {
+    /**
+     * Get some information about a matrixId: displayName and avatar url
+     */
+    suspend fun getProfileInfo(matrixId: String): LoginProfileInfo
 
     /**
-     * @param login the login field
-     * @param password the password field
+     * Login to the homeserver.
+     *
+     * @param login the login field. Can be a user name, or a msisdn (email or phone number) associated to the account
+     * @param password the password of the account
      * @param deviceName the initial device name
-     * @param callback  the matrix callback on which you'll receive the result of authentication.
-     * @return a [Cancelable]
+     * @return a [Session] if the login is successful
      */
     suspend fun login(login: String,
                       password: String,
                       deviceName: String): Session
 
     /**
-     * Exchange a login token to an access token
+     * Exchange a login token to an access token.
+     *
+     * @param loginToken login token, obtain when login has happen in a WebView, using SSO
+     * @return a [Session] if the login is successful
      */
     suspend fun loginWithToken(loginToken: String): Session
 
     /**
-     * Reset user password
+     * Ask the homeserver to reset the user password. The password will not be reset until
+     * [resetPasswordMailConfirmed] is successfully called.
+     *
+     * @param email an email previously associated to the account the user wants the password to be reset.
+     * @param newPassword the desired new password
      */
     suspend fun resetPassword(email: String,
                               newPassword: String)
 
     /**
      * Confirm the new password, once the user has checked their email
+     * When this method succeed, tha account password will be effectively modified.
      */
     suspend fun resetPasswordMailConfirmed()
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt
index 38a5a77291c1e8688b0cd6981ea54cda8350915f..621253faa5c7d31692024c32602591777f8edd0b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationWizard.kt
@@ -16,30 +16,98 @@
 
 package org.matrix.android.sdk.api.auth.registration
 
+/**
+ * Set of methods to be able to create an account on a homeserver.
+ *
+ * Common scenario to register an account successfully:
+ *  - Call [getRegistrationFlow] to check that you application supports all the mandatory registration stages
+ *  - Call [createAccount] to start the account creation
+ *  - Fulfill all mandatory stages using the methods [performReCaptcha] [acceptTerms] [dummy], etc.
+ *
+ * More documentation can be found in the file https://github.com/vector-im/element-android/blob/main/docs/signup.md
+ * and https://matrix.org/docs/spec/client_server/latest#account-registration-and-management
+ */
 interface RegistrationWizard {
-
+    /**
+     * Call this method to get the possible registration flow of the current homeserver.
+     * It can be useful to ensure that your application implementation supports all the stages
+     * required to create an account. If it is not the case, you will have to use the web fallback
+     * to let the user create an account with your application.
+     * See [org.matrix.android.sdk.api.auth.AuthenticationService.getFallbackUrl]
+     */
     suspend fun getRegistrationFlow(): RegistrationResult
 
-    suspend fun createAccount(userName: String, password: String, initialDeviceDisplayName: String?): RegistrationResult
+    /**
+     * Can be call to check is the desired userName is available for registration on the current homeserver.
+     * It may also fails if the desired userName is not correctly formatted or does not follow any restriction on
+     * the homeserver. Ex: userName with only digits may be rejected.
+     * @param userName the desired username. Ex: "alice"
+     */
+    suspend fun registrationAvailable(userName: String): RegistrationAvailability
+
+    /**
+     * This is the first method to call in order to create an account and start the registration process.
+     *
+     * @param userName the desired username. Ex: "alice"
+     * @param password the desired password
+     * @param initialDeviceDisplayName the device display name
+     */
+    suspend fun createAccount(userName: String?,
+                              password: String?,
+                              initialDeviceDisplayName: String?): RegistrationResult
 
+    /**
+     * Perform the "m.login.recaptcha" stage.
+     *
+     * @param response the response from ReCaptcha
+     */
     suspend fun performReCaptcha(response: String): RegistrationResult
 
+    /**
+     * Perform the "m.login.terms" stage.
+     */
     suspend fun acceptTerms(): RegistrationResult
 
+    /**
+     * Perform the "m.login.dummy" stage.
+     */
     suspend fun dummy(): RegistrationResult
 
+    /**
+     * Perform the "m.login.email.identity" or "m.login.msisdn" stage.
+     *
+     * @param threePid the threePid to add to the account. If this is an email, the homeserver will send an email
+     * to validate it. For a msisdn a SMS will be sent.
+     */
     suspend fun addThreePid(threePid: RegisterThreePid): RegistrationResult
 
+    /**
+     * Ask the homeserver to send again the current threePid (email or msisdn).
+     */
     suspend fun sendAgainThreePid(): RegistrationResult
 
+    /**
+     * Send the code received by SMS to validate a msisdn.
+     * If the code is correct, the registration request will be executed to validate the msisdn.
+     */
     suspend fun handleValidateThreePid(code: String): RegistrationResult
 
+    /**
+     * Useful to poll the homeserver when waiting for the email to be validated by the user.
+     * Once the email is validated, this method will return successfully.
+     * @param delayMillis the SDK can wait before sending the request
+     */
     suspend fun checkIfEmailHasBeenValidated(delayMillis: Long): RegistrationResult
 
-    suspend fun registrationAvailable(userName: String): RegistrationAvailability
-
+    /**
+     * This is the current ThreePid, waiting for validation. The SDK will store it in database, so it can be
+     * restored even if the app has been killed during the registration
+     */
     val currentThreePid: String?
 
-    // True when login and password has been sent with success to the homeserver
+    /**
+     * True when login and password have been sent with success to the homeserver, i.e. [createAccount] has been
+     * called successfully.
+     */
     val isRegistrationStarted: Boolean
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Failure.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Failure.kt
index b2419033649c36684ff79cd54bff16dcd71a08f3..8f1bbb69415304e017729891c44f093d17608087 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Failure.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Failure.kt
@@ -32,7 +32,6 @@ import java.io.IOException
  */
 sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) {
     data class Unknown(val throwable: Throwable? = null) : Failure(throwable)
-    data class Cancelled(val throwable: Throwable? = null) : Failure(throwable)
     data class UnrecognizedCertificateFailure(val url: String, val fingerprint: Fingerprint) : Failure()
     data class NetworkConnection(val ioException: IOException? = null) : Failure(ioException)
     data class ServerError(val error: MatrixError, val httpCode: Int) : Failure(RuntimeException(error.toString()))
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/MatrixError.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/MatrixError.kt
index 3820a442aaa7ef42416a7c7301539e39eebf6f50..73b0fe0a7c0604310398706bd243ed166b3c4c17 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/MatrixError.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/MatrixError.kt
@@ -41,7 +41,7 @@ data class MatrixError(
         // For M_LIMIT_EXCEEDED
         @Json(name = "retry_after_ms") val retryAfterMillis: Long? = null,
         // For M_UNKNOWN_TOKEN
-        @Json(name = "soft_logout") val isSoftLogout: Boolean = false,
+        @Json(name = "soft_logout") val isSoftLogout: Boolean? = null,
         // For M_INVALID_PEPPER
         // {"error": "pepper does not match 'erZvr'", "lookup_pepper": "pQgMS", "algorithm": "sha256", "errcode": "M_INVALID_PEPPER"}
         @Json(name = "lookup_pepper") val newLookupPepper: String? = null,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/ActiveSpaceFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/ActiveSpaceFilter.kt
new file mode 100644
index 0000000000000000000000000000000000000000..48619b9394bb85ba20700c3a9f63147ee750c64f
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/ActiveSpaceFilter.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.query
+
+sealed class ActiveSpaceFilter {
+    object None : ActiveSpaceFilter()
+    data class ActiveSpace(val currentSpaceId: String?) : ActiveSpaceFilter()
+    data class ExcludeSpace(val spaceId: String) : ActiveSpaceFilter()
+}
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 a15799d862ff4a601deb57ea1fb0df2e4e291228..b5f90e87ea92a4184e3f774fe8150442c7819b5f 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
@@ -39,6 +39,7 @@ import org.matrix.android.sdk.api.session.identity.IdentityService
 import org.matrix.android.sdk.api.session.initsync.InitialSyncProgressService
 import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService
 import org.matrix.android.sdk.api.session.media.MediaService
+import org.matrix.android.sdk.api.session.openid.OpenIdService
 import org.matrix.android.sdk.api.session.permalinks.PermalinkService
 import org.matrix.android.sdk.api.session.profile.ProfileService
 import org.matrix.android.sdk.api.session.pushers.PushersService
@@ -48,6 +49,7 @@ import org.matrix.android.sdk.api.session.search.SearchService
 import org.matrix.android.sdk.api.session.securestorage.SecureStorageService
 import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
 import org.matrix.android.sdk.api.session.signout.SignOutService
+import org.matrix.android.sdk.api.session.space.SpaceService
 import org.matrix.android.sdk.api.session.sync.FilterService
 import org.matrix.android.sdk.api.session.sync.SyncState
 import org.matrix.android.sdk.api.session.terms.TermsService
@@ -227,6 +229,16 @@ interface Session :
      */
     fun thirdPartyService(): ThirdPartyService
 
+    /**
+     * Returns the space service associated with the session
+     */
+    fun spaceService(): SpaceService
+
+    /**
+     * Returns the open id service associated with the session
+     */
+    fun openIdService(): OpenIdService
+
     /**
      * Add a listener to the session.
      * @param listener the listener to add.
@@ -249,13 +261,13 @@ interface Session :
     /**
      * A global session listener to get notified for some events.
      */
-    interface Listener {
+    interface Listener : SessionLifecycleObserver {
         /**
          * Possible cases:
          * - The access token is not valid anymore,
          * - a M_CONSENT_NOT_GIVEN error has been received from the homeserver
          */
-        fun onGlobalError(globalError: GlobalError)
+        fun onGlobalError(session: Session, globalError: GlobalError)
     }
 
     val sharedSecretStorageService: SharedSecretStorageService
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionLifecycleObserver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/SessionLifecycleObserver.kt
similarity index 80%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionLifecycleObserver.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/SessionLifecycleObserver.kt
index cb37fbec75054ad2eff7be0f5ccf8585034865db..b76e454e4b528f9b02b5a7388d759914d3279d49 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionLifecycleObserver.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/SessionLifecycleObserver.kt
@@ -14,20 +14,19 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.session
+package org.matrix.android.sdk.api.session
 
 import androidx.annotation.MainThread
 
 /**
  * This defines methods associated with some lifecycle events of a session.
- * A list of SessionLifecycle will be injected into [DefaultSession]
  */
-internal interface SessionLifecycleObserver {
+interface SessionLifecycleObserver {
     /*
     Called when the session is opened
      */
     @MainThread
-    fun onSessionStarted() {
+    fun onSessionStarted(session: Session) {
         // noop
     }
 
@@ -35,7 +34,7 @@ internal interface SessionLifecycleObserver {
     Called when the session is cleared
      */
     @MainThread
-    fun onClearCache() {
+    fun onClearCache(session: Session) {
         // noop
     }
 
@@ -43,7 +42,7 @@ internal interface SessionLifecycleObserver {
     Called when the session is closed
      */
     @MainThread
-    fun onSessionStopped() {
+    fun onSessionStopped(session: Session) {
         // noop
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUploadStateTracker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUploadStateTracker.kt
index 924da6c19bc1d67851e8cb299da027f640459ed5..ec63eb0be2e242cc0a0e1b830593fc1de0197868 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUploadStateTracker.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUploadStateTracker.kt
@@ -31,6 +31,8 @@ interface ContentUploadStateTracker {
     sealed class State {
         object Idle : State()
         object EncryptingThumbnail : State()
+        object CompressingImage : State()
+        data class CompressingVideo(val percent: Float) : State()
         data class UploadingThumbnail(val current: Long, val total: Long) : State()
         data class Encrypting(val current: Long, val total: Long) : State()
         data class Uploading(val current: Long, val total: Long) : State()
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 89b873febbb6d593d93fe470acf11a7e1a2b4ee0..6400dd644408f59b01bd2858bb0f4613e485fe57 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
@@ -28,6 +28,8 @@ import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
 import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
 import org.matrix.android.sdk.internal.di.MoshiProvider
 import org.json.JSONObject
+import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.api.failure.MatrixError
 import timber.log.Timber
 
 typealias Content = JsonDict
@@ -90,6 +92,16 @@ data class Event(
     @Transient
     var sendState: SendState = SendState.UNKNOWN
 
+    @Transient
+    var sendStateDetails: String? = null
+
+    fun sendStateError(): MatrixError? {
+        return sendStateDetails?.let {
+            val matrixErrorAdapter = MoshiProvider.providesMoshi().adapter(MatrixError::class.java)
+            tryOrNull { matrixErrorAdapter.fromJson(it) }
+        }
+    }
+
     /**
      * The `age` value transcoded in a timestamp based on the device clock when the SDK received
      * the event from the home server.
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 905e18b8e8b95cef884fca97ba468b14454e900f..d2befca1ee146e5a1c239320f9dcb875a30589c3 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
@@ -52,6 +52,10 @@ object EventType {
     const val STATE_ROOM_GUEST_ACCESS = "m.room.guest_access"
     const val STATE_ROOM_POWER_LEVELS = "m.room.power_levels"
 
+    const val STATE_SPACE_CHILD = "m.space.child"
+
+    const val STATE_SPACE_PARENT = "m.space.parent"
+
     /**
      * Note that this Event has been deprecated, see
      * - https://matrix.org/docs/spec/client_server/r0.6.1#historical-events
@@ -74,6 +78,7 @@ object EventType {
     const val CALL_NEGOTIATE = "m.call.negotiate"
     const val CALL_REJECT = "m.call.reject"
     const val CALL_HANGUP = "m.call.hangup"
+
     // This type is not processed by the client, just sent to the server
     const val CALL_REPLACES = "m.call.replaces"
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt
index adfdc2498e40c45e4b05d2e1ce0f86224b4ca97d..23dc1e0ba8906a8596e8d5172bce5b3d7cdacebe 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/file/FileService.kt
@@ -29,14 +29,19 @@ import java.io.File
  */
 interface FileService {
 
-    enum class FileState {
-        IN_CACHE,
-        DOWNLOADING,
-        UNKNOWN
+    sealed class FileState {
+        /**
+         * The original file is in cache, but the decrypted files can be deleted for security reason.
+         * To decrypt the file again, call [downloadFile], the encrypted file will not be downloaded again
+         * @param decryptedFileInCache true if the decrypted file is available. Always true for clear files.
+         */
+        data class InCache(val decryptedFileInCache: Boolean) : FileState()
+        object Downloading : FileState()
+        object Unknown : FileState()
     }
 
     /**
-     * Download a file.
+     * Download a file if necessary and ensure that if the file is encrypted, the file is decrypted.
      * Result will be a decrypted file, stored in the cache folder. url parameter will be used to create unique filename to avoid name collision.
      */
     suspend fun downloadFile(fileName: String,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/openid/OpenIdService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/openid/OpenIdService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..65f6214f93345feff02d7a1abcc8a3617cd8d758
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/openid/OpenIdService.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.openid
+
+interface OpenIdService {
+
+    /**
+     * Gets an OpenID token object that the requester may supply to another service to verify their identity in Matrix.
+     * The generated token is only valid for exchanging for user information from the federation API for OpenID.
+     */
+    suspend fun getOpenIdToken(): OpenIdToken
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/openid/OpenIdToken.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/openid/OpenIdToken.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2c2ea6568171f225b0a9696e1dd1b49c77672a2d
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/openid/OpenIdToken.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.openid
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class OpenIdToken(
+        /**
+         * Required. An access token the consumer may use to verify the identity of the person who generated the token.
+         * This is given to the federation API GET /openid/userinfo to verify the user's identity.
+         */
+        @Json(name = "access_token")
+        val accessToken: String,
+
+        /**
+         * Required. The string "Bearer".
+         */
+        @Json(name = "token_type")
+        val tokenType: String,
+
+        /**
+         * Required. The homeserver domain the consumer should use when attempting to verify the user's identity.
+         */
+        @Json(name = "matrix_server_name")
+        val matrixServerName: String,
+
+        /**
+         * Required. The number of seconds before this token expires and a new one must be generated.
+         */
+        @Json(name = "expires_in")
+        val expiresIn: Int
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt
index e493adeaf273ad5cb4421a659d48de1764094e5e..05fa24946a3fe21f61cfb993f5662e9df48ea29d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt
@@ -66,6 +66,7 @@ interface ProfileService {
     /**
      * Get the combined profile information for this user.
      * This may return keys which are not limited to displayname or avatar_url.
+     * If server is configured as limit_profile_requests_to_users_who_share_rooms: true then response can be HTTP 403.
      * @param userId the userId param to look for
      *
      */
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt
index 9ea820f5b3d80280d19833976e01dbbf32e09d2c..a5ec100f64cf29bfd6dc19889d2362deb589e98d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt
@@ -66,12 +66,13 @@ interface PushersService {
 
     /**
      * Directly ask the push gateway to send a push to this device
+     * If successful, the push gateway has accepted the request. In this case, the app should receive a Push with the provided eventId.
+     * In case of error, PusherRejected will be thrown. In this case it means that the pushkey is not valid.
+     *
      * @param url the push gateway url (full path)
      * @param appId the application id
      * @param pushkey the FCM token
      * @param eventId the eventId which will be sent in the Push message. Use a fake eventId.
-     * @param callback callback to know if the push gateway has accepted the request. In this case, the app should receive a Push with the provided eventId.
-     *                 In case of error, PusherRejected failure can happen. In this case it means that the pushkey is not valid.
      */
     suspend fun testPush(url: String,
                          appId: String,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt
index 257c83564e0c2df663e485b5947a9899b4cbcd5d..8c434fc44016246de6cf3c867a226be9d8ad6bbd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt
@@ -34,6 +34,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineService
 import org.matrix.android.sdk.api.session.room.typing.TypingService
 import org.matrix.android.sdk.api.session.room.uploads.UploadsService
 import org.matrix.android.sdk.api.session.search.SearchResult
+import org.matrix.android.sdk.api.session.space.Space
 import org.matrix.android.sdk.api.util.Optional
 
 /**
@@ -82,7 +83,7 @@ interface Room :
      * @param beforeLimit how many events before the result are returned.
      * @param afterLimit how many events after the result are returned.
      * @param includeProfile requests that the server returns the historic profile information for the users that sent the events that were returned.
-     * @param callback Callback to get the search result
+     * @return The search result
      */
     suspend fun search(searchTerm: String,
                nextBatch: String?,
@@ -91,4 +92,9 @@ interface Room :
                beforeLimit: Int,
                afterLimit: Int,
                includeProfile: Boolean): SearchResult
+
+    /**
+     * Use this room as a Space, if the type is correct.
+     */
+    fun asSpace(): Space?
 }
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 8c833644ee51af5b04e541c905855018d5e5ee11..871c5378a6c19fcbbc76f3fdc403e55441d984d5 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
@@ -18,15 +18,14 @@ package org.matrix.android.sdk.api.session.room
 
 import androidx.lifecycle.LiveData
 import androidx.paging.PagedList
-import org.matrix.android.sdk.api.MatrixCallback
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
+import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
 import org.matrix.android.sdk.api.session.room.model.RoomSummary
 import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
 import org.matrix.android.sdk.api.session.room.peeking.PeekResult
 import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
-import org.matrix.android.sdk.api.util.Cancelable
 import org.matrix.android.sdk.api.util.Optional
 import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription
 
@@ -38,22 +37,19 @@ interface RoomService {
     /**
      * Create a room asynchronously
      */
-    fun createRoom(createRoomParams: CreateRoomParams,
-                   callback: MatrixCallback<String>): Cancelable
+    suspend fun createRoom(createRoomParams: CreateRoomParams): String
 
     /**
      * Create a direct room asynchronously. This is a facility method to create a direct room with the necessary parameters
      */
-    fun createDirectRoom(otherUserId: String,
-                         callback: MatrixCallback<String>): Cancelable {
+    suspend fun createDirectRoom(otherUserId: String): String {
         return createRoom(
                 CreateRoomParams()
                         .apply {
                             invitedUserIds.add(otherUserId)
                             setDirectMessage()
                             enableEncryptionIfInvitedUsersSupportIt = true
-                        },
-                callback
+                        }
         )
     }
 
@@ -63,10 +59,9 @@ interface RoomService {
      * @param reason optional reason for joining the room
      * @param viaServers the servers to attempt to join the room through. One of the servers must be participating in the room.
      */
-    fun joinRoom(roomIdOrAlias: String,
-                 reason: String? = null,
-                 viaServers: List<String> = emptyList(),
-                 callback: MatrixCallback<Unit>): Cancelable
+    suspend fun joinRoom(roomIdOrAlias: String,
+                         reason: String? = null,
+                         viaServers: List<String> = emptyList())
 
     /**
      * Get a room from a roomId
@@ -112,20 +107,18 @@ interface RoomService {
      * Inform the Matrix SDK that a room is displayed.
      * The SDK will update the breadcrumbs in the user account data
      */
-    fun onRoomDisplayed(roomId: String): Cancelable
+    suspend fun onRoomDisplayed(roomId: String)
 
     /**
      * Mark all rooms as read
      */
-    fun markAllAsRead(roomIds: List<String>,
-                      callback: MatrixCallback<Unit>): Cancelable
+    suspend fun markAllAsRead(roomIds: List<String>)
 
     /**
      * Resolve a room alias to a room ID.
      */
-    fun getRoomIdByAlias(roomAlias: String,
-                         searchOnServer: Boolean,
-                         callback: MatrixCallback<Optional<RoomAliasDescription>>): Cancelable
+    suspend fun getRoomIdByAlias(roomAlias: String,
+                                 searchOnServer: Boolean): Optional<RoomAliasDescription>
 
     /**
      * Delete a room alias
@@ -172,26 +165,28 @@ interface RoomService {
     /**
      * Get some state events about a room
      */
-    fun getRoomState(roomId: String, callback: MatrixCallback<List<Event>>)
+    suspend fun getRoomState(roomId: String): List<Event>
 
     /**
      * Use this if you want to get information from a room that you are not yet in (or invited)
      * It might be possible to get some information on this room if it is public or if guest access is allowed
      * This call will try to gather some information on this room, but it could fail and get nothing more
      */
-    fun peekRoom(roomIdOrAlias: String, callback: MatrixCallback<PeekResult>)
+    suspend fun peekRoom(roomIdOrAlias: String): PeekResult
 
     /**
      * TODO Doc
      */
     fun getPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams,
-                                  pagedListConfig: PagedList.Config = defaultPagedListConfig): LiveData<PagedList<RoomSummary>>
+                                  pagedListConfig: PagedList.Config = defaultPagedListConfig,
+                                  sortOrder: RoomSortOrder = RoomSortOrder.ACTIVITY): LiveData<PagedList<RoomSummary>>
 
     /**
      * TODO Doc
      */
     fun getFilteredPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams,
-                                          pagedListConfig: PagedList.Config = defaultPagedListConfig): UpdatableFilterLivePageResult
+                                          pagedListConfig: PagedList.Config = defaultPagedListConfig,
+                                          sortOrder: RoomSortOrder = RoomSortOrder.ACTIVITY): UpdatableLivePageResult
 
     /**
      * TODO Doc
@@ -205,4 +200,12 @@ interface RoomService {
                 .setEnablePlaceholders(false)
                 .setPrefetchDistance(10)
                 .build()
+
+    fun getFlattenRoomSummaryChildrenOf(spaceId: String?, memberships: List<Membership> = Membership.activeMemberships()) : List<RoomSummary>
+
+    /**
+     * Returns all the children of this space, as LiveData
+     */
+    fun getFlattenRoomSummaryChildrenOfLive(spaceId: String?,
+                                            memberships: List<Membership> = Membership.activeMemberships()): LiveData<List<RoomSummary>>
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSortOrder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSortOrder.kt
new file mode 100644
index 0000000000000000000000000000000000000000..36da24252733ced7607170ef3f5e7b43020bb433
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomSortOrder.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.room
+
+enum class RoomSortOrder {
+    NAME,
+    ACTIVITY,
+    NONE
+}
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 7e04ebb5f295fbed1ec39a22f444a63fdae81fc7..88ec2de768a4774e7ac19b8ae7de9d8db34eb067 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
@@ -16,15 +16,35 @@
 
 package org.matrix.android.sdk.api.session.room
 
+import org.matrix.android.sdk.api.query.ActiveSpaceFilter
 import org.matrix.android.sdk.api.query.QueryStringValue
 import org.matrix.android.sdk.api.query.RoomCategoryFilter
 import org.matrix.android.sdk.api.query.RoomTagQueryFilter
 import org.matrix.android.sdk.api.session.room.model.Membership
+import org.matrix.android.sdk.api.session.room.model.RoomType
+import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams
 
 fun roomSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {}): RoomSummaryQueryParams {
     return RoomSummaryQueryParams.Builder().apply(init).build()
 }
 
+fun spaceSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {}): SpaceSummaryQueryParams {
+    return RoomSummaryQueryParams.Builder()
+            .apply(init)
+            .apply {
+                includeType = listOf(RoomType.SPACE)
+                excludeType = null
+                roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
+            }
+            .build()
+}
+
+enum class RoomCategoryFilter {
+    ONLY_DM,
+    ONLY_ROOMS,
+    ALL
+}
+
 /**
  * This class can be used to filter room summaries to use with:
  * [org.matrix.android.sdk.api.session.room.Room] and [org.matrix.android.sdk.api.session.room.RoomService]
@@ -35,7 +55,11 @@ data class RoomSummaryQueryParams(
         val canonicalAlias: QueryStringValue,
         val memberships: List<Membership>,
         val roomCategoryFilter: RoomCategoryFilter?,
-        val roomTagQueryFilter: RoomTagQueryFilter?
+        val roomTagQueryFilter: RoomTagQueryFilter?,
+        val excludeType: List<String?>?,
+        val includeType: List<String?>?,
+        val activeSpaceFilter: ActiveSpaceFilter?,
+        var activeGroupId: String? = null
 ) {
 
     class Builder {
@@ -46,6 +70,10 @@ data class RoomSummaryQueryParams(
         var memberships: List<Membership> = Membership.all()
         var roomCategoryFilter: RoomCategoryFilter? = RoomCategoryFilter.ALL
         var roomTagQueryFilter: RoomTagQueryFilter? = null
+        var excludeType: List<String?>? = listOf(RoomType.SPACE)
+        var includeType: List<String?>? = null
+        var activeSpaceFilter: ActiveSpaceFilter = ActiveSpaceFilter.None
+        var activeGroupId: String? = null
 
         fun build() = RoomSummaryQueryParams(
                 roomId = roomId,
@@ -53,7 +81,11 @@ data class RoomSummaryQueryParams(
                 canonicalAlias = canonicalAlias,
                 memberships = memberships,
                 roomCategoryFilter = roomCategoryFilter,
-                roomTagQueryFilter = roomTagQueryFilter
+                roomTagQueryFilter = roomTagQueryFilter,
+                excludeType = excludeType,
+                includeType = includeType,
+                activeSpaceFilter = activeSpaceFilter,
+                activeGroupId = activeGroupId
         )
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableFilterLivePageResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableLivePageResult.kt
similarity index 72%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableFilterLivePageResult.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableLivePageResult.kt
index 71b3c665e709b2bbff8c4b41db0e91f9af016402..b83f57f5ef83ac73626fc8d19118f81eb96eb151 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableFilterLivePageResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/UpdatableLivePageResult.kt
@@ -20,8 +20,16 @@ import androidx.lifecycle.LiveData
 import androidx.paging.PagedList
 import org.matrix.android.sdk.api.session.room.model.RoomSummary
 
-interface UpdatableFilterLivePageResult {
+interface UpdatableLivePageResult {
     val livePagedList: LiveData<PagedList<RoomSummary>>
 
-    fun updateQuery(queryParams: RoomSummaryQueryParams)
+    fun updateQuery(builder: (RoomSummaryQueryParams) -> RoomSummaryQueryParams)
+
+    val liveBoundaries: LiveData<ResultBoundaries>
 }
+
+data class ResultBoundaries(
+        val frontLoaded: Boolean = false,
+        val endLoaded: Boolean = false,
+        val zeroItemLoaded: Boolean = false
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/alias/RoomAliasError.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/alias/RoomAliasError.kt
index d2cb7c58a91747e5a3c882365921268945c9d50a..1102eda11c2ffcc89cd61f1da4811a61bf5327d2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/alias/RoomAliasError.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/alias/RoomAliasError.kt
@@ -17,7 +17,7 @@
 package org.matrix.android.sdk.api.session.room.alias
 
 sealed class RoomAliasError : Throwable() {
-    object AliasEmpty : RoomAliasError()
+    object AliasIsBlank : RoomAliasError()
     object AliasNotAvailable : RoomAliasError()
     object AliasInvalid : RoomAliasError()
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/CreateRoomFailure.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/CreateRoomFailure.kt
index 208cdd45563fd120e1d3acd5e689d9c2b0500c5a..deab0ca3e75ee1a76259df29b3277f892406c447 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/CreateRoomFailure.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/CreateRoomFailure.kt
@@ -21,7 +21,7 @@ import org.matrix.android.sdk.api.failure.MatrixError
 import org.matrix.android.sdk.api.session.room.alias.RoomAliasError
 
 sealed class CreateRoomFailure : Failure.FeatureFailure() {
-    object CreatedWithTimeout : CreateRoomFailure()
+    data class CreatedWithTimeout(val roomID: String) : CreateRoomFailure()
     data class CreatedWithFederationFailure(val matrixError: MatrixError) : CreateRoomFailure()
     data class AliasError(val aliasError: RoomAliasError) : CreateRoomFailure()
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PowerLevelsContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PowerLevelsContent.kt
index e778f5740ddff6e2ab5eae1b546715f75fa8fa22..5c46db7166efbaadd24479c70e3fee3cd3626e40 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PowerLevelsContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PowerLevelsContent.kt
@@ -28,43 +28,43 @@ data class PowerLevelsContent(
         /**
          * The level required to ban a user. Defaults to 50 if unspecified.
          */
-        @Json(name = "ban") val ban: Int = Role.Moderator.value,
+        @Json(name = "ban") val ban: Int? = null,
         /**
          * The level required to kick a user. Defaults to 50 if unspecified.
          */
-        @Json(name = "kick") val kick: Int = Role.Moderator.value,
+        @Json(name = "kick") val kick: Int? = null,
         /**
          * The level required to invite a user. Defaults to 50 if unspecified.
          */
-        @Json(name = "invite") val invite: Int = Role.Moderator.value,
+        @Json(name = "invite") val invite: Int? = null,
         /**
          * The level required to redact an event. Defaults to 50 if unspecified.
          */
-        @Json(name = "redact") val redact: Int = Role.Moderator.value,
+        @Json(name = "redact") val redact: Int? = null,
         /**
          * The default level required to send message events. Can be overridden by the events key. Defaults to 0 if unspecified.
          */
-        @Json(name = "events_default") val eventsDefault: Int = Role.Default.value,
+        @Json(name = "events_default") val eventsDefault: Int? = null,
         /**
          * The level required to send specific event types. This is a mapping from event type to power level required.
          */
-        @Json(name = "events") val events: Map<String, Int> = emptyMap(),
+        @Json(name = "events") val events: Map<String, Int>? = null,
         /**
          * The default power level for every user in the room, unless their user_id is mentioned in the users key. Defaults to 0 if unspecified.
          */
-        @Json(name = "users_default") val usersDefault: Int = Role.Default.value,
+        @Json(name = "users_default") val usersDefault: Int? = null,
         /**
          * The power levels for specific users. This is a mapping from user_id to power level for that user.
          */
-        @Json(name = "users") val users: Map<String, Int> = emptyMap(),
+        @Json(name = "users") val users: Map<String, Int>? = null,
         /**
          * The default level required to send state events. Can be overridden by the events key. Defaults to 50 if unspecified.
          */
-        @Json(name = "state_default") val stateDefault: Int = Role.Moderator.value,
+        @Json(name = "state_default") val stateDefault: Int? = null,
         /**
          * The power level requirements for specific notification types. This is a mapping from key to power level for that notifications key.
          */
-        @Json(name = "notifications") val notifications: Map<String, Any> = emptyMap()
+        @Json(name = "notifications") val notifications: Map<String, Any>? = null
 ) {
     /**
      * Return a copy of this content with a new power level for the specified user
@@ -74,7 +74,7 @@ data class PowerLevelsContent(
      */
     fun setUserPowerLevel(userId: String, powerLevel: Int?): PowerLevelsContent {
         return copy(
-                users = users.toMutableMap().apply {
+                users = users.orEmpty().toMutableMap().apply {
                     if (powerLevel == null || powerLevel == usersDefault) {
                         remove(userId)
                     } else {
@@ -91,7 +91,7 @@ data class PowerLevelsContent(
      * @return the level, default to Moderator if the key is not found
      */
     fun notificationLevel(key: String): Int {
-        return when (val value = notifications[key]) {
+        return when (val value = notifications.orEmpty()[key]) {
             // the first implementation was a string value
             is String -> value.toInt()
             is Double -> value.toInt()
@@ -107,3 +107,12 @@ data class PowerLevelsContent(
         const val NOTIFICATIONS_ROOM_KEY = "room"
     }
 }
+
+// Fallback to default value, defined in the Matrix specification
+fun PowerLevelsContent.banOrDefault() = ban ?: Role.Moderator.value
+fun PowerLevelsContent.kickOrDefault() = kick ?: Role.Moderator.value
+fun PowerLevelsContent.inviteOrDefault() = invite ?: Role.Moderator.value
+fun PowerLevelsContent.redactOrDefault() = redact ?: Role.Moderator.value
+fun PowerLevelsContent.eventsDefaultOrDefault() = eventsDefault ?: Role.Default.value
+fun PowerLevelsContent.usersDefaultOrDefault() = usersDefault ?: Role.Default.value
+fun PowerLevelsContent.stateDefaultOrDefault() = stateDefault ?: Role.Moderator.value
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomGuestAccessContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomGuestAccessContent.kt
index 0760c6f1b42900df128467e6e0795b266b5de284..020e7ed39e50c6c40400bb2025ccdffc19e63de4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomGuestAccessContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomGuestAccessContent.kt
@@ -40,7 +40,7 @@ data class RoomGuestAccessContent(
 }
 
 @JsonClass(generateAdapter = false)
-enum class GuestAccess {
-    @Json(name = "can_join") CanJoin,
-    @Json(name = "forbidden") Forbidden
+enum class GuestAccess(val value: String) {
+    @Json(name = "can_join") CanJoin("can_join"),
+    @Json(name = "forbidden") Forbidden("forbidden")
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRules.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRules.kt
index f3e8d357f3149f6990ba9a259d0e57a8f34c9da8..a86301a2761dbc5cc6d7c6d1e9540e38579d3be5 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRules.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRules.kt
@@ -24,9 +24,10 @@ import com.squareup.moshi.JsonClass
  * Enum for [RoomJoinRulesContent] : https://matrix.org/docs/spec/client_server/r0.4.0#m-room-join-rules
  */
 @JsonClass(generateAdapter = false)
-enum class RoomJoinRules {
-    @Json(name = "public") PUBLIC,
-    @Json(name = "invite") INVITE,
-    @Json(name = "knock") KNOCK,
-    @Json(name = "private") PRIVATE
+enum class RoomJoinRules(val value: String) {
+    @Json(name = "public") PUBLIC("public"),
+    @Json(name = "invite") INVITE("invite"),
+    @Json(name = "knock") KNOCK("knock"),
+    @Json(name = "private") PRIVATE("private"),
+    @Json(name = "restricted") RESTRICTED("restricted")
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesAllowEntry.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesAllowEntry.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7b87bc34d2917c514f9e36286b893fe566955823
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesAllowEntry.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.matrix.android.sdk.api.session.room.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class RoomJoinRulesAllowEntry(
+        /**
+         * space: The room ID of the space to check the membership of.
+         */
+        @Json(name = "space") val spaceID: String,
+        /**
+         * via: A list of servers which may be used to peek for membership of the space.
+         */
+        @Json(name = "via") val via: List<String>
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesContent.kt
index 8082486b22f1c8174704eee4c50d715b037e851b..33f402cad3ded1d9ef2a966411332bc7baa71f5d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomJoinRulesContent.kt
@@ -1,5 +1,6 @@
 /*
  * Copyright 2020 The Matrix.org Foundation C.I.C.
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -26,14 +27,19 @@ import timber.log.Timber
  */
 @JsonClass(generateAdapter = true)
 data class RoomJoinRulesContent(
-        @Json(name = "join_rule") val _joinRules: String? = null
+        @Json(name = "join_rule") val _joinRules: String? = null,
+        /**
+         * If the allow key is an empty list (or not a list at all), then the room reverts to standard public join rules
+         */
+        @Json(name = "allow") val allowList: List<RoomJoinRulesAllowEntry>? = null
 ) {
     val joinRules: RoomJoinRules? = when (_joinRules) {
-        "public"  -> RoomJoinRules.PUBLIC
-        "invite"  -> RoomJoinRules.INVITE
-        "knock"   -> RoomJoinRules.KNOCK
+        "public" -> RoomJoinRules.PUBLIC
+        "invite" -> RoomJoinRules.INVITE
+        "knock" -> RoomJoinRules.KNOCK
         "private" -> RoomJoinRules.PRIVATE
-        else      -> {
+        "restricted" -> RoomJoinRules.RESTRICTED
+        else         -> {
             Timber.w("Invalid value for RoomJoinRules: `$_joinRules`")
             null
         }
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 9455a83afff2309c5649d8b16391b46d8090c205..cae4775e7173580d369b3564ad5bc60e7cad7427 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
@@ -26,7 +26,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
  * This class holds some data of a room.
  * It can be retrieved by [org.matrix.android.sdk.api.session.room.Room] and [org.matrix.android.sdk.api.session.room.RoomService]
  */
-data class RoomSummary constructor(
+data class RoomSummary(
         val roomId: String,
         // Computed display name
         val displayName: String = "",
@@ -35,7 +35,9 @@ data class RoomSummary constructor(
         val avatarUrl: String = "",
         val canonicalAlias: String? = null,
         val aliases: List<String> = emptyList(),
+        val joinRules: RoomJoinRules? = null,
         val isDirect: Boolean = false,
+        val directUserId: String? = null,
         val joinedMembersCount: Int? = 0,
         val invitedMembersCount: Int? = 0,
         val latestPreviewableEvent: TimelineEvent? = null,
@@ -54,7 +56,11 @@ data class RoomSummary constructor(
         val inviterId: String? = null,
         val breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS,
         val roomEncryptionTrustLevel: RoomEncryptionTrustLevel? = null,
-        val hasFailedSending: Boolean = false
+        val hasFailedSending: Boolean = false,
+        val roomType: String? = null,
+        val spaceParents: List<SpaceParentInfo>? = null,
+        val spaceChildren: List<SpaceChildInfo>? = null,
+        val flattenParentIds: List<String> = emptyList()
 ) {
 
     val isVersioned: Boolean
@@ -69,6 +75,9 @@ data class RoomSummary constructor(
     val isFavorite: Boolean
         get() = hasTag(RoomTag.ROOM_TAG_FAVOURITE)
 
+    val isPublic: Boolean
+        get() = joinRules == RoomJoinRules.PUBLIC
+
     fun hasTag(tag: String) = tags.any { it.name == tag }
 
     val canStartCall: Boolean
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomThirdPartyInviteContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomThirdPartyInviteContent.kt
index 56503e3e35624a1b1008c2ab88d9c32c56409085..a8a2cfb68b2b51ad4e9b41319d436813186ff241 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomThirdPartyInviteContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomThirdPartyInviteContent.kt
@@ -47,7 +47,7 @@ data class RoomThirdPartyInviteContent(
         /**
          * Keys with which the token may be signed.
          */
-        @Json(name = "public_keys") val publicKeys: List<PublicKeys>? = emptyList()
+        @Json(name = "public_keys") val publicKeys: List<PublicKeys>?
 )
 
 @JsonClass(generateAdapter = true)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomType.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b0f3a56d67cccda2da6d744c778b31527088f484
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomType.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.room.model
+
+object RoomType {
+
+    const val SPACE = "m.space"
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt
new file mode 100644
index 0000000000000000000000000000000000000000..66293bcb8c6107bf88e4c0b8c0e07b15b7e4e171
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.room.model
+
+data class SpaceChildInfo(
+        val childRoomId: String,
+        // We might not know this child at all,
+        // i.e we just know it exists but no info on type/name/etc..
+        val isKnown: Boolean,
+        val roomType: String?,
+        val name: String?,
+        val topic: String?,
+        val avatarUrl: String?,
+        val order: String?,
+        val activeMemberCount: Int?,
+        val autoJoin: Boolean,
+        val viaServers: List<String>,
+        val parentRoomId: String?,
+        val suggested: Boolean?,
+        val canonicalAlias: String?,
+        val aliases: List<String>?
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceParentInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceParentInfo.kt
new file mode 100644
index 0000000000000000000000000000000000000000..5ed81b06464a32ddd68901d3cfba5256abf29497
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceParentInfo.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.room.model
+
+data class SpaceParentInfo(
+        val parentId: String?,
+        val roomSummary: RoomSummary?,
+        val canonical: Boolean?,
+        val viaServers: List<String>
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt
index 80e3741a0c90e4ed721e4f452ff59807827e2b0b..ca8c66bb3b5b9115a4811c2ab11255e05a139612 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomParams.kt
@@ -18,13 +18,15 @@ package org.matrix.android.sdk.api.session.room.model.create
 
 import android.net.Uri
 import org.matrix.android.sdk.api.session.identity.ThreePid
+import org.matrix.android.sdk.api.session.room.model.GuestAccess
 import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
 import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
 import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
+import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry
 import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
 
 // TODO Give a way to include other initial states
-class CreateRoomParams {
+open class CreateRoomParams {
     /**
      * A public visibility indicates that the room will be shown in the published room list.
      * A private visibility will hide the room from the published room list.
@@ -68,6 +70,11 @@ class CreateRoomParams {
      */
     val invite3pids = mutableListOf<ThreePid>()
 
+    /**
+     * Initial Guest Access
+     */
+    var guestAccess: GuestAccess? = null
+
     /**
      * If set to true, when the room will be created, if cross-signing is enabled and we can get keys for every invited users,
      * the encryption will be enabled on the created room
@@ -111,6 +118,17 @@ class CreateRoomParams {
             }
         }
 
+    var roomType: String? = null // RoomType.MESSAGING
+        set(value) {
+            field = value
+            if (value != null) {
+                creationContent[CREATION_CONTENT_KEY_ROOM_TYPE] = value
+            } else {
+                // This is the default value, we remove the field
+                creationContent.remove(CREATION_CONTENT_KEY_ROOM_TYPE)
+            }
+        }
+
     /**
      * The power level content to override in the default power level event
      */
@@ -136,7 +154,12 @@ class CreateRoomParams {
         algorithm = MXCRYPTO_ALGORITHM_MEGOLM
     }
 
+    var roomVersion: String? = null
+
+    var joinRuleRestricted: List<RoomJoinRulesAllowEntry>? = null
+
     companion object {
         private const val CREATION_CONTENT_KEY_M_FEDERATE = "m.federate"
+        private const val CREATION_CONTENT_KEY_ROOM_TYPE = "type"
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/RoomCreateContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/RoomCreateContent.kt
index 0b595b1b2b0a399e8985d08e1bcb0bc0f1f5e9bd..52e5c0e9c7f67081934570e0aa22b1fd6b523f3a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/RoomCreateContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/RoomCreateContent.kt
@@ -26,5 +26,7 @@ import com.squareup.moshi.JsonClass
 data class RoomCreateContent(
         @Json(name = "creator") val creator: String? = null,
         @Json(name = "room_version") val roomVersion: String? = null,
-        @Json(name = "predecessor") val predecessor: Predecessor? = null
+        @Json(name = "predecessor") val predecessor: Predecessor? = null,
+        // Defines the room type, see #RoomType (user extensible)
+        @Json(name = "type") val type: String? = null
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/FileInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/FileInfo.kt
index e85bb0800a4bb63fb4391b4f7969024776ca2bb3..f21074096eff00850147cd62bfcb10e9bc830585 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/FileInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/FileInfo.kt
@@ -47,3 +47,10 @@ data class FileInfo(
          */
         @Json(name = "thumbnail_file") val thumbnailFile: EncryptedFileInfo? = null
 )
+
+/**
+ * Get the url of the encrypted thumbnail or of the thumbnail
+ */
+fun FileInfo.getThumbnailUrl(): String? {
+        return thumbnailFile?.url ?: thumbnailUrl
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ImageInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ImageInfo.kt
index 048febec3994329e82b5ad0b27cf1e6fe9b3e67e..c38ef5bc276839509783cd431580b48b38f4d633 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ImageInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ImageInfo.kt
@@ -40,7 +40,7 @@ data class ImageInfo(
         /**
          * Size of the image in bytes.
          */
-        @Json(name = "size") val size: Int = 0,
+        @Json(name = "size") val size: Long = 0,
 
         /**
          * Metadata about the image referred to in thumbnail_url.
@@ -57,3 +57,10 @@ data class ImageInfo(
          */
         @Json(name = "thumbnail_file") val thumbnailFile: EncryptedFileInfo? = null
 )
+
+/**
+ * Get the url of the encrypted thumbnail or of the thumbnail
+ */
+fun ImageInfo.getThumbnailUrl(): String? {
+        return thumbnailFile?.url ?: thumbnailUrl
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt
index a6908dce5b12a3dab1fa56c128ceb12e6f9ef4ec..a76c3c5b64d4d48f2570747cb28372b8462fcd42 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt
@@ -37,3 +37,10 @@ data class LocationInfo(
          */
         @Json(name = "thumbnail_file") val thumbnailFile: EncryptedFileInfo? = null
 )
+
+/**
+ * Get the url of the encrypted thumbnail or of the thumbnail
+ */
+fun LocationInfo.getThumbnailUrl(): String? {
+        return thumbnailFile?.url ?: thumbnailUrl
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/VideoInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/VideoInfo.kt
index 8379ee933836700a4d00d023d47cf4ab8df7cc42..8a36c26313d1d00ae7a0e2e9b7a4a19ad6e83f3c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/VideoInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/VideoInfo.kt
@@ -62,3 +62,10 @@ data class VideoInfo(
          */
         @Json(name = "thumbnail_file") val thumbnailFile: EncryptedFileInfo? = null
 )
+
+/**
+ * Get the url of the encrypted thumbnail or of the thumbnail
+ */
+fun VideoInfo.getThumbnailUrl(): String? {
+        return thumbnailFile?.url ?: thumbnailUrl
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt
index db70dadef38aca0af96bf92d7c9be164416ad225..888950dc126b118e64debae5c7f6e889a2ba2927 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/peeking/PeekResult.kt
@@ -16,6 +16,8 @@
 
 package org.matrix.android.sdk.api.session.room.peeking
 
+import org.matrix.android.sdk.api.util.MatrixItem
+
 sealed class PeekResult {
     data class Success(
             val roomId: String,
@@ -24,7 +26,9 @@ sealed class PeekResult {
             val topic: String?,
             val avatarUrl: String?,
             val numJoinedMembers: Int?,
-            val viaServers: List<String>
+            val roomType: String?,
+            val viaServers: List<String>,
+            val someMembers: List<MatrixItem.UserItem>?
     ) : PeekResult()
 
     data class PeekingNotAllowed(
@@ -34,4 +38,6 @@ sealed class PeekResult {
     ) : PeekResult()
 
     object UnknownAlias : PeekResult()
+
+    fun isSuccess() = this is Success
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/PowerLevelsHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/PowerLevelsHelper.kt
index 4f1253c6df9d33936267145fef863bb0a36ea9de..99139723a82f15ce874d337f9b667fb653ccc47e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/PowerLevelsHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/powerlevels/PowerLevelsHelper.kt
@@ -18,6 +18,13 @@
 package org.matrix.android.sdk.api.session.room.powerlevels
 
 import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
+import org.matrix.android.sdk.api.session.room.model.banOrDefault
+import org.matrix.android.sdk.api.session.room.model.eventsDefaultOrDefault
+import org.matrix.android.sdk.api.session.room.model.inviteOrDefault
+import org.matrix.android.sdk.api.session.room.model.kickOrDefault
+import org.matrix.android.sdk.api.session.room.model.redactOrDefault
+import org.matrix.android.sdk.api.session.room.model.stateDefaultOrDefault
+import org.matrix.android.sdk.api.session.room.model.usersDefaultOrDefault
 
 /**
  * This class is an helper around PowerLevelsContent.
@@ -31,9 +38,9 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
      * @return the power level
      */
     fun getUserPowerLevelValue(userId: String): Int {
-        return powerLevelsContent.users.getOrElse(userId) {
-            powerLevelsContent.usersDefault
-        }
+        return powerLevelsContent.users
+                ?.get(userId)
+                ?: powerLevelsContent.usersDefaultOrDefault()
     }
 
     /**
@@ -45,7 +52,7 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
     fun getUserRole(userId: String): Role {
         val value = getUserPowerLevelValue(userId)
         // I think we should use powerLevelsContent.usersDefault, but Ganfra told me that it was like that on riot-Web
-        return Role.fromValue(value, powerLevelsContent.eventsDefault)
+        return Role.fromValue(value, powerLevelsContent.eventsDefaultOrDefault())
     }
 
     /**
@@ -59,11 +66,11 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
     fun isUserAllowedToSend(userId: String, isState: Boolean, eventType: String?): Boolean {
         return if (userId.isNotEmpty()) {
             val powerLevel = getUserPowerLevelValue(userId)
-            val minimumPowerLevel = powerLevelsContent.events[eventType]
+            val minimumPowerLevel = powerLevelsContent.events?.get(eventType)
                     ?: if (isState) {
-                        powerLevelsContent.stateDefault
+                        powerLevelsContent.stateDefaultOrDefault()
                     } else {
-                        powerLevelsContent.eventsDefault
+                        powerLevelsContent.eventsDefaultOrDefault()
                     }
             powerLevel >= minimumPowerLevel
         } else false
@@ -76,7 +83,7 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
      */
     fun isUserAbleToInvite(userId: String): Boolean {
         val powerLevel = getUserPowerLevelValue(userId)
-        return powerLevel >= powerLevelsContent.invite
+        return powerLevel >= powerLevelsContent.inviteOrDefault()
     }
 
     /**
@@ -86,7 +93,7 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
      */
     fun isUserAbleToBan(userId: String): Boolean {
         val powerLevel = getUserPowerLevelValue(userId)
-        return powerLevel >= powerLevelsContent.ban
+        return powerLevel >= powerLevelsContent.banOrDefault()
     }
 
     /**
@@ -96,7 +103,7 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
      */
     fun isUserAbleToKick(userId: String): Boolean {
         val powerLevel = getUserPowerLevelValue(userId)
-        return powerLevel >= powerLevelsContent.kick
+        return powerLevel >= powerLevelsContent.kickOrDefault()
     }
 
     /**
@@ -106,6 +113,6 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
      */
     fun isUserAbleToRedact(userId: String): Boolean {
         val powerLevel = getUserPowerLevelValue(userId)
-        return powerLevel >= powerLevelsContent.redact
+        return powerLevel >= powerLevelsContent.redactOrDefault()
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomAggregateNotificationCount.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomAggregateNotificationCount.kt
index 066178b1ecfc5f36ea25dbf6f5b876e7455cf584..b3440059e83af932932b764bd7f4c8cb52f86971 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomAggregateNotificationCount.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/summary/RoomAggregateNotificationCount.kt
@@ -20,6 +20,6 @@ data class RoomAggregateNotificationCount(
         val notificationCount: Int,
         val highlightCount: Int
 ) {
-    val totalCount = notificationCount + highlightCount
+    val totalCount = notificationCount
     val isHighlight = highlightCount > 0
 }
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 32f6b94cd8d7a6c903df4e00739e942a2fc1c31e..4a6462477dc653eb91d1c02487494876642602ef 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
@@ -21,6 +21,7 @@ import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.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.isReply
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
@@ -151,6 +152,10 @@ fun TimelineEvent.isReply(): Boolean {
     return root.isReply()
 }
 
+fun TimelineEvent.isEdition(): Boolean {
+    return root.isEdition()
+}
+
 fun TimelineEvent.getTextEditableContent(): String? {
     val lastContent = getLastMessageContent()
     return if (isReply()) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/CreateSpaceParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/CreateSpaceParams.kt
new file mode 100644
index 0000000000000000000000000000000000000000..42e6584838ce998e4370733e4e64e6d98f031d68
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/CreateSpaceParams.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.space
+
+import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
+import org.matrix.android.sdk.api.session.room.model.RoomType
+import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
+
+class CreateSpaceParams : CreateRoomParams() {
+
+    init {
+        // Space-rooms are distinguished from regular messaging rooms by the m.room.type of m.space
+        roomType = RoomType.SPACE
+
+        // Space-rooms should be created with a power level for events_default of 100,
+        // to prevent the rooms accidentally/maliciously clogging up with messages from random members of the space.
+        powerLevelContentOverride = PowerLevelsContent(
+                eventsDefault = 100
+        )
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/JoinSpaceResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/JoinSpaceResult.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e8c69977c6a59c510a04129e975869f8bffa904f
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/JoinSpaceResult.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.space
+
+sealed class JoinSpaceResult {
+    object Success : JoinSpaceResult()
+    data class Fail(val error: Throwable) : JoinSpaceResult()
+
+    /** Success fully joined the space, but failed to join all or some of it's rooms */
+    data class PartialSuccess(val failedRooms: Map<String, Throwable>) : JoinSpaceResult()
+
+    fun isSuccess() = this is Success || this is PartialSuccess
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/Space.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/Space.kt
new file mode 100644
index 0000000000000000000000000000000000000000..db25762c2f2b7436e71a9f25754e1bc1b7b1d22e
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/Space.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.space
+
+import org.matrix.android.sdk.api.session.room.Room
+import org.matrix.android.sdk.api.session.room.model.RoomSummary
+
+interface Space {
+
+    fun asRoom(): Room
+
+    val spaceId: String
+
+    suspend fun leave(reason: String? = null)
+
+    /**
+     * A current snapshot of [RoomSummary] associated with the space
+     */
+    fun spaceSummary(): RoomSummary?
+
+    suspend fun addChildren(roomId: String,
+                            viaServers: List<String>?,
+                            order: String?,
+                            autoJoin: Boolean = false,
+                            suggested: Boolean? = false)
+
+    suspend fun removeChildren(roomId: String)
+
+    @Throws
+    suspend fun setChildrenOrder(roomId: String, order: String?)
+
+    @Throws
+    suspend fun setChildrenAutoJoin(roomId: String, autoJoin: Boolean)
+
+    @Throws
+    suspend fun setChildrenSuggested(roomId: String, suggested: Boolean)
+
+//    fun getChildren() : List<IRoomSummary>
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..fedf38fe065053b30f22f9e0487889aa9595813b
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt
@@ -0,0 +1,87 @@
+/*
+ * 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.space
+
+import android.net.Uri
+import androidx.lifecycle.LiveData
+import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
+import org.matrix.android.sdk.api.session.room.model.RoomSummary
+import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
+import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult
+
+typealias SpaceSummaryQueryParams = RoomSummaryQueryParams
+
+interface SpaceService {
+
+    /**
+     * Create a space asynchronously
+     * @return the spaceId of the created space
+     */
+    suspend fun createSpace(params: CreateSpaceParams): String
+
+    /**
+     * Just a shortcut for space creation for ease of use
+     */
+    suspend fun createSpace(name: String, topic: String?, avatarUri: Uri?, isPublic: Boolean): String
+
+    /**
+     * Get a space from a roomId
+     * @param spaceId the roomId to look for.
+     * @return a space with spaceId or null if room type is not space
+     */
+    fun getSpace(spaceId: String): Space?
+
+    /**
+     * Try to resolve (peek) rooms and subspace in this space.
+     * Use this call get preview of children of this space, particularly useful to get a
+     * preview of rooms that you did not join yet.
+     */
+    suspend fun peekSpace(spaceId: String): SpacePeekResult
+
+    /**
+     * Get's information of a space by querying the server
+     */
+    suspend fun querySpaceChildren(spaceId: String,
+                                   suggestedOnly: Boolean? = null,
+                                   autoJoinedOnly: Boolean? = null): Pair<RoomSummary, List<SpaceChildInfo>>
+
+    /**
+     * Get a live list of space summaries. This list is refreshed as soon as the data changes.
+     * @return the [LiveData] of List[SpaceSummary]
+     */
+    fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams): LiveData<List<RoomSummary>>
+
+    fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List<RoomSummary>
+
+    suspend fun joinSpace(spaceIdOrAlias: String,
+                          reason: String? = null,
+                          viaServers: List<String> = emptyList()): JoinSpaceResult
+
+    suspend fun rejectInvite(spaceId: String, reason: String?)
+
+//    fun getSpaceParentsOfRoom(roomId: String) : List<SpaceSummary>
+
+    /**
+     * Let this room declare that it has a parent.
+     * @param canonical true if it should be the main parent of this room
+     * In practice, well behaved rooms should only have one canonical parent, but given this is not enforced:
+     * if multiple are present the client should select the one with the lowest room ID, as determined via a lexicographic utf-8 ordering.
+     */
+    suspend fun setSpaceParent(childRoomId: String, parentSpaceId: String, canonical: Boolean, viaServers: List<String>)
+
+    fun getRootSpaceSummaries(): List<RoomSummary>
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceChildContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceChildContent.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0c33cfa1e60949aeea120f94287bf021c14278d1
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceChildContent.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.space.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+/**
+ *  "content": {
+ *      "via": ["example.com"],
+ *      "order": "abcd",
+ *      "default": true
+ *  }
+ */
+@JsonClass(generateAdapter = true)
+data class SpaceChildContent(
+        /**
+         * Key which gives a list of candidate servers that can be used to join the room
+         * Children where via is not present are ignored.
+         */
+        @Json(name = "via") val via: List<String>? = null,
+        /**
+         * The order key is a string which is used to provide a default ordering of siblings in the room list.
+         * (Rooms are sorted based on a lexicographic ordering of order values; rooms with no order come last.
+         * orders which are not strings, or do not consist solely of ascii characters in the range \x20 (space) to \x7F (~),
+         * or consist of more than 50 characters, are forbidden and should be ignored if received.)
+         */
+        @Json(name = "order") val order: String? = null,
+        /**
+         * The auto_join flag on a child listing allows a space admin to list the sub-spaces and rooms in that space which should
+         * be automatically joined by members of that space.
+         * (This is not a force-join, which are descoped for a future MSC; the user can subsequently part these room if they desire.)
+         */
+        @Json(name = "auto_join") val autoJoin: Boolean? = false,
+
+        /**
+         * If `suggested` is set to `true`, that indicates that the child should be advertised to
+         * members of the space by the client. This could be done by showing them eagerly
+         * in the room list. This is should be ignored if `auto_join` is set to `true`.
+         */
+        @Json(name = "suggested") val suggested: Boolean? = false
+) {
+    /**
+     * Orders which are not strings, or do not consist solely of ascii characters in the range \x20 (space) to \x7F (~),
+     * or consist of more than 50 characters, are forbidden and should be ignored if received.)
+     */
+    fun validOrder(): String? {
+        return order
+                ?.takeIf { it.length <= 50 }
+                ?.takeIf { ORDER_VALID_CHAR_REGEX.matches(it) }
+    }
+
+    companion object {
+        private val ORDER_VALID_CHAR_REGEX = "[ -~]+".toRegex()
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceParentContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceParentContent.kt
new file mode 100644
index 0000000000000000000000000000000000000000..871a4949147b590d4a8d598123b7f19f8992975b
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/model/SpaceParentContent.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.space.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+/**
+ *  Rooms can claim parents via the m.space.parent state event.
+ * {
+ *  "type": "m.space.parent",
+ *  "state_key": "!space:example.com",
+ *  "content": {
+ *      "via": ["example.com"],
+ *      "canonical": true,
+ *  }
+ * }
+ */
+@JsonClass(generateAdapter = true)
+data class SpaceParentContent(
+        /**
+         * Key which gives a list of candidate servers that can be used to join the parent.
+         * Parents where via is not present are ignored.
+         */
+        @Json(name = "via") val via: List<String>? = null,
+        /**
+         * Canonical determines whether this is the main parent for the space.
+         * When a user joins a room with a canonical parent, clients may switch to view the room
+         * in the context of that space, peeking into it in order to find other rooms and group them together.
+         * In practice, well behaved rooms should only have one canonical parent, but given this is not enforced:
+         * if multiple are present the client should select the one with the lowest room ID, as determined via a lexicographic utf-8 ordering.
+         */
+        @Json(name = "canonical") val canonical: Boolean? = false
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetContent.kt
index 2c4c03b7d45d39b0623179d32708da77eb76b823..7a4231c277ebffa7cdb7c0006076a9427849fb85 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetContent.kt
@@ -20,6 +20,7 @@ import android.annotation.SuppressLint
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
 import org.matrix.android.sdk.api.util.JsonDict
+import org.matrix.android.sdk.internal.util.safeCapitalize
 
 /**
  * Ref: https://github.com/matrix-org/matrix-doc/issues/1236
@@ -39,6 +40,6 @@ data class WidgetContent(
 
     @SuppressLint("DefaultLocale")
     fun getHumanName(): String {
-        return (name ?: type ?: "").capitalize()
+        return (name ?: type ?: "").safeCapitalize()
     }
 }
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 db229a6453e53bc35272c911ab0285c7a872ecdd..deb279eb95f063acf97575441ea68a2b122b10ae 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
@@ -20,6 +20,7 @@ import org.matrix.android.sdk.BuildConfig
 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.SpaceChildInfo
 import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoom
 import org.matrix.android.sdk.api.session.room.sender.SenderInfo
 import org.matrix.android.sdk.api.session.user.model.User
@@ -116,22 +117,22 @@ sealed class MatrixItem(
                     var first = dn[startIndex]
 
                     // LEFT-TO-RIGHT MARK
-                    if (dn.length >= 2 && 0x200e == first.toInt()) {
+                    if (dn.length >= 2 && 0x200e == first.code) {
                         startIndex++
                         first = dn[startIndex]
                     }
 
                     // check if it’s the start of a surrogate pair
-                    if (first.toInt() in 0xD800..0xDBFF && dn.length > startIndex + 1) {
+                    if (first.code in 0xD800..0xDBFF && dn.length > startIndex + 1) {
                         val second = dn[startIndex + 1]
-                        if (second.toInt() in 0xDC00..0xDFFF) {
+                        if (second.code in 0xDC00..0xDFFF) {
                             length++
                         }
                     }
 
                     dn.substring(startIndex, startIndex + length)
                 }
-                .toUpperCase(Locale.ROOT)
+                .uppercase(Locale.ROOT)
     }
 
     companion object {
@@ -157,3 +158,5 @@ fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name ?: getPrimaryAl
 fun RoomMemberSummary.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)
 
 fun SenderInfo.toMatrixItem() = MatrixItem.UserItem(userId, disambiguatedDisplayName, avatarUrl)
+
+fun SpaceChildInfo.toMatrixItem() = MatrixItem.RoomItem(childRoomId, name ?: canonicalAlias ?: "", avatarUrl)
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 c74999b4abbca2310c0ab41976a4d7ede124f22e..182b37f2ad018e609a1d480fabb3659c427c59de 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
@@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.extensions.orFalse
 object MimeTypes {
     const val Any: String = "*/*"
     const val OctetStream = "application/octet-stream"
+    const val Apk = "application/vnd.android.package-archive"
 
     const val Images = "image/*"
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthAPI.kt
index f93f285c6eaca6e4a730bcb69d8a52aba30fb444..5a9fa9edf65263338e8ba47845099cba1914d3bd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthAPI.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthAPI.kt
@@ -17,6 +17,7 @@
 package org.matrix.android.sdk.internal.auth
 
 import org.matrix.android.sdk.api.auth.data.Credentials
+import org.matrix.android.sdk.api.util.JsonDict
 import org.matrix.android.sdk.internal.auth.data.Availability
 import org.matrix.android.sdk.internal.auth.data.LoginFlowResponse
 import org.matrix.android.sdk.internal.auth.data.PasswordLoginParams
@@ -73,6 +74,15 @@ internal interface AuthAPI {
     @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register/available")
     suspend fun registerAvailable(@Query("username") username: String): Availability
 
+    /**
+     * Get the combined profile information for this user.
+     * This API may be used to fetch the user's own profile information or other users; either locally or on remote homeservers.
+     * This API may return keys which are not limited to displayname or avatar_url.
+     * @param userId the user id to fetch profile info
+     */
+    @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "profile/{userId}")
+    suspend fun getProfile(@Path("userId") userId: String): JsonDict
+
     /**
      * Add 3Pid during registration
      * Ref: https://gist.github.com/jryans/839a09bf0c5a70e2f36ed990d50ed928
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 e26286ad2f5b64bca88b469a69ddd4e6cd9a4196..46256f4b81fc05cb4f5735c2341cf0be24702a93 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
@@ -144,16 +144,14 @@ internal class DefaultAuthenticationService @Inject constructor(
         }
         return result.fold(
                 {
-                    if (it is LoginFlowResult.Success) {
-                        // The homeserver exists and up to date, keep the config
-                        // Homeserver url may have been changed, if it was a Riot url
-                        val alteredHomeServerConnectionConfig = homeServerConnectionConfig.copy(
-                                homeServerUri = Uri.parse(it.homeServerUrl)
-                        )
-
-                        pendingSessionData = PendingSessionData(alteredHomeServerConnectionConfig)
-                                .also { data -> pendingSessionStore.savePendingSessionData(data) }
-                    }
+                    // The homeserver exists and up to date, keep the config
+                    // Homeserver url may have been changed, if it was a Riot url
+                    val alteredHomeServerConnectionConfig = homeServerConnectionConfig.copy(
+                            homeServerUri = Uri.parse(it.homeServerUrl)
+                    )
+
+                    pendingSessionData = PendingSessionData(alteredHomeServerConnectionConfig)
+                            .also { data -> pendingSessionStore.savePendingSessionData(data) }
                     it
                 },
                 {
@@ -307,12 +305,12 @@ internal class DefaultAuthenticationService @Inject constructor(
         val loginFlowResponse = executeRequest(null) {
             authAPI.getLoginFlows()
         }
-        return LoginFlowResult.Success(
-                loginFlowResponse.flows.orEmpty().mapNotNull { it.type },
-                loginFlowResponse.flows.orEmpty().firstOrNull { it.type == LoginFlowTypes.SSO }?.ssoIdentityProvider,
-                versions.isLoginAndRegistrationSupportedBySdk(),
-                homeServerUrl,
-                !versions.isSupportedBySdk()
+        return LoginFlowResult(
+                supportedLoginTypes = loginFlowResponse.flows.orEmpty().mapNotNull { it.type },
+                ssoIdentityProviders = loginFlowResponse.flows.orEmpty().firstOrNull { it.type == LoginFlowTypes.SSO }?.ssoIdentityProvider,
+                isLoginAndRegistrationSupported = versions.isLoginAndRegistrationSupportedBySdk(),
+                homeServerUrl = homeServerUrl,
+                isOutdatedHomeserver = !versions.isSupportedBySdk()
         )
     }
 
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 8b81f42e03873f47c2108c90039141fe5c90f1fb..854caf8a622a90adf17c907dc4e36d658423da06 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.login.LoginProfileInfo
 import org.matrix.android.sdk.api.auth.login.LoginWizard
 import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
 import org.matrix.android.sdk.api.session.Session
@@ -30,6 +31,7 @@ import org.matrix.android.sdk.internal.auth.db.PendingSessionData
 import org.matrix.android.sdk.internal.auth.registration.AddThreePidRegistrationParams
 import org.matrix.android.sdk.internal.auth.registration.RegisterAddThreePidTask
 import org.matrix.android.sdk.internal.network.executeRequest
+import org.matrix.android.sdk.internal.session.content.DefaultContentUrlResolver
 
 internal class DefaultLoginWizard(
         private val authAPI: AuthAPI,
@@ -39,6 +41,15 @@ internal class DefaultLoginWizard(
 
     private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here")
 
+    private val getProfileTask: GetProfileTask = DefaultGetProfileTask(
+            authAPI,
+            DefaultContentUrlResolver(pendingSessionData.homeServerConnectionConfig)
+    )
+
+    override suspend fun getProfileInfo(matrixId: String): LoginProfileInfo {
+        return getProfileTask.execute(GetProfileTask.Params(matrixId))
+    }
+
     override suspend fun login(login: String,
                                password: String,
                                deviceName: String): Session {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/GetProfileTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/GetProfileTask.kt
new file mode 100644
index 0000000000000000000000000000000000000000..bb9faf49c4a73b83aea6daf25033ceb23d41b63d
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/GetProfileTask.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.auth.login
+
+import org.matrix.android.sdk.api.auth.login.LoginProfileInfo
+import org.matrix.android.sdk.api.session.content.ContentUrlResolver
+import org.matrix.android.sdk.api.session.profile.ProfileService
+import org.matrix.android.sdk.internal.auth.AuthAPI
+import org.matrix.android.sdk.internal.network.executeRequest
+import org.matrix.android.sdk.internal.task.Task
+
+internal interface GetProfileTask : Task<GetProfileTask.Params, LoginProfileInfo> {
+    data class Params(
+            val userId: String
+    )
+}
+
+internal class DefaultGetProfileTask(
+        private val authAPI: AuthAPI,
+        private val contentUrlResolver: ContentUrlResolver
+) : GetProfileTask {
+
+    override suspend fun execute(params: GetProfileTask.Params): LoginProfileInfo {
+        val info = executeRequest(null) {
+            authAPI.getProfile(params.userId)
+        }
+
+        return LoginProfileInfo(
+                matrixId = params.userId,
+                displayName = info[ProfileService.DISPLAY_NAME_KEY] as? String,
+                fullAvatarUrl = contentUrlResolver.resolveFullSize(info[ProfileService.AVATAR_URL_KEY] as? String)
+        )
+    }
+}
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 4a3d53a8fc9328b80da39bdc83ee6a2781fcd5b9..4a156e74cd994743205e42240a5d6f887c065b53 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
@@ -66,8 +66,8 @@ internal class DefaultRegistrationWizard(
         return performRegistrationRequest(params)
     }
 
-    override suspend fun createAccount(userName: String,
-                                       password: String,
+    override suspend fun createAccount(userName: String?,
+                                       password: String?,
                                        initialDeviceDisplayName: String?): RegistrationResult {
         val params = RegistrationParams(
                 username = userName,
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 2163b2a5e0276046d414bb1e4da8ff4d5d112284..7f5cfe8df103cfa036eacae1bd760855dd2006eb 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
@@ -545,14 +545,14 @@ internal class DefaultCryptoService @Inject constructor(
         val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId)
 
         if (!existingAlgorithm.isNullOrEmpty() && existingAlgorithm != algorithm) {
-            Timber.e("## CRYPTO | setEncryptionInRoom() : Ignoring m.room.encryption event which requests a change of config in $roomId")
+            Timber.e("## CRYPTO | setEncryptionInRoom() : Ignoring m.room.encryption event which requests a change of config in $roomId")
             return false
         }
 
         val encryptingClass = MXCryptoAlgorithms.hasEncryptorClassForAlgorithm(algorithm)
 
         if (!encryptingClass) {
-            Timber.e("## CRYPTO | setEncryptionInRoom() : Unable to encrypt room $roomId with $algorithm")
+            Timber.e("## CRYPTO | setEncryptionInRoom() : Unable to encrypt room $roomId with $algorithm")
             return false
         }
 
@@ -649,17 +649,17 @@ internal class DefaultCryptoService @Inject constructor(
             val safeAlgorithm = alg
             if (safeAlgorithm != null) {
                 val t0 = System.currentTimeMillis()
-                Timber.v("## CRYPTO | encryptEventContent() starts")
+                Timber.v("## CRYPTO | encryptEventContent() starts")
                 runCatching {
                     val content = safeAlgorithm.encryptEventContent(eventContent, eventType, userIds)
-                    Timber.v("## CRYPTO | encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms")
+                    Timber.v("## CRYPTO | encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms")
                     MXEncryptEventContentResult(content, EventType.ENCRYPTED)
                 }.foldToCallback(callback)
             } else {
                 val algorithm = getEncryptionAlgorithm(roomId)
                 val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON,
                         algorithm ?: MXCryptoError.NO_MORE_ALGORITHM_REASON)
-                Timber.e("## CRYPTO | encryptEventContent() : $reason")
+                Timber.e("## CRYPTO | encryptEventContent() : $reason")
                 callback.onFailure(Failure.CryptoError(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_ENCRYPT, reason)))
             }
         }
@@ -769,7 +769,7 @@ internal class DefaultCryptoService @Inject constructor(
         }
         val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(roomKeyContent.roomId, roomKeyContent.algorithm)
         if (alg == null) {
-            Timber.e("## CRYPTO | GOSSIP onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}")
+            Timber.e("## CRYPTO | GOSSIP onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}")
             return
         }
         alg.onRoomKeyEvent(event, keysBackupService)
@@ -777,7 +777,7 @@ internal class DefaultCryptoService @Inject constructor(
 
     private fun onKeyWithHeldReceived(event: Event) {
         val withHeldContent = event.getClearContent().toModel<RoomKeyWithHeldContent>() ?: return Unit.also {
-            Timber.i("## CRYPTO | Malformed onKeyWithHeldReceived() : missing fields")
+            Timber.i("## CRYPTO | Malformed onKeyWithHeldReceived() : missing fields")
         }
         Timber.i("## CRYPTO | onKeyWithHeldReceived() received from:${event.senderId}, content <$withHeldContent>")
         val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(withHeldContent.roomId, withHeldContent.algorithm)
@@ -790,16 +790,16 @@ internal class DefaultCryptoService @Inject constructor(
     }
 
     private fun onSecretSendReceived(event: Event) {
-        Timber.i("## CRYPTO | GOSSIP onSecretSend() from ${event.senderId} : onSecretSendReceived ${event.content?.get("sender_key")}")
+        Timber.i("## CRYPTO | GOSSIP onSecretSend() from ${event.senderId} : onSecretSendReceived ${event.content?.get("sender_key")}")
         if (!event.isEncrypted()) {
             // secret send messages must be encrypted
-            Timber.e("## CRYPTO | GOSSIP onSecretSend() :Received unencrypted secret send event")
+            Timber.e("## CRYPTO | GOSSIP onSecretSend() :Received unencrypted secret send event")
             return
         }
 
         // Was that sent by us?
         if (event.senderId != userId) {
-            Timber.e("## CRYPTO | GOSSIP onSecretSend() : Ignore secret from other user ${event.senderId}")
+            Timber.e("## CRYPTO | GOSSIP onSecretSend() : Ignore secret from other user ${event.senderId}")
             return
         }
 
@@ -809,13 +809,13 @@ internal class DefaultCryptoService @Inject constructor(
                 .getOutgoingSecretKeyRequests().firstOrNull { it.requestId == secretContent.requestId }
 
         if (existingRequest == null) {
-            Timber.i("## CRYPTO | GOSSIP onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}")
+            Timber.i("## CRYPTO | GOSSIP onSecretSend() : Ignore secret that was not requested: ${secretContent.requestId}")
             return
         }
 
         if (!handleSDKLevelGossip(existingRequest.secretName, secretContent.secretValue)) {
             // TODO Ask to application layer?
-            Timber.v("## CRYPTO | onSecretSend() : secret not handled by SDK")
+            Timber.v("## CRYPTO | onSecretSend() : secret not handled by SDK")
         }
     }
 
@@ -972,13 +972,13 @@ internal class DefaultCryptoService @Inject constructor(
         cryptoCoroutineScope.launch(coroutineDispatchers.main) {
             runCatching {
                 withContext(coroutineDispatchers.crypto) {
-                    Timber.v("## CRYPTO | importRoomKeys starts")
+                    Timber.v("## CRYPTO | importRoomKeys starts")
 
                     val t0 = System.currentTimeMillis()
                     val roomKeys = MXMegolmExportEncryption.decryptMegolmKeyFile(roomKeysAsArray, password)
                     val t1 = System.currentTimeMillis()
 
-                    Timber.v("## CRYPTO | importRoomKeys : decryptMegolmKeyFile done in ${t1 - t0} ms")
+                    Timber.v("## CRYPTO | importRoomKeys : decryptMegolmKeyFile done in ${t1 - t0} ms")
 
                     val importedSessions = MoshiProvider.providesMoshi()
                             .adapter<List<MegolmSessionData>>(Types.newParameterizedType(List::class.java, MegolmSessionData::class.java))
@@ -986,7 +986,7 @@ internal class DefaultCryptoService @Inject constructor(
 
                     val t2 = System.currentTimeMillis()
 
-                    Timber.v("## CRYPTO | importRoomKeys : JSON parsing ${t2 - t1} ms")
+                    Timber.v("## CRYPTO | importRoomKeys : JSON parsing ${t2 - t1} ms")
 
                     if (importedSessions == null) {
                         throw Exception("Error")
@@ -1125,7 +1125,7 @@ internal class DefaultCryptoService @Inject constructor(
      */
     override fun reRequestRoomKeyForEvent(event: Event) {
         val wireContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also {
-            Timber.e("## CRYPTO | reRequestRoomKeyForEvent Failed to re-request key, null content")
+            Timber.e("## CRYPTO | reRequestRoomKeyForEvent Failed to re-request key, null content")
         }
 
         val requestBody = RoomKeyRequestBody(
@@ -1140,18 +1140,18 @@ internal class DefaultCryptoService @Inject constructor(
 
     override fun requestRoomKeyForEvent(event: Event) {
         val wireContent = event.content.toModel<EncryptedEventContent>() ?: return Unit.also {
-            Timber.e("## CRYPTO | requestRoomKeyForEvent Failed to request key, null content eventId: ${event.eventId}")
+            Timber.e("## CRYPTO | requestRoomKeyForEvent Failed to request key, null content eventId: ${event.eventId}")
         }
 
         cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
 //            if (!isStarted()) {
-//                Timber.v("## CRYPTO | requestRoomKeyForEvent() : wait after e2e init")
+//                Timber.v("## CRYPTO | requestRoomKeyForEvent() : wait after e2e init")
 //                internalStart(false)
 //            }
             roomDecryptorProvider
                     .getOrCreateRoomDecryptor(event.roomId, wireContent.algorithm)
                     ?.requestKeysForEvent(event, false) ?: run {
-                Timber.v("## CRYPTO | requestRoomKeyForEvent() : No room decryptor for roomId:${event.roomId} algorithm:${wireContent.algorithm}")
+                Timber.v("## CRYPTO | requestRoomKeyForEvent() : No room decryptor for roomId:${event.roomId} algorithm:${wireContent.algorithm}")
             }
         }
     }
@@ -1180,11 +1180,11 @@ internal class DefaultCryptoService @Inject constructor(
 //        val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0
 //        val now = System.currentTimeMillis()
 //        if (now - lastForcedDate < CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
-//            Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another")
+//            Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another")
 //            return
 //        }
 //
-//        Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}")
+//        Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}")
 //        lastNewSessionForcedDates.setObject(senderId, deviceKey, now)
 //
 //        cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
@@ -1201,7 +1201,7 @@ internal class DefaultCryptoService @Inject constructor(
 //            val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
 //            val sendToDeviceMap = MXUsersDevicesMap<Any>()
 //            sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload)
-//            Timber.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}")
+//            Timber.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}")
 //            val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
 //            sendToDeviceTask.execute(sendToDeviceParams)
 //        }
@@ -1290,12 +1290,12 @@ internal class DefaultCryptoService @Inject constructor(
 
     override fun prepareToEncrypt(roomId: String, callback: MatrixCallback<Unit>) {
         cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
-            Timber.d("## CRYPTO | prepareToEncrypt() : Check room members up to date")
+            Timber.d("## CRYPTO | prepareToEncrypt() : Check room members up to date")
             // Ensure to load all room members
             try {
                 loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId))
             } catch (failure: Throwable) {
-                Timber.e("## CRYPTO | prepareToEncrypt() : Failed to load room members")
+                Timber.e("## CRYPTO | prepareToEncrypt() : Failed to load room members")
                 callback.onFailure(failure)
                 return@launch
             }
@@ -1308,7 +1308,7 @@ internal class DefaultCryptoService @Inject constructor(
 
             if (alg == null) {
                 val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON, MXCryptoError.NO_MORE_ALGORITHM_REASON)
-                Timber.e("## CRYPTO | prepareToEncrypt() : $reason")
+                Timber.e("## CRYPTO | prepareToEncrypt() : $reason")
                 callback.onFailure(IllegalArgumentException("Missing algorithm"))
                 return@launch
             }
@@ -1318,7 +1318,7 @@ internal class DefaultCryptoService @Inject constructor(
             }.fold(
                     { callback.onSuccess(Unit) },
                     {
-                        Timber.e("## CRYPTO | prepareToEncrypt() failed.")
+                        Timber.e("## CRYPTO | prepareToEncrypt() failed.")
                         callback.onFailure(it)
                     }
             )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt
index e5f1c011f8c2c349ca9d4bac26eb67c8c7beb9b8..63f15aaf6e6ca0ce3555b566746f282191194696 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt
@@ -111,7 +111,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
                     res = !notReadyToRetryHS.contains(userId.substringAfter(':'))
                 }
             } catch (e: Exception) {
-                Timber.e(e, "## CRYPTO | canRetryKeysDownload() failed")
+                Timber.e(e, "## CRYPTO | canRetryKeysDownload() failed")
             }
         }
 
@@ -150,7 +150,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
 
         for (userId in userIds) {
             if (!deviceTrackingStatuses.containsKey(userId) || TRACKING_STATUS_NOT_TRACKED == deviceTrackingStatuses[userId]) {
-                Timber.v("## CRYPTO | startTrackingDeviceList() : Now tracking device list for $userId")
+                Timber.v("## CRYPTO | startTrackingDeviceList() : Now tracking device list for $userId")
                 deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
                 isUpdated = true
             }
@@ -178,7 +178,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
 
         for (userId in changed) {
             if (deviceTrackingStatuses.containsKey(userId)) {
-                Timber.v("## CRYPTO | handleDeviceListsChanges() : Marking device list outdated for $userId")
+                Timber.v("## CRYPTO | handleDeviceListsChanges() : Marking device list outdated for $userId")
                 deviceTrackingStatuses[userId] = TRACKING_STATUS_PENDING_DOWNLOAD
                 isUpdated = true
             }
@@ -186,7 +186,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
 
         for (userId in left) {
             if (deviceTrackingStatuses.containsKey(userId)) {
-                Timber.v("## CRYPTO | handleDeviceListsChanges() : No longer tracking device list for $userId")
+                Timber.v("## CRYPTO | handleDeviceListsChanges() : No longer tracking device list for $userId")
                 deviceTrackingStatuses[userId] = TRACKING_STATUS_NOT_TRACKED
                 isUpdated = true
             }
@@ -276,7 +276,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
      * @param forceDownload Always download the keys even if cached.
      */
     suspend fun downloadKeys(userIds: List<String>?, forceDownload: Boolean): MXUsersDevicesMap<CryptoDeviceInfo> {
-        Timber.v("## CRYPTO | downloadKeys() : forceDownload $forceDownload : $userIds")
+        Timber.v("## CRYPTO | downloadKeys() : forceDownload $forceDownload : $userIds")
         // Map from userId -> deviceId -> MXDeviceInfo
         val stored = MXUsersDevicesMap<CryptoDeviceInfo>()
 
@@ -305,13 +305,13 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
             }
         }
         return if (downloadUsers.isEmpty()) {
-            Timber.v("## CRYPTO | downloadKeys() : no new user device")
+            Timber.v("## CRYPTO | downloadKeys() : no new user device")
             stored
         } else {
-            Timber.v("## CRYPTO | downloadKeys() : starts")
+            Timber.v("## CRYPTO | downloadKeys() : starts")
             val t0 = System.currentTimeMillis()
             val result = doKeyDownloadForUsers(downloadUsers)
-            Timber.v("## CRYPTO | downloadKeys() : doKeyDownloadForUsers succeeds after ${System.currentTimeMillis() - t0} ms")
+            Timber.v("## CRYPTO | downloadKeys() : doKeyDownloadForUsers succeeds after ${System.currentTimeMillis() - t0} ms")
             result.also {
                 it.addEntriesFromMap(stored)
             }
@@ -324,7 +324,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
      * @param downloadUsers the user ids list
      */
     private suspend fun doKeyDownloadForUsers(downloadUsers: List<String>): MXUsersDevicesMap<CryptoDeviceInfo> {
-        Timber.v("## CRYPTO | doKeyDownloadForUsers() : doKeyDownloadForUsers ${downloadUsers.logLimit()}")
+        Timber.v("## CRYPTO | doKeyDownloadForUsers() : doKeyDownloadForUsers ${downloadUsers.logLimit()}")
         // get the user ids which did not already trigger a keys download
         val filteredUsers = downloadUsers.filter { MatrixPatterns.isUserId(it) }
         if (filteredUsers.isEmpty()) {
@@ -335,16 +335,16 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
         val response = try {
             downloadKeysForUsersTask.execute(params)
         } catch (throwable: Throwable) {
-            Timber.e(throwable, "## CRYPTO | doKeyDownloadForUsers(): error")
+            Timber.e(throwable, "## CRYPTO | doKeyDownloadForUsers(): error")
             onKeysDownloadFailed(filteredUsers)
             throw throwable
         }
-        Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users")
+        Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users")
         for (userId in filteredUsers) {
             // al devices =
             val models = response.deviceKeys?.get(userId)?.mapValues { entry -> CryptoInfoMapper.map(entry.value) }
 
-            Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for $userId : $models")
+            Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for $userId : $models")
             if (!models.isNullOrEmpty()) {
                 val workingCopy = models.toMutableMap()
                 for ((deviceId, deviceInfo) in models) {
@@ -377,13 +377,13 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
             }
 
             val masterKey = response.masterKeys?.get(userId)?.toCryptoModel().also {
-                Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : MSK ${it?.unpaddedBase64PublicKey}")
+                Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : MSK ${it?.unpaddedBase64PublicKey}")
             }
             val selfSigningKey = response.selfSigningKeys?.get(userId)?.toCryptoModel()?.also {
-                Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : SSK ${it.unpaddedBase64PublicKey}")
+                Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : SSK ${it.unpaddedBase64PublicKey}")
             }
             val userSigningKey = response.userSigningKeys?.get(userId)?.toCryptoModel()?.also {
-                Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}")
+                Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}")
             }
             cryptoStore.storeUserCrossSigningKeys(
                     userId,
@@ -411,28 +411,28 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
      */
     private fun validateDeviceKeys(deviceKeys: CryptoDeviceInfo?, userId: String, deviceId: String, previouslyStoredDeviceKeys: CryptoDeviceInfo?): Boolean {
         if (null == deviceKeys) {
-            Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys is null from $userId:$deviceId")
+            Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys is null from $userId:$deviceId")
             return false
         }
 
         if (null == deviceKeys.keys) {
-            Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys.keys is null from $userId:$deviceId")
+            Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys.keys is null from $userId:$deviceId")
             return false
         }
 
         if (null == deviceKeys.signatures) {
-            Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys.signatures is null from $userId:$deviceId")
+            Timber.e("## CRYPTO | validateDeviceKeys() : deviceKeys.signatures is null from $userId:$deviceId")
             return false
         }
 
         // Check that the user_id and device_id in the received deviceKeys are correct
         if (deviceKeys.userId != userId) {
-            Timber.e("## CRYPTO | validateDeviceKeys() : Mismatched user_id ${deviceKeys.userId} from $userId:$deviceId")
+            Timber.e("## CRYPTO | validateDeviceKeys() : Mismatched user_id ${deviceKeys.userId} from $userId:$deviceId")
             return false
         }
 
         if (deviceKeys.deviceId != deviceId) {
-            Timber.e("## CRYPTO | validateDeviceKeys() : Mismatched device_id ${deviceKeys.deviceId} from $userId:$deviceId")
+            Timber.e("## CRYPTO | validateDeviceKeys() : Mismatched device_id ${deviceKeys.deviceId} from $userId:$deviceId")
             return false
         }
 
@@ -440,21 +440,21 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
         val signKey = deviceKeys.keys[signKeyId]
 
         if (null == signKey) {
-            Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no ed25519 key")
+            Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no ed25519 key")
             return false
         }
 
         val signatureMap = deviceKeys.signatures[userId]
 
         if (null == signatureMap) {
-            Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no map for $userId")
+            Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} has no map for $userId")
             return false
         }
 
         val signature = signatureMap[signKeyId]
 
         if (null == signature) {
-            Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} is not signed")
+            Timber.e("## CRYPTO | validateDeviceKeys() : Device $userId:${deviceKeys.deviceId} is not signed")
             return false
         }
 
@@ -469,7 +469,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
         }
 
         if (!isVerified) {
-            Timber.e("## CRYPTO | validateDeviceKeys() : Unable to verify signature on device " + userId + ":"
+            Timber.e("## CRYPTO | validateDeviceKeys() : Unable to verify signature on device " + userId + ":"
                     + deviceKeys.deviceId + " with error " + errorMessage)
             return false
         }
@@ -480,12 +480,12 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
                 // best off sticking with the original keys.
                 //
                 // Should we warn the user about it somehow?
-                Timber.e("## CRYPTO | validateDeviceKeys() : WARNING:Ed25519 key for device " + userId + ":"
+                Timber.e("## CRYPTO | validateDeviceKeys() : WARNING:Ed25519 key for device " + userId + ":"
                         + deviceKeys.deviceId + " has changed : "
                         + previouslyStoredDeviceKeys.fingerprint() + " -> " + signKey)
 
-                Timber.e("## CRYPTO | validateDeviceKeys() : $previouslyStoredDeviceKeys -> $deviceKeys")
-                Timber.e("## CRYPTO | validateDeviceKeys() : ${previouslyStoredDeviceKeys.keys} -> ${deviceKeys.keys}")
+                Timber.e("## CRYPTO | validateDeviceKeys() : $previouslyStoredDeviceKeys -> $deviceKeys")
+                Timber.e("## CRYPTO | validateDeviceKeys() : ${previouslyStoredDeviceKeys.keys} -> ${deviceKeys.keys}")
 
                 return false
             }
@@ -499,7 +499,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
      * This method must be called on getEncryptingThreadHandler() thread.
      */
     suspend fun refreshOutdatedDeviceLists() {
-        Timber.v("## CRYPTO | refreshOutdatedDeviceLists()")
+        Timber.v("## CRYPTO | refreshOutdatedDeviceLists()")
         val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
 
         val users = deviceTrackingStatuses.keys.filterTo(mutableListOf()) { userId ->
@@ -518,10 +518,10 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
             doKeyDownloadForUsers(users)
         }.fold(
                 {
-                    Timber.v("## CRYPTO | refreshOutdatedDeviceLists() : done")
+                    Timber.v("## CRYPTO | refreshOutdatedDeviceLists() : done")
                 },
                 {
-                    Timber.e(it, "## CRYPTO | refreshOutdatedDeviceLists() : ERROR updating device keys for users $users")
+                    Timber.e(it, "## CRYPTO | refreshOutdatedDeviceLists() : ERROR updating device keys for users $users")
                 }
         )
     }
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 32324896fa3acd7868cb3cfadd83e4003f9e95df..8d86380e39e24a31b3a7206a131c726acd5e562e 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
@@ -92,20 +92,20 @@ internal class EventDecryptor @Inject constructor(
     private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
         val eventContent = event.content
         if (eventContent == null) {
-            Timber.e("## CRYPTO | decryptEvent : empty event content")
+            Timber.e("## CRYPTO | decryptEvent : empty event content")
             throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
         } else {
             val algorithm = eventContent["algorithm"]?.toString()
             val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm)
             if (alg == null) {
                 val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm)
-                Timber.e("## CRYPTO | decryptEvent() : $reason")
+                Timber.e("## CRYPTO | decryptEvent() : $reason")
                 throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason)
             } else {
                 try {
                     return alg.decryptEvent(event, timeline)
                 } catch (mxCryptoError: MXCryptoError) {
-                    Timber.v("## CRYPTO | internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError")
+                    Timber.v("## CRYPTO | internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError")
                     if (algorithm == MXCRYPTO_ALGORITHM_OLM) {
                         if (mxCryptoError is MXCryptoError.Base
                                 && mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) {
@@ -119,7 +119,7 @@ internal class EventDecryptor @Inject constructor(
                                             markOlmSessionForUnwedging(event.senderId ?: "", it)
                                         }
                                         ?: run {
-                                            Timber.i("## CRYPTO | internalDecryptEvent() : Failed to find sender crypto device for unwedging")
+                                            Timber.i("## CRYPTO | internalDecryptEvent() : Failed to find sender crypto device for unwedging")
                                         }
                             }
                         }
@@ -137,18 +137,18 @@ internal class EventDecryptor @Inject constructor(
         val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0
         val now = System.currentTimeMillis()
         if (now - lastForcedDate < DefaultCryptoService.CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
-            Timber.w("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another")
+            Timber.w("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another")
             return
         }
 
-        Timber.i("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}")
+        Timber.i("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}")
         lastNewSessionForcedDates.setObject(senderId, deviceKey, now)
 
         // offload this from crypto thread (?)
         cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
             val ensured = ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true)
 
-            Timber.i("## CRYPTO | markOlmSessionForUnwedging() : ensureOlmSessionsForDevicesAction isEmpty:${ensured.isEmpty}")
+            Timber.i("## CRYPTO | markOlmSessionForUnwedging() : ensureOlmSessionsForDevicesAction isEmpty:${ensured.isEmpty}")
 
             // Now send a blank message on that session so the other side knows about it.
             // (The keyshare request is sent in the clear so that won't do)
@@ -161,13 +161,13 @@ internal class EventDecryptor @Inject constructor(
             val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
             val sendToDeviceMap = MXUsersDevicesMap<Any>()
             sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload)
-            Timber.i("## CRYPTO | markOlmSessionForUnwedging() : sending dummy to $senderId:${deviceInfo.deviceId}")
+            Timber.i("## CRYPTO | markOlmSessionForUnwedging() : sending dummy to $senderId:${deviceInfo.deviceId}")
             withContext(coroutineDispatchers.io) {
                 val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
                 try {
                     sendToDeviceTask.execute(sendToDeviceParams)
                 } catch (failure: Throwable) {
-                    Timber.e(failure, "## CRYPTO | markOlmSessionForUnwedging() : failed to send dummy to $senderId:${deviceInfo.deviceId}")
+                    Timber.e(failure, "## CRYPTO | markOlmSessionForUnwedging() : failed to send dummy to $senderId:${deviceInfo.deviceId}")
                 }
             }
         }
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 787d16defc718ebc2b45c24f99181f8ec870440b..a29ac457fb956178d2be73f9b13c53b112f0fc5e 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
@@ -74,7 +74,7 @@ internal class MXMegolmDecryption(private val userId: String,
 
     @Throws(MXCryptoError::class)
     private fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult {
-        Timber.v("## CRYPTO | decryptEvent ${event.eventId} , requestKeysOnFail:$requestKeysOnFail")
+        Timber.v("## CRYPTO | decryptEvent ${event.eventId}, requestKeysOnFail:$requestKeysOnFail")
         if (event.roomId.isNullOrBlank()) {
             throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
         }
@@ -360,7 +360,7 @@ internal class MXMegolmDecryption(private val userId: String,
                                             },
                                             {
                                                 // TODO
-                                                Timber.e(it, "## CRYPTO | shareKeysWithDevice: failed to get session for request $body")
+                                                Timber.e(it, "## CRYPTO | shareKeysWithDevice: failed to get session for request $body")
                                             }
 
                                     )
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 697711d05161d8440b14edaa9b9dac78c119654e..a7564444759960a157922029b4b68fbe3b89c911 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
@@ -80,9 +80,9 @@ internal class MXMegolmEncryption(
                                              eventType: String,
                                              userIds: List<String>): Content {
         val ts = System.currentTimeMillis()
-        Timber.v("## CRYPTO | encryptEventContent : getDevicesInRoom")
+        Timber.v("## CRYPTO | encryptEventContent : getDevicesInRoom")
         val devices = getDevicesInRoom(userIds)
-        Timber.v("## CRYPTO | encryptEventContent ${System.currentTimeMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.map}")
+        Timber.v("## CRYPTO | encryptEventContent ${System.currentTimeMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.map}")
         val outboundSession = ensureOutboundSession(devices.allowedDevices)
 
         return encryptContent(outboundSession, eventType, eventContent)
@@ -91,7 +91,7 @@ internal class MXMegolmEncryption(
                     // annoyingly we have to serialize again the saved outbound session to store message index :/
                     // if not we would see duplicate message index errors
                     olmDevice.storeOutboundGroupSessionForRoom(roomId, outboundSession.sessionId)
-                    Timber.v("## CRYPTO | encryptEventContent: Finished in ${System.currentTimeMillis() - ts} millis")
+                    Timber.v("## CRYPTO | encryptEventContent: Finished in ${System.currentTimeMillis() - ts} millis")
                 }
     }
 
@@ -118,13 +118,13 @@ internal class MXMegolmEncryption(
 
     override suspend fun preshareKey(userIds: List<String>) {
         val ts = System.currentTimeMillis()
-        Timber.v("## CRYPTO | preshareKey : getDevicesInRoom")
+        Timber.v("## CRYPTO | preshareKey : getDevicesInRoom")
         val devices = getDevicesInRoom(userIds)
         val outboundSession = ensureOutboundSession(devices.allowedDevices)
 
         notifyWithheldForSession(devices.withHeldDevices, outboundSession)
 
-        Timber.v("## CRYPTO | preshareKey ${System.currentTimeMillis() - ts} millis")
+        Timber.v("## CRYPTO | preshareKey ${System.currentTimeMillis() - ts} millis")
     }
 
     /**
@@ -133,7 +133,7 @@ internal class MXMegolmEncryption(
      * @return the session description
      */
     private fun prepareNewSessionInRoom(): MXOutboundSessionInfo {
-        Timber.v("## CRYPTO | prepareNewSessionInRoom() ")
+        Timber.v("## CRYPTO | prepareNewSessionInRoom() ")
         val sessionId = olmDevice.createOutboundGroupSessionForRoom(roomId)
 
         val keysClaimedMap = HashMap<String, String>()
@@ -153,7 +153,7 @@ internal class MXMegolmEncryption(
      * @param devicesInRoom the devices list
      */
     private suspend fun ensureOutboundSession(devicesInRoom: MXUsersDevicesMap<CryptoDeviceInfo>): MXOutboundSessionInfo {
-        Timber.v("## CRYPTO | ensureOutboundSession start")
+        Timber.v("## CRYPTO | ensureOutboundSession start")
         var session = outboundSession
         if (session == null
                 // Need to make a brand new session?
@@ -190,7 +190,7 @@ internal class MXMegolmEncryption(
                                  devicesByUsers: Map<String, List<CryptoDeviceInfo>>) {
         // nothing to send, the task is done
         if (devicesByUsers.isEmpty()) {
-            Timber.v("## CRYPTO | shareKey() : nothing more to do")
+            Timber.v("## CRYPTO | shareKey() : nothing more to do")
             return
         }
         // reduce the map size to avoid request timeout when there are too many devices (Users size  * devices per user)
@@ -203,7 +203,7 @@ internal class MXMegolmEncryption(
                 break
             }
         }
-        Timber.v("## CRYPTO | shareKey() ; sessionId<${session.sessionId}> userId ${subMap.keys}")
+        Timber.v("## CRYPTO | shareKey() ; sessionId<${session.sessionId}> userId ${subMap.keys}")
         shareUserDevicesKey(session, subMap)
         val remainingDevices = devicesByUsers - subMap.keys
         shareKey(session, remainingDevices)
@@ -232,11 +232,11 @@ internal class MXMegolmEncryption(
         payload["content"] = submap
 
         var t0 = System.currentTimeMillis()
-        Timber.v("## CRYPTO | shareUserDevicesKey() : starts")
+        Timber.v("## CRYPTO | shareUserDevicesKey() : starts")
 
         val results = ensureOlmSessionsForDevicesAction.handle(devicesByUser)
         Timber.v(
-                """## CRYPTO | shareUserDevicesKey(): ensureOlmSessionsForDevices succeeds after ${System.currentTimeMillis() - t0} ms"""
+                """## CRYPTO | shareUserDevicesKey(): ensureOlmSessionsForDevices succeeds after ${System.currentTimeMillis() - t0} ms"""
                         .trimMargin()
         )
         val contentMap = MXUsersDevicesMap<Any>()
@@ -257,7 +257,7 @@ internal class MXMegolmEncryption(
                     noOlmToNotify.add(UserDevice(userId, deviceID))
                     continue
                 }
-                Timber.i("## CRYPTO | shareUserDevicesKey() : Add to share keys contentMap for $userId:$deviceID")
+                Timber.i("## CRYPTO | shareUserDevicesKey() : Add to share keys contentMap for $userId:$deviceID")
                 contentMap.setObject(userId, deviceID, messageEncrypter.encryptMessage(payload, listOf(sessionResult.deviceInfo)))
                 haveTargets = true
             }
@@ -289,17 +289,17 @@ internal class MXMegolmEncryption(
 
         if (haveTargets) {
             t0 = System.currentTimeMillis()
-            Timber.i("## CRYPTO | shareUserDevicesKey() ${session.sessionId} : has target")
+            Timber.i("## CRYPTO | shareUserDevicesKey() ${session.sessionId} : has target")
             val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, contentMap)
             try {
                 sendToDeviceTask.execute(sendToDeviceParams)
-                Timber.i("## CRYPTO | shareUserDevicesKey() : sendToDevice succeeds after ${System.currentTimeMillis() - t0} ms")
+                Timber.i("## CRYPTO | shareUserDevicesKey() : sendToDevice succeeds after ${System.currentTimeMillis() - t0} ms")
             } catch (failure: Throwable) {
                 // What to do here...
-                Timber.e("## CRYPTO | shareUserDevicesKey() : Failed to share session <${session.sessionId}> with $devicesByUser ")
+                Timber.e("## CRYPTO | shareUserDevicesKey() : Failed to share session <${session.sessionId}> with $devicesByUser ")
             }
         } else {
-            Timber.i("## CRYPTO | shareUserDevicesKey() : no need to sharekey")
+            Timber.i("## CRYPTO | shareUserDevicesKey() : no need to sharekey")
         }
 
         if (noOlmToNotify.isNotEmpty()) {
@@ -317,7 +317,7 @@ internal class MXMegolmEncryption(
                                           sessionId: String,
                                           senderKey: String?,
                                           code: WithHeldCode) {
-        Timber.i("## CRYPTO | notifyKeyWithHeld() :sending withheld key for $targets session:$sessionId and code $code")
+        Timber.i("## CRYPTO | notifyKeyWithHeld() :sending withheld key for $targets session:$sessionId and code $code")
         val withHeldContent = RoomKeyWithHeldContent(
                 roomId = roomId,
                 senderKey = senderKey,
@@ -336,7 +336,7 @@ internal class MXMegolmEncryption(
         try {
             sendToDeviceTask.execute(params)
         } catch (failure: Throwable) {
-            Timber.e("## CRYPTO | notifyKeyWithHeld() : Failed to notify withheld key for $targets session: $sessionId ")
+            Timber.e("## CRYPTO | notifyKeyWithHeld() : Failed to notify withheld key for $targets session: $sessionId ")
         }
     }
 
@@ -473,7 +473,7 @@ internal class MXMegolmEncryption(
         val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
         val sendToDeviceMap = MXUsersDevicesMap<Any>()
         sendToDeviceMap.setObject(userId, deviceId, encodedPayload)
-        Timber.i("## CRYPTO | reshareKey() : sending session $sessionId to $userId:$deviceId")
+        Timber.i("## CRYPTO | reshareKey() : sending session $sessionId to $userId:$deviceId")
         val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
         return try {
             sendToDeviceTask.execute(sendToDeviceParams)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoDeviceInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoDeviceInfo.kt
index 7eebbd9b2c912f47872a6c58f6f3ed4ca849ea61..4004294d973da3fdfa47dda69d1e9055e29c7522 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoDeviceInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/CryptoDeviceInfo.kt
@@ -44,7 +44,7 @@ data class CryptoDeviceInfo(
      */
     fun fingerprint(): String? {
         return keys
-                ?.takeIf { !deviceId.isBlank() }
+                ?.takeIf { deviceId.isNotBlank() }
                 ?.get("ed25519:$deviceId")
     }
 
@@ -53,7 +53,7 @@ data class CryptoDeviceInfo(
      */
     fun identityKey(): String? {
         return keys
-                ?.takeIf { !deviceId.isBlank() }
+                ?.takeIf { deviceId.isNotBlank() }
                 ?.get("curve25519:$deviceId")
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXDeviceInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXDeviceInfo.kt
index 3c651c27a0810d34fbeac4f090d47451ed0841fd..00b8bde5d999aea1a843e537bfedab6ca7ce048f 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXDeviceInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXDeviceInfo.kt
@@ -103,7 +103,7 @@ data class MXDeviceInfo(
      */
     fun fingerprint(): String? {
         return keys
-                ?.takeIf { !deviceId.isBlank() }
+                ?.takeIf { deviceId.isNotBlank() }
                 ?.get("ed25519:$deviceId")
     }
 
@@ -112,7 +112,7 @@ data class MXDeviceInfo(
      */
     fun identityKey(): String? {
         return keys
-                ?.takeIf { !deviceId.isBlank() }
+                ?.takeIf { deviceId.isNotBlank() }
                 ?.get("curve25519:$deviceId")
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt
index d8b9d3cd864a91050a139fa365d84fd32a65380a..7fa48c3da1b56318f7d031cd7d1767eff24d4e70 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt
@@ -23,6 +23,7 @@ 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.send.LocalEchoRepository
 import org.matrix.android.sdk.internal.task.Task
+import org.matrix.android.sdk.internal.util.toMatrixErrorStr
 import javax.inject.Inject
 
 internal interface SendVerificationMessageTask : Task<SendVerificationMessageTask.Params, String> {
@@ -55,7 +56,7 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
             localEchoRepository.updateSendState(localId, event.roomId, SendState.SENT)
             return response.eventId
         } catch (e: Throwable) {
-            localEchoRepository.updateSendState(localId, event.roomId, SendState.UNDELIVERED)
+            localEchoRepository.updateSendState(localId, event.roomId, SendState.UNDELIVERED, e.toMatrixErrorStr())
             throw e
         }
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/HkdfSha256.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/HkdfSha256.kt
index 6d52e682bc9c013225397d1ce2066ea18c656419..6839ccd3262c5679c27d3e977196a0405f0e1752 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/HkdfSha256.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tools/HkdfSha256.kt
@@ -57,7 +57,7 @@ object HkdfSha256 {
 
         /*
           The output OKM is calculated as follows:
-          Notation | -> When the message is composed of several elements we use concatenation (denoted |) in the second argument;
+          Notation | -> When the message is composed of several elements we use concatenation (denoted |) in the second argument;
 
 
            N = ceil(L/HashLen)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt
index c7885ce449edbbfd20e7708dba3664782b132e48..4bf01a28095a6cbf390dc43b5d82623726f1e0d3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt
@@ -345,7 +345,7 @@ internal abstract class SASDefaultVerificationTransaction(
     }
 
     protected fun hashUsingAgreedHashMethod(toHash: String): String? {
-        if ("sha256" == accepted?.hash?.toLowerCase(Locale.ROOT)) {
+        if ("sha256" == accepted?.hash?.lowercase(Locale.ROOT)) {
             val olmUtil = OlmUtility()
             val hashBytes = olmUtil.sha256(toHash)
             olmUtil.releaseUtility()
@@ -355,7 +355,7 @@ internal abstract class SASDefaultVerificationTransaction(
     }
 
     private fun macUsingAgreedMethod(message: String, info: String): String? {
-        return when (accepted?.messageAuthenticationCode?.toLowerCase(Locale.ROOT)) {
+        return when (accepted?.messageAuthenticationCode?.lowercase(Locale.ROOT)) {
             SAS_MAC_SHA256_LONGKDF -> getSAS().calculateMacLongKdf(message, info)
             SAS_MAC_SHA256         -> getSAS().calculateMac(message, info)
             else                   -> null
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/Extensions.kt
index 6bc3483e651ad5958a35ce34300e3527e32971c1..76e88442b60759cc492971feffdcf801a883708e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/Extensions.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/Extensions.kt
@@ -48,7 +48,7 @@ fun QrCodeData.toEncodedString(): String {
 
     // TransactionId
     transactionId.forEach {
-        result += it.toByte()
+        result += it.code.toByte()
     }
 
     // Keys
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DatabaseCleaner.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DatabaseCleaner.kt
index f11ecc5d75f8b6209b0739bca723e0e034318881..ee58880eb833a1bea6ef0ef557ab746e5fb0ac4d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DatabaseCleaner.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/DatabaseCleaner.kt
@@ -20,6 +20,7 @@ import io.realm.Realm
 import io.realm.RealmConfiguration
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
+import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.internal.database.helper.nextDisplayIndex
 import org.matrix.android.sdk.internal.database.model.ChunkEntity
 import org.matrix.android.sdk.internal.database.model.ChunkEntityFields
@@ -29,7 +30,7 @@ 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.model.deleteOnCascade
 import org.matrix.android.sdk.internal.di.SessionDatabase
-import org.matrix.android.sdk.internal.session.SessionLifecycleObserver
+import org.matrix.android.sdk.api.session.SessionLifecycleObserver
 import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
 import org.matrix.android.sdk.internal.task.TaskExecutor
 import timber.log.Timber
@@ -47,7 +48,7 @@ private const val MIN_NUMBER_OF_EVENTS_BY_CHUNK = 300
 internal class DatabaseCleaner @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration,
                                                    private val taskExecutor: TaskExecutor) : SessionLifecycleObserver {
 
-    override fun onSessionStarted() {
+    override fun onSessionStarted(session: Session) {
         taskExecutor.executorScope.launch(Dispatchers.Default) {
             awaitTransaction(realmConfiguration) { realm ->
                 val allRooms = realm.where(RoomEntity::class.java).findAll()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmKeysUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmKeysUtils.kt
index 0e3a7a2c49b602f1017e3db7beefd9e65e401ec9..d5ff7a0f84975bd2618a27de25ebfe36fe7d5473 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmKeysUtils.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmKeysUtils.kt
@@ -71,7 +71,7 @@ internal class RealmKeysUtils @Inject constructor(context: Context,
         val encodedKey = Base64.encodeToString(key, Base64.NO_PADDING)
         val toStore = secretStoringUtils.securelyStoreString(encodedKey, alias)
         sharedPreferences.edit {
-            putString("${ENCRYPTED_KEY_PREFIX}_$alias", Base64.encodeToString(toStore!!, Base64.NO_PADDING))
+            putString("${ENCRYPTED_KEY_PREFIX}_$alias", Base64.encodeToString(toStore, Base64.NO_PADDING))
         }
         return key
     }
@@ -84,7 +84,7 @@ internal class RealmKeysUtils @Inject constructor(context: Context,
         val encryptedB64 = sharedPreferences.getString("${ENCRYPTED_KEY_PREFIX}_$alias", null)
         val encryptedKey = Base64.decode(encryptedB64, Base64.NO_PADDING)
         val b64 = secretStoringUtils.loadSecureSecret(encryptedKey, alias)
-        return Base64.decode(b64!!, Base64.NO_PADDING)
+        return Base64.decode(b64, Base64.NO_PADDING)
     }
 
     fun configureEncryption(realmConfigurationBuilder: RealmConfiguration.Builder, alias: String) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmLiveEntityObserver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmLiveEntityObserver.kt
index 2a0cd963b2506c5a1f8547993661766a1899bad5..c602ed7075919addb62c6e4fbcb08a5caf53bf69 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmLiveEntityObserver.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmLiveEntityObserver.kt
@@ -17,7 +17,7 @@
 package org.matrix.android.sdk.internal.database
 
 import com.zhuinden.monarchy.Monarchy
-import org.matrix.android.sdk.internal.session.SessionLifecycleObserver
+import org.matrix.android.sdk.api.session.SessionLifecycleObserver
 import org.matrix.android.sdk.internal.util.createBackgroundHandler
 import io.realm.Realm
 import io.realm.RealmChangeListener
@@ -28,6 +28,7 @@ import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.SupervisorJob
 import kotlinx.coroutines.android.asCoroutineDispatcher
 import kotlinx.coroutines.cancelChildren
+import org.matrix.android.sdk.api.session.Session
 import java.util.concurrent.atomic.AtomicBoolean
 import java.util.concurrent.atomic.AtomicReference
 
@@ -46,7 +47,7 @@ internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val r
     private val backgroundRealm = AtomicReference<Realm>()
     private lateinit var results: AtomicReference<RealmResults<T>>
 
-    override fun onSessionStarted() {
+    override fun onSessionStarted(session: Session) {
         if (isStarted.compareAndSet(false, true)) {
             BACKGROUND_HANDLER.post {
                 val realm = Realm.getInstance(realmConfiguration)
@@ -58,7 +59,7 @@ internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val r
         }
     }
 
-    override fun onSessionStopped() {
+    override fun onSessionStopped(session: Session) {
         if (isStarted.compareAndSet(true, false)) {
             BACKGROUND_HANDLER.post {
                 results.getAndSet(null).removeAllChangeListeners()
@@ -70,7 +71,7 @@ internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val r
         }
     }
 
-    override fun onClearCache() {
+    override fun onClearCache(session: Session) {
         observerScope.coroutineContext.cancelChildren()
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionProvider.kt
index f8d5d323a5a4f9015709cbd84773b6cea57ae682..52fbabb49fe85e220fbfa8347f6ab39b5c203821 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionProvider.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionProvider.kt
@@ -20,8 +20,9 @@ import android.os.Looper
 import androidx.annotation.MainThread
 import com.zhuinden.monarchy.Monarchy
 import io.realm.Realm
+import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.internal.di.SessionDatabase
-import org.matrix.android.sdk.internal.session.SessionLifecycleObserver
+import org.matrix.android.sdk.api.session.SessionLifecycleObserver
 import org.matrix.android.sdk.internal.session.SessionScope
 import javax.inject.Inject
 import kotlin.concurrent.getOrSet
@@ -44,14 +45,14 @@ internal class RealmSessionProvider @Inject constructor(@SessionDatabase private
     }
 
     @MainThread
-    override fun onSessionStarted() {
+    override fun onSessionStarted(session: Session) {
         realmThreadLocal.getOrSet {
             Realm.getInstance(monarchy.realmConfiguration)
         }
     }
 
     @MainThread
-    override fun onSessionStopped() {
+    override fun onSessionStopped(session: Session) {
         realmThreadLocal.get()?.close()
         realmThreadLocal.remove()
     }
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 1daae906f2e902de3d302c776a46a29e4734861d..d810c8b1a8bb85d47a0e9e38b0952bb38fab1026 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
@@ -19,7 +19,11 @@ package org.matrix.android.sdk.internal.database
 import io.realm.DynamicRealm
 import io.realm.FieldAttribute
 import io.realm.RealmMigration
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
+import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
 import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
+import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields
 import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntityFields
 import org.matrix.android.sdk.internal.database.model.EditionOfEventFields
 import org.matrix.android.sdk.internal.database.model.EventEntityFields
@@ -30,14 +34,17 @@ import org.matrix.android.sdk.internal.database.model.RoomEntityFields
 import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType
 import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
 import org.matrix.android.sdk.internal.database.model.RoomTagEntityFields
+import org.matrix.android.sdk.internal.database.model.SpaceChildSummaryEntityFields
+import org.matrix.android.sdk.internal.database.model.SpaceParentSummaryEntityFields
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
+import org.matrix.android.sdk.internal.di.MoshiProvider
 import timber.log.Timber
 import javax.inject.Inject
 
 class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
 
     companion object {
-        const val SESSION_STORE_SCHEMA_VERSION = 9L
+        const val SESSION_STORE_SCHEMA_VERSION = 13L
     }
 
     override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
@@ -52,6 +59,10 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
         if (oldVersion <= 6) migrateTo7(realm)
         if (oldVersion <= 7) migrateTo8(realm)
         if (oldVersion <= 8) migrateTo9(realm)
+        if (oldVersion <= 9) migrateTo10(realm)
+        if (oldVersion <= 10) migrateTo11(realm)
+        if (oldVersion <= 11) migrateTo12(realm)
+        if (oldVersion <= 12) migrateTo13(realm)
     }
 
     private fun migrateTo1(realm: DynamicRealm) {
@@ -136,10 +147,6 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
         Timber.d("Step 7 -> 8")
 
         val editionOfEventSchema = realm.schema.create("EditionOfEvent")
-                .apply {
-                    // setEmbedded does not return `this`...
-                    isEmbedded = true
-                }
                 .addField(EditionOfEventFields.CONTENT, String::class.java)
                 .addField(EditionOfEventFields.EVENT_ID, String::class.java)
                 .setRequired(EditionOfEventFields.EVENT_ID, true)
@@ -154,9 +161,13 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
                 ?.removeField("lastEditTs")
                 ?.removeField("sourceLocalEchoEvents")
                 ?.addRealmListField(EditAggregatedSummaryEntityFields.EDITIONS.`$`, editionOfEventSchema)
+
+        // This has to be done once a parent use the model as a child
+        // See https://github.com/realm/realm-java/issues/7402
+        editionOfEventSchema.isEmbedded = true
     }
 
-    fun migrateTo9(realm: DynamicRealm) {
+    private fun migrateTo9(realm: DynamicRealm) {
         Timber.d("Step 8 -> 9")
 
         realm.schema.get("RoomSummaryEntity")
@@ -174,7 +185,6 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
                 ?.addIndex(RoomSummaryEntityFields.IS_SERVER_NOTICE)
 
                 ?.transform { obj ->
-
                     val isFavorite = obj.getList(RoomSummaryEntityFields.TAGS.`$`).any {
                         it.getString(RoomTagEntityFields.TAG_NAME) == RoomTag.ROOM_TAG_FAVOURITE
                     }
@@ -194,4 +204,85 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
                             }
                 }
     }
+
+    private fun migrateTo10(realm: DynamicRealm) {
+        Timber.d("Step 9 -> 10")
+        realm.schema.create("SpaceChildSummaryEntity")
+                ?.addField(SpaceChildSummaryEntityFields.ORDER, String::class.java)
+                ?.addField(SpaceChildSummaryEntityFields.CHILD_ROOM_ID, String::class.java)
+                ?.addField(SpaceChildSummaryEntityFields.AUTO_JOIN, Boolean::class.java)
+                ?.setNullable(SpaceChildSummaryEntityFields.AUTO_JOIN, true)
+                ?.addRealmObjectField(SpaceChildSummaryEntityFields.CHILD_SUMMARY_ENTITY.`$`, realm.schema.get("RoomSummaryEntity")!!)
+                ?.addRealmListField(SpaceChildSummaryEntityFields.VIA_SERVERS.`$`, String::class.java)
+
+        realm.schema.create("SpaceParentSummaryEntity")
+                ?.addField(SpaceParentSummaryEntityFields.PARENT_ROOM_ID, String::class.java)
+                ?.addField(SpaceParentSummaryEntityFields.CANONICAL, Boolean::class.java)
+                ?.setNullable(SpaceParentSummaryEntityFields.CANONICAL, true)
+                ?.addRealmObjectField(SpaceParentSummaryEntityFields.PARENT_SUMMARY_ENTITY.`$`, realm.schema.get("RoomSummaryEntity")!!)
+                ?.addRealmListField(SpaceParentSummaryEntityFields.VIA_SERVERS.`$`, String::class.java)
+
+        val creationContentAdapter = MoshiProvider.providesMoshi().adapter(RoomCreateContent::class.java)
+        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)
+                ?.transform { obj ->
+
+                    val creationEvent = realm.where("CurrentStateEventEntity")
+                            .equalTo(CurrentStateEventEntityFields.ROOM_ID, obj.getString(RoomSummaryEntityFields.ROOM_ID))
+                            .equalTo(CurrentStateEventEntityFields.TYPE, EventType.STATE_ROOM_CREATE)
+                            .findFirst()
+
+                    val roomType = creationEvent?.getObject(CurrentStateEventEntityFields.ROOT.`$`)
+                            ?.getString(EventEntityFields.CONTENT)?.let {
+                                creationContentAdapter.fromJson(it)?.type
+                            }
+
+                    obj.setString(RoomSummaryEntityFields.ROOM_TYPE, roomType)
+                }
+                ?.addRealmListField(RoomSummaryEntityFields.PARENTS.`$`, realm.schema.get("SpaceParentSummaryEntity")!!)
+                ?.addRealmListField(RoomSummaryEntityFields.CHILDREN.`$`, realm.schema.get("SpaceChildSummaryEntity")!!)
+    }
+
+    private fun migrateTo11(realm: DynamicRealm) {
+        Timber.d("Step 10 -> 11")
+        realm.schema.get("EventEntity")
+                ?.addField(EventEntityFields.SEND_STATE_DETAILS, String::class.java)
+    }
+
+    private fun migrateTo12(realm: DynamicRealm) {
+        Timber.d("Step 11 -> 12")
+
+        val joinRulesContentAdapter = MoshiProvider.providesMoshi().adapter(RoomJoinRulesContent::class.java)
+        realm.schema.get("RoomSummaryEntity")
+                ?.addField(RoomSummaryEntityFields.JOIN_RULES_STR, String::class.java)
+                ?.transform { obj ->
+                    val joinRulesEvent = realm.where("CurrentStateEventEntity")
+                            .equalTo(CurrentStateEventEntityFields.ROOM_ID, obj.getString(RoomSummaryEntityFields.ROOM_ID))
+                            .equalTo(CurrentStateEventEntityFields.TYPE, EventType.STATE_ROOM_JOIN_RULES)
+                            .findFirst()
+
+                    val roomJoinRules = joinRulesEvent?.getObject(CurrentStateEventEntityFields.ROOT.`$`)
+                            ?.getString(EventEntityFields.CONTENT)?.let {
+                                joinRulesContentAdapter.fromJson(it)?.joinRules
+                            }
+
+                    obj.setString(RoomSummaryEntityFields.JOIN_RULES_STR, roomJoinRules?.name)
+                }
+
+        realm.schema.get("SpaceChildSummaryEntity")
+                ?.addField(SpaceChildSummaryEntityFields.SUGGESTED, Boolean::class.java)
+                ?.setNullable(SpaceChildSummaryEntityFields.SUGGESTED, true)
+    }
+
+    private fun migrateTo13(realm: DynamicRealm) {
+        Timber.d("Step 12 -> 13")
+
+        // Fix issue with the nightly build. Eventually play again the migration which has been included in migrateTo12()
+        realm.schema.get("SpaceChildSummaryEntity")
+                ?.takeIf { !it.hasField(SpaceChildSummaryEntityFields.SUGGESTED) }
+                ?.addField(SpaceChildSummaryEntityFields.SUGGESTED, Boolean::class.java)
+                ?.setNullable(SpaceChildSummaryEntityFields.SUGGESTED, true)
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt
index a4a2fadd21d9045e7fefe5eab3a7f132cfcce6d7..613b38e3403b1023bc9a9bb9fa46b8b23311c6c7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt
@@ -80,6 +80,7 @@ internal object EventMapper {
         ).also {
             it.ageLocalTs = eventEntity.ageLocalTs
             it.sendState = eventEntity.sendState
+            it.sendStateDetails = eventEntity.sendStateDetails
             eventEntity.decryptionResultJson?.let { json ->
                 try {
                     it.mxDecryptionResult = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).fromJson(json)
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 6dc70b60fc71937647211b6d58b8a9019096e395..3fea15bd3dd7facf9edb659156506b9a37b9b2b3 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
@@ -17,6 +17,8 @@
 package org.matrix.android.sdk.internal.database.mapper
 
 import org.matrix.android.sdk.api.session.room.model.RoomSummary
+import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
+import org.matrix.android.sdk.api.session.room.model.SpaceParentInfo
 import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
 import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
 import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker
@@ -42,7 +44,9 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
                 name = roomSummaryEntity.name ?: "",
                 topic = roomSummaryEntity.topic ?: "",
                 avatarUrl = roomSummaryEntity.avatarUrl ?: "",
+                joinRules = roomSummaryEntity.joinRules,
                 isDirect = roomSummaryEntity.isDirect,
+                directUserId = roomSummaryEntity.directUserId,
                 latestPreviewableEvent = latestEvent,
                 joinedMembersCount = roomSummaryEntity.joinedMembersCount,
                 invitedMembersCount = roomSummaryEntity.invitedMembersCount,
@@ -63,7 +67,35 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
                 breadcrumbsIndex = roomSummaryEntity.breadcrumbsIndex,
                 roomEncryptionTrustLevel = roomSummaryEntity.roomEncryptionTrustLevel,
                 inviterId = roomSummaryEntity.inviterId,
-                hasFailedSending = roomSummaryEntity.hasFailedSending
+                hasFailedSending = roomSummaryEntity.hasFailedSending,
+                roomType = roomSummaryEntity.roomType,
+                spaceParents = roomSummaryEntity.parents.map { relationInfoEntity ->
+                    SpaceParentInfo(
+                            parentId = relationInfoEntity.parentRoomId,
+                            roomSummary = relationInfoEntity.parentSummaryEntity?.let { map(it) },
+                            canonical = relationInfoEntity.canonical ?: false,
+                            viaServers = relationInfoEntity.viaServers.toList()
+                    )
+                },
+                spaceChildren = roomSummaryEntity.children.map {
+                    SpaceChildInfo(
+                            childRoomId = it.childRoomId ?: "",
+                            isKnown = it.childSummaryEntity != null,
+                            roomType = it.childSummaryEntity?.roomType,
+                            name = it.childSummaryEntity?.name,
+                            topic = it.childSummaryEntity?.topic,
+                            avatarUrl = it.childSummaryEntity?.avatarUrl,
+                            activeMemberCount = it.childSummaryEntity?.joinedMembersCount,
+                            order = it.order,
+                            autoJoin = it.autoJoin ?: false,
+                            viaServers = it.viaServers.toList(),
+                            parentRoomId = roomSummaryEntity.roomId,
+                            suggested = it.suggested,
+                            canonicalAlias = it.childSummaryEntity?.canonicalAlias,
+                            aliases = it.childSummaryEntity?.aliases?.toList()
+                    )
+                },
+                flattenParentIds = roomSummaryEntity.flattenParentIds?.split("|") ?: emptyList()
         )
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt
index fe59f4fceb03edf412267caac73cf3ae4a680d9c..c9edbcd889b6cdc6ab1d40d705ff904fa44dc3fa 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt
@@ -32,6 +32,8 @@ internal open class EventEntity(@Index var eventId: String = "",
                                 @Index var stateKey: String? = null,
                                 var originServerTs: Long? = null,
                                 @Index var sender: String? = null,
+                                // Can contain a serialized MatrixError
+                                var sendStateDetails: String? = null,
                                 var age: Long? = 0,
                                 var unsignedData: String? = null,
                                 var redacts: String? = null,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt
index 3ff2532604803b7d9c0080ec0174e038f55c9be0..58297776f06b1f47189aee4cf22ad3ffa718abc1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt
@@ -43,6 +43,5 @@ internal open class RoomEntity(@PrimaryKey var roomId: String = "",
         set(value) {
             membersLoadStatusStr = 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 c87ac15a78337778b9be15fe13f98740349d7ea7..1001f9cd66ea11d0ae57d6cdd265478d6406c467 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
@@ -21,13 +21,18 @@ import io.realm.RealmObject
 import io.realm.annotations.Index
 import io.realm.annotations.PrimaryKey
 import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
+import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.session.room.model.Membership
+import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
 import org.matrix.android.sdk.api.session.room.model.RoomSummary
 import org.matrix.android.sdk.api.session.room.model.VersioningState
 import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
 
 internal open class RoomSummaryEntity(
-        @PrimaryKey var roomId: String = ""
+        @PrimaryKey var roomId: String = "",
+        var roomType: String? = null,
+        var parents: RealmList<SpaceParentSummaryEntity> = RealmList(),
+        var children: RealmList<SpaceChildSummaryEntity> = RealmList()
 ) : RealmObject() {
 
     var displayName: String? = ""
@@ -204,6 +209,16 @@ internal open class RoomSummaryEntity(
             if (value != field) field = value
         }
 
+    var flattenParentIds: String? = null
+        set(value) {
+            if (value != field) field = value
+        }
+
+    var groupIds: String? = null
+        set(value) {
+            if (value != field) field = value
+        }
+
     @Index
     private var membershipStr: String = Membership.NONE.name
 
@@ -229,6 +244,19 @@ internal open class RoomSummaryEntity(
             }
         }
 
+    private var joinRulesStr: String? = null
+    var joinRules: RoomJoinRules?
+        get() {
+            return joinRulesStr?.let {
+                tryOrNull { RoomJoinRules.valueOf(it) }
+            }
+        }
+        set(value) {
+            if (value?.name != joinRulesStr) {
+                joinRulesStr = value?.name
+            }
+        }
+
     var roomEncryptionTrustLevel: RoomEncryptionTrustLevel?
         get() {
             return roomEncryptionTrustLevelStr?.let {
@@ -244,6 +272,5 @@ internal open class RoomSummaryEntity(
                 roomEncryptionTrustLevelStr = value?.name
             }
         }
-
     companion object
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt
index 6e6096cf8a62111742d2306224ff91a125830b97..72ae512fa5a444529809da11b63028355ba4251e 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
@@ -61,6 +61,8 @@ import io.realm.annotations.RealmModule
             CurrentStateEventEntity::class,
             UserAccountDataEntity::class,
             ScalarTokenEntity::class,
-            WellknownIntegrationManagerConfigEntity::class
+            WellknownIntegrationManagerConfigEntity::class,
+            SpaceChildSummaryEntity::class,
+            SpaceParentSummaryEntity::class
         ])
 internal class SessionRealmModule
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceChildSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceChildSummaryEntity.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ce1afbb507417d905faf80c93556e2043c53588c
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceChildSummaryEntity.kt
@@ -0,0 +1,46 @@
+/*
+ * 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
+
+/**
+ * Decorates room summary with space related information.
+ */
+internal open class SpaceChildSummaryEntity(
+//        var isSpace: Boolean = false,
+
+        var order: String? = null,
+
+        var autoJoin: Boolean? = null,
+
+        var suggested: Boolean? = null,
+
+        var childRoomId: String? = null,
+        // Link to the actual space summary if it is known locally
+        var childSummaryEntity: RoomSummaryEntity? = null,
+
+        var viaServers: RealmList<String> = RealmList()
+//        var owner: RoomSummaryEntity? = null,
+
+//        var level: Int = 0
+
+) : RealmObject() {
+
+    companion object
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceParentSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceParentSummaryEntity.kt
new file mode 100644
index 0000000000000000000000000000000000000000..30517717f4393927c7d8005aa589ed8e32c3cd9a
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceParentSummaryEntity.kt
@@ -0,0 +1,45 @@
+/*
+ * 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
+
+/**
+ * Decorates room summary with space related information.
+ */
+internal open class SpaceParentSummaryEntity(
+        /**
+         * Determines whether this is the main parent for the space
+         * When a user joins a room with a canonical parent, clients may switch to view the room in the context of that space,
+         * peeking into it in order to find other rooms and group them together.
+         * In practice, well behaved rooms should only have one canonical parent, but given this is not enforced:
+         * if multiple are present the client should select the one with the lowest room ID,
+         * as determined via a lexicographic utf-8 ordering.
+         */
+        var canonical: Boolean? = null,
+
+        var parentRoomId: String? = null,
+        // Link to the actual space summary if it is known locally
+        var parentSummaryEntity: RoomSummaryEntity? = null,
+
+        var viaServers: RealmList<String> = RealmList()
+
+) : RealmObject() {
+
+    companion object
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt
index 9d6fa29bb278a83a3ec1f66469edbdf3ea4cc309..5bc519e96049aede5d142aff61e37cb462069f28 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt
@@ -36,6 +36,7 @@ import org.matrix.android.sdk.internal.session.TestInterceptor
 import org.matrix.android.sdk.internal.task.TaskExecutor
 import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver
 import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
+import org.matrix.android.sdk.internal.util.system.SystemModule
 import org.matrix.olm.OlmManager
 import java.io.File
 
@@ -44,6 +45,7 @@ import java.io.File
     NetworkModule::class,
     AuthModule::class,
     RawModule::class,
+    SystemModule::class,
     NoOpTestModule::class
 ])
 @MatrixScope
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt
index 0246bae024eed8933e5f2224939ad9655d652a1e..e045cebd3ed0c1cb55043c8e33a660a8b7f1fde7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/Request.kt
@@ -88,8 +88,8 @@ internal suspend inline fun <DATA> executeRequest(globalErrorReceiver: GlobalErr
                 throw when (exception) {
                     is IOException              -> Failure.NetworkConnection(exception)
                     is Failure.ServerError,
-                    is Failure.OtherServerError -> exception
-                    is CancellationException    -> Failure.Cancelled(exception)
+                    is Failure.OtherServerError,
+                    is CancellationException    -> exception
                     else                        -> Failure.Unknown(exception)
                 }
             }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitExtensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitExtensions.kt
index 7132b4ff7a8518fb034d543f646953629f2bc203..2116063626b16d2758b3427022b11c90a8373b9f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitExtensions.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitExtensions.kt
@@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.failure.MatrixError
 import org.matrix.android.sdk.internal.di.MoshiProvider
 import kotlinx.coroutines.suspendCancellableCoroutine
 import okhttp3.ResponseBody
+import org.matrix.android.sdk.api.extensions.orFalse
 import retrofit2.HttpException
 import retrofit2.Response
 import timber.log.Timber
@@ -91,7 +92,7 @@ private fun toFailure(errorBody: ResponseBody?, httpCode: Int, globalErrorReceiv
             } else if (httpCode == HttpURLConnection.HTTP_UNAUTHORIZED /* 401 */
                     && matrixError.code == MatrixError.M_UNKNOWN_TOKEN) {
                 // Also send this error to the globalErrorReceiver, for a global management
-                globalErrorReceiver?.handleGlobalError(GlobalError.InvalidToken(matrixError.isSoftLogout))
+                globalErrorReceiver?.handleGlobalError(GlobalError.InvalidToken(matrixError.isSoftLogout.orFalse()))
             }
 
             return Failure.ServerError(matrixError, httpCode)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryRoomOrderProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryRoomOrderProcessor.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7a06c2129c0cb12a3bc7a326437663a86791122c
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryRoomOrderProcessor.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.query
+
+import io.realm.RealmQuery
+import io.realm.Sort
+import org.matrix.android.sdk.api.session.room.RoomSortOrder
+import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
+import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
+
+internal fun RealmQuery<RoomSummaryEntity>.process(sortOrder: RoomSortOrder): RealmQuery<RoomSummaryEntity> {
+    when (sortOrder) {
+        RoomSortOrder.NAME     -> {
+            sort(RoomSummaryEntityFields.DISPLAY_NAME, Sort.ASCENDING)
+        }
+        RoomSortOrder.ACTIVITY -> {
+            sort(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, Sort.DESCENDING)
+        }
+        RoomSortOrder.NONE     -> {
+        }
+    }
+    return this
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryStringValueProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryStringValueProcessor.kt
index 899024458a548af33fb73b355c3ec2cfa5664b83..fd3368223139837781fe7a2f97d64d6f48183d1e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryStringValueProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/query/QueryStringValueProcessor.kt
@@ -16,10 +16,10 @@
 
 package org.matrix.android.sdk.internal.query
 
-import org.matrix.android.sdk.api.query.QueryStringValue
 import io.realm.Case
 import io.realm.RealmObject
 import io.realm.RealmQuery
+import org.matrix.android.sdk.api.query.QueryStringValue
 import timber.log.Timber
 
 fun <T : RealmObject> RealmQuery<T>.process(field: String, queryStringValue: QueryStringValue): RealmQuery<T> {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt
index d05ee48c1bf41c33e520600100756af615e8ef3a..a284d976d0525187ce18e5306e488f7a29f2cd6f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt
@@ -219,7 +219,7 @@ internal class DefaultFileService @Inject constructor(
                                fileName: String,
                                mimeType: String?,
                                elementToDecrypt: ElementToDecrypt?): Boolean {
-        return fileState(mxcUrl, fileName, mimeType, elementToDecrypt) == FileService.FileState.IN_CACHE
+        return fileState(mxcUrl, fileName, mimeType, elementToDecrypt) is FileService.FileState.InCache
     }
 
     internal data class CachedFiles(
@@ -256,12 +256,17 @@ internal class DefaultFileService @Inject constructor(
                            fileName: String,
                            mimeType: String?,
                            elementToDecrypt: ElementToDecrypt?): FileService.FileState {
-        mxcUrl ?: return FileService.FileState.UNKNOWN
-        if (getFiles(mxcUrl, fileName, mimeType, elementToDecrypt != null).file.exists()) return FileService.FileState.IN_CACHE
+        mxcUrl ?: return FileService.FileState.Unknown
+        val files = getFiles(mxcUrl, fileName, mimeType, elementToDecrypt != null)
+        if (files.file.exists()) {
+            return FileService.FileState.InCache(
+                    decryptedFileInCache = files.getClearFile().exists()
+            )
+        }
         val isDownloading = synchronized(ongoing) {
             ongoing[mxcUrl] != null
         }
-        return if (isDownloading) FileService.FileState.DOWNLOADING else FileService.FileState.UNKNOWN
+        return if (isDownloading) FileService.FileState.Downloading else FileService.FileState.Unknown
     }
 
     /**
@@ -286,7 +291,7 @@ internal class DefaultFileService @Inject constructor(
                     Timber.v("Get size of ${it.absolutePath}")
                     true
                 }
-                .sumBy { it.length().toInt() }
+                .sumOf { it.length().toInt() }
     }
 
     override fun clearCache() {
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 821a9cba8c58f8b24493de43c5a10232b6094e53..b100a336a79b2c3cc909312bd1941391d47706db 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
@@ -19,13 +19,15 @@ package org.matrix.android.sdk.internal.session
 import androidx.annotation.MainThread
 import dagger.Lazy
 import io.realm.RealmConfiguration
+import kotlinx.coroutines.NonCancellable
+import kotlinx.coroutines.withContext
 import okhttp3.OkHttpClient
 import org.matrix.android.sdk.api.auth.data.SessionParams
 import org.matrix.android.sdk.api.failure.GlobalError
 import org.matrix.android.sdk.api.federation.FederationService
 import org.matrix.android.sdk.api.pushrules.PushRuleService
-import org.matrix.android.sdk.api.session.initsync.InitialSyncProgressService
 import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.api.session.SessionLifecycleObserver
 import org.matrix.android.sdk.api.session.account.AccountService
 import org.matrix.android.sdk.api.session.accountdata.AccountDataService
 import org.matrix.android.sdk.api.session.cache.CacheService
@@ -38,8 +40,10 @@ 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.initsync.InitialSyncProgressService
 import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService
 import org.matrix.android.sdk.api.session.media.MediaService
+import org.matrix.android.sdk.api.session.openid.OpenIdService
 import org.matrix.android.sdk.api.session.permalinks.PermalinkService
 import org.matrix.android.sdk.api.session.profile.ProfileService
 import org.matrix.android.sdk.api.session.pushers.PushersService
@@ -49,6 +53,7 @@ import org.matrix.android.sdk.api.session.search.SearchService
 import org.matrix.android.sdk.api.session.securestorage.SecureStorageService
 import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
 import org.matrix.android.sdk.api.session.signout.SignOutService
+import org.matrix.android.sdk.api.session.space.SpaceService
 import org.matrix.android.sdk.api.session.sync.FilterService
 import org.matrix.android.sdk.api.session.terms.TermsService
 import org.matrix.android.sdk.api.session.thirdparty.ThirdPartyService
@@ -120,6 +125,8 @@ internal class DefaultSession @Inject constructor(
         private val integrationManagerService: IntegrationManagerService,
         private val thirdPartyService: Lazy<ThirdPartyService>,
         private val callSignalingService: Lazy<CallSignalingService>,
+        private val spaceService: Lazy<SpaceService>,
+        private val openIdService: Lazy<OpenIdService>,
         @UnauthenticatedWithCertificate
         private val unauthenticatedWithCertificateOkHttpClient: Lazy<OkHttpClient>
 ) : Session,
@@ -159,7 +166,12 @@ internal class DefaultSession @Inject constructor(
         isOpen = true
         cryptoService.get().ensureDevice()
         uiHandler.post {
-            lifecycleObservers.forEach { it.onSessionStarted() }
+            lifecycleObservers.forEach {
+                it.onSessionStarted(this)
+            }
+            sessionListeners.dispatch {
+                it.onSessionStarted(this)
+            }
         }
         globalErrorHandler.listener = this
     }
@@ -200,7 +212,10 @@ internal class DefaultSession @Inject constructor(
         stopSync()
         // timelineEventDecryptor.destroy()
         uiHandler.post {
-            lifecycleObservers.forEach { it.onSessionStopped() }
+            lifecycleObservers.forEach { it.onSessionStopped(this) }
+            sessionListeners.dispatch {
+                it.onSessionStopped(this)
+            }
         }
         cryptoService.get().close()
         isOpen = false
@@ -225,14 +240,23 @@ internal class DefaultSession @Inject constructor(
         stopSync()
         stopAnyBackgroundSync()
         uiHandler.post {
-            lifecycleObservers.forEach { it.onClearCache() }
+            lifecycleObservers.forEach {
+                it.onClearCache(this)
+            }
+            sessionListeners.dispatch {
+                it.onClearCache(this)
+            }
+        }
+        withContext(NonCancellable) {
+            cacheService.get().clearCache()
         }
-        cacheService.get().clearCache()
         workManagerProvider.cancelAllWorks()
     }
 
     override fun onGlobalError(globalError: GlobalError) {
-        sessionListeners.dispatchGlobalError(globalError)
+        sessionListeners.dispatch {
+            it.onGlobalError(this, globalError)
+        }
     }
 
     override fun contentUrlResolver() = contentUrlResolver
@@ -265,6 +289,10 @@ internal class DefaultSession @Inject constructor(
 
     override fun thirdPartyService(): ThirdPartyService = thirdPartyService.get()
 
+    override fun spaceService(): SpaceService = spaceService.get()
+
+    override fun openIdService(): OpenIdService = openIdService.get()
+
     override fun getOkHttpClient(): OkHttpClient {
         return unauthenticatedWithCertificateOkHttpClient.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 7e1e3d0f7097b89daefecebc24c5fd3d89a0c0fe..9a936b73c2346b0ac2d63d49a62b906a66bb8350 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
@@ -52,6 +52,7 @@ import org.matrix.android.sdk.internal.session.room.send.RedactEventWorker
 import org.matrix.android.sdk.internal.session.room.send.SendEventWorker
 import org.matrix.android.sdk.internal.session.search.SearchModule
 import org.matrix.android.sdk.internal.session.signout.SignOutModule
+import org.matrix.android.sdk.internal.session.space.SpaceModule
 import org.matrix.android.sdk.internal.session.sync.SyncModule
 import org.matrix.android.sdk.internal.session.sync.SyncTask
 import org.matrix.android.sdk.internal.session.sync.SyncTokenStore
@@ -63,6 +64,7 @@ import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataModul
 import org.matrix.android.sdk.internal.session.widgets.WidgetModule
 import org.matrix.android.sdk.internal.task.TaskExecutor
 import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
+import org.matrix.android.sdk.internal.util.system.SystemModule
 
 @Component(dependencies = [MatrixComponent::class],
         modules = [
@@ -79,6 +81,7 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
             CacheModule::class,
             MediaModule::class,
             CryptoModule::class,
+            SystemModule::class,
             PushersModule::class,
             OpenIdModule::class,
             WidgetModule::class,
@@ -91,7 +94,8 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
             FederationModule::class,
             CallModule::class,
             SearchModule::class,
-            ThirdPartyModule::class
+            ThirdPartyModule::class,
+            SpaceModule::class
         ]
 )
 @SessionScope
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionCoroutineScopeHolder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionCoroutineScopeHolder.kt
new file mode 100644
index 0000000000000000000000000000000000000000..82a8f79fd51217542add0dd24d6fdc9887f53183
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionCoroutineScopeHolder.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session
+
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancelChildren
+import javax.inject.Inject
+import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.api.session.SessionLifecycleObserver
+
+@SessionScope
+internal class SessionCoroutineScopeHolder @Inject constructor(): SessionLifecycleObserver {
+
+    val scope: CoroutineScope = CoroutineScope(SupervisorJob())
+
+    override fun onSessionStopped(session: Session) {
+        scope.cancelChildren()
+    }
+
+    override fun onClearCache(session: Session) {
+        scope.cancelChildren()
+    }
+
+    private fun CoroutineScope.cancelChildren() {
+        coroutineContext.cancelChildren(CancellationException("Closing session"))
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionListeners.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionListeners.kt
index 64f2d249f33cfc826d408b4b946dd8ac330b7a14..563ff4ada322cb827702b3ef7612e1e25a99caad 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionListeners.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionListeners.kt
@@ -16,7 +16,6 @@
 
 package org.matrix.android.sdk.internal.session
 
-import org.matrix.android.sdk.api.failure.GlobalError
 import org.matrix.android.sdk.api.session.Session
 import javax.inject.Inject
 
@@ -36,10 +35,10 @@ internal class SessionListeners @Inject constructor() {
         }
     }
 
-    fun dispatchGlobalError(globalError: GlobalError) {
+    fun dispatch(block: (Session.Listener) -> Unit) {
         synchronized(listeners) {
             listeners.forEach {
-                it.onGlobalError(globalError)
+                block(it)
             }
         }
     }
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 e61e4ecd893270b597253975efa76401423d1c26..de74b3481822893085a9c3ed49e11ac318ba7f43 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
@@ -33,10 +33,12 @@ import org.matrix.android.sdk.api.auth.data.SessionParams
 import org.matrix.android.sdk.api.auth.data.sessionId
 import org.matrix.android.sdk.api.crypto.MXCryptoConfig
 import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.api.session.SessionLifecycleObserver
 import org.matrix.android.sdk.api.session.accountdata.AccountDataService
 import org.matrix.android.sdk.api.session.events.EventService
 import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
 import org.matrix.android.sdk.api.session.initsync.InitialSyncProgressService
+import org.matrix.android.sdk.api.session.openid.OpenIdService
 import org.matrix.android.sdk.api.session.permalinks.PermalinkService
 import org.matrix.android.sdk.api.session.securestorage.SecureStorageService
 import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
@@ -81,6 +83,7 @@ import org.matrix.android.sdk.internal.session.homeserver.DefaultHomeServerCapab
 import org.matrix.android.sdk.internal.session.identity.DefaultIdentityService
 import org.matrix.android.sdk.internal.session.initsync.DefaultInitialSyncProgressService
 import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManager
+import org.matrix.android.sdk.internal.session.openid.DefaultOpenIdService
 import org.matrix.android.sdk.internal.session.permalinks.DefaultPermalinkService
 import org.matrix.android.sdk.internal.session.room.EventRelationsAggregationProcessor
 import org.matrix.android.sdk.internal.session.room.create.RoomCreateEventProcessor
@@ -343,6 +346,10 @@ internal abstract class SessionModule {
     @IntoSet
     abstract fun bindRealmSessionProvider(provider: RealmSessionProvider): SessionLifecycleObserver
 
+    @Binds
+    @IntoSet
+    abstract fun bindSessionCoroutineScopeHolder(holder: SessionCoroutineScopeHolder): SessionLifecycleObserver
+
     @Binds
     @IntoSet
     abstract fun bindEventSenderProcessorAsSessionLifecycleObserver(processor: EventSenderProcessorCoroutine): SessionLifecycleObserver
@@ -368,6 +375,9 @@ internal abstract class SessionModule {
     @Binds
     abstract fun bindPermalinkService(service: DefaultPermalinkService): PermalinkService
 
+    @Binds
+    abstract fun bindOpenIdTokenService(service: DefaultOpenIdService): OpenIdService
+
     @Binds
     abstract fun bindTypingUsersTracker(tracker: DefaultTypingUsersTracker): TypingUsersTracker
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUploadStateTracker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUploadStateTracker.kt
index 754f12bd684831f8d57b390331d4e72e49a92e9d..17e0a930c1424756721fb91f5cac4ee4a1a3b667 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUploadStateTracker.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUploadStateTracker.kt
@@ -78,6 +78,16 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU
         updateState(key, progressData)
     }
 
+    internal fun setCompressingImage(key: String) {
+        val progressData = ContentUploadStateTracker.State.CompressingImage
+        updateState(key, progressData)
+    }
+
+    internal fun setCompressingVideo(key: String, percent: Float) {
+        val progressData = ContentUploadStateTracker.State.CompressingVideo(percent)
+        updateState(key, progressData)
+    }
+
     internal fun setProgress(key: String, current: Long, total: Long) {
         val progressData = ContentUploadStateTracker.State.Uploading(current, total)
         updateState(key, progressData)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt
index 8fa595db3044188d23787eb055db1ab4a514f5d0..6a4dd2639206113062b28cb0fa10fdcc874b9618 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt
@@ -31,24 +31,31 @@ import okhttp3.RequestBody.Companion.toRequestBody
 import okio.BufferedSink
 import okio.source
 import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.api.failure.Failure
+import org.matrix.android.sdk.api.failure.MatrixError
 import org.matrix.android.sdk.api.session.content.ContentUrlResolver
+import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
 import org.matrix.android.sdk.internal.di.Authenticated
 import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
 import org.matrix.android.sdk.internal.network.ProgressRequestBody
 import org.matrix.android.sdk.internal.network.awaitResponse
 import org.matrix.android.sdk.internal.network.toFailure
+import org.matrix.android.sdk.internal.session.homeserver.DefaultHomeServerCapabilitiesService
+import org.matrix.android.sdk.internal.util.TemporaryFileCreator
 import java.io.File
 import java.io.FileNotFoundException
 import java.io.IOException
-import java.util.UUID
 import javax.inject.Inject
 
-internal class FileUploader @Inject constructor(@Authenticated
-                                                private val okHttpClient: OkHttpClient,
-                                                private val globalErrorReceiver: GlobalErrorReceiver,
-                                                private val context: Context,
-                                                contentUrlResolver: ContentUrlResolver,
-                                                moshi: Moshi) {
+internal class FileUploader @Inject constructor(
+        @Authenticated private val okHttpClient: OkHttpClient,
+        private val globalErrorReceiver: GlobalErrorReceiver,
+        private val homeServerCapabilitiesService: DefaultHomeServerCapabilitiesService,
+        private val context: Context,
+        private val temporaryFileCreator: TemporaryFileCreator,
+        contentUrlResolver: ContentUrlResolver,
+        moshi: Moshi
+) {
 
     private val uploadUrl = contentUrlResolver.uploadUrl
     private val responseAdapter = moshi.adapter(ContentUploadResponse::class.java)
@@ -57,6 +64,21 @@ internal class FileUploader @Inject constructor(@Authenticated
                            filename: String?,
                            mimeType: String?,
                            progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
+        // Check size limit
+        val maxUploadFileSize = homeServerCapabilitiesService.getHomeServerCapabilities().maxUploadFileSize
+
+        if (maxUploadFileSize != HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN
+                && file.length() > maxUploadFileSize) {
+            // Known limitation and file too big for the server, save the pain to upload it
+            throw Failure.ServerError(
+                    error = MatrixError(
+                            code = MatrixError.M_TOO_LARGE,
+                            message = "Cannot upload files larger than ${maxUploadFileSize / 1048576L}mb"
+                    ),
+                    httpCode = 413
+            )
+        }
+
         val uploadBody = object : RequestBody() {
             override fun contentLength() = file.length()
 
@@ -90,7 +112,7 @@ internal class FileUploader @Inject constructor(@Authenticated
         val inputStream = withContext(Dispatchers.IO) {
             context.contentResolver.openInputStream(uri)
         } ?: throw FileNotFoundException()
-        val workingFile = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir)
+        val workingFile = temporaryFileCreator.create()
         workingFile.outputStream().use {
             inputStream.copyTo(it)
         }
@@ -99,11 +121,17 @@ internal class FileUploader @Inject constructor(@Authenticated
         }
     }
 
-    private suspend fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): ContentUploadResponse {
+    private suspend fun upload(uploadBody: RequestBody,
+                               filename: String?,
+                               progressListener: ProgressRequestBody.Listener?): ContentUploadResponse {
         val urlBuilder = uploadUrl.toHttpUrlOrNull()?.newBuilder() ?: throw RuntimeException()
 
         val httpUrl = urlBuilder
-                .addQueryParameter("filename", filename)
+                .apply {
+                    if (filename != null) {
+                        addQueryParameter("filename", filename)
+                    }
+                }
                 .build()
 
         val requestBody = if (progressListener != null) ProgressRequestBody(uploadBody, progressListener) else uploadBody
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt
index 1d6cd61060ff6dd7c91d2ab723b3066351f7b4fd..9b01d0a00e033fa6976c79dba71acdcdd0a17cf9 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt
@@ -16,19 +16,20 @@
 
 package org.matrix.android.sdk.internal.session.content
 
-import android.content.Context
 import android.graphics.Bitmap
 import android.graphics.BitmapFactory
 import android.graphics.Matrix
 import androidx.exifinterface.media.ExifInterface
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.withContext
+import org.matrix.android.sdk.internal.util.TemporaryFileCreator
 import timber.log.Timber
 import java.io.File
-import java.util.UUID
 import javax.inject.Inject
 
-internal class ImageCompressor @Inject constructor(private val context: Context) {
+internal class ImageCompressor @Inject constructor(
+        private val temporaryFileCreator: TemporaryFileCreator
+) {
     suspend fun compress(
             imageFile: File,
             desiredWidth: Int,
@@ -45,7 +46,7 @@ internal class ImageCompressor @Inject constructor(private val context: Context)
                 }
             } ?: return@withContext imageFile
 
-            val destinationFile = createDestinationFile()
+            val destinationFile = temporaryFileCreator.create()
 
             runCatching {
                 destinationFile.outputStream().use {
@@ -53,7 +54,7 @@ internal class ImageCompressor @Inject constructor(private val context: Context)
                 }
             }
 
-            return@withContext destinationFile
+            destinationFile
         }
     }
 
@@ -64,16 +65,16 @@ internal class ImageCompressor @Inject constructor(private val context: Context)
                     val orientation = exifInfo.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
                     val matrix = Matrix()
                     when (orientation) {
-                        ExifInterface.ORIENTATION_ROTATE_270      -> matrix.postRotate(270f)
-                        ExifInterface.ORIENTATION_ROTATE_180      -> matrix.postRotate(180f)
-                        ExifInterface.ORIENTATION_ROTATE_90       -> matrix.postRotate(90f)
+                        ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270f)
+                        ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180f)
+                        ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90f)
                         ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.preScale(-1f, 1f)
-                        ExifInterface.ORIENTATION_FLIP_VERTICAL   -> matrix.preScale(1f, -1f)
-                        ExifInterface.ORIENTATION_TRANSPOSE       -> {
+                        ExifInterface.ORIENTATION_FLIP_VERTICAL -> matrix.preScale(1f, -1f)
+                        ExifInterface.ORIENTATION_TRANSPOSE -> {
                             matrix.preRotate(-90f)
                             matrix.preScale(-1f, 1f)
                         }
-                        ExifInterface.ORIENTATION_TRANSVERSE      -> {
+                        ExifInterface.ORIENTATION_TRANSVERSE -> {
                             matrix.preRotate(90f)
                             matrix.preScale(-1f, 1f)
                         }
@@ -116,8 +117,4 @@ internal class ImageCompressor @Inject constructor(private val context: Context)
             null
         }
     }
-
-    private fun createDestinationFile(): File {
-        return File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir)
-    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt
index 3b727690bfddccb54b07fc90c50740c1b90a54b9..06cbf1ba90241f7dd101e071ae92e3c0fbf0639b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt
@@ -18,9 +18,12 @@ package org.matrix.android.sdk.internal.session.content
 
 import android.content.Context
 import android.graphics.BitmapFactory
+import android.media.MediaMetadataRetriever
+import androidx.core.net.toUri
 import androidx.work.WorkerParameters
 import com.squareup.moshi.JsonClass
 import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.api.listeners.ProgressListener
 import org.matrix.android.sdk.api.session.content.ContentAttachmentData
 import org.matrix.android.sdk.api.session.events.model.toContent
 import org.matrix.android.sdk.api.session.events.model.toModel
@@ -41,12 +44,13 @@ import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker
 import org.matrix.android.sdk.internal.session.room.send.LocalEchoIdentifiers
 import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
 import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker
+import org.matrix.android.sdk.internal.util.TemporaryFileCreator
+import org.matrix.android.sdk.internal.util.toMatrixErrorStr
 import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
 import org.matrix.android.sdk.internal.worker.SessionWorkerParams
 import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
 import timber.log.Timber
 import java.io.File
-import java.util.UUID
 import javax.inject.Inject
 
 private data class NewAttachmentAttributes(
@@ -77,7 +81,9 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
     @Inject lateinit var fileService: DefaultFileService
     @Inject lateinit var cancelSendTracker: CancelSendTracker
     @Inject lateinit var imageCompressor: ImageCompressor
+    @Inject lateinit var videoCompressor: VideoCompressor
     @Inject lateinit var localEchoRepository: LocalEchoRepository
+    @Inject lateinit var temporaryFileCreator: TemporaryFileCreator
 
     override fun injectWith(injector: SessionComponent) {
         injector.inject(this)
@@ -109,7 +115,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
         val attachment = params.attachment
         val filesToDelete = mutableListOf<File>()
 
-        try {
+        return try {
             val inputStream = context.contentResolver.openInputStream(attachment.queryUri)
                     ?: return Result.success(
                             WorkerParamsFactory.toData(
@@ -120,7 +126,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
                     )
 
             // always use a temporary file, it guaranties that we could report progress on upload and simplifies the flows
-            val workingFile = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir)
+            val workingFile = temporaryFileCreator.create()
                     .also { filesToDelete.add(it) }
             workingFile.outputStream().use { outputStream ->
                 inputStream.use { inputStream ->
@@ -128,8 +134,6 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
                 }
             }
 
-            val uploadThumbnailResult = dealWithThumbnail(params)
-
             val progressListener = object : ProgressRequestBody.Listener {
                 override fun onProgress(current: Long, total: Long) {
                     notifyTracker(params) {
@@ -144,7 +148,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
 
             var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null
 
-            return try {
+            try {
                 val fileToUpload: File
                 var newAttachmentAttributes = NewAttachmentAttributes(
                         params.attachment.width?.toInt(),
@@ -156,6 +160,8 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
                         // Do not compress gif
                         && attachment.mimeType != MimeTypes.Gif
                         && params.compressBeforeSending) {
+                    notifyTracker(params) { contentUploadStateTracker.setCompressingImage(it) }
+
                     fileToUpload = imageCompressor.compress(workingFile, MAX_IMAGE_SIZE, MAX_IMAGE_SIZE)
                             .also { compressedFile ->
                                 // Get new Bitmap size
@@ -170,6 +176,48 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
                                 }
                             }
                             .also { filesToDelete.add(it) }
+                } else if (attachment.type == ContentAttachmentData.Type.VIDEO
+                        // Do not compress gif
+                        && attachment.mimeType != MimeTypes.Gif
+                        && params.compressBeforeSending) {
+                    fileToUpload = videoCompressor.compress(workingFile, object : ProgressListener {
+                        override fun onProgress(progress: Int, total: Int) {
+                            notifyTracker(params) { contentUploadStateTracker.setCompressingVideo(it, progress.toFloat()) }
+                        }
+                    })
+                            .let { videoCompressionResult ->
+                                when (videoCompressionResult) {
+                                    is VideoCompressionResult.Success           -> {
+                                        val compressedFile = videoCompressionResult.compressedFile
+                                        var compressedWidth: Int? = null
+                                        var compressedHeight: Int? = null
+
+                                        tryOrNull {
+                                            context.contentResolver.openFileDescriptor(compressedFile.toUri(), "r")?.use { pfd ->
+                                                MediaMetadataRetriever().let {
+                                                    it.setDataSource(pfd.fileDescriptor)
+                                                    compressedWidth = it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toInt()
+                                                    compressedHeight = it.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toInt()
+                                                }
+                                            }
+                                        }
+
+                                        // Get new Video file size and dimensions
+                                        newAttachmentAttributes = newAttachmentAttributes.copy(
+                                                newFileSize = compressedFile.length(),
+                                                newWidth = compressedWidth ?: newAttachmentAttributes.newWidth,
+                                                newHeight = compressedHeight ?: newAttachmentAttributes.newHeight
+                                        )
+                                        compressedFile
+                                                .also { filesToDelete.add(it) }
+                                    }
+                                    VideoCompressionResult.CompressionNotNeeded,
+                                    VideoCompressionResult.CompressionCancelled,
+                                    is VideoCompressionResult.CompressionFailed -> {
+                                        workingFile
+                                    }
+                                }
+                            }
                 } else {
                     fileToUpload = workingFile
                     // Fix: OpenableColumns.SIZE may return -1 or 0
@@ -180,9 +228,8 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
 
                 val encryptedFile: File?
                 val contentUploadResponse = if (params.isEncrypted) {
-                    Timber.v("## FileService: Encrypt file")
-
-                    encryptedFile = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir)
+                    Timber.v("## Encrypt file")
+                    encryptedFile = temporaryFileCreator.create()
                             .also { filesToDelete.add(it) }
 
                     uploadedFileEncryptedFileInfo =
@@ -191,19 +238,25 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
                                     contentUploadStateTracker.setEncrypting(it, read.toLong(), total.toLong())
                                 }
                             }
-
-                    Timber.v("## FileService: Uploading file")
-
-                    fileUploader
-                            .uploadFile(encryptedFile, attachment.name, MimeTypes.OctetStream, progressListener)
+                    Timber.v("## Uploading file")
+                    fileUploader.uploadFile(
+                            file = encryptedFile,
+                            filename = null,
+                            mimeType = MimeTypes.OctetStream,
+                            progressListener = progressListener
+                    )
                 } else {
-                    Timber.v("## FileService: Clear file")
+                    Timber.v("## Uploading clear file")
                     encryptedFile = null
-                    fileUploader
-                            .uploadFile(fileToUpload, attachment.name, attachment.getSafeMimeType(), progressListener)
+                    fileUploader.uploadFile(
+                            file = fileToUpload,
+                            filename = attachment.name,
+                            mimeType = attachment.getSafeMimeType(),
+                            progressListener = progressListener
+                    )
                 }
 
-                Timber.v("## FileService: Update cache storage for ${contentUploadResponse.contentUri}")
+                Timber.v("## Update cache storage for ${contentUploadResponse.contentUri}")
                 try {
                     fileService.storeDataFor(
                             mxcUrl = contentUploadResponse.contentUri,
@@ -212,11 +265,13 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
                             originalFile = workingFile,
                             encryptedFile = encryptedFile
                     )
-                    Timber.v("## FileService: cache storage updated")
+                    Timber.v("## cache storage updated")
                 } catch (failure: Throwable) {
-                    Timber.e(failure, "## FileService: Failed to update file cache")
+                    Timber.e(failure, "## Failed to update file cache")
                 }
 
+                val uploadThumbnailResult = dealWithThumbnail(params)
+
                 handleSuccess(params,
                         contentUploadResponse.contentUri,
                         uploadedFileEncryptedFileInfo,
@@ -224,12 +279,12 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
                         uploadThumbnailResult?.uploadedThumbnailEncryptedFileInfo,
                         newAttachmentAttributes)
             } catch (t: Throwable) {
-                Timber.e(t, "## FileService: ERROR ${t.localizedMessage}")
+                Timber.e(t, "## ERROR ${t.localizedMessage}")
                 handleFailure(params, t)
             }
         } catch (e: Exception) {
-            Timber.e(e, "## FileService: ERROR")
-            return handleFailure(params, e)
+            Timber.e(e, "## ERROR")
+            handleFailure(params, e)
         } finally {
             // Delete all temporary files
             filesToDelete.forEach {
@@ -260,19 +315,23 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
                             Timber.v("Encrypt thumbnail")
                             notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) }
                             val encryptionResult = MXEncryptedAttachments.encryptAttachment(thumbnailData.bytes.inputStream(), thumbnailData.mimeType)
-                            val contentUploadResponse = fileUploader.uploadByteArray(encryptionResult.encryptedByteArray,
-                                    "thumb_${params.attachment.name}",
-                                    MimeTypes.OctetStream,
-                                    thumbnailProgressListener)
+                            val contentUploadResponse = fileUploader.uploadByteArray(
+                                    byteArray = encryptionResult.encryptedByteArray,
+                                    filename = null,
+                                    mimeType = MimeTypes.OctetStream,
+                                    progressListener = thumbnailProgressListener
+                            )
                             UploadThumbnailResult(
                                     contentUploadResponse.contentUri,
                                     encryptionResult.encryptedFileInfo
                             )
                         } else {
-                            val contentUploadResponse = fileUploader.uploadByteArray(thumbnailData.bytes,
-                                    "thumb_${params.attachment.name}",
-                                    thumbnailData.mimeType,
-                                    thumbnailProgressListener)
+                            val contentUploadResponse = fileUploader.uploadByteArray(
+                                    byteArray = thumbnailData.bytes,
+                                    filename = "thumb_${params.attachment.name}",
+                                    mimeType = thumbnailData.mimeType,
+                                    progressListener = thumbnailProgressListener
+                            )
                             UploadThumbnailResult(
                                     contentUploadResponse.contentUri,
                                     null
@@ -291,7 +350,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
         return Result.success(
                 WorkerParamsFactory.toData(
                         params.copy(
-                                lastFailureMessage = failure.localizedMessage
+                                lastFailureMessage = failure.toMatrixErrorStr()
                         )
                 )
         )
@@ -328,8 +387,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
             val messageContent: MessageContent? = event.asDomain().content.toModel()
             val updatedContent = when (messageContent) {
                 is MessageImageContent -> messageContent.update(url, encryptedFileInfo, newAttachmentAttributes)
-                is MessageVideoContent -> messageContent.update(url, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo,
-                        newAttachmentAttributes.newFileSize)
+                is MessageVideoContent -> messageContent.update(url, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo, newAttachmentAttributes)
                 is MessageFileContent  -> messageContent.update(url, encryptedFileInfo, newAttachmentAttributes.newFileSize)
                 is MessageAudioContent -> messageContent.update(url, encryptedFileInfo, newAttachmentAttributes.newFileSize)
                 else                   -> messageContent
@@ -351,7 +409,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
                 info = info?.copy(
                         width = newAttachmentAttributes?.newWidth ?: info.width,
                         height = newAttachmentAttributes?.newHeight ?: info.height,
-                        size = newAttachmentAttributes?.newFileSize?.toInt() ?: info.size
+                        size = newAttachmentAttributes?.newFileSize ?: info.size
                 )
         )
     }
@@ -360,14 +418,16 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
                                            encryptedFileInfo: EncryptedFileInfo?,
                                            thumbnailUrl: String?,
                                            thumbnailEncryptedFileInfo: EncryptedFileInfo?,
-                                           size: Long): MessageVideoContent {
+                                           newAttachmentAttributes: NewAttachmentAttributes?): MessageVideoContent {
         return copy(
                 url = if (encryptedFileInfo == null) url else null,
                 encryptedFileInfo = encryptedFileInfo?.copy(url = url),
                 videoInfo = videoInfo?.copy(
                         thumbnailUrl = if (thumbnailEncryptedFileInfo == null) thumbnailUrl else null,
                         thumbnailFile = thumbnailEncryptedFileInfo?.copy(url = thumbnailUrl),
-                        size = size
+                        width = newAttachmentAttributes?.newWidth ?: videoInfo.width,
+                        height = newAttachmentAttributes?.newHeight ?: videoInfo.height,
+                        size = newAttachmentAttributes?.newFileSize ?: videoInfo.size
                 )
         )
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressionResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressionResult.kt
new file mode 100644
index 0000000000000000000000000000000000000000..87d5c7e6a3ebbb2a8aec1dca8b81a170d08e0358
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressionResult.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.content
+
+import java.io.File
+
+internal sealed class VideoCompressionResult {
+    data class Success(val compressedFile: File) : VideoCompressionResult()
+    object CompressionNotNeeded : VideoCompressionResult()
+    object CompressionCancelled : VideoCompressionResult()
+    data class CompressionFailed(val failure: Throwable) : VideoCompressionResult()
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressor.kt
new file mode 100644
index 0000000000000000000000000000000000000000..05aaf4e9f16066686c43071f3195480b06fd5b1d
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressor.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.content
+
+import com.otaliastudios.transcoder.Transcoder
+import com.otaliastudios.transcoder.TranscoderListener
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.withContext
+import org.matrix.android.sdk.api.listeners.ProgressListener
+import org.matrix.android.sdk.internal.util.TemporaryFileCreator
+import timber.log.Timber
+import java.io.File
+import javax.inject.Inject
+
+internal class VideoCompressor @Inject constructor(
+        private val temporaryFileCreator: TemporaryFileCreator
+) {
+
+    suspend fun compress(videoFile: File,
+                         progressListener: ProgressListener?): VideoCompressionResult {
+        val destinationFile = temporaryFileCreator.create()
+
+        val job = Job()
+
+        Timber.d("Compressing: start")
+        progressListener?.onProgress(0, 100)
+
+        var result: Int = -1
+        var failure: Throwable? = null
+        Transcoder.into(destinationFile.path)
+                .addDataSource(videoFile.path)
+                .setListener(object : TranscoderListener {
+                    override fun onTranscodeProgress(progress: Double) {
+                        Timber.d("Compressing: $progress%")
+                        progressListener?.onProgress((progress * 100).toInt(), 100)
+                    }
+
+                    override fun onTranscodeCompleted(successCode: Int) {
+                        Timber.d("Compressing: success: $successCode")
+                        result = successCode
+                        job.complete()
+                    }
+
+                    override fun onTranscodeCanceled() {
+                        Timber.d("Compressing: cancel")
+                        job.cancel()
+                    }
+
+                    override fun onTranscodeFailed(exception: Throwable) {
+                        Timber.w(exception, "Compressing: failure")
+                        failure = exception
+                        job.completeExceptionally(exception)
+                    }
+                })
+                .transcode()
+
+        job.join()
+
+        // Note: job is also cancelled if completeExceptionally() was called
+        if (job.isCancelled) {
+            // Delete now the temporary file
+            deleteFile(destinationFile)
+            return when (val finalFailure = failure) {
+                null -> {
+                    // We do not throw a CancellationException, because it's not critical, we will try to send the original file
+                    // Anyway this should never occurs, since we never cancel the return value of transcode()
+                    Timber.w("Compressing: A failure occurred")
+                    VideoCompressionResult.CompressionCancelled
+                }
+                else -> {
+                    // Compression failure can also be considered as not critical, but let the caller decide
+                    Timber.w("Compressing: Job cancelled")
+                    VideoCompressionResult.CompressionFailed(finalFailure)
+                }
+            }
+        }
+
+        progressListener?.onProgress(100, 100)
+
+        return when (result) {
+            Transcoder.SUCCESS_TRANSCODED -> {
+                VideoCompressionResult.Success(destinationFile)
+            }
+            Transcoder.SUCCESS_NOT_NEEDED -> {
+                // Delete now the temporary file
+                deleteFile(destinationFile)
+                VideoCompressionResult.CompressionNotNeeded
+            }
+            else                          -> {
+                // Should not happen...
+                // Delete now the temporary file
+                deleteFile(destinationFile)
+                Timber.w("Unknown result: $result")
+                VideoCompressionResult.CompressionFailed(IllegalStateException("Unknown result: $result"))
+            }
+        }
+    }
+
+    private suspend fun deleteFile(file: File) {
+        withContext(Dispatchers.IO) {
+            file.delete()
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt
index f5391d6cdb113bcc3b3d1a244aabfca9d2eff3f4..475781ef017a4eda68f64f12ad6ec96620f074e7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt
@@ -36,7 +36,7 @@ import org.matrix.android.sdk.internal.di.AuthenticatedIdentity
 import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate
 import org.matrix.android.sdk.internal.extensions.observeNotNull
 import org.matrix.android.sdk.internal.network.RetrofitFactory
-import org.matrix.android.sdk.internal.session.SessionLifecycleObserver
+import org.matrix.android.sdk.api.session.SessionLifecycleObserver
 import org.matrix.android.sdk.internal.session.SessionScope
 import org.matrix.android.sdk.internal.session.identity.data.IdentityStore
 import org.matrix.android.sdk.internal.session.openid.GetOpenIdTokenTask
@@ -51,6 +51,7 @@ import org.matrix.android.sdk.internal.util.ensureProtocol
 import kotlinx.coroutines.withContext
 import okhttp3.OkHttpClient
 import org.matrix.android.sdk.api.extensions.orFalse
+import org.matrix.android.sdk.api.session.Session
 import timber.log.Timber
 import javax.inject.Inject
 import javax.net.ssl.HttpsURLConnection
@@ -86,7 +87,7 @@ internal class DefaultIdentityService @Inject constructor(
 
     private val listeners = mutableSetOf<IdentityServiceListener>()
 
-    override fun onSessionStarted() {
+    override fun onSessionStarted(session: Session) {
         lifecycleRegistry.currentState = Lifecycle.State.STARTED
         // Observe the account data change
         accountDataDataSource
@@ -111,7 +112,7 @@ internal class DefaultIdentityService @Inject constructor(
         }
     }
 
-    override fun onSessionStopped() {
+    override fun onSessionStopped(session: Session) {
         lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAuthAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAuthAPI.kt
index 1671859585f1d35cff1f218d1df1d69dae8ff8ae..9d990d4d8f9340a91bbb07dd8a792c8eb216a4ef 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAuthAPI.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityAuthAPI.kt
@@ -16,9 +16,9 @@
 
 package org.matrix.android.sdk.internal.session.identity
 
+import org.matrix.android.sdk.api.session.openid.OpenIdToken
 import org.matrix.android.sdk.internal.network.NetworkConstants
 import org.matrix.android.sdk.internal.session.identity.model.IdentityRegisterResponse
-import org.matrix.android.sdk.internal.session.openid.RequestOpenIdTokenResponse
 import retrofit2.http.Body
 import retrofit2.http.GET
 import retrofit2.http.POST
@@ -52,5 +52,5 @@ internal interface IdentityAuthAPI {
      * The request body is the same as the values returned by /openid/request_token in the Client-Server API.
      */
     @POST(NetworkConstants.URI_IDENTITY_PATH_V2 + "account/register")
-    suspend fun register(@Body openIdToken: RequestOpenIdTokenResponse): IdentityRegisterResponse
+    suspend fun register(@Body openIdToken: OpenIdToken): IdentityRegisterResponse
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt
index 4f6e906766801806c67a0d6cc154b1d25fff4828..114695062c867b107589a2bf91c16bc6c045f05a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt
@@ -117,7 +117,7 @@ internal class DefaultIdentityBulkLookupTask @Inject constructor(
         return withOlmUtility { olmUtility ->
             threePids.map { threePid ->
                 base64ToBase64Url(
-                        olmUtility.sha256(threePid.value.toLowerCase(Locale.ROOT)
+                        olmUtility.sha256(threePid.value.lowercase(Locale.ROOT)
                                 + " " + threePid.toMedium() + " " + pepper)
                 )
             }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRegisterTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRegisterTask.kt
index 8cc854bd940daa821d0f785db5fb31375aafa547..1800d0eebe2cc76280ac6a9ba58b4f710188500c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRegisterTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityRegisterTask.kt
@@ -16,16 +16,16 @@
 
 package org.matrix.android.sdk.internal.session.identity
 
+import org.matrix.android.sdk.api.session.openid.OpenIdToken
 import org.matrix.android.sdk.internal.network.executeRequest
 import org.matrix.android.sdk.internal.session.identity.model.IdentityRegisterResponse
-import org.matrix.android.sdk.internal.session.openid.RequestOpenIdTokenResponse
 import org.matrix.android.sdk.internal.task.Task
 import javax.inject.Inject
 
 internal interface IdentityRegisterTask : Task<IdentityRegisterTask.Params, IdentityRegisterResponse> {
     data class Params(
             val identityAuthAPI: IdentityAuthAPI,
-            val openIdTokenResponse: RequestOpenIdTokenResponse
+            val openIdToken: OpenIdToken
     )
 }
 
@@ -33,7 +33,7 @@ internal class DefaultIdentityRegisterTask @Inject constructor() : IdentityRegis
 
     override suspend fun execute(params: IdentityRegisterTask.Params): IdentityRegisterResponse {
         return executeRequest(null) {
-            params.identityAuthAPI.register(params.openIdTokenResponse)
+            params.identityAuthAPI.register(params.openIdToken)
         }
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManager.kt
index e34615d269dd8e097115f45ba8c3893908dace55..3df9a00cc15fa494a95945c5ccd8b85889a98740 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManager.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/integrationmanager/IntegrationManager.kt
@@ -21,6 +21,7 @@ import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.LifecycleRegistry
 import com.zhuinden.monarchy.Monarchy
 import org.matrix.android.sdk.api.MatrixConfiguration
+import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerConfig
 import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService
@@ -29,7 +30,7 @@ import org.matrix.android.sdk.api.session.widgets.model.WidgetType
 import org.matrix.android.sdk.internal.database.model.WellknownIntegrationManagerConfigEntity
 import org.matrix.android.sdk.internal.di.SessionDatabase
 import org.matrix.android.sdk.internal.extensions.observeNotNull
-import org.matrix.android.sdk.internal.session.SessionLifecycleObserver
+import org.matrix.android.sdk.api.session.SessionLifecycleObserver
 import org.matrix.android.sdk.internal.session.SessionScope
 import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
 import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
@@ -77,7 +78,7 @@ internal class IntegrationManager @Inject constructor(matrixConfiguration: Matri
         currentConfigs.add(defaultConfig)
     }
 
-    override fun onSessionStarted() {
+    override fun onSessionStarted(session: Session) {
         lifecycleRegistry.currentState = Lifecycle.State.STARTED
         observeWellknownConfig()
         accountDataDataSource
@@ -105,7 +106,7 @@ internal class IntegrationManager @Inject constructor(matrixConfiguration: Matri
                 }
     }
 
-    override fun onSessionStopped() {
+    override fun onSessionStopped(session: Session) {
         lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/DefaultOpenIdService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/DefaultOpenIdService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b90a2435f7d1c5ff6378ffd1621febcf48d9e9ae
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/DefaultOpenIdService.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.openid
+
+import org.matrix.android.sdk.api.session.openid.OpenIdService
+import org.matrix.android.sdk.api.session.openid.OpenIdToken
+import javax.inject.Inject
+
+internal class DefaultOpenIdService @Inject constructor(private val getOpenIdTokenTask: GetOpenIdTokenTask): OpenIdService {
+
+    override suspend fun getOpenIdToken(): OpenIdToken {
+        return getOpenIdTokenTask.execute(Unit)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/GetOpenIdTokenTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/GetOpenIdTokenTask.kt
index 8481a6ab93d6eaec9e1af4b454c8f86746b6d11d..a6ad025b8d769301d58ede89a545daf15410c2ec 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/GetOpenIdTokenTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/GetOpenIdTokenTask.kt
@@ -16,20 +16,21 @@
 
 package org.matrix.android.sdk.internal.session.openid
 
+import org.matrix.android.sdk.api.session.openid.OpenIdToken
 import org.matrix.android.sdk.internal.di.UserId
 import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
 import org.matrix.android.sdk.internal.network.executeRequest
 import org.matrix.android.sdk.internal.task.Task
 import javax.inject.Inject
 
-internal interface GetOpenIdTokenTask : Task<Unit, RequestOpenIdTokenResponse>
+internal interface GetOpenIdTokenTask : Task<Unit, OpenIdToken>
 
 internal class DefaultGetOpenIdTokenTask @Inject constructor(
         @UserId private val userId: String,
         private val openIdAPI: OpenIdAPI,
         private val globalErrorReceiver: GlobalErrorReceiver) : GetOpenIdTokenTask {
 
-    override suspend fun execute(params: Unit): RequestOpenIdTokenResponse {
+    override suspend fun execute(params: Unit): OpenIdToken {
         return executeRequest(globalErrorReceiver) {
             openIdAPI.openIdToken(userId)
         }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdAPI.kt
index ed090b845d02888f912ede1657b20bab8ce540c9..eb8c841d5771afb2c656d3a962842429628e3508 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdAPI.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/openid/OpenIdAPI.kt
@@ -16,6 +16,7 @@
 
 package org.matrix.android.sdk.internal.session.openid
 
+import org.matrix.android.sdk.api.session.openid.OpenIdToken
 import org.matrix.android.sdk.api.util.JsonDict
 import org.matrix.android.sdk.internal.network.NetworkConstants
 import retrofit2.http.Body
@@ -34,5 +35,5 @@ internal interface OpenIdAPI {
      */
     @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/openid/request_token")
     suspend fun openIdToken(@Path("userId") userId: String,
-                            @Body body: JsonDict = emptyMap()): RequestOpenIdTokenResponse
+                            @Body body: JsonDict = emptyMap()): OpenIdToken
 }
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 71a6e224bfdbe6ac837573f17f8517c4a9ec9166..970752449aa04e00960327939be32ba09d4f2225 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
@@ -18,19 +18,13 @@ package org.matrix.android.sdk.internal.session.permalinks
 
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.permalinks.PermalinkService.Companion.MATRIX_TO_URL_BASE
-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.internal.di.UserId
-import org.matrix.android.sdk.internal.session.room.RoomGetter
-import java.net.URLEncoder
 import javax.inject.Inject
-import javax.inject.Provider
 
 internal class PermalinkFactory @Inject constructor(
         @UserId
         private val userId: String,
-        // Use a provider to fix circular Dagger dependency
-        private val roomGetterProvider: Provider<RoomGetter>
+        private val viaParameterFinder: ViaParameterFinder
 ) {
 
     fun createPermalink(event: Event): String? {
@@ -50,12 +44,12 @@ internal class PermalinkFactory @Inject constructor(
         return if (roomId.isEmpty()) {
             null
         } else {
-            MATRIX_TO_URL_BASE + escape(roomId) + computeViaParams(userId, roomId)
+            MATRIX_TO_URL_BASE + escape(roomId) + viaParameterFinder.computeViaParams(userId, roomId)
         }
     }
 
     fun createPermalink(roomId: String, eventId: String): String {
-        return MATRIX_TO_URL_BASE + escape(roomId) + "/" + escape(eventId) + computeViaParams(userId, roomId)
+        return MATRIX_TO_URL_BASE + escape(roomId) + "/" + escape(eventId) + viaParameterFinder.computeViaParams(userId, roomId)
     }
 
     fun getLinkedId(url: String): String? {
@@ -66,25 +60,6 @@ internal class PermalinkFactory @Inject constructor(
         } else null
     }
 
-    /**
-     * Compute the via parameters.
-     * Take up to 3 homeserver domains, taking the most representative one regarding room members and including the
-     * current user one.
-     */
-    private fun computeViaParams(userId: String, roomId: String): String {
-        val userHomeserver = userId.substringAfter(":")
-        return getUserIdsOfJoinedMembers(roomId)
-                .map { it.substringAfter(":") }
-                .groupBy { it }
-                .mapValues { it.value.size }
-                .toMutableMap()
-                // Ensure the user homeserver will be included
-                .apply { this[userHomeserver] = Int.MAX_VALUE }
-                .let { map -> map.keys.sortedByDescending { map[it] } }
-                .take(3)
-                .joinToString(prefix = "?via=", separator = "&via=") { URLEncoder.encode(it, "utf-8") }
-    }
-
     /**
      * Escape '/' in id, because it is used as a separator
      *
@@ -104,15 +79,4 @@ internal class PermalinkFactory @Inject constructor(
     private fun unescape(id: String): String {
         return id.replace("%2F", "/")
     }
-
-    /**
-     * Get a set of userIds of joined members of a room
-     */
-    private fun getUserIdsOfJoinedMembers(roomId: String): Set<String> {
-        return roomGetterProvider.get().getRoom(roomId)
-                ?.getRoomMembers(roomMemberQueryParams { memberships = listOf(Membership.JOIN) })
-                ?.map { it.userId }
-                .orEmpty()
-                .toSet()
-    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/ViaParameterFinder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/ViaParameterFinder.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0da60e9ba2affa030270720565911884ab7ab36d
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/ViaParameterFinder.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.permalinks
+
+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.internal.di.UserId
+import org.matrix.android.sdk.internal.session.room.RoomGetter
+import java.net.URLEncoder
+import javax.inject.Inject
+import javax.inject.Provider
+
+internal class ViaParameterFinder @Inject constructor(
+        @UserId private val userId: String,
+        private val roomGetterProvider: Provider<RoomGetter>
+) {
+
+    fun computeViaParams(roomId: String, max: Int): List<String> {
+        return computeViaParams(userId, roomId, max)
+    }
+
+    /**
+     * Compute the via parameters.
+     * Take up to 3 homeserver domains, taking the most representative one regarding room members and including the
+     * current user one.
+     */
+    fun computeViaParams(userId: String, roomId: String): String {
+        return computeViaParams(userId, roomId, 3)
+                .joinToString(prefix = "?via=", separator = "&via=") { URLEncoder.encode(it, "utf-8") }
+    }
+
+    fun computeViaParams(userId: String, roomId: String, max: Int): List<String> {
+        val userHomeserver = userId.substringAfter(":")
+        return getUserIdsOfJoinedMembers(roomId)
+                .map { it.substringAfter(":") }
+                .groupBy { it }
+                .mapValues { it.value.size }
+                .toMutableMap()
+                // Ensure the user homeserver will be included
+                .apply { this[userHomeserver] = Int.MAX_VALUE }
+                .let { map -> map.keys.sortedByDescending { map[it] } }
+                .take(max)
+    }
+
+    /**
+     * Get a set of userIds of joined members of a room
+     */
+    private fun getUserIdsOfJoinedMembers(roomId: String): Set<String> {
+        return roomGetterProvider.get().getRoom(roomId)
+                ?.getRoomMembers(roomMemberQueryParams { memberships = listOf(Membership.JOIN) })
+                ?.map { it.userId }
+                .orEmpty()
+                .toSet()
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileAPI.kt
index 5113b821e8726764e8530b112f342b7c5edf0a24..485a4973cab0f838e566ff57f3ee4092bd0ad769 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileAPI.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileAPI.kt
@@ -33,6 +33,7 @@ internal interface ProfileAPI {
      * Get the combined profile information for this user.
      * This API may be used to fetch the user's own profile information or other users; either locally or on remote homeservers.
      * This API may return keys which are not limited to displayname or avatar_url.
+     * If server is configured as limit_profile_requests_to_users_who_share_rooms: true then response can be HTTP 403.
      * @param userId the user id to fetch profile info
      */
     @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "profile/{userId}")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt
index 1d8eb6c95ea7d2628b15ed3ecb86e9f8517eb10b..a5e066dae80331f7c8601fcde66af626d69bedab 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt
@@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.session.room.alias.AliasService
 import org.matrix.android.sdk.api.session.room.call.RoomCallService
 import org.matrix.android.sdk.api.session.room.members.MembershipService
 import org.matrix.android.sdk.api.session.room.model.RoomSummary
+import org.matrix.android.sdk.api.session.room.model.RoomType
 import org.matrix.android.sdk.api.session.room.model.relation.RelationService
 import org.matrix.android.sdk.api.session.room.notification.RoomPushRuleService
 import org.matrix.android.sdk.api.session.room.read.ReadService
@@ -36,11 +37,14 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineService
 import org.matrix.android.sdk.api.session.room.typing.TypingService
 import org.matrix.android.sdk.api.session.room.uploads.UploadsService
 import org.matrix.android.sdk.api.session.search.SearchResult
+import org.matrix.android.sdk.api.session.space.Space
 import org.matrix.android.sdk.api.util.Optional
 import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
+import org.matrix.android.sdk.internal.session.permalinks.ViaParameterFinder
 import org.matrix.android.sdk.internal.session.room.state.SendStateTask
 import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
 import org.matrix.android.sdk.internal.session.search.SearchTask
+import org.matrix.android.sdk.internal.session.space.DefaultSpace
 import org.matrix.android.sdk.internal.util.awaitCallback
 import java.security.InvalidParameterException
 import javax.inject.Inject
@@ -63,6 +67,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
                                                private val roomMembersService: MembershipService,
                                                private val roomPushRuleService: RoomPushRuleService,
                                                private val sendStateTask: SendStateTask,
+                                               private val viaParameterFinder: ViaParameterFinder,
                                                private val searchTask: SearchTask) :
         Room,
         TimelineService by timelineService,
@@ -148,4 +153,9 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
                 )
         )
     }
+
+    override fun asSpace(): Space? {
+        if (roomSummary()?.roomType != RoomType.SPACE) return null
+        return DefaultSpace(this, roomSummaryDataSource, viaParameterFinder)
+    }
 }
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 bd63ba480eb8e3c29f6491dda18ed54b29dff56a..d9fe1288e29d0a538321ed390d9125773d3945e2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomService.kt
@@ -20,19 +20,19 @@ import androidx.lifecycle.LiveData
 import androidx.lifecycle.Transformations
 import androidx.paging.PagedList
 import com.zhuinden.monarchy.Monarchy
-import org.matrix.android.sdk.api.MatrixCallback
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.room.Room
 import org.matrix.android.sdk.api.session.room.RoomService
+import org.matrix.android.sdk.api.session.room.RoomSortOrder
 import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
-import org.matrix.android.sdk.api.session.room.UpdatableFilterLivePageResult
+import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult
 import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
+import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
 import org.matrix.android.sdk.api.session.room.model.RoomSummary
 import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
 import org.matrix.android.sdk.api.session.room.peeking.PeekResult
 import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
-import org.matrix.android.sdk.api.util.Cancelable
 import org.matrix.android.sdk.api.util.Optional
 import org.matrix.android.sdk.api.util.toOptional
 import org.matrix.android.sdk.internal.database.mapper.asDomain
@@ -50,8 +50,6 @@ import org.matrix.android.sdk.internal.session.room.peeking.ResolveRoomStateTask
 import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask
 import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
 import org.matrix.android.sdk.internal.session.user.accountdata.UpdateBreadcrumbsTask
-import org.matrix.android.sdk.internal.task.TaskExecutor
-import org.matrix.android.sdk.internal.task.configureWith
 import org.matrix.android.sdk.internal.util.fetchCopied
 import javax.inject.Inject
 
@@ -67,16 +65,11 @@ internal class DefaultRoomService @Inject constructor(
         private val peekRoomTask: PeekRoomTask,
         private val roomGetter: RoomGetter,
         private val roomSummaryDataSource: RoomSummaryDataSource,
-        private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
-        private val taskExecutor: TaskExecutor
+        private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource
 ) : RoomService {
 
-    override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback<String>): Cancelable {
-        return createRoomTask
-                .configureWith(createRoomParams) {
-                    this.callback = callback
-                }
-                .executeBy(taskExecutor)
+    override suspend fun createRoom(createRoomParams: CreateRoomParams): String {
+        return createRoomTask.executeRetry(createRoomParams, 3)
     }
 
     override fun getRoom(roomId: String): Room? {
@@ -99,14 +92,14 @@ internal class DefaultRoomService @Inject constructor(
         return roomSummaryDataSource.getRoomSummariesLive(queryParams)
     }
 
-    override fun getPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams, pagedListConfig: PagedList.Config)
+    override fun getPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams, pagedListConfig: PagedList.Config, sortOrder: RoomSortOrder)
             : LiveData<PagedList<RoomSummary>> {
-        return roomSummaryDataSource.getSortedPagedRoomSummariesLive(queryParams, pagedListConfig)
+        return roomSummaryDataSource.getSortedPagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder)
     }
 
-    override fun getFilteredPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams, pagedListConfig: PagedList.Config)
-            : UpdatableFilterLivePageResult {
-        return roomSummaryDataSource.getFilteredPagedRoomSummariesLive(queryParams, pagedListConfig)
+    override fun getFilteredPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams, pagedListConfig: PagedList.Config, sortOrder: RoomSortOrder)
+            : UpdatableLivePageResult {
+        return roomSummaryDataSource.getUpdatablePagedRoomSummariesLive(queryParams, pagedListConfig, sortOrder)
     }
 
     override fun getNotificationCountForRooms(queryParams: RoomSummaryQueryParams): RoomAggregateNotificationCount {
@@ -121,34 +114,20 @@ internal class DefaultRoomService @Inject constructor(
         return roomSummaryDataSource.getBreadcrumbsLive(queryParams)
     }
 
-    override fun onRoomDisplayed(roomId: String): Cancelable {
-        return updateBreadcrumbsTask
-                .configureWith(UpdateBreadcrumbsTask.Params(roomId))
-                .executeBy(taskExecutor)
+    override suspend fun onRoomDisplayed(roomId: String) {
+        updateBreadcrumbsTask.execute(UpdateBreadcrumbsTask.Params(roomId))
     }
 
-    override fun joinRoom(roomIdOrAlias: String, reason: String?, viaServers: List<String>, callback: MatrixCallback<Unit>): Cancelable {
-        return joinRoomTask
-                .configureWith(JoinRoomTask.Params(roomIdOrAlias, reason, viaServers)) {
-                    this.callback = callback
-                }
-                .executeBy(taskExecutor)
+    override suspend fun joinRoom(roomIdOrAlias: String, reason: String?, viaServers: List<String>) {
+        joinRoomTask.execute(JoinRoomTask.Params(roomIdOrAlias, reason, viaServers))
     }
 
-    override fun markAllAsRead(roomIds: List<String>, callback: MatrixCallback<Unit>): Cancelable {
-        return markAllRoomsReadTask
-                .configureWith(MarkAllRoomsReadTask.Params(roomIds)) {
-                    this.callback = callback
-                }
-                .executeBy(taskExecutor)
+    override suspend fun markAllAsRead(roomIds: List<String>) {
+        markAllRoomsReadTask.execute(MarkAllRoomsReadTask.Params(roomIds))
     }
 
-    override fun getRoomIdByAlias(roomAlias: String, searchOnServer: Boolean, callback: MatrixCallback<Optional<RoomAliasDescription>>): Cancelable {
-        return roomIdByAliasTask
-                .configureWith(GetRoomIdByAliasTask.Params(roomAlias, searchOnServer)) {
-                    this.callback = callback
-                }
-                .executeBy(taskExecutor)
+    override suspend fun getRoomIdByAlias(roomAlias: String, searchOnServer: Boolean): Optional<RoomAliasDescription> {
+        return roomIdByAliasTask.execute(GetRoomIdByAliasTask.Params(roomAlias, searchOnServer))
     }
 
     override suspend fun deleteRoomAlias(roomAlias: String) {
@@ -179,19 +158,25 @@ internal class DefaultRoomService @Inject constructor(
         }
     }
 
-    override fun getRoomState(roomId: String, callback: MatrixCallback<List<Event>>) {
-        resolveRoomStateTask
-                .configureWith(ResolveRoomStateTask.Params(roomId)) {
-                    this.callback = callback
-                }
-                .executeBy(taskExecutor)
+    override suspend fun getRoomState(roomId: String): List<Event> {
+        return resolveRoomStateTask.execute(ResolveRoomStateTask.Params(roomId))
     }
 
-    override fun peekRoom(roomIdOrAlias: String, callback: MatrixCallback<PeekResult>) {
-        peekRoomTask
-                .configureWith(PeekRoomTask.Params(roomIdOrAlias)) {
-                    this.callback = callback
-                }
-                .executeBy(taskExecutor)
+    override suspend fun peekRoom(roomIdOrAlias: String): PeekResult {
+        return peekRoomTask.execute(PeekRoomTask.Params(roomIdOrAlias))
+    }
+
+    override fun getFlattenRoomSummaryChildrenOf(spaceId: String?, memberships: List<Membership>): List<RoomSummary> {
+        if (spaceId == null) {
+            return roomSummaryDataSource.getFlattenOrphanRooms()
+        }
+        return roomSummaryDataSource.getAllRoomSummaryChildOf(spaceId, memberships)
+    }
+
+    override fun getFlattenRoomSummaryChildrenOfLive(spaceId: String?, memberships: List<Membership>): LiveData<List<RoomSummary>> {
+        if (spaceId == null) {
+            return roomSummaryDataSource.getFlattenOrphanRoomsLive()
+        }
+        return roomSummaryDataSource.getAllRoomSummaryChildOfLive(spaceId, memberships)
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt
index 90640b47000dbfa674b9a0d793c1ae6ed39a35af..3f743c29229d4977adc881ca4ed0f80ba068443b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt
@@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.room
 import org.matrix.android.sdk.api.session.crypto.CryptoService
 import org.matrix.android.sdk.api.session.room.Room
 import org.matrix.android.sdk.internal.session.SessionScope
+import org.matrix.android.sdk.internal.session.permalinks.ViaParameterFinder
 import org.matrix.android.sdk.internal.session.room.alias.DefaultAliasService
 import org.matrix.android.sdk.internal.session.room.call.DefaultRoomCallService
 import org.matrix.android.sdk.internal.session.room.draft.DefaultDraftService
@@ -60,6 +61,7 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService:
                                                       private val membershipServiceFactory: DefaultMembershipService.Factory,
                                                       private val roomPushRuleServiceFactory: DefaultRoomPushRuleService.Factory,
                                                       private val sendStateTask: SendStateTask,
+                                                      private val viaParameterFinder: ViaParameterFinder,
                                                       private val searchTask: SearchTask) :
         RoomFactory {
 
@@ -83,7 +85,8 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService:
                 roomMembersService = membershipServiceFactory.create(roomId),
                 roomPushRuleService = roomPushRuleServiceFactory.create(roomId),
                 sendStateTask = sendStateTask,
-                searchTask = searchTask
+                searchTask = searchTask,
+                viaParameterFinder = viaParameterFinder
         )
     }
 }
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 5133f72932b2fe3d0ffe05009bb0cc9a2eea7c7a..8f3445bec384ea67ad43366f10dd1e968ec2c8dc 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
@@ -24,6 +24,7 @@ import org.commonmark.renderer.html.HtmlRenderer
 import org.matrix.android.sdk.api.session.file.FileService
 import org.matrix.android.sdk.api.session.room.RoomDirectoryService
 import org.matrix.android.sdk.api.session.room.RoomService
+import org.matrix.android.sdk.api.session.space.SpaceService
 import org.matrix.android.sdk.internal.session.DefaultFileService
 import org.matrix.android.sdk.internal.session.SessionScope
 import org.matrix.android.sdk.internal.session.directory.DirectoryAPI
@@ -89,6 +90,7 @@ import org.matrix.android.sdk.internal.session.room.typing.DefaultSendTypingTask
 import org.matrix.android.sdk.internal.session.room.typing.SendTypingTask
 import org.matrix.android.sdk.internal.session.room.uploads.DefaultGetUploadsTask
 import org.matrix.android.sdk.internal.session.room.uploads.GetUploadsTask
+import org.matrix.android.sdk.internal.session.space.DefaultSpaceService
 import retrofit2.Retrofit
 
 @Module
@@ -135,6 +137,9 @@ internal abstract class RoomModule {
     @Binds
     abstract fun bindRoomService(service: DefaultRoomService): RoomService
 
+    @Binds
+    abstract fun bindSpaceService(service: DefaultSpaceService): SpaceService
+
     @Binds
     abstract fun bindRoomDirectoryService(service: DefaultRoomDirectoryService): RoomDirectoryService
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/SpaceGetter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/SpaceGetter.kt
new file mode 100644
index 0000000000000000000000000000000000000000..fed3ff542b7e6baf4de0dcebe89f6aaa036fa475
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/SpaceGetter.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.room
+
+import org.matrix.android.sdk.api.session.space.Space
+import javax.inject.Inject
+
+internal interface SpaceGetter {
+    fun get(spaceId: String): Space?
+}
+
+internal class DefaultSpaceGetter @Inject constructor(
+        private val roomGetter: RoomGetter
+) : SpaceGetter {
+
+    override fun get(spaceId: String): Space? {
+        return roomGetter.getRoom(spaceId)?.asSpace()
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasAvailabilityChecker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasAvailabilityChecker.kt
index 9faf50dd8b70b2a6fadab5387ef51644f578c853..b39cbaa5825c69ecfda9656115e8827542626fa8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasAvailabilityChecker.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasAvailabilityChecker.kt
@@ -36,7 +36,11 @@ internal class RoomAliasAvailabilityChecker @Inject constructor(
     @Throws(RoomAliasError::class)
     suspend fun check(aliasLocalPart: String?) {
         if (aliasLocalPart.isNullOrEmpty()) {
-            throw RoomAliasError.AliasEmpty
+            // don't check empty or not provided alias
+            return
+        }
+        if (aliasLocalPart.isBlank()) {
+            throw RoomAliasError.AliasIsBlank
         }
         // Check alias availability
         val fullAlias = aliasLocalPart.toFullLocalAlias(userId)
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 13d403e2e4bee4588988a6ebc70790d78d4e81aa..69352688e3b74382804011f01d9c49fb745ad8b2 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
@@ -111,5 +111,12 @@ internal data class CreateRoomBody(
          * The power level content to override in the default power level event
          */
         @Json(name = "power_level_content_override")
-        val powerLevelContentOverride: PowerLevelsContent?
+        val powerLevelContentOverride: PowerLevelsContent?,
+
+        /**
+         * The room version to set for the room. If not provided, the homeserver is to use its configured default.
+         * If provided, the homeserver will return a 400 error with the errcode M_UNSUPPORTED_ROOM_VERSION if it does not support the room version.
+         */
+        @Json(name = "room_version")
+        val roomVersion: String?
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt
index 5e823fc87fe015d89c0eadb0f980c64c77a042c5..018b8653886ac861ffc7ca791cdda340af72af20 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt
@@ -20,13 +20,19 @@ import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.toContent
 import org.matrix.android.sdk.api.session.identity.IdentityServiceError
 import org.matrix.android.sdk.api.session.identity.toMedium
+import org.matrix.android.sdk.api.session.room.model.GuestAccess
+import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
+import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
+import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
 import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
 import org.matrix.android.sdk.api.util.MimeTypes
 import org.matrix.android.sdk.internal.crypto.DeviceListManager
 import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
 import org.matrix.android.sdk.internal.di.AuthenticatedIdentity
+import org.matrix.android.sdk.internal.di.UserId
 import org.matrix.android.sdk.internal.network.token.AccessTokenProvider
 import org.matrix.android.sdk.internal.session.content.FileUploader
 import org.matrix.android.sdk.internal.session.identity.EnsureIdentityTokenTask
@@ -43,6 +49,8 @@ internal class CreateRoomBodyBuilder @Inject constructor(
         private val deviceListManager: DeviceListManager,
         private val identityStore: IdentityStore,
         private val fileUploader: FileUploader,
+        @UserId
+        private val userId: String,
         @AuthenticatedIdentity
         private val accessTokenProvider: AccessTokenProvider
 ) {
@@ -68,10 +76,17 @@ internal class CreateRoomBodyBuilder @Inject constructor(
                     }
                 }
 
+        if (params.joinRuleRestricted != null) {
+            params.roomVersion = "org.matrix.msc3083"
+            params.historyVisibility = params.historyVisibility ?: RoomHistoryVisibility.SHARED
+            params.guestAccess = params.guestAccess ?: GuestAccess.Forbidden
+        }
         val initialStates = listOfNotNull(
                 buildEncryptionWithAlgorithmEvent(params),
                 buildHistoryVisibilityEvent(params),
-                buildAvatarEvent(params)
+                buildAvatarEvent(params),
+                buildGuestAccess(params),
+                buildJoinRulesRestricted(params)
         )
                 .takeIf { it.isNotEmpty() }
 
@@ -80,13 +95,15 @@ internal class CreateRoomBodyBuilder @Inject constructor(
                 roomAliasName = params.roomAliasName,
                 name = params.name,
                 topic = params.topic,
-                invitedUserIds = params.invitedUserIds,
+                invitedUserIds = params.invitedUserIds.filter { it != userId },
                 invite3pids = invite3pids,
                 creationContent = params.creationContent.takeIf { it.isNotEmpty() },
                 initialStates = initialStates,
                 preset = params.preset,
                 isDirect = params.isDirect,
-                powerLevelContentOverride = params.powerLevelContentOverride
+                powerLevelContentOverride = params.powerLevelContentOverride,
+                roomVersion = params.roomVersion
+
         )
     }
 
@@ -120,6 +137,31 @@ internal class CreateRoomBodyBuilder @Inject constructor(
                 }
     }
 
+    private fun buildGuestAccess(params: CreateRoomParams): Event? {
+        return params.guestAccess
+                ?.let {
+                    Event(
+                            type = EventType.STATE_ROOM_GUEST_ACCESS,
+                            stateKey = "",
+                            content = mapOf("guest_access" to it.value)
+                    )
+                }
+    }
+
+    private fun buildJoinRulesRestricted(params: CreateRoomParams): Event? {
+        return params.joinRuleRestricted
+                ?.let { allowList ->
+                    Event(
+                            type = EventType.STATE_ROOM_JOIN_RULES,
+                            stateKey = "",
+                            content = RoomJoinRulesContent(
+                                    _joinRules = RoomJoinRules.RESTRICTED.value,
+                                    allowList = allowList
+                            ).toContent()
+                    )
+                }
+    }
+
     /**
      * Add the crypto algorithm to the room creation parameters.
      */
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 bafe2b90ae0b197773436d386d29ff56b8083080..de6a71e58157e947ac381dccc1442b6641374806 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
@@ -102,7 +102,7 @@ internal class DefaultCreateRoomTask @Inject constructor(
                         .equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name)
             }
         } catch (exception: TimeoutCancellationException) {
-            throw CreateRoomFailure.CreatedWithTimeout
+            throw CreateRoomFailure.CreatedWithTimeout(roomId)
         }
 
         Realm.getInstance(realmConfiguration).executeTransactionAsync {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt
index 5b211c505fe5b48a9bd2d665db1b52c82880cf1a..c6f4bbb4e1a9fdd7cadb464acfd87ea4a42d5a31 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/peeking/PeekRoomTask.kt
@@ -23,11 +23,14 @@ import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.room.model.RoomAvatarContent
 import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent
 import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
+import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
 import org.matrix.android.sdk.api.session.room.model.RoomNameContent
 import org.matrix.android.sdk.api.session.room.model.RoomTopicContent
+import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
 import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsFilter
 import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams
 import org.matrix.android.sdk.api.session.room.peeking.PeekResult
+import org.matrix.android.sdk.api.util.MatrixItem
 import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask
 import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask
 import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask
@@ -100,7 +103,9 @@ internal class DefaultPeekRoomTask @Inject constructor(
                     name = publicRepoResult.name,
                     topic = publicRepoResult.topic,
                     numJoinedMembers = publicRepoResult.numJoinedMembers,
-                    viaServers = serverList
+                    viaServers = serverList,
+                    roomType = null, // would be nice to get that from directory...
+                    someMembers = null
             )
         }
 
@@ -125,11 +130,25 @@ internal class DefaultPeekRoomTask @Inject constructor(
                     ?.let { it.content?.toModel<RoomCanonicalAliasContent>()?.canonicalAlias }
 
             // not sure if it's the right way to do that :/
-            val memberCount = stateEvents
+            val membersEvent = stateEvents
                     .filter { it.type == EventType.STATE_ROOM_MEMBER && it.stateKey?.isNotEmpty() == true }
+
+            val memberCount = membersEvent
                     .distinctBy { it.stateKey }
                     .count()
 
+            val someMembers = membersEvent.mapNotNull { ev ->
+                ev.content?.toModel<RoomMemberContent>()?.let {
+                    MatrixItem.UserItem(ev.stateKey ?: "", it.displayName, it.avatarUrl)
+                }
+            }
+
+            val roomType = stateEvents
+                    .lastOrNull { it.type == EventType.STATE_ROOM_CREATE }
+                    ?.content
+                    ?.toModel<RoomCreateContent>()
+                    ?.type
+
             return PeekResult.Success(
                     roomId = roomId,
                     alias = alias,
@@ -137,7 +156,9 @@ internal class DefaultPeekRoomTask @Inject constructor(
                     name = name,
                     topic = topic,
                     numJoinedMembers = memberCount,
-                    viaServers = serverList
+                    roomType = roomType,
+                    viaServers = serverList,
+                    someMembers = someMembers
             )
         } catch (failure: Throwable) {
             // Would be M_FORBIDDEN if cannot peek :/
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt
index 5fe06287d277aea08a8d68a1ec3ecfd8a20f1d1c..a666d40fc319d96d1d3b0e851314358bf83c58fe 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt
@@ -99,6 +99,7 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor:
             entity.age = editedEventEntity.age
             entity.originServerTs = editedEventEntity.originServerTs
             entity.sendState = editedEventEntity.sendState
+            entity.sendStateDetails = editedEventEntity.sendStateDetails
         }
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relationship/RoomChildRelationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relationship/RoomChildRelationInfo.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2efea7f118f91f8c21e9ebe08adf108b93bcd02c
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relationship/RoomChildRelationInfo.kt
@@ -0,0 +1,104 @@
+/*
+ * 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.room.relationship
+
+import io.realm.Realm
+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.space.model.SpaceChildContent
+import org.matrix.android.sdk.api.session.space.model.SpaceParentContent
+import org.matrix.android.sdk.internal.database.mapper.ContentMapper
+import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
+import org.matrix.android.sdk.internal.database.query.whereType
+
+/**
+ * Relationship between rooms and spaces
+ * The intention is that rooms and spaces form a hierarchy, which clients can use to structure the user's room list into a tree view.
+ * The parent/child relationship can be expressed in one of two ways:
+ *  - The admins of a space can advertise rooms and subspaces for their space by setting m.space.child state events.
+ *  The state_key is the ID of a child room or space, and the content should contain a via key which gives
+ *  a list of candidate servers that can be used to join the room. present: true key is included to distinguish from a deleted state event.
+ *
+ *  - Separately, rooms can claim parents via the m.room.parent state event.
+ */
+internal class RoomChildRelationInfo(
+        private val realm: Realm,
+        private val roomId: String
+) {
+
+    data class SpaceChildInfo(
+            val roomId: String,
+            val order: String?,
+            val autoJoin: Boolean,
+            val viaServers: List<String>
+    )
+
+    data class SpaceParentInfo(
+            val roomId: String,
+            val canonical: Boolean,
+            val viaServers: List<String>,
+            val stateEventSender: String
+    )
+
+    /**
+     * Gets the ordered list of valid child description.
+     */
+    fun getDirectChildrenDescriptions(): List<SpaceChildInfo> {
+        return CurrentStateEventEntity.whereType(realm, roomId, EventType.STATE_SPACE_CHILD)
+                .findAll()
+//                .also {
+//                    Timber.v("## Space: Found ${it.count()} m.space.child state events for $roomId")
+//                }
+                .mapNotNull {
+                    ContentMapper.map(it.root?.content).toModel<SpaceChildContent>()?.let { scc ->
+//                        Timber.v("## Space child desc state event $scc")
+                        // Children where via is not present are ignored.
+                        scc.via?.let { via ->
+                            SpaceChildInfo(
+                                    roomId = it.stateKey,
+                                    order = scc.validOrder(),
+                                    autoJoin = scc.autoJoin ?: false,
+                                    viaServers = via
+                            )
+                        }
+                    }
+                }
+                .sortedBy { it.order }
+    }
+
+    fun getParentDescriptions(): List<SpaceParentInfo> {
+        return CurrentStateEventEntity.whereType(realm, roomId, EventType.STATE_SPACE_PARENT)
+                .findAll()
+//                .also {
+//                    Timber.v("## Space: Found ${it.count()} m.space.parent state events for $roomId")
+//                }
+                .mapNotNull {
+                    ContentMapper.map(it.root?.content).toModel<SpaceParentContent>()?.let { scc ->
+//                        Timber.v("## Space parent desc state event $scc")
+                        // Parent where via is not present are ignored.
+                        scc.via?.let { via ->
+                            SpaceParentInfo(
+                                    roomId = it.stateKey,
+                                    canonical = scc.canonical ?: false,
+                                    viaServers = via,
+                                    stateEventSender = it.root?.sender ?: ""
+                            )
+                        }
+                    }
+                }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
index 26a87557ff490686964c163542bf0965c92e2768..449189e6b56cb0ef723e5f8bf8e67511b24c4f86 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
@@ -109,14 +109,6 @@ internal class DefaultSendService @AssistedInject constructor(
                 .let { sendEvent(it) }
     }
 
-    override fun sendMedias(attachments: List<ContentAttachmentData>,
-                            compressBeforeSending: Boolean,
-                            roomIds: Set<String>): Cancelable {
-        return attachments.mapTo(CancelableBag()) {
-            sendMedia(it, compressBeforeSending, roomIds)
-        }
-    }
-
     override fun redactEvent(event: Event, reason: String?): Cancelable {
         // TODO manage media/attachements?
         val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason)
@@ -149,7 +141,7 @@ internal class DefaultSendService @AssistedInject constructor(
                 is MessageImageContent -> {
                     // The image has not yet been sent
                     val attachmentData = ContentAttachmentData(
-                            size = messageContent.info!!.size.toLong(),
+                            size = messageContent.info!!.size,
                             mimeType = messageContent.info.mimeType!!,
                             width = messageContent.info.width.toLong(),
                             height = messageContent.info.height.toLong(),
@@ -240,6 +232,14 @@ internal class DefaultSendService @AssistedInject constructor(
         }
     }
 
+    override fun sendMedias(attachments: List<ContentAttachmentData>,
+                            compressBeforeSending: Boolean,
+                            roomIds: Set<String>): Cancelable {
+        return attachments.mapTo(CancelableBag()) {
+            sendMedia(it, compressBeforeSending, roomIds)
+        }
+    }
+
     override fun sendMedia(attachment: ContentAttachmentData,
                            compressBeforeSending: Boolean,
                            roomIds: Set<String>): Cancelable {
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 432a4af062046896c48ea15929b4e971ad67a1f9..c1ad6205c38c383b7d4d76e3c498605e524211fa 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
@@ -244,7 +244,7 @@ internal class LocalEchoEventFactory @Inject constructor(
                         mimeType = attachment.getSafeMimeType(),
                         width = width?.toInt() ?: 0,
                         height = height?.toInt() ?: 0,
-                        size = attachment.size.toInt()
+                        size = attachment.size
                 ),
                 url = attachment.queryUri.toString()
         )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt
index 70245cbd5e49eead24cd825787ecc5bad2588016..e98e5646afe38a5eeba1c4e68ac75db85ea63098 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt
@@ -87,7 +87,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
         }
     }
 
-    fun updateSendState(eventId: String, roomId: String?, sendState: SendState) {
+    fun updateSendState(eventId: String, roomId: String?, sendState: SendState, sendStateDetails: String? = null) {
         Timber.v("## SendEvent: [${System.currentTimeMillis()}] Update local state of $eventId to ${sendState.name}")
         timelineInput.onLocalEchoUpdated(roomId = roomId ?: "", eventId = eventId, sendState = sendState)
         updateEchoAsync(eventId) { realm, sendingEventEntity ->
@@ -96,6 +96,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
             } else {
                 sendingEventEntity.sendState = sendState
             }
+            sendingEventEntity.sendStateDetails = sendStateDetails
             roomSummaryUpdater.updateSendingInformation(realm, sendingEventEntity.roomId)
         }
     }
@@ -161,6 +162,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
             val timelineEvents = TimelineEventEntity.where(realm, roomId, eventIds).findAll()
             timelineEvents.forEach {
                 it.root?.sendState = sendState
+                it.root?.sendStateDetails = null
             }
             roomSummaryUpdater.updateSendingInformation(realm, roomId)
         }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt
index bc307bc74f7fed0bcd166ea0310583ce2e31177a..e889f1a61ba00d6a47f13a8d36dbb00eea8bcfa4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt
@@ -55,7 +55,12 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo
 
     override fun doOnError(params: Params): Result {
         params.localEchoIds.forEach { localEchoIds ->
-            localEchoRepository.updateSendState(localEchoIds.eventId, localEchoIds.roomId, SendState.UNDELIVERED)
+            localEchoRepository.updateSendState(
+                    eventId = localEchoIds.eventId,
+                    roomId = localEchoIds.roomId,
+                    sendState = SendState.UNDELIVERED,
+                    sendStateDetails = params.lastFailureMessage
+            )
         }
 
         return super.doOnError(params)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt
index d55dce57afaadb87a322ba60772616008c7d482f..cd7911910dd8eda81a4c13038f49cd450fd69d8c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt
@@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.internal.crypto.tasks.SendEventTask
 import org.matrix.android.sdk.internal.di.SessionDatabase
 import org.matrix.android.sdk.internal.session.SessionComponent
+import org.matrix.android.sdk.internal.util.toMatrixErrorStr
 import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
 import org.matrix.android.sdk.internal.worker.SessionWorkerParams
 import timber.log.Timber
@@ -77,7 +78,12 @@ internal class SendEventWorker(context: Context,
         }
 
         if (params.lastFailureMessage != null) {
-            localEchoRepository.updateSendState(event.eventId, event.roomId, SendState.UNDELIVERED)
+            localEchoRepository.updateSendState(
+                    eventId = event.eventId,
+                    roomId = event.roomId,
+                    sendState = SendState.UNDELIVERED,
+                    sendStateDetails = params.lastFailureMessage
+            )
             // Transmit the error
             return Result.success(inputData)
                     .also { Timber.e("Work cancelled due to input error from parent") }
@@ -90,7 +96,12 @@ internal class SendEventWorker(context: Context,
         } catch (exception: Throwable) {
             if (/*currentAttemptCount >= MAX_NUMBER_OF_RETRY_BEFORE_FAILING ||**/ !exception.shouldBeRetried()) {
                 Timber.e("## SendEvent: [${System.currentTimeMillis()}]  Send event Failed cannot retry ${params.eventId} > ${exception.localizedMessage}")
-                localEchoRepository.updateSendState(event.eventId, event.roomId, SendState.UNDELIVERED)
+                localEchoRepository.updateSendState(
+                        eventId = event.eventId,
+                        roomId = event.roomId,
+                        sendState = SendState.UNDELIVERED,
+                        sendStateDetails = exception.toMatrixErrorStr()
+                )
                 Result.success()
             } else {
                 Timber.e("## SendEvent: [${System.currentTimeMillis()}]  Send event Failed schedule retry ${params.eventId} > ${exception.localizedMessage}")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt
index 8bafa5f882c7f7d32c41664b3554cf15ca8113a0..cd5bf575dba4d66837949d8b9fa92bbba321db5c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessor.kt
@@ -18,7 +18,7 @@ package org.matrix.android.sdk.internal.session.room.send.queue
 
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.util.Cancelable
-import org.matrix.android.sdk.internal.session.SessionLifecycleObserver
+import org.matrix.android.sdk.api.session.SessionLifecycleObserver
 
 internal interface EventSenderProcessor: SessionLifecycleObserver {
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt
index a5c09f5ff69aa80ab593fa0bb49479e0efd6b332..80bfd02b0eef8bcec3f29cde2d5d2f2b4451c673 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorCoroutine.kt
@@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.auth.data.SessionParams
 import org.matrix.android.sdk.api.failure.Failure
 import org.matrix.android.sdk.api.failure.MatrixError
 import org.matrix.android.sdk.api.failure.getRetryDelay
+import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.api.session.crypto.CryptoService
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.util.Cancelable
@@ -72,7 +73,7 @@ internal class EventSenderProcessorCoroutine @Inject constructor(
      */
     private val cancelableBag = ConcurrentHashMap<String, Cancelable>()
 
-    override fun onSessionStarted() {
+    override fun onSessionStarted(session: Session) {
         // We should check for sending events not handled because app was killed
         // But we should be careful of only took those that was submitted to us, because if it's
         // for example it's a media event it is handled by some worker and he will handle it
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 b79a86dd7e0ce8392171d1aa560112b5587b9bbc..9db7cc90394fbb7378231dc963314e58d7ddac30 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
@@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.failure.Failure
 import org.matrix.android.sdk.api.failure.MatrixError
 import org.matrix.android.sdk.api.failure.isTokenError
+import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.api.session.crypto.CryptoService
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.sync.SyncState
@@ -64,11 +65,11 @@ internal class EventSenderProcessorThread @Inject constructor(
         memento.unTrack(task)
     }
 
-    override fun onSessionStarted() {
+    override fun onSessionStarted(session: Session) {
         start()
     }
 
-    override fun onSessionStopped() {
+    override fun onSessionStopped(session: Session) {
         interrupt()
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt
index 615bc99096d63055e1df98b7cea56f9279a998bc..ff2afb5d61aff480092658ea0c8d9900e0de1f8b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt
@@ -34,6 +34,7 @@ import org.matrix.android.sdk.api.util.JsonDict
 import org.matrix.android.sdk.api.util.MimeTypes
 import org.matrix.android.sdk.api.util.Optional
 import org.matrix.android.sdk.internal.session.content.FileUploader
+import java.lang.UnsupportedOperationException
 
 internal class DefaultStateService @AssistedInject constructor(@Assisted private val roomId: String,
                                                                private val stateEventDataSource: StateEventDataSource,
@@ -73,7 +74,7 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
                 eventType = eventType,
                 body = body.toSafeJson(eventType)
         )
-        sendStateTask.execute(params)
+        sendStateTask.executeRetry(params, 3)
     }
 
     private fun JsonDict.toSafeJson(eventType: String): JsonDict {
@@ -127,6 +128,7 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
 
     override suspend fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?) {
         if (joinRules != null) {
+            if (joinRules == RoomJoinRules.RESTRICTED) throw UnsupportedOperationException("No yet supported")
             sendStateEvent(
                     eventType = EventType.STATE_ROOM_JOIN_RULES,
                     body = mapOf("join_rule" to joinRules),
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SafePowerLevelContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SafePowerLevelContent.kt
index a97709e38ba2d1d3e0b83a99503ab93c0e99cb4b..197b4f8688c7d38cf244785b21bb39d96bd40df3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SafePowerLevelContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/SafePowerLevelContent.kt
@@ -21,22 +21,21 @@ import com.squareup.moshi.JsonClass
 import org.matrix.android.sdk.api.session.events.model.toContent
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
-import org.matrix.android.sdk.api.session.room.powerlevels.Role
 import org.matrix.android.sdk.api.util.JsonDict
 
 @JsonClass(generateAdapter = true)
 internal data class SerializablePowerLevelsContent(
-        @Json(name = "ban") val ban: Int = Role.Moderator.value,
-        @Json(name = "kick") val kick: Int = Role.Moderator.value,
-        @Json(name = "invite") val invite: Int = Role.Moderator.value,
-        @Json(name = "redact") val redact: Int = Role.Moderator.value,
-        @Json(name = "events_default") val eventsDefault: Int = Role.Default.value,
-        @Json(name = "events") val events: Map<String, Int> = emptyMap(),
-        @Json(name = "users_default") val usersDefault: Int = Role.Default.value,
-        @Json(name = "users") val users: Map<String, Int> = emptyMap(),
-        @Json(name = "state_default") val stateDefault: Int = Role.Moderator.value,
+        @Json(name = "ban") val ban: Int?,
+        @Json(name = "kick") val kick: Int?,
+        @Json(name = "invite") val invite: Int?,
+        @Json(name = "redact") val redact: Int?,
+        @Json(name = "events_default") val eventsDefault: Int?,
+        @Json(name = "events") val events: Map<String, Int>?,
+        @Json(name = "users_default") val usersDefault: Int?,
+        @Json(name = "users") val users: Map<String, Int>?,
+        @Json(name = "state_default") val stateDefault: Int?,
         // `Int` is the diff here (instead of `Any`)
-        @Json(name = "notifications") val notifications: Map<String, Int> = emptyMap()
+        @Json(name = "notifications") val notifications: Map<String, Int>?
 )
 
 internal fun JsonDict.toSafePowerLevelsContentDict(): JsonDict {
@@ -52,7 +51,7 @@ internal fun JsonDict.toSafePowerLevelsContentDict(): JsonDict {
                         usersDefault = content.usersDefault,
                         users = content.users,
                         stateDefault = content.stateDefault,
-                        notifications = content.notifications.mapValues { content.notificationLevel(it.key)  }
+                        notifications = content.notifications?.mapValues { content.notificationLevel(it.key)  }
                 )
             }
             ?.toContent()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/GraphUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/GraphUtils.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b7e6548b54f8855a7e349b7243478b2e84006f9f
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/GraphUtils.kt
@@ -0,0 +1,166 @@
+/*
+ * Copyright (c) 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.room.summary
+
+import java.util.LinkedList
+
+data class GraphNode(
+        val name: String
+)
+
+data class GraphEdge(
+        val source: GraphNode,
+        val destination: GraphNode
+)
+
+class Graph {
+
+    private val adjacencyList: HashMap<GraphNode, ArrayList<GraphEdge>> = HashMap()
+
+    fun getOrCreateNode(name: String): GraphNode {
+        return adjacencyList.entries.firstOrNull { it.key.name == name }?.key
+                ?: GraphNode(name).also {
+                    adjacencyList[it] = ArrayList()
+                }
+    }
+
+    fun addEdge(sourceName: String, destinationName: String) {
+        val source = getOrCreateNode(sourceName)
+        val destination = getOrCreateNode(destinationName)
+        adjacencyList.getOrPut(source) { ArrayList() }.add(
+                GraphEdge(source, destination)
+        )
+    }
+
+    fun addEdge(source: GraphNode, destination: GraphNode) {
+        adjacencyList.getOrPut(source) { ArrayList() }.add(
+                GraphEdge(source, destination)
+        )
+    }
+
+    fun edgesOf(node: GraphNode): List<GraphEdge> {
+        return adjacencyList[node]?.toList() ?: emptyList()
+    }
+
+    fun withoutEdges(edgesToPrune: List<GraphEdge>): Graph {
+        val output = Graph()
+        this.adjacencyList.forEach { (vertex, edges) ->
+            output.getOrCreateNode(vertex.name)
+            edges.forEach {
+                if (!edgesToPrune.contains(it)) {
+                    // add it
+                    output.addEdge(it.source, it.destination)
+                }
+            }
+        }
+        return output
+    }
+
+    /**
+     * Depending on the chosen starting point the background edge might change
+     */
+    fun findBackwardEdges(startFrom: GraphNode? = null): List<GraphEdge> {
+        val backwardEdges = mutableSetOf<GraphEdge>()
+        val visited = mutableMapOf<GraphNode, Int>()
+        val notVisited = -1
+        val inPath = 0
+        val completed = 1
+        adjacencyList.keys.forEach {
+            visited[it] = notVisited
+        }
+        val stack = LinkedList<GraphNode>()
+
+        (startFrom ?: adjacencyList.entries.firstOrNull { visited[it.key] == notVisited }?.key)
+                ?.let {
+                    stack.push(it)
+                    visited[it] = inPath
+                }
+
+        while (stack.isNotEmpty()) {
+//            Timber.w("VAL: current stack: ${stack.reversed().joinToString { it.name }}")
+            val vertex = stack.peek() ?: break
+            // peek a path to follow
+            var destination: GraphNode? = null
+            edgesOf(vertex).forEach {
+                when (visited[it.destination]) {
+                    notVisited -> {
+                        // it's a candidate
+                        destination = it.destination
+                    }
+                    inPath -> {
+                        // Cycle!!
+                        backwardEdges.add(it)
+                    }
+                    completed -> {
+                        // dead end
+                    }
+                }
+            }
+            if (destination == null) {
+                // dead end, pop
+                stack.pop().let {
+                    visited[it] = completed
+                }
+            } else {
+                // go down this path
+                stack.push(destination)
+                visited[destination!!] = inPath
+            }
+
+            if (stack.isEmpty()) {
+                // try to get another graph of forest?
+                adjacencyList.entries.firstOrNull { visited[it.key] == notVisited }?.key?.let {
+                    stack.push(it)
+                    visited[it] = inPath
+                }
+            }
+        }
+
+        return backwardEdges.toList()
+    }
+
+    /**
+     * Only call that on acyclic graph!
+     */
+    fun flattenDestination(): Map<GraphNode, Set<GraphNode>> {
+        val result = HashMap<GraphNode, Set<GraphNode>>()
+        adjacencyList.keys.forEach { vertex ->
+            result[vertex] = flattenOf(vertex)
+        }
+        return result
+    }
+
+    private fun flattenOf(node: GraphNode): Set<GraphNode> {
+        val result = mutableSetOf<GraphNode>()
+        val edgesOf = edgesOf(node)
+        result.addAll(edgesOf.map { it.destination })
+        edgesOf.forEach {
+            result.addAll(flattenOf(it.destination))
+        }
+        return result
+    }
+
+    override fun toString(): String {
+        return buildString {
+            adjacencyList.forEach { (node, edges) ->
+                append("${node.name} : [")
+                append(edges.joinToString(" ") { it.destination.name })
+                append("]\n")
+            }
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/HierarchyLiveDataHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/HierarchyLiveDataHelper.kt
new file mode 100644
index 0000000000000000000000000000000000000000..29db8431fd117c12703ef37c2801aee34c24630a
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/HierarchyLiveDataHelper.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.matrix.android.sdk.internal.session.room.summary
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MediatorLiveData
+import org.matrix.android.sdk.api.session.room.model.Membership
+import org.matrix.android.sdk.api.session.room.model.RoomSummary
+import org.matrix.android.sdk.api.util.Optional
+
+internal class HierarchyLiveDataHelper(
+        val spaceId: String,
+        val memberships: List<Membership>,
+        val roomSummaryDataSource: RoomSummaryDataSource) {
+
+    private val sources = HashMap<String, LiveData<Optional<RoomSummary>>>()
+    private val mediatorLiveData = MediatorLiveData<List<String>>()
+
+    fun liveData(): LiveData<List<String>> = mediatorLiveData
+
+    init {
+        onChange()
+    }
+
+    private fun parentsToCheck(): List<RoomSummary> {
+        val spaces = ArrayList<RoomSummary>()
+        roomSummaryDataSource.getSpaceSummary(spaceId)?.let {
+            roomSummaryDataSource.flattenSubSpace(it, emptyList(), spaces, memberships)
+        }
+        return spaces
+    }
+
+    private fun onChange() {
+        val existingSources = sources.keys.toList()
+        val newSources = parentsToCheck().map { it.roomId }
+        val addedSources = newSources.filter { !existingSources.contains(it) }
+        val removedSource = existingSources.filter { !newSources.contains(it) }
+        addedSources.forEach {
+            val liveData = roomSummaryDataSource.getSpaceSummaryLive(it)
+            mediatorLiveData.addSource(liveData) { onChange() }
+            sources[it] = liveData
+        }
+
+        removedSource.forEach {
+            sources[it]?.let { mediatorLiveData.removeSource(it) }
+        }
+
+        sources[spaceId]?.value?.getOrNull()?.let { spaceSummary ->
+            val results = ArrayList<RoomSummary>()
+            roomSummaryDataSource.flattenChild(spaceSummary, emptyList(), results, memberships)
+            mediatorLiveData.postValue(results.map { it.roomId })
+        }
+    }
+}
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 dd3fbe04b27890a13a3325e0ac39bb28a65c476d..126458b0820e175e874cedf429847bd332bc1228 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
@@ -1,5 +1,6 @@
 /*
  * Copyright 2020 The Matrix.org Foundation C.I.C.
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -17,6 +18,7 @@
 package org.matrix.android.sdk.internal.session.room.summary
 
 import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.Transformations
 import androidx.paging.LivePagedListBuilder
 import androidx.paging.PagedList
@@ -24,12 +26,21 @@ import com.zhuinden.monarchy.Monarchy
 import io.realm.Realm
 import io.realm.RealmQuery
 import io.realm.Sort
+import io.realm.kotlin.where
+import org.matrix.android.sdk.api.query.ActiveSpaceFilter
 import org.matrix.android.sdk.api.query.RoomCategoryFilter
+import org.matrix.android.sdk.api.session.room.ResultBoundaries
+import org.matrix.android.sdk.api.session.room.RoomSortOrder
 import org.matrix.android.sdk.api.session.room.RoomSummaryQueryParams
-import org.matrix.android.sdk.api.session.room.UpdatableFilterLivePageResult
+import org.matrix.android.sdk.api.session.room.UpdatableLivePageResult
+import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.api.session.room.model.RoomSummary
+import org.matrix.android.sdk.api.session.room.model.RoomType
 import org.matrix.android.sdk.api.session.room.model.VersioningState
+import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
+import org.matrix.android.sdk.api.session.room.spaceSummaryQueryParams
 import org.matrix.android.sdk.api.session.room.summary.RoomAggregateNotificationCount
+import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams
 import org.matrix.android.sdk.api.util.Optional
 import org.matrix.android.sdk.api.util.toOptional
 import org.matrix.android.sdk.internal.database.mapper.RoomSummaryMapper
@@ -79,11 +90,60 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat
 
     fun getRoomSummariesLive(queryParams: RoomSummaryQueryParams): LiveData<List<RoomSummary>> {
         return monarchy.findAllMappedWithChanges(
-                { roomSummariesQuery(it, queryParams) },
+                {
+                    roomSummariesQuery(it, queryParams)
+                            .sort(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, Sort.DESCENDING)
+                },
                 { roomSummaryMapper.map(it) }
         )
     }
 
+    fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams): LiveData<List<RoomSummary>> {
+        return getRoomSummariesLive(queryParams)
+    }
+
+    fun getSpaceSummary(roomIdOrAlias: String): RoomSummary? {
+        return getRoomSummary(roomIdOrAlias)
+                ?.takeIf { it.roomType == RoomType.SPACE }
+    }
+
+    fun getSpaceSummaryLive(roomId: String): LiveData<Optional<RoomSummary>> {
+        val liveData = monarchy.findAllMappedWithChanges(
+                { realm ->
+                    RoomSummaryEntity.where(realm, roomId)
+                            .isNotEmpty(RoomSummaryEntityFields.DISPLAY_NAME)
+                            .equalTo(RoomSummaryEntityFields.ROOM_TYPE, RoomType.SPACE)
+                },
+                {
+                    roomSummaryMapper.map(it)
+                }
+        )
+        return Transformations.map(liveData) { results ->
+            results.firstOrNull().toOptional()
+        }
+    }
+
+    fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List<RoomSummary> {
+        return getRoomSummaries(spaceSummaryQueryParams)
+    }
+
+    fun getRootSpaceSummaries(): List<RoomSummary> {
+        return getRoomSummaries(spaceSummaryQueryParams {
+            memberships = listOf(Membership.JOIN)
+        })
+                .let { allJoinedSpace ->
+                    val allFlattenChildren = arrayListOf<RoomSummary>()
+                    allJoinedSpace.forEach {
+                        flattenSubSpace(it, emptyList(), allFlattenChildren, listOf(Membership.JOIN), false)
+                    }
+                    val knownNonOrphan = allFlattenChildren.map { it.roomId }.distinct()
+                    // keep only root rooms
+                    allJoinedSpace.filter { candidate ->
+                        !knownNonOrphan.contains(candidate.roomId)
+                    }
+                }
+    }
+
     fun getBreadcrumbs(queryParams: RoomSummaryQueryParams): List<RoomSummary> {
         return monarchy.fetchAllMappedSync(
                 { breadcrumbsQuery(it, queryParams) },
@@ -105,10 +165,10 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat
     }
 
     fun getSortedPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams,
-                                        pagedListConfig: PagedList.Config): LiveData<PagedList<RoomSummary>> {
+                                        pagedListConfig: PagedList.Config,
+                                        sortOrder: RoomSortOrder): LiveData<PagedList<RoomSummary>> {
         val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
-            roomSummariesQuery(realm, queryParams)
-                    .sort(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, Sort.DESCENDING)
+            roomSummariesQuery(realm, queryParams).process(sortOrder)
         }
         val dataSourceFactory = realmDataSourceFactory.map {
             roomSummaryMapper.map(it)
@@ -119,30 +179,48 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat
         )
     }
 
-    fun getFilteredPagedRoomSummariesLive(queryParams: RoomSummaryQueryParams,
-                                          pagedListConfig: PagedList.Config): UpdatableFilterLivePageResult {
+    fun getUpdatablePagedRoomSummariesLive(queryParams: RoomSummaryQueryParams,
+                                           pagedListConfig: PagedList.Config,
+                                           sortOrder: RoomSortOrder): UpdatableLivePageResult {
         val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
-            roomSummariesQuery(realm, queryParams)
-                    .sort(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, Sort.DESCENDING)
+            roomSummariesQuery(realm, queryParams).process(sortOrder)
         }
         val dataSourceFactory = realmDataSourceFactory.map {
             roomSummaryMapper.map(it)
         }
 
+        val boundaries = MutableLiveData(ResultBoundaries())
+
         val mapped = monarchy.findAllPagedWithChanges(
                 realmDataSourceFactory,
-                LivePagedListBuilder(dataSourceFactory, pagedListConfig)
+                LivePagedListBuilder(dataSourceFactory, pagedListConfig).also {
+                    it.setBoundaryCallback(object : PagedList.BoundaryCallback<RoomSummary>() {
+                        override fun onItemAtEndLoaded(itemAtEnd: RoomSummary) {
+                            boundaries.postValue(boundaries.value?.copy(frontLoaded = true))
+                        }
+
+                        override fun onItemAtFrontLoaded(itemAtFront: RoomSummary) {
+                            boundaries.postValue(boundaries.value?.copy(endLoaded = true))
+                        }
+
+                        override fun onZeroItemsLoaded() {
+                            boundaries.postValue(boundaries.value?.copy(zeroItemLoaded = true))
+                        }
+                    })
+                }
         )
 
-        return object : UpdatableFilterLivePageResult {
+        return object : UpdatableLivePageResult {
             override val livePagedList: LiveData<PagedList<RoomSummary>> = mapped
 
-            override fun updateQuery(queryParams: RoomSummaryQueryParams) {
+            override fun updateQuery(builder: (RoomSummaryQueryParams) -> RoomSummaryQueryParams) {
                 realmDataSourceFactory.updateQuery {
-                    roomSummariesQuery(it, queryParams)
-                            .sort(RoomSummaryEntityFields.LAST_ACTIVITY_TIME, Sort.DESCENDING)
+                    roomSummariesQuery(it, builder.invoke(queryParams)).process(sortOrder)
                 }
             }
+
+            override val liveBoundaries: LiveData<ResultBoundaries>
+                get() = boundaries
         }
     }
 
@@ -170,10 +248,10 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat
 
         queryParams.roomCategoryFilter?.let {
             when (it) {
-                RoomCategoryFilter.ONLY_DM                 -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
-                RoomCategoryFilter.ONLY_ROOMS              -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, false)
+                RoomCategoryFilter.ONLY_DM -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
+                RoomCategoryFilter.ONLY_ROOMS -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, false)
                 RoomCategoryFilter.ONLY_WITH_NOTIFICATIONS -> query.greaterThan(RoomSummaryEntityFields.NOTIFICATION_COUNT, 0)
-                RoomCategoryFilter.ALL                     -> {
+                RoomCategoryFilter.ALL -> {
                     // nop
                 }
             }
@@ -189,6 +267,160 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat
                 query.equalTo(RoomSummaryEntityFields.IS_SERVER_NOTICE, sn)
             }
         }
+
+        queryParams.excludeType?.forEach {
+            query.notEqualTo(RoomSummaryEntityFields.ROOM_TYPE, it)
+        }
+        queryParams.includeType?.forEach {
+            query.equalTo(RoomSummaryEntityFields.ROOM_TYPE, it)
+        }
+        when (queryParams.roomCategoryFilter) {
+            RoomCategoryFilter.ONLY_DM -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
+            RoomCategoryFilter.ONLY_ROOMS -> query.equalTo(RoomSummaryEntityFields.IS_DIRECT, false)
+            RoomCategoryFilter.ONLY_WITH_NOTIFICATIONS -> query.greaterThan(RoomSummaryEntityFields.NOTIFICATION_COUNT, 0)
+            RoomCategoryFilter.ALL -> Unit // nop
+        }
+
+        // Timber.w("VAL: activeSpaceId : ${queryParams.activeSpaceId}")
+        when (queryParams.activeSpaceFilter) {
+            is ActiveSpaceFilter.ActiveSpace -> {
+                // It's annoying but for now realm java does not support querying in primitive list :/
+                // https://github.com/realm/realm-java/issues/5361
+                if (queryParams.activeSpaceFilter.currentSpaceId == null) {
+                    // orphan rooms
+                    query.isNull(RoomSummaryEntityFields.FLATTEN_PARENT_IDS)
+                } else {
+                    query.contains(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, queryParams.activeSpaceFilter.currentSpaceId)
+                }
+            }
+            is ActiveSpaceFilter.ExcludeSpace -> {
+                query.not().contains(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, queryParams.activeSpaceFilter.spaceId)
+            }
+            else                              -> {
+                // nop
+            }
+        }
+
+        if (queryParams.activeGroupId != null) {
+            query.contains(RoomSummaryEntityFields.GROUP_IDS, queryParams.activeGroupId!!)
+        }
         return query
     }
+
+    fun getAllRoomSummaryChildOf(spaceAliasOrId: String, memberShips: List<Membership>): List<RoomSummary> {
+        val space = getSpaceSummary(spaceAliasOrId) ?: return emptyList()
+        val result = ArrayList<RoomSummary>()
+        flattenChild(space, emptyList(), result, memberShips)
+        return result
+    }
+
+    fun getAllRoomSummaryChildOfLive(spaceId: String, memberShips: List<Membership>): LiveData<List<RoomSummary>> {
+        // we want to listen to all spaces in hierarchy and on change compute back all childs
+        // and switch map to listen those?
+        val mediatorLiveData = HierarchyLiveDataHelper(spaceId, memberShips, this).liveData()
+
+        return Transformations.switchMap(mediatorLiveData) { allIds ->
+            monarchy.findAllMappedWithChanges(
+                    {
+                        it.where<RoomSummaryEntity>()
+                                .`in`(RoomSummaryEntityFields.ROOM_ID, allIds.toTypedArray())
+                                .`in`(RoomSummaryEntityFields.MEMBERSHIP_STR, memberShips.map { it.name }.toTypedArray())
+                                .equalTo(RoomSummaryEntityFields.IS_DIRECT, false)
+                    },
+                    {
+                        roomSummaryMapper.map(it)
+                    })
+        }
+    }
+
+    fun getFlattenOrphanRooms(): List<RoomSummary> {
+        return getRoomSummaries(
+                roomSummaryQueryParams {
+                    memberships = Membership.activeMemberships()
+                    excludeType = listOf(RoomType.SPACE)
+                    roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
+                }
+        ).filter { isOrphan(it) }
+    }
+
+    fun getFlattenOrphanRoomsLive(): LiveData<List<RoomSummary>> {
+        return Transformations.map(
+                getRoomSummariesLive(roomSummaryQueryParams {
+                    memberships = Membership.activeMemberships()
+                    excludeType = listOf(RoomType.SPACE)
+                    roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
+                })
+        ) {
+            it.filter { isOrphan(it) }
+        }
+    }
+
+    private fun isOrphan(roomSummary: RoomSummary): Boolean {
+        if (roomSummary.roomType == RoomType.SPACE && roomSummary.membership.isActive()) {
+            return false
+        }
+        // all parents line should be orphan
+        roomSummary.spaceParents?.forEach { info ->
+            if (info.roomSummary != null && !info.roomSummary.membership.isLeft()) {
+                if (!isOrphan(info.roomSummary)) {
+                    return false
+                }
+            }
+        }
+
+        // it may not have a parent relation but could be a child of some other....
+        for (spaceSummary in getSpaceSummaries(spaceSummaryQueryParams { memberships = Membership.activeMemberships() })) {
+            if (spaceSummary.spaceChildren?.any { it.childRoomId == roomSummary.roomId } == true) {
+                return false
+            }
+        }
+
+        return true
+    }
+
+    fun flattenChild(current: RoomSummary, parenting: List<String>, output: MutableList<RoomSummary>, memberShips: List<Membership>) {
+        current.spaceChildren?.sortedBy { it.order ?: it.name }?.forEach { childInfo ->
+            if (childInfo.roomType == RoomType.SPACE) {
+                // Add recursive
+                if (!parenting.contains(childInfo.childRoomId)) { // avoid cycles!
+                    getSpaceSummary(childInfo.childRoomId)?.let { subSpace ->
+                        if (memberShips.isEmpty() || memberShips.contains(subSpace.membership)) {
+                            flattenChild(subSpace, parenting + listOf(current.roomId), output, memberShips)
+                        }
+                    }
+                }
+            } else if (childInfo.isKnown) {
+                getRoomSummary(childInfo.childRoomId)?.let {
+                    if (memberShips.isEmpty() || memberShips.contains(it.membership)) {
+                        if (!it.isDirect) {
+                            output.add(it)
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    fun flattenSubSpace(current: RoomSummary,
+                        parenting: List<String>,
+                        output: MutableList<RoomSummary>,
+                        memberShips: List<Membership>,
+                        includeCurrent: Boolean = true) {
+        if (includeCurrent) {
+            output.add(current)
+        }
+        current.spaceChildren?.sortedBy { it.order ?: it.name }?.forEach {
+            if (it.roomType == RoomType.SPACE) {
+                // Add recursive
+                if (!parenting.contains(it.childRoomId)) { // avoid cycles!
+                    getSpaceSummary(it.childRoomId)?.let { subSpace ->
+                        if (memberShips.isEmpty() || memberShips.contains(subSpace.membership)) {
+                            output.add(subSpace)
+                            flattenSubSpace(subSpace, parenting + listOf(current.roomId), output, memberShips)
+                        }
+                    }
+                }
+            }
+        }
+    }
 }
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 7913bf71a2ac69f55687fc0c840e8091ac47af56..d488fdfc2a91fa07e400afd7a170715a355a5b1d 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
@@ -17,14 +17,18 @@
 package org.matrix.android.sdk.internal.session.room.summary
 
 import io.realm.Realm
+import io.realm.kotlin.createObject
 import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.api.session.room.model.RoomAliasesContent
 import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent
+import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
 import org.matrix.android.sdk.api.session.room.model.RoomNameContent
 import org.matrix.android.sdk.api.session.room.model.RoomTopicContent
+import org.matrix.android.sdk.api.session.room.model.RoomType
+import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
 import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.internal.crypto.EventDecryptor
 import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
@@ -34,29 +38,40 @@ 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.EventEntity
 import org.matrix.android.sdk.internal.database.model.EventEntityFields
+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
+import org.matrix.android.sdk.internal.database.model.SpaceChildSummaryEntity
+import org.matrix.android.sdk.internal.database.model.SpaceParentSummaryEntity
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
 import org.matrix.android.sdk.internal.database.query.findAllInRoomWithSendStates
 import org.matrix.android.sdk.internal.database.query.getOrCreate
 import org.matrix.android.sdk.internal.database.query.getOrNull
 import org.matrix.android.sdk.internal.database.query.isEventRead
+import org.matrix.android.sdk.internal.database.query.where
 import org.matrix.android.sdk.internal.database.query.whereType
 import org.matrix.android.sdk.internal.di.UserId
+import org.matrix.android.sdk.internal.extensions.clearWith
+import org.matrix.android.sdk.internal.query.process
 import org.matrix.android.sdk.internal.session.room.RoomAvatarResolver
 import org.matrix.android.sdk.internal.session.room.membership.RoomDisplayNameResolver
 import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
+import org.matrix.android.sdk.internal.session.room.relationship.RoomChildRelationInfo
+import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
 import org.matrix.android.sdk.internal.session.sync.model.RoomSyncSummary
 import org.matrix.android.sdk.internal.session.sync.model.RoomSyncUnreadNotifications
 import timber.log.Timber
 import javax.inject.Inject
+import kotlin.system.measureTimeMillis
 
 internal class RoomSummaryUpdater @Inject constructor(
         @UserId private val userId: String,
         private val roomDisplayNameResolver: RoomDisplayNameResolver,
         private val roomAvatarResolver: RoomAvatarResolver,
         private val eventDecryptor: EventDecryptor,
-        private val crossSigningService: DefaultCrossSigningService) {
+        private val crossSigningService: DefaultCrossSigningService,
+        private val stateEventDataSource: StateEventDataSource) {
 
     fun update(realm: Realm,
                roomId: String,
@@ -89,6 +104,12 @@ internal class RoomSummaryUpdater @Inject constructor(
         val lastTopicEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_TOPIC, stateKey = "")?.root
         val lastCanonicalAliasEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_CANONICAL_ALIAS, stateKey = "")?.root
         val lastAliasesEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ALIASES, stateKey = "")?.root
+        val roomCreateEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_CREATE, stateKey = "")?.root
+        val joinRulesEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_JOIN_RULES, stateKey = "")?.root
+
+        val roomType = ContentMapper.map(roomCreateEvent?.content).toModel<RoomCreateContent>()?.type
+        roomSummaryEntity.roomType = roomType
+        Timber.v("## Space: Updating summary room [$roomId] roomType: [$roomType]")
 
         // Don't use current state for this one as we are only interested in having MXCRYPTO_ALGORITHM_MEGOLM event in the room
         val encryptionEvent = EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
@@ -111,6 +132,7 @@ internal class RoomSummaryUpdater @Inject constructor(
         roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(realm, roomId)
         roomSummaryEntity.name = ContentMapper.map(lastNameEvent?.content).toModel<RoomNameContent>()?.name
         roomSummaryEntity.topic = ContentMapper.map(lastTopicEvent?.content).toModel<RoomTopicContent>()?.topic
+        roomSummaryEntity.joinRules = ContentMapper.map(joinRulesEvent?.content).toModel<RoomJoinRulesContent>()?.joinRules
         roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent
         roomSummaryEntity.canonicalAlias = ContentMapper.map(lastCanonicalAliasEvent?.content).toModel<RoomCanonicalAliasContent>()
                 ?.canonicalAlias
@@ -163,4 +185,234 @@ internal class RoomSummaryUpdater @Inject constructor(
         roomSummaryEntity.updateHasFailedSending()
         roomSummaryEntity.latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
     }
+
+    /**
+     * Should be called at the end of the room sync, to check and validate all parent/child relations
+     */
+    fun validateSpaceRelationship(realm: Realm) {
+        measureTimeMillis {
+            val lookupMap = realm.where(RoomSummaryEntity::class.java)
+                    .process(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.activeMemberships())
+                    .equalTo(RoomSummaryEntityFields.IS_DIRECT, false)
+                    // we order by roomID to be consistent when breaking parent/child cycles
+                    .sort(RoomSummaryEntityFields.ROOM_ID)
+                    .findAll().map {
+                        it.flattenParentIds = null
+                        it to emptyList<RoomSummaryEntity>().toMutableSet()
+                    }
+                    .toMap()
+
+            lookupMap.keys.forEach { lookedUp ->
+                if (lookedUp.roomType == RoomType.SPACE) {
+                    // get childrens
+
+                    lookedUp.children.clearWith { it.deleteFromRealm() }
+
+                    RoomChildRelationInfo(realm, lookedUp.roomId).getDirectChildrenDescriptions().forEach { child ->
+
+                        lookedUp.children.add(
+                                realm.createObject<SpaceChildSummaryEntity>().apply {
+                                    this.childRoomId = child.roomId
+                                    this.childSummaryEntity = RoomSummaryEntity.where(realm, child.roomId).findFirst()
+                                    this.order = child.order
+                                    this.autoJoin = child.autoJoin
+                                    this.viaServers.addAll(child.viaServers)
+                                }
+                        )
+
+                        RoomSummaryEntity.where(realm, child.roomId)
+                                .process(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.activeMemberships())
+                                .findFirst()
+                                ?.let { childSum ->
+                                    lookupMap.entries.firstOrNull { it.key.roomId == lookedUp.roomId }?.let { entry ->
+                                        if (entry.value.indexOfFirst { it.roomId == childSum.roomId } == -1) {
+                                            // add looked up as a parent
+                                            entry.value.add(childSum)
+                                        }
+                                    }
+                                }
+                    }
+                } else {
+                    lookedUp.parents.clearWith { it.deleteFromRealm() }
+                    // can we check parent relations here??
+                    RoomChildRelationInfo(realm, lookedUp.roomId).getParentDescriptions()
+                            .map { parentInfo ->
+
+                                lookedUp.parents.add(
+                                        realm.createObject<SpaceParentSummaryEntity>().apply {
+                                            this.parentRoomId = parentInfo.roomId
+                                            this.parentSummaryEntity = RoomSummaryEntity.where(realm, parentInfo.roomId).findFirst()
+                                            this.canonical = parentInfo.canonical
+                                            this.viaServers.addAll(parentInfo.viaServers)
+                                        }
+                                )
+
+                                RoomSummaryEntity.where(realm, parentInfo.roomId)
+                                        .process(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.activeMemberships())
+                                        .findFirst()
+                                        ?.let { parentSum ->
+                                            if (lookupMap[parentSum]?.indexOfFirst { it.roomId == lookedUp.roomId } == -1) {
+                                                // add lookedup as a parent
+                                                lookupMap[parentSum]?.add(lookedUp)
+                                            }
+                                        }
+                            }
+                }
+            }
+
+            // Simple algorithm to break cycles
+            // Need more work to decide how to break, probably need to be as consistent as possible
+            // and also find best way to root the tree
+
+            val graph = Graph()
+            lookupMap
+                    // focus only on joined spaces, as room are just leaf
+                    .filter { it.key.roomType == RoomType.SPACE && it.key.membership == Membership.JOIN }
+                    .forEach { (sum, children) ->
+                        graph.getOrCreateNode(sum.roomId)
+                        children.forEach {
+                            graph.addEdge(it.roomId, sum.roomId)
+                        }
+                    }
+
+            val backEdges = graph.findBackwardEdges()
+            Timber.v("## SPACES: Cycle detected = ${backEdges.isNotEmpty()}")
+
+            // break cycles
+            backEdges.forEach { edge ->
+                lookupMap.entries.find { it.key.roomId == edge.source.name }?.let {
+                    it.value.removeAll { it.roomId == edge.destination.name }
+                }
+            }
+
+            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 ->
+
+//                                    Timber.w("## SPACES: ${childSum.name} is ${childSum.roomId} fc: ${childSum.flattenParentIds}")
+//                                    var allParents = 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}")
+                                }
+                            }
+                        }
+                    }
+
+            // we need also to filter DMs...
+            // it's more annoying as based on if the other members belong the space or not
+            RoomSummaryEntity.where(realm)
+                    .equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
+                    .process(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.activeMemberships())
+                    .findAll()
+                    .forEach { dmRoom ->
+                        val relatedSpaces = lookupMap.keys
+                                .filter { it.roomType == RoomType.SPACE }
+                                .filter {
+                                    dmRoom.otherMemberIds.toList().intersect(it.otherMemberIds.toList()).isNotEmpty()
+                                }
+                                .map { it.roomId }
+                                .distinct()
+                        val flattenRelated = mutableListOf<String>().apply {
+                            addAll(relatedSpaces)
+                            relatedSpaces.map { flattenSpaceParents[it] }.forEach {
+                                if (it != null) addAll(it)
+                            }
+                        }.distinct()
+                        if (flattenRelated.isNotEmpty()) {
+                            // we keep real m.child/m.parent relations and add the one for common memberships
+                            dmRoom.flattenParentIds += "|${flattenRelated.joinToString("|")}|"
+                        }
+//                        Timber.v("## SPACES: flatten of ${dmRoom.otherMemberIds.joinToString(",")} is ${dmRoom.flattenParentIds}")
+                    }
+
+            // Maybe a good place to count the number of notifications for spaces?
+
+            realm.where(RoomSummaryEntity::class.java)
+                    .process(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.activeMemberships())
+                    .equalTo(RoomSummaryEntityFields.ROOM_TYPE, RoomType.SPACE)
+                    .findAll().forEach { space ->
+                        // get all children
+                        var highlightCount = 0
+                        var notificationCount = 0
+                        realm.where(RoomSummaryEntity::class.java)
+                                .process(RoomSummaryEntityFields.MEMBERSHIP_STR, listOf(Membership.JOIN))
+                                .notEqualTo(RoomSummaryEntityFields.ROOM_TYPE, RoomType.SPACE)
+                                // also we do not count DM in here, because home space will already show them
+                                .equalTo(RoomSummaryEntityFields.IS_DIRECT, false)
+                                .contains(RoomSummaryEntityFields.FLATTEN_PARENT_IDS, space.roomId)
+                                .findAll().forEach {
+                                    highlightCount += it.highlightCount
+                                    notificationCount += it.notificationCount
+                                }
+
+                        space.highlightCount = highlightCount
+                        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")
+        }
+    }
+
+//    private fun isValidCanonical() : Boolean {
+//
+//    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt
index c38dcd00a73387e3ec1fe0697a607ef2bd8141ed..a7cba2fe99ce93432a31e48638b8630080692291 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt
@@ -149,7 +149,7 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
                     }
                             ?: ChunkEntity.create(realm, prevToken, nextToken)
 
-                    if (receivedChunk.events.isEmpty() && !receivedChunk.hasMore()) {
+                    if (receivedChunk.events.isNullOrEmpty() && !receivedChunk.hasMore()) {
                         handleReachEnd(realm, roomId, direction, currentChunk)
                     } else {
                         handlePagination(realm, roomId, direction, receivedChunk, currentChunk)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt
index c3b2d7f161998bc38abccec3832cca4967cec2ce..fad1840e51b07349427572f59e3744a321f63dc0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt
@@ -18,12 +18,14 @@
 
 package org.matrix.android.sdk.internal.session.securestorage
 
+import android.annotation.SuppressLint
 import android.content.Context
 import android.os.Build
 import android.security.KeyPairGeneratorSpec
 import android.security.keystore.KeyGenParameterSpec
 import android.security.keystore.KeyProperties
 import androidx.annotation.RequiresApi
+import org.matrix.android.sdk.internal.util.system.BuildVersionSdkIntProvider
 import timber.log.Timber
 import java.io.ByteArrayInputStream
 import java.io.ByteArrayOutputStream
@@ -32,6 +34,7 @@ import java.io.InputStream
 import java.io.ObjectInputStream
 import java.io.ObjectOutputStream
 import java.io.OutputStream
+import java.lang.IllegalArgumentException
 import java.math.BigInteger
 import java.security.KeyPairGenerator
 import java.security.KeyStore
@@ -58,23 +61,19 @@ import javax.security.auth.x500.X500Principal
  * is not available.
  *
  * <b>Android [K-M[</b>
- * For android >=KITKAT and <M, we use the keystore to generate and store a private/public key pair. Then for each secret, a
+ * For android >=L and <M, we use the keystore to generate and store a private/public key pair. Then for each secret, a
  * random secret key in generated to perform encryption.
  * This secret key is encrypted with the public RSA key and stored with the encrypted secret.
  * In order to decrypt the encrypted secret key will be retrieved then decrypted with the RSA private key.
  *
- * <b>Older androids</b>
- * For older androids as a fallback we generate an AES key from the alias using PBKDF2 with random salt.
- * The salt and iv are stored with encrypted data.
- *
  * Sample usage:
  * <code>
  *     val secret = "The answer is 42"
- *     val KEncrypted = SecretStoringUtils.securelyStoreString(secret, "myAlias", context)
+ *     val KEncrypted = SecretStoringUtils.securelyStoreString(secret, "myAlias")
  *     //This can be stored anywhere e.g. encoded in b64 and stored in preference for example
  *
  *     //to get back the secret, just call
- *     val kDecrypted = SecretStoringUtils.loadSecureSecret(KEncrypted!!, "myAlias", context)
+ *     val kDecrypted = SecretStoringUtils.loadSecureSecret(KEncrypted, "myAlias")
  * </code>
  *
  * You can also just use this utility to store a secret key, and use any encryption algorithm that you want.
@@ -82,7 +81,10 @@ import javax.security.auth.x500.X500Principal
  * Important: Keys stored in the keystore can be wiped out (depends of the OS version, like for example if you
  * add a pin or change the schema); So you might and with a useless pile of bytes.
  */
-internal class SecretStoringUtils @Inject constructor(private val context: Context) {
+internal class SecretStoringUtils @Inject constructor(
+        private val context: Context,
+        private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider
+) {
 
     companion object {
         private const val ANDROID_KEY_STORE = "AndroidKeyStore"
@@ -91,7 +93,6 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte
 
         private const val FORMAT_API_M: Byte = 0
         private const val FORMAT_1: Byte = 1
-        private const val FORMAT_2: Byte = 2
     }
 
     private val keyStore: KeyStore by lazy {
@@ -113,43 +114,52 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte
     /**
      * Encrypt the given secret using the android Keystore.
      * On android >= M, will directly use the keystore to generate a symmetric key
-     * On android >= KitKat and <M, as symmetric key gen is not available, will use an symmetric key generated
+     * On android >= Lollipop and <M, as symmetric key gen is not available, will use an symmetric key generated
      * in the keystore to encrypted a random symmetric key. The encrypted symmetric key is returned
      * in the bytearray (in can be stored anywhere, it is encrypted)
-     * On older version a key in generated from alias with random salt.
      *
      * The secret is encrypted using the following method: AES/GCM/NoPadding
      */
+    @SuppressLint("NewApi")
     @Throws(Exception::class)
-    fun securelyStoreString(secret: String, keyAlias: String): ByteArray? {
+    fun securelyStoreString(secret: String, keyAlias: String): ByteArray {
         return when {
-            Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> encryptStringM(secret, keyAlias)
-            else                                           -> encryptString(secret, keyAlias)
+            buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> encryptStringM(secret, keyAlias)
+            else                                                      -> encryptString(secret, keyAlias)
         }
     }
 
     /**
      * Decrypt a secret that was encrypted by #securelyStoreString()
      */
+    @SuppressLint("NewApi")
     @Throws(Exception::class)
-    fun loadSecureSecret(encrypted: ByteArray, keyAlias: String): String? {
-        return when {
-            Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> decryptStringM(encrypted, keyAlias)
-            else                                           -> decryptString(encrypted, keyAlias)
+    fun loadSecureSecret(encrypted: ByteArray, keyAlias: String): String {
+        encrypted.inputStream().use { inputStream ->
+            // First get the format
+            return when (val format = inputStream.read().toByte()) {
+                FORMAT_API_M -> decryptStringM(inputStream, keyAlias)
+                FORMAT_1     -> decryptString(inputStream, keyAlias)
+                else         -> throw IllegalArgumentException("Unknown format $format")
+            }
         }
     }
 
+    @SuppressLint("NewApi")
     fun securelyStoreObject(any: Any, keyAlias: String, output: OutputStream) {
         when {
-            Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> saveSecureObjectM(keyAlias, output, any)
-            else                                           -> saveSecureObject(keyAlias, output, any)
+            buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> saveSecureObjectM(keyAlias, output, any)
+            else                                                      -> saveSecureObject(keyAlias, output, any)
         }
     }
 
+    @SuppressLint("NewApi")
     fun <T> loadSecureSecret(inputStream: InputStream, keyAlias: String): T? {
-        return when {
-            Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> loadSecureObjectM(keyAlias, inputStream)
-            else                                           -> loadSecureObject(keyAlias, inputStream)
+        // First get the format
+        return when (val format = inputStream.read().toByte()) {
+            FORMAT_API_M -> loadSecureObjectM(keyAlias, inputStream)
+            FORMAT_1     -> loadSecureObject(keyAlias, inputStream)
+            else         -> throw IllegalArgumentException("Unknown format $format")
         }
     }
 
@@ -180,7 +190,7 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte
         - Store the encrypted AES
      Generate a key pair for encryption
      */
-    fun getOrGenerateKeyPairForAlias(alias: String): KeyStore.PrivateKeyEntry {
+    private fun getOrGenerateKeyPairForAlias(alias: String): KeyStore.PrivateKeyEntry {
         val privateKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.PrivateKeyEntry)
 
         if (privateKeyEntry != null) return privateKeyEntry
@@ -193,7 +203,7 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte
                 .setAlias(alias)
                 .setSubject(X500Principal("CN=$alias"))
                 .setSerialNumber(BigInteger.TEN)
-                // .setEncryptionRequired() requires that the phone as a pin/schema
+                // .setEncryptionRequired() requires that the phone has a pin/schema
                 .setStartDate(start.time)
                 .setEndDate(end.time)
                 .build()
@@ -205,7 +215,7 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte
     }
 
     @RequiresApi(Build.VERSION_CODES.M)
-    fun encryptStringM(text: String, keyAlias: String): ByteArray? {
+    private fun encryptStringM(text: String, keyAlias: String): ByteArray {
         val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias)
 
         val cipher = Cipher.getInstance(AES_MODE)
@@ -217,8 +227,8 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte
     }
 
     @RequiresApi(Build.VERSION_CODES.M)
-    private fun decryptStringM(encryptedChunk: ByteArray, keyAlias: String): String {
-        val (iv, encryptedText) = formatMExtract(encryptedChunk.inputStream())
+    private fun decryptStringM(inputStream: InputStream, keyAlias: String): String {
+        val (iv, encryptedText) = formatMExtract(inputStream)
 
         val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias)
 
@@ -229,7 +239,7 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte
         return String(cipher.doFinal(encryptedText), Charsets.UTF_8)
     }
 
-    private fun encryptString(text: String, keyAlias: String): ByteArray? {
+    private fun encryptString(text: String, keyAlias: String): ByteArray {
         // we generate a random symmetric key
         val key = ByteArray(16)
         secureRandom.nextBytes(key)
@@ -246,8 +256,8 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte
         return format1Make(encryptedKey, iv, encryptedBytes)
     }
 
-    private fun decryptString(data: ByteArray, keyAlias: String): String? {
-        val (encryptedKey, iv, encrypted) = format1Extract(ByteArrayInputStream(data))
+    private fun decryptString(inputStream: InputStream, keyAlias: String): String {
+        val (encryptedKey, iv, encrypted) = format1Extract(inputStream)
 
         // we need to decrypt the key
         val sKeyBytes = rsaDecrypt(keyAlias, ByteArrayInputStream(encryptedKey))
@@ -307,30 +317,11 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte
         output.write(bos1.toByteArray())
     }
 
-//    @RequiresApi(Build.VERSION_CODES.M)
-//    @Throws(IOException::class)
-//    fun saveSecureObjectM(keyAlias: String, file: File, writeObject: Any) {
-//        FileOutputStream(file).use {
-//            saveSecureObjectM(keyAlias, it, writeObject)
-//        }
-//    }
-//
-//    @RequiresApi(Build.VERSION_CODES.M)
-//    @Throws(IOException::class)
-//    fun <T> loadSecureObjectM(keyAlias: String, file: File): T? {
-//        FileInputStream(file).use {
-//            return loadSecureObjectM<T>(keyAlias, it)
-//        }
-//    }
-
     @RequiresApi(Build.VERSION_CODES.M)
     @Throws(IOException::class)
     private fun <T> loadSecureObjectM(keyAlias: String, inputStream: InputStream): T? {
         val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias)
 
-        val format = inputStream.read()
-        assert(format.toByte() == FORMAT_API_M)
-
         val ivSize = inputStream.read()
         val iv = ByteArray(ivSize)
         inputStream.read(iv, 0, ivSize)
@@ -393,9 +384,6 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte
     }
 
     private fun formatMExtract(bis: InputStream): Pair<ByteArray, ByteArray> {
-        val format = bis.read().toByte()
-        assert(format == FORMAT_API_M)
-
         val ivSize = bis.read()
         val iv = ByteArray(ivSize)
         bis.read(iv, 0, ivSize)
@@ -414,9 +402,6 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte
     }
 
     private fun format1Extract(bis: InputStream): Triple<ByteArray, ByteArray, ByteArray> {
-        val format = bis.read()
-        assert(format.toByte() == FORMAT_1)
-
         val keySizeBig = bis.read()
         val keySizeLow = bis.read()
         val encryptedKeySize = keySizeBig.shl(8) + keySizeLow
@@ -443,32 +428,4 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte
 
         return bos.toByteArray()
     }
-
-    private fun format2Make(salt: ByteArray, iv: ByteArray, encryptedBytes: ByteArray): ByteArray {
-        val bos = ByteArrayOutputStream(3 + salt.size + iv.size + encryptedBytes.size)
-        bos.write(FORMAT_2.toInt())
-        bos.write(salt.size)
-        bos.write(salt)
-        bos.write(iv.size)
-        bos.write(iv)
-        bos.write(encryptedBytes)
-
-        return bos.toByteArray()
-    }
-
-    private fun format2Extract(bis: InputStream): Triple<ByteArray, ByteArray, ByteArray> {
-        val format = bis.read()
-        assert(format.toByte() == FORMAT_2)
-
-        val saltSize = bis.read()
-        val salt = ByteArray(saltSize)
-        bis.read(salt)
-
-        val ivSize = bis.read()
-        val iv = ByteArray(ivSize)
-        bis.read(iv)
-
-        val encrypted = bis.readBytes()
-        return Triple(salt, iv, encrypted)
-    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpace.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpace.kt
new file mode 100644
index 0000000000000000000000000000000000000000..70c52bf4aec8169596a706a6929af844e774b65c
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpace.kt
@@ -0,0 +1,154 @@
+/*
+ * 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.space
+
+import org.matrix.android.sdk.api.query.QueryStringValue
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.toContent
+import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.room.Room
+import org.matrix.android.sdk.api.session.room.model.RoomSummary
+import org.matrix.android.sdk.api.session.space.Space
+import org.matrix.android.sdk.api.session.space.model.SpaceChildContent
+import org.matrix.android.sdk.internal.session.permalinks.ViaParameterFinder
+import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
+
+internal class DefaultSpace(
+        private val room: Room,
+        private val spaceSummaryDataSource: RoomSummaryDataSource,
+        private val viaParameterFinder: ViaParameterFinder
+) : Space {
+
+    override fun asRoom(): Room {
+        return room
+    }
+
+    override val spaceId = room.roomId
+
+    override suspend fun leave(reason: String?) {
+        return room.leave(reason)
+    }
+
+    override fun spaceSummary(): RoomSummary? {
+        return spaceSummaryDataSource.getSpaceSummary(room.roomId)
+    }
+
+    override suspend fun addChildren(roomId: String,
+                                     viaServers: List<String>?,
+                                     order: String?,
+                                     autoJoin: Boolean,
+                                     suggested: Boolean?) {
+        // Find best via
+
+        room.sendStateEvent(
+                eventType = EventType.STATE_SPACE_CHILD,
+                stateKey = roomId,
+                body = SpaceChildContent(
+                        via = viaServers ?: viaParameterFinder.computeViaParams(roomId, 3),
+                        autoJoin = autoJoin,
+                        order = order,
+                        suggested = suggested
+                ).toContent()
+        )
+    }
+
+    override suspend fun removeChildren(roomId: String) {
+//        val existing = room.getStateEvents(setOf(EventType.STATE_SPACE_CHILD), QueryStringValue.Equals(roomId))
+//                .firstOrNull()
+//                ?.content.toModel<SpaceChildContent>()
+//                ?: // should we throw here?
+//                return
+
+        // edit state event and set via to null
+        room.sendStateEvent(
+                eventType = EventType.STATE_SPACE_CHILD,
+                stateKey = roomId,
+                body = SpaceChildContent(
+                        order = null,
+                        via = null,
+                        autoJoin = null,
+                        suggested = null
+                ).toContent()
+        )
+    }
+
+    override suspend fun setChildrenOrder(roomId: String, order: String?) {
+        val existing = room.getStateEvents(setOf(EventType.STATE_SPACE_CHILD), QueryStringValue.Equals(roomId))
+                .firstOrNull()
+                ?.content.toModel<SpaceChildContent>()
+                ?: throw IllegalArgumentException("$roomId is not a child of this space")
+
+        // edit state event and set via to null
+        room.sendStateEvent(
+                eventType = EventType.STATE_SPACE_CHILD,
+                stateKey = roomId,
+                body = SpaceChildContent(
+                        order = order,
+                        via = existing.via,
+                        autoJoin = existing.autoJoin,
+                        suggested = existing.suggested
+                ).toContent()
+        )
+    }
+
+    override suspend fun setChildrenAutoJoin(roomId: String, autoJoin: Boolean) {
+        val existing = room.getStateEvents(setOf(EventType.STATE_SPACE_CHILD), QueryStringValue.Equals(roomId))
+                .firstOrNull()
+                ?.content.toModel<SpaceChildContent>()
+                ?: throw IllegalArgumentException("$roomId is not a child of this space")
+
+        if (existing.autoJoin == autoJoin) {
+            // nothing to do?
+            return
+        }
+
+        // edit state event and set via to null
+        room.sendStateEvent(
+                eventType = EventType.STATE_SPACE_CHILD,
+                stateKey = roomId,
+                body = SpaceChildContent(
+                        order = existing.order,
+                        via = existing.via,
+                        autoJoin = autoJoin,
+                        suggested = existing.suggested
+                ).toContent()
+        )
+    }
+
+    override suspend fun setChildrenSuggested(roomId: String, suggested: Boolean) {
+        val existing = room.getStateEvents(setOf(EventType.STATE_SPACE_CHILD), QueryStringValue.Equals(roomId))
+                .firstOrNull()
+                ?.content.toModel<SpaceChildContent>()
+                ?: throw IllegalArgumentException("$roomId is not a child of this space")
+
+        if (existing.suggested == suggested) {
+            // nothing to do?
+            return
+        }
+        // edit state event and set via to null
+        room.sendStateEvent(
+                eventType = EventType.STATE_SPACE_CHILD,
+                stateKey = roomId,
+                body = SpaceChildContent(
+                        order = existing.order,
+                        via = existing.via,
+                        autoJoin = existing.autoJoin,
+                        suggested = suggested
+                ).toContent()
+        )
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d0ad19245f9860a2982b96a1eac15d865bd12637
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt
@@ -0,0 +1,207 @@
+/*
+ * 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.space
+
+import android.net.Uri
+import androidx.lifecycle.LiveData
+import org.matrix.android.sdk.api.query.QueryStringValue
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.toContent
+import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.room.model.GuestAccess
+import org.matrix.android.sdk.api.session.room.model.Membership
+import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
+import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
+import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
+import org.matrix.android.sdk.api.session.room.model.RoomSummary
+import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
+import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset
+import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
+import org.matrix.android.sdk.api.session.space.CreateSpaceParams
+import org.matrix.android.sdk.api.session.space.JoinSpaceResult
+import org.matrix.android.sdk.api.session.space.Space
+import org.matrix.android.sdk.api.session.space.SpaceService
+import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams
+import org.matrix.android.sdk.api.session.space.model.SpaceChildContent
+import org.matrix.android.sdk.api.session.space.model.SpaceParentContent
+import org.matrix.android.sdk.internal.di.UserId
+import org.matrix.android.sdk.internal.session.room.RoomGetter
+import org.matrix.android.sdk.internal.session.room.SpaceGetter
+import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask
+import org.matrix.android.sdk.internal.session.room.membership.leaving.LeaveRoomTask
+import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
+import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
+import org.matrix.android.sdk.internal.session.space.peeking.PeekSpaceTask
+import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult
+import javax.inject.Inject
+
+internal class DefaultSpaceService @Inject constructor(
+        @UserId private val userId: String,
+        private val createRoomTask: CreateRoomTask,
+        private val joinSpaceTask: JoinSpaceTask,
+        private val spaceGetter: SpaceGetter,
+        private val roomGetter: RoomGetter,
+        private val roomSummaryDataSource: RoomSummaryDataSource,
+        private val stateEventDataSource: StateEventDataSource,
+        private val peekSpaceTask: PeekSpaceTask,
+        private val resolveSpaceInfoTask: ResolveSpaceInfoTask,
+        private val leaveRoomTask: LeaveRoomTask
+) : SpaceService {
+
+    override suspend fun createSpace(params: CreateSpaceParams): String {
+        return createRoomTask.executeRetry(params, 3)
+    }
+
+    override suspend fun createSpace(name: String, topic: String?, avatarUri: Uri?, isPublic: Boolean): String {
+        return createSpace(CreateSpaceParams().apply {
+            this.name = name
+            this.topic = topic
+            this.avatarUri = avatarUri
+            if (isPublic) {
+                this.powerLevelContentOverride = (powerLevelContentOverride ?: PowerLevelsContent()).copy(
+                        invite = 0
+                )
+                this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT
+                this.historyVisibility = RoomHistoryVisibility.WORLD_READABLE
+                this.guestAccess = GuestAccess.CanJoin
+            } else {
+                this.preset = CreateRoomPreset.PRESET_PRIVATE_CHAT
+                visibility = RoomDirectoryVisibility.PRIVATE
+            }
+        })
+    }
+
+    override fun getSpace(spaceId: String): Space? {
+        return spaceGetter.get(spaceId)
+    }
+
+    override fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams): LiveData<List<RoomSummary>> {
+        return roomSummaryDataSource.getSpaceSummariesLive(queryParams)
+    }
+
+    override fun getSpaceSummaries(spaceSummaryQueryParams: SpaceSummaryQueryParams): List<RoomSummary> {
+        return roomSummaryDataSource.getSpaceSummaries(spaceSummaryQueryParams)
+    }
+
+    override fun getRootSpaceSummaries(): List<RoomSummary> {
+        return roomSummaryDataSource.getRootSpaceSummaries()
+    }
+
+    override suspend fun peekSpace(spaceId: String): SpacePeekResult {
+        return peekSpaceTask.execute(PeekSpaceTask.Params(spaceId))
+    }
+
+    override suspend fun querySpaceChildren(spaceId: String,
+                                            suggestedOnly: Boolean?,
+                                            autoJoinedOnly: Boolean?): Pair<RoomSummary, List<SpaceChildInfo>> {
+        return resolveSpaceInfoTask.execute(ResolveSpaceInfoTask.Params.withId(spaceId, suggestedOnly, autoJoinedOnly)).let { response ->
+            val spaceDesc = response.rooms?.firstOrNull { it.roomId == spaceId }
+            Pair(
+                    first = RoomSummary(
+                            roomId = spaceDesc?.roomId ?: spaceId,
+                            roomType = spaceDesc?.roomType,
+                            name = spaceDesc?.name ?: "",
+                            displayName = spaceDesc?.name ?: "",
+                            topic = spaceDesc?.topic ?: "",
+                            joinedMembersCount = spaceDesc?.numJoinedMembers,
+                            avatarUrl = spaceDesc?.avatarUrl ?: "",
+                            encryptionEventTs = null,
+                            typingUsers = emptyList(),
+                            isEncrypted = false,
+                            flattenParentIds = emptyList()
+                    ),
+                    second = response.rooms
+                            ?.filter { it.roomId != spaceId }
+                            ?.flatMap { childSummary ->
+                                response.events
+                                        ?.filter { it.stateKey == childSummary.roomId && it.type == EventType.STATE_SPACE_CHILD }
+                                        ?.mapNotNull { childStateEv ->
+                                            // create a child entry for everytime this room is the child of a space
+                                            // beware that a room could appear then twice in this list
+                                            childStateEv.content.toModel<SpaceChildContent>()?.let { childStateEvContent ->
+                                                SpaceChildInfo(
+                                                        childRoomId = childSummary.roomId,
+                                                        isKnown = true,
+                                                        roomType = childSummary.roomType,
+                                                        name = childSummary.name,
+                                                        topic = childSummary.topic,
+                                                        avatarUrl = childSummary.avatarUrl,
+                                                        order = childStateEvContent.order,
+                                                        autoJoin = childStateEvContent.autoJoin ?: false,
+                                                        viaServers = childStateEvContent.via.orEmpty(),
+                                                        activeMemberCount = childSummary.numJoinedMembers,
+                                                        parentRoomId = childStateEv.roomId,
+                                                        suggested = childStateEvContent.suggested,
+                                                        canonicalAlias = childSummary.canonicalAlias,
+                                                        aliases = childSummary.aliases
+                                                )
+                                            }
+                                        }.orEmpty()
+                            }
+                            .orEmpty()
+            )
+        }
+    }
+
+    override suspend fun joinSpace(spaceIdOrAlias: String,
+                                   reason: String?,
+                                   viaServers: List<String>): JoinSpaceResult {
+        return joinSpaceTask.execute(JoinSpaceTask.Params(spaceIdOrAlias, reason, viaServers))
+    }
+
+    override suspend fun rejectInvite(spaceId: String, reason: String?) {
+        leaveRoomTask.execute(LeaveRoomTask.Params(spaceId, reason))
+    }
+
+//    override fun getSpaceParentsOfRoom(roomId: String): List<SpaceSummary> {
+//        return spaceSummaryDataSource.getParentsOfRoom(roomId)
+//    }
+
+    override suspend fun setSpaceParent(childRoomId: String, parentSpaceId: String, canonical: Boolean, viaServers: List<String>) {
+        // Should we perform some validation here?,
+        // and if client want to bypass, it could use sendStateEvent directly?
+        if (canonical) {
+            // check that we can send m.child in the parent room
+            if (roomSummaryDataSource.getRoomSummary(parentSpaceId)?.membership != Membership.JOIN) {
+                throw UnsupportedOperationException("Cannot add canonical child if not member of parent")
+            }
+            val powerLevelsEvent = stateEventDataSource.getStateEvent(
+                    roomId = parentSpaceId,
+                    eventType = EventType.STATE_ROOM_POWER_LEVELS,
+                    stateKey = QueryStringValue.NoCondition
+            )
+            val powerLevelsContent = powerLevelsEvent?.content?.toModel<PowerLevelsContent>()
+                    ?: throw UnsupportedOperationException("Cannot add canonical child, missing powerlevel")
+            val powerLevelsHelper = PowerLevelsHelper(powerLevelsContent)
+            if (!powerLevelsHelper.isUserAllowedToSend(userId, true, EventType.STATE_SPACE_CHILD)) {
+                throw UnsupportedOperationException("Cannot add canonical child, not enough power level")
+            }
+        }
+
+        val room = roomGetter.getRoom(childRoomId)
+                ?: throw IllegalArgumentException("Unknown Room $childRoomId")
+
+        room.sendStateEvent(
+                eventType = EventType.STATE_SPACE_PARENT,
+                stateKey = parentSpaceId,
+                body = SpaceParentContent(
+                        via = viaServers,
+                        canonical = canonical
+                ).toContent()
+        )
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e9d5ba519370037fc306c58448eed3f45fe5b750
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/JoinSpaceTask.kt
@@ -0,0 +1,136 @@
+/*
+ * 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.space
+
+import io.realm.RealmConfiguration
+import kotlinx.coroutines.TimeoutCancellationException
+import org.matrix.android.sdk.api.session.room.model.Membership
+import org.matrix.android.sdk.api.session.room.model.RoomType
+import org.matrix.android.sdk.api.session.space.JoinSpaceResult
+import org.matrix.android.sdk.internal.database.awaitNotEmptyResult
+import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
+import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
+import org.matrix.android.sdk.internal.di.SessionDatabase
+import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask
+import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryDataSource
+import org.matrix.android.sdk.internal.task.Task
+import timber.log.Timber
+import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+
+internal interface JoinSpaceTask : Task<JoinSpaceTask.Params, JoinSpaceResult> {
+    data class Params(
+            val roomIdOrAlias: String,
+            val reason: String?,
+            val viaServers: List<String> = emptyList()
+    )
+}
+
+internal class DefaultJoinSpaceTask @Inject constructor(
+        private val joinRoomTask: JoinRoomTask,
+        @SessionDatabase
+        private val realmConfiguration: RealmConfiguration,
+        private val roomSummaryDataSource: RoomSummaryDataSource
+) : JoinSpaceTask {
+
+    override suspend fun execute(params: JoinSpaceTask.Params): JoinSpaceResult {
+        Timber.v("## Space: > Joining root space ${params.roomIdOrAlias} ...")
+        try {
+            joinRoomTask.execute(JoinRoomTask.Params(
+                    params.roomIdOrAlias,
+                    params.reason,
+                    params.viaServers
+            ))
+        } catch (failure: Throwable) {
+            return JoinSpaceResult.Fail(failure)
+        }
+        Timber.v("## Space: < Joining root space done for ${params.roomIdOrAlias}")
+        // we want to wait for sync result to check for auto join rooms
+
+        Timber.v("## Space: > Wait for post joined sync ${params.roomIdOrAlias} ...")
+        try {
+            awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(2L)) { realm ->
+                realm.where(RoomSummaryEntity::class.java)
+                        .apply {
+                            if (params.roomIdOrAlias.startsWith("!")) {
+                                equalTo(RoomSummaryEntityFields.ROOM_ID, params.roomIdOrAlias)
+                            } else {
+                                equalTo(RoomSummaryEntityFields.CANONICAL_ALIAS, params.roomIdOrAlias)
+                            }
+                        }
+                        .equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name)
+            }
+        } catch (exception: TimeoutCancellationException) {
+            Timber.w("## Space: > Error created with timeout")
+            return JoinSpaceResult.PartialSuccess(emptyMap())
+        }
+
+        val errors = mutableMapOf<String, Throwable>()
+        Timber.v("## Space: > Sync done ...")
+        // after that i should have the children (? do I need to paginate to get state)
+        val summary = roomSummaryDataSource.getSpaceSummary(params.roomIdOrAlias)
+        Timber.v("## Space: Found space summary Name:[${summary?.name}] children: ${summary?.spaceChildren?.size}")
+        summary?.spaceChildren?.forEach {
+//            val childRoomSummary = it.roomSummary ?: return@forEach
+            Timber.v("## Space: Processing child :[${it.childRoomId}] autoJoin:${it.autoJoin}")
+            if (it.autoJoin) {
+                // I should try to join as well
+                if (it.roomType == RoomType.SPACE) {
+                    // recursively join auto-joined child of this space?
+                    when (val subspaceJoinResult = execute(JoinSpaceTask.Params(it.childRoomId, null, it.viaServers))) {
+                        JoinSpaceResult.Success           -> {
+                            // nop
+                        }
+                        is JoinSpaceResult.Fail           -> {
+                            errors[it.childRoomId] = subspaceJoinResult.error
+                        }
+                        is JoinSpaceResult.PartialSuccess -> {
+                            errors.putAll(subspaceJoinResult.failedRooms)
+                        }
+                    }
+                } else {
+                    try {
+                        Timber.v("## Space: Joining room child ${it.childRoomId}")
+                        joinRoomTask.execute(JoinRoomTask.Params(
+                                roomIdOrAlias = it.childRoomId,
+                                reason = "Auto-join parent space",
+                                viaServers = it.viaServers
+                        ))
+                    } catch (failure: Throwable) {
+                        errors[it.childRoomId] = failure
+                        Timber.e("## Space: Failed to join room child ${it.childRoomId}")
+                    }
+                }
+            }
+        }
+
+        return if (errors.isEmpty()) {
+            JoinSpaceResult.Success
+        } else {
+            JoinSpaceResult.PartialSuccess(errors)
+        }
+    }
+}
+
+// try {
+//    awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm ->
+//        realm.where(RoomEntity::class.java)
+//                .equalTo(RoomEntityFields.ROOM_ID, roomId)
+//    }
+// } catch (exception: TimeoutCancellationException) {
+//    throw CreateRoomFailure.CreatedWithTimeout
+// }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d2be49b70b0f0f603f2fa79f2f58578ea00d85f0
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/ResolveSpaceInfoTask.kt
@@ -0,0 +1,63 @@
+/*
+ * 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.space
+
+import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
+import org.matrix.android.sdk.internal.network.executeRequest
+import org.matrix.android.sdk.internal.task.Task
+import javax.inject.Inject
+
+internal interface ResolveSpaceInfoTask : Task<ResolveSpaceInfoTask.Params, SpacesResponse> {
+    data class Params(
+            val spaceId: String,
+            val maxRoomPerSpace: Int?,
+            val limit: Int,
+            val batchToken: String?,
+            val suggestedOnly: Boolean?,
+            val autoJoinOnly: Boolean?
+    ) {
+        companion object {
+            fun withId(spaceId: String, suggestedOnly: Boolean?, autoJoinOnly: Boolean?) =
+                    Params(
+                            spaceId = spaceId,
+                            maxRoomPerSpace = 10,
+                            limit = 20,
+                            batchToken = null,
+                            suggestedOnly = suggestedOnly,
+                            autoJoinOnly = autoJoinOnly
+                    )
+        }
+    }
+}
+
+internal class DefaultResolveSpaceInfoTask @Inject constructor(
+        private val spaceApi: SpaceApi,
+        private val globalErrorReceiver: GlobalErrorReceiver
+) : ResolveSpaceInfoTask {
+    override suspend fun execute(params: ResolveSpaceInfoTask.Params): SpacesResponse {
+        val body = SpaceSummaryParams(
+                maxRoomPerSpace = params.maxRoomPerSpace,
+                limit = params.limit,
+                batch = params.batchToken ?: "",
+                autoJoinedOnly = params.autoJoinOnly,
+                suggestedOnly = params.suggestedOnly
+        )
+        return executeRequest(globalErrorReceiver) {
+            spaceApi.getSpaces(params.spaceId, body)
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0fcc95fdb33e208f9ab55bcb8391f90a301ccb92
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceApi.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.space
+
+import org.matrix.android.sdk.internal.network.NetworkConstants
+import retrofit2.http.Body
+import retrofit2.http.POST
+import retrofit2.http.Path
+
+internal interface SpaceApi {
+
+    /**
+     *
+     * POST /_matrix/client/r0/rooms/{roomID}/spaces
+     *  {
+     *    "max_rooms_per_space": 5,      // The maximum number of rooms/subspaces to return for a given space, if negative unbounded. default: -1.
+     *    "auto_join_only": true,        // If true, only return m.space.child events with auto_join:true, default: false, which returns all events.
+     *    "limit": 100,                  // The maximum number of rooms/subspaces to return, server can override this, default: 100.
+     *    "batch": "opaque_string"       // A token to use if this is a subsequent HTTP hit, default: "".
+     *  }
+     *
+     * Ref:
+     * - MSC 2946 https://github.com/matrix-org/matrix-doc/blob/kegan/spaces-summary/proposals/2946-spaces-summary.md
+     * - https://hackmd.io/fNYh4tjUT5mQfR1uuRzWDA
+     */
+    @POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "org.matrix.msc2946/rooms/{roomId}/spaces")
+    suspend fun getSpaces(@Path("roomId") spaceId: String,
+                  @Body params: SpaceSummaryParams): SpacesResponse
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceChildSummaryResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceChildSummaryResponse.kt
new file mode 100644
index 0000000000000000000000000000000000000000..5021ff638f24ad5e92d81fc5aaa9b16ce12bf2af
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceChildSummaryResponse.kt
@@ -0,0 +1,96 @@
+/*
+ * 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.space
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+internal data class SpaceChildSummaryResponse(
+        /**
+         * The total number of state events which point to or from this room (inbound/outbound edges).
+         * This includes all m.space.child events in the room, in addition to m.room.parent events which point to this room as a parent.
+         */
+        @Json(name = "num_refs") val numRefs: Int? = null,
+
+        /**
+         * The room type, which is m.space for subspaces.
+         * It can be omitted if there is no room type in which case it should be interpreted as a normal room.
+         */
+        @Json(name = "room_type") val roomType: String? = null,
+
+        /**
+         * Aliases of the room. May be empty.
+         */
+        @Json(name = "aliases")
+        val aliases: List<String>? = null,
+
+        /**
+         * The canonical alias of the room, if any.
+         */
+        @Json(name = "canonical_alias")
+        val canonicalAlias: String? = null,
+
+        /**
+         * The name of the room, if any.
+         */
+        @Json(name = "name")
+        val name: String? = null,
+
+        /**
+         * Required. The number of members joined to the room.
+         */
+        @Json(name = "num_joined_members")
+        val numJoinedMembers: Int = 0,
+
+        /**
+         * Required. The ID of the room.
+         */
+        @Json(name = "room_id")
+        val roomId: String,
+
+        /**
+         * The topic of the room, if any.
+         */
+        @Json(name = "topic")
+        val topic: String? = null,
+
+        /**
+         * Required. Whether the room may be viewed by guest users without joining.
+         */
+        @Json(name = "world_readable")
+        val worldReadable: Boolean = false,
+
+        /**
+         * Required. Whether guest users may join the room and participate in it. If they can,
+         * they will be subject to ordinary power level rules like any other user.
+         */
+        @Json(name = "guest_can_join")
+        val guestCanJoin: Boolean = false,
+
+        /**
+         * The URL for the room's avatar, if one is set.
+         */
+        @Json(name = "avatar_url")
+        val avatarUrl: String? = null,
+
+        /**
+         * Undocumented item
+         */
+        @Json(name = "m.federate")
+        val isFederated: Boolean = false
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceModule.kt
new file mode 100644
index 0000000000000000000000000000000000000000..87425f4af2f3c0a4ae8e0854c5a6c02f13b3827d
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceModule.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.space
+
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import org.matrix.android.sdk.internal.session.SessionScope
+import org.matrix.android.sdk.internal.session.room.DefaultSpaceGetter
+import org.matrix.android.sdk.internal.session.room.SpaceGetter
+import org.matrix.android.sdk.internal.session.space.peeking.DefaultPeekSpaceTask
+import org.matrix.android.sdk.internal.session.space.peeking.PeekSpaceTask
+import retrofit2.Retrofit
+
+@Module
+internal abstract class SpaceModule {
+
+    @Module
+    companion object {
+        @Provides
+        @JvmStatic
+        @SessionScope
+        fun providesSpacesAPI(retrofit: Retrofit): SpaceApi {
+            return retrofit.create(SpaceApi::class.java)
+        }
+    }
+
+    @Binds
+    abstract fun bindResolveSpaceTask(task: DefaultResolveSpaceInfoTask): ResolveSpaceInfoTask
+
+    @Binds
+    abstract fun bindPeekSpaceTask(task: DefaultPeekSpaceTask): PeekSpaceTask
+
+    @Binds
+    abstract fun bindJoinSpaceTask(task: DefaultJoinSpaceTask): JoinSpaceTask
+
+    @Binds
+    abstract fun bindSpaceGetter(getter: DefaultSpaceGetter): SpaceGetter
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceSummaryParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceSummaryParams.kt
new file mode 100644
index 0000000000000000000000000000000000000000..013db1c286256a277e5a8d55606a3fdbdb518a22
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpaceSummaryParams.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.space
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+internal data class SpaceSummaryParams(
+        /**  The maximum number of rooms/subspaces to return for a given space, if negative unbounded. default: -1 */
+        @Json(name = "max_rooms_per_space") val maxRoomPerSpace: Int?,
+        /** The maximum number of rooms/subspaces to return, server can override this, default: 100 */
+        @Json(name = "limit") val limit: Int?,
+        /** A token to use if this is a subsequent HTTP hit, default: "". */
+        @Json(name = "batch") val batch: String = "",
+        /** whether we should only return children with the "suggested" flag set. */
+        @Json(name = "suggested_only") val suggestedOnly: Boolean?,
+        /** whether we should only return children with the "suggested" flag set. */
+        @Json(name = "auto_join_only") val autoJoinedOnly: Boolean?
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpacesResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpacesResponse.kt
new file mode 100644
index 0000000000000000000000000000000000000000..20d63c881482c247b0c99be29dae7c43a0fff233
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/SpacesResponse.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.space
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.session.events.model.Event
+
+@JsonClass(generateAdapter = true)
+internal data class SpacesResponse(
+        /** Its presence indicates that there are more results to return. */
+        @Json(name = "next_batch") val nextBatch: String? = null,
+        /** Rooms information like name/avatar/type ... */
+        @Json(name = "rooms") val rooms: List<SpaceChildSummaryResponse>? = null,
+        /** These are the edges of the graph. The objects in the array are complete (or stripped?) m.room.parent or m.space.child events. */
+        @Json(name = "events") val events: List<Event>? = null
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/PeekSpaceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/PeekSpaceTask.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f6b156a6fb167bf0a50a9f73578706d24e882f8d
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/PeekSpaceTask.kt
@@ -0,0 +1,143 @@
+/*
+ * 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.space.peeking
+
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.room.model.RoomType
+import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
+import org.matrix.android.sdk.api.session.room.peeking.PeekResult
+import org.matrix.android.sdk.api.session.space.model.SpaceChildContent
+import org.matrix.android.sdk.internal.session.room.peeking.PeekRoomTask
+import org.matrix.android.sdk.internal.session.room.peeking.ResolveRoomStateTask
+import org.matrix.android.sdk.internal.task.Task
+import timber.log.Timber
+import javax.inject.Inject
+
+internal interface PeekSpaceTask : Task<PeekSpaceTask.Params, SpacePeekResult> {
+    data class Params(
+            val roomIdOrAlias: String,
+            // A depth limit as a simple protection against cycles
+            val maxDepth: Int = 4
+    )
+}
+
+internal class DefaultPeekSpaceTask @Inject constructor(
+        private val peekRoomTask: PeekRoomTask,
+        private val resolveRoomStateTask: ResolveRoomStateTask
+) : PeekSpaceTask {
+
+    override suspend fun execute(params: PeekSpaceTask.Params): SpacePeekResult {
+        val peekResult = peekRoomTask.execute(PeekRoomTask.Params(params.roomIdOrAlias))
+        val roomResult = peekResult as? PeekResult.Success ?: return SpacePeekResult.FailedToResolve(params.roomIdOrAlias, peekResult)
+
+        // check the room type
+        // kind of duplicate cause we already did it in Peek? could we pass on the result??
+        val stateEvents = try {
+            resolveRoomStateTask.execute(ResolveRoomStateTask.Params(roomResult.roomId))
+        } catch (failure: Throwable) {
+            return SpacePeekResult.FailedToResolve(params.roomIdOrAlias, peekResult)
+        }
+        val isSpace = stateEvents
+                .lastOrNull { it.type == EventType.STATE_ROOM_CREATE && it.stateKey == "" }
+                ?.content
+                ?.toModel<RoomCreateContent>()
+                ?.type == RoomType.SPACE
+
+        if (!isSpace) return SpacePeekResult.NotSpaceType(params.roomIdOrAlias)
+
+        val children = peekChildren(stateEvents, 0, params.maxDepth)
+
+        return SpacePeekResult.Success(
+                SpacePeekSummary(
+                        params.roomIdOrAlias,
+                        peekResult,
+                        children
+                )
+        )
+    }
+
+    private suspend fun peekChildren(stateEvents: List<Event>, depth: Int, maxDepth: Int): List<ISpaceChild> {
+        if (depth >= maxDepth) return emptyList()
+        val childRoomsIds = stateEvents
+                .filter {
+                    it.type == EventType.STATE_SPACE_CHILD && !it.stateKey.isNullOrEmpty()
+                            // Children where via is not present are ignored.
+                            && it.content?.toModel<SpaceChildContent>()?.via != null
+                }
+                .map { it.stateKey to it.content?.toModel<SpaceChildContent>() }
+
+        Timber.v("## SPACE_PEEK: found ${childRoomsIds.size} present children")
+
+        val spaceChildResults = mutableListOf<ISpaceChild>()
+        childRoomsIds.forEach { entry ->
+
+            Timber.v("## SPACE_PEEK: peeking child $entry")
+            // peek each child
+            val childId = entry.first ?: return@forEach
+            try {
+                val childPeek = peekRoomTask.execute(PeekRoomTask.Params(childId))
+
+                val childStateEvents = resolveRoomStateTask.execute(ResolveRoomStateTask.Params(childId))
+                val createContent = childStateEvents
+                        .lastOrNull { it.type == EventType.STATE_ROOM_CREATE && it.stateKey == "" }
+                        ?.let { it.content?.toModel<RoomCreateContent>() }
+
+                if (!childPeek.isSuccess() || createContent == null) {
+                    Timber.v("## SPACE_PEEK: cannot peek child $entry")
+                    // can't peek :/
+                    spaceChildResults.add(
+                            SpaceChildPeekResult(
+                                    childId, childPeek, entry.second?.autoJoin, entry.second?.order
+                            )
+                    )
+                    // continue to next child
+                    return@forEach
+                }
+                val type = createContent.type
+                if (type == RoomType.SPACE) {
+                    Timber.v("## SPACE_PEEK: subspace child $entry")
+                    spaceChildResults.add(
+                            SpaceSubChildPeekResult(
+                                    childId,
+                                    childPeek,
+                                    entry.second?.autoJoin,
+                                    entry.second?.order,
+                                    peekChildren(childStateEvents, depth + 1, maxDepth)
+                            )
+                    )
+                } else
+                /** if (type == RoomType.MESSAGING || type == null)*/
+                {
+                    Timber.v("## SPACE_PEEK: room child $entry")
+                    spaceChildResults.add(
+                            SpaceChildPeekResult(
+                                    childId, childPeek, entry.second?.autoJoin, entry.second?.order
+                            )
+                    )
+                }
+
+                // let's check child info
+            } catch (failure: Throwable) {
+                // can this happen?
+                Timber.e(failure, "## Failed to resolve space child")
+            }
+        }
+        return spaceChildResults
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/SpacePeekResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/SpacePeekResult.kt
new file mode 100644
index 0000000000000000000000000000000000000000..1df62e94e84e2b89eb1623b8bd9a3d6a76cda6ca
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/peeking/SpacePeekResult.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.space.peeking
+
+import org.matrix.android.sdk.api.session.room.peeking.PeekResult
+
+// TODO Move to api package
+data class SpacePeekSummary(
+        val idOrAlias: String,
+        val roomPeekResult: PeekResult.Success,
+        val children: List<ISpaceChild>
+)
+
+interface ISpaceChild {
+    val id: String
+    val roomPeekResult: PeekResult
+    val default: Boolean?
+    val order: String?
+}
+
+data class SpaceChildPeekResult(
+        override val id: String,
+        override val roomPeekResult: PeekResult,
+        override val default: Boolean? = null,
+        override val order: String? = null
+) : ISpaceChild
+
+data class SpaceSubChildPeekResult(
+        override val id: String,
+        override val roomPeekResult: PeekResult,
+        override val default: Boolean?,
+        override val order: String?,
+        val children: List<ISpaceChild>
+) : ISpaceChild
+
+sealed class SpacePeekResult {
+    abstract class SpacePeekError : SpacePeekResult()
+    data class FailedToResolve(val spaceId: String, val roomPeekResult: PeekResult) : SpacePeekError()
+    data class NotSpaceType(val spaceId: String) : SpacePeekError()
+
+    data class Success(val summary: SpacePeekSummary): SpacePeekResult()
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/CryptoSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/CryptoSyncHandler.kt
index ae60faf9057a9680c1575fd0dd31b68a7d15142f..411a9c5c065eadf238c4a8b43cede5472352fd16 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/CryptoSyncHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/CryptoSyncHandler.kt
@@ -64,7 +64,7 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService:
      * @return true if the event has been decrypted
      */
     private fun decryptToDeviceEvent(event: Event, timelineId: String?): Boolean {
-        Timber.v("## CRYPTO | decryptToDeviceEvent")
+        Timber.v("## CRYPTO | decryptToDeviceEvent")
         if (event.getClearType() == EventType.ENCRYPTED) {
             var result: MXEventDecryptionResult? = null
             try {
@@ -76,7 +76,7 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService:
                 val deviceId = cryptoService.getCryptoDeviceInfo(event.senderId!!).firstOrNull {
                     it.identityKey() == senderKey
                 }?.deviceId ?: senderKey
-                Timber.e("## CRYPTO | Failed to decrypt to device event from ${event.senderId}|$deviceId reason:<${event.mCryptoError ?: exception}>")
+                Timber.e("## CRYPTO | Failed to decrypt to device event from ${event.senderId}|$deviceId reason:<${event.mCryptoError ?: exception}>")
             }
 
             if (null != result) {
@@ -89,7 +89,7 @@ internal class CryptoSyncHandler @Inject constructor(private val cryptoService:
                 return true
             } else {
                 // should not happen
-                Timber.e("## CRYPTO | ERROR NULL DECRYPTION RESULT from ${event.senderId}")
+                Timber.e("## CRYPTO | ERROR NULL DECRYPTION RESULT from ${event.senderId}")
             }
         }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/ReadReceiptHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/ReadReceiptHandler.kt
index e5d9217db71da4c0a25dda37a5bc2494688b9d51..fc1a2c387014d8f51f9068f8667023c64f1b6941 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/ReadReceiptHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/ReadReceiptHandler.kt
@@ -143,7 +143,7 @@ internal class ReadReceiptHandler @Inject constructor(
         @Suppress("UNCHECKED_CAST")
         val content = dataFromFile
                 .events
-                .firstOrNull { it.type == EventType.RECEIPT }
+                ?.firstOrNull { it.type == EventType.RECEIPT }
                 ?.content as? ReadReceiptContent
 
         if (content == null) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt
index 2bb606e921f7536400e4f03820335c4f08a6a82a..7cebbb0192279127bb463ece2a1da75604edd24e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt
@@ -95,8 +95,14 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
         handleRoomSync(realm, HandlingStrategy.JOINED(roomsSyncResponse.join), isInitialSync, aggregator, reporter)
         handleRoomSync(realm, HandlingStrategy.INVITED(roomsSyncResponse.invite), isInitialSync, aggregator, reporter)
         handleRoomSync(realm, HandlingStrategy.LEFT(roomsSyncResponse.leave), isInitialSync, aggregator, reporter)
+
+        // post room sync validation
+//        roomSummaryUpdater.validateSpaceRelationship(realm)
     }
 
+    fun postSyncSpaceHierarchyHandle(realm: Realm) {
+        roomSummaryUpdater.validateSpaceRelationship(realm)
+    }
     // PRIVATE METHODS *****************************************************************************
 
     private fun handleRoomSync(realm: Realm,
@@ -212,6 +218,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                 val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it }
                 val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType)
                 CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
+                    // Timber.v("## Space state event: $eventEntity")
                     eventId = event.eventId
                     root = eventEntity
                 }
@@ -455,7 +462,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
     }
 
     private fun handleRoomAccountDataEvents(realm: Realm, roomId: String, accountData: RoomSyncAccountData) {
-        for (event in accountData.events) {
+        accountData.events?.forEach { event ->
             val eventType = event.getClearType()
             if (eventType == EventType.TAG) {
                 val content = event.getClearContent().toModel<RoomTagContent>()
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 8e243c3443d9deb6b09f2479f230bc215e3b9656..157787c8cf1d4d989eb1f66edc8bd189e599989d 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
@@ -132,6 +132,11 @@ internal class SyncResponseHandler @Inject constructor(
 
         Timber.v("On sync completed")
         cryptoSyncHandler.onSyncCompleted(syncResponse)
+
+        // post sync stuffs
+        monarchy.writeAsync {
+            roomSyncHandler.postSyncSpaceHierarchyHandle(it)
+        }
     }
 
     /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt
index 424c24663cf2ae604f30de81d8ad0145ee524aff..de8d009892fd86d793b29274e2d4b82a8e05d6f7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt
@@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.Observer
 import com.squareup.moshi.JsonEncodingException
+import kotlinx.coroutines.CancellationException
 import org.matrix.android.sdk.api.failure.Failure
 import org.matrix.android.sdk.api.failure.isTokenError
 import org.matrix.android.sdk.api.session.sync.SyncState
@@ -199,7 +200,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
             if (failure is Failure.NetworkConnection && failure.cause is SocketTimeoutException) {
                 // Timeout are not critical
                 Timber.v("Timeout")
-            } else if (failure is Failure.Cancelled) {
+            } else if (failure is CancellationException) {
                 Timber.v("Cancelled")
             } else if (failure.isTokenError()) {
                 // No token or invalid token, stop the thread
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncAccountData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncAccountData.kt
index 1c35d812ee84179e1ca513da1226ad3c60c970cd..a2375507d8b7ecdb6a46e5c76b8f3864102477f0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncAccountData.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncAccountData.kt
@@ -25,5 +25,5 @@ internal data class RoomSyncAccountData(
         /**
          * List of account data events (array of Event).
          */
-        @Json(name = "events") val events: List<Event> = emptyList()
+        @Json(name = "events") val events: List<Event>? = null
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncEphemeral.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncEphemeral.kt
index d59dddb3ea1e15b9676e0b21e77826fd3dce2520..f2135db6b7f949d84ce1fde3d94e73d2b9c2a83d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncEphemeral.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncEphemeral.kt
@@ -26,5 +26,5 @@ internal data class RoomSyncEphemeral(
         /**
          * List of ephemeral events (array of Event).
          */
-        @Json(name = "events") val events: List<Event> = emptyList()
+        @Json(name = "events") val events: List<Event>? = null
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncState.kt
index 5355b7eef1c83c6dd7fa59508551ee21931327cb..f86f05d0003fac911fe0326b2f05088595611429 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncState.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncState.kt
@@ -27,5 +27,5 @@ internal data class RoomSyncState(
         /**
          * List of state events (array of Event). The resulting state corresponds to the *start* of the timeline.
          */
-        @Json(name = "events") val events: List<Event> = emptyList()
+        @Json(name = "events") val events: List<Event>? = null
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncTimeline.kt
index ddf430099a5c553810ce59da14d8d71a44b79234..27bbc4343f73e7e30263385edc3709905e2ff9a2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncTimeline.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/RoomSyncTimeline.kt
@@ -27,7 +27,7 @@ internal data class RoomSyncTimeline(
         /**
          * List of events (array of Event).
          */
-        @Json(name = "events") val events: List<Event> = emptyList(),
+        @Json(name = "events") val events: List<Event>? = null,
 
         /**
          * Boolean which tells whether there are more events on the server
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetURLFormatter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetURLFormatter.kt
index 0937f6d18be4255bd02d1dbcdf8ea04e36c847d4..f7664bf3c2cfc5b719bfb4a5549b0eee1c8f3101 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetURLFormatter.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetURLFormatter.kt
@@ -17,12 +17,13 @@
 package org.matrix.android.sdk.internal.session.widgets
 
 import org.matrix.android.sdk.api.MatrixConfiguration
+import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerConfig
 import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerService
 import org.matrix.android.sdk.api.session.widgets.WidgetURLFormatter
 import org.matrix.android.sdk.api.util.appendParamToUrl
 import org.matrix.android.sdk.api.util.appendParamsToUrl
-import org.matrix.android.sdk.internal.session.SessionLifecycleObserver
+import org.matrix.android.sdk.api.session.SessionLifecycleObserver
 import org.matrix.android.sdk.internal.session.SessionScope
 import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManager
 import org.matrix.android.sdk.internal.session.widgets.token.GetScalarTokenTask
@@ -37,12 +38,12 @@ internal class DefaultWidgetURLFormatter @Inject constructor(private val integra
     private lateinit var currentConfig: IntegrationManagerConfig
     private var whiteListedUrls: List<String> = emptyList()
 
-    override fun onSessionStarted() {
+    override fun onSessionStarted(session: Session) {
         setupWithConfiguration()
         integrationManager.addListener(this)
     }
 
-    override fun onSessionStopped() {
+    override fun onSessionStopped(session: Session) {
         integrationManager.removeListener(this)
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt
index 32442124872eaf11da5faab986e5f585a01aac4e..d741dbc966446bc957b39b0c2ad7875763d249d8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt
@@ -22,6 +22,7 @@ import androidx.lifecycle.LifecycleRegistry
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.Transformations
 import org.matrix.android.sdk.api.query.QueryStringValue
+import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
 import org.matrix.android.sdk.api.session.accountdata.UserAccountDataTypes
 import org.matrix.android.sdk.api.session.events.model.Content
@@ -34,7 +35,7 @@ import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
 import org.matrix.android.sdk.api.session.widgets.WidgetManagementFailure
 import org.matrix.android.sdk.api.session.widgets.model.Widget
 import org.matrix.android.sdk.internal.di.UserId
-import org.matrix.android.sdk.internal.session.SessionLifecycleObserver
+import org.matrix.android.sdk.api.session.SessionLifecycleObserver
 import org.matrix.android.sdk.internal.session.SessionScope
 import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManager
 import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
@@ -57,12 +58,12 @@ internal class WidgetManager @Inject constructor(private val integrationManager:
     private val lifecycleOwner: LifecycleOwner = LifecycleOwner { lifecycleRegistry }
     private val lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(lifecycleOwner)
 
-    override fun onSessionStarted() {
+    override fun onSessionStarted(session: Session) {
         lifecycleRegistry.currentState = Lifecycle.State.STARTED
         integrationManager.addListener(this)
     }
 
-    override fun onSessionStopped() {
+    override fun onSessionStopped(session: Session) {
         integrationManager.removeListener(this)
         lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPI.kt
index 6652628026018bc3defde3f164151ef4391646f8..bfc243c213a866b942911fd4d48efd6d48bd67d6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPI.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetsAPI.kt
@@ -15,7 +15,7 @@
  */
 package org.matrix.android.sdk.internal.session.widgets
 
-import org.matrix.android.sdk.internal.session.openid.RequestOpenIdTokenResponse
+import org.matrix.android.sdk.api.session.openid.OpenIdToken
 import retrofit2.http.Body
 import retrofit2.http.GET
 import retrofit2.http.POST
@@ -29,7 +29,7 @@ internal interface WidgetsAPI {
      * @param body the body content (Ref: https://github.com/matrix-org/matrix-doc/pull/1961)
      */
     @POST("register")
-    suspend fun register(@Body body: RequestOpenIdTokenResponse,
+    suspend fun register(@Body body: OpenIdToken,
                          @Query("v") version: String?): RegisterWidgetResponse
 
     @GET("account")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/ConfigurableTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/ConfigurableTask.kt
index 97f9a0dd51330c6291bc47130fbf232c61d0b8e4..bc80cf7ee8bd3fd7af38f5f6d07b16a098f264ef 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/ConfigurableTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/ConfigurableTask.kt
@@ -37,7 +37,8 @@ internal data class ConfigurableTask<PARAMS, RESULT>(
         val id: UUID,
         val callbackThread: TaskThread,
         val executionThread: TaskThread,
-        val callback: MatrixCallback<RESULT>
+        val callback: MatrixCallback<RESULT>,
+        val maxRetryCount: Int = 0
 
 ) : Task<PARAMS, RESULT> by task {
 
@@ -57,7 +58,8 @@ internal data class ConfigurableTask<PARAMS, RESULT>(
                 id = id,
                 callbackThread = callbackThread,
                 executionThread = executionThread,
-                callback = callback
+                callback = callback,
+                maxRetryCount = retryCount
         )
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/Task.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/Task.kt
index a6c80a0b1a82540f1f3971b900b0d41e49e9d0ff..a5d031e02ae2b6f5e408659771b1cc8667b49487 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/Task.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/Task.kt
@@ -16,7 +16,29 @@
 
 package org.matrix.android.sdk.internal.task
 
+import kotlinx.coroutines.delay
+import org.matrix.android.sdk.api.failure.Failure
+import org.matrix.android.sdk.api.failure.shouldBeRetried
+import timber.log.Timber
+
 internal interface Task<PARAMS, RESULT> {
 
     suspend fun execute(params: PARAMS): RESULT
+
+    suspend fun executeRetry(params: PARAMS, remainingRetry: Int) : RESULT {
+        return try {
+            execute(params)
+        } catch (failure: Throwable) {
+            if (failure.shouldBeRetried() && remainingRetry > 0) {
+                Timber.d(failure, "## TASK: Retriable error")
+                if (failure is Failure.ServerError) {
+                    val waitTime = failure.error.retryAfterMillis ?: 0L
+                    Timber.d(failure, "## TASK: Quota wait time $waitTime")
+                    delay(waitTime + 100)
+                }
+                return executeRetry(params, remainingRetry - 1)
+            }
+            throw failure
+        }
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/TaskExecutor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/TaskExecutor.kt
index 478a356432543b42be9623ff6af436aa3449dd52..4da16eff226e81d4611ea53412746d47098aa222 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/TaskExecutor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/TaskExecutor.kt
@@ -40,9 +40,9 @@ internal class TaskExecutor @Inject constructor(private val coroutineDispatchers
                 .launch(task.callbackThread.toDispatcher()) {
                     val resultOrFailure = runCatching {
                         withContext(task.executionThread.toDispatcher()) {
-                            Timber.v("Enqueue task $task")
-                            Timber.v("Execute task $task on ${Thread.currentThread().name}")
-                            task.execute(task.params)
+                            Timber.v("## TASK: Enqueue task $task")
+                            Timber.v("## TASK: Execute task $task on ${Thread.currentThread().name}")
+                            task.executeRetry(task.params, task.maxRetryCount)
                         }
                     }
                     resultOrFailure
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FailureExt.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FailureExt.kt
new file mode 100644
index 0000000000000000000000000000000000000000..8c78feeac3d883d6d2ab668946a4254e1c86cc49
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FailureExt.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.util
+
+import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.api.failure.Failure
+import org.matrix.android.sdk.api.failure.MatrixError
+import org.matrix.android.sdk.internal.di.MoshiProvider
+
+/**
+ * Try to extract and serialize a MatrixError, or default to localizedMessage
+ */
+internal fun Throwable.toMatrixErrorStr(): String {
+    return (this as? Failure.ServerError)
+            ?.let {
+                // Serialize the MatrixError in this case
+                val adapter = MoshiProvider.providesMoshi().adapter(MatrixError::class.java)
+                tryOrNull { adapter.toJson(error) }
+            }
+            ?: localizedMessage
+            ?: "error"
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt
index e19b1bcca7c7ad625101efcb9121f95279bf471a..47f20913ecaac069dc8eb7bbb502faeaf6019fc6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Hash.kt
@@ -27,7 +27,7 @@ fun String.md5() = try {
     digest.update(toByteArray())
     digest.digest()
             .joinToString("") { String.format("%02X", it) }
-            .toLowerCase(Locale.ROOT)
+            .lowercase(Locale.ROOT)
 } catch (exc: Exception) {
     // Should not happen, but just in case
     hashCode().toString()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt
index 2fabca4be86e5f7a66c237788d3992fde700452f..aa0b92aa45806a83cc365ea4ddd137e0eea83444 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt
@@ -17,6 +17,7 @@
 package org.matrix.android.sdk.internal.util
 
 import timber.log.Timber
+import java.util.Locale
 
 /**
  * Convert a string to an UTF8 String
@@ -24,7 +25,7 @@ import timber.log.Timber
  * @param s the string to convert
  * @return the utf-8 string
  */
-fun convertToUTF8(s: String): String {
+internal fun convertToUTF8(s: String): String {
     return try {
         val bytes = s.toByteArray(Charsets.UTF_8)
         String(bytes)
@@ -40,7 +41,7 @@ fun convertToUTF8(s: String): String {
  * @param s the string to convert
  * @return the utf-16 string
  */
-fun convertFromUTF8(s: String): String {
+internal fun convertFromUTF8(s: String): String {
     return try {
         val bytes = s.toByteArray()
         String(bytes, Charsets.UTF_8)
@@ -56,7 +57,7 @@ fun convertFromUTF8(s: String): String {
  * @param subString  the string to search for
  * @return whether a match was found
  */
-fun String.caseInsensitiveFind(subString: String): Boolean {
+internal fun String.caseInsensitiveFind(subString: String): Boolean {
     // add sanity checks
     if (subString.isEmpty() || isEmpty()) {
         return false
@@ -78,3 +79,14 @@ internal val spaceChars = "[\u00A0\u2000-\u200B\u2800\u3000]".toRegex()
  * Strip all the UTF-8 chars which are actually spaces
  */
 internal fun String.replaceSpaceChars() = replace(spaceChars, "")
+
+// String.capitalize is now deprecated
+internal fun String.safeCapitalize(): String {
+    return replaceFirstChar { char ->
+        if (char.isLowerCase()) {
+            char.titlecase(Locale.getDefault())
+        } else {
+            char.toString()
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/TemporaryFileCreator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/TemporaryFileCreator.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2790ffba36f0d982ca4183106d010162b6e6de63
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/TemporaryFileCreator.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.util
+
+import android.content.Context
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import java.io.File
+import java.util.UUID
+import javax.inject.Inject
+
+internal class TemporaryFileCreator @Inject constructor(
+        private val context: Context
+) {
+    suspend fun create(): File {
+        return withContext(Dispatchers.IO) {
+            File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir)
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/BuildVersionSdkIntProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/BuildVersionSdkIntProvider.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b660796ad82d483d993e815171f418bd60d27b0c
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/BuildVersionSdkIntProvider.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.util.system
+
+internal interface BuildVersionSdkIntProvider {
+    /**
+     * Return the current version of the Android SDK
+     */
+    fun get(): Int
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/DefaultBuildVersionSdkIntProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/DefaultBuildVersionSdkIntProvider.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d9f0064f38f984ab2dc3d5e3a576f67c261f5aa0
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/DefaultBuildVersionSdkIntProvider.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.util.system
+
+import android.os.Build
+import javax.inject.Inject
+
+internal class DefaultBuildVersionSdkIntProvider @Inject constructor()
+    : BuildVersionSdkIntProvider {
+    override fun get() = Build.VERSION.SDK_INT
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/SystemModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/SystemModule.kt
new file mode 100644
index 0000000000000000000000000000000000000000..8a7b50175a2978335d76e83f58a32c5877b7865c
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/SystemModule.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.util.system
+
+import dagger.Binds
+import dagger.Module
+
+@Module
+internal abstract class SystemModule {
+
+    @Binds
+    abstract fun bindBuildVersionSdkIntProvider(provider: DefaultBuildVersionSdkIntProvider): BuildVersionSdkIntProvider
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/util/GraphUtilsTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/util/GraphUtilsTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d28192a2820172f2ba72472efbe84584ff7aab07
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/util/GraphUtilsTest.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.util
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+import org.matrix.android.sdk.MatrixTest
+import org.matrix.android.sdk.internal.session.room.summary.Graph
+
+@FixMethodOrder(MethodSorters.JVM)
+class GraphUtilsTest : MatrixTest {
+
+    @Test
+    fun testCreateGraph() {
+        val graph = Graph()
+
+        graph.addEdge("E", "C")
+        graph.addEdge("B", "A")
+        graph.addEdge("C", "A")
+        graph.addEdge("D", "C")
+        graph.addEdge("E", "D")
+
+        graph.getOrCreateNode("F")
+
+        System.out.println(graph.toString())
+
+        val backEdges = graph.findBackwardEdges(graph.getOrCreateNode("E"))
+
+        assertTrue("There should not be any cycle in this graphs", backEdges.isEmpty())
+    }
+
+    @Test
+    fun testCycleGraph() {
+        val graph = Graph()
+
+        graph.addEdge("E", "C")
+        graph.addEdge("B", "A")
+        graph.addEdge("C", "A")
+        graph.addEdge("D", "C")
+        graph.addEdge("E", "D")
+
+        graph.getOrCreateNode("F")
+
+        // adding loops
+        graph.addEdge("C", "E")
+        graph.addEdge("B", "B")
+
+        System.out.println(graph.toString())
+
+        val backEdges = graph.findBackwardEdges(graph.getOrCreateNode("E"))
+        System.out.println(backEdges.joinToString(" | ") { "${it.source.name} -> ${it.destination.name}" })
+
+        assertEquals("There should be 2 backward edges not ${backEdges.size}", 2, backEdges.size)
+
+        val edge1 = backEdges.find { it.source.name == "C" }
+        assertNotNull("There should be a back edge from C", edge1)
+        assertEquals("There should be a back edge C -> E", "E", edge1!!.destination.name)
+
+        val edge2 = backEdges.find { it.source.name == "B" }
+        assertNotNull("There should be a back edge from B", edge2)
+        assertEquals("There should be a back edge C -> C", "B", edge2!!.destination.name)
+
+        // clean the graph
+        val acyclicGraph = graph.withoutEdges(backEdges)
+        System.out.println(acyclicGraph.toString())
+
+        assertTrue("There should be no backward edges", acyclicGraph.findBackwardEdges(acyclicGraph.getOrCreateNode("E")).isEmpty())
+
+        val flatten = acyclicGraph.flattenDestination()
+
+        assertTrue(flatten[acyclicGraph.getOrCreateNode("A")]!!.isEmpty())
+
+        val flattenParentsB = flatten[acyclicGraph.getOrCreateNode("B")]
+        assertTrue(flattenParentsB!!.size == 1)
+        assertTrue(flattenParentsB.contains(acyclicGraph.getOrCreateNode("A")))
+
+        val flattenParentsE = flatten[acyclicGraph.getOrCreateNode("E")]
+        assertEquals(3, flattenParentsE!!.size)
+        assertTrue(flattenParentsE.contains(acyclicGraph.getOrCreateNode("A")))
+        assertTrue(flattenParentsE.contains(acyclicGraph.getOrCreateNode("C")))
+        assertTrue(flattenParentsE.contains(acyclicGraph.getOrCreateNode("D")))
+
+//        System.out.println(
+//                buildString {
+//                    flatten.entries.forEach {
+//                        append("${it.key.name}: [")
+//                        append(it.value.joinToString(",") { it.name })
+//                        append("]\n")
+//                    }
+//                }
+//        )
+    }
+}
diff --git a/tools/import_from_element.sh b/tools/import_from_element.sh
index 36a1646dc6bd755a522a451454b4047a8aa620a5..331df43c2d9c244266a5532f99b0478a0032d065 100755
--- a/tools/import_from_element.sh
+++ b/tools/import_from_element.sh
@@ -39,6 +39,8 @@ cp -r ${elementAndroidPath}/matrix-sdk-android .
 # Add all changes to git
 git add -A
 
+read -p "Press enter to build the library (update the version first?)"
+
 # Build the library
 ./gradlew clean assembleRelease