From 4cad0d2f07bbae62dffa660d6873b73fec2152cf Mon Sep 17 00:00:00 2001
From: Benoit Marty <benoit@matrix.org>
Date: Wed, 20 Oct 2021 11:18:21 +0200
Subject: [PATCH] Import v1.3.4 from Element Android

---
 dependencies.gradle                           |  14 ++-
 matrix-sdk-android/build.gradle               |  10 +-
 .../sdk/SingleThreadCoroutineDispatcher.kt    |   2 +-
 .../MatrixCoroutineDispatchers.kt             |   6 +-
 .../matrix/android/sdk/api/session/Session.kt |   5 +
 .../sdk/api/session/events/model/Event.kt     |   5 +
 .../api/session/presence/PresenceService.kt   |  41 +++++++
 .../session/presence/model/PresenceEnum.kt    |  36 ++++++
 .../session/presence/model/UserPresence.kt    |  24 ++++
 .../sdk/api/session/pushers/PushersService.kt | 110 ++++++++++++------
 .../android/sdk/api/session/room/Room.kt      |   3 +
 .../session/room/model/RoomMemberSummary.kt   |   3 +
 .../sdk/api/session/room/model/RoomSummary.kt |   2 +
 .../internal/crypto/DefaultCryptoService.kt   |   2 +-
 .../sdk/internal/crypto/DeviceListManager.kt  |   2 +-
 .../sdk/internal/crypto/EventDecryptor.kt     |  53 +++++----
 .../crypto/InboundGroupSessionStore.kt        |   2 +-
 .../crypto/IncomingGossipingRequestManager.kt |   2 +-
 .../crypto/OutgoingGossipingRequestManager.kt |   2 +-
 .../EnsureOlmSessionsForDevicesAction.kt      |   4 +-
 .../algorithms/megolm/MXMegolmDecryption.kt   |   2 +-
 .../megolm/MXMegolmDecryptionFactory.kt       |   2 +-
 .../algorithms/megolm/MXMegolmEncryption.kt   |   2 +-
 .../megolm/MXMegolmEncryptionFactory.kt       |   2 +-
 .../algorithms/olm/MXOlmEncryptionFactory.kt  |   2 +-
 .../crypto/crosssigning/ComputeTrustTask.kt   |   2 +-
 .../DefaultCrossSigningService.kt             |   2 +-
 .../keysbackup/DefaultKeysBackupService.kt    |   8 +-
 .../keysbackup/model/MegolmBackupAuthData.kt  |   2 +-
 .../DefaultSharedSecretStorageService.kt      |   2 +-
 .../internal/crypto/store/IMXCryptoStore.kt   |   3 +-
 .../crypto/store/db/RealmCryptoStore.kt       |  33 +++---
 .../DefaultVerificationService.kt             |   2 +-
 .../database/RealmSessionStoreMigration.kt    |  28 ++++-
 .../mapper/RoomMemberSummaryMapper.kt         |   2 +
 .../database/mapper/RoomSummaryMapper.kt      |   2 +
 .../database/model/RoomMemberSummaryEntity.kt |   6 +
 .../database/model/RoomSummaryEntity.kt       |   6 +
 .../database/model/SessionRealmModule.kt      |   4 +-
 .../model/presence/UserPresenceEntity.kt      |  51 ++++++++
 .../database/query/RoomMemberEntityQueries.kt |  11 ++
 .../query/RoomSummaryEntityQueries.kt         |   9 ++
 .../query/UserPresenceEntityQueries.kt        |  29 +++++
 .../sdk/internal/di/MatrixComponent.kt        |   2 +-
 .../android/sdk/internal/di/MatrixModule.kt   |   2 +-
 .../sdk/internal/extensions/LiveData.kt       |  23 ++++
 .../sdk/internal/network/RequestExecutor.kt   |  36 ++++++
 .../sdk/internal/network/RequestModule.kt     |  29 +++++
 .../internal/session/DefaultFileService.kt    |   2 +-
 .../sdk/internal/session/DefaultSession.kt    |   5 +
 .../sdk/internal/session/SessionComponent.kt  |   8 +-
 .../session/cleanup/CleanupSession.kt         |  51 +++++---
 .../session/content/ImageCompressor.kt        |   9 +-
 .../session/content/ImageExifTagRemover.kt    |  86 ++++++++++++++
 .../session/content/UploadContentWorker.kt    |  14 ++-
 .../identity/DefaultIdentityService.kt        |   2 +-
 .../internal/session/presence/PresenceAPI.kt  |  43 +++++++
 .../session/presence/di/PresenceModule.kt     |  53 +++++++++
 .../presence/model/GetPresenceResponse.kt     |  33 ++++++
 .../session/presence/model/PresenceContent.kt |  52 +++++++++
 .../session/presence/model/SetPresenceBody.kt |  28 +++++
 .../service/DefaultPresenceService.kt         |  48 ++++++++
 .../presence/service/task/GetPresenceTask.kt  |  42 +++++++
 .../presence/service/task/SetPresenceTask.kt  |  47 ++++++++
 .../session/profile/DefaultProfileService.kt  |   2 +-
 .../internal/session/pushers/AddPusherTask.kt |  78 +++++++++++++
 .../session/pushers/AddPusherWorker.kt        |  50 +-------
 .../session/pushers/DefaultPushersService.kt  |  90 +++++++-------
 .../internal/session/pushers/JsonPusher.kt    |  10 +-
 .../internal/session/pushers/PushersModule.kt |   3 +
 .../sdk/internal/session/room/DefaultRoom.kt  |   4 +-
 .../sdk/internal/session/room/RoomFactory.kt  |   7 +-
 .../session/room/draft/DefaultDraftService.kt |   2 +-
 .../membership/RoomMemberEntityFactory.kt     |   4 +-
 .../room/membership/RoomMemberEventHandler.kt |  21 +++-
 .../session/room/read/SetReadMarkersTask.kt   |   4 +-
 .../session/room/timeline/DefaultTimeline.kt  |   2 +-
 .../room/timeline/DefaultTimelineService.kt   |   2 +-
 .../session/sync/SyncResponseHandler.kt       |  20 +++-
 .../SyncResponsePostTreatmentAggregator.kt    |   2 +-
 .../sync/{ => handler}/CryptoSyncHandler.kt   |   2 +-
 .../sync/{ => handler}/GroupSyncHandler.kt    |   2 +-
 .../sync/handler/PresenceSyncHandler.kt       |  61 ++++++++++
 ...cResponsePostTreatmentAggregatorHandler.kt |   4 +-
 .../UserAccountDataSyncHandler.kt             |   2 +-
 .../{ => handler/room}/ReadReceiptHandler.kt  |   4 +-
 .../room}/RoomFullyReadHandler.kt             |   2 +-
 .../{ => handler/room}/RoomSyncHandler.kt     |   5 +-
 .../sync/{ => handler/room}/RoomTagHandler.kt |   2 +-
 .../room}/RoomTypingUsersHandler.kt           |   2 +-
 .../internal/session/sync/job/SyncService.kt  |   2 +-
 .../parsing/RoomSyncAccountDataHandler.kt     |   4 +-
 .../DefaultSessionAccountDataService.kt       |   2 +-
 .../android/sdk/internal/task/TaskExecutor.kt |   2 +-
 .../pushers/DefaultAddPusherTaskTest.kt       | 103 ++++++++++++++++
 .../sdk/test/fakes/FakeGlobalErrorReceiver.kt |  26 +++++
 .../android/sdk/test/fakes/FakeMonarchy.kt    |  55 +++++++++
 .../android/sdk/test/fakes/FakePushersAPI.kt  |  45 +++++++
 .../sdk/test/fakes/FakeRequestExecutor.kt     |  31 +++++
 99 files changed, 1546 insertions(+), 266 deletions(-)
 rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/{internal/util => api}/MatrixCoroutineDispatchers.kt (85%)
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/presence/PresenceService.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/presence/model/PresenceEnum.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/presence/model/UserPresence.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/presence/UserPresenceEntity.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/UserPresenceEntityQueries.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RequestExecutor.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RequestModule.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageExifTagRemover.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/PresenceAPI.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/di/PresenceModule.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/model/GetPresenceResponse.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/model/PresenceContent.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/model/SetPresenceBody.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/service/DefaultPresenceService.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/service/task/GetPresenceTask.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/presence/service/task/SetPresenceTask.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddPusherTask.kt
 rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/{ => handler}/CryptoSyncHandler.kt (98%)
 rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/{ => handler}/GroupSyncHandler.kt (98%)
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/PresenceSyncHandler.kt
 rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/{ => handler}/SyncResponsePostTreatmentAggregatorHandler.kt (93%)
 rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/{ => handler}/UserAccountDataSyncHandler.kt (99%)
 rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/{ => handler/room}/ReadReceiptHandler.kt (96%)
 rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/{ => handler/room}/RoomFullyReadHandler.kt (95%)
 rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/{ => handler/room}/RoomSyncHandler.kt (98%)
 rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/{ => handler/room}/RoomTagHandler.kt (95%)
 rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/{ => handler/room}/RoomTypingUsersHandler.kt (96%)
 create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt
 create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeGlobalErrorReceiver.kt
 create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt
 create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakePushersAPI.kt
 create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRequestExecutor.kt

