diff --git a/CHANGES.md b/CHANGES.md
index ca705ea53b62adc3f7110b3a125a332a63fccac6..01cac0b47acd8aeddb3cab2469b12ccf379959d5 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,16 @@
 Please also refer to the Changelog of Element Android: https://github.com/vector-im/element-android/blob/main/CHANGES.md
 
+Changes in Matrix-SDK 1.3.4 (2021-10-20)
+===================================================
+
+Imported from Element 1.3.4. (https://github.com/vector-im/element-android/releases/tag/v1.3.4)
+
+SDK API changes ⚠️
+------------------
+Add Presence support:
+- PresenceService has been added, with the ability to set the presence of the current user
+- For DM, the presence of the other user is added to the RoomSummary
+
 Changes in Matrix-SDK 1.3.2 (2021-10-08)
 ===================================================
 
diff --git a/dependencies.gradle b/dependencies.gradle
index 92358952db5c828d612dd79ddcb377e8a84f0f80..7fa42a666f0bb8fccbd69efe145c73cf14d5083d 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -7,7 +7,7 @@ ext.versions = [
         'targetCompat'      : JavaVersion.VERSION_11,
 ]
 
-def gradle = "7.0.2"
+def gradle = "7.0.3"
 // Ref: https://kotlinlang.org/releases.html
 def kotlin = "1.5.31"
 def kotlinCoroutines = "1.5.2"
@@ -19,6 +19,7 @@ def moshi = "1.12.0"
 def lifecycle = "2.2.0"
 def rxBinding = "3.1.0"
 def epoxy = "4.6.2"
+def mavericks = "2.4.0"
 def glide = "4.12.0"
 def bigImageViewer = "1.8.1"
 def jjwt = "0.11.2"
@@ -36,8 +37,6 @@ ext.libs = [
                 'kotlinPlugin'            : "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin"
         ],
         jetbrains   : [
-                'kotlinStdlibJdk7'        : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin",
-                'kotlinStdlib'            : "org.jetbrains.kotlin:kotlin-stdlib:$kotlin",
                 'coroutinesCore'          : "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutines",
                 'coroutinesAndroid'       : "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinCoroutines",
                 'coroutinesRx2'           : "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$kotlinCoroutines"
@@ -98,7 +97,9 @@ ext.libs = [
                 'epoxyGlide'             : "com.airbnb.android:epoxy-glide-preloading:$epoxy",
                 'epoxyProcessor'         : "com.airbnb.android:epoxy-processor:$epoxy",
                 'epoxyPaging'            : "com.airbnb.android:epoxy-paging:$epoxy",
-                'mvrx'                   : "com.airbnb.android:mvrx:1.5.1"
+                'mavericks'              : "com.airbnb.android:mavericks:$mavericks",
+                'mavericksRx'            : "com.airbnb.android:mavericks-rxjava2:$mavericks",
+                'mavericksTesting'       : "com.airbnb.android:mavericks-testing:$mavericks"
         ],
         mockk      : [
                 'mockk'                   : "io.mockk:mockk:$mockk",
@@ -123,10 +124,13 @@ ext.libs = [
                 'jjwtImpl'               : "io.jsonwebtoken:jjwt-impl:$jjwt",
                 'jjwtOrgjson'            : "io.jsonwebtoken:jjwt-orgjson:$jjwt"
         ],
-        vanniktech: [
+        vanniktech  : [
                 'emojiMaterial'          : "com.vanniktech:emoji-material:$vanniktechEmoji",
                 'emojiGoogle'            : "com.vanniktech:emoji-google:$vanniktechEmoji"
         ],
+        apache      : [
+                'commonsImaging'         : "org.apache.sanselan:sanselan:0.97-incubator"
+        ],
         tests       : [
                 'kluent'                 : "org.amshove.kluent:kluent-android:1.68",
                 'timberJunitRule'        : "net.lachlanmckee:timber-junit-rule:1.0.1",
diff --git a/gradle.properties b/gradle.properties
index a00acf5c92e35e0d88a092e9d1fbc90958bdfcbd..970f551b196656343ba73975e84b813a4c24632d 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -26,7 +26,7 @@ vector.httpLogLevel=NONE
 # Ref: https://github.com/vanniktech/gradle-maven-publish-plugin
 GROUP=org.matrix.android
 POM_ARTIFACT_ID=matrix-android-sdk2
-VERSION_NAME=1.3.2
+VERSION_NAME=1.3.4
 
 POM_PACKAGING=aar
 
diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle
index 7196a97bd9cd07f9d4627d139c66d3b2d935bbaf..12b37b00b23d826819ee1de20f337e502f94be1d 100644
--- a/matrix-sdk-android/build.gradle
+++ b/matrix-sdk-android/build.gradle
@@ -103,8 +103,6 @@ static def gitRevisionDate() {
 }
 
 dependencies {
-
-    implementation libs.jetbrains.kotlinStdlibJdk7
     implementation libs.jetbrains.coroutinesCore
     implementation libs.jetbrains.coroutinesAndroid
 
@@ -133,6 +131,7 @@ dependencies {
 
     // Database
     implementation 'com.github.Zhuinden:realm-monarchy:0.7.1'
+
     kapt 'dk.ilios:realmfieldnameshelper:2.0.0'
 
     // Work
@@ -156,8 +155,11 @@ dependencies {
     // Video compression
     implementation 'com.otaliastudios:transcoder:0.10.4'
 
+    // Exif data handling
+    implementation libs.apache.commonsImaging
+
     // Phone number https://github.com/google/libphonenumber
-    implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.34'
+    implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.35'
 
     testImplementation libs.tests.junit
     testImplementation 'org.robolectric:robolectric:4.6.1'
@@ -168,6 +170,8 @@ dependencies {
     implementation libs.jetbrains.coroutinesAndroid
     // Plant Timber tree for test
     testImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
+    // Transitively required for mocking realm as monarchy doesn't expose Rx
+    testImplementation libs.rx.rxKotlin
 
     kaptAndroidTest libs.dagger.daggerCompiler
     androidTestImplementation libs.androidx.testCore
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/SingleThreadCoroutineDispatcher.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/SingleThreadCoroutineDispatcher.kt
index 192f6442b22c97b5436a530f1da914ab938fbde9..3e3af10799a4d07daa6395ad1c96af33fcf9032b 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/SingleThreadCoroutineDispatcher.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/SingleThreadCoroutineDispatcher.kt
@@ -18,7 +18,7 @@ package org.matrix.android.sdk
 
 import kotlinx.coroutines.Dispatchers.Main
 import kotlinx.coroutines.asCoroutineDispatcher
-import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import java.util.concurrent.Executors
 
 internal val testCoroutineDispatchers = MatrixCoroutineDispatchers(Main, Main, Main, Main,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/MatrixCoroutineDispatchers.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixCoroutineDispatchers.kt
similarity index 85%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/MatrixCoroutineDispatchers.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixCoroutineDispatchers.kt
index b44a543c1cadb8d4fd116d8fda91c0b650110b5e..592974be743e02e5e671f6e21964f4a073bd89dd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/MatrixCoroutineDispatchers.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixCoroutineDispatchers.kt
@@ -5,7 +5,7 @@
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
- * http://www.apache.org/licenses/LICENSE-2.0
+ *     http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
@@ -14,11 +14,11 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.util
+package org.matrix.android.sdk.api
 
 import kotlinx.coroutines.CoroutineDispatcher
 
-internal data class MatrixCoroutineDispatchers(
+data class MatrixCoroutineDispatchers(
         val io: CoroutineDispatcher,
         val computation: CoroutineDispatcher,
         val main: CoroutineDispatcher,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt
index bde68da9d7e4d1563eedf22cf4fc9bb31d117a66..d4bfd4ee8c13ebe02c81ff1157b1b27525acde5e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt
@@ -20,6 +20,7 @@ import androidx.annotation.MainThread
 import androidx.lifecycle.LiveData
 import kotlinx.coroutines.flow.SharedFlow
 import okhttp3.OkHttpClient
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.auth.data.SessionParams
 import org.matrix.android.sdk.api.failure.GlobalError
 import org.matrix.android.sdk.api.federation.FederationService
@@ -42,6 +43,7 @@ import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerS
 import org.matrix.android.sdk.api.session.media.MediaService
 import org.matrix.android.sdk.api.session.openid.OpenIdService
 import org.matrix.android.sdk.api.session.permalinks.PermalinkService
+import org.matrix.android.sdk.api.session.presence.PresenceService
 import org.matrix.android.sdk.api.session.profile.ProfileService
 import org.matrix.android.sdk.api.session.pushers.PushersService
 import org.matrix.android.sdk.api.session.room.RoomDirectoryService
@@ -75,6 +77,7 @@ interface Session :
         TermsService,
         EventService,
         ProfileService,
+        PresenceService,
         PushRuleService,
         PushersService,
         SyncStatusService,
@@ -82,6 +85,8 @@ interface Session :
         SecureStorageService,
         AccountService {
 
+    val coroutineDispatchers: MatrixCoroutineDispatchers
+
     /**
      * The params associated to the session
      */
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
index 96b44ce8c9034616ecdcc0e2b1eac3759ca5a12c..169f90dbca0a5b023a8f27b32e21af705cdd2051 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
@@ -30,6 +30,7 @@ import org.matrix.android.sdk.api.util.JsonDict
 import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
 import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
 import org.matrix.android.sdk.internal.di.MoshiProvider
+import org.matrix.android.sdk.internal.session.presence.model.PresenceContent
 import timber.log.Timber
 
 typealias Content = JsonDict
@@ -305,3 +306,7 @@ fun Event.isReply(): Boolean {
 fun Event.isEdition(): Boolean {
     return getRelationContent()?.takeIf { it.type == RelationType.REPLACE }?.eventId != null
 }
+
+fun Event.getPresenceContent(): PresenceContent? {
+    return content.toModel<PresenceContent>()
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/presence/PresenceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/presence/PresenceService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..82a81f4b64e064fb61cd0d3536ebc63259349172
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/presence/PresenceService.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.matrix.android.sdk.api.session.presence
+
+import org.matrix.android.sdk.api.session.presence.model.PresenceEnum
+import org.matrix.android.sdk.api.session.presence.model.UserPresence
+
+/**
+ * This interface defines methods for handling user presence information.
+ */
+interface PresenceService {
+    /**
+     * Update the presence status for the current user
+     * @param presence the new presence state
+     * @param statusMsg the status message to attach to this state
+     */
+    suspend fun setMyPresence(presence: PresenceEnum, statusMsg: String? = null)
+
+    /**
+     * Fetch the given user's presence state.
+     * @param userId the userId whose presence state to get.
+     */
+    suspend fun fetchPresence(userId: String): UserPresence
+
+    // TODO Add live data (of Flow) of the presence of a userId
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/presence/model/PresenceEnum.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/presence/model/PresenceEnum.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6d9994ef1ca9e75b0bde56257e4ba7603f260fbd
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/presence/model/PresenceEnum.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.presence.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = false)
+enum class PresenceEnum(val value: String) {
+    @Json(name = "online")
+    ONLINE("online"),
+
+    @Json(name = "offline")
+    OFFLINE("offline"),
+
+    @Json(name = "unavailable")
+    UNAVAILABLE("unavailable");
+
+    companion object {
+        fun from(s: String): PresenceEnum? = values().find { it.value == s }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/presence/model/UserPresence.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/presence/model/UserPresence.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6b33ff07d5a5d38f1bd9a0aef109a3f28cfc095f
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/presence/model/UserPresence.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.presence.model
+
+data class UserPresence(
+        val lastActiveAgo: Long? = null,
+        val statusMessage: String? = null,
+        val isCurrentlyActive: Boolean? = null,
+        val presence: PresenceEnum = PresenceEnum.OFFLINE
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt
index 2cd17952c688ac2a985960ce00345660bd090120..f884d3e8906b8c298d55191254729d41ef363069 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt
@@ -29,38 +29,19 @@ interface PushersService {
      * Add a new HTTP pusher.
      * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-pushers-set
      *
-     * @param pushkey           This is a unique identifier for this pusher. The value you should use for
-     *                          this is the routing or destination address information for the notification,
-     *                          for example, the APNS token for APNS or the Registration ID for GCM. If your
-     *                          notification client has no such concept, use any unique identifier. Max length, 512 chars.
-     * @param appId             the application id
-     *                          This is a reverse-DNS style identifier for the application. It is recommended
-     *                          that this end with the platform, such that different platform versions get
-     *                          different app identifiers. Max length, 64 chars.
-     * @param profileTag        This string determines which set of device specific rules this pusher executes.
-     * @param lang              The preferred language for receiving notifications (e.g. "en" or "en-US").
-     * @param appDisplayName    A human readable string that will allow the user to identify what application owns this pusher.
-     * @param deviceDisplayName A human readable string that will allow the user to identify what device owns this pusher.
-     * @param url               The URL to use to send notifications to. MUST be an HTTPS URL with a path of /_matrix/push/v1/notify.
-     * @param append            If true, the homeserver should add another pusher with the given pushkey and App ID in addition
-     *                          to any others with different user IDs. Otherwise, the homeserver must remove any other pushers
-     *                          with the same App ID and pushkey for different users.
-     * @param withEventIdOnly   true to limit the push content to only id and not message content
-     *                          Ref: https://matrix.org/docs/spec/push_gateway/r0.1.1#homeserver-behaviour
+     * @throws [InvalidParameterException] if a parameter is not correct
+     */
+    suspend fun addHttpPusher(httpPusher: HttpPusher)
+
+    /**
+     * Enqueues a new HTTP pusher via the WorkManager API.
+     * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-pushers-set
      *
-     * @return                  A work request uuid. Can be used to listen to the status
-     *                          (LiveData<WorkInfo> status = workManager.getWorkInfoByIdLiveData(<UUID>))
+     * @return A work request uuid. Can be used to listen to the status
+     *         (LiveData<WorkInfo> status = workManager.getWorkInfoByIdLiveData(<UUID>))
      * @throws [InvalidParameterException] if a parameter is not correct
      */
-    fun addHttpPusher(pushkey: String,
-                      appId: String,
-                      profileTag: String,
-                      lang: String,
-                      appDisplayName: String,
-                      deviceDisplayName: String,
-                      url: String,
-                      append: Boolean,
-                      withEventIdOnly: Boolean): UUID
+    fun enqueueAddHttpPusher(httpPusher: HttpPusher): UUID
 
     /**
      * Add a new Email pusher.
@@ -75,16 +56,14 @@ interface PushersService {
      *                          to any others with different user IDs. Otherwise, the homeserver must remove any other pushers
      *                          with the same App ID and pushkey for different users. Typically We always want to append for
      *                          email pushers since we don't want to stop other accounts notifying to the same email address.
-     * @return                  A work request uuid. Can be used to listen to the status
-     *                          (LiveData<WorkInfo> status = workManager.getWorkInfoByIdLiveData(<UUID>))
      * @throws [InvalidParameterException] if a parameter is not correct
      */
-    fun addEmailPusher(email: String,
-                       lang: String,
-                       emailBranding: String,
-                       appDisplayName: String,
-                       deviceDisplayName: String,
-                       append: Boolean = true): UUID
+    suspend fun addEmailPusher(email: String,
+                               lang: String,
+                               emailBranding: String,
+                               appDisplayName: String,
+                               deviceDisplayName: String,
+                               append: Boolean = true)
 
     /**
      * Directly ask the push gateway to send a push to this device
@@ -128,4 +107,61 @@ interface PushersService {
      * Get the current pushers
      */
     fun getPushers(): List<Pusher>
+
+    data class HttpPusher(
+
+            /**
+             * This is a unique identifier for this pusher. The value you should use for
+             * this is the routing or destination address information for the notification,
+             * for example, the APNS token for APNS or the Registration ID for GCM. If your
+             * notification client has no such concept, use any unique identifier. Max length, 512 chars.
+             */
+            val pushkey: String,
+
+            /**
+             * The application id
+             * This is a reverse-DNS style identifier for the application. It is recommended
+             * that this end with the platform, such that different platform versions get
+             * different app identifiers. Max length, 64 chars.
+             */
+            val appId: String,
+
+            /**
+             * This string determines which set of device specific rules this pusher executes.
+             */
+            val profileTag: String,
+
+            /**
+             * The preferred language for receiving notifications (e.g. "en" or "en-US").
+             */
+            val lang: String,
+
+            /**
+             * A human readable string that will allow the user to identify what application owns this pusher.
+             */
+            val appDisplayName: String,
+
+            /**
+             * A human readable string that will allow the user to identify what device owns this pusher.
+             */
+            val deviceDisplayName: String,
+
+            /**
+             * The URL to use to send notifications to. MUST be an HTTPS URL with a path of /_matrix/push/v1/notify.
+             */
+            val url: String,
+
+            /**
+             * If true, the homeserver should add another pusher with the given pushkey and App ID in addition
+             * to any others with different user IDs. Otherwise, the homeserver must remove any other pushers
+             * with the same App ID and pushkey for different users.
+             */
+            val append: Boolean,
+
+            /**
+             * true to limit the push content to only id and not message content
+             * Ref: https://matrix.org/docs/spec/push_gateway/r0.1.1#homeserver-behaviour
+             */
+            val withEventIdOnly: Boolean
+    )
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt
index ebe96b638298096ae360f11f0c46e6a512d1ced2..6c0e730499468b4139fbccd9e69d610efb690887 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/Room.kt
@@ -17,6 +17,7 @@
 package org.matrix.android.sdk.api.session.room
 
 import androidx.lifecycle.LiveData
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataService
 import org.matrix.android.sdk.api.session.room.alias.AliasService
 import org.matrix.android.sdk.api.session.room.call.RoomCallService
@@ -61,6 +62,8 @@ interface Room :
         RoomAccountDataService,
         RoomVersionService {
 
+    val coroutineDispatchers: MatrixCoroutineDispatchers
+
     /**
      * The roomId of this room
      */
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomMemberSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomMemberSummary.kt
index fba3a1dd719f6e213abfa8752c733fa946c99856..39177a4296b8983dcb5b8e66e5e1c7531e05b9ac 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomMemberSummary.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomMemberSummary.kt
@@ -16,12 +16,15 @@
 
 package org.matrix.android.sdk.api.session.room.model
 
+import org.matrix.android.sdk.api.session.presence.model.UserPresence
+
 /**
  * Class representing a simplified version of EventType.STATE_ROOM_MEMBER state event content
  */
 data class RoomMemberSummary constructor(
         val membership: Membership,
         val userId: String,
+        val userPresence: UserPresence? = null,
         val displayName: String? = null,
         val avatarUrl: String? = null
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt
index cae4775e7173580d369b3564ad5bc60e7cad7427..10cad026bcbe1100dfbb4ecfe31d6b90d0cd6e60 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt
@@ -17,6 +17,7 @@
 package org.matrix.android.sdk.api.session.room.model
 
 import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
+import org.matrix.android.sdk.api.session.presence.model.UserPresence
 import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
 import org.matrix.android.sdk.api.session.room.send.UserDraft
 import org.matrix.android.sdk.api.session.room.sender.SenderInfo
@@ -38,6 +39,7 @@ data class RoomSummary(
         val joinRules: RoomJoinRules? = null,
         val isDirect: Boolean = false,
         val directUserId: String? = null,
+        val directUserPresence: UserPresence? = null,
         val joinedMembersCount: Int? = 0,
         val invitedMembersCount: Int? = 0,
         val latestPreviewableEvent: TimelineEvent? = null,
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 7115ff5db2f910c544cd1fa5b8c0fb05c2d82568..7b96148e2e55c4a32ca0da22086157231b15cbd3 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
@@ -29,6 +29,7 @@ import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.withContext
 import org.matrix.android.sdk.api.MatrixCallback
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.NoOpMatrixCallback
 import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
 import org.matrix.android.sdk.api.crypto.MXCryptoConfig
@@ -93,7 +94,6 @@ import org.matrix.android.sdk.internal.task.TaskThread
 import org.matrix.android.sdk.internal.task.configureWith
 import org.matrix.android.sdk.internal.task.launchToCallback
 import org.matrix.android.sdk.internal.util.JsonCanonicalizer
-import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
 import org.matrix.olm.OlmManager
 import timber.log.Timber
 import java.util.concurrent.atomic.AtomicBoolean
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 8a91376b60782cdcfabec5ef765c7a450c91964c..494e6d7cc7a3138d43fe4709161ccce2b22224dd 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
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.crypto
 
 import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.launch
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.MatrixPatterns
 import org.matrix.android.sdk.api.auth.data.Credentials
 import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel
@@ -29,7 +30,6 @@ import org.matrix.android.sdk.internal.crypto.tasks.DownloadKeysForUsersTask
 import org.matrix.android.sdk.internal.session.SessionScope
 import org.matrix.android.sdk.internal.session.sync.SyncTokenStore
 import org.matrix.android.sdk.internal.task.TaskExecutor
-import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.internal.util.logLimit
 import timber.log.Timber
 import javax.inject.Inject
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt
index fe17dd08e47b283a483c81a46ce1aa74886869c5..57381eacfb47954160d19e3e4b8e05618fb85f72 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt
@@ -20,6 +20,7 @@ import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import org.matrix.android.sdk.api.MatrixCallback
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
@@ -27,16 +28,17 @@ import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
 import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
 import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
+import org.matrix.android.sdk.internal.crypto.model.MXOlmSessionResult
 import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
 import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
 import org.matrix.android.sdk.internal.extensions.foldToCallback
 import org.matrix.android.sdk.internal.session.SessionScope
-import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
 import timber.log.Timber
 import javax.inject.Inject
-import kotlin.jvm.Throws
+
+private const val SEND_TO_DEVICE_RETRY_COUNT = 3
 
 @SessionScope
 internal class EventDecryptor @Inject constructor(
@@ -146,29 +148,36 @@ internal class EventDecryptor @Inject constructor(
 
         // offload this from crypto thread (?)
         cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
-            val ensured = ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true)
+            runCatching { ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true) }.fold(
+                    onSuccess = { sendDummyToDevice(ensured = it, deviceInfo, senderId) },
+                    onFailure = {
+                        Timber.e("## CRYPTO | markOlmSessionForUnwedging() : failed to ensure device info ${senderId}${deviceInfo.deviceId}")
+                    }
+            )
+        }
+    }
 
-            Timber.i("## CRYPTO | markOlmSessionForUnwedging() : ensureOlmSessionsForDevicesAction isEmpty:${ensured.isEmpty}")
+    private suspend fun sendDummyToDevice(ensured: MXUsersDevicesMap<MXOlmSessionResult>, deviceInfo: CryptoDeviceInfo, senderId: String) {
+        Timber.i("## CRYPTO | markOlmSessionForUnwedging() : ensureOlmSessionsForDevicesAction isEmpty:${ensured.isEmpty}")
 
-            // Now send a blank message on that session so the other side knows about it.
-            // (The keyshare request is sent in the clear so that won't do)
-            // We send this first such that, as long as the toDevice messages arrive in the
-            // same order we sent them, the other end will get this first, set up the new session,
-            // then get the keyshare request and send the key over this new session (because it
-            // is the session it has most recently received a message on).
-            val payloadJson = mapOf<String, Any>("type" to EventType.DUMMY)
+        // Now send a blank message on that session so the other side knows about it.
+        // (The keyshare request is sent in the clear so that won't do)
+        // We send this first such that, as long as the toDevice messages arrive in the
+        // same order we sent them, the other end will get this first, set up the new session,
+        // then get the keyshare request and send the key over this new session (because it
+        // is the session it has most recently received a message on).
+        val payloadJson = mapOf<String, Any>("type" to EventType.DUMMY)
 
-            val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
-            val sendToDeviceMap = MXUsersDevicesMap<Any>()
-            sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload)
-            Timber.i("## CRYPTO | markOlmSessionForUnwedging() : sending dummy to $senderId:${deviceInfo.deviceId}")
-            withContext(coroutineDispatchers.io) {
-                val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
-                try {
-                    sendToDeviceTask.execute(sendToDeviceParams)
-                } catch (failure: Throwable) {
-                    Timber.e(failure, "## CRYPTO | markOlmSessionForUnwedging() : failed to send dummy to $senderId:${deviceInfo.deviceId}")
-                }
+        val encodedPayload = messageEncrypter.encryptMessage(payloadJson, listOf(deviceInfo))
+        val sendToDeviceMap = MXUsersDevicesMap<Any>()
+        sendToDeviceMap.setObject(senderId, deviceInfo.deviceId, encodedPayload)
+        Timber.i("## CRYPTO | markOlmSessionForUnwedging() : sending dummy to $senderId:${deviceInfo.deviceId}")
+        withContext(coroutineDispatchers.io) {
+            val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
+            try {
+                sendToDeviceTask.executeRetry(sendToDeviceParams, remainingRetry = SEND_TO_DEVICE_RETRY_COUNT)
+            } catch (failure: Throwable) {
+                Timber.e(failure, "## CRYPTO | markOlmSessionForUnwedging() : failed to send dummy to $senderId:${deviceInfo.deviceId}")
             }
         }
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt
index 3825a5dab2e4d72284d58a50c8dd969a682584a6..e7a46750b0a84afad9c7b07a3a26a37d5a349a22 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt
@@ -19,10 +19,10 @@ package org.matrix.android.sdk.internal.crypto
 import android.util.LruCache
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper2
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
-import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
 import timber.log.Timber
 import java.util.Timer
 import java.util.TimerTask
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt
index e8640d5011d769e4edc76cfb62ffc25b9b2d83bc..220f25ec80096fb1b2b0c0214666639a05402a4e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/IncomingGossipingRequestManager.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.crypto
 
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.auth.data.Credentials
 import org.matrix.android.sdk.api.crypto.MXCryptoConfig
 import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
@@ -38,7 +39,6 @@ import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId
 import org.matrix.android.sdk.internal.di.SessionId
 import org.matrix.android.sdk.internal.session.SessionScope
-import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
 import timber.log.Timber
 import java.util.concurrent.Executors
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt
index 6fc71036688495f5365a379e6ab6b20d315e7660..fd60e4326041d6248d560ce0b38c49c1e151ba6a 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OutgoingGossipingRequestManager.kt
@@ -19,13 +19,13 @@ package org.matrix.android.sdk.internal.crypto
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import org.matrix.android.sdk.internal.crypto.tasks.createUniqueTxnId
 import org.matrix.android.sdk.internal.crypto.util.RequestIdHelper
 import org.matrix.android.sdk.internal.di.SessionId
 import org.matrix.android.sdk.internal.session.SessionScope
-import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
 import timber.log.Timber
 import javax.inject.Inject
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt
index 52876b0fff87a577976a08f5f9d46ce4c49c688c..3979ff1fb4879a36c52644093ca4a9b3c0a124f2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/actions/EnsureOlmSessionsForDevicesAction.kt
@@ -25,6 +25,8 @@ import org.matrix.android.sdk.internal.crypto.tasks.ClaimOneTimeKeysForUsersDevi
 import timber.log.Timber
 import javax.inject.Inject
 
+private const val ONE_TIME_KEYS_RETRY_COUNT = 3
+
 internal class EnsureOlmSessionsForDevicesAction @Inject constructor(
         private val olmDevice: MXOlmDevice,
         private val oneTimeKeysForUsersDeviceTask: ClaimOneTimeKeysForUsersDeviceTask) {
@@ -72,7 +74,7 @@ internal class EnsureOlmSessionsForDevicesAction @Inject constructor(
         Timber.i("## CRYPTO | claimOneTimeKeysForUsersDevices() : $usersDevicesToClaim")
 
         val claimParams = ClaimOneTimeKeysForUsersDeviceTask.Params(usersDevicesToClaim)
-        val oneTimeKeys = oneTimeKeysForUsersDeviceTask.execute(claimParams)
+        val oneTimeKeys = oneTimeKeysForUsersDeviceTask.executeRetry(claimParams, remainingRetry = ONE_TIME_KEYS_RETRY_COUNT)
         Timber.v("## CRYPTO | claimOneTimeKeysForUsersDevices() : keysClaimResponse.oneTimeKeys: $oneTimeKeys")
         for ((userId, deviceInfos) in devicesByUser) {
             for (deviceInfo in deviceInfos) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt
index d2f6bd0382b490a37881fa1322821e6632f6e62b..d7411ad0be7ebfcefe0b1504a724e376d3cb66d6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm
 
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
@@ -41,7 +42,6 @@ import org.matrix.android.sdk.internal.crypto.model.rest.ForwardedRoomKeyContent
 import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
-import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
 import timber.log.Timber
 
 internal class MXMegolmDecryption(private val userId: String,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt
index 91640523fc0feb08294db92693a13d2878722cbd..29f9d193f84700a34983c208a6a28f3bd7859bb9 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryptionFactory.kt
@@ -17,6 +17,7 @@
 package org.matrix.android.sdk.internal.crypto.algorithms.megolm
 
 import kotlinx.coroutines.CoroutineScope
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.internal.crypto.DeviceListManager
 import org.matrix.android.sdk.internal.crypto.MXOlmDevice
 import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestManager
@@ -25,7 +26,6 @@ import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
 import org.matrix.android.sdk.internal.di.UserId
-import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
 import javax.inject.Inject
 
 internal class MXMegolmDecryptionFactory @Inject constructor(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt
index 63fe678229fae6ab1b7a432ee85ce4f8168b4122..031bb4e194f489584425c962f3abb8793109e526 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryption.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.crypto.algorithms.megolm
 
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
 import org.matrix.android.sdk.api.session.events.model.Content
 import org.matrix.android.sdk.api.session.events.model.Event
@@ -39,7 +40,6 @@ import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepo
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
 import org.matrix.android.sdk.internal.util.JsonCanonicalizer
-import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.internal.util.convertToUTF8
 import timber.log.Timber
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt
index 9f6312ea97804478bdd3d8cdbaf1e80ab4dead1e..238d7eed880a119686d2d405c958ef8bb5d0b459 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmEncryptionFactory.kt
@@ -17,6 +17,7 @@
 package org.matrix.android.sdk.internal.crypto.algorithms.megolm
 
 import kotlinx.coroutines.CoroutineScope
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.internal.crypto.DeviceListManager
 import org.matrix.android.sdk.internal.crypto.MXOlmDevice
 import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
@@ -27,7 +28,6 @@ import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
 import org.matrix.android.sdk.internal.di.DeviceId
 import org.matrix.android.sdk.internal.di.UserId
-import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
 import javax.inject.Inject
 
 internal class MXMegolmEncryptionFactory @Inject constructor(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryptionFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryptionFactory.kt
index 68a95e395bb0ee3306ecb7f7cbdfcbc5fa643712..50ce2d2bf3d02a65f509b3e05b7b0998950000a8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryptionFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmEncryptionFactory.kt
@@ -16,12 +16,12 @@
 
 package org.matrix.android.sdk.internal.crypto.algorithms.olm
 
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.internal.crypto.DeviceListManager
 import org.matrix.android.sdk.internal.crypto.MXOlmDevice
 import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForUsersAction
 import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
-import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
 import javax.inject.Inject
 
 internal class MXOlmEncryptionFactory @Inject constructor(private val olmDevice: MXOlmDevice,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ComputeTrustTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ComputeTrustTask.kt
index 113255bb7ea8cf26be480d2910afc0fddce321d4..b470ab34bb703161487ef19ec9cdb36463dd2fa9 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ComputeTrustTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ComputeTrustTask.kt
@@ -16,13 +16,13 @@
 package org.matrix.android.sdk.internal.crypto.crosssigning
 
 import kotlinx.coroutines.withContext
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
 import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
 import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
 import org.matrix.android.sdk.internal.di.UserId
 import org.matrix.android.sdk.internal.task.Task
-import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
 import javax.inject.Inject
 
 internal interface ComputeTrustTask : Task<ComputeTrustTask.Params, RoomEncryptionTrustLevel> {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt
index 8a851b12674bfc206e175ca759746df2d7ddce52..83de06a66872abb103e9654744f4e5cde678fe55 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt
@@ -22,6 +22,7 @@ import androidx.work.ExistingWorkPolicy
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
 import org.matrix.android.sdk.api.MatrixCallback
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
 import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
@@ -42,7 +43,6 @@ import org.matrix.android.sdk.internal.task.TaskExecutor
 import org.matrix.android.sdk.internal.task.TaskThread
 import org.matrix.android.sdk.internal.task.configureWith
 import org.matrix.android.sdk.internal.util.JsonCanonicalizer
-import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.internal.util.logLimit
 import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
 import org.matrix.olm.OlmPkSigning
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt
index c6e2c1217f8712a17f48bb691c35fcf5d262bbc3..b20168eaa3cd25ffe869d8ce252ae3595ee8f142 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt
@@ -26,6 +26,7 @@ import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import org.matrix.android.sdk.api.MatrixCallback
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.auth.data.Credentials
 import org.matrix.android.sdk.api.failure.Failure
 import org.matrix.android.sdk.api.failure.MatrixError
@@ -83,7 +84,6 @@ import org.matrix.android.sdk.internal.task.TaskExecutor
 import org.matrix.android.sdk.internal.task.TaskThread
 import org.matrix.android.sdk.internal.task.configureWith
 import org.matrix.android.sdk.internal.util.JsonCanonicalizer
-import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.internal.util.awaitCallback
 import org.matrix.olm.OlmException
 import org.matrix.olm.OlmPkDecryption
@@ -410,7 +410,7 @@ internal class DefaultKeysBackupService @Inject constructor(
         val keysBackupVersionTrust = KeysBackupVersionTrust()
         val authData = keysBackupVersion.getAuthDataAsMegolmBackupAuthData()
 
-        if (authData == null || authData.publicKey.isEmpty() || authData.signatures.isEmpty()) {
+        if (authData == null || authData.publicKey.isEmpty() || authData.signatures.isNullOrEmpty()) {
             Timber.v("getKeysBackupTrust: Key backup is absent or missing required data")
             return keysBackupVersionTrust
         }
@@ -478,7 +478,7 @@ internal class DefaultKeysBackupService @Inject constructor(
             cryptoCoroutineScope.launch(coroutineDispatchers.main) {
                 val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) {
                     // Get current signatures, or create an empty set
-                    val myUserSignatures = authData.signatures[userId].orEmpty().toMutableMap()
+                    val myUserSignatures = authData.signatures?.get(userId).orEmpty().toMutableMap()
 
                     if (trust) {
                         // Add current device signature
@@ -497,7 +497,7 @@ internal class DefaultKeysBackupService @Inject constructor(
                     // Create an updated version of KeysVersionResult
                     val newMegolmBackupAuthData = authData.copy()
 
-                    val newSignatures = newMegolmBackupAuthData.signatures.toMutableMap()
+                    val newSignatures = newMegolmBackupAuthData.signatures.orEmpty().toMutableMap()
                     newSignatures[userId] = myUserSignatures
 
                     val newMegolmBackupAuthDataWithNewSignature = newMegolmBackupAuthData.copy(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupAuthData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupAuthData.kt
index 54b92546e9bdf6db74d93851c5d70738fd3e1e4e..17c895762c400bf6549dbdb1a31abebc42520391 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupAuthData.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/model/MegolmBackupAuthData.kt
@@ -51,7 +51,7 @@ data class MegolmBackupAuthData(
          * userId -> (deviceSignKeyId -> signature)
          */
         @Json(name = "signatures")
-        val signatures: Map<String, Map<String, String>>
+        val signatures: Map<String, Map<String, String>>? = null
 ) {
 
     fun toJsonDict(): JsonDict {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt
index ad3bc012dfb16381c0b738cb725939de96a0fa41..e6d8b5e84f246d5ee171184875fa3cf54b403214 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.crypto.secrets
 
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.withContext
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.listeners.ProgressListener
 import org.matrix.android.sdk.api.session.accountdata.SessionAccountDataService
@@ -44,7 +45,6 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.util.computeRecoveryKey
 import org.matrix.android.sdk.internal.crypto.tools.HkdfSha256
 import org.matrix.android.sdk.internal.crypto.tools.withOlmDecryption
 import org.matrix.android.sdk.internal.di.UserId
-import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
 import org.matrix.olm.OlmPkMessage
 import java.security.SecureRandom
 import javax.crypto.Cipher
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 238d06738ccabaab272c75f54cac3411d78ca2e5..9b75f88f917316491252f313bf5729455a3d97d7 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
@@ -379,7 +379,8 @@ internal interface IMXCryptoStore {
 
     fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map<String, List<String>>): OutgoingSecretRequest?
 
-    fun saveGossipingEvent(event: Event)
+    fun saveGossipingEvent(event: Event) = saveGossipingEvents(listOf(event))
+
     fun saveGossipingEvents(events: List<Event>)
 
     fun updateGossipingRequestState(request: IncomingShareRequestCommon, state: GossipingRequestState) {
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 3c8f74d419e78b6f9213d4a8e9feec9b35e5c876..40678a6ce6415782003706c9a5b9abb6a116dbe0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt
@@ -25,6 +25,7 @@ import io.realm.Realm
 import io.realm.RealmConfiguration
 import io.realm.Sort
 import io.realm.kotlin.where
+import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.room.send.SendState
@@ -100,6 +101,8 @@ import org.matrix.olm.OlmAccount
 import org.matrix.olm.OlmException
 import org.matrix.olm.OlmOutboundGroupSession
 import timber.log.Timber
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
 import javax.inject.Inject
 import kotlin.collections.set
 
@@ -137,8 +140,11 @@ internal class RealmCryptoStore @Inject constructor(
         newSessionListeners.remove(listener)
     }
 
+    private val monarchyWriteAsyncExecutor = Executors.newSingleThreadExecutor()
+
     private val monarchy = Monarchy.Builder()
             .setRealmConfiguration(realmConfiguration)
+            .setWriteAsyncExecutor(monarchyWriteAsyncExecutor)
             .build()
 
     init {
@@ -199,6 +205,14 @@ internal class RealmCryptoStore @Inject constructor(
     }
 
     override fun close() {
+        // Ensure no async request will be run later
+        val tasks = monarchyWriteAsyncExecutor.shutdownNow()
+        Timber.w("Closing RealmCryptoStore, ${tasks.size} async task(s) cancelled")
+        tryOrNull("Interrupted") {
+            // Wait 1 minute max
+            monarchyWriteAsyncExecutor.awaitTermination(1, TimeUnit.MINUTES)
+        }
+
         olmSessionsToRelease.forEach {
             it.value.olmSession.releaseSession()
         }
@@ -1163,8 +1177,8 @@ internal class RealmCryptoStore @Inject constructor(
     }
 
     override fun saveGossipingEvents(events: List<Event>) {
-        val now = System.currentTimeMillis()
         monarchy.writeAsync { realm ->
+            val now = System.currentTimeMillis()
             events.forEach { event ->
                 val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now
                 val entity = GossipingEventEntity(
@@ -1182,23 +1196,6 @@ internal class RealmCryptoStore @Inject constructor(
         }
     }
 
-    override fun saveGossipingEvent(event: Event) {
-        monarchy.writeAsync { realm ->
-            val now = System.currentTimeMillis()
-            val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now
-            val entity = GossipingEventEntity(
-                    type = event.type,
-                    sender = event.senderId,
-                    ageLocalTs = ageLocalTs,
-                    content = ContentMapper.map(event.content)
-            ).apply {
-                sendState = SendState.SYNCED
-                decryptionResultJson = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult)
-                decryptionErrorCode = event.mCryptoError?.name
-            }
-            realm.insertOrUpdate(entity)
-        }
-    }
 //    override fun getOutgoingRoomKeyRequestByState(states: Set<ShareRequestState>): OutgoingRoomKeyRequest? {
 //        val statesIndex = states.map { it.ordinal }.toTypedArray()
 //        return doRealmQueryAndCopy(realmConfiguration) { realm ->
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt
index 768109979d3c37b9c1c5f2a429695ab1b819822f..388ecb965952802695fff2d543fa7b8347599a9d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/DefaultVerificationService.kt
@@ -21,6 +21,7 @@ import android.os.Looper
 import dagger.Lazy
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
 import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
 import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
@@ -83,7 +84,6 @@ import org.matrix.android.sdk.internal.di.DeviceId
 import org.matrix.android.sdk.internal.di.UserId
 import org.matrix.android.sdk.internal.session.SessionScope
 import org.matrix.android.sdk.internal.task.TaskExecutor
-import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
 import timber.log.Timber
 import java.util.UUID
 import javax.inject.Inject
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
index aa96ca5e1a99078e2ba74c90430a77673d575b0a..05137f810529f83b80799266739c3b66370ff376 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
@@ -35,19 +35,21 @@ import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityField
 import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntityFields
 import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntityFields
 import org.matrix.android.sdk.internal.database.model.RoomEntityFields
+import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
 import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType
 import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
 import org.matrix.android.sdk.internal.database.model.RoomTagEntityFields
 import org.matrix.android.sdk.internal.database.model.SpaceChildSummaryEntityFields
 import org.matrix.android.sdk.internal.database.model.SpaceParentSummaryEntityFields
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
+import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntityFields
 import org.matrix.android.sdk.internal.di.MoshiProvider
 import org.matrix.android.sdk.internal.query.process
 import timber.log.Timber
 
 internal object RealmSessionStoreMigration : RealmMigration {
 
-    const val SESSION_STORE_SCHEMA_VERSION = 17L
+    const val SESSION_STORE_SCHEMA_VERSION = 18L
 
     override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
         Timber.v("Migrating Realm Session from $oldVersion to $newVersion")
@@ -69,6 +71,7 @@ internal object RealmSessionStoreMigration : RealmMigration {
         if (oldVersion <= 14) migrateTo15(realm)
         if (oldVersion <= 15) migrateTo16(realm)
         if (oldVersion <= 16) migrateTo17(realm)
+        if (oldVersion <= 17) migrateTo18(realm)
     }
 
     private fun migrateTo1(realm: DynamicRealm) {
@@ -338,4 +341,27 @@ internal object RealmSessionStoreMigration : RealmMigration {
         realm.schema.get("EventInsertEntity")
                 ?.addField(EventInsertEntityFields.CAN_BE_PROCESSED, Boolean::class.java)
     }
+
+    private fun migrateTo18(realm: DynamicRealm) {
+        Timber.d("Step 17 -> 18")
+        realm.schema.create("UserPresenceEntity")
+                ?.addField(UserPresenceEntityFields.USER_ID, String::class.java)
+                ?.addPrimaryKey(UserPresenceEntityFields.USER_ID)
+                ?.setRequired(UserPresenceEntityFields.USER_ID, true)
+                ?.addField(UserPresenceEntityFields.PRESENCE_STR, String::class.java)
+                ?.addField(UserPresenceEntityFields.LAST_ACTIVE_AGO, Long::class.java)
+                ?.setNullable(UserPresenceEntityFields.LAST_ACTIVE_AGO, true)
+                ?.addField(UserPresenceEntityFields.STATUS_MESSAGE, String::class.java)
+                ?.addField(UserPresenceEntityFields.IS_CURRENTLY_ACTIVE, Boolean::class.java)
+                ?.setNullable(UserPresenceEntityFields.IS_CURRENTLY_ACTIVE, true)
+                ?.addField(UserPresenceEntityFields.AVATAR_URL, String::class.java)
+                ?.addField(UserPresenceEntityFields.DISPLAY_NAME, String::class.java)
+
+        val userPresenceEntity = realm.schema.get("UserPresenceEntity") ?: return
+        realm.schema.get("RoomSummaryEntity")
+                ?.addRealmObjectField(RoomSummaryEntityFields.DIRECT_USER_PRESENCE.`$`, userPresenceEntity)
+
+        realm.schema.get("RoomMemberSummaryEntity")
+                ?.addRealmObjectField(RoomMemberSummaryEntityFields.USER_PRESENCE_ENTITY.`$`, userPresenceEntity)
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomMemberSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomMemberSummaryMapper.kt
index 2365a39567ab6a544b2e57a0407ccd3cae461d59..efd9b68011fe95a47f91a2a203ac6d5ce5dada64 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomMemberSummaryMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomMemberSummaryMapper.kt
@@ -18,12 +18,14 @@ package org.matrix.android.sdk.internal.database.mapper
 
 import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
 import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
+import org.matrix.android.sdk.internal.database.model.presence.toUserPresence
 
 internal object RoomMemberSummaryMapper {
 
     fun map(roomMemberSummaryEntity: RoomMemberSummaryEntity): RoomMemberSummary {
         return RoomMemberSummary(
                 userId = roomMemberSummaryEntity.userId,
+                userPresence = roomMemberSummaryEntity.userPresenceEntity?.toUserPresence(),
                 avatarUrl = roomMemberSummaryEntity.avatarUrl,
                 displayName = roomMemberSummaryEntity.displayName,
                 membership = roomMemberSummaryEntity.membership
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt
index 0cf431c340c0809a64ac23576084597b2fdffa27..5900ef6b76dcf840d9f3f7974ac7525df07aa6a9 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt
@@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
 import org.matrix.android.sdk.api.session.room.model.SpaceParentInfo
 import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
 import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
+import org.matrix.android.sdk.internal.database.model.presence.toUserPresence
 import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker
 import javax.inject.Inject
 
@@ -48,6 +49,7 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
                 joinRules = roomSummaryEntity.joinRules,
                 isDirect = roomSummaryEntity.isDirect,
                 directUserId = roomSummaryEntity.directUserId,
+                directUserPresence = roomSummaryEntity.directUserPresence?.toUserPresence(),
                 latestPreviewableEvent = latestEvent,
                 joinedMembersCount = roomSummaryEntity.joinedMembersCount,
                 invitedMembersCount = roomSummaryEntity.invitedMembersCount,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomMemberSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomMemberSummaryEntity.kt
index 75771ff12c7fded14eb2699b81f8714b823b9025..a8a76d16811f661fcbf4f2ce2bbc9b8a9a43aa33 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomMemberSummaryEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomMemberSummaryEntity.kt
@@ -21,6 +21,7 @@ import io.realm.annotations.Index
 import io.realm.annotations.PrimaryKey
 import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.api.util.MatrixItem
+import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity
 
 internal open class RoomMemberSummaryEntity(@PrimaryKey var primaryKey: String = "",
                                             @Index var userId: String = "",
@@ -40,6 +41,11 @@ internal open class RoomMemberSummaryEntity(@PrimaryKey var primaryKey: String =
             membershipStr = value.name
         }
 
+    var userPresenceEntity: UserPresenceEntity? = null
+        set(value) {
+            if (value != field) field = value
+        }
+
     fun toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)
 
     companion object
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt
index 64dc08e8274d1e98bd201a363a8a3eda6d14f38d..88b8886936a2802ff9ff8527f47443beb3bad19d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt
@@ -27,6 +27,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
 import org.matrix.android.sdk.api.session.room.model.RoomSummary
 import org.matrix.android.sdk.api.session.room.model.VersioningState
 import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
+import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity
 
 internal open class RoomSummaryEntity(
         @PrimaryKey var roomId: String = "",
@@ -204,6 +205,11 @@ internal open class RoomSummaryEntity(
             if (value != field) field = value
         }
 
+    var directUserPresence: UserPresenceEntity? = null
+        set(value) {
+            if (value != field) field = value
+        }
+
     var hasFailedSending: Boolean = false
         set(value) {
             if (value != field) field = value
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt
index 19472e21d96838372b3fa283d3dee5ca23547b9d..c090777972194dc6cef0f40f286bb796f6f7d1a4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt
@@ -17,6 +17,7 @@
 package org.matrix.android.sdk.internal.database.model
 
 import io.realm.annotations.RealmModule
+import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity
 
 /**
  * Realm module for Session
@@ -64,6 +65,7 @@ import io.realm.annotations.RealmModule
             WellknownIntegrationManagerConfigEntity::class,
             RoomAccountDataEntity::class,
             SpaceChildSummaryEntity::class,
-            SpaceParentSummaryEntity::class
+            SpaceParentSummaryEntity::class,
+            UserPresenceEntity::class
         ])
 internal class SessionRealmModule
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/presence/UserPresenceEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/presence/UserPresenceEntity.kt
new file mode 100644
index 0000000000000000000000000000000000000000..5713337ec574fc9893ec2e91ee44ecca4259e9e8
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/presence/UserPresenceEntity.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.database.model.presence
+
+import io.realm.RealmObject
+import io.realm.annotations.PrimaryKey
+import org.matrix.android.sdk.api.session.presence.model.PresenceEnum
+import org.matrix.android.sdk.api.session.presence.model.UserPresence
+
+internal open class UserPresenceEntity(@PrimaryKey var userId: String = "",
+                                       var lastActiveAgo: Long? = null,
+                                       var statusMessage: String? = null,
+                                       var isCurrentlyActive: Boolean? = null,
+                                       var avatarUrl: String? = null,
+                                       var displayName: String? = null
+) : RealmObject() {
+
+    var presence: PresenceEnum
+        get() {
+            return PresenceEnum.valueOf(presenceStr)
+        }
+        set(value) {
+            presenceStr = value.name
+        }
+
+    private var presenceStr: String = PresenceEnum.UNAVAILABLE.name
+
+    companion object
+}
+
+internal fun UserPresenceEntity.toUserPresence() =
+        UserPresence(
+                lastActiveAgo,
+                statusMessage,
+                isCurrentlyActive,
+                presence
+        )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomMemberEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomMemberEntityQueries.kt
index a19a9cf725c33947eee634033a2d5232b5360bce..1ea06b9dfbc6a47c502d8edc913858cb28608dd0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomMemberEntityQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomMemberEntityQueries.kt
@@ -21,6 +21,7 @@ import io.realm.RealmQuery
 import io.realm.kotlin.where
 import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
 import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
+import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity
 
 internal fun RoomMemberSummaryEntity.Companion.where(realm: Realm, roomId: String, userId: String? = null): RealmQuery<RoomMemberSummaryEntity> {
     val query = realm
@@ -32,3 +33,13 @@ internal fun RoomMemberSummaryEntity.Companion.where(realm: Realm, roomId: Strin
     }
     return query
 }
+
+internal fun RoomMemberSummaryEntity.Companion.updateUserPresence(realm: Realm, userId: String, userPresenceEntity: UserPresenceEntity) {
+    realm.where<RoomMemberSummaryEntity>()
+            .equalTo(RoomMemberSummaryEntityFields.USER_ID, userId)
+            .isNull(RoomMemberSummaryEntityFields.USER_PRESENCE_ENTITY.`$`)
+            .findAll()
+            .map {
+                it.userPresenceEntity = userPresenceEntity
+            }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomSummaryEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomSummaryEntityQueries.kt
index 5294f849afb077d91ad45dbbabb47c5835267665..d1b05a4932c2dc70e7028dec264a82fe292b230f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomSummaryEntityQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/RoomSummaryEntityQueries.kt
@@ -23,6 +23,7 @@ import io.realm.kotlin.createObject
 import io.realm.kotlin.where
 import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
 import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
+import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity
 
 internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = null): RealmQuery<RoomSummaryEntity> {
     val query = realm.where<RoomSummaryEntity>()
@@ -67,3 +68,11 @@ internal fun RoomSummaryEntity.Companion.isDirect(realm: Realm, roomId: String):
             .findAll()
             .isNotEmpty()
 }
+
+internal fun RoomSummaryEntity.Companion.updateDirectUserPresence(realm: Realm, directUserId: String, userPresenceEntity: UserPresenceEntity) {
+    RoomSummaryEntity.where(realm)
+            .equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
+            .equalTo(RoomSummaryEntityFields.DIRECT_USER_ID, directUserId)
+            .findFirst()
+            ?.directUserPresence = userPresenceEntity
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserPresenceEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserPresenceEntityQueries.kt
new file mode 100644
index 0000000000000000000000000000000000000000..22790b6f4fe9aadc6b97c46bda938d4c95474d27
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserPresenceEntityQueries.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.database.query
+
+import io.realm.Realm
+import io.realm.RealmQuery
+import io.realm.kotlin.where
+import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity
+import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntityFields
+
+internal fun UserPresenceEntity.Companion.where(realm: Realm, userId: String): RealmQuery<UserPresenceEntity> {
+    return realm
+            .where<UserPresenceEntity>()
+            .equalTo(UserPresenceEntityFields.USER_ID, userId)
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt
index 5bc519e96049aede5d142aff61e37cb462069f28..81a067f2c010a2898551a4ad9272dafb04080f0d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt
@@ -24,6 +24,7 @@ import dagger.Component
 import okhttp3.OkHttpClient
 import org.matrix.android.sdk.api.Matrix
 import org.matrix.android.sdk.api.MatrixConfiguration
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.auth.AuthenticationService
 import org.matrix.android.sdk.api.auth.HomeServerHistoryService
 import org.matrix.android.sdk.api.raw.RawService
@@ -35,7 +36,6 @@ import org.matrix.android.sdk.internal.session.MockHttpInterceptor
 import org.matrix.android.sdk.internal.session.TestInterceptor
 import org.matrix.android.sdk.internal.task.TaskExecutor
 import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver
-import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.internal.util.system.SystemModule
 import org.matrix.olm.OlmManager
 import java.io.File
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixModule.kt
index 4cd960f4264b724fb8fef7f4c6420b8241c64bef..9cab307c6153638cffcb76e84279b5346cc71a78 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixModule.kt
@@ -23,7 +23,7 @@ import dagger.Provides
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.android.asCoroutineDispatcher
 import kotlinx.coroutines.asCoroutineDispatcher
-import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.internal.util.createBackgroundHandler
 import org.matrix.olm.OlmManager
 import java.io.File
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/LiveData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/LiveData.kt
index 718e7869ddf5d3e3fd5d0897fedebd5038d54d60..fecbb874d0b798d20896c9203c735ae94eca1803 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/LiveData.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/extensions/LiveData.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.extensions
 
 import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.LiveData
+import androidx.lifecycle.MediatorLiveData
 import androidx.lifecycle.Observer
 
 inline fun <T> LiveData<T>.observeK(owner: LifecycleOwner, crossinline observer: (T?) -> Unit) {
@@ -27,3 +28,25 @@ inline fun <T> LiveData<T>.observeK(owner: LifecycleOwner, crossinline observer:
 inline fun <T> LiveData<T>.observeNotNull(owner: LifecycleOwner, crossinline observer: (T) -> Unit) {
     this.observe(owner, Observer { it?.run(observer) })
 }
+
+fun <T1, T2, R> combineLatest(source1: LiveData<T1>, source2: LiveData<T2>, mapper: (T1, T2) -> R): LiveData<R> {
+    val combined = MediatorLiveData<R>()
+    var source1Result: T1? = null
+    var source2Result: T2? = null
+
+    fun notify() {
+        if (source1Result != null && source2Result != null) {
+            combined.value = mapper(source1Result!!, source2Result!!)
+        }
+    }
+
+    combined.addSource(source1) {
+        source1Result = it
+        notify()
+    }
+    combined.addSource(source2) {
+        source2Result = it
+        notify()
+    }
+    return combined
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RequestExecutor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RequestExecutor.kt
new file mode 100644
index 0000000000000000000000000000000000000000..5cd2d880003ba2caf9d6f956b11d60c587e58e7e
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RequestExecutor.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.network
+import org.matrix.android.sdk.internal.network.executeRequest as internalExecuteRequest
+
+internal interface RequestExecutor {
+    suspend fun <DATA> executeRequest(globalErrorReceiver: GlobalErrorReceiver?,
+                                      canRetry: Boolean = false,
+                                      maxDelayBeforeRetry: Long = 32_000L,
+                                      maxRetriesCount: Int = 4,
+                                      requestBlock: suspend () -> DATA): DATA
+}
+
+internal object DefaultRequestExecutor : RequestExecutor {
+    override suspend fun <DATA> executeRequest(globalErrorReceiver: GlobalErrorReceiver?,
+                                               canRetry: Boolean,
+                                               maxDelayBeforeRetry: Long,
+                                               maxRetriesCount: Int,
+                                               requestBlock: suspend () -> DATA): DATA {
+        return internalExecuteRequest(globalErrorReceiver, canRetry, maxDelayBeforeRetry, maxRetriesCount, requestBlock)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RequestModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RequestModule.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7b2bb9fcba338d0a4922fa31b5eb9834bdcdd8c0
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RequestModule.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.network
+
+import dagger.Module
+import dagger.Provides
+
+@Module
+internal object RequestModule {
+
+    @Provides
+    fun providesRequestExecutor(): RequestExecutor {
+        return DefaultRequestExecutor
+    }
+}
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 414c018074dad12c116ad4c4594a47d1fd14dc5e..46c596787652d6a245744c44602dd7d78e46d056 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.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.failure.Failure
 import org.matrix.android.sdk.api.session.content.ContentUrlResolver
 import org.matrix.android.sdk.api.session.file.FileService
@@ -33,7 +34,6 @@ import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments
 import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory
 import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificateWithProgress
 import org.matrix.android.sdk.internal.session.download.DownloadProgressInterceptor.Companion.DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER
-import org.matrix.android.sdk.internal.util.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
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt
index d41bf8a70214bf496caef00874fe20f021af9224..6a6bce5ce28e2f5b14940fa1afef164ae0d49c10 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt
@@ -22,6 +22,7 @@ import io.realm.RealmConfiguration
 import kotlinx.coroutines.NonCancellable
 import kotlinx.coroutines.withContext
 import okhttp3.OkHttpClient
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.auth.data.SessionParams
 import org.matrix.android.sdk.api.failure.GlobalError
 import org.matrix.android.sdk.api.federation.FederationService
@@ -45,6 +46,7 @@ import org.matrix.android.sdk.api.session.integrationmanager.IntegrationManagerS
 import org.matrix.android.sdk.api.session.media.MediaService
 import org.matrix.android.sdk.api.session.openid.OpenIdService
 import org.matrix.android.sdk.api.session.permalinks.PermalinkService
+import org.matrix.android.sdk.api.session.presence.PresenceService
 import org.matrix.android.sdk.api.session.profile.ProfileService
 import org.matrix.android.sdk.api.session.pushers.PushersService
 import org.matrix.android.sdk.api.session.room.RoomDirectoryService
@@ -86,6 +88,7 @@ internal class DefaultSession @Inject constructor(
         private val globalErrorHandler: GlobalErrorHandler,
         @SessionId
         override val sessionId: String,
+        override val coroutineDispatchers: MatrixCoroutineDispatchers,
         @SessionDatabase private val realmConfiguration: RealmConfiguration,
         private val lifecycleObservers: Set<@JvmSuppressWildcards SessionLifecycleObserver>,
         private val sessionListeners: SessionListeners,
@@ -127,6 +130,7 @@ internal class DefaultSession @Inject constructor(
         private val callSignalingService: Lazy<CallSignalingService>,
         private val spaceService: Lazy<SpaceService>,
         private val openIdService: Lazy<OpenIdService>,
+        private val presenceService: Lazy<PresenceService>,
         @UnauthenticatedWithCertificate
         private val unauthenticatedWithCertificateOkHttpClient: Lazy<OkHttpClient>
 ) : Session,
@@ -145,6 +149,7 @@ internal class DefaultSession @Inject constructor(
         SecureStorageService by secureStorageService.get(),
         HomeServerCapabilitiesService by homeServerCapabilitiesService.get(),
         ProfileService by profileService.get(),
+        PresenceService by presenceService.get(),
         AccountService by accountService.get() {
 
     override val sharedSecretStorageService: SharedSecretStorageService
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt
index 71031a4614ec8c1315c06df0d777780ad284822b..bc8a70753086901243df739a70aef66e6f97432d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session
 
 import dagger.BindsInstance
 import dagger.Component
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.auth.data.SessionParams
 import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.internal.crypto.CancelGossipRequestWorker
@@ -29,6 +30,7 @@ import org.matrix.android.sdk.internal.crypto.verification.SendVerificationMessa
 import org.matrix.android.sdk.internal.di.MatrixComponent
 import org.matrix.android.sdk.internal.federation.FederationModule
 import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker
+import org.matrix.android.sdk.internal.network.RequestModule
 import org.matrix.android.sdk.internal.session.account.AccountModule
 import org.matrix.android.sdk.internal.session.cache.CacheModule
 import org.matrix.android.sdk.internal.session.call.CallModule
@@ -42,6 +44,7 @@ import org.matrix.android.sdk.internal.session.identity.IdentityModule
 import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManagerModule
 import org.matrix.android.sdk.internal.session.media.MediaModule
 import org.matrix.android.sdk.internal.session.openid.OpenIdModule
+import org.matrix.android.sdk.internal.session.presence.di.PresenceModule
 import org.matrix.android.sdk.internal.session.profile.ProfileModule
 import org.matrix.android.sdk.internal.session.pushers.AddPusherWorker
 import org.matrix.android.sdk.internal.session.pushers.PushersModule
@@ -62,7 +65,6 @@ import org.matrix.android.sdk.internal.session.user.UserModule
 import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataModule
 import org.matrix.android.sdk.internal.session.widgets.WidgetModule
 import org.matrix.android.sdk.internal.task.TaskExecutor
-import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.internal.util.system.SystemModule
 
 @Component(dependencies = [MatrixComponent::class],
@@ -94,7 +96,9 @@ import org.matrix.android.sdk.internal.util.system.SystemModule
             CallModule::class,
             SearchModule::class,
             ThirdPartyModule::class,
-            SpaceModule::class
+            SpaceModule::class,
+            PresenceModule::class,
+            RequestModule::class
         ]
 )
 @SessionScope
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cleanup/CleanupSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cleanup/CleanupSession.kt
index d4374e0702f896310adf80bd4394df1d899ae570..e8d3eb1a78d40411f122808be5d6a5077322ad24 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cleanup/CleanupSession.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cleanup/CleanupSession.kt
@@ -18,7 +18,7 @@ package org.matrix.android.sdk.internal.session.cleanup
 
 import io.realm.Realm
 import io.realm.RealmConfiguration
-import org.matrix.android.sdk.BuildConfig
+import kotlinx.coroutines.delay
 import org.matrix.android.sdk.internal.SessionManager
 import org.matrix.android.sdk.internal.auth.SessionParamsStore
 import org.matrix.android.sdk.internal.crypto.CryptoModule
@@ -51,37 +51,56 @@ internal class CleanupSession @Inject constructor(
         @UserMd5 private val userMd5: String
 ) {
     suspend fun handle() {
+        val sessionRealmCount = Realm.getGlobalInstanceCount(realmSessionConfiguration)
+        val cryptoRealmCount = Realm.getGlobalInstanceCount(realmCryptoConfiguration)
+        Timber.d("Realm instance ($sessionRealmCount - $cryptoRealmCount)")
+
         Timber.d("Cleanup: delete session params...")
         sessionParamsStore.delete(sessionId)
 
         Timber.d("Cleanup: cancel pending works...")
         workManagerProvider.cancelAllWorks()
 
+        Timber.d("Cleanup: release session...")
+        sessionManager.releaseSession(sessionId)
+
         Timber.d("Cleanup: clear session data...")
         clearSessionDataTask.execute(Unit)
 
         Timber.d("Cleanup: clear crypto data...")
         clearCryptoDataTask.execute(Unit)
 
-        Timber.d("Cleanup: clear file system")
-        sessionFiles.deleteRecursively()
-        sessionCache.deleteRecursively()
-
         Timber.d("Cleanup: clear the database keys")
         realmKeysUtils.clear(SessionModule.getKeyAlias(userMd5))
         realmKeysUtils.clear(CryptoModule.getKeyAlias(userMd5))
 
-        Timber.d("Cleanup: release session...")
-        sessionManager.releaseSession(sessionId)
+        // Wait for all the Realm instance to be released properly. Closing Realm instance is async.
+        // After that we can safely delete the Realm files
+        waitRealmRelease()
+
+        Timber.d("Cleanup: clear file system")
+        sessionFiles.deleteRecursively()
+        sessionCache.deleteRecursively()
+    }
+
+    private suspend fun waitRealmRelease() {
+        var timeToWaitMillis = MAX_TIME_TO_WAIT_MILLIS
+        do {
+            val sessionRealmCount = Realm.getGlobalInstanceCount(realmSessionConfiguration)
+            val cryptoRealmCount = Realm.getGlobalInstanceCount(realmCryptoConfiguration)
+            Timber.d("Wait for all Realm instance to be closed ($sessionRealmCount - $cryptoRealmCount)")
+            if (sessionRealmCount > 0 || cryptoRealmCount > 0) {
+                Timber.d("Waiting ${TIME_TO_WAIT_MILLIS}ms")
+                delay(TIME_TO_WAIT_MILLIS)
+                timeToWaitMillis -= TIME_TO_WAIT_MILLIS
+            } else {
+                timeToWaitMillis = 0
+            }
+        } while (timeToWaitMillis > 0)
+    }
 
-        // Sanity check
-        if (BuildConfig.DEBUG) {
-            Realm.getGlobalInstanceCount(realmSessionConfiguration)
-                    .takeIf { it > 0 }
-                    ?.let { Timber.e("All realm instance for session has not been closed ($it)") }
-            Realm.getGlobalInstanceCount(realmCryptoConfiguration)
-                    .takeIf { it > 0 }
-                    ?.let { Timber.e("All realm instance for crypto has not been closed ($it)") }
-        }
+    companion object {
+        private const val MAX_TIME_TO_WAIT_MILLIS = 10_000L
+        private const val TIME_TO_WAIT_MILLIS = 10L
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt
index 9b01d0a00e033fa6976c79dba71acdcdd0a17cf9..01eb52ff2270e875444f6ae7c4e18c583c6a4fbc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt
@@ -20,22 +20,23 @@ import android.graphics.Bitmap
 import android.graphics.BitmapFactory
 import android.graphics.Matrix
 import androidx.exifinterface.media.ExifInterface
-import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.withContext
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.internal.util.TemporaryFileCreator
 import timber.log.Timber
 import java.io.File
 import javax.inject.Inject
 
 internal class ImageCompressor @Inject constructor(
-        private val temporaryFileCreator: TemporaryFileCreator
+        private val temporaryFileCreator: TemporaryFileCreator,
+        private val coroutineDispatchers: MatrixCoroutineDispatchers
 ) {
     suspend fun compress(
             imageFile: File,
             desiredWidth: Int,
             desiredHeight: Int,
             desiredQuality: Int = 80): File {
-        return withContext(Dispatchers.IO) {
+        return withContext(coroutineDispatchers.io) {
             val compressedBitmap = BitmapFactory.Options().run {
                 inJustDecodeBounds = true
                 decodeBitmap(imageFile, this)
@@ -52,6 +53,8 @@ internal class ImageCompressor @Inject constructor(
                 destinationFile.outputStream().use {
                     compressedBitmap.compress(Bitmap.CompressFormat.JPEG, desiredQuality, it)
                 }
+            }.onFailure {
+                return@withContext imageFile
             }
 
             destinationFile
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageExifTagRemover.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageExifTagRemover.kt
new file mode 100644
index 0000000000000000000000000000000000000000..239a768498500f298815e7d1e602163f58d3f919
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageExifTagRemover.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.content
+
+import kotlinx.coroutines.withContext
+import org.apache.sanselan.Sanselan
+import org.apache.sanselan.formats.jpeg.JpegImageMetadata
+import org.apache.sanselan.formats.jpeg.exifRewrite.ExifRewriter
+import org.apache.sanselan.formats.tiff.constants.ExifTagConstants
+import org.apache.sanselan.formats.tiff.constants.GPSTagConstants
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
+import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.internal.util.TemporaryFileCreator
+import java.io.BufferedOutputStream
+import java.io.File
+import java.io.FileOutputStream
+import javax.inject.Inject
+
+/**
+ * This class is responsible for removing Exif tags from image files
+ */
+
+internal class ImageExifTagRemover @Inject constructor(
+        private val temporaryFileCreator: TemporaryFileCreator,
+        private val coroutineDispatchers: MatrixCoroutineDispatchers
+) {
+
+    /**
+     * Remove sensitive exif tags from a jpeg image file.
+     * Scrubbing exif tags like GPS location and user comments
+     * @param jpegImageFile The image file to be scrubbed
+     * @return the new scrubbed image file, or the original file if the operation failed
+     */
+    suspend fun removeSensitiveJpegExifTags(jpegImageFile: File): File = withContext(coroutineDispatchers.io) {
+        val outputSet = tryOrNull("Unable to read JpegImageMetadata") {
+            (Sanselan.getMetadata(jpegImageFile) as? JpegImageMetadata)?.exif?.outputSet
+        } ?: return@withContext jpegImageFile
+
+        tryOrNull("Unable to remove ExifData") {
+            outputSet.removeField(ExifTagConstants.EXIF_TAG_GPSINFO)
+            outputSet.removeField(ExifTagConstants.EXIF_TAG_SUBJECT_LOCATION_1)
+            outputSet.removeField(ExifTagConstants.EXIF_TAG_SUBJECT_LOCATION_2)
+            outputSet.removeField(ExifTagConstants.EXIF_TAG_USER_COMMENT)
+            outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_ALTITUDE)
+            outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_ALTITUDE_REF)
+            outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_LONGITUDE)
+            outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_LONGITUDE_REF)
+            outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_DEST_LONGITUDE)
+            outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_DEST_LONGITUDE_REF)
+            outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_LATITUDE)
+            outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_LATITUDE_REF)
+            outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_DEST_LATITUDE)
+            outputSet.removeField(GPSTagConstants.GPS_TAG_GPS_DEST_LATITUDE_REF)
+        } ?: return@withContext jpegImageFile
+
+        val scrubbedFile = temporaryFileCreator.create()
+        return@withContext runCatching {
+            FileOutputStream(scrubbedFile).use { fos ->
+                val outputStream = BufferedOutputStream(fos)
+                ExifRewriter().updateExifMetadataLossless(jpegImageFile, outputStream, outputSet)
+            }
+        }.fold(
+                onSuccess = {
+                    scrubbedFile
+                },
+                onFailure = {
+                    scrubbedFile.delete()
+                    jpegImageFile
+                }
+        )
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt
index 11c200c54b0e975cad26e08be28efda549ccae96..0c5a90ca60cb2df91fc878c11e9219c1d18039a7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt
@@ -64,7 +64,7 @@ private data class NewAttachmentAttributes(
  * Possible next worker    : Always [MultipleEventSendingDispatcherWorker]
  */
 internal class UploadContentWorker(val context: Context, params: WorkerParameters) :
-    SessionSafeCoroutineWorker<UploadContentWorker.Params>(context, params, Params::class.java) {
+        SessionSafeCoroutineWorker<UploadContentWorker.Params>(context, params, Params::class.java) {
 
     @JsonClass(generateAdapter = true)
     internal data class Params(
@@ -81,6 +81,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
     @Inject lateinit var fileService: DefaultFileService
     @Inject lateinit var cancelSendTracker: CancelSendTracker
     @Inject lateinit var imageCompressor: ImageCompressor
+    @Inject lateinit var imageExitTagRemover: ImageExifTagRemover
     @Inject lateinit var videoCompressor: VideoCompressor
     @Inject lateinit var thumbnailExtractor: ThumbnailExtractor
     @Inject lateinit var localEchoRepository: LocalEchoRepository
@@ -114,7 +115,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
         }
 
         val attachment = params.attachment
-        val filesToDelete = mutableListOf<File>()
+        val filesToDelete = hashSetOf<File>()
 
         return try {
             val inputStream = context.contentResolver.openInputStream(attachment.queryUri)
@@ -219,6 +220,10 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
                                     }
                                 }
                             }
+                } else if (attachment.type == ContentAttachmentData.Type.IMAGE && !params.compressBeforeSending) {
+                    fileToUpload = imageExitTagRemover.removeSensitiveJpegExifTags(workingFile)
+                            .also { filesToDelete.add(it) }
+                    newAttachmentAttributes = newAttachmentAttributes.copy(newFileSize = fileToUpload.length())
                 } else {
                     fileToUpload = workingFile
                     // Fix: OpenableColumns.SIZE may return -1 or 0
@@ -291,6 +296,11 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
             filesToDelete.forEach {
                 tryOrNull { it.delete() }
             }
+
+            // Delete the temporary voice message file
+            if (params.attachment.type == ContentAttachmentData.Type.AUDIO && params.attachment.mimeType == MimeTypes.Ogg) {
+                context.contentResolver.delete(params.attachment.queryUri, null, null)
+            }
         }
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt
index da37948cd4fb95f3682176937943db0d91ab44c5..37d9a4e74f8d649576ca9ee7583721d672ac6e72 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/DefaultIdentityService.kt
@@ -22,6 +22,7 @@ import androidx.lifecycle.LifecycleRegistry
 import dagger.Lazy
 import kotlinx.coroutines.withContext
 import okhttp3.OkHttpClient
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.auth.data.SessionParams
 import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.extensions.tryOrNull
@@ -51,7 +52,6 @@ import org.matrix.android.sdk.internal.session.profile.UnbindThreePidsTask
 import org.matrix.android.sdk.internal.session.sync.model.accountdata.IdentityServerContent
 import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask
 import org.matrix.android.sdk.internal.session.user.accountdata.UserAccountDataDataSource
-import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.internal.util.ensureProtocol
 import timber.log.Timber
 import javax.inject.Inject
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/PresenceAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/PresenceAPI.kt
new file mode 100644
index 0000000000000000000000000000000000000000..53d0d5e9631e8f7599545cc272e22e8866b48574
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/PresenceAPI.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.presence
+
+import org.matrix.android.sdk.internal.network.NetworkConstants
+import org.matrix.android.sdk.internal.session.presence.model.GetPresenceResponse
+import org.matrix.android.sdk.internal.session.presence.model.SetPresenceBody
+import retrofit2.http.Body
+import retrofit2.http.GET
+import retrofit2.http.PUT
+import retrofit2.http.Path
+
+internal interface PresenceAPI {
+
+    /**
+     * Set the presence status of the current user
+     * Ref: https://matrix.org/docs/spec/client_server/latest#put-matrix-client-r0-presence-userid-status
+     */
+    @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "presence/{userId}/status")
+    suspend fun setPresence(@Path("userId") userId: String,
+                            @Body body: SetPresenceBody)
+
+    /**
+     * Get the given user's presence state.
+     * Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-presence-userid-status
+     */
+    @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "presence/{userId}/status")
+    suspend fun getPresence(@Path("userId") userId: String): GetPresenceResponse
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/di/PresenceModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/di/PresenceModule.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6b2ee76046d053bcffd7549675e10687aa226dd3
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/di/PresenceModule.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.presence.di
+
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import org.matrix.android.sdk.api.session.presence.PresenceService
+import org.matrix.android.sdk.internal.session.SessionScope
+import org.matrix.android.sdk.internal.session.presence.PresenceAPI
+import org.matrix.android.sdk.internal.session.presence.service.DefaultPresenceService
+import org.matrix.android.sdk.internal.session.presence.service.task.DefaultGetPresenceTask
+import org.matrix.android.sdk.internal.session.presence.service.task.DefaultSetPresenceTask
+import org.matrix.android.sdk.internal.session.presence.service.task.GetPresenceTask
+import org.matrix.android.sdk.internal.session.presence.service.task.SetPresenceTask
+import retrofit2.Retrofit
+
+@Module
+internal abstract class PresenceModule {
+
+    @Module
+    companion object {
+        @Provides
+        @JvmStatic
+        @SessionScope
+        fun providesPresenceAPI(retrofit: Retrofit): PresenceAPI {
+            return retrofit.create(PresenceAPI::class.java)
+        }
+    }
+
+    @Binds
+    abstract fun bindPresenceService(service: DefaultPresenceService): PresenceService
+
+    @Binds
+    abstract fun bindSetPresenceTask(task: DefaultSetPresenceTask): SetPresenceTask
+
+    @Binds
+    abstract fun bindGetPresenceTask(task: DefaultGetPresenceTask): GetPresenceTask
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/model/GetPresenceResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/model/GetPresenceResponse.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a7552f7b027900f4e3cdc524d795a1013cee9185
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/model/GetPresenceResponse.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.presence.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.session.presence.model.PresenceEnum
+
+@JsonClass(generateAdapter = true)
+data class GetPresenceResponse(
+        @Json(name = "presence")
+        val presence: PresenceEnum,
+        @Json(name = "last_active_ago")
+        val lastActiveAgo: Long? = null,
+        @Json(name = "status_msg")
+        val message: String? = null,
+        @Json(name = "currently_active")
+        val isCurrentlyActive: Boolean? = null
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/model/PresenceContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/model/PresenceContent.kt
new file mode 100644
index 0000000000000000000000000000000000000000..45e0fcf06efa07c175568aea621d679e415e2386
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/model/PresenceContent.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.presence.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.session.presence.model.PresenceEnum
+
+/**
+ * Class representing the EventType.PRESENCE event content
+ */
+@JsonClass(generateAdapter = true)
+data class PresenceContent(
+        /**
+         * Required. The presence state for this user. One of: ["online", "offline", "unavailable"]
+         */
+        @Json(name = "presence") val presence: PresenceEnum,
+        /**
+         * The last time since this used performed some action, in milliseconds.
+         */
+        @Json(name = "last_active_ago") val lastActiveAgo: Long? = null,
+        /**
+         * An optional description to accompany the presence.
+         */
+        @Json(name = "status_msg") val statusMessage: String? = null,
+        /**
+         * Whether the user is currently active
+         */
+        @Json(name = "currently_active") val isCurrentlyActive: Boolean = false,
+        /**
+         * The current avatar URL for this user, if any.
+         */
+        @Json(name = "avatar_url") val avatarUrl: String? = null,
+        /**
+         * The current display name for this user, if any.
+         */
+        @Json(name = "displayname") val displayName: String? = null
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/model/SetPresenceBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/model/SetPresenceBody.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0c81791ec0f8482498f5138bf87a605d215ba89e
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/model/SetPresenceBody.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.matrix.android.sdk.internal.session.presence.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.api.session.presence.model.PresenceEnum
+
+@JsonClass(generateAdapter = true)
+internal data class SetPresenceBody(
+        @Json(name = "presence")
+        val presence: PresenceEnum,
+        @Json(name = "status_msg")
+        val statusMsg: String?
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/service/DefaultPresenceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/service/DefaultPresenceService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..1083d5b4c2e4878cb56a084222d9fc695c0e4f96
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/service/DefaultPresenceService.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.matrix.android.sdk.internal.session.presence.service
+
+import org.matrix.android.sdk.api.session.presence.PresenceService
+import org.matrix.android.sdk.api.session.presence.model.PresenceEnum
+import org.matrix.android.sdk.api.session.presence.model.UserPresence
+import org.matrix.android.sdk.internal.di.UserId
+import org.matrix.android.sdk.internal.session.presence.service.task.GetPresenceTask
+import org.matrix.android.sdk.internal.session.presence.service.task.SetPresenceTask
+import javax.inject.Inject
+
+internal class DefaultPresenceService @Inject constructor(
+        @UserId private val userId: String,
+        private val setPresenceTask: SetPresenceTask,
+        private val getPresenceTask: GetPresenceTask
+) : PresenceService {
+
+    override suspend fun setMyPresence(presence: PresenceEnum, statusMsg: String?) {
+        setPresenceTask.execute(SetPresenceTask.Params(userId, presence, statusMsg))
+    }
+
+    override suspend fun fetchPresence(userId: String): UserPresence {
+        val result = getPresenceTask.execute(GetPresenceTask.Params(userId))
+
+        return UserPresence(
+                lastActiveAgo = result.lastActiveAgo,
+                statusMessage = result.message,
+                isCurrentlyActive = result.isCurrentlyActive,
+                presence = result.presence
+        )
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/service/task/GetPresenceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/service/task/GetPresenceTask.kt
new file mode 100644
index 0000000000000000000000000000000000000000..bb628dbab426bbf1f039ee8c262d1c10c68cb17d
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/service/task/GetPresenceTask.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.matrix.android.sdk.internal.session.presence.service.task
+
+import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
+import org.matrix.android.sdk.internal.network.executeRequest
+import org.matrix.android.sdk.internal.session.presence.PresenceAPI
+import org.matrix.android.sdk.internal.session.presence.model.GetPresenceResponse
+import org.matrix.android.sdk.internal.task.Task
+import javax.inject.Inject
+
+internal abstract class GetPresenceTask : Task<GetPresenceTask.Params, GetPresenceResponse> {
+    data class Params(
+            val userId: String
+    )
+}
+
+internal class DefaultGetPresenceTask @Inject constructor(
+        private val presenceAPI: PresenceAPI,
+        private val globalErrorReceiver: GlobalErrorReceiver
+) : GetPresenceTask() {
+    override suspend fun execute(params: Params): GetPresenceResponse {
+        return executeRequest(globalErrorReceiver) {
+            presenceAPI.getPresence(params.userId)
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/service/task/SetPresenceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/service/task/SetPresenceTask.kt
new file mode 100644
index 0000000000000000000000000000000000000000..1b3bdabd3047ee41180670b29d163d159d3039a0
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/service/task/SetPresenceTask.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.matrix.android.sdk.internal.session.presence.service.task
+
+import org.matrix.android.sdk.api.session.presence.model.PresenceEnum
+import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
+import org.matrix.android.sdk.internal.network.executeRequest
+import org.matrix.android.sdk.internal.session.presence.PresenceAPI
+import org.matrix.android.sdk.internal.session.presence.model.SetPresenceBody
+import org.matrix.android.sdk.internal.task.Task
+import javax.inject.Inject
+
+internal abstract class SetPresenceTask : Task<SetPresenceTask.Params, Any> {
+    data class Params(
+            val userId: String,
+            val presence: PresenceEnum,
+            val statusMsg: String?
+    )
+}
+
+internal class DefaultSetPresenceTask @Inject constructor(
+        private val presenceAPI: PresenceAPI,
+        private val globalErrorReceiver: GlobalErrorReceiver
+) : SetPresenceTask() {
+
+    override suspend fun execute(params: Params): Any {
+        return executeRequest(globalErrorReceiver) {
+            val setPresenceBody = SetPresenceBody(params.presence, params.statusMsg)
+            presenceAPI.setPresence(params.userId, setPresenceBody)
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt
index 386fec8256f62d8b13fbe71c693c2f1fccf9d9da..a19832c5230dbdb68ce7003edcc12950978a7665 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt
@@ -22,6 +22,7 @@ import androidx.lifecycle.LiveData
 import com.zhuinden.monarchy.Monarchy
 import io.realm.kotlin.where
 import kotlinx.coroutines.withContext
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
 import org.matrix.android.sdk.api.session.identity.ThreePid
 import org.matrix.android.sdk.api.session.profile.ProfileService
@@ -35,7 +36,6 @@ import org.matrix.android.sdk.internal.session.content.FileUploader
 import org.matrix.android.sdk.internal.session.user.UserStore
 import org.matrix.android.sdk.internal.task.TaskExecutor
 import org.matrix.android.sdk.internal.task.configureWith
-import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
 import javax.inject.Inject
 
 internal class DefaultProfileService @Inject constructor(private val taskExecutor: TaskExecutor,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddPusherTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddPusherTask.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7d81e19265c3eef69921db05e216bd51c72209d3
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddPusherTask.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.pushers
+
+import com.zhuinden.monarchy.Monarchy
+import org.matrix.android.sdk.api.session.pushers.PusherState
+import org.matrix.android.sdk.internal.database.mapper.toEntity
+import org.matrix.android.sdk.internal.database.model.PusherEntity
+import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.di.SessionDatabase
+import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
+import org.matrix.android.sdk.internal.network.RequestExecutor
+import org.matrix.android.sdk.internal.task.Task
+import org.matrix.android.sdk.internal.util.awaitTransaction
+import javax.inject.Inject
+
+internal interface AddPusherTask : Task<AddPusherTask.Params, Unit> {
+    data class Params(val pusher: JsonPusher)
+}
+
+internal class DefaultAddPusherTask @Inject constructor(
+        private val pushersAPI: PushersAPI,
+        @SessionDatabase private val monarchy: Monarchy,
+        private val requestExecutor: RequestExecutor,
+        private val globalErrorReceiver: GlobalErrorReceiver
+) : AddPusherTask {
+    override suspend fun execute(params: AddPusherTask.Params) {
+        val pusher = params.pusher
+        try {
+            setPusher(pusher)
+        } catch (error: Throwable) {
+            monarchy.awaitTransaction { realm ->
+                PusherEntity.where(realm, pusher.pushKey).findFirst()?.let {
+                    it.state = PusherState.FAILED_TO_REGISTER
+                }
+            }
+            throw error
+        }
+    }
+
+    private suspend fun setPusher(pusher: JsonPusher) {
+        requestExecutor.executeRequest(globalErrorReceiver) {
+            pushersAPI.setPusher(pusher)
+        }
+        monarchy.awaitTransaction { realm ->
+            val echo = PusherEntity.where(realm, pusher.pushKey).findFirst()
+            if (echo == null) {
+                pusher.toEntity().also {
+                    it.state = PusherState.REGISTERED
+                    realm.insertOrUpdate(it)
+                }
+            } else {
+                echo.appDisplayName = pusher.appDisplayName
+                echo.appId = pusher.appId
+                echo.kind = pusher.kind
+                echo.lang = pusher.lang
+                echo.profileTag = pusher.profileTag
+                echo.data?.format = pusher.data?.format
+                echo.data?.url = pusher.data?.url
+                echo.state = PusherState.REGISTERED
+            }
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddPusherWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddPusherWorker.kt
index 63fd855c08aae3523e4ce25f8def4787f56fcab8..4df42b2cfbef50715b2614d953bdb3ab86aab181 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddPusherWorker.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddPusherWorker.kt
@@ -18,17 +18,8 @@ package org.matrix.android.sdk.internal.session.pushers
 import android.content.Context
 import androidx.work.WorkerParameters
 import com.squareup.moshi.JsonClass
-import com.zhuinden.monarchy.Monarchy
 import org.matrix.android.sdk.api.failure.Failure
-import org.matrix.android.sdk.api.session.pushers.PusherState
-import org.matrix.android.sdk.internal.database.mapper.toEntity
-import org.matrix.android.sdk.internal.database.model.PusherEntity
-import org.matrix.android.sdk.internal.database.query.where
-import org.matrix.android.sdk.internal.di.SessionDatabase
-import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
-import org.matrix.android.sdk.internal.network.executeRequest
 import org.matrix.android.sdk.internal.session.SessionComponent
-import org.matrix.android.sdk.internal.util.awaitTransaction
 import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
 import org.matrix.android.sdk.internal.worker.SessionWorkerParams
 import javax.inject.Inject
@@ -43,9 +34,7 @@ internal class AddPusherWorker(context: Context, params: WorkerParameters) :
             override val lastFailureMessage: String? = null
     ) : SessionWorkerParams
 
-    @Inject lateinit var pushersAPI: PushersAPI
-    @Inject @SessionDatabase lateinit var monarchy: Monarchy
-    @Inject lateinit var globalErrorReceiver: GlobalErrorReceiver
+    @Inject lateinit var addPusherTask: AddPusherTask
 
     override fun injectWith(injector: SessionComponent) {
         injector.inject(this)
@@ -58,20 +47,12 @@ internal class AddPusherWorker(context: Context, params: WorkerParameters) :
             return Result.failure()
         }
         return try {
-            setPusher(pusher)
+            addPusherTask.execute(AddPusherTask.Params(pusher))
             Result.success()
         } catch (exception: Throwable) {
             when (exception) {
                 is Failure.NetworkConnection -> Result.retry()
-                else                         -> {
-                    monarchy.awaitTransaction { realm ->
-                        PusherEntity.where(realm, pusher.pushKey).findFirst()?.let {
-                            // update it
-                            it.state = PusherState.FAILED_TO_REGISTER
-                        }
-                    }
-                    Result.failure()
-                }
+                else                         -> Result.failure()
             }
         }
     }
@@ -79,29 +60,4 @@ internal class AddPusherWorker(context: Context, params: WorkerParameters) :
     override fun buildErrorParams(params: Params, message: String): Params {
         return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
     }
-
-    private suspend fun setPusher(pusher: JsonPusher) {
-        executeRequest(globalErrorReceiver) {
-            pushersAPI.setPusher(pusher)
-        }
-        monarchy.awaitTransaction { realm ->
-            val echo = PusherEntity.where(realm, pusher.pushKey).findFirst()
-            if (echo != null) {
-                // update it
-                echo.appDisplayName = pusher.appDisplayName
-                echo.appId = pusher.appId
-                echo.kind = pusher.kind
-                echo.lang = pusher.lang
-                echo.profileTag = pusher.profileTag
-                echo.data?.format = pusher.data?.format
-                echo.data?.url = pusher.data?.url
-                echo.state = PusherState.REGISTERED
-            } else {
-                pusher.toEntity().also {
-                    it.state = PusherState.REGISTERED
-                    realm.insertOrUpdate(it)
-                }
-            }
-        }
-    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt
index 9a50abfe3572e3510da9da2dd9a30c0216f1b081..e87c27e601573e5e6a97cba49b238473a58d87bd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt
@@ -30,7 +30,6 @@ import org.matrix.android.sdk.internal.session.pushers.gateway.PushGatewayNotify
 import org.matrix.android.sdk.internal.task.TaskExecutor
 import org.matrix.android.sdk.internal.task.configureWith
 import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
-import java.security.InvalidParameterException
 import java.util.UUID
 import java.util.concurrent.TimeUnit
 import javax.inject.Inject
@@ -41,6 +40,7 @@ internal class DefaultPushersService @Inject constructor(
         @SessionId private val sessionId: String,
         private val getPusherTask: GetPushersTask,
         private val pushGatewayNotifyTask: PushGatewayNotifyTask,
+        private val addPusherTask: AddPusherTask,
         private val removePusherTask: RemovePusherTask,
         private val taskExecutor: TaskExecutor
 ) : PushersService {
@@ -58,51 +58,48 @@ internal class DefaultPushersService @Inject constructor(
                 .executeBy(taskExecutor)
     }
 
-    override fun addHttpPusher(pushkey: String,
-                               appId: String,
-                               profileTag: String,
-                               lang: String,
-                               appDisplayName: String,
-                               deviceDisplayName: String,
-                               url: String,
-                               append: Boolean,
-                               withEventIdOnly: Boolean
-    ) = addPusher(
-            JsonPusher(
-                    pushKey = pushkey,
-                    kind = Pusher.KIND_HTTP,
-                    appId = appId,
-                    profileTag = profileTag,
-                    lang = lang,
-                    appDisplayName = appDisplayName,
-                    deviceDisplayName = deviceDisplayName,
-                    data = JsonPusherData(url, EVENT_ID_ONLY.takeIf { withEventIdOnly }),
-                    append = append
-            )
-    )
+    override fun enqueueAddHttpPusher(httpPusher: PushersService.HttpPusher): UUID {
+        return enqueueAddPusher(httpPusher.toJsonPusher())
+    }
 
-    override fun addEmailPusher(email: String,
-                                lang: String,
-                                emailBranding: String,
-                                appDisplayName: String,
-                                deviceDisplayName: String,
-                                append: Boolean
-    ) = addPusher(
-            JsonPusher(
-                    pushKey = email,
-                    kind = Pusher.KIND_EMAIL,
-                    appId = Pusher.APP_ID_EMAIL,
-                    profileTag = "",
-                    lang = lang,
-                    appDisplayName = appDisplayName,
-                    deviceDisplayName = deviceDisplayName,
-                    data = JsonPusherData(brand = emailBranding),
-                    append = append
-            )
+    override suspend fun addHttpPusher(httpPusher: PushersService.HttpPusher) {
+        addPusherTask.execute(AddPusherTask.Params(httpPusher.toJsonPusher()))
+    }
+
+    private fun PushersService.HttpPusher.toJsonPusher() = JsonPusher(
+            pushKey = pushkey,
+            kind = "http",
+            appId = appId,
+            profileTag = profileTag,
+            lang = lang,
+            appDisplayName = appDisplayName,
+            deviceDisplayName = deviceDisplayName,
+            data = JsonPusherData(url, EVENT_ID_ONLY.takeIf { withEventIdOnly }),
+            append = append
     )
 
-    private fun addPusher(pusher: JsonPusher): UUID {
-        pusher.validateParameters()
+    override suspend fun addEmailPusher(email: String,
+                                        lang: String,
+                                        emailBranding: String,
+                                        appDisplayName: String,
+                                        deviceDisplayName: String,
+                                        append: Boolean) {
+        addPusherTask.execute(
+                AddPusherTask.Params(JsonPusher(
+                        pushKey = email,
+                        kind = Pusher.KIND_EMAIL,
+                        appId = Pusher.APP_ID_EMAIL,
+                        profileTag = "",
+                        lang = lang,
+                        appDisplayName = appDisplayName,
+                        deviceDisplayName = deviceDisplayName,
+                        data = JsonPusherData(brand = emailBranding),
+                        append = append
+                ))
+        )
+    }
+
+    private fun enqueueAddPusher(pusher: JsonPusher): UUID {
         val params = AddPusherWorker.Params(sessionId, pusher)
         val request = workManagerProvider.matrixOneTimeWorkRequestBuilder<AddPusherWorker>()
                 .setConstraints(WorkManagerProvider.workConstraints)
@@ -113,13 +110,6 @@ internal class DefaultPushersService @Inject constructor(
         return request.id
     }
 
-    private fun JsonPusher.validateParameters() {
-        // Do some parameter checks. It's ok to throw Exception, to inform developer of the problem
-        if (pushKey.length > 512) throw InvalidParameterException("pushkey should not exceed 512 chars")
-        if (appId.length > 64) throw InvalidParameterException("appId should not exceed 64 chars")
-        data?.url?.let { url -> if ("/_matrix/push/v1/notify" !in url) throw InvalidParameterException("url should contain '/_matrix/push/v1/notify'") }
-    }
-
     override suspend fun removePusher(pusher: Pusher) {
         removePusher(pusher.pushKey, pusher.appId)
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/JsonPusher.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/JsonPusher.kt
index a594675e28aa40b79ae804fc1510a3491ab2a9c2..8dc0954694c8e1a3a9bdd59eaa01695cf6f53d15 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/JsonPusher.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/JsonPusher.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.pushers
 import com.squareup.moshi.Json
 import com.squareup.moshi.JsonClass
 import org.matrix.android.sdk.internal.di.SerializeNulls
+import java.security.InvalidParameterException
 
 /**
  * Example:
@@ -112,4 +113,11 @@ internal data class JsonPusher(
          */
         @Json(name = "append")
         val append: Boolean? = false
-)
+) {
+    init {
+        // Do some parameter checks. It's ok to throw Exception, to inform developer of the problem
+        if (pushKey.length > 512) throw InvalidParameterException("pushkey should not exceed 512 chars")
+        if (appId.length > 64) throw InvalidParameterException("appId should not exceed 64 chars")
+        data?.url?.let { url -> if ("/_matrix/push/v1/notify" !in url) throw InvalidParameterException("url should contain '/_matrix/push/v1/notify'") }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushersModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushersModule.kt
index 4030c63514eb16fde9bb1c1104c67cb592b109a5..d53a4eed65462a96b60c8db9a967ef0133d7ebd9 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushersModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushersModule.kt
@@ -65,6 +65,9 @@ internal abstract class PushersModule {
     @Binds
     abstract fun bindSavePushRulesTask(task: DefaultSavePushRulesTask): SavePushRulesTask
 
+    @Binds
+    abstract fun bindAddPusherTask(task: DefaultAddPusherTask): AddPusherTask
+
     @Binds
     abstract fun bindRemovePusherTask(task: DefaultRemovePusherTask): RemovePusherTask
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt
index 8afd690f642d82fcf62dcf175dfdd11f90b57d15..cb4bcdb606a24975bc8c49266fe04c06d585ea69 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoom.kt
@@ -17,6 +17,7 @@
 package org.matrix.android.sdk.internal.session.room
 
 import androidx.lifecycle.LiveData
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.session.crypto.CryptoService
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.room.Room
@@ -71,7 +72,8 @@ internal class DefaultRoom(override val roomId: String,
                            private val roomVersionService: RoomVersionService,
                            private val sendStateTask: SendStateTask,
                            private val viaParameterFinder: ViaParameterFinder,
-                           private val searchTask: SearchTask
+                           private val searchTask: SearchTask,
+                           override val coroutineDispatchers: MatrixCoroutineDispatchers
 ) :
         Room,
         TimelineService by timelineService,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt
index d44eb32529b3954fb8d66938aaf15299550a30f2..4ab06338a2555b06080a4cd4b2fc6858e34c8ee0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomFactory.kt
@@ -16,6 +16,7 @@
 
 package org.matrix.android.sdk.internal.session.room
 
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.session.crypto.CryptoService
 import org.matrix.android.sdk.api.session.room.Room
 import org.matrix.android.sdk.internal.session.SessionScope
@@ -66,7 +67,8 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService:
                                                       private val roomAccountDataServiceFactory: DefaultRoomAccountDataService.Factory,
                                                       private val sendStateTask: SendStateTask,
                                                       private val viaParameterFinder: ViaParameterFinder,
-                                                      private val searchTask: SearchTask) :
+                                                      private val searchTask: SearchTask,
+                                                      private val coroutineDispatchers: MatrixCoroutineDispatchers) :
         RoomFactory {
 
     override fun create(roomId: String): Room {
@@ -92,7 +94,8 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService:
                 roomVersionService = roomVersionServiceFactory.create(roomId),
                 sendStateTask = sendStateTask,
                 searchTask = searchTask,
-                viaParameterFinder = viaParameterFinder
+                viaParameterFinder = viaParameterFinder,
+                coroutineDispatchers = coroutineDispatchers
         )
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DefaultDraftService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DefaultDraftService.kt
index 046f8ba8ba59bba7926729868f69ad7c47e73137..3867e0dc8d3fe6cd589f115593385e46875d31d3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DefaultDraftService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DefaultDraftService.kt
@@ -21,10 +21,10 @@ import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import kotlinx.coroutines.withContext
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.session.room.send.DraftService
 import org.matrix.android.sdk.api.session.room.send.UserDraft
 import org.matrix.android.sdk.api.util.Optional
-import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
 
 internal class DefaultDraftService @AssistedInject constructor(@Assisted private val roomId: String,
                                                                private val draftRepository: DraftRepository,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEntityFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEntityFactory.kt
index f78b5d7992b30e687c8699e03c7a221909dc41f6..d6a04de5e7d0b30099b4da0f5c153b4a03196ca1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEntityFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEntityFactory.kt
@@ -18,10 +18,11 @@ package org.matrix.android.sdk.internal.session.room.membership
 
 import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
 import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
+import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity
 
 internal object RoomMemberEntityFactory {
 
-    fun create(roomId: String, userId: String, roomMember: RoomMemberContent): RoomMemberSummaryEntity {
+    fun create(roomId: String, userId: String, roomMember: RoomMemberContent, presence: UserPresenceEntity?): RoomMemberSummaryEntity {
         val primaryKey = "${roomId}_$userId"
         return RoomMemberSummaryEntity(
                 primaryKey = primaryKey,
@@ -31,6 +32,7 @@ internal object RoomMemberEntityFactory {
                 avatarUrl = roomMember.avatarUrl
         ).apply {
             membership = roomMember.membership
+            userPresenceEntity = presence
         }
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEventHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEventHandler.kt
index 7528f80cc2ece1e4d0a65f829d28d9e0b215ff8a..25c124bd6bea067456c2b97471e948fb89ecc814 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEventHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEventHandler.kt
@@ -20,6 +20,9 @@ import io.realm.Realm
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
+import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
+import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity
+import org.matrix.android.sdk.internal.database.query.where
 import org.matrix.android.sdk.internal.di.UserId
 import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent
 import org.matrix.android.sdk.internal.session.sync.SyncResponsePostTreatmentAggregator
@@ -47,7 +50,13 @@ internal class RoomMemberEventHandler @Inject constructor(
         if (roomMember == null) {
             return false
         }
-        val roomMemberEntity = RoomMemberEntityFactory.create(roomId, userId, roomMember)
+        val roomMemberEntity = RoomMemberEntityFactory.create(
+                roomId,
+                userId,
+                roomMember,
+                // When an update is happening, insertOrUpdate replace existing values with null if they are not provided,
+                // but we want to preserve presence record value and not replace it with null
+                getExistingPresenceState(realm, roomId, userId))
         realm.insertOrUpdate(roomMemberEntity)
         if (roomMember.membership.isActive()) {
             val userEntity = UserEntityFactory.create(userId, roomMember)
@@ -60,7 +69,15 @@ internal class RoomMemberEventHandler @Inject constructor(
         if (mxId != null && mxId != myUserId) {
             aggregator?.directChatsToCheck?.put(roomId, mxId)
         }
-
         return true
     }
+
+    /**
+     * Get the already existing presence state for a specific user & room in order NOT to be replaced in RoomMemberSummaryEntity
+     * by NULL value.
+     */
+
+    private fun getExistingPresenceState(realm: Realm, roomId: String, userId: String): UserPresenceEntity? {
+        return RoomMemberSummaryEntity.where(realm, roomId, userId).findFirst()?.userPresenceEntity
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt
index eb48958afb17c5ef801e9fc702efe1630c02328a..64f1cc34f12896ac21d39c241ae93c2a142e2527 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetReadMarkersTask.kt
@@ -30,8 +30,8 @@ import org.matrix.android.sdk.internal.di.UserId
 import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
 import org.matrix.android.sdk.internal.network.executeRequest
 import org.matrix.android.sdk.internal.session.room.RoomAPI
-import org.matrix.android.sdk.internal.session.sync.ReadReceiptHandler
-import org.matrix.android.sdk.internal.session.sync.RoomFullyReadHandler
+import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler
+import org.matrix.android.sdk.internal.session.sync.handler.room.RoomFullyReadHandler
 import org.matrix.android.sdk.internal.task.Task
 import org.matrix.android.sdk.internal.util.awaitTransaction
 import timber.log.Timber
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
index c0e428ec85178e1f76e6c0ce5bd9e98287a829b4..0c917448cc4cb8889666a6d8acf46d5445d367a4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
@@ -42,7 +42,7 @@ import org.matrix.android.sdk.internal.database.query.findAllInRoomWithSendState
 import org.matrix.android.sdk.internal.database.query.where
 import org.matrix.android.sdk.internal.database.query.whereRoomId
 import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
-import org.matrix.android.sdk.internal.session.sync.ReadReceiptHandler
+import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler
 import org.matrix.android.sdk.internal.task.TaskExecutor
 import org.matrix.android.sdk.internal.task.configureWith
 import org.matrix.android.sdk.internal.util.Debouncer
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt
index 8de36d0427633af6a7fdfbfa150cf0c1df95028d..47e8f7e3a333df8541eaf0d4ce229f6559cb9cfc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt
@@ -37,7 +37,7 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
 import org.matrix.android.sdk.internal.database.query.where
 import org.matrix.android.sdk.internal.di.SessionDatabase
 import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
-import org.matrix.android.sdk.internal.session.sync.ReadReceiptHandler
+import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler
 import org.matrix.android.sdk.internal.task.TaskExecutor
 
 internal class DefaultTimelineService @AssistedInject constructor(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt
index 8c7401ab475c201f3e23147e1fcbcde9de80069a..335f61962335c3d6965c1d3b2426bfc02baa189e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt
@@ -33,6 +33,12 @@ import org.matrix.android.sdk.internal.session.group.GetGroupDataWorker
 import org.matrix.android.sdk.internal.session.initsync.ProgressReporter
 import org.matrix.android.sdk.internal.session.initsync.reportSubtask
 import org.matrix.android.sdk.internal.session.notification.ProcessEventForPushTask
+import org.matrix.android.sdk.internal.session.sync.handler.CryptoSyncHandler
+import org.matrix.android.sdk.internal.session.sync.handler.GroupSyncHandler
+import org.matrix.android.sdk.internal.session.sync.handler.PresenceSyncHandler
+import org.matrix.android.sdk.internal.session.sync.handler.SyncResponsePostTreatmentAggregatorHandler
+import org.matrix.android.sdk.internal.session.sync.handler.UserAccountDataSyncHandler
+import org.matrix.android.sdk.internal.session.sync.handler.room.RoomSyncHandler
 import org.matrix.android.sdk.internal.util.awaitTransaction
 import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
 import timber.log.Timber
@@ -55,7 +61,9 @@ internal class SyncResponseHandler @Inject constructor(
         private val cryptoService: DefaultCryptoService,
         private val tokenStore: SyncTokenStore,
         private val processEventForPushTask: ProcessEventForPushTask,
-        private val pushRuleService: PushRuleService) {
+        private val pushRuleService: PushRuleService,
+        private val presenceSyncHandler: PresenceSyncHandler
+) {
 
     suspend fun handleResponse(syncResponse: SyncResponse,
                                fromToken: String?,
@@ -118,6 +126,13 @@ internal class SyncResponseHandler @Inject constructor(
             }.also {
                 Timber.v("Finish handling accountData in $it ms")
             }
+
+            measureTimeMillis {
+                Timber.v("Handle Presence")
+                presenceSyncHandler.handle(realm, syncResponse.presence)
+            }.also {
+                Timber.v("Finish handling Presence in $it ms")
+            }
             tokenStore.saveToken(realm, syncResponse.nextBatch)
         }
 
@@ -145,7 +160,8 @@ internal class SyncResponseHandler @Inject constructor(
     private fun dispatchInvitedRoom(roomsSyncResponse: RoomsSyncResponse) {
         roomsSyncResponse.invite.keys.forEach { roomId ->
             sessionListeners.dispatch { session, listener ->
-                listener.onNewInvitedRoom(session, roomId) }
+                listener.onNewInvitedRoom(session, roomId)
+            }
         }
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregator.kt
index cc4ccc2e464689ed5019ae8b389b7f1ca44f1d8f..fe44531390bef1254433fe4c113c3038031ffda1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregator.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregator.kt
@@ -5,7 +5,7 @@
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
- *     http://www.apache.org/licenses/LICENSE-2.0
+ *    http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/CryptoSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt
similarity index 98%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/CryptoSyncHandler.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt
index cec5689a828f5c424c8eb25bf885e7db7f740b4a..c5ec34176c643fbaf682530d69833b0e71b25e12 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/CryptoSyncHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.session.sync
+package org.matrix.android.sdk.internal.session.sync.handler
 
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
 import org.matrix.android.sdk.api.session.events.model.Event
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/GroupSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/GroupSyncHandler.kt
similarity index 98%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/GroupSyncHandler.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/GroupSyncHandler.kt
index 2b054e578f0ab9f3a85a53aca29b34c8cac3afef..552462e25e69e451edd1533a8451db14a5897701 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/GroupSyncHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/GroupSyncHandler.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.session.sync
+package org.matrix.android.sdk.internal.session.sync.handler
 
 import io.realm.Realm
 import org.matrix.android.sdk.api.session.initsync.InitSyncStep
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/PresenceSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/PresenceSyncHandler.kt
new file mode 100644
index 0000000000000000000000000000000000000000..fe173a35c3855f2993006f091cbd70174cc6281c
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/PresenceSyncHandler.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.sync.handler
+
+import io.realm.Realm
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.getPresenceContent
+import org.matrix.android.sdk.api.session.sync.model.PresenceSyncResponse
+import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntity
+import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
+import org.matrix.android.sdk.internal.database.model.presence.UserPresenceEntity
+import org.matrix.android.sdk.internal.database.query.updateDirectUserPresence
+import org.matrix.android.sdk.internal.database.query.updateUserPresence
+import javax.inject.Inject
+
+internal class PresenceSyncHandler @Inject constructor() {
+
+    fun handle(realm: Realm, presenceSyncResponse: PresenceSyncResponse?) {
+        presenceSyncResponse?.events
+                ?.filter { event -> event.type == EventType.PRESENCE }
+                ?.forEach { event ->
+                    val content = event.getPresenceContent() ?: return@forEach
+                    val userId = event.senderId ?: return@forEach
+                    val userPresenceEntity = UserPresenceEntity(
+                            userId = userId,
+                            lastActiveAgo = content.lastActiveAgo,
+                            statusMessage = content.statusMessage,
+                            isCurrentlyActive = content.isCurrentlyActive,
+                            avatarUrl = content.avatarUrl,
+                            displayName = content.displayName
+                    ).also {
+                        it.presence = content.presence
+                    }
+
+                    storePresenceToDB(realm, userPresenceEntity)
+                }
+    }
+
+    /**
+     * Store user presence to DB and update Direct Rooms and Room Member Summaries accordingly
+     */
+    private fun storePresenceToDB(realm: Realm, userPresenceEntity: UserPresenceEntity) =
+            realm.copyToRealmOrUpdate(userPresenceEntity)?.apply {
+                RoomSummaryEntity.updateDirectUserPresence(realm, userPresenceEntity.userId, this)
+                RoomMemberSummaryEntity.updateUserPresence(realm, userPresenceEntity.userId, this)
+            }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregatorHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/SyncResponsePostTreatmentAggregatorHandler.kt
similarity index 93%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregatorHandler.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/SyncResponsePostTreatmentAggregatorHandler.kt
index db1100d76c5cdd9eef17876822f5a6416403ccb5..1e0e87a450e4ecf9aeccbf3a2c898182920235d1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregatorHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/SyncResponsePostTreatmentAggregatorHandler.kt
@@ -14,9 +14,11 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.session.sync
+package org.matrix.android.sdk.internal.session.sync.handler
 
 import org.matrix.android.sdk.api.MatrixPatterns
+import org.matrix.android.sdk.internal.session.sync.RoomSyncEphemeralTemporaryStore
+import org.matrix.android.sdk.internal.session.sync.SyncResponsePostTreatmentAggregator
 import org.matrix.android.sdk.internal.session.sync.model.accountdata.toMutable
 import org.matrix.android.sdk.internal.session.user.accountdata.DirectChatsHelper
 import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/UserAccountDataSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt
similarity index 99%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/UserAccountDataSyncHandler.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt
index a9926c70aa9bf581a9b8918c330fc19c8b439859..3e38cd78391a9c1849353eb33d8c7e93d790496f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/UserAccountDataSyncHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UserAccountDataSyncHandler.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.session.sync
+package org.matrix.android.sdk.internal.session.sync.handler
 
 import com.zhuinden.monarchy.Monarchy
 import io.realm.Realm
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/ReadReceiptHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt
similarity index 96%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/ReadReceiptHandler.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt
index fc1a2c387014d8f51f9068f8667023c64f1b6941..025ee329f8a0dd6731b67458760428e401a4d7ac 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/ReadReceiptHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ReadReceiptHandler.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.session.sync
+package org.matrix.android.sdk.internal.session.sync.handler.room
 
 import io.realm.Realm
 import org.matrix.android.sdk.api.session.events.model.EventType
@@ -23,6 +23,8 @@ import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity
 import org.matrix.android.sdk.internal.database.query.createUnmanaged
 import org.matrix.android.sdk.internal.database.query.getOrCreate
 import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.session.sync.RoomSyncEphemeralTemporaryStore
+import org.matrix.android.sdk.internal.session.sync.SyncResponsePostTreatmentAggregator
 import timber.log.Timber
 import javax.inject.Inject
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomFullyReadHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomFullyReadHandler.kt
similarity index 95%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomFullyReadHandler.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomFullyReadHandler.kt
index 3d0db212c2586bf9fbe5b4990f14b33159ccba84..b5c8a099d358b95c773c8f6f1ac1363b5879a176 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomFullyReadHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomFullyReadHandler.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.session.sync
+package org.matrix.android.sdk.internal.session.sync.handler.room
 
 import io.realm.Realm
 import org.matrix.android.sdk.internal.database.model.ReadMarkerEntity
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt
similarity index 98%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt
index 52e5b6b58d8c86359296e81ae733d4619e4af88b..8c4af81c99a38665f030ef0183510bc4297afcd5 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.session.sync
+package org.matrix.android.sdk.internal.session.sync.handler.room
 
 import io.realm.Realm
 import io.realm.kotlin.createObject
@@ -62,6 +62,9 @@ import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater
 import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
 import org.matrix.android.sdk.internal.session.room.timeline.TimelineInput
 import org.matrix.android.sdk.internal.session.room.typing.TypingEventContent
+import org.matrix.android.sdk.internal.session.sync.InitialSyncStrategy
+import org.matrix.android.sdk.internal.session.sync.SyncResponsePostTreatmentAggregator
+import org.matrix.android.sdk.internal.session.sync.initialSyncStrategy
 import org.matrix.android.sdk.internal.session.sync.parsing.RoomSyncAccountDataHandler
 import org.matrix.android.sdk.internal.util.computeBestChunkSize
 import timber.log.Timber
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTagHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomTagHandler.kt
similarity index 95%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTagHandler.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomTagHandler.kt
index 8997435be4b081a689ecb490066fb9f58b8ff799..55b15624bc4c328ad4fb748715d2df41b5df2ac1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTagHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomTagHandler.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.session.sync
+package org.matrix.android.sdk.internal.session.sync.handler.room
 
 import io.realm.Realm
 import org.matrix.android.sdk.api.session.room.model.tag.RoomTagContent
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTypingUsersHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomTypingUsersHandler.kt
similarity index 96%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTypingUsersHandler.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomTypingUsersHandler.kt
index 1433d891434b8b99fb4b8fa5dc331fc7329e74ad..63db13a5b804871163c24174c532bc3fdbfac4e4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomTypingUsersHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomTypingUsersHandler.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.session.sync
+package org.matrix.android.sdk.internal.session.sync.handler.room
 
 import io.realm.Realm
 import org.matrix.android.sdk.api.session.room.sender.SenderInfo
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt
index cce169c2464af123907e3c25f942b08692313746..9480cc73f142ebdc50405cc3ec52e5e66adb7cd6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncService.kt
@@ -25,6 +25,7 @@ import kotlinx.coroutines.SupervisorJob
 import kotlinx.coroutines.cancelChildren
 import kotlinx.coroutines.launch
 import org.matrix.android.sdk.api.Matrix
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.failure.Failure
 import org.matrix.android.sdk.api.failure.isTokenError
 import org.matrix.android.sdk.api.session.Session
@@ -34,7 +35,6 @@ import org.matrix.android.sdk.internal.session.sync.SyncPresence
 import org.matrix.android.sdk.internal.session.sync.SyncTask
 import org.matrix.android.sdk.internal.task.TaskExecutor
 import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver
-import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
 import timber.log.Timber
 import java.net.SocketTimeoutException
 import java.util.concurrent.atomic.AtomicBoolean
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt
index 6ca008c5b18e01707113892384d20d475aa95b8f..5e7bde87e77d2a668e1e9369331debe9ef283206 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/parsing/RoomSyncAccountDataHandler.kt
@@ -28,8 +28,8 @@ import org.matrix.android.sdk.internal.database.model.RoomAccountDataEntityField
 import org.matrix.android.sdk.internal.database.model.RoomEntity
 import org.matrix.android.sdk.internal.database.query.getOrCreate
 import org.matrix.android.sdk.internal.session.room.read.FullyReadContent
-import org.matrix.android.sdk.internal.session.sync.RoomFullyReadHandler
-import org.matrix.android.sdk.internal.session.sync.RoomTagHandler
+import org.matrix.android.sdk.internal.session.sync.handler.room.RoomFullyReadHandler
+import org.matrix.android.sdk.internal.session.sync.handler.room.RoomTagHandler
 import javax.inject.Inject
 
 internal class RoomSyncAccountDataHandler @Inject constructor(private val roomTagHandler: RoomTagHandler,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt
index e5c338d51105a018cfdbb1cd2843cd4715dda0e6..ddcac475ee6d2cae50c79281d9e61596ef88a17e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt
@@ -25,7 +25,7 @@ import org.matrix.android.sdk.api.session.room.accountdata.RoomAccountDataEvent
 import org.matrix.android.sdk.api.util.Optional
 import org.matrix.android.sdk.internal.di.SessionDatabase
 import org.matrix.android.sdk.internal.session.room.accountdata.RoomAccountDataDataSource
-import org.matrix.android.sdk.internal.session.sync.UserAccountDataSyncHandler
+import org.matrix.android.sdk.internal.session.sync.handler.UserAccountDataSyncHandler
 import org.matrix.android.sdk.internal.task.TaskExecutor
 import org.matrix.android.sdk.internal.task.configureWith
 import org.matrix.android.sdk.internal.util.awaitCallback
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/TaskExecutor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/TaskExecutor.kt
index 86848d10188af2b7d72d8c8e9dcd63192acebfc1..57574a96fa9f120d705b421c6176d5a50c8cf343 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/TaskExecutor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/task/TaskExecutor.kt
@@ -21,10 +21,10 @@ import kotlinx.coroutines.SupervisorJob
 import kotlinx.coroutines.cancelChildren
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
+import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.util.Cancelable
 import org.matrix.android.sdk.internal.di.MatrixScope
 import org.matrix.android.sdk.internal.extensions.foldToCallback
-import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.internal.util.toCancelable
 import timber.log.Timber
 import javax.inject.Inject
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..c8be0f54871b23e29b0fdd0ce95c89157ea43d9d
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt
@@ -0,0 +1,103 @@
+/*
+ * 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.pushers
+
+import kotlinx.coroutines.runBlocking
+import org.amshove.kluent.internal.assertFailsWith
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.Test
+import org.matrix.android.sdk.api.session.pushers.PusherState
+import org.matrix.android.sdk.internal.database.model.PusherEntity
+import org.matrix.android.sdk.test.fakes.FakeGlobalErrorReceiver
+import org.matrix.android.sdk.test.fakes.FakeMonarchy
+import org.matrix.android.sdk.test.fakes.FakePushersAPI
+import org.matrix.android.sdk.test.fakes.FakeRequestExecutor
+import java.net.SocketException
+
+private val A_JSON_PUSHER = JsonPusher(
+        pushKey = "push-key",
+        kind = "http",
+        appId = "m.email",
+        appDisplayName = "Element",
+        deviceDisplayName = null,
+        profileTag = "",
+        lang = "en-GB",
+        data = JsonPusherData(brand = "Element")
+)
+
+class DefaultAddPusherTaskTest {
+
+    private val pushersAPI = FakePushersAPI()
+    private val monarchy = FakeMonarchy()
+
+    private val addPusherTask = DefaultAddPusherTask(
+            pushersAPI = pushersAPI,
+            monarchy = monarchy.instance,
+            requestExecutor = FakeRequestExecutor(),
+            globalErrorReceiver = FakeGlobalErrorReceiver()
+    )
+
+    @Test
+    fun `given no persisted pusher when adding Pusher then updates api and inserts result with Registered state`() {
+        monarchy.givenWhereReturns<PusherEntity>(result = null)
+
+        runBlocking { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
+
+        pushersAPI.verifySetPusher(A_JSON_PUSHER)
+        monarchy.verifyInsertOrUpdate<PusherEntity> {
+            withArg { actual ->
+                actual.state shouldBeEqualTo PusherState.REGISTERED
+            }
+        }
+    }
+
+    @Test
+    fun `given a persisted pusher when adding Pusher then updates api and mutates persisted result with Registered state`() {
+        val realmResult = PusherEntity(appDisplayName = null)
+        monarchy.givenWhereReturns(result = realmResult)
+
+        runBlocking { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
+
+        pushersAPI.verifySetPusher(A_JSON_PUSHER)
+
+        realmResult.appDisplayName shouldBeEqualTo A_JSON_PUSHER.appDisplayName
+        realmResult.state shouldBeEqualTo PusherState.REGISTERED
+    }
+
+    @Test
+    fun `given a persisted push entity and SetPush API fails when adding Pusher then mutates persisted result with Failed registration state and rethrows error`() {
+        val realmResult = PusherEntity()
+        monarchy.givenWhereReturns(result = realmResult)
+        pushersAPI.givenSetPusherErrors(SocketException())
+
+        assertFailsWith<SocketException> {
+            runBlocking { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
+        }
+
+        realmResult.state shouldBeEqualTo PusherState.FAILED_TO_REGISTER
+    }
+
+    @Test
+    fun `given no persisted push entity and SetPush API fails when adding Pusher then rethrows error`() {
+        monarchy.givenWhereReturns<PusherEntity>(result = null)
+        pushersAPI.givenSetPusherErrors(SocketException())
+
+        assertFailsWith<SocketException> {
+            runBlocking { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeGlobalErrorReceiver.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeGlobalErrorReceiver.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ebddb3fafa5428695a091d935dea56d868d763a6
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeGlobalErrorReceiver.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.test.fakes
+
+import org.matrix.android.sdk.api.failure.GlobalError
+import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
+
+internal class FakeGlobalErrorReceiver : GlobalErrorReceiver {
+    override fun handleGlobalError(globalError: GlobalError) {
+        // do nothing
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0a22ef899664023c842b35034aee27c5c233acb5
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.test.fakes
+
+import com.zhuinden.monarchy.Monarchy
+import io.mockk.MockKVerificationScope
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.verify
+import io.realm.Realm
+import io.realm.RealmModel
+import io.realm.RealmQuery
+import io.realm.kotlin.where
+import org.matrix.android.sdk.internal.util.awaitTransaction
+
+internal class FakeMonarchy {
+
+    val instance = mockk<Monarchy>()
+    private val realm = mockk<Realm>(relaxed = true)
+
+    init {
+        mockkStatic("org.matrix.android.sdk.internal.util.MonarchyKt")
+        coEvery {
+            instance.awaitTransaction(any<suspend (Realm) -> Any>())
+        } coAnswers {
+            secondArg<suspend (Realm) -> Any>().invoke(realm)
+        }
+    }
+
+    inline fun <reified T : RealmModel> givenWhereReturns(result: T?) {
+        val queryResult = mockk<RealmQuery<T>>(relaxed = true)
+        every { queryResult.findFirst() } returns result
+        every { realm.where<T>() } returns queryResult
+    }
+
+    inline fun <reified T : RealmModel> verifyInsertOrUpdate(crossinline verification: MockKVerificationScope.() -> T) {
+        verify { realm.insertOrUpdate(verification()) }
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakePushersAPI.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakePushersAPI.kt
new file mode 100644
index 0000000000000000000000000000000000000000..29f93c2fafa1b16a1cd4e657c2ced0af428b6501
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakePushersAPI.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.test.fakes
+
+import org.amshove.kluent.shouldBeEqualTo
+import org.matrix.android.sdk.internal.session.pushers.GetPushersResponse
+import org.matrix.android.sdk.internal.session.pushers.JsonPusher
+import org.matrix.android.sdk.internal.session.pushers.PushersAPI
+
+internal class FakePushersAPI : PushersAPI {
+
+    private var setRequestPayload: JsonPusher? = null
+    private var error: Throwable? = null
+
+    override suspend fun getPushers(): GetPushersResponse {
+        TODO("Not yet implemented")
+    }
+
+    override suspend fun setPusher(jsonPusher: JsonPusher) {
+        error?.let { throw it }
+        setRequestPayload = jsonPusher
+    }
+
+    fun verifySetPusher(payload: JsonPusher) {
+        this.setRequestPayload shouldBeEqualTo payload
+    }
+
+    fun givenSetPusherErrors(error: Throwable) {
+        this.error = error
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRequestExecutor.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRequestExecutor.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2f332a89a807be3d15d43842ac9e34b8a848fadb
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRequestExecutor.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.test.fakes
+
+import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
+import org.matrix.android.sdk.internal.network.RequestExecutor
+
+internal class FakeRequestExecutor : RequestExecutor {
+
+    override suspend fun <DATA> executeRequest(globalErrorReceiver: GlobalErrorReceiver?,
+                                               canRetry: Boolean,
+                                               maxDelayBeforeRetry: Long,
+                                               maxRetriesCount: Int,
+                                               requestBlock: suspend () -> DATA): DATA {
+        return requestBlock()
+    }
+}