diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle
index 391e4087689c7397ce749dff3c20fe6063124dd0..2717f95ed814fc459f625fec1970bcf9abd0c51a 100644
--- a/matrix-sdk-android/build.gradle
+++ b/matrix-sdk-android/build.gradle
@@ -21,7 +21,7 @@ android {
         minSdkVersion 21
         targetSdkVersion 30
         versionCode 1
-        versionName "1.1.13"
+        versionName "1.2.0"
         // Multidex is useful for tests
         multiDexEnabled true
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -112,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.37'
+    def daggerVersion = '2.38'
     def work_version = '2.5.0'
     def retrofit_version = '2.9.0'
 
@@ -120,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.3.0"
+    implementation "androidx.appcompat:appcompat:1.3.1"
     implementation "androidx.core:core-ktx:1.6.0"
 
     implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
@@ -169,7 +169,7 @@ dependencies {
     implementation 'com.otaliastudios:transcoder:0.10.3'
 
     // Phone number https://github.com/google/libphonenumber
-    implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.27'
+    implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.28'
 
     testImplementation 'junit:junit:4.13.2'
     testImplementation 'org.robolectric:robolectric:4.5.1'
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/logger/LoggerTag.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/logger/LoggerTag.kt
new file mode 100644
index 0000000000000000000000000000000000000000..51f9b50699b6437f7e3e5a2349e024aa38db03b8
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/logger/LoggerTag.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.api.logger
+
+/**
+ * Parent class for custom logger tags. Can be used with Timber :
+ *
+ * val loggerTag = LoggerTag("MyTag", LoggerTag.VOIP)
+ * Timber.tag(loggerTag.value).v("My log message")
+ */
+open class LoggerTag(_value: String, parentTag: LoggerTag? = null) {
+
+    object VOIP : LoggerTag("VOIP")
+
+    val value: String = if (parentTag == null) {
+        _value
+    } else {
+        "${parentTag.value}/$_value"
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt
index d9bf5cfd13b2b0bb3123df5943b82d5d1f30f46a..45343686798e21d4123d2f3f95834a5bf90bd05d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt
@@ -31,7 +31,13 @@ interface PushRuleService {
 
     suspend fun addPushRule(kind: RuleKind, pushRule: PushRule)
 
-    suspend fun updatePushRuleActions(kind: RuleKind, oldPushRule: PushRule, newPushRule: PushRule)
+    /**
+     * Enables/Disables a push rule and updates the actions if necessary
+     * @param enable Enables/Disables the rule
+     * @param actions Actions to update if not null
+     */
+
+    suspend fun updatePushRuleActions(kind: RuleKind, ruleId: String, enable: Boolean, actions: List<Action>?)
 
     suspend fun removePushRule(kind: RuleKind, pushRule: PushRule)
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/PushRule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/PushRule.kt
index 3a9fc4fb834a89e0e738842cc7e3eb3c6ce23bce..31d7770a9f4a62193825f39e81f97dc9f85ac6a7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/PushRule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/PushRule.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.pushrules.rest
 
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.pushrules.Action
 import org.matrix.android.sdk.api.pushrules.getActions
 import org.matrix.android.sdk.api.pushrules.toJson
@@ -100,6 +101,13 @@ data class PushRule(
         )
     }
 
+    /**
+     * Get the highlight status. As spec mentions assume false if no tweak present.
+     */
+    fun getHighlight(): Boolean {
+        return getActions().filterIsInstance<Action.Highlight>().firstOrNull()?.highlight.orFalse()
+    }
+
     /**
      * Set the notification status.
      *
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallState.kt
index 2dbd1c9b01bf03bac7a6176552e69cb279fc8c46..47a63b4a251a48d260c8c4d081858adc1f729107 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallState.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallState.kt
@@ -16,6 +16,8 @@
 
 package org.matrix.android.sdk.api.session.call
 
+import org.matrix.android.sdk.api.session.room.model.call.EndCallReason
+
 sealed class CallState {
 
     /** Idle, setting up objects */
@@ -42,6 +44,6 @@ sealed class CallState {
      * */
     data class Connected(val iceConnectionState: MxPeerConnectionState) : CallState()
 
-    /** Terminated.  Incoming/Outgoing call, the call is terminated */
-    object Terminated : CallState()
+    /** Ended.  Incoming/Outgoing call, the call is terminated */
+    data class Ended(val reason: EndCallReason? = null) : CallState()
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt
index fcc9f7072dee4ae863648e7418bde20a633a3a16..dd23e81cc6a062697aa29bf1322727b6e6ed2697 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/MxCall.kt
@@ -18,7 +18,7 @@ package org.matrix.android.sdk.api.session.call
 
 import org.matrix.android.sdk.api.session.room.model.call.CallCandidate
 import org.matrix.android.sdk.api.session.room.model.call.CallCapabilities
-import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
+import org.matrix.android.sdk.api.session.room.model.call.EndCallReason
 import org.matrix.android.sdk.api.session.room.model.call.SdpType
 import org.matrix.android.sdk.api.util.Optional
 
@@ -69,7 +69,7 @@ interface MxCall : MxCallDetail {
     /**
      * End the call
      */
-    fun hangUp(reason: CallHangupContent.Reason? = null)
+    fun hangUp(reason: EndCallReason? = null)
 
     /**
      * Start a call
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentAttachmentData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentAttachmentData.kt
index 98a84b8b662176a71685f37e52fd8cfe83e18d37..7ee26de8db3063903fce7d1f282f560963b4a46d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentAttachmentData.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentAttachmentData.kt
@@ -35,7 +35,8 @@ data class ContentAttachmentData(
         val name: String? = null,
         val queryUri: Uri,
         val mimeType: String?,
-        val type: Type
+        val type: Type,
+        val waveform: List<Int>? = null
 ) : Parcelable {
 
     @JsonClass(generateAdapter = false)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt
index 10c2db45357c850fe455870961c22d1d2420f8b5..b49236c3382eba41deb76eef8a3c3e4c026d8d9d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt
@@ -40,7 +40,63 @@ data class HomeServerCapabilities(
          */
         val roomVersions: RoomVersionCapabilities? = null
 ) {
+
+    enum class RoomCapabilitySupport {
+        SUPPORTED,
+        SUPPORTED_UNSTABLE,
+        UNSUPPORTED,
+        UNKNOWN
+    }
+
+    /**
+     * Check if a feature is supported by the homeserver.
+     * @return
+     *  UNKNOWN if the server does not implement room caps
+     *  UNSUPPORTED if this feature is not supported
+     *  SUPPORTED if this feature is supported by a stable version
+     *  SUPPORTED_UNSTABLE if this feature is supported by an unstable version
+     *  (unstable version should only be used for dev/experimental purpose)
+     */
+    fun isFeatureSupported(feature: String): RoomCapabilitySupport {
+        if (roomVersions?.capabilities == null) return RoomCapabilitySupport.UNKNOWN
+        val info = roomVersions.capabilities[feature] ?: return RoomCapabilitySupport.UNSUPPORTED
+
+        val preferred = info.preferred ?: info.support.lastOrNull()
+        val versionCap = roomVersions.supportedVersion.firstOrNull { it.version == preferred }
+
+        return when {
+            versionCap == null                            -> {
+                RoomCapabilitySupport.UNKNOWN
+            }
+            versionCap.status == RoomVersionStatus.STABLE -> {
+                RoomCapabilitySupport.SUPPORTED
+            }
+            else                                          -> {
+                RoomCapabilitySupport.SUPPORTED_UNSTABLE
+            }
+        }
+    }
+    fun isFeatureSupported(feature: String, byRoomVersion: String): Boolean {
+        if (roomVersions?.capabilities == null) return false
+        val info = roomVersions.capabilities[feature] ?: return false
+
+        return info.preferred == byRoomVersion || info.support.contains(byRoomVersion)
+    }
+
+    /**
+     * Use this method to know if you should force a version when creating
+     * a room that requires this feature.
+     * You can also use #isFeatureSupported prior to this call to check if the
+     * feature is supported and report some feedback to user.
+     */
+    fun versionOverrideForFeature(feature: String) : String? {
+        val cap = roomVersions?.capabilities?.get(feature)
+        return cap?.preferred ?: cap?.support?.lastOrNull()
+    }
+
     companion object {
         const val MAX_UPLOAD_FILE_SIZE_UNKNOWN = -1L
+        const val ROOM_CAP_KNOCK = "knock"
+        const val ROOM_CAP_RESTRICTED = "restricted"
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/RoomVersionModel.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/RoomVersionModel.kt
index 7798b4cc6388ad9313b643b515cbb9dd1e4234e8..9f8e9aa1d122a89423f440b5263fcb0773ed93cf 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/RoomVersionModel.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/RoomVersionModel.kt
@@ -18,7 +18,9 @@ package org.matrix.android.sdk.api.session.homeserver
 
 data class RoomVersionCapabilities(
         val defaultRoomVersion: String,
-        val supportedVersion: List<RoomVersionInfo>
+        val supportedVersion: List<RoomVersionInfo>,
+        // Keys are capabilities defined per spec, as for now knock or restricted
+        val capabilities: Map<String, RoomCapabilitySupport>?
 )
 
 data class RoomVersionInfo(
@@ -26,6 +28,11 @@ data class RoomVersionInfo(
         val status: RoomVersionStatus
 )
 
+data class RoomCapabilitySupport(
+        val preferred: String?,
+        val support: List<String>
+)
+
 enum class RoomVersionStatus {
     STABLE,
     UNSTABLE
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 88ec2de768a4774e7ac19b8ae7de9d8db34eb067..b4408575187b8ec5d362980ada7d9e0aa135cef4 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
@@ -39,12 +39,6 @@ fun spaceSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) =
             .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]
@@ -59,11 +53,10 @@ data class RoomSummaryQueryParams(
         val excludeType: List<String?>?,
         val includeType: List<String?>?,
         val activeSpaceFilter: ActiveSpaceFilter?,
-        var activeGroupId: String? = null
+        val activeGroupId: String? = null
 ) {
 
     class Builder {
-
         var roomId: QueryStringValue = QueryStringValue.IsNotEmpty
         var displayName: QueryStringValue = QueryStringValue.IsNotEmpty
         var canonicalAlias: QueryStringValue = QueryStringValue.NoCondition
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt
index 9d6e1a7eaeb09e39308235fdb543827a45694c70..31f801dd6f7b1e8615515920fbfef6c3bd451109 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallHangupContent.kt
@@ -43,29 +43,5 @@ data class CallHangupContent(
          * or `invite_timeout` for when the other party did not answer in time.
          * One of: ["ice_failed", "invite_timeout"]
          */
-        @Json(name = "reason") val reason: Reason? = null
-) : CallSignalingContent {
-    @JsonClass(generateAdapter = false)
-    enum class Reason {
-        @Json(name = "ice_failed")
-        ICE_FAILED,
-
-        @Json(name = "ice_timeout")
-        ICE_TIMEOUT,
-
-        @Json(name = "user_hangup")
-        USER_HANGUP,
-
-        @Json(name = "replaced")
-        REPLACED,
-
-        @Json(name = "user_media_failed")
-        USER_MEDIA_FAILED,
-
-        @Json(name = "invite_timeout")
-        INVITE_TIMEOUT,
-
-        @Json(name = "unknown_error")
-        UNKWOWN_ERROR
-    }
-}
+        @Json(name = "reason") val reason: EndCallReason? = null
+) : CallSignalingContent
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallRejectContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallRejectContent.kt
index ea412fbe3eba6fd353c671e6d3486020b79dfefd..1b9a7186e2d54c76aa821bce90c0df21821dc4f0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallRejectContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallRejectContent.kt
@@ -36,5 +36,10 @@ data class CallRejectContent(
         /**
          * Required. The version of the VoIP specification this message adheres to.
          */
-        @Json(name = "version") override val version: String?
+        @Json(name = "version") override val version: String?,
+
+        /**
+         * Optional error reason for the reject.
+         */
+        @Json(name = "reason") val reason: EndCallReason? = null
 ) : CallSignalingContent
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/EndCallReason.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/EndCallReason.kt
new file mode 100644
index 0000000000000000000000000000000000000000..60e038b2f939e8a2a13b73615a298dbad9b871ad
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/EndCallReason.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.room.model.call
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = false)
+enum class EndCallReason {
+    @Json(name = "ice_failed")
+    ICE_FAILED,
+
+    @Json(name = "ice_timeout")
+    ICE_TIMEOUT,
+
+    @Json(name = "user_hangup")
+    USER_HANGUP,
+
+    @Json(name = "replaced")
+    REPLACED,
+
+    @Json(name = "user_media_failed")
+    USER_MEDIA_FAILED,
+
+    @Json(name = "invite_timeout")
+    INVITE_TIMEOUT,
+
+    @Json(name = "unknown_error")
+    UNKWOWN_ERROR,
+
+    @Json(name = "user_busy")
+    USER_BUSY,
+
+    @Json(name = "answered_elsewhere")
+    ANSWERED_ELSEWHERE
+}
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 ca8c66bb3b5b9115a4811c2ab11255e05a139612..566790600036cd34616faeea7261b9148e367e4b 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
@@ -22,10 +22,8 @@ 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
 open class CreateRoomParams {
     /**
      * A public visibility indicates that the room will be shown in the published room list.
@@ -103,6 +101,13 @@ open class CreateRoomParams {
      */
     val creationContent = mutableMapOf<String, Any>()
 
+    /**
+     * A list of state events to set in the new room. This allows the user to override the default state events
+     * set in the new room. The expected format of the state events are an object with type, state_key and content keys set.
+     * Takes precedence over events set by preset, but gets overridden by name and topic keys.
+     */
+    val initialStates = mutableListOf<CreateRoomStateEvent>()
+
     /**
      * Set to true to disable federation of this room.
      * Default: false
@@ -156,7 +161,7 @@ open class CreateRoomParams {
 
     var roomVersion: String? = null
 
-    var joinRuleRestricted: List<RoomJoinRulesAllowEntry>? = null
+    var featurePreset: RoomFeaturePreset? = null
 
     companion object {
         private const val CREATION_CONTENT_KEY_M_FEDERATE = "m.federate"
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomStateEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomStateEvent.kt
new file mode 100644
index 0000000000000000000000000000000000000000..fcfdc3e33369bca09e0eb728fc4097dd32b39ee6
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/CreateRoomStateEvent.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.api.session.room.model.create
+
+import org.matrix.android.sdk.api.session.events.model.Content
+
+data class CreateRoomStateEvent(
+        /**
+         * Required. The type of event to send.
+         */
+        val type: String,
+
+        /**
+         * Required. The content of the event.
+         */
+        val content: Content,
+
+        /**
+         * The state_key of the state event. Defaults to an empty string.
+         */
+        val stateKey: String = ""
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/RoomFeaturePreset.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/RoomFeaturePreset.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f5f722d7833059eaa3d2d81d00f287249df0641b
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/create/RoomFeaturePreset.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.api.session.room.model.create
+
+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.homeserver.HomeServerCapabilities
+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.RoomJoinRulesAllowEntry
+import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
+
+interface RoomFeaturePreset {
+
+    fun updateRoomParams(params: CreateRoomParams)
+
+    fun setupInitialStates(): List<Event>?
+}
+
+class RestrictedRoomPreset(val homeServerCapabilities: HomeServerCapabilities, val restrictedList: List<RoomJoinRulesAllowEntry>) : RoomFeaturePreset {
+
+    override fun updateRoomParams(params: CreateRoomParams) {
+        params.historyVisibility = params.historyVisibility ?: RoomHistoryVisibility.SHARED
+        params.guestAccess = params.guestAccess ?: GuestAccess.Forbidden
+        params.roomVersion = homeServerCapabilities.versionOverrideForFeature(HomeServerCapabilities.ROOM_CAP_RESTRICTED)
+    }
+
+    override fun setupInitialStates(): List<Event>? {
+        return listOf(
+                Event(
+                        type = EventType.STATE_ROOM_JOIN_RULES,
+                        stateKey = "",
+                        content = RoomJoinRulesContent(
+                                _joinRules = RoomJoinRules.RESTRICTED.value,
+                                allowList = restrictedList
+                        ).toContent()
+                )
+        )
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/AudioInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/AudioInfo.kt
index b72c8e5dfd07f8e360cb18af295fb7b7da929b28..ea6bdfafadce97842964c8f2f30b105f85ad3c13 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/AudioInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/AudioInfo.kt
@@ -24,15 +24,15 @@ data class AudioInfo(
         /**
          * The mimetype of the audio e.g. "audio/aac".
          */
-        @Json(name = "mimetype") val mimeType: String?,
+        @Json(name = "mimetype") val mimeType: String? = null,
 
         /**
          * The size of the audio clip in bytes.
          */
-        @Json(name = "size") val size: Long = 0,
+        @Json(name = "size") val size: Long? = null,
 
         /**
          * The duration of the audio in milliseconds.
          */
-        @Json(name = "duration") val duration: Int = 0
+        @Json(name = "duration") val duration: Int? = null
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/AudioWaveformInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/AudioWaveformInfo.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d576f1057a29fa8a42e4b66f375b6f7acc5a62a4
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/AudioWaveformInfo.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.message
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+/**
+ * See https://github.com/matrix-org/matrix-doc/blob/travis/msc/audio-waveform/proposals/3246-audio-waveform.md
+ */
+@JsonClass(generateAdapter = true)
+data class AudioWaveformInfo(
+        @Json(name = "duration")
+        val duration: Int? = null,
+
+        /**
+         * The array should have no less than 30 elements and no more than 120.
+         * List of integers between zero and 1024, inclusive.
+         */
+        @Json(name = "waveform")
+        val waveform: List<Int>? = null
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioContent.kt
index b4ba5c0a66e93561fd0b613ffb56ac6c10445f72..1bcb10d88cf753987af8c51e7acf373df5833737 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioContent.kt
@@ -20,6 +20,7 @@ import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
 import org.matrix.android.sdk.api.session.events.model.Content
 import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
+import org.matrix.android.sdk.api.util.JsonDict
 import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
 
 @JsonClass(generateAdapter = true)
@@ -50,7 +51,17 @@ data class MessageAudioContent(
         /**
          * Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
          */
-        @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
+        @Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null,
+
+        /**
+         * Encapsulates waveform and duration of the audio.
+         */
+        @Json(name = "org.matrix.msc1767.audio") val audioWaveformInfo: AudioWaveformInfo? = null,
+
+        /**
+         * Indicates that is a voice message.
+         */
+        @Json(name = "org.matrix.msc3245.voice") val voiceMessageIndicator: JsonDict? = null
 ) : MessageWithAttachmentContent {
 
     override val mimeType: String?
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt
index e614ea91d649b8cd712926d78c0a1a6176bdb2a8..4d3f95233d05d54e8202c1a48084b9913ecb001b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt
@@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.session.events.model.Event
 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.RoomJoinRulesAllowEntry
 import org.matrix.android.sdk.api.util.JsonDict
 import org.matrix.android.sdk.api.util.Optional
 
@@ -53,7 +54,7 @@ interface StateService {
     /**
      * Update the join rule and/or the guest access
      */
-    suspend fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?)
+    suspend fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?, allowList: List<RoomJoinRulesAllowEntry>? = null)
 
     /**
      * Update the avatar of the room
@@ -91,4 +92,8 @@ interface StateService {
      * @param eventTypes Set of eventType to observe. If empty, all state events will be observed
      */
     fun getStateEventsLive(eventTypes: Set<String>, stateKey: QueryStringValue = QueryStringValue.NoCondition): LiveData<List<Event>>
+
+    suspend fun setJoinRulePublic()
+    suspend fun setJoinRuleInviteOnly()
+    suspend fun setJoinRuleRestricted(allowList: List<String>)
 }
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 182b37f2ad018e609a1d480fabb3659c427c59de..ef47775f1bec34033b2e42ab9207ed67de510c06 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
@@ -31,6 +31,8 @@ object MimeTypes {
     const val Jpeg = "image/jpeg"
     const val Gif = "image/gif"
 
+    const val Ogg = "audio/ogg"
+
     fun String?.normalizeMimeType() = if (this == BadJpg) Jpeg else this
 
     fun String?.isMimeTypeImage() = this?.startsWith("image/").orFalse()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/SessionCreator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/SessionCreator.kt
index 160fd2d5567eb5e0f66d75a21f1401e52e358db4..cc00c963ea1b2550254ccf5346a36f7f1cdbab4f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/SessionCreator.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/SessionCreator.kt
@@ -49,6 +49,8 @@ internal class DefaultSessionCreator @Inject constructor(
                 // remove trailing "/"
                 ?.trim { it == '/' }
                 ?.takeIf { it.isNotBlank() }
+                // It can be the same value, so in this case, do not check again the validity
+                ?.takeIf { it != homeServerConnectionConfig.homeServerUriBase.toString() }
                 ?.also { Timber.d("Overriding homeserver url to $it (will check if valid)") }
                 ?.let { Uri.parse(it) }
                 ?.takeIf {
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 d170ae3dd306d5cbe654bc4c6b5b1646117302b3..563c89095040dc6c3a6d2b9bdae6140e547ace80 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
@@ -314,6 +314,12 @@ internal class DefaultCryptoService @Inject constructor(
         cryptoCoroutineScope.launchToCallback(coroutineDispatchers.crypto, NoOpMatrixCallback()) {
             // Open the store
             cryptoStore.open()
+
+            if (!cryptoStore.areDeviceKeysUploaded()) {
+                // Schedule upload of OTK
+                oneTimeKeysUploader.updateOneTimeKeyCount(0)
+            }
+
             // this can throw if no network
             tryOrNull {
                 uploadDeviceKeys()
@@ -905,7 +911,7 @@ internal class DefaultCryptoService @Inject constructor(
      * Upload my user's device keys.
      */
     private suspend fun uploadDeviceKeys() {
-        if (cryptoStore.getDeviceKeysUploaded()) {
+        if (cryptoStore.areDeviceKeysUploaded()) {
             Timber.d("Keys already uploaded, nothing to do")
             return
         }
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 63f15aaf6e6ca0ce3555b566746f282191194696..79910c6de29b9c40b6af67ea47be2a6a9c65422a 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
@@ -16,6 +16,7 @@
 
 package org.matrix.android.sdk.internal.crypto
 
+import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.launch
 import org.matrix.android.sdk.api.MatrixPatterns
 import org.matrix.android.sdk.api.auth.data.Credentials
@@ -336,7 +337,12 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
             downloadKeysForUsersTask.execute(params)
         } catch (throwable: Throwable) {
             Timber.e(throwable, "## CRYPTO | doKeyDownloadForUsers(): error")
-            onKeysDownloadFailed(filteredUsers)
+            if (throwable is CancellationException) {
+                // the crypto module is getting closed, so we cannot access the DB anymore
+                Timber.w("The crypto module is closed, ignoring this error")
+            } else {
+                onKeysDownloadFailed(filteredUsers)
+            }
             throw throwable
         }
         Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt
index 6695234d621940d0dd24a7299b2077f07bf65837..c4b62fe9fee560c047786e10499b13eb817156ed 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OneTimeKeysUploader.kt
@@ -16,6 +16,7 @@
 
 package org.matrix.android.sdk.internal.crypto
 
+import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.internal.crypto.model.MXKey
 import org.matrix.android.sdk.internal.crypto.model.rest.KeysUploadResponse
 import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask
@@ -77,6 +78,10 @@ internal class OneTimeKeysUploader @Inject constructor(
         // discard the oldest private keys first. This will eventually clean
         // out stale private keys that won't receive a message.
         val keyLimit = floor(maxOneTimeKeys / 2.0).toInt()
+        if (oneTimeKeyCount == null) {
+            // Ask the server how many otk he has
+            oneTimeKeyCount = fetchOtkCount()
+        }
         val oneTimeKeyCountFromSync = oneTimeKeyCount
         if (oneTimeKeyCountFromSync != null) {
             // We need to keep a pool of one time public keys on the server so that
@@ -90,17 +95,22 @@ internal class OneTimeKeysUploader @Inject constructor(
             // private keys clogging up our local storage.
             // So we need some kind of engineering compromise to balance all of
             // these factors.
-            try {
+            tryOrNull("Unable to upload OTK") {
                 val uploadedKeys = uploadOTK(oneTimeKeyCountFromSync, keyLimit)
                 Timber.v("## uploadKeys() : success, $uploadedKeys key(s) sent")
-            } finally {
-                oneTimeKeyCheckInProgress = false
             }
         } else {
             Timber.w("maybeUploadOneTimeKeys: waiting to know the number of OTK from the sync")
-            oneTimeKeyCheckInProgress = false
             lastOneTimeKeyCheck = 0
         }
+        oneTimeKeyCheckInProgress = false
+    }
+
+    private suspend fun fetchOtkCount(): Int? {
+        return tryOrNull("Unable to get OTK count") {
+            val result = uploadKeysTask.execute(UploadKeysTask.Params(null, null))
+            result.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)
+        }
     }
 
     /**
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 4004294d973da3fdfa47dda69d1e9055e29c7522..5e7744853aa464a9515fb1fb9c807e1c5321617d 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
@@ -18,8 +18,6 @@ package org.matrix.android.sdk.internal.crypto.model
 import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel
 import org.matrix.android.sdk.internal.crypto.model.rest.DeviceKeys
 import org.matrix.android.sdk.internal.crypto.model.rest.UnsignedDeviceInfo
-import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMapper
-import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity
 
 data class CryptoDeviceInfo(
         val deviceId: String,
@@ -77,7 +75,3 @@ data class CryptoDeviceInfo(
 internal fun CryptoDeviceInfo.toRest(): DeviceKeys {
     return CryptoInfoMapper.map(this)
 }
-
-internal fun CryptoDeviceInfo.toEntity(): DeviceInfoEntity {
-    return CryptoMapper.mapToEntity(this)
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt
index 181bd94cc7e832718a8aa3595108c03e092040cb..3d12e74fcdd061833de6a12d988ee5abab3e3432 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt
@@ -475,7 +475,7 @@ internal interface IMXCryptoStore {
     fun getGossipingEvents(): List<Event>
 
     fun setDeviceKeysUploaded(uploaded: Boolean)
-    fun getDeviceKeysUploaded(): Boolean
+    fun areDeviceKeysUploaded(): Boolean
     fun tidyUpDataBase()
     fun logDbUsageInfo()
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt
index 9ae93d61eb88be0b71cd6aeedf42a058ff123ff3..d99799883658e236bba62e3509a75d927f169a4f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt
@@ -51,7 +51,6 @@ import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper
 import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent
 import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
 import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
-import org.matrix.android.sdk.internal.crypto.model.toEntity
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo
 import org.matrix.android.sdk.internal.crypto.store.SavedKeyBackupKeyInfo
@@ -280,24 +279,37 @@ internal class RealmCryptoStore @Inject constructor(
     override fun storeUserDevices(userId: String, devices: Map<String, CryptoDeviceInfo>?) {
         doRealmTransaction(realmConfiguration) { realm ->
             if (devices == null) {
+                Timber.d("Remove user $userId")
                 // Remove the user
                 UserEntity.delete(realm, userId)
             } else {
-                UserEntity.getOrCreate(realm, userId)
-                        .let { u ->
-                            // Add the devices
-                            val currentKnownDevices = u.devices.toList()
-                            val new = devices.map { entry -> entry.value.toEntity() }
-                            new.forEach { entity ->
-                                // Maintain first time seen
-                                val existing = currentKnownDevices.firstOrNull { it.deviceId == entity.deviceId && it.identityKey == entity.identityKey }
-                                entity.firstTimeSeenLocalTs = existing?.firstTimeSeenLocalTs ?: System.currentTimeMillis()
-                                realm.insertOrUpdate(entity)
-                            }
-                            // Ensure all other devices are deleted
-                            u.devices.clearWith { it.deleteOnCascade() }
-                            u.devices.addAll(new)
+                val userEntity = UserEntity.getOrCreate(realm, userId)
+                // First delete the removed devices
+                val deviceIds = devices.keys
+                userEntity.devices.toTypedArray().iterator().let {
+                    while (it.hasNext()) {
+                        val deviceInfoEntity = it.next()
+                        if (deviceInfoEntity.deviceId !in deviceIds) {
+                            Timber.d("Remove device ${deviceInfoEntity.deviceId} of user $userId")
+                            deviceInfoEntity.deleteOnCascade()
                         }
+                    }
+                }
+                // Then update existing devices or add new one
+                devices.values.forEach { cryptoDeviceInfo ->
+                    val existingDeviceInfoEntity = userEntity.devices.firstOrNull { it.deviceId == cryptoDeviceInfo.deviceId }
+                    if (existingDeviceInfoEntity == null) {
+                        // Add the device
+                        Timber.d("Add device ${cryptoDeviceInfo.deviceId} of user $userId")
+                        val newEntity = CryptoMapper.mapToEntity(cryptoDeviceInfo)
+                        newEntity.firstTimeSeenLocalTs = System.currentTimeMillis()
+                        userEntity.devices.add(newEntity)
+                    } else {
+                        // Update the device
+                        Timber.d("Update device ${cryptoDeviceInfo.deviceId} of user $userId")
+                        CryptoMapper.updateDeviceInfoEntity(existingDeviceInfoEntity, cryptoDeviceInfo)
+                    }
+                }
             }
         }
     }
@@ -937,7 +949,7 @@ internal class RealmCryptoStore @Inject constructor(
         }
     }
 
-    override fun getDeviceKeysUploaded(): Boolean {
+    override fun areDeviceKeysUploaded(): Boolean {
         return doWithRealm(realmConfiguration) {
             it.where<CryptoMetadataEntity>().findFirst()?.deviceKeysSentToServer
         } ?: false
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt
index 8e77f5b82329468b34d39d1591f31e152ef07f74..2846be993255241c15d9f5279373581645acecad 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt
@@ -55,7 +55,7 @@ internal object RealmCryptoStoreMigration : RealmMigration {
     // 0, 1, 2: legacy Riot-Android
     // 3: migrate to RiotX schema
     // 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6)
-    const val CRYPTO_STORE_SCHEMA_VERSION = 12L
+    const val CRYPTO_STORE_SCHEMA_VERSION = 13L
 
     private fun RealmObjectSchema.addFieldIfNotExists(fieldName: String, fieldType: Class<*>): RealmObjectSchema {
         if (!hasField(fieldName)) {
@@ -93,6 +93,7 @@ internal object RealmCryptoStoreMigration : RealmMigration {
         if (oldVersion <= 9) migrateTo10(realm)
         if (oldVersion <= 10) migrateTo11(realm)
         if (oldVersion <= 11) migrateTo12(realm)
+        if (oldVersion <= 12) migrateTo13(realm)
     }
 
     private fun migrateTo1Legacy(realm: DynamicRealm) {
@@ -497,4 +498,60 @@ internal object RealmCryptoStoreMigration : RealmMigration {
         realm.schema.get("CryptoRoomEntity")
                 ?.addRealmObjectField(CryptoRoomEntityFields.OUTBOUND_SESSION_INFO.`$`, outboundEntitySchema)
     }
+
+    // Version 13L delete unreferenced TrustLevelEntity
+    private fun migrateTo13(realm: DynamicRealm) {
+        Timber.d("Step 12 -> 13")
+
+        // Use a trick to do that... Ref: https://stackoverflow.com/questions/55221366
+        val trustLevelEntitySchema = realm.schema.get("TrustLevelEntity")
+
+        /*
+        Creating a new temp field called isLinked which is set to true for those which are
+        references by other objects. Rest of them are set to false. Then removing all
+        those which are false and hence duplicate and unnecessary. Then removing the temp field
+        isLinked
+         */
+        var mainCounter = 0
+        var deviceInfoCounter = 0
+        var keyInfoCounter = 0
+        val deleteCounter: Int
+
+        trustLevelEntitySchema
+                ?.addField("isLinked", Boolean::class.java)
+                ?.transform { obj ->
+                    // Setting to false for all by default
+                    obj.set("isLinked", false)
+                    mainCounter++
+                }
+
+        realm.schema.get("DeviceInfoEntity")?.transform { obj ->
+            // Setting to true for those which are referenced in DeviceInfoEntity
+            deviceInfoCounter++
+            obj.getObject("trustLevelEntity")?.set("isLinked", true)
+        }
+
+        realm.schema.get("KeyInfoEntity")?.transform { obj ->
+            // Setting to true for those which are referenced in KeyInfoEntity
+            keyInfoCounter++
+            obj.getObject("trustLevelEntity")?.set("isLinked", true)
+        }
+
+        // Removing all those which are set as false
+        realm.where("TrustLevelEntity")
+                .equalTo("isLinked", false)
+                .findAll()
+                .also { deleteCounter = it.size }
+                .deleteAllFromRealm()
+
+        trustLevelEntitySchema?.removeField("isLinked")
+
+        Timber.w("TrustLevelEntity cleanup: $mainCounter entities")
+        Timber.w("TrustLevelEntity cleanup: $deviceInfoCounter entities referenced in DeviceInfoEntities")
+        Timber.w("TrustLevelEntity cleanup: $keyInfoCounter entities referenced in KeyInfoEntity")
+        Timber.w("TrustLevelEntity cleanup: $deleteCounter entities deleted!")
+        if (mainCounter != deviceInfoCounter + keyInfoCounter + deleteCounter) {
+            Timber.e("TrustLevelEntity cleanup: Something is not correct...")
+        }
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMapper.kt
index 37d144169055cf3a30d9c195a882211c3f44212e..7ba986699ab60bf26acf5e2891225096a0fb6897 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CryptoMapper.kt
@@ -44,23 +44,32 @@ object CryptoMapper {
     ))
 
     internal fun mapToEntity(deviceInfo: CryptoDeviceInfo): DeviceInfoEntity {
-        return DeviceInfoEntity(
-                primaryKey = DeviceInfoEntity.createPrimaryKey(deviceInfo.userId, deviceInfo.deviceId),
-                userId = deviceInfo.userId,
-                deviceId = deviceInfo.deviceId,
-                algorithmListJson = listMigrationAdapter.toJson(deviceInfo.algorithms),
-                keysMapJson = mapMigrationAdapter.toJson(deviceInfo.keys),
-                signatureMapJson = mapMigrationAdapter.toJson(deviceInfo.signatures),
-                isBlocked = deviceInfo.isBlocked,
-                trustLevelEntity = deviceInfo.trustLevel?.let {
-                    TrustLevelEntity(
-                            crossSignedVerified = it.crossSigningVerified,
-                            locallyVerified = it.locallyVerified
-                    )
-                },
-                // We store the device name if present now
-                unsignedMapJson = deviceInfo.unsigned?.deviceDisplayName
-        )
+        return DeviceInfoEntity(primaryKey = DeviceInfoEntity.createPrimaryKey(deviceInfo.userId, deviceInfo.deviceId))
+                .also { updateDeviceInfoEntity(it, deviceInfo) }
+    }
+
+    internal fun updateDeviceInfoEntity(entity: DeviceInfoEntity, deviceInfo: CryptoDeviceInfo) {
+        entity.userId = deviceInfo.userId
+        entity.deviceId = deviceInfo.deviceId
+        entity.algorithmListJson = listMigrationAdapter.toJson(deviceInfo.algorithms)
+        entity.keysMapJson = mapMigrationAdapter.toJson(deviceInfo.keys)
+        entity.signatureMapJson = mapMigrationAdapter.toJson(deviceInfo.signatures)
+        entity.isBlocked = deviceInfo.isBlocked
+        val deviceInfoTrustLevel = deviceInfo.trustLevel
+        if (deviceInfoTrustLevel == null) {
+            entity.trustLevelEntity?.deleteFromRealm()
+            entity.trustLevelEntity = null
+        } else {
+            if (entity.trustLevelEntity == null) {
+                // Create a new TrustLevelEntity object
+                entity.trustLevelEntity = TrustLevelEntity()
+            }
+            // Update the existing TrustLevelEntity object
+            entity.trustLevelEntity?.crossSignedVerified = deviceInfoTrustLevel.crossSigningVerified
+            entity.trustLevelEntity?.locallyVerified = deviceInfoTrustLevel.locallyVerified
+        }
+        // We store the device name if present now
+        entity.unsignedMapJson = deviceInfo.unsigned?.deviceDisplayName
     }
 
     internal fun mapToModel(deviceInfoEntity: DeviceInfoEntity): CryptoDeviceInfo {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt
index 0dbbe656c71bb7fac8e473c4e8ee8ab04bba1d7a..45f81439377b97a00ae26d332b86ecddbea8144c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationTransportToDevice.kt
@@ -68,7 +68,7 @@ internal class VerificationTransportToDevice(
             contentMap.setObject(otherUserId, it, keyReq)
         }
         sendToDeviceTask
-                .configureWith(SendToDeviceTask.Params(MessageType.MSGTYPE_VERIFICATION_REQUEST, contentMap, localId)) {
+                .configureWith(SendToDeviceTask.Params(MessageType.MSGTYPE_VERIFICATION_REQUEST, contentMap)) {
                     this.callback = object : MatrixCallback<Unit> {
                         override fun onSuccess(data: Unit) {
                             Timber.v("## verification [$tx.transactionId] send toDevice request success")
@@ -124,7 +124,7 @@ internal class VerificationTransportToDevice(
         contentMap.setObject(tx.otherUserId, tx.otherDeviceId, toSendToDeviceObject)
 
         sendToDeviceTask
-                .configureWith(SendToDeviceTask.Params(type, contentMap, tx.transactionId)) {
+                .configureWith(SendToDeviceTask.Params(type, contentMap)) {
                     this.callback = object : MatrixCallback<Unit> {
                         override fun onSuccess(data: Unit) {
                             Timber.v("## SAS verification [$tx.transactionId] toDevice type '$type' success.")
@@ -155,7 +155,7 @@ internal class VerificationTransportToDevice(
         val contentMap = MXUsersDevicesMap<Any>()
         contentMap.setObject(otherUserId, otherUserDeviceId, cancelMessage)
         sendToDeviceTask
-                .configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_DONE, contentMap, transactionId)) {
+                .configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_DONE, contentMap)) {
                     this.callback = object : MatrixCallback<Unit> {
                         override fun onSuccess(data: Unit) {
                             onDone?.invoke()
@@ -176,7 +176,7 @@ internal class VerificationTransportToDevice(
         val contentMap = MXUsersDevicesMap<Any>()
         contentMap.setObject(otherUserId, otherUserDeviceId, cancelMessage)
         sendToDeviceTask
-                .configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap, transactionId)) {
+                .configureWith(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap)) {
                     this.callback = object : MatrixCallback<Unit> {
                         override fun onSuccess(data: Unit) {
                             Timber.v("## SAS verification [$transactionId] canceled for reason ${code.value}")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt
index 2575cdef2683d4d1706ba1c082d4f80030672d03..8b6d263f8c7e7bbf6ffe3bd4ed58e689d36451f6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.database.mapper
 
 import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
+import org.matrix.android.sdk.api.session.homeserver.RoomCapabilitySupport
 import org.matrix.android.sdk.api.session.homeserver.RoomVersionCapabilities
 import org.matrix.android.sdk.api.session.homeserver.RoomVersionInfo
 import org.matrix.android.sdk.api.session.homeserver.RoomVersionStatus
@@ -45,19 +46,28 @@ internal object HomeServerCapabilitiesMapper {
         roomVersionsJson ?: return null
 
         return tryOrNull {
-            MoshiProvider.providesMoshi().adapter(RoomVersions::class.java).fromJson(roomVersionsJson)?.let {
+            MoshiProvider.providesMoshi().adapter(RoomVersions::class.java).fromJson(roomVersionsJson)?.let { roomVersions ->
                 RoomVersionCapabilities(
-                        defaultRoomVersion = it.default ?: DefaultRoomVersionService.DEFAULT_ROOM_VERSION,
-                        supportedVersion = it.available.entries.map { entry ->
-                            RoomVersionInfo(
-                                    version = entry.key,
-                                    status = if (entry.value == "stable") {
-                                        RoomVersionStatus.STABLE
-                                    } else {
-                                        RoomVersionStatus.UNSTABLE
-                                    }
-                            )
-                        }
+                        defaultRoomVersion = roomVersions.default ?: DefaultRoomVersionService.DEFAULT_ROOM_VERSION,
+                        supportedVersion = roomVersions.available?.entries?.map { entry ->
+                            RoomVersionInfo(entry.key, RoomVersionStatus.STABLE
+                                    .takeIf { entry.value == "stable" }
+                                    ?: RoomVersionStatus.UNSTABLE)
+                        }.orEmpty(),
+                        capabilities = roomVersions.roomCapabilities?.entries?.mapNotNull { entry ->
+                            (entry.value as? Map<*, *>)?.let {
+                                val preferred = it["preferred"] as? String ?: return@mapNotNull null
+                                val support = (it["support"] as? List<*>)?.filterIsInstance<String>()
+                                entry.key to RoomCapabilitySupport(preferred, support.orEmpty())
+                            }
+                        }?.toMap()
+                        // Just for debug purpose
+//                                ?: mapOf(
+//                                HomeServerCapabilities.ROOM_CAP_RESTRICTED to RoomCapabilitySupport(
+//                                        preferred = null,
+//                                        support = listOf("org.matrix.msc3083")
+//                                )
+//                                )
                 )
             }
         }
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 a284d976d0525187ce18e5306e488f7a29f2cd6f..414c018074dad12c116ad4c4594a47d1fd14dc5e 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
@@ -25,6 +25,7 @@ import kotlinx.coroutines.completeWith
 import kotlinx.coroutines.withContext
 import okhttp3.OkHttpClient
 import okhttp3.Request
+import org.matrix.android.sdk.api.failure.Failure
 import org.matrix.android.sdk.api.session.content.ContentUrlResolver
 import org.matrix.android.sdk.api.session.file.FileService
 import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
@@ -33,6 +34,7 @@ import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory
 import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificateWithProgress
 import org.matrix.android.sdk.internal.session.download.DownloadProgressInterceptor.Companion.DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER
 import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
+import org.matrix.android.sdk.internal.util.file.AtomicFileCreator
 import org.matrix.android.sdk.internal.util.md5
 import org.matrix.android.sdk.internal.util.writeToFile
 import timber.log.Timber
@@ -96,6 +98,9 @@ internal class DefaultFileService @Inject constructor(
             }
         }
 
+        var atomicFileDownload: AtomicFileCreator? = null
+        var atomicFileDecrypt: AtomicFileCreator? = null
+
         if (existingDownload != null) {
             // FIXME If the first downloader cancels then we'll unfortunately be cancelled too.
             return existingDownload.await()
@@ -120,19 +125,30 @@ internal class DefaultFileService @Inject constructor(
                             .header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER, url)
                             .build()
 
-                    val response = okHttpClient.newCall(request).execute()
+                    val response = try {
+                        okHttpClient.newCall(request).execute()
+                    } catch (failure: Throwable) {
+                        throw if (failure is IOException) {
+                            Failure.NetworkConnection(failure)
+                        } else {
+                            failure
+                        }
+                    }
 
                     if (!response.isSuccessful) {
-                        throw IOException()
+                        throw Failure.NetworkConnection(IOException())
                     }
 
-                    val source = response.body?.source() ?: throw IOException()
+                    val source = response.body?.source() ?: throw Failure.NetworkConnection(IOException())
 
                     Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${!source.exhausted()}")
 
                     // Write the file to cache (encrypted version if the file is encrypted)
-                    writeToFile(source.inputStream(), cachedFiles.file)
+                    // Write to a part file first, so if we abort before done, we don't have a broken cached file
+                    val atomicFileCreator = AtomicFileCreator(cachedFiles.file).also { atomicFileDownload = it }
+                    writeToFile(source.inputStream(), atomicFileCreator.partFile)
                     response.close()
+                    atomicFileCreator.commit()
                 } else {
                     Timber.v("## FileService: cache hit for $url")
                 }
@@ -145,8 +161,10 @@ internal class DefaultFileService @Inject constructor(
                     Timber.v("## FileService: decrypt file")
                     // Ensure the parent folder exists
                     cachedFiles.decryptedFile.parentFile?.mkdirs()
+                    // Write to a part file first, so if we abort before done, we don't have a broken cached file
+                    val atomicFileCreator = AtomicFileCreator(cachedFiles.decryptedFile).also { atomicFileDecrypt = it }
                     val decryptSuccess = cachedFiles.file.inputStream().use { inputStream ->
-                        cachedFiles.decryptedFile.outputStream().buffered().use { outputStream ->
+                        atomicFileCreator.partFile.outputStream().buffered().use { outputStream ->
                             MXEncryptedAttachments.decryptAttachment(
                                     inputStream,
                                     elementToDecrypt,
@@ -154,6 +172,7 @@ internal class DefaultFileService @Inject constructor(
                             )
                         }
                     }
+                    atomicFileCreator.commit()
                     if (!decryptSuccess) {
                         throw IllegalStateException("Decryption error")
                     }
@@ -174,6 +193,11 @@ internal class DefaultFileService @Inject constructor(
         }
         toNotify?.completeWith(result)
 
+        result.onFailure {
+            atomicFileDownload?.cancel()
+            atomicFileDecrypt?.cancel()
+        }
+
         return result.getOrThrow()
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt
index 473adeb8d268eac74e9a63ada3d52082508e4e90..bdc254fc99cf20d4ffb75b650357e12fe6824d60 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt
@@ -20,11 +20,14 @@ import io.realm.Realm
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.internal.database.model.EventInsertType
+import org.matrix.android.sdk.api.logger.LoggerTag
 import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
 import org.matrix.android.sdk.internal.session.SessionScope
 import timber.log.Timber
 import javax.inject.Inject
 
+private val loggerTag = LoggerTag("CallEventProcessor", LoggerTag.VOIP)
+
 @SessionScope
 internal class CallEventProcessor @Inject constructor(private val callSignalingHandler: CallSignalingHandler)
     : EventInsertLiveProcessor {
@@ -71,14 +74,8 @@ internal class CallEventProcessor @Inject constructor(private val callSignalingH
     }
 
     private fun dispatchToCallSignalingHandlerIfNeeded(event: Event) {
-        val now = System.currentTimeMillis()
         event.roomId ?: return Unit.also {
-            Timber.w("Event with no room id ${event.eventId}")
-        }
-        val age = now - (event.ageLocalTs ?: now)
-        if (age > 40_000) {
-            // Too old to ring?
-            return
+            Timber.tag(loggerTag.value).w("Event with no room id ${event.eventId}")
         }
         callSignalingHandler.onCallEvent(event)
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt
index b0901af7196fc211ba23b4f247edb5afda383000..59058bf9767648cea08f3dea30bbfb7bfca60502 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt
@@ -16,6 +16,7 @@
 
 package org.matrix.android.sdk.internal.session.call
 
+import org.matrix.android.sdk.api.logger.LoggerTag
 import org.matrix.android.sdk.api.session.call.CallListener
 import org.matrix.android.sdk.api.session.call.CallState
 import org.matrix.android.sdk.api.session.call.MxCall
@@ -36,6 +37,9 @@ import org.matrix.android.sdk.internal.session.SessionScope
 import timber.log.Timber
 import javax.inject.Inject
 
+private val loggerTag = LoggerTag("CallSignalingHandler", LoggerTag.VOIP)
+private const val MAX_AGE_TO_RING = 40_000
+
 @SessionScope
 internal class CallSignalingHandler @Inject constructor(private val activeCallHandler: ActiveCallHandler,
                                                         private val mxCallFactory: MxCallFactory,
@@ -111,12 +115,12 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa
             return
         }
         if (call.isOutgoing) {
-            Timber.v("Got selectAnswer for an outbound call: ignoring")
+            Timber.tag(loggerTag.value).v("Got selectAnswer for an outbound call: ignoring")
             return
         }
         val selectedPartyId = content.selectedPartyId
         if (selectedPartyId == null) {
-            Timber.w("Got nonsensical select_answer with null selected_party_id: ignoring")
+            Timber.tag(loggerTag.value).w("Got nonsensical select_answer with null selected_party_id: ignoring")
             return
         }
         callListenersDispatcher.onCallSelectAnswerReceived(content)
@@ -130,7 +134,7 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa
             return
         }
         if (call.opponentPartyId != null && !call.partyIdsMatches(content)) {
-            Timber.v("Ignoring candidates from party ID ${content.partyId} we have chosen party ID ${call.opponentPartyId}")
+            Timber.tag(loggerTag.value).v("Ignoring candidates from party ID ${content.partyId} we have chosen party ID ${call.opponentPartyId}")
             return
         }
         callListenersDispatcher.onCallIceCandidateReceived(call, content)
@@ -163,10 +167,10 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa
         // party ID must match (our chosen partner hanging up the call) or be undefined (we haven't chosen
         // a partner yet but we're treating the hangup as a reject as per VoIP v0)
         if (call.opponentPartyId != null && !call.partyIdsMatches(content)) {
-            Timber.v("Ignoring hangup from party ID ${content.partyId} we have chosen party ID ${call.opponentPartyId}")
+            Timber.tag(loggerTag.value).v("Ignoring hangup from party ID ${content.partyId} we have chosen party ID ${call.opponentPartyId}")
             return
         }
-        if (call.state != CallState.Terminated) {
+        if (call.state !is CallState.Ended) {
             activeCallHandler.removeCall(content.callId)
             callListenersDispatcher.onCallHangupReceived(content)
         }
@@ -180,12 +184,18 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa
         if (event.roomId == null || event.senderId == null) {
             return
         }
+        val now = System.currentTimeMillis()
+        val age = now - (event.ageLocalTs ?: now)
+        if (age > MAX_AGE_TO_RING) {
+            Timber.tag(loggerTag.value).w("Call invite is too old to ring.")
+            return
+        }
         val content = event.getClearContent().toModel<CallInviteContent>() ?: return
 
         content.callId ?: return
         if (invitedCallIds.contains(content.callId)) {
             // Call is already known, maybe due to fast lane. Ignore
-            Timber.d("Ignoring already known call invite")
+            Timber.tag(loggerTag.value).d("Ignoring already known call invite")
             return
         }
         val incomingCall = mxCallFactory.createIncomingCall(
@@ -214,7 +224,8 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa
             callListenersDispatcher.onCallManagedByOtherSession(content.callId)
         } else {
             if (call.opponentPartyId != null) {
-                Timber.v("Ignoring answer from party ID ${content.partyId} we already have an answer from ${call.opponentPartyId}")
+                Timber.tag(loggerTag.value)
+                        .v("Ignoring answer from party ID ${content.partyId} we already have an answer from ${call.opponentPartyId}")
                 return
             }
             mxCallFactory.updateOutgoingCallWithOpponentData(call, event.senderId, content, content.capabilities)
@@ -231,7 +242,7 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa
             activeCallHandler.getCallWithId(it)
         }
         if (currentCall == null) {
-            Timber.v("Call with id $callId is null")
+            Timber.tag(loggerTag.value).v("Call with id $callId is null")
         }
         return currentCall
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt
index da1f84cc899f297a337de1c408af899f8c1ebf08..4a949e21a691c20fd8da9e51ee05e84f6b5ef882 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt
@@ -21,7 +21,6 @@ import org.matrix.android.sdk.api.session.call.CallSignalingService
 import org.matrix.android.sdk.api.session.call.MxCall
 import org.matrix.android.sdk.api.session.call.TurnServerResponse
 import org.matrix.android.sdk.internal.session.SessionScope
-import timber.log.Timber
 import javax.inject.Inject
 
 @SessionScope
@@ -51,7 +50,6 @@ internal class DefaultCallSignalingService @Inject constructor(
     }
 
     override fun getCallWithId(callId: String): MxCall? {
-        Timber.v("## VOIP getCallWithId $callId all calls ${activeCallHandler.getActiveCallsLiveData().value?.map { it.callId }}")
         return activeCallHandler.getCallWithId(callId)
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt
index f101685a4b46b6f6248d79e635956bf139d8dda6..9fc84e6fe534644e8daa4236be64acf0c88438ee 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/model/MxCallImpl.kt
@@ -17,6 +17,7 @@
 package org.matrix.android.sdk.internal.session.call.model
 
 import org.matrix.android.sdk.api.MatrixConfiguration
+import org.matrix.android.sdk.api.logger.LoggerTag
 import org.matrix.android.sdk.api.session.call.CallIdGenerator
 import org.matrix.android.sdk.api.session.call.CallState
 import org.matrix.android.sdk.api.session.call.MxCall
@@ -38,6 +39,7 @@ import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent
 import org.matrix.android.sdk.api.session.room.model.call.CallReplacesContent
 import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent
 import org.matrix.android.sdk.api.session.room.model.call.CallSignalingContent
+import org.matrix.android.sdk.api.session.room.model.call.EndCallReason
 import org.matrix.android.sdk.api.session.room.model.call.SdpType
 import org.matrix.android.sdk.api.util.Optional
 import org.matrix.android.sdk.internal.session.call.DefaultCallSignalingService
@@ -47,6 +49,8 @@ import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProces
 import timber.log.Timber
 import java.math.BigDecimal
 
+private val loggerTag = LoggerTag("MxCallImpl", LoggerTag.VOIP)
+
 internal class MxCallImpl(
         override val callId: String,
         override val isOutgoing: Boolean,
@@ -93,7 +97,7 @@ internal class MxCallImpl(
             try {
                 it.onStateUpdate(this)
             } catch (failure: Throwable) {
-                Timber.d("dispatchStateChange failed for call $callId : ${failure.localizedMessage}")
+                Timber.tag(loggerTag.value).d("dispatchStateChange failed for call $callId : ${failure.localizedMessage}")
             }
         }
     }
@@ -109,7 +113,7 @@ internal class MxCallImpl(
 
     override fun offerSdp(sdpString: String) {
         if (!isOutgoing) return
-        Timber.v("## VOIP offerSdp $callId")
+        Timber.tag(loggerTag.value).v("offerSdp $callId")
         state = CallState.Dialing
         CallInviteContent(
                 callId = callId,
@@ -124,7 +128,7 @@ internal class MxCallImpl(
     }
 
     override fun sendLocalCallCandidates(candidates: List<CallCandidate>) {
-        Timber.v("Send local call canditates $callId: $candidates")
+        Timber.tag(loggerTag.value).v("Send local call canditates $callId: $candidates")
         CallCandidatesContent(
                 callId = callId,
                 partyId = ourPartyId,
@@ -141,11 +145,11 @@ internal class MxCallImpl(
 
     override fun reject() {
         if (opponentVersion < 1) {
-            Timber.v("Opponent version is less than 1 ($opponentVersion): sending hangup instead of reject")
-            hangUp()
+            Timber.tag(loggerTag.value).v("Opponent version is less than 1 ($opponentVersion): sending hangup instead of reject")
+            hangUp(EndCallReason.USER_HANGUP)
             return
         }
-        Timber.v("## VOIP reject $callId")
+        Timber.tag(loggerTag.value).v("reject $callId")
         CallRejectContent(
                 callId = callId,
                 partyId = ourPartyId,
@@ -153,24 +157,24 @@ internal class MxCallImpl(
         )
                 .let { createEventAndLocalEcho(type = EventType.CALL_REJECT, roomId = roomId, content = it.toContent()) }
                 .also { eventSenderProcessor.postEvent(it) }
-        state = CallState.Terminated
+        state = CallState.Ended(reason = EndCallReason.USER_HANGUP)
     }
 
-    override fun hangUp(reason: CallHangupContent.Reason?) {
-        Timber.v("## VOIP hangup $callId")
+    override fun hangUp(reason: EndCallReason?) {
+        Timber.tag(loggerTag.value).v("hangup $callId")
         CallHangupContent(
                 callId = callId,
                 partyId = ourPartyId,
-                reason = reason ?: CallHangupContent.Reason.USER_HANGUP,
+                reason = reason,
                 version = MxCall.VOIP_PROTO_VERSION.toString()
         )
                 .let { createEventAndLocalEcho(type = EventType.CALL_HANGUP, roomId = roomId, content = it.toContent()) }
                 .also { eventSenderProcessor.postEvent(it) }
-        state = CallState.Terminated
+        state = CallState.Ended(reason)
     }
 
     override fun accept(sdpString: String) {
-        Timber.v("## VOIP accept $callId")
+        Timber.tag(loggerTag.value).v("accept $callId")
         if (isOutgoing) return
         state = CallState.Answering
         CallAnswerContent(
@@ -185,7 +189,7 @@ internal class MxCallImpl(
     }
 
     override fun negotiate(sdpString: String, type: SdpType) {
-        Timber.v("## VOIP negotiate $callId")
+        Timber.tag(loggerTag.value).v("negotiate $callId")
         CallNegotiateContent(
                 callId = callId,
                 partyId = ourPartyId,
@@ -198,7 +202,7 @@ internal class MxCallImpl(
     }
 
     override fun selectAnswer() {
-        Timber.v("## VOIP select answer $callId")
+        Timber.tag(loggerTag.value).v("select answer $callId")
         if (isOutgoing) return
         state = CallState.Answering
         CallSelectAnswerContent(
@@ -219,7 +223,7 @@ internal class MxCallImpl(
         val profileInfo = try {
             getProfileInfoTask.execute(profileInfoParams)
         } catch (failure: Throwable) {
-            Timber.v("Fail fetching profile info of $targetUserId while transferring call")
+            Timber.tag(loggerTag.value).v("Fail fetching profile info of $targetUserId while transferring call")
             null
         }
         CallReplacesContent(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt
index c4bc09a233b8237ebd29e26ff1182d1635f3b34e..1fe4f9d90a6f76c835fe5d13f9635af634163129 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetCapabilitiesResult.kt
@@ -70,7 +70,22 @@ internal data class RoomVersions(
          * Required. A detailed description of the room versions the server supports.
          */
         @Json(name = "available")
-        val available: JsonDict
+        val available: JsonDict? = null,
+
+        /**
+         *  "room_capabilities": {
+         *      "knock" : {
+         *              "preferred": "7",
+         *              "support" : ["7"]
+         *      },
+         *      "restricted" : {
+         *              "preferred": "9",
+         *              "support" : ["8", "9"]
+         *      }
+         * }
+         */
+        @Json(name = "room_capabilities")
+        val roomCapabilities: JsonDict? = null
 )
 
 // The spec says: If not present, the client should assume that password changes are possible via the API
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt
index 38f6b08b43b39d9eba976a7820f0faaeb0f6652a..4e8abcf7840c38f5e314ab7770ae0ce42f6f508b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt
@@ -113,8 +113,8 @@ internal class DefaultPushRuleService @Inject constructor(
         addPushRuleTask.execute(AddPushRuleTask.Params(kind, pushRule))
     }
 
-    override suspend fun updatePushRuleActions(kind: RuleKind, oldPushRule: PushRule, newPushRule: PushRule) {
-        updatePushRuleActionsTask.execute(UpdatePushRuleActionsTask.Params(kind, oldPushRule, newPushRule))
+    override suspend fun updatePushRuleActions(kind: RuleKind, ruleId: String, enable: Boolean, actions: List<Action>?) {
+        updatePushRuleActionsTask.execute(UpdatePushRuleActionsTask.Params(kind, ruleId, enable, actions))
     }
 
     override suspend fun removePushRule(kind: RuleKind, pushRule: PushRule) {
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
index 82565d8118612d63dcc92cb67a5fe98f5dca60f9..21f55bbc42535b6da82316844e01155d36512504 100644
--- 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
@@ -17,17 +17,24 @@
 package org.matrix.android.sdk.internal.session.permalinks
 
 import org.matrix.android.sdk.api.MatrixPatterns.getDomain
+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.toModel
 import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
 import org.matrix.android.sdk.api.session.room.model.Membership
+import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
+import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
 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.state.StateEventDataSource
 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>
+        private val roomGetterProvider: Provider<RoomGetter>,
+        private val stateEventDataSource: StateEventDataSource
 ) {
 
     fun computeViaParams(roomId: String, max: Int): List<String> {
@@ -70,4 +77,28 @@ internal class ViaParameterFinder @Inject constructor(
                 .orEmpty()
                 .toSet()
     }
+
+    fun computeViaParamsForRestricted(roomId: String, max: Int): List<String> {
+        val userThatCanInvite = roomGetterProvider.get().getRoom(roomId)
+                ?.getRoomMembers(roomMemberQueryParams { memberships = listOf(Membership.JOIN) })
+                ?.map { it.userId }
+                ?.filter { userCanInvite(userId, roomId) }
+                .orEmpty()
+                .toSet()
+
+        return userThatCanInvite.map { it.getDomain() }
+                .groupBy { it }
+                .mapValues { it.value.size }
+                .toMutableMap()
+                .let { map -> map.keys.sortedByDescending { map[it] } }
+                .take(max)
+    }
+
+    fun userCanInvite(userId: String, roomId: String): Boolean {
+        val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition)
+                ?.content?.toModel<PowerLevelsContent>()
+                ?.let { PowerLevelsHelper(it) }
+
+        return powerLevelsHelper?.isUserAbleToInvite(userId) ?: false
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/UpdatePushRuleActionsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/UpdatePushRuleActionsTask.kt
index 2a24aee8929111559584efbf6fda0e3d97deeca3..b8dbabd09ebcd6eb1cfa5511164499546b2f7906 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/UpdatePushRuleActionsTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/UpdatePushRuleActionsTask.kt
@@ -15,8 +15,9 @@
  */
 package org.matrix.android.sdk.internal.session.pushers
 
+import org.matrix.android.sdk.api.pushrules.Action
 import org.matrix.android.sdk.api.pushrules.RuleKind
-import org.matrix.android.sdk.api.pushrules.rest.PushRule
+import org.matrix.android.sdk.api.pushrules.toJson
 import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
 import org.matrix.android.sdk.internal.network.executeRequest
 import org.matrix.android.sdk.internal.task.Task
@@ -25,8 +26,9 @@ import javax.inject.Inject
 internal interface UpdatePushRuleActionsTask : Task<UpdatePushRuleActionsTask.Params, Unit> {
     data class Params(
             val kind: RuleKind,
-            val oldPushRule: PushRule,
-            val newPushRule: PushRule
+            val ruleId: String,
+            val enable: Boolean,
+            val actions: List<Action>?
     )
 }
 
@@ -36,20 +38,14 @@ internal class DefaultUpdatePushRuleActionsTask @Inject constructor(
 ) : UpdatePushRuleActionsTask {
 
     override suspend fun execute(params: UpdatePushRuleActionsTask.Params) {
-        if (params.oldPushRule.enabled != params.newPushRule.enabled) {
-            // First change enabled state
             executeRequest(globalErrorReceiver) {
-                pushRulesApi.updateEnableRuleStatus(params.kind.value, params.newPushRule.ruleId, params.newPushRule.enabled)
+                pushRulesApi.updateEnableRuleStatus(params.kind.value, params.ruleId, enable = params.enable)
             }
-        }
-
-        if (params.newPushRule.enabled) {
-            // Also ensure the actions are up to date
-            val body = mapOf("actions" to params.newPushRule.actions)
-
-            executeRequest(globalErrorReceiver) {
-                pushRulesApi.updateRuleActions(params.kind.value, params.newPushRule.ruleId, body)
+            if (params.actions != null) {
+                val body = mapOf("actions" to params.actions.toJson())
+                executeRequest(globalErrorReceiver) {
+                    pushRulesApi.updateRuleActions(params.kind.value, params.ruleId, body)
+                }
             }
-        }
     }
 }
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 86d2b70da1a87e83dcebb491e19afb7da2a42534..9bb3899f2fdc966b183379f1cc7c3798df9ce9cb 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
@@ -17,16 +17,10 @@
 package org.matrix.android.sdk.internal.session.room.create
 
 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
@@ -45,7 +39,6 @@ import javax.inject.Inject
 
 internal class CreateRoomBodyBuilder @Inject constructor(
         private val ensureIdentityTokenTask: EnsureIdentityTokenTask,
-        private val crossSigningService: CrossSigningService,
         private val deviceListManager: DeviceListManager,
         private val identityStore: IdentityStore,
         private val fileUploader: FileUploader,
@@ -76,18 +69,18 @@ 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),
-                buildGuestAccess(params),
-                buildJoinRulesRestricted(params)
-        )
+        params.featurePreset?.updateRoomParams(params)
+
+        val initialStates = (
+                listOfNotNull(
+                        buildEncryptionWithAlgorithmEvent(params),
+                        buildHistoryVisibilityEvent(params),
+                        buildAvatarEvent(params),
+                        buildGuestAccess(params)
+                )
+                        + params.featurePreset?.setupInitialStates().orEmpty()
+                        + buildCustomInitialStates(params)
+                )
                 .takeIf { it.isNotEmpty() }
 
         return CreateRoomBody(
@@ -95,7 +88,7 @@ internal class CreateRoomBodyBuilder @Inject constructor(
                 roomAliasName = params.roomAliasName,
                 name = params.name,
                 topic = params.topic,
-                invitedUserIds = params.invitedUserIds.filter { it != userId },
+                invitedUserIds = params.invitedUserIds.filter { it != userId }.takeIf { it.isNotEmpty() },
                 invite3pids = invite3pids,
                 creationContent = params.creationContent.takeIf { it.isNotEmpty() },
                 initialStates = initialStates,
@@ -103,10 +96,19 @@ internal class CreateRoomBodyBuilder @Inject constructor(
                 isDirect = params.isDirect,
                 powerLevelContentOverride = params.powerLevelContentOverride,
                 roomVersion = params.roomVersion
-
         )
     }
 
+    private fun buildCustomInitialStates(params: CreateRoomParams): List<Event> {
+        return params.initialStates.map {
+            Event(
+                    type = it.type,
+                    stateKey = it.stateKey,
+                    content = it.content
+            )
+        }
+    }
+
     private suspend fun buildAvatarEvent(params: CreateRoomParams): Event? {
         return params.avatarUri?.let { avatarUri ->
             // First upload the image, ignoring any error
@@ -148,20 +150,6 @@ internal class CreateRoomBodyBuilder @Inject constructor(
                 }
     }
 
-    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/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
index 092fec4d72217f01e0b5611d519035c6478c5efc..a64b9039474a9f4c54dc113a55dd86de2735d257 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
@@ -184,7 +184,8 @@ internal class DefaultSendService @AssistedInject constructor(
                             mimeType = messageContent.mimeType,
                             name = messageContent.body,
                             queryUri = Uri.parse(messageContent.url),
-                            type = ContentAttachmentData.Type.AUDIO
+                            type = ContentAttachmentData.Type.AUDIO,
+                            waveform = messageContent.audioWaveformInfo?.waveform
                     )
                     localEchoRepository.updateSendState(localEcho.eventId, roomId, SendState.UNSENT)
                     internalSendMedia(listOf(localEcho.root), attachmentData, true)
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 f505b13b337ee1325f4e3eacd544d4e6ccf87292..c610326a9409625c91b0a7e965891f797462f0e2 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
@@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.events.model.RelationType
 import org.matrix.android.sdk.api.session.events.model.UnsignedData
 import org.matrix.android.sdk.api.session.events.model.toContent
 import org.matrix.android.sdk.api.session.room.model.message.AudioInfo
+import org.matrix.android.sdk.api.session.room.model.message.AudioWaveformInfo
 import org.matrix.android.sdk.api.session.room.model.message.FileInfo
 import org.matrix.android.sdk.api.session.room.model.message.ImageInfo
 import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
@@ -74,6 +75,7 @@ internal class LocalEchoEventFactory @Inject constructor(
         private val markdownParser: MarkdownParser,
         private val textPillsUtils: TextPillsUtils,
         private val thumbnailExtractor: ThumbnailExtractor,
+        private val waveformSanitizer: WaveFormSanitizer,
         private val localEchoRepository: LocalEchoRepository,
         private val permalinkFactory: PermalinkFactory
 ) {
@@ -289,14 +291,21 @@ internal class LocalEchoEventFactory @Inject constructor(
     }
 
     private fun createAudioEvent(roomId: String, attachment: ContentAttachmentData): Event {
+        val isVoiceMessage = attachment.waveform != null
         val content = MessageAudioContent(
                 msgType = MessageType.MSGTYPE_AUDIO,
                 body = attachment.name ?: "audio",
                 audioInfo = AudioInfo(
+                        duration = attachment.duration?.toInt(),
                         mimeType = attachment.getSafeMimeType()?.takeIf { it.isNotBlank() },
                         size = attachment.size
                 ),
-                url = attachment.queryUri.toString()
+                url = attachment.queryUri.toString(),
+                audioWaveformInfo = if (!isVoiceMessage) null else AudioWaveformInfo(
+                        duration = attachment.duration?.toInt(),
+                        waveform = waveformSanitizer.sanitize(attachment.waveform)
+                ),
+                voiceMessageIndicator = if (!isVoiceMessage) null else emptyMap()
         )
         return createMessageEvent(roomId, content)
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/WaveFormSanitizer.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/WaveFormSanitizer.kt
new file mode 100644
index 0000000000000000000000000000000000000000..78a03f3775c6e7aa4f3ed5134ea26cef199b9986
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/WaveFormSanitizer.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.send
+
+import timber.log.Timber
+import javax.inject.Inject
+import kotlin.math.abs
+import kotlin.math.ceil
+
+internal class WaveFormSanitizer @Inject constructor() {
+    private companion object {
+        const val MIN_NUMBER_OF_VALUES = 30
+        const val MAX_NUMBER_OF_VALUES = 120
+
+        const val MAX_VALUE = 1024
+    }
+
+    /**
+     * The array should have no less than 30 elements and no more than 120.
+     * List of integers between zero and 1024, inclusive.
+     */
+    fun sanitize(waveForm: List<Int>?): List<Int>? {
+        if (waveForm.isNullOrEmpty()) {
+            return null
+        }
+
+        // Limit the number of items
+        val sizeInRangeList = mutableListOf<Int>()
+        when {
+            waveForm.size < MIN_NUMBER_OF_VALUES -> {
+                // Repeat the same value to have at least 30 items
+                val repeatTimes = ceil(MIN_NUMBER_OF_VALUES / waveForm.size.toDouble()).toInt()
+                waveForm.map { value ->
+                    repeat(repeatTimes) {
+                        sizeInRangeList.add(value)
+                    }
+                }
+            }
+            waveForm.size > MAX_NUMBER_OF_VALUES -> {
+                val keepOneOf = ceil(waveForm.size.toDouble() / MAX_NUMBER_OF_VALUES).toInt()
+                waveForm.mapIndexed { idx, value ->
+                    if (idx % keepOneOf == 0) {
+                        sizeInRangeList.add(value)
+                    }
+                }
+            }
+            else                                 -> {
+                sizeInRangeList.addAll(waveForm)
+            }
+        }
+
+        // OK, ensure all items are positive
+        val positiveList = sizeInRangeList.map {
+            abs(it)
+        }
+
+        // Ensure max is not above MAX_VALUE
+        val max = positiveList.maxOrNull() ?: MAX_VALUE
+
+        val finalList = if (max > MAX_VALUE) {
+            // Reduce the values
+            positiveList.map {
+                it * MAX_VALUE / max
+            }
+        } else {
+            positiveList
+        }
+
+        Timber.d("Sanitize from ${waveForm.size} items to ${finalList.size} items. Max value was $max")
+        return finalList
+    }
+}
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 ff2afb5d61aff480092658ea0c8d9900e0de1f8b..7eed22f65fcb29af921bc454c79c55527135025b 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
@@ -19,8 +19,8 @@ package org.matrix.android.sdk.internal.session.room.state
 import android.net.Uri
 import androidx.lifecycle.LiveData
 import dagger.assisted.Assisted
-import dagger.assisted.AssistedInject
 import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
 import org.matrix.android.sdk.api.query.QueryStringValue
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
@@ -29,17 +29,20 @@ import org.matrix.android.sdk.api.session.room.model.GuestAccess
 import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent
 import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
 import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
+import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry
+import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
 import org.matrix.android.sdk.api.session.room.state.StateService
 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
+import org.matrix.android.sdk.internal.session.permalinks.ViaParameterFinder
 
 internal class DefaultStateService @AssistedInject constructor(@Assisted private val roomId: String,
                                                                private val stateEventDataSource: StateEventDataSource,
                                                                private val sendStateTask: SendStateTask,
-                                                               private val fileUploader: FileUploader
+                                                               private val fileUploader: FileUploader,
+                                                               private val viaParameterFinder: ViaParameterFinder
 ) : StateService {
 
     @AssistedFactory
@@ -126,12 +129,19 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
         )
     }
 
-    override suspend fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?) {
+    override suspend fun updateJoinRule(joinRules: RoomJoinRules?, guestAccess: GuestAccess?, allowList: List<RoomJoinRulesAllowEntry>?) {
         if (joinRules != null) {
-            if (joinRules == RoomJoinRules.RESTRICTED) throw UnsupportedOperationException("No yet supported")
+            val body = if (joinRules == RoomJoinRules.RESTRICTED) {
+                RoomJoinRulesContent(
+                        _joinRules = RoomJoinRules.RESTRICTED.value,
+                        allowList = allowList
+                ).toContent()
+            } else {
+                mapOf("join_rule" to joinRules)
+            }
             sendStateEvent(
                     eventType = EventType.STATE_ROOM_JOIN_RULES,
-                    body = mapOf("join_rule" to joinRules),
+                    body = body,
                     stateKey = null
             )
         }
@@ -160,4 +170,20 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
                 stateKey = null
         )
     }
+
+    override suspend fun setJoinRulePublic() {
+        updateJoinRule(RoomJoinRules.PUBLIC, null)
+    }
+
+    override suspend fun setJoinRuleInviteOnly() {
+        updateJoinRule(RoomJoinRules.INVITE, null)
+    }
+
+    override suspend fun setJoinRuleRestricted(allowList: List<String>) {
+        // we need to compute correct via parameters and check if PL are correct
+        val allowEntries = allowList.map { spaceId ->
+            RoomJoinRulesAllowEntry(spaceId, viaParameterFinder.computeViaParamsForRestricted(spaceId, 3))
+        }
+        updateJoinRule(RoomJoinRules.RESTRICTED, null, allowEntries)
+    }
 }
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 bff1af60ca9af4c1e8f3df7f489592ba76c4c908..0b8c6df8066c6274acaf8c257619a030b1f45ed3 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
@@ -247,10 +247,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
                 }
             }
@@ -274,15 +274,15 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat
             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_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
+            RoomCategoryFilter.ALL                     -> Unit // nop
         }
 
         // Timber.w("VAL: activeSpaceId : ${queryParams.activeSpaceId}")
         when (queryParams.activeSpaceFilter) {
-            is ActiveSpaceFilter.ActiveSpace -> {
+            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) {
@@ -300,8 +300,8 @@ internal class RoomSummaryDataSource @Inject constructor(@SessionDatabase privat
             }
         }
 
-        if (queryParams.activeGroupId != null) {
-            query.contains(RoomSummaryEntityFields.GROUP_IDS, queryParams.activeGroupId!!)
+        queryParams.activeGroupId?.let { activeGroupId ->
+            query.contains(RoomSummaryEntityFields.GROUP_IDS, activeGroupId)
         }
         return query
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/file/AtomicFileCreator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/file/AtomicFileCreator.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ca10c0ed0f85cecb5b098e3f80d758bfeca0a9e9
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/file/AtomicFileCreator.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.file
+
+import timber.log.Timber
+import java.io.File
+
+internal class AtomicFileCreator(private val file: File) {
+    val partFile = File(file.parentFile, "${file.name}.part")
+
+    init {
+        if (file.exists()) {
+            Timber.w("## AtomicFileCreator: target file ${file.path} exists, it should not happen.")
+        }
+        if (partFile.exists()) {
+            Timber.d("## AtomicFileCreator: discard aborted part file ${partFile.path}")
+            // No need to delete the file, we will overwrite it
+        }
+    }
+
+    fun cancel() {
+        partFile.delete()
+    }
+
+    fun commit() {
+        partFile.renameTo(file)
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/send/WaveFormSanitizerTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/send/WaveFormSanitizerTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..23c8aeb76bbc212909d6aacd2be5ef1592449561
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/send/WaveFormSanitizerTest.kt
@@ -0,0 +1,123 @@
+/*
+ * 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.send
+
+import org.amshove.kluent.shouldBe
+import org.amshove.kluent.shouldBeInRange
+import org.junit.Test
+
+class WaveFormSanitizerTest {
+
+    private val waveFormSanitizer = WaveFormSanitizer()
+
+    @Test
+    fun sanitizeNull() {
+        waveFormSanitizer.sanitize(null) shouldBe null
+    }
+
+    @Test
+    fun sanitizeEmpty() {
+        waveFormSanitizer.sanitize(emptyList()) shouldBe null
+    }
+
+    @Test
+    fun sanitizeSingleton() {
+        val result = waveFormSanitizer.sanitize(listOf(1))!!
+        result.size shouldBe 30
+        checkResult(result)
+    }
+
+    @Test
+    fun sanitize29() {
+        val list = generateSequence { 1 }.take(29).toList()
+        val result = waveFormSanitizer.sanitize(list)!!
+        checkResult(result)
+    }
+
+    @Test
+    fun sanitize30() {
+        val list = generateSequence { 1 }.take(30).toList()
+        val result = waveFormSanitizer.sanitize(list)!!
+        result.size shouldBe 30
+        checkResult(result)
+    }
+
+    @Test
+    fun sanitize31() {
+        val list = generateSequence { 1 }.take(31).toList()
+        val result = waveFormSanitizer.sanitize(list)!!
+        checkResult(result)
+    }
+
+    @Test
+    fun sanitize119() {
+        val list = generateSequence { 1 }.take(119).toList()
+        val result = waveFormSanitizer.sanitize(list)!!
+        checkResult(result)
+    }
+
+    @Test
+    fun sanitize120() {
+        val list = generateSequence { 1 }.take(120).toList()
+        val result = waveFormSanitizer.sanitize(list)!!
+        result.size shouldBe 120
+        checkResult(result)
+    }
+
+    @Test
+    fun sanitize121() {
+        val list = generateSequence { 1 }.take(121).toList()
+        val result = waveFormSanitizer.sanitize(list)!!
+        checkResult(result)
+    }
+
+    @Test
+    fun sanitize1024() {
+        val list = generateSequence { 1 }.take(1024).toList()
+        val result = waveFormSanitizer.sanitize(list)!!
+        checkResult(result)
+    }
+
+    @Test
+    fun sanitizeNegative() {
+        val list = generateSequence { -1 }.take(30).toList()
+        val result = waveFormSanitizer.sanitize(list)!!
+        checkResult(result)
+    }
+
+    @Test
+    fun sanitizeMaxValue() {
+        val list = generateSequence { 1025 }.take(30).toList()
+        val result = waveFormSanitizer.sanitize(list)!!
+        checkResult(result)
+    }
+
+    @Test
+    fun sanitizeNegativeMaxValue() {
+        val list = generateSequence { -1025 }.take(30).toList()
+        val result = waveFormSanitizer.sanitize(list)!!
+        checkResult(result)
+    }
+
+    private fun checkResult(result: List<Int>) {
+        result.forEach {
+            it shouldBeInRange 0..1024
+        }
+
+        result.size shouldBeInRange 30..120
+    }
+}