diff --git a/dependencies.gradle b/dependencies.gradle
index 92358952..7fa42a66 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/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle
index 7196a97b..12b37b00 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 192f6442..3e3af107 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 b44a543c..592974be 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 bde68da9..d4bfd4ee 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 96b44ce8..169f90db 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 00000000..82a81f4b
--- /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 00000000..6d9994ef
--- /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 00000000..6b33ff07
--- /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 2cd17952..f884d3e8 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 ebe96b63..6c0e7304 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 fba3a1dd..39177a42 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 cae4775e..10cad026 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 7115ff5d..7b96148e 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 8a91376b..494e6d7c 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 fe17dd08..57381eac 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 3825a5da..e7a46750 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 e8640d50..220f25ec 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 6fc71036..fd60e432 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 52876b0f..3979ff1f 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 d2f6bd03..d7411ad0 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 91640523..29f9d193 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 63fe6782..031bb4e1 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 9f6312ea..238d7eed 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 68a95e39..50ce2d2b 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 113255bb..b470ab34 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 8a851b12..83de06a6 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 c6e2c121..b20168ea 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 54b92546..17c89576 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 ad3bc012..e6d8b5e8 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 238d0673..9b75f88f 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 3c8f74d4..40678a6c 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 76810997..388ecb96 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 aa96ca5e..05137f81 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 2365a395..efd9b680 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 0cf431c3..5900ef6b 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 75771ff1..a8a76d16 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 64dc08e8..88b88869 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 19472e21..c0907779 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 00000000..5713337e
--- /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 a19a9cf7..1ea06b9d 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 5294f849..d1b05a49 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 00000000..22790b6f
--- /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 5bc519e9..81a067f2 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 4cd960f4..9cab307c 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 718e7869..fecbb874 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 00000000..5cd2d880
--- /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 00000000..7b2bb9fc
--- /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 414c0180..46c59678 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 d41bf8a7..6a6bce5c 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 71031a46..bc8a7075 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 d4374e07..e8d3eb1a 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 9b01d0a0..01eb52ff 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 00000000..239a7684
--- /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 11c200c5..0c5a90ca 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 da37948c..37d9a4e7 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 00000000..53d0d5e9
--- /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 00000000..6b2ee760
--- /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 00000000..a7552f7b
--- /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 00000000..45e0fcf0
--- /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 00000000..0c81791e
--- /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 00000000..1083d5b4
--- /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 00000000..bb628dba
--- /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 00000000..1b3bdabd
--- /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 386fec82..a19832c5 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 00000000..7d81e192
--- /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 63fd855c..4df42b2c 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 9a50abfe..e87c27e6 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 a594675e..8dc09546 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 4030c635..d53a4eed 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 8afd690f..cb4bcdb6 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 d44eb325..4ab06338 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 046f8ba8..3867e0dc 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 f78b5d79..d6a04de5 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 7528f80c..25c124bd 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 eb48958a..64f1cc34 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 c0e428ec..0c917448 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 8de36d04..47e8f7e3 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 8c7401ab..335f6196 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 cc4ccc2e..fe445313 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 cec5689a..c5ec3417 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 2b054e57..552462e2 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 00000000..fe173a35
--- /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 db1100d7..1e0e87a4 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 a9926c70..3e38cd78 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 fc1a2c38..025ee329 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 3d0db212..b5c8a099 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 52e5b6b5..8c4af81c 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 8997435b..55b15624 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 1433d891..63db13a5 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 cce169c2..9480cc73 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 6ca008c5..5e7bde87 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 e5c338d5..ddcac475 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 86848d10..57574a96 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 00000000..c8be0f54
--- /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 00000000..ebddb3fa
--- /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 00000000..0a22ef89
--- /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 00000000..29f93c2f
--- /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 00000000..2f332a89
--- /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()
+    }
+}
-- 
GitLab