From 7e7a9533453c5de2e3b90b03333a24bb0d7a42f0 Mon Sep 17 00:00:00 2001
From: Valere <valeref@matrix.org>
Date: Thu, 22 Oct 2020 16:52:32 +0200
Subject: [PATCH 01/23] Crypto cleaning and perf improvement

---
 .../crypto/CryptoSessionInfoProvider.kt       |  61 ++++
 .../internal/crypto/DefaultCryptoService.kt   | 171 +++-------
 .../sdk/internal/crypto/DeviceListManager.kt  |   2 +-
 .../sdk/internal/crypto/EventDecryptor.kt     | 169 ++++++++++
 .../crypto/InboundGroupSessionStore.kt        |  96 ++++++
 .../sdk/internal/crypto/MXOlmDevice.kt        |   8 +-
 .../crypto/OutgoingGossipingRequestManager.kt |   4 +-
 .../DefaultCrossSigningService.kt             | 123 +++++--
 .../SessionToCryptoRoomMembersUpdate.kt       |  26 --
 .../crypto/crosssigning/ShieldTrustUpdater.kt | 126 -------
 .../crypto/crosssigning/UpdateTrustWorker.kt  | 318 ++++++++++++++++++
 .../keysbackup/DefaultKeysBackupService.kt    |  13 +-
 .../internal/crypto/tasks/EncryptEventTask.kt |  11 +-
 .../internal/crypto/tasks/SendEventTask.kt    |   7 +-
 .../tasks/SendVerificationMessageTask.kt      |  13 +-
 .../DefaultVerificationService.kt             |   8 +-
 .../SendVerificationMessageWorker.kt          |   5 +-
 .../VerificationMessageProcessor.kt           |   6 +-
 .../database/EventInsertLiveObserver.kt       |  41 ++-
 .../sdk/internal/session/DefaultSession.kt    |   5 +-
 .../sdk/internal/session/SessionComponent.kt  |   3 +
 .../sdk/internal/session/SessionModule.kt     |   5 -
 .../EventRelationsAggregationProcessor.kt     |   8 +-
 .../room/relation/DefaultRelationService.kt   |  15 +-
 .../session/room/send/DefaultSendService.kt   |   8 +-
 .../session/room/send/SendEventWorker.kt      |   2 +-
 .../room/send/queue/SendEventQueuedTask.kt    |   2 +-
 .../room/summary/RoomSummaryUpdater.kt        |  34 +-
 .../session/room/timeline/DefaultTimeline.kt  |   2 +-
 .../room/timeline/TimelineEventDecryptor.kt   |  48 +--
 .../internal/session/sync/RoomSyncHandler.kt  |  10 +-
 .../internal/session/sync/job/SyncThread.kt   |   2 +-
 32 files changed, 898 insertions(+), 454 deletions(-)
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt
 delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/SessionToCryptoRoomMembersUpdate.kt
 delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ShieldTrustUpdater.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt
new file mode 100644
index 0000000000..68cc139521
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.crypto
+
+import com.zhuinden.monarchy.Monarchy
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.internal.database.model.EventEntity
+import org.matrix.android.sdk.internal.database.model.EventEntityFields
+import org.matrix.android.sdk.internal.database.query.whereType
+import org.matrix.android.sdk.internal.di.SessionDatabase
+import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
+import org.matrix.android.sdk.internal.util.fetchCopied
+import javax.inject.Inject
+
+/**
+ * The crypto module needs some information regarding rooms that are stored
+ * in the session DB, this class encapsulate this functionality
+ */
+class CryptoSessionInfoProvider @Inject constructor(
+        @SessionDatabase private val monarchy: Monarchy,
+) {
+
+    fun isRoomEncrypted(roomId: String): Boolean {
+        val encryptionEvent = monarchy.fetchCopied { realm ->
+            EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
+                    .contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
+                    .isNotNull(EventEntityFields.STATE_KEY) // should be an empty key
+                    .findFirst()
+        }
+        return encryptionEvent != null
+    }
+
+    /**
+     * @param allActive if true return joined as well as invited, if false, only joined
+     */
+     fun getRoomUserIdsForCrypto(roomId: String, allActive: Boolean): List<String> {
+        var userIds: List<String> = emptyList()
+        monarchy.doWithRealm { realm ->
+            userIds = if (allActive) {
+                RoomMemberHelper(realm, roomId).getActiveRoomMemberIds()
+            } else {
+                RoomMemberHelper(realm, roomId).getJoinedRoomMemberIds()
+            }
+        }
+        return userIds
+    }
+}
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 b78afe6d41..0a18aba8ba 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
@@ -17,12 +17,9 @@
 package org.matrix.android.sdk.internal.crypto
 
 import android.content.Context
-import android.os.Handler
-import android.os.Looper
 import androidx.annotation.VisibleForTesting
 import androidx.lifecycle.LiveData
 import com.squareup.moshi.Types
-import com.zhuinden.monarchy.Monarchy
 import dagger.Lazy
 import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.CoroutineScope
@@ -51,9 +48,7 @@ import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
 import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityContent
 import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
-import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
 import org.matrix.android.sdk.internal.crypto.actions.MegolmSessionDataImporter
-import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
 import org.matrix.android.sdk.internal.crypto.actions.SetDeviceVerificationAction
 import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
 import org.matrix.android.sdk.internal.crypto.algorithms.IMXWithHeldExtension
@@ -68,7 +63,6 @@ import org.matrix.android.sdk.internal.crypto.model.MXDeviceInfo
 import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult
 import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
 import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
-import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent
 import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyContent
 import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent
 import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent
@@ -82,21 +76,15 @@ import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceTask
 import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceWithUserPasswordTask
 import org.matrix.android.sdk.internal.crypto.tasks.GetDeviceInfoTask
 import org.matrix.android.sdk.internal.crypto.tasks.GetDevicesTask
-import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
 import org.matrix.android.sdk.internal.crypto.tasks.SetDeviceNameTask
 import org.matrix.android.sdk.internal.crypto.tasks.UploadKeysTask
 import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService
-import org.matrix.android.sdk.internal.database.model.EventEntity
-import org.matrix.android.sdk.internal.database.model.EventEntityFields
-import org.matrix.android.sdk.internal.database.query.whereType
 import org.matrix.android.sdk.internal.di.DeviceId
 import org.matrix.android.sdk.internal.di.MoshiProvider
-import org.matrix.android.sdk.internal.di.SessionDatabase
 import org.matrix.android.sdk.internal.di.UserId
 import org.matrix.android.sdk.internal.extensions.foldToCallback
 import org.matrix.android.sdk.internal.session.SessionScope
 import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
-import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
 import org.matrix.android.sdk.internal.session.sync.model.SyncResponse
 import org.matrix.android.sdk.internal.task.TaskExecutor
 import org.matrix.android.sdk.internal.task.TaskThread
@@ -104,11 +92,11 @@ 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.android.sdk.internal.util.fetchCopied
 import org.matrix.olm.OlmManager
 import timber.log.Timber
 import java.util.concurrent.atomic.AtomicBoolean
 import javax.inject.Inject
+import kotlin.jvm.Throws
 import kotlin.math.max
 
 /**
@@ -171,28 +159,16 @@ internal class DefaultCryptoService @Inject constructor(
         private val setDeviceNameTask: SetDeviceNameTask,
         private val uploadKeysTask: UploadKeysTask,
         private val loadRoomMembersTask: LoadRoomMembersTask,
-        @SessionDatabase private val monarchy: Monarchy,
+        private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
         private val coroutineDispatchers: MatrixCoroutineDispatchers,
         private val taskExecutor: TaskExecutor,
         private val cryptoCoroutineScope: CoroutineScope,
-        private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
-        private val sendToDeviceTask: SendToDeviceTask,
-        private val messageEncrypter: MessageEncrypter
+        private val eventDecryptor: EventDecryptor
 ) : CryptoService {
 
-    init {
-        verificationService.cryptoService = this
-    }
-
-    private val uiHandler = Handler(Looper.getMainLooper())
-
     private val isStarting = AtomicBoolean(false)
     private val isStarted = AtomicBoolean(false)
 
-    // The date of the last time we forced establishment
-    // of a new session for each user:device.
-    private val lastNewSessionForcedDates = MXUsersDevicesMap<Long>()
-
     fun onStateEvent(roomId: String, event: Event) {
         when (event.getClearType()) {
             EventType.STATE_ROOM_ENCRYPTION         -> onRoomEncryptionEvent(roomId, event)
@@ -612,13 +588,7 @@ internal class DefaultCryptoService @Inject constructor(
      * @return true if the room is encrypted with algorithm MXCRYPTO_ALGORITHM_MEGOLM
      */
     override fun isRoomEncrypted(roomId: String): Boolean {
-        val encryptionEvent = monarchy.fetchCopied { realm ->
-            EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
-                    .contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
-                    .isNotNull(EventEntityFields.STATE_KEY)
-                    .findFirst()
-        }
-        return encryptionEvent != null
+        return cryptoSessionInfoProvider.isRoomEncrypted(roomId)
     }
 
     /**
@@ -660,11 +630,8 @@ internal class DefaultCryptoService @Inject constructor(
                                      eventType: String,
                                      roomId: String,
                                      callback: MatrixCallback<MXEncryptEventContentResult>) {
+        // moved to crypto scope to have uptodate values
         cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
-//            if (!isStarted()) {
-//                Timber.v("## CRYPTO | encryptEventContent() : wait after e2e init")
-//                internalStart(false)
-//            }
             val userIds = getRoomUserIds(roomId)
             var alg = roomEncryptorsStore.get(roomId)
             if (alg == null) {
@@ -720,14 +687,7 @@ internal class DefaultCryptoService @Inject constructor(
      * @param callback the callback to return data or null
      */
     override fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) {
-        cryptoCoroutineScope.launch {
-            val result = runCatching {
-                withContext(coroutineDispatchers.crypto) {
-                    internalDecryptEvent(event, timeline)
-                }
-            }
-            result.foldToCallback(callback)
-        }
+        eventDecryptor.decryptEventAsync(event, timeline, callback)
     }
 
     /**
@@ -739,42 +699,7 @@ internal class DefaultCryptoService @Inject constructor(
      */
     @Throws(MXCryptoError::class)
     private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
-        val eventContent = event.content
-        if (eventContent == null) {
-            Timber.e("## CRYPTO | decryptEvent : empty event content")
-            throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
-        } else {
-            val algorithm = eventContent["algorithm"]?.toString()
-            val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm)
-            if (alg == null) {
-                val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm)
-                Timber.e("## CRYPTO | decryptEvent() : $reason")
-                throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason)
-            } else {
-                try {
-                    return alg.decryptEvent(event, timeline)
-                } catch (mxCryptoError: MXCryptoError) {
-                    Timber.d("## CRYPTO | internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError")
-                    if (algorithm == MXCRYPTO_ALGORITHM_OLM) {
-                        if (mxCryptoError is MXCryptoError.Base
-                                && mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) {
-                            // need to find sending device
-                            val olmContent = event.content.toModel<OlmEventContent>()
-                            cryptoStore.getUserDevices(event.senderId ?: "")
-                                    ?.values
-                                    ?.firstOrNull { it.identityKey() == olmContent?.senderKey }
-                                    ?.let {
-                                        markOlmSessionForUnwedging(event.senderId ?: "", it)
-                                    }
-                                    ?: run {
-                                        Timber.v("## CRYPTO | markOlmSessionForUnwedging() : Failed to find sender crypto device")
-                                    }
-                        }
-                    }
-                    throw mxCryptoError
-                }
-            }
-        }
+       return eventDecryptor.decryptEvent(event, timeline)
     }
 
     /**
@@ -828,7 +753,7 @@ internal class DefaultCryptoService @Inject constructor(
      */
     private fun onRoomKeyEvent(event: Event) {
         val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return
-        Timber.v("## CRYPTO | GOSSIP onRoomKeyEvent() : type<${event.type}> , sessionId<${roomKeyContent.sessionId}>")
+        Timber.v("## CRYPTO | GOSSIP onRoomKeyEvent() : type<${event.getClearType()}> , sessionId<${roomKeyContent.sessionId}>")
         if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) {
             Timber.e("## CRYPTO | GOSSIP onRoomKeyEvent() : missing fields")
             return
@@ -935,19 +860,9 @@ internal class DefaultCryptoService @Inject constructor(
     }
 
     private fun getRoomUserIds(roomId: String): List<String> {
-        var userIds: List<String> = emptyList()
-        monarchy.doWithRealm { realm ->
-            // Check whether the event content must be encrypted for the invited members.
-            val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser()
-                    && shouldEncryptForInvitedMembers(roomId)
-
-            userIds = if (encryptForInvitedMembers) {
-                RoomMemberHelper(realm, roomId).getActiveRoomMemberIds()
-            } else {
-                RoomMemberHelper(realm, roomId).getJoinedRoomMemberIds()
-            }
-        }
-        return userIds
+        val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser()
+                && shouldEncryptForInvitedMembers(roomId)
+        return cryptoSessionInfoProvider.getRoomUserIdsForCrypto(roomId, encryptForInvitedMembers)
     }
 
     /**
@@ -1257,38 +1172,38 @@ internal class DefaultCryptoService @Inject constructor(
         incomingGossipingRequestManager.removeRoomKeysRequestListener(listener)
     }
 
-    private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) {
-        val deviceKey = deviceInfo.identityKey()
-
-        val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0
-        val now = System.currentTimeMillis()
-        if (now - lastForcedDate < CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
-            Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another")
-            return
-        }
-
-        Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}")
-        lastNewSessionForcedDates.setObject(senderId, deviceKey, now)
-
-        cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
-            ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true)
-
-            // 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.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}")
-            val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
-            sendToDeviceTask.execute(sendToDeviceParams)
-        }
-    }
+//    private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) {
+//        val deviceKey = deviceInfo.identityKey()
+//
+//        val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0
+//        val now = System.currentTimeMillis()
+//        if (now - lastForcedDate < CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
+//            Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another")
+//            return
+//        }
+//
+//        Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}")
+//        lastNewSessionForcedDates.setObject(senderId, deviceKey, now)
+//
+//        cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
+//            ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true)
+//
+//            // 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.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}")
+//            val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
+//            sendToDeviceTask.execute(sendToDeviceParams)
+//        }
+//    }
 
     /**
      * Provides the list of unknown devices
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 ab30d3052d..42df6b354b 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
@@ -377,7 +377,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
         }
 
         // Update devices trust for these users
-        dispatchDeviceChange(downloadUsers)
+        // dispatchDeviceChange(downloadUsers)
 
         return onKeysDownloadSucceed(filteredUsers, response.failures)
     }
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
new file mode 100644
index 0000000000..b423b5d738
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt
@@ -0,0 +1,169 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.crypto
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.matrix.android.sdk.api.MatrixCallback
+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
+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.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
+
+@SessionScope
+internal class EventDecryptor @Inject constructor(
+        private val cryptoCoroutineScope: CoroutineScope,
+        private val coroutineDispatchers: MatrixCoroutineDispatchers,
+        private val roomDecryptorProvider: RoomDecryptorProvider,
+        private val messageEncrypter: MessageEncrypter,
+        private val sendToDeviceTask: SendToDeviceTask,
+        private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
+        private val cryptoStore: IMXCryptoStore
+) {
+
+    // The date of the last time we forced establishment
+    // of a new session for each user:device.
+    private val lastNewSessionForcedDates = MXUsersDevicesMap<Long>()
+
+    /**
+     * Decrypt an event
+     *
+     * @param event    the raw event.
+     * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
+     * @return the MXEventDecryptionResult data, or throw in case of error
+     */
+    @Throws(MXCryptoError::class)
+    fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
+        return internalDecryptEvent(event, timeline)
+    }
+
+    /**
+     * Decrypt an event asynchronously
+     *
+     * @param event    the raw event.
+     * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
+     * @param callback the callback to return data or null
+     */
+    fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) {
+        // is it needed to do that on the crypto scope??
+        cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
+            runCatching {
+                internalDecryptEvent(event, timeline)
+            }.foldToCallback(callback)
+        }
+    }
+
+    /**
+     * Decrypt an event
+     *
+     * @param event    the raw event.
+     * @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
+     * @return the MXEventDecryptionResult data, or null in case of error
+     */
+    @Throws(MXCryptoError::class)
+    private fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
+        val eventContent = event.content
+        if (eventContent == null) {
+            Timber.e("## CRYPTO | decryptEvent : empty event content")
+            throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
+        } else {
+            val algorithm = eventContent["algorithm"]?.toString()
+            val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm)
+            if (alg == null) {
+                val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm)
+                Timber.e("## CRYPTO | decryptEvent() : $reason")
+                throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason)
+            } else {
+                try {
+                    return alg.decryptEvent(event, timeline)
+                } catch (mxCryptoError: MXCryptoError) {
+                    Timber.d("## CRYPTO | internalDecryptEvent : Failed to decrypt ${event.eventId} reason: $mxCryptoError")
+                    if (algorithm == MXCRYPTO_ALGORITHM_OLM) {
+                        if (mxCryptoError is MXCryptoError.Base
+                                && mxCryptoError.errorType == MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE) {
+                            // need to find sending device
+                            cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
+                                val olmContent = event.content.toModel<OlmEventContent>()
+                                cryptoStore.getUserDevices(event.senderId ?: "")
+                                        ?.values
+                                        ?.firstOrNull { it.identityKey() == olmContent?.senderKey }
+                                        ?.let {
+                                            markOlmSessionForUnwedging(event.senderId ?: "", it)
+                                        }
+                                        ?: run {
+                                            Timber.v("## CRYPTO | markOlmSessionForUnwedging() : Failed to find sender crypto device")
+                                        }
+                            }
+                        }
+                    }
+                    throw mxCryptoError
+                }
+            }
+        }
+    }
+
+    // coroutineDispatchers.crypto scope
+    private fun markOlmSessionForUnwedging(senderId: String, deviceInfo: CryptoDeviceInfo) {
+        val deviceKey = deviceInfo.identityKey()
+
+        val lastForcedDate = lastNewSessionForcedDates.getObject(senderId, deviceKey) ?: 0
+        val now = System.currentTimeMillis()
+        if (now - lastForcedDate < DefaultCryptoService.CRYPTO_MIN_FORCE_SESSION_PERIOD_MILLIS) {
+            Timber.d("## CRYPTO | markOlmSessionForUnwedging: New session already forced with device at $lastForcedDate. Not forcing another")
+            return
+        }
+
+        Timber.d("## CRYPTO | markOlmSessionForUnwedging from $senderId:${deviceInfo.deviceId}")
+        lastNewSessionForcedDates.setObject(senderId, deviceKey, now)
+
+        // offload this from crypto thread (?)
+        cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
+            ensureOlmSessionsForDevicesAction.handle(mapOf(senderId to listOf(deviceInfo)), force = true)
+
+            // 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.v("## CRYPTO | markOlmSessionForUnwedging() : sending to $senderId:${deviceInfo.deviceId}")
+            withContext(coroutineDispatchers.io) {
+                val sendToDeviceParams = SendToDeviceTask.Params(EventType.ENCRYPTED, sendToDeviceMap)
+                sendToDeviceTask.execute(sendToDeviceParams)
+            }
+        }
+    }
+}
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
new file mode 100644
index 0000000000..722fc0d92d
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/InboundGroupSessionStore.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.crypto
+
+import android.util.LruCache
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+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
+import javax.inject.Inject
+
+/**
+ * Allows to cache and batch store operations on inbound group session store.
+ * Because it is used in the decrypt flow, that can be called quite rapidly
+ */
+internal class InboundGroupSessionStore @Inject constructor(
+        private val store: IMXCryptoStore,
+        private val cryptoCoroutineScope: CoroutineScope,
+        private val coroutineDispatchers: MatrixCoroutineDispatchers) {
+
+    private data class CacheKey(
+            val sessionId: String,
+            val senderKey: String
+    )
+
+    private val sessionCache = object : LruCache<CacheKey, OlmInboundGroupSessionWrapper2>(30) {
+        override fun entryRemoved(evicted: Boolean, key: CacheKey?, oldValue: OlmInboundGroupSessionWrapper2?, newValue: OlmInboundGroupSessionWrapper2?) {
+            if (evicted && oldValue != null) {
+                cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
+                    Timber.v("## Inbound: entryRemoved ${oldValue.roomId}-${oldValue.senderKey}")
+                    store.storeInboundGroupSessions(listOf(oldValue))
+                }
+            }
+        }
+    }
+
+    private val timer = Timer()
+    private var timerTask: TimerTask? = null
+
+    private val dirtySession = mutableListOf<OlmInboundGroupSessionWrapper2>()
+
+    @Synchronized
+    fun getInboundGroupSession(sessionId: String, senderKey: String): OlmInboundGroupSessionWrapper2? {
+        synchronized(sessionCache) {
+            val known = sessionCache[CacheKey(sessionId, senderKey)]
+            Timber.v("## Inbound: getInboundGroupSession in cache ${known != null}")
+            return known ?: store.getInboundGroupSession(sessionId, senderKey)?.also {
+                Timber.v("## Inbound: getInboundGroupSession cache populate ${it.roomId}")
+                sessionCache.put(CacheKey(sessionId, senderKey), it)
+            }
+        }
+    }
+
+    @Synchronized
+    fun storeInBoundGroupSession(wrapper: OlmInboundGroupSessionWrapper2) {
+        Timber.v("## Inbound: getInboundGroupSession mark as dirty ${wrapper.roomId}-${wrapper.senderKey}")
+        // We want to batch this a bit for performances
+        dirtySession.add(wrapper)
+
+        timerTask?.cancel()
+        timerTask = object : TimerTask() {
+            override fun run() {
+                batchSave()
+            }
+        }
+        timer.schedule(timerTask!!, 2_000)
+    }
+
+    @Synchronized
+    private fun batchSave() {
+        val toSave = mutableListOf<OlmInboundGroupSessionWrapper2>().apply { addAll(dirtySession) }
+        dirtySession.clear()
+        cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
+            Timber.v("## Inbound: getInboundGroupSession batching save of ${dirtySession.size}")
+            store.storeInboundGroupSessions(toSave)
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt
index 7a546993b8..1a4d1136c8 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXOlmDevice.kt
@@ -44,7 +44,9 @@ internal class MXOlmDevice @Inject constructor(
         /**
          * The store where crypto data is saved.
          */
-        private val store: IMXCryptoStore) {
+        private val store: IMXCryptoStore,
+        private val inboundGroupSessionStore: InboundGroupSessionStore
+        ) {
 
     /**
      * @return the Curve25519 key for the account.
@@ -657,7 +659,7 @@ internal class MXOlmDevice @Inject constructor(
                 timelineSet.add(messageIndexKey)
             }
 
-            store.storeInboundGroupSessions(listOf(session))
+            inboundGroupSessionStore.storeInBoundGroupSession(session)
             val payload = try {
                 val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
                 val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage)
@@ -745,7 +747,7 @@ internal class MXOlmDevice @Inject constructor(
             throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.ERROR_MISSING_PROPERTY_REASON)
         }
 
-        val session = store.getInboundGroupSession(sessionId, senderKey)
+        val session =  inboundGroupSessionStore.getInboundGroupSession(sessionId, senderKey)
 
         if (session != null) {
             // Check that the room id matches the original one for the session. This stops
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 efda663230..c86f2be0a3 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
@@ -88,7 +88,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
      * @param requestBody requestBody
      */
     fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) {
-        cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
+        cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
             cancelRoomKeyRequest(requestBody, false)
         }
     }
@@ -99,7 +99,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
      * @param requestBody requestBody
      */
     fun resendRoomKeyRequest(requestBody: RoomKeyRequestBody) {
-        cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
+        cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
             cancelRoomKeyRequest(requestBody, true)
         }
     }
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 b5056a0efd..1871dba0e2 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
@@ -17,6 +17,8 @@
 package org.matrix.android.sdk.internal.crypto.crosssigning
 
 import androidx.lifecycle.LiveData
+import androidx.work.BackoffPolicy
+import androidx.work.ExistingWorkPolicy
 import org.matrix.android.sdk.api.MatrixCallback
 import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
@@ -39,15 +41,20 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.internal.util.withoutPrefix
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
-import org.greenrobot.eventbus.EventBus
+import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
+import org.matrix.android.sdk.internal.di.SessionId
+import org.matrix.android.sdk.internal.di.WorkManagerProvider
+import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
 import org.matrix.olm.OlmPkSigning
 import org.matrix.olm.OlmUtility
 import timber.log.Timber
+import java.util.concurrent.TimeUnit
 import javax.inject.Inject
 
 @SessionScope
 internal class DefaultCrossSigningService @Inject constructor(
         @UserId private val userId: String,
+        @SessionId private val sessionId: String,
         private val cryptoStore: IMXCryptoStore,
         private val deviceListManager: DeviceListManager,
         private val initializeCrossSigningTask: InitializeCrossSigningTask,
@@ -55,7 +62,7 @@ internal class DefaultCrossSigningService @Inject constructor(
         private val taskExecutor: TaskExecutor,
         private val coroutineDispatchers: MatrixCoroutineDispatchers,
         private val cryptoCoroutineScope: CoroutineScope,
-        private val eventBus: EventBus) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener {
+        private val workManagerProvider: WorkManagerProvider) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener {
 
     private var olmUtility: OlmUtility? = null
 
@@ -360,6 +367,12 @@ internal class DefaultCrossSigningService @Inject constructor(
         // First let's get my user key
         val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId)
 
+        checkOtherMSKTrusted(myCrossSigningInfo, cryptoStore.getCrossSigningInfo(otherUserId))
+
+        return UserTrustResult.Success
+    }
+
+    fun checkOtherMSKTrusted(myCrossSigningInfo: MXCrossSigningInfo?, otherInfo: MXCrossSigningInfo?): UserTrustResult {
         val myUserKey = myCrossSigningInfo?.userKey()
                 ?: return UserTrustResult.CrossSigningNotConfigured(userId)
 
@@ -368,15 +381,15 @@ internal class DefaultCrossSigningService @Inject constructor(
         }
 
         // Let's get the other user  master key
-        val otherMasterKey = cryptoStore.getCrossSigningInfo(otherUserId)?.masterKey()
-                ?: return UserTrustResult.UnknownCrossSignatureInfo(otherUserId)
+        val otherMasterKey = otherInfo?.masterKey()
+                ?: return UserTrustResult.UnknownCrossSignatureInfo(otherInfo?.userId ?: "")
 
         val masterKeySignaturesMadeByMyUserKey = otherMasterKey.signatures
                 ?.get(userId) // Signatures made by me
                 ?.get("ed25519:${myUserKey.unpaddedBase64PublicKey}")
 
         if (masterKeySignaturesMadeByMyUserKey.isNullOrBlank()) {
-            Timber.d("## CrossSigning  checkUserTrust false for $otherUserId, not signed by my UserSigningKey")
+            Timber.d("## CrossSigning  checkUserTrust false for ${otherInfo.userId}, not signed by my UserSigningKey")
             return UserTrustResult.KeyNotSigned(otherMasterKey)
         }
 
@@ -396,6 +409,15 @@ internal class DefaultCrossSigningService @Inject constructor(
         // and that MSK is trusted (i know the private key, or is signed by a trusted device)
         val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId)
 
+        return checkSelfTrust(myCrossSigningInfo, cryptoStore.getUserDeviceList(userId))
+    }
+
+    fun checkSelfTrust(myCrossSigningInfo: MXCrossSigningInfo?, myDevices: List<CryptoDeviceInfo>?): UserTrustResult {
+        // Special case when it's me,
+        // I have to check that MSK -> USK -> SSK
+        // and that MSK is trusted (i know the private key, or is signed by a trusted device)
+//        val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId)
+
         val myMasterKey = myCrossSigningInfo?.masterKey()
                 ?: return UserTrustResult.CrossSigningNotConfigured(userId)
 
@@ -423,7 +445,7 @@ internal class DefaultCrossSigningService @Inject constructor(
             // Maybe it's signed by a locally trusted device?
             myMasterKey.signatures?.get(userId)?.forEach { (key, value) ->
                 val potentialDeviceId = key.withoutPrefix("ed25519:")
-                val potentialDevice = cryptoStore.getUserDevice(userId, potentialDeviceId)
+                val potentialDevice = myDevices?.firstOrNull { it.deviceId == potentialDeviceId } // cryptoStore.getUserDevice(userId, potentialDeviceId)
                 if (potentialDevice != null && potentialDevice.isVerified) {
                     // Check signature validity?
                     try {
@@ -561,6 +583,8 @@ internal class DefaultCrossSigningService @Inject constructor(
         cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
             cryptoStore.markMyMasterKeyAsLocallyTrusted(true)
             checkSelfTrust()
+            // re-verify all trusts
+            onUsersDeviceUpdate(listOf(userId))
         }
     }
 
@@ -666,6 +690,55 @@ internal class DefaultCrossSigningService @Inject constructor(
         return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted))
     }
 
+    fun checkDeviceTrust(myKeys: MXCrossSigningInfo?, otherKeys: MXCrossSigningInfo?, otherDevice: CryptoDeviceInfo) : DeviceTrustResult {
+        val locallyTrusted = otherDevice.trustLevel?.isLocallyVerified()
+        myKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(userId))
+
+        if (!myKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(myKeys))
+
+        otherKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(otherDevice.userId))
+
+        // TODO should we force verification ?
+        if (!otherKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(otherKeys))
+
+        // Check if the trust chain is valid
+        /*
+         *  ┏━━━━━━━━┓                             ┏━━━━━━━━┓
+         *  ┃ ALICE  ┃                             ┃  BOB   ┃
+         *  ┗━━━━━━━━┛                             ┗━━━━━━━━┛
+         *   MSK                      ┌────────────▶MSK
+         *                            │
+         *     │                      │               │
+         *     │    SSK               │               └──▶ SSK  ──────────────────┐
+         *     │                      │                                           │
+         *     │                      │                    USK                    │
+         *     └──▶ USK   ────────────┘              (not visible by              │
+         *                                                Alice)                  │
+         *                                                                        ▼
+         *                                                                ┌──────────────┐
+         *                                                                │ BOB's Device │
+         *                                                                └──────────────┘
+         */
+
+        val otherSSKSignature = otherDevice.signatures?.get(otherKeys.userId)?.get("ed25519:${otherKeys.selfSigningKey()?.unpaddedBase64PublicKey}")
+                ?: return legacyFallbackTrust(
+                        locallyTrusted,
+                        DeviceTrustResult.MissingDeviceSignature(otherDevice.deviceId, otherKeys.selfSigningKey()
+                                ?.unpaddedBase64PublicKey
+                                ?: ""
+                        )
+                )
+
+        // Check  bob's device is signed by bob's SSK
+        try {
+            olmUtility!!.verifyEd25519Signature(otherSSKSignature, otherKeys.selfSigningKey()?.unpaddedBase64PublicKey, otherDevice.canonicalSignable())
+        } catch (e: Throwable) {
+            return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.InvalidDeviceSignature(otherDevice.deviceId, otherSSKSignature, e))
+        }
+
+        return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted))
+    }
+
     private fun legacyFallbackTrust(locallyTrusted: Boolean?, crossSignTrustFail: DeviceTrustResult): DeviceTrustResult {
         return if (locallyTrusted == true) {
             DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = false, locallyVerified = true))
@@ -675,36 +748,18 @@ internal class DefaultCrossSigningService @Inject constructor(
     }
 
     override fun onUsersDeviceUpdate(userIds: List<String>) {
-        cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
-            Timber.d("## CrossSigning - onUsersDeviceUpdate for ${userIds.size} users")
-            userIds.forEach { otherUserId ->
-                checkUserTrust(otherUserId).let {
-                    Timber.v("## CrossSigning - update trust for $otherUserId , verified=${it.isVerified()}")
-                    setUserKeysAsTrusted(otherUserId, it.isVerified())
-                }
-            }
-        }
+        Timber.d("## CrossSigning - onUsersDeviceUpdate for $userIds")
+        val workerParams = UpdateTrustWorker.Params(sessionId = sessionId, updatedUserIds = userIds)
+        val workerData = WorkerParamsFactory.toData(workerParams)
 
-        // now check device trust
-        cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
-            userIds.forEach { otherUserId ->
-                // TODO if my keys have changes, i should recheck all devices of all users?
-                val devices = cryptoStore.getUserDeviceList(otherUserId)
-                devices?.forEach { device ->
-                    val updatedTrust = checkDeviceTrust(otherUserId, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false)
-                    Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust")
-                    cryptoStore.setDeviceTrust(otherUserId, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified())
-                }
+        val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<UpdateTrustWorker>()
+                .setInputData(workerData)
+                .setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS)
+                .build()
 
-                if (otherUserId == userId) {
-                    // It's me, i should check if a newly trusted device is signing my master key
-                    // In this case it will change my MSK trust, and should then re-trigger a check of all other user trust
-                    setUserKeysAsTrusted(otherUserId, checkSelfTrust().isVerified())
-                }
-            }
-
-            eventBus.post(CryptoToSessionUserTrustChange(userIds))
-        }
+        workManagerProvider.workManager
+                .beginUniqueWork("TRUST_UPDATE_QUEUE", ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
+                .enqueue()
     }
 
     private fun setUserKeysAsTrusted(otherUserId: String, trusted: Boolean) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/SessionToCryptoRoomMembersUpdate.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/SessionToCryptoRoomMembersUpdate.kt
deleted file mode 100644
index 271b9e52d3..0000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/SessionToCryptoRoomMembersUpdate.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright 2020 The Matrix.org Foundation C.I.C.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.matrix.android.sdk.internal.crypto.crosssigning
-
-data class SessionToCryptoRoomMembersUpdate(
-        val roomId: String,
-        val isDirect: Boolean,
-        val userIds: List<String>
-)
-
-data class CryptoToSessionUserTrustChange(
-        val userIds: List<String>
-)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ShieldTrustUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ShieldTrustUpdater.kt
deleted file mode 100644
index 05ceba5965..0000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/ShieldTrustUpdater.kt
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- * Copyright 2020 The Matrix.org Foundation C.I.C.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.matrix.android.sdk.internal.crypto.crosssigning
-
-import org.matrix.android.sdk.api.extensions.orFalse
-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.RoomSummaryEntity
-import org.matrix.android.sdk.internal.database.query.where
-import org.matrix.android.sdk.internal.di.SessionDatabase
-import org.matrix.android.sdk.internal.session.SessionLifecycleObserver
-import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater
-import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
-import org.matrix.android.sdk.internal.task.TaskExecutor
-import org.matrix.android.sdk.internal.util.createBackgroundHandler
-import io.realm.Realm
-import io.realm.RealmConfiguration
-import kotlinx.coroutines.android.asCoroutineDispatcher
-import kotlinx.coroutines.launch
-import org.greenrobot.eventbus.EventBus
-import org.greenrobot.eventbus.Subscribe
-import timber.log.Timber
-import java.util.concurrent.atomic.AtomicBoolean
-import java.util.concurrent.atomic.AtomicReference
-import javax.inject.Inject
-
-internal class ShieldTrustUpdater @Inject constructor(
-        private val eventBus: EventBus,
-        private val computeTrustTask: ComputeTrustTask,
-        private val taskExecutor: TaskExecutor,
-        @SessionDatabase private val sessionRealmConfiguration: RealmConfiguration,
-        private val roomSummaryUpdater: RoomSummaryUpdater
-) : SessionLifecycleObserver {
-
-    companion object {
-        private val BACKGROUND_HANDLER = createBackgroundHandler("SHIELD_CRYPTO_DB_THREAD")
-        private val BACKGROUND_HANDLER_DISPATCHER = BACKGROUND_HANDLER.asCoroutineDispatcher()
-    }
-
-    private val backgroundSessionRealm = AtomicReference<Realm>()
-
-    private val isStarted = AtomicBoolean()
-
-    override fun onStart() {
-        if (isStarted.compareAndSet(false, true)) {
-            eventBus.register(this)
-            BACKGROUND_HANDLER.post {
-                backgroundSessionRealm.set(Realm.getInstance(sessionRealmConfiguration))
-            }
-        }
-    }
-
-    override fun onStop() {
-        if (isStarted.compareAndSet(true, false)) {
-            eventBus.unregister(this)
-            BACKGROUND_HANDLER.post {
-                backgroundSessionRealm.getAndSet(null).also {
-                    it?.close()
-                }
-            }
-        }
-    }
-
-    @Subscribe
-    fun onRoomMemberChange(update: SessionToCryptoRoomMembersUpdate) {
-        if (!isStarted.get()) {
-            return
-        }
-        taskExecutor.executorScope.launch(BACKGROUND_HANDLER_DISPATCHER) {
-            val updatedTrust = computeTrustTask.execute(ComputeTrustTask.Params(update.userIds, update.isDirect))
-            // We need to send that back to session base
-            backgroundSessionRealm.get()?.executeTransaction { realm ->
-                roomSummaryUpdater.updateShieldTrust(realm, update.roomId, updatedTrust)
-            }
-        }
-    }
-
-    @Subscribe
-    fun onTrustUpdate(update: CryptoToSessionUserTrustChange) {
-        if (!isStarted.get()) {
-            return
-        }
-        onCryptoDevicesChange(update.userIds)
-    }
-
-    private fun onCryptoDevicesChange(users: List<String>) {
-        taskExecutor.executorScope.launch(BACKGROUND_HANDLER_DISPATCHER) {
-            val realm = backgroundSessionRealm.get() ?: return@launch
-            val distinctRoomIds = realm.where(RoomMemberSummaryEntity::class.java)
-                    .`in`(RoomMemberSummaryEntityFields.USER_ID, users.toTypedArray())
-                    .distinct(RoomMemberSummaryEntityFields.ROOM_ID)
-                    .findAll()
-                    .map { it.roomId }
-
-            distinctRoomIds.forEach { roomId ->
-                val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
-                if (roomSummary?.isEncrypted.orFalse()) {
-                    val allActiveRoomMembers = RoomMemberHelper(realm, roomId).getActiveRoomMemberIds()
-                    try {
-                        val updatedTrust = computeTrustTask.execute(
-                                ComputeTrustTask.Params(allActiveRoomMembers, roomSummary?.isDirect == true)
-                        )
-                        realm.executeTransaction {
-                            roomSummaryUpdater.updateShieldTrust(it, roomId, updatedTrust)
-                        }
-                    } catch (failure: Throwable) {
-                        Timber.e(failure)
-                    }
-                }
-            }
-        }
-    }
-}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt
new file mode 100644
index 0000000000..e3ac387ddc
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt
@@ -0,0 +1,318 @@
+/*
+ * Copyright 2020 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.crypto.crosssigning
+
+import android.content.Context
+import androidx.work.WorkerParameters
+import com.squareup.moshi.JsonClass
+import io.realm.Realm
+import io.realm.RealmConfiguration
+import io.realm.kotlin.where
+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.crypto.store.db.mapper.CrossSigningKeysMapper
+import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntity
+import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields
+import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoMapper
+import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntity
+import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntity
+import org.matrix.android.sdk.internal.crypto.store.db.model.UserEntityFields
+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.RoomSummaryEntity
+import org.matrix.android.sdk.internal.database.query.where
+import org.matrix.android.sdk.internal.di.CryptoDatabase
+import org.matrix.android.sdk.internal.di.SessionDatabase
+import org.matrix.android.sdk.internal.di.UserId
+import org.matrix.android.sdk.internal.session.SessionComponent
+import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
+import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
+import org.matrix.android.sdk.internal.worker.SessionWorkerParams
+import timber.log.Timber
+import javax.inject.Inject
+
+internal class UpdateTrustWorker(context: Context,
+                                 params: WorkerParameters)
+    : SessionSafeCoroutineWorker<UpdateTrustWorker.Params>(context, params, Params::class.java) {
+
+    @JsonClass(generateAdapter = true)
+    internal data class Params(
+            override val sessionId: String,
+            override val lastFailureMessage: String? = null,
+            val updatedUserIds: List<String>
+    ) : SessionWorkerParams
+
+    @Inject lateinit var crossSigningService: DefaultCrossSigningService
+
+    // It breaks the crypto store contract, but we need to batch things :/
+    @CryptoDatabase @Inject lateinit var realmConfiguration: RealmConfiguration
+    @UserId @Inject lateinit var myUserId: String
+    @Inject lateinit var crossSigningKeysMapper: CrossSigningKeysMapper
+    @SessionDatabase @Inject lateinit var sessionRealmConfiguration: RealmConfiguration
+
+    //    @Inject lateinit var roomSummaryUpdater: RoomSummaryUpdater
+    @Inject lateinit var cryptoStore: IMXCryptoStore
+
+    override fun injectWith(injector: SessionComponent) {
+        injector.inject(this)
+    }
+
+    override suspend fun doSafeWork(params: Params): Result {
+        var userList = params.updatedUserIds
+        // Unfortunately we don't have much info on what did exactly changed (is it the cross signing keys of that user,
+        // or a new device?) So we check all again :/
+
+        Timber.d("## CrossSigning - Updating trust for $userList")
+
+        // First we check that the users MSK are trusted by mine
+        // After that we check the trust chain for each devices of each users
+        Realm.getInstance(realmConfiguration).use { realm ->
+            realm.executeTransaction {
+                // By mapping here to model, this object is not live
+                // I should update it if needed
+                var myCrossSigningInfo = realm.where(CrossSigningInfoEntity::class.java)
+                        .equalTo(CrossSigningInfoEntityFields.USER_ID, myUserId)
+                        .findFirst()?.let { mapCrossSigningInfoEntity(it) }
+
+                var myTrustResult: UserTrustResult? = null
+
+                if (userList.contains(myUserId)) {
+                    Timber.d("## CrossSigning - Clear all trust as a change on my user was detected")
+                    // i am in the list.. but i don't know exactly the delta of change :/
+                    // If it's my cross signing keys we should refresh all trust
+                    // do it anyway ?
+                    userList = realm.where(CrossSigningInfoEntity::class.java)
+                            .findAll().mapNotNull { it.userId }
+                    Timber.d("## CrossSigning - Updating trust for all $userList")
+
+                    // check right now my keys and mark it as trusted as other trust depends on it
+                    val myDevices = realm.where<UserEntity>()
+                            .equalTo(UserEntityFields.USER_ID, myUserId)
+                            .findFirst()
+                            ?.devices
+                            ?.map { deviceInfo ->
+                                CryptoMapper.mapToModel(deviceInfo)
+                            }
+                    myTrustResult = crossSigningService.checkSelfTrust(myCrossSigningInfo, myDevices).also {
+                        updateCrossSigningKeysTrust(realm, myUserId, it.isVerified())
+                        // update model reference
+                        myCrossSigningInfo = realm.where(CrossSigningInfoEntity::class.java)
+                                .equalTo(CrossSigningInfoEntityFields.USER_ID, myUserId)
+                                .findFirst()?.let { mapCrossSigningInfoEntity(it) }
+                    }
+                }
+
+                val otherInfos = userList.map {
+                    it to realm.where(CrossSigningInfoEntity::class.java)
+                            .equalTo(CrossSigningInfoEntityFields.USER_ID, it)
+                            .findFirst()?.let { mapCrossSigningInfoEntity(it) }
+                }
+                        .toMap()
+
+                val trusts = otherInfos.map { infoEntry ->
+                    infoEntry.key to when (infoEntry.key) {
+                        myUserId -> myTrustResult
+                        else     -> {
+                            crossSigningService.checkOtherMSKTrusted(myCrossSigningInfo, infoEntry.value).also {
+                                Timber.d("## CrossSigning - user:${infoEntry.key} result:$it")
+                            }
+                        }
+                    }
+                }.toMap()
+
+                // TODO! if it's me and my keys has changed... I have to reset trust for everyone!
+                // i have all the new trusts, update DB
+                trusts.forEach {
+                    val verified = it.value?.isVerified() == true
+                    updateCrossSigningKeysTrust(realm, it.key, verified)
+                }
+
+                // Ok so now we have to check device trust for all these users..
+                Timber.v("## CrossSigning - Updating devices cross trust users ${trusts.keys}")
+                trusts.keys.forEach {
+                    val devicesEntities = realm.where<UserEntity>()
+                            .equalTo(UserEntityFields.USER_ID, it)
+                            .findFirst()
+                            ?.devices
+
+                    val trustMap = devicesEntities?.map { device ->
+                        device to crossSigningService.checkDeviceTrust(myCrossSigningInfo, otherInfos[it], CryptoMapper.mapToModel(device))
+                    }?.toMap()
+
+                    // Update trust if needed
+                    devicesEntities?.forEach { device ->
+                        val crossSignedVerified = trustMap?.get(device)?.isCrossSignedVerified()
+                        Timber.d("## CrossSigning - Trust for ${device.userId}|${device.deviceId} : cross verified: ${trustMap?.get(device)}")
+                        if (device.trustLevelEntity?.crossSignedVerified != crossSignedVerified) {
+                            Timber.d("## CrossSigning - Trust change detected for ${device.userId}|${device.deviceId} : cross verified: $crossSignedVerified")
+                            // need to save
+                            val trustEntity = device.trustLevelEntity
+                            if (trustEntity == null) {
+                                realm.createObject(TrustLevelEntity::class.java).let {
+                                    it.locallyVerified = false
+                                    it.crossSignedVerified = crossSignedVerified
+                                    device.trustLevelEntity = it
+                                }
+                            } else {
+                                trustEntity.crossSignedVerified = crossSignedVerified
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        // So Cross Signing keys trust is updated, device trust is updated
+        // We can now update room shields? in the session DB?
+
+        Timber.d("## CrossSigning - Updating shields for impacted rooms...")
+        Realm.getInstance(sessionRealmConfiguration).use { it ->
+            it.executeTransaction { realm ->
+                val distinctRoomIds = realm.where(RoomMemberSummaryEntity::class.java)
+                        .`in`(RoomMemberSummaryEntityFields.USER_ID, userList.toTypedArray())
+                        .distinct(RoomMemberSummaryEntityFields.ROOM_ID)
+                        .findAll()
+                        .map { it.roomId }
+                Timber.d("## CrossSigning -  ... impacted rooms $distinctRoomIds")
+                distinctRoomIds.forEach { roomId ->
+                    val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
+                    if (roomSummary?.isEncrypted == true) {
+                        Timber.d("## CrossSigning - Check shield state for room $roomId")
+                        val allActiveRoomMembers = RoomMemberHelper(realm, roomId).getActiveRoomMemberIds()
+                        try {
+                            val updatedTrust = computeRoomShield(allActiveRoomMembers, roomSummary)
+                            if (roomSummary.roomEncryptionTrustLevel != updatedTrust) {
+                                Timber.d("## CrossSigning - Shield change detected for $roomId -> $updatedTrust")
+                                roomSummary.roomEncryptionTrustLevel = updatedTrust
+                            }
+                        } catch (failure: Throwable) {
+                            Timber.e(failure)
+                        }
+                    }
+                }
+            }
+        }
+
+        return Result.success()
+    }
+
+    private fun updateCrossSigningKeysTrust(realm: Realm, userId: String, verified: Boolean) {
+        val xInfoEntity = realm.where(CrossSigningInfoEntity::class.java)
+                .equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
+                .findFirst()
+        xInfoEntity?.crossSigningKeys?.forEach { info ->
+            // optimization to avoid trigger updates when there is no change..
+            if (info.trustLevelEntity?.isVerified() != verified) {
+                Timber.d("## CrossSigning - Trust change for $userId : $verified")
+                val level = info.trustLevelEntity
+                if (level == null) {
+                    val newLevel = realm.createObject(TrustLevelEntity::class.java)
+                    newLevel.locallyVerified = verified
+                    newLevel.crossSignedVerified = verified
+                    info.trustLevelEntity = newLevel
+                } else {
+                    level.locallyVerified = verified
+                    level.crossSignedVerified = verified
+                }
+            }
+        }
+    }
+
+    private fun computeRoomShield(activeMemberUserIds: List<String>, roomSummaryEntity: RoomSummaryEntity): RoomEncryptionTrustLevel {
+        Timber.d("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} -> $activeMemberUserIds")
+        // The set of “all users” depends on the type of room:
+        // For regular / topic rooms, all users including yourself, are considered when decorating a room
+        // For 1:1 and group DM rooms, all other users (i.e. excluding yourself) are considered when decorating a room
+        val listToCheck = if (roomSummaryEntity.isDirect) {
+            activeMemberUserIds.filter { it != myUserId }
+        } else {
+            activeMemberUserIds
+        }
+
+        val allTrustedUserIds = listToCheck
+                .filter { userId ->
+                    Realm.getInstance(realmConfiguration).use {
+                        it.where(CrossSigningInfoEntity::class.java)
+                                .equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
+                                .findFirst()?.let { mapCrossSigningInfoEntity(it) }?.isTrusted() == true
+                    }
+                }
+        val myCrossKeys = Realm.getInstance(realmConfiguration).use {
+            it.where(CrossSigningInfoEntity::class.java)
+                    .equalTo(CrossSigningInfoEntityFields.USER_ID, myUserId)
+                    .findFirst()?.let { mapCrossSigningInfoEntity(it) }
+        }
+
+        return if (allTrustedUserIds.isEmpty()) {
+            RoomEncryptionTrustLevel.Default
+        } else {
+            // If one of the verified user as an untrusted device -> warning
+            // If all devices of all verified users are trusted -> green
+            // else -> black
+            allTrustedUserIds
+                    .mapNotNull { uid ->
+                        Realm.getInstance(realmConfiguration).use {
+                            it.where<UserEntity>()
+                                    .equalTo(UserEntityFields.USER_ID, uid)
+                                    .findFirst()
+                                    ?.devices
+                                    ?.map {
+                                        CryptoMapper.mapToModel(it)
+                                    }
+                        }
+                    }
+                    .flatten()
+                    .let { allDevices ->
+                        Timber.v("## CrossSigning - computeRoomShield ${roomSummaryEntity.roomId} devices ${allDevices.map { it.deviceId }}")
+                        if (myCrossKeys != null) {
+                            allDevices.any { !it.trustLevel?.crossSigningVerified.orFalse() }
+                        } else {
+                            // Legacy method
+                            allDevices.any { !it.isVerified }
+                        }
+                    }
+                    .let { hasWarning ->
+                        if (hasWarning) {
+                            RoomEncryptionTrustLevel.Warning
+                        } else {
+                            if (listToCheck.size == allTrustedUserIds.size) {
+                                // all users are trusted and all devices are verified
+                                RoomEncryptionTrustLevel.Trusted
+                            } else {
+                                RoomEncryptionTrustLevel.Default
+                            }
+                        }
+                    }
+        }
+    }
+
+    private fun mapCrossSigningInfoEntity(xsignInfo: CrossSigningInfoEntity): MXCrossSigningInfo {
+        val userId = xsignInfo.userId ?: ""
+        return MXCrossSigningInfo(
+                userId = userId,
+                crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull {
+                    crossSigningKeysMapper.map(userId, it)
+                }
+        )
+    }
+
+    override fun buildErrorParams(params: Params, message: String): Params {
+        return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt
index 64579c1b67..bbd8ebd000 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
@@ -234,7 +234,10 @@ internal class DefaultKeysBackupService @Inject constructor(
                     this.callback = object : MatrixCallback<KeysVersion> {
                         override fun onSuccess(data: KeysVersion) {
                             // Reset backup markers.
-                            cryptoStore.resetBackupMarkers()
+                            cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
+                                // move tx out of UI thread
+                                cryptoStore.resetBackupMarkers()
+                            }
 
                             val keyBackupVersion = KeysVersionResult(
                                     algorithm = createKeysBackupVersionBody.algorithm,
@@ -596,7 +599,9 @@ internal class DefaultKeysBackupService @Inject constructor(
                     val importResult = awaitCallback<ImportRoomKeysResult> {
                         restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, null, null, null, it)
                     }
-                    cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version)
+                    withContext(coroutineDispatchers.crypto) {
+                        cryptoStore.saveBackupRecoveryKey(recoveryKey, keysBackupVersion.version)
+                    }
                     Timber.i("onSecretKeyGossip: Recovered keys ${importResult.successfullyNumberOfImportedKeys} out of ${importResult.totalNumberOfKeys}")
                 } else {
                     Timber.e("onSecretKeyGossip: Recovery key is not valid ${keysBackupVersion.version}")
@@ -1123,7 +1128,9 @@ internal class DefaultKeysBackupService @Inject constructor(
 
             if (retrievedMegolmBackupAuthData != null) {
                 keysBackupVersion = keysVersionResult
-                cryptoStore.setKeyBackupVersion(keysVersionResult.version)
+                cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
+                    cryptoStore.setKeyBackupVersion(keysVersionResult.version)
+                }
 
                 onServerDataRetrieved(keysVersionResult.count, keysVersionResult.hash)
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt
index 1b88fbe9cc..56b267decd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/EncryptEventTask.kt
@@ -33,14 +33,13 @@ internal interface EncryptEventTask : Task<EncryptEventTask.Params, Event> {
     data class Params(val roomId: String,
                       val event: Event,
                       /**Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to)*/
-                      val keepKeys: List<String>? = null,
-                      val crypto: CryptoService
+                      val keepKeys: List<String>? = null
     )
 }
 
 internal class DefaultEncryptEventTask @Inject constructor(
-//        private val crypto: CryptoService
-        private val localEchoRepository: LocalEchoRepository
+        private val localEchoRepository: LocalEchoRepository,
+        private val cryptoService: CryptoService
 ) : EncryptEventTask {
     override suspend fun execute(params: EncryptEventTask.Params): Event {
         // don't want to wait for any query
@@ -60,7 +59,7 @@ internal class DefaultEncryptEventTask @Inject constructor(
 //        try {
         // let it throws
         awaitCallback<MXEncryptEventContentResult> {
-            params.crypto.encryptEventContent(localMutableContent, localEvent.type, params.roomId, it)
+            cryptoService.encryptEventContent(localMutableContent, localEvent.type, params.roomId, it)
         }.let { result ->
             val modifiedContent = HashMap(result.eventContent)
             params.keepKeys?.forEach { toKeep ->
@@ -81,7 +80,7 @@ internal class DefaultEncryptEventTask @Inject constructor(
                         ).toContent(),
                         forwardingCurve25519KeyChain = emptyList(),
                         senderCurve25519Key = result.eventContent["sender_key"] as? String,
-                        claimedEd25519Key = params.crypto.getMyDevice().fingerprint()
+                        claimedEd25519Key = cryptoService.getMyDevice().fingerprint()
                 )
             } else {
                 null
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt
index 1a712036c8..8b739c4b64 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt
@@ -16,7 +16,6 @@
 package org.matrix.android.sdk.internal.crypto.tasks
 
 import org.greenrobot.eventbus.EventBus
-import org.matrix.android.sdk.api.session.crypto.CryptoService
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.room.send.SendState
 import org.matrix.android.sdk.internal.network.executeRequest
@@ -29,8 +28,7 @@ import javax.inject.Inject
 internal interface SendEventTask : Task<SendEventTask.Params, String> {
     data class Params(
             val event: Event,
-            val encrypt: Boolean,
-            val cryptoService: CryptoService?
+            val encrypt: Boolean
     )
 }
 
@@ -68,8 +66,7 @@ internal class DefaultSendEventTask @Inject constructor(
             return encryptEventTask.execute(EncryptEventTask.Params(
                     params.event.roomId ?: "",
                     params.event,
-                    listOf("m.relates_to"),
-                    params.cryptoService!!
+                    listOf("m.relates_to")
             ))
         }
         return params.event
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt
index 782300c7b0..cedb7a6618 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt
@@ -15,21 +15,20 @@
  */
 package org.matrix.android.sdk.internal.crypto.tasks
 
-import org.matrix.android.sdk.api.session.crypto.CryptoService
+import org.greenrobot.eventbus.EventBus
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.room.send.SendState
+import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
 import org.matrix.android.sdk.internal.network.executeRequest
 import org.matrix.android.sdk.internal.session.room.RoomAPI
 import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
 import org.matrix.android.sdk.internal.session.room.send.SendResponse
 import org.matrix.android.sdk.internal.task.Task
-import org.greenrobot.eventbus.EventBus
 import javax.inject.Inject
 
 internal interface SendVerificationMessageTask : Task<SendVerificationMessageTask.Params, String> {
     data class Params(
-            val event: Event,
-            val cryptoService: CryptoService?
+            val event: Event
     )
 }
 
@@ -37,6 +36,7 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
         private val localEchoRepository: LocalEchoRepository,
         private val encryptEventTask: DefaultEncryptEventTask,
         private val roomAPI: RoomAPI,
+        private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
         private val eventBus: EventBus) : SendVerificationMessageTask {
 
     override suspend fun execute(params: SendVerificationMessageTask.Params): String {
@@ -62,13 +62,12 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
     }
 
     private suspend fun handleEncryption(params: SendVerificationMessageTask.Params): Event {
-        if (params.cryptoService?.isRoomEncrypted(params.event.roomId ?: "") == true) {
+        if (cryptoSessionInfoProvider.isRoomEncrypted(params.event.roomId ?: "")) {
             try {
                 return encryptEventTask.execute(EncryptEventTask.Params(
                         params.event.roomId ?: "",
                         params.event,
-                        listOf("m.relates_to"),
-                        params.cryptoService
+                        listOf("m.relates_to")
                 ))
             } catch (throwable: Throwable) {
                 // We said it's ok to send verification request in clear
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 c0f4671046..f3a794154c 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
@@ -20,7 +20,6 @@ import android.os.Handler
 import android.os.Looper
 import dagger.Lazy
 import org.matrix.android.sdk.api.MatrixCallback
-import org.matrix.android.sdk.api.session.crypto.CryptoService
 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
@@ -111,9 +110,6 @@ internal class DefaultVerificationService @Inject constructor(
 
     private val uiHandler = Handler(Looper.getMainLooper())
 
-    // Cannot be injected in constructor as it creates a dependency cycle
-    lateinit var cryptoService: CryptoService
-
     // map [sender : [transaction]]
     private val txMap = HashMap<String, HashMap<String, DefaultVerificationTransaction>>()
 
@@ -1203,7 +1199,9 @@ internal class DefaultVerificationService @Inject constructor(
         // TODO refactor this with the DM one
         Timber.i("## Requesting verification to user: $otherUserId with device list $otherDevices")
 
-        val targetDevices = otherDevices ?: cryptoService.getUserDevices(otherUserId).map { it.deviceId }
+        val targetDevices = otherDevices ?: cryptoStore.getUserDevices(otherUserId)
+                ?.values?.map { it.deviceId } ?: emptyList()
+
         val requestsForUser = pendingRequests.getOrPut(otherUserId) { mutableListOf() }
 
         val transport = verificationTransportToDeviceFactory.createTransport(null)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt
index fa7cd2e6f9..538d7b56e9 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SendVerificationMessageWorker.kt
@@ -20,7 +20,6 @@ import androidx.work.Data
 import androidx.work.WorkerParameters
 import com.squareup.moshi.JsonClass
 import org.matrix.android.sdk.api.failure.shouldBeRetried
-import org.matrix.android.sdk.api.session.crypto.CryptoService
 import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask
 import org.matrix.android.sdk.internal.session.SessionComponent
 import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker
@@ -47,7 +46,6 @@ internal class SendVerificationMessageWorker(context: Context,
 
     @Inject lateinit var sendVerificationMessageTask: SendVerificationMessageTask
     @Inject lateinit var localEchoRepository: LocalEchoRepository
-    @Inject lateinit var cryptoService: CryptoService
     @Inject lateinit var cancelSendTracker: CancelSendTracker
 
     override fun injectWith(injector: SessionComponent) {
@@ -70,8 +68,7 @@ internal class SendVerificationMessageWorker(context: Context,
         return try {
             val resultEventId = sendVerificationMessageTask.execute(
                     SendVerificationMessageTask.Params(
-                            event = localEvent,
-                            cryptoService = cryptoService
+                            event = localEvent
                     )
             )
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt
index 4994325625..74827eeb2a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt
@@ -15,7 +15,6 @@
  */
 package org.matrix.android.sdk.internal.crypto.verification
 
-import org.matrix.android.sdk.api.session.crypto.CryptoService
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
 import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
 import org.matrix.android.sdk.api.session.events.model.Event
@@ -34,12 +33,13 @@ import org.matrix.android.sdk.internal.di.DeviceId
 import org.matrix.android.sdk.internal.di.UserId
 import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
 import io.realm.Realm
+import org.matrix.android.sdk.internal.crypto.EventDecryptor
 import timber.log.Timber
 import java.util.ArrayList
 import javax.inject.Inject
 
 internal class VerificationMessageProcessor @Inject constructor(
-        private val cryptoService: CryptoService,
+        private val eventDecryptor: EventDecryptor,
         private val verificationService: DefaultVerificationService,
         @UserId private val userId: String,
         @DeviceId private val deviceId: String?
@@ -82,7 +82,7 @@ internal class VerificationMessageProcessor @Inject constructor(
             // TODO use a global event decryptor? attache to session and that listen to new sessionId?
             // for now decrypt sync
             try {
-                val result = cryptoService.decryptEvent(event, "")
+                val result = eventDecryptor.decryptEvent(event, "")
                 event.mxDecryptionResult = OlmDecryptionResult(
                         payload = result.clearEvent,
                         senderKey = result.senderCurve25519Key,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt
index 11a877e7c4..71f978c03c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt
@@ -17,10 +17,6 @@
 package org.matrix.android.sdk.internal.database
 
 import com.zhuinden.monarchy.Monarchy
-import org.matrix.android.sdk.api.session.crypto.CryptoService
-import org.matrix.android.sdk.api.session.crypto.MXCryptoError
-import org.matrix.android.sdk.api.session.events.model.Event
-import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
 import org.matrix.android.sdk.internal.database.mapper.asDomain
 import org.matrix.android.sdk.internal.database.model.EventEntity
 import org.matrix.android.sdk.internal.database.model.EventInsertEntity
@@ -31,12 +27,13 @@ import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
 import io.realm.RealmConfiguration
 import io.realm.RealmResults
 import kotlinx.coroutines.launch
+import org.matrix.android.sdk.internal.crypto.EventDecryptor
 import timber.log.Timber
 import javax.inject.Inject
 
 internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration,
                                                            private val processors: Set<@JvmSuppressWildcards EventInsertLiveProcessor>,
-                                                           private val cryptoService: CryptoService)
+                                                           private val eventDecryptor: EventDecryptor)
     : RealmLiveEntityObserver<EventInsertEntity>(realmConfiguration) {
 
     override val query = Monarchy.Query<EventInsertEntity> {
@@ -74,7 +71,7 @@ internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase real
                         return@forEach
                     }
                     val domainEvent = event.asDomain()
-                    decryptIfNeeded(domainEvent)
+//                    decryptIfNeeded(domainEvent)
                     processors.filter {
                         it.shouldProcess(eventId, domainEvent.getClearType(), eventInsert.insertType)
                     }.forEach {
@@ -89,22 +86,22 @@ internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase real
         }
     }
 
-    private fun decryptIfNeeded(event: Event) {
-        if (event.isEncrypted() && event.mxDecryptionResult == null) {
-            try {
-                val result = cryptoService.decryptEvent(event, event.roomId ?: "")
-                event.mxDecryptionResult = OlmDecryptionResult(
-                        payload = result.clearEvent,
-                        senderKey = result.senderCurve25519Key,
-                        keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
-                        forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
-                )
-            } catch (e: MXCryptoError) {
-                Timber.v("Failed to decrypt event")
-                // TODO -> we should keep track of this and retry, or some processing will never be handled
-            }
-        }
-    }
+//    private fun decryptIfNeeded(event: Event) {
+//        if (event.isEncrypted() && event.mxDecryptionResult == null) {
+//            try {
+//                val result = eventDecryptor.decryptEvent(event, event.roomId ?: "")
+//                event.mxDecryptionResult = OlmDecryptionResult(
+//                        payload = result.clearEvent,
+//                        senderKey = result.senderCurve25519Key,
+//                        keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
+//                        forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
+//                )
+//            } catch (e: MXCryptoError) {
+//                Timber.v("Failed to decrypt event")
+//                // TODO -> we should keep track of this and retry, or some processing will never be handled
+//            }
+//        }
+//    }
 
     private fun shouldProcess(eventInsertEntity: EventInsertEntity): Boolean {
         return processors.any {
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 359dd265d8..7e182525a9 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
@@ -65,7 +65,6 @@ import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate
 import org.matrix.android.sdk.internal.di.WorkManagerProvider
 import org.matrix.android.sdk.internal.session.identity.DefaultIdentityService
 import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
-import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor
 import org.matrix.android.sdk.internal.session.sync.SyncTokenStore
 import org.matrix.android.sdk.internal.session.sync.job.SyncThread
 import org.matrix.android.sdk.internal.session.sync.job.SyncWorker
@@ -115,7 +114,6 @@ internal class DefaultSession @Inject constructor(
         private val accountDataService: Lazy<AccountDataService>,
         private val _sharedSecretStorageService: Lazy<SharedSecretStorageService>,
         private val accountService: Lazy<AccountService>,
-        private val timelineEventDecryptor: TimelineEventDecryptor,
         private val coroutineDispatchers: MatrixCoroutineDispatchers,
         private val defaultIdentityService: DefaultIdentityService,
         private val integrationManagerService: IntegrationManagerService,
@@ -162,7 +160,6 @@ internal class DefaultSession @Inject constructor(
             lifecycleObservers.forEach { it.onStart() }
         }
         eventBus.register(this)
-        timelineEventDecryptor.start()
         eventSenderProcessor.start()
     }
 
@@ -200,7 +197,7 @@ internal class DefaultSession @Inject constructor(
     override fun close() {
         assert(isOpen)
         stopSync()
-        timelineEventDecryptor.destroy()
+       // timelineEventDecryptor.destroy()
         uiHandler.post {
             lifecycleObservers.forEach { it.onStop() }
         }
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 ed586d35f8..e6fd5a7a0c 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
@@ -24,6 +24,7 @@ import org.matrix.android.sdk.internal.crypto.CancelGossipRequestWorker
 import org.matrix.android.sdk.internal.crypto.CryptoModule
 import org.matrix.android.sdk.internal.crypto.SendGossipRequestWorker
 import org.matrix.android.sdk.internal.crypto.SendGossipWorker
+import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker
 import org.matrix.android.sdk.internal.crypto.verification.SendVerificationMessageWorker
 import org.matrix.android.sdk.internal.di.MatrixComponent
 import org.matrix.android.sdk.internal.di.SessionAssistedInjectModule
@@ -128,6 +129,8 @@ internal interface SessionComponent {
 
     fun inject(worker: SendGossipWorker)
 
+    fun inject(worker: UpdateTrustWorker)
+
     @Component.Factory
     interface Factory {
         fun create(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt
index 102a34d9de..32949d60c4 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt
@@ -41,7 +41,6 @@ import org.matrix.android.sdk.api.session.permalinks.PermalinkService
 import org.matrix.android.sdk.api.session.securestorage.SecureStorageService
 import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
 import org.matrix.android.sdk.api.session.typing.TypingUsersTracker
-import org.matrix.android.sdk.internal.crypto.crosssigning.ShieldTrustUpdater
 import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService
 import org.matrix.android.sdk.internal.crypto.tasks.DefaultRedactEventTask
 import org.matrix.android.sdk.internal.crypto.tasks.RedactEventTask
@@ -333,10 +332,6 @@ internal abstract class SessionModule {
     @IntoSet
     abstract fun bindWidgetUrlFormatter(formatter: DefaultWidgetURLFormatter): SessionLifecycleObserver
 
-    @Binds
-    @IntoSet
-    abstract fun bindShieldTrustUpdated(updater: ShieldTrustUpdater): SessionLifecycleObserver
-
     @Binds
     @IntoSet
     abstract fun bindIdentityService(service: DefaultIdentityService): SessionLifecycleObserver
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
index 9ff0deec89..d090ba5296 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
@@ -15,7 +15,7 @@
  */
 package org.matrix.android.sdk.internal.session.room
 
-import org.matrix.android.sdk.api.session.crypto.CryptoService
+import io.realm.Realm
 import org.matrix.android.sdk.api.session.events.model.AggregatedAnnotation
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
@@ -47,7 +47,6 @@ import org.matrix.android.sdk.internal.database.query.getOrCreate
 import org.matrix.android.sdk.internal.database.query.where
 import org.matrix.android.sdk.internal.di.UserId
 import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
-import io.realm.Realm
 import timber.log.Timber
 import javax.inject.Inject
 
@@ -78,9 +77,8 @@ private fun VerificationState?.toState(newState: VerificationState): Verificatio
     return newState
 }
 
-internal class EventRelationsAggregationProcessor @Inject constructor(@UserId private val userId: String,
-                                                                      private val cryptoService: CryptoService
-) : EventInsertLiveProcessor {
+internal class EventRelationsAggregationProcessor @Inject constructor(@UserId private val userId: String)
+    : EventInsertLiveProcessor {
 
     private val allowedTypes = listOf(
             EventType.MESSAGE,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt
index 38c542e07e..a7f3f83980 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt
@@ -21,7 +21,6 @@ import com.squareup.inject.assisted.Assisted
 import com.squareup.inject.assisted.AssistedInject
 import com.zhuinden.monarchy.Monarchy
 import org.matrix.android.sdk.api.MatrixCallback
-import org.matrix.android.sdk.api.session.crypto.CryptoService
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
 import org.matrix.android.sdk.api.session.room.model.message.MessageType
@@ -31,13 +30,13 @@ import org.matrix.android.sdk.api.util.Cancelable
 import org.matrix.android.sdk.api.util.NoOpCancellable
 import org.matrix.android.sdk.api.util.Optional
 import org.matrix.android.sdk.api.util.toOptional
+import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
 import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
 import org.matrix.android.sdk.internal.database.mapper.asDomain
 import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
 import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
 import org.matrix.android.sdk.internal.database.query.where
 import org.matrix.android.sdk.internal.di.SessionDatabase
-import org.matrix.android.sdk.internal.di.SessionId
 import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
 import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
 import org.matrix.android.sdk.internal.task.TaskExecutor
@@ -47,11 +46,9 @@ import timber.log.Timber
 
 internal class DefaultRelationService @AssistedInject constructor(
         @Assisted private val roomId: String,
-        @SessionId private val sessionId: String,
-//        private val timeLineSendEventWorkCommon: TimelineSendEventWorkCommon,
         private val eventSenderProcessor: EventSenderProcessor,
         private val eventFactory: LocalEchoEventFactory,
-        private val cryptoService: CryptoService,
+        private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
         private val findReactionEventForUndoTask: FindReactionEventForUndoTask,
         private val fetchEditHistoryTask: FetchEditHistoryTask,
         private val timelineEventMapper: TimelineEventMapper,
@@ -122,7 +119,7 @@ internal class DefaultRelationService @AssistedInject constructor(
         val event = eventFactory
                 .createReplaceTextEvent(roomId, targetEventId, newBodyText, newBodyAutoMarkdown, msgType, compatibilityBodyText)
                 .also { saveLocalEcho(it) }
-        return eventSenderProcessor.postEvent(event, cryptoService.isRoomEncrypted(roomId))
+        return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
     }
 
     override fun editReply(replyToEdit: TimelineEvent,
@@ -139,11 +136,11 @@ internal class DefaultRelationService @AssistedInject constructor(
                 compatibilityBodyText
         )
                 .also { saveLocalEcho(it) }
-        return eventSenderProcessor.postEvent(event, cryptoService.isRoomEncrypted(roomId))
+        return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
     }
 
     override fun fetchEditHistory(eventId: String, callback: MatrixCallback<List<Event>>) {
-        val params = FetchEditHistoryTask.Params(roomId, cryptoService.isRoomEncrypted(roomId), eventId)
+        val params = FetchEditHistoryTask.Params(roomId, cryptoSessionInfoProvider.isRoomEncrypted(roomId), eventId)
         fetchEditHistoryTask
                 .configureWith(params) {
                     this.callback = callback
@@ -156,7 +153,7 @@ internal class DefaultRelationService @AssistedInject constructor(
                 ?.also { saveLocalEcho(it) }
                 ?: return null
 
-        return eventSenderProcessor.postEvent(event, cryptoService.isRoomEncrypted(roomId))
+        return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(roomId))
     }
 
     override fun getEventAnnotationsSummary(eventId: String): EventAnnotationsSummary? {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
index 5c395c1907..b13ce15da6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
@@ -25,7 +25,6 @@ import com.squareup.inject.assisted.Assisted
 import com.squareup.inject.assisted.AssistedInject
 import kotlinx.coroutines.launch
 import org.matrix.android.sdk.api.session.content.ContentAttachmentData
-import org.matrix.android.sdk.api.session.crypto.CryptoService
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage
 import org.matrix.android.sdk.api.session.events.model.isTextMessage
@@ -45,6 +44,7 @@ import org.matrix.android.sdk.api.util.Cancelable
 import org.matrix.android.sdk.api.util.CancelableBag
 import org.matrix.android.sdk.api.util.JsonDict
 import org.matrix.android.sdk.api.util.NoOpCancellable
+import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider
 import org.matrix.android.sdk.internal.di.SessionId
 import org.matrix.android.sdk.internal.di.WorkManagerProvider
 import org.matrix.android.sdk.internal.session.content.UploadContentWorker
@@ -64,7 +64,7 @@ internal class DefaultSendService @AssistedInject constructor(
         private val workManagerProvider: WorkManagerProvider,
         @SessionId private val sessionId: String,
         private val localEchoEventFactory: LocalEchoEventFactory,
-        private val cryptoService: CryptoService,
+        private val cryptoSessionInfoProvider: CryptoSessionInfoProvider,
         private val taskExecutor: TaskExecutor,
         private val localEchoRepository: LocalEchoRepository,
         private val eventSenderProcessor: EventSenderProcessor,
@@ -251,7 +251,7 @@ internal class DefaultSendService @AssistedInject constructor(
     private fun internalSendMedia(allLocalEchoes: List<Event>, attachment: ContentAttachmentData, compressBeforeSending: Boolean): Cancelable {
         val cancelableBag = CancelableBag()
 
-        allLocalEchoes.groupBy { cryptoService.isRoomEncrypted(it.roomId!!) }
+        allLocalEchoes.groupBy { cryptoSessionInfoProvider.isRoomEncrypted(it.roomId!!) }
                 .apply {
                     keys.forEach { isRoomEncrypted ->
                         // Should never be empty
@@ -282,7 +282,7 @@ internal class DefaultSendService @AssistedInject constructor(
     }
 
     private fun sendEvent(event: Event): Cancelable {
-        return eventSenderProcessor.postEvent(event, cryptoService.isRoomEncrypted(event.roomId!!))
+        return eventSenderProcessor.postEvent(event, cryptoSessionInfoProvider.isRoomEncrypted(event.roomId!!))
     }
 
     private fun createLocalEcho(event: Event) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt
index 2c835ff56c..37a429d242 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt
@@ -87,7 +87,7 @@ internal class SendEventWorker(context: Context,
 
         Timber.v("## SendEvent: [${System.currentTimeMillis()}] Send event ${params.eventId}")
         return try {
-            sendEventTask.execute(SendEventTask.Params(event, params.isEncrypted ?: cryptoService.isRoomEncrypted(event.roomId), cryptoService))
+            sendEventTask.execute(SendEventTask.Params(event, params.isEncrypted ?: cryptoService.isRoomEncrypted(event.roomId)))
             Result.success()
         } catch (exception: Throwable) {
             if (/*currentAttemptCount >= MAX_NUMBER_OF_RETRY_BEFORE_FAILING ||**/ !exception.shouldBeRetried()) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/SendEventQueuedTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/SendEventQueuedTask.kt
index 09da0908f9..21a4145a9d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/SendEventQueuedTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/SendEventQueuedTask.kt
@@ -38,7 +38,7 @@ internal class SendEventQueuedTask(
     override fun toString() = "[SendEventRunnableTask ${event.eventId}]"
 
     override suspend fun execute() {
-        sendEventTask.execute(SendEventTask.Params(event, encrypt, cryptoService))
+        sendEventTask.execute(SendEventTask.Params(event, encrypt))
     }
 
     override fun onTaskFailed() {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt
index f9a27c367c..8c71604183 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt
@@ -16,10 +16,8 @@
 
 package org.matrix.android.sdk.internal.session.room.summary
 
-import dagger.Lazy
 import io.realm.Realm
-import org.greenrobot.eventbus.EventBus
-import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
+import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.room.model.Membership
@@ -28,9 +26,11 @@ import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent
 import org.matrix.android.sdk.api.session.room.model.RoomNameContent
 import org.matrix.android.sdk.api.session.room.model.RoomTopicContent
 import org.matrix.android.sdk.api.session.room.send.SendState
+import org.matrix.android.sdk.internal.crypto.EventDecryptor
 import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
-import org.matrix.android.sdk.internal.crypto.crosssigning.SessionToCryptoRoomMembersUpdate
+import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService
 import org.matrix.android.sdk.internal.database.mapper.ContentMapper
+import org.matrix.android.sdk.internal.database.mapper.asDomain
 import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
 import org.matrix.android.sdk.internal.database.model.EventEntity
 import org.matrix.android.sdk.internal.database.model.EventEntityFields
@@ -46,7 +46,6 @@ import org.matrix.android.sdk.internal.di.UserId
 import org.matrix.android.sdk.internal.session.room.RoomAvatarResolver
 import org.matrix.android.sdk.internal.session.room.membership.RoomDisplayNameResolver
 import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
-import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor
 import org.matrix.android.sdk.internal.session.sync.model.RoomSyncSummary
 import org.matrix.android.sdk.internal.session.sync.model.RoomSyncUnreadNotifications
 import timber.log.Timber
@@ -56,8 +55,8 @@ internal class RoomSummaryUpdater @Inject constructor(
         @UserId private val userId: String,
         private val roomDisplayNameResolver: RoomDisplayNameResolver,
         private val roomAvatarResolver: RoomAvatarResolver,
-        private val timelineEventDecryptor: Lazy<TimelineEventDecryptor>,
-        private val eventBus: EventBus) {
+        private val eventDecryptor: EventDecryptor,
+        private val crossSigningService: DefaultCrossSigningService) {
 
     fun update(realm: Realm,
                roomId: String,
@@ -126,9 +125,14 @@ internal class RoomSummaryUpdater @Inject constructor(
         }
         roomSummaryEntity.updateHasFailedSending()
 
-        if (latestPreviewableEvent?.root?.type == EventType.ENCRYPTED && latestPreviewableEvent.root?.decryptionResultJson == null) {
+        val root = latestPreviewableEvent?.root
+        if (root?.type == EventType.ENCRYPTED && root.decryptionResultJson == null) {
             Timber.v("Should decrypt ${latestPreviewableEvent.eventId}")
-            timelineEventDecryptor.get().requestDecryption(TimelineEventDecryptor.DecryptionRequest(latestPreviewableEvent.eventId, ""))
+            // mmm i want to decrypt now or is it ok to do it async?
+            tryOrNull {
+                eventDecryptor.decryptEvent(root.asDomain(), "")
+                // eventDecryptor.decryptEventAsync(root.asDomain(), "", NoOpMatrixCallback())
+            }
         }
 
         if (updateMembers) {
@@ -142,7 +146,8 @@ internal class RoomSummaryUpdater @Inject constructor(
             roomSummaryEntity.otherMemberIds.clear()
             roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers)
             if (roomSummaryEntity.isEncrypted) {
-                eventBus.post(SessionToCryptoRoomMembersUpdate(roomId, roomSummaryEntity.isDirect, roomSummaryEntity.otherMemberIds.toList() + userId))
+                // mmm maybe we could only refresh shield instead of checking trust also?
+                crossSigningService.onUsersDeviceUpdate(roomSummaryEntity.otherMemberIds.toList())
             }
         }
     }
@@ -156,13 +161,4 @@ internal class RoomSummaryUpdater @Inject constructor(
         roomSummaryEntity.updateHasFailedSending()
         roomSummaryEntity.latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
     }
-
-    fun updateShieldTrust(realm: Realm,
-                          roomId: String,
-                          trust: RoomEncryptionTrustLevel?) {
-        val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId)
-        if (roomSummaryEntity.isEncrypted) {
-            roomSummaryEntity.roomEncryptionTrustLevel = trust
-        }
-    }
 }
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 1e5c3600d3..f720a4ede0 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
@@ -630,7 +630,7 @@ internal class DefaultTimeline(
 
             if (timelineEvent.isEncrypted()
                     && timelineEvent.root.mxDecryptionResult == null) {
-                timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(it, timelineID)) }
+                timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineID)) }
             }
 
             val position = if (direction == Timeline.Direction.FORWARDS) 0 else builtEvents.size
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt
index e91487eab0..3517f26c5d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDecryptor.kt
@@ -15,24 +15,22 @@
  */
 package org.matrix.android.sdk.internal.session.room.timeline
 
+import io.realm.Realm
+import io.realm.RealmConfiguration
 import org.matrix.android.sdk.api.session.crypto.CryptoService
 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.toModel
 import org.matrix.android.sdk.internal.crypto.NewSessionListener
 import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
-import org.matrix.android.sdk.internal.database.mapper.asDomain
 import org.matrix.android.sdk.internal.database.model.EventEntity
 import org.matrix.android.sdk.internal.database.query.where
 import org.matrix.android.sdk.internal.di.SessionDatabase
-import org.matrix.android.sdk.internal.session.SessionScope
-import io.realm.Realm
-import io.realm.RealmConfiguration
 import timber.log.Timber
 import java.util.concurrent.ExecutorService
 import java.util.concurrent.Executors
 import javax.inject.Inject
 
-@SessionScope
 internal class TimelineEventDecryptor @Inject constructor(
         @SessionDatabase
         private val realmConfiguration: RealmConfiguration,
@@ -83,14 +81,14 @@ internal class TimelineEventDecryptor @Inject constructor(
         synchronized(unknownSessionsFailure) {
             for (requests in unknownSessionsFailure.values) {
                 if (request in requests) {
-                    Timber.d("Skip Decryption request for event ${request.eventId}, unknown session")
+                    Timber.d("Skip Decryption request for event ${request.event.eventId}, unknown session")
                     return
                 }
             }
         }
         synchronized(existingRequests) {
             if (!existingRequests.add(request)) {
-                Timber.d("Skip Decryption request for event ${request.eventId}, already requested")
+                Timber.d("Skip Decryption request for event ${request.event.eventId}, already requested")
                 return
             }
         }
@@ -101,25 +99,29 @@ internal class TimelineEventDecryptor @Inject constructor(
         }
     }
 
-    private fun processDecryptRequest(request: DecryptionRequest, realm: Realm) = realm.executeTransaction {
-        val eventId = request.eventId
+    private fun processDecryptRequest(request: DecryptionRequest, realm: Realm) {
+        val event = request.event
         val timelineId = request.timelineId
-        Timber.v("Decryption request for event $eventId")
-        val eventEntity = EventEntity.where(realm, eventId = eventId).findFirst()
-                ?: return@executeTransaction Unit.also {
-                    Timber.d("Decryption request for unknown message")
-                }
-        val event = eventEntity.asDomain()
         try {
-            val result = cryptoService.decryptEvent(event, timelineId)
-            Timber.v("Successfully decrypted event $eventId")
-            eventEntity.setDecryptionResult(result)
+            val result = cryptoService.decryptEvent(request.event, timelineId)
+            Timber.v("Successfully decrypted event ${event.eventId}")
+            realm.executeTransaction {
+                EventEntity.where(it, eventId = event.eventId ?: "")
+                        .findFirst()
+                        ?.setDecryptionResult(result)
+            }
         } catch (e: MXCryptoError) {
-            Timber.v(e, "Failed to decrypt event $eventId")
+            Timber.v("Failed to decrypt event ${event.eventId} : ${e.localizedMessage}")
             if (e is MXCryptoError.Base /*&& e.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID*/) {
                 // Keep track of unknown sessions to automatically try to decrypt on new session
-                eventEntity.decryptionErrorCode = e.errorType.name
-                eventEntity.decryptionErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
+                realm.executeTransaction {
+                    EventEntity.where(it, eventId = event.eventId ?: "")
+                            .findFirst()
+                            ?.let {
+                                it.decryptionErrorCode = e.errorType.name
+                                it.decryptionErrorReason = e.technicalMessage.takeIf { it.isNotEmpty() } ?: e.detailedErrorDescription
+                            }
+                }
                 event.content?.toModel<EncryptedEventContent>()?.let { content ->
                     content.sessionId?.let { sessionId ->
                         synchronized(unknownSessionsFailure) {
@@ -130,7 +132,7 @@ internal class TimelineEventDecryptor @Inject constructor(
                 }
             }
         } catch (t: Throwable) {
-            Timber.e("Failed to decrypt event $eventId, ${t.localizedMessage}")
+            Timber.e("Failed to decrypt event ${event.eventId}, ${t.localizedMessage}")
         } finally {
             synchronized(existingRequests) {
                 existingRequests.remove(request)
@@ -139,7 +141,7 @@ internal class TimelineEventDecryptor @Inject constructor(
     }
 
     data class DecryptionRequest(
-            val eventId: String,
+            val event: Event,
             val timelineId: String
     )
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt
index 8589889b30..b1b2f65dc2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt
@@ -16,6 +16,9 @@
 
 package org.matrix.android.sdk.internal.session.sync
 
+import io.realm.Realm
+import io.realm.kotlin.createObject
+import org.greenrobot.eventbus.EventBus
 import org.matrix.android.sdk.R
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
 import org.matrix.android.sdk.api.session.events.model.Event
@@ -54,16 +57,12 @@ import org.matrix.android.sdk.internal.session.room.read.FullyReadContent
 import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater
 import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimeline
 import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
-import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor
 import org.matrix.android.sdk.internal.session.room.typing.TypingEventContent
 import org.matrix.android.sdk.internal.session.sync.model.InvitedRoomSync
 import org.matrix.android.sdk.internal.session.sync.model.RoomSync
 import org.matrix.android.sdk.internal.session.sync.model.RoomSyncAccountData
 import org.matrix.android.sdk.internal.session.sync.model.RoomSyncEphemeral
 import org.matrix.android.sdk.internal.session.sync.model.RoomsSyncResponse
-import io.realm.Realm
-import io.realm.kotlin.createObject
-import org.greenrobot.eventbus.EventBus
 import timber.log.Timber
 import javax.inject.Inject
 
@@ -76,8 +75,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
                                                    private val roomTypingUsersHandler: RoomTypingUsersHandler,
                                                    private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
                                                    @UserId private val userId: String,
-                                                   private val eventBus: EventBus,
-                                                   private val timelineEventDecryptor: TimelineEventDecryptor) {
+                                                   private val eventBus: EventBus) {
 
     sealed class HandlingStrategy {
         data class JOINED(val data: Map<String, RoomSync>) : HandlingStrategy()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt
index cfd7865269..74cba5e796 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt
@@ -54,7 +54,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
                                               private val networkConnectivityChecker: NetworkConnectivityChecker,
                                               private val backgroundDetectionObserver: BackgroundDetectionObserver,
                                               private val activeCallHandler: ActiveCallHandler
-) : Thread(), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {
+) : Thread("SyncThread"), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {
 
     private var state: SyncState = SyncState.Idle
     private var liveState = MutableLiveData<SyncState>(state)

From aa238775c63cc88dd66358af136019aec9da5402 Mon Sep 17 00:00:00 2001
From: Valere <valeref@matrix.org>
Date: Thu, 22 Oct 2020 17:19:13 +0200
Subject: [PATCH 02/23] Fix copyrights

---
 .../android/sdk/internal/crypto/CryptoSessionInfoProvider.kt    | 2 +-
 .../org/matrix/android/sdk/internal/crypto/EventDecryptor.kt    | 2 +-
 .../android/sdk/internal/crypto/InboundGroupSessionStore.kt     | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt
index 68cc139521..833168bf3e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2020 New Vector Ltd
+ * Copyright 2020 The Matrix.org Foundation C.I.C.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
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 b423b5d738..38488f1ca7 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
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2020 New Vector Ltd
+ * Copyright 2020 The Matrix.org Foundation C.I.C.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
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 722fc0d92d..a57b66cd4e 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
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2020 New Vector Ltd
+ * Copyright 2020 The Matrix.org Foundation C.I.C.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.

From f39938d17d189d31e34b6bf84259c967c1814dbd Mon Sep 17 00:00:00 2001
From: Valere <valeref@matrix.org>
Date: Fri, 23 Oct 2020 13:51:29 +0200
Subject: [PATCH 03/23] Fix / device check was not reading up to date trust

---
 .../sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt   | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt
index e3ac387ddc..f28fe7d642 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt
@@ -152,7 +152,11 @@ internal class UpdateTrustWorker(context: Context,
                             ?.devices
 
                     val trustMap = devicesEntities?.map { device ->
-                        device to crossSigningService.checkDeviceTrust(myCrossSigningInfo, otherInfos[it], CryptoMapper.mapToModel(device))
+                        // get up to date from DB has could have been updated
+                        val otherInfo = realm.where(CrossSigningInfoEntity::class.java)
+                                .equalTo(CrossSigningInfoEntityFields.USER_ID, it)
+                                .findFirst()?.let { mapCrossSigningInfoEntity(it) }
+                        device to crossSigningService.checkDeviceTrust(myCrossSigningInfo, otherInfo, CryptoMapper.mapToModel(device))
                     }?.toMap()
 
                     // Update trust if needed

From b9fdc14e182f019c8b35e3578dcc65da9535d7e0 Mon Sep 17 00:00:00 2001
From: Valere <valeref@matrix.org>
Date: Fri, 23 Oct 2020 13:51:35 +0200
Subject: [PATCH 04/23] add internal

---
 .../android/sdk/internal/crypto/CryptoSessionInfoProvider.kt    | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt
index 833168bf3e..225a9af1ec 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt
@@ -30,7 +30,7 @@ import javax.inject.Inject
  * The crypto module needs some information regarding rooms that are stored
  * in the session DB, this class encapsulate this functionality
  */
-class CryptoSessionInfoProvider @Inject constructor(
+internal class CryptoSessionInfoProvider @Inject constructor(
         @SessionDatabase private val monarchy: Monarchy,
 ) {
 

From 9bf505963109149ddab09ba12b71116969022c45 Mon Sep 17 00:00:00 2001
From: Valere <valeref@matrix.org>
Date: Fri, 23 Oct 2020 17:44:33 +0200
Subject: [PATCH 05/23] Fix test helper not working

---
 .../android/sdk/common/CommonTestHelper.kt    | 14 ++++--
 .../android/sdk/common/CryptoTestHelper.kt    | 46 +++----------------
 .../crypto/CryptoSessionInfoProvider.kt       |  2 +-
 3 files changed, 16 insertions(+), 46 deletions(-)

diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
index 1c912b365f..cbe4cca8a3 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
@@ -88,7 +88,10 @@ class CommonTestHelper(context: Context) {
     fun syncSession(session: Session) {
         val lock = CountDownLatch(1)
 
-        GlobalScope.launch(Dispatchers.Main) { session.open() }
+        val job = GlobalScope.launch(Dispatchers.Main) {
+            session.open()
+        }
+        runBlocking { job.join() }
 
         session.startSync(true)
 
@@ -341,7 +344,7 @@ class CommonTestHelper(context: Context) {
     }
 
     // Transform a method with a MatrixCallback to a synchronous method
-    inline fun <reified T> doSync(block: (MatrixCallback<T>) -> Unit): T {
+    inline fun <reified T> doSync(timeout: Long? = TestConstants.timeOutMillis, block: (MatrixCallback<T>) -> Unit): T {
         val lock = CountDownLatch(1)
         var result: T? = null
 
@@ -354,7 +357,7 @@ class CommonTestHelper(context: Context) {
 
         block.invoke(callback)
 
-        await(lock)
+        await(lock, timeout)
 
         assertNotNull(result)
         return result!!
@@ -366,8 +369,9 @@ class CommonTestHelper(context: Context) {
     fun Iterable<Session>.signOutAndClose() = forEach { signOutAndClose(it) }
 
     fun signOutAndClose(session: Session) {
-        doSync<Unit> { session.signOut(true, it) }
-        session.close()
+        doSync<Unit>(60_000) { session.signOut(true, it) }
+        // no need signout will close
+        // session.close()
     }
 }
 
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt
index 370b416f54..f1b5fca5e5 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt
@@ -32,9 +32,6 @@ import org.matrix.android.sdk.api.session.room.model.Membership
 import org.matrix.android.sdk.api.session.room.model.RoomSummary
 import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
 import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
-import org.matrix.android.sdk.api.session.room.timeline.Timeline
-import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
-import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
 import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
 import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
 import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupAuthData
@@ -197,47 +194,16 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
         val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!!
         val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
 
-        val lock = CountDownLatch(1)
-
-        val bobEventsListener = object : Timeline.Listener {
-            override fun onTimelineFailure(throwable: Throwable) {
-                // noop
-            }
-
-            override fun onNewTimelineEvents(eventIds: List<String>) {
-                // noop
-            }
-
-            override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
-                val messages = snapshot.filter { it.root.getClearType() == EventType.MESSAGE }
-                        .groupBy { it.root.senderId!! }
-
-                // Alice has sent 2 messages and Bob has sent 3 messages
-                if (messages[aliceSession.myUserId]?.size == 2 && messages[bobSession.myUserId]?.size == 3) {
-                    lock.countDown()
-                }
-            }
-        }
-
-        val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(20))
-        bobTimeline.start()
-        bobTimeline.addListener(bobEventsListener)
-
         // Alice sends a message
-        roomFromAlicePOV.sendTextMessage(messagesFromAlice[0])
+        mTestHelper.sendTextMessage(roomFromAlicePOV, messagesFromAlice[0], 1)
+//        roomFromAlicePOV.sendTextMessage(messagesFromAlice[0])
 
         // Bob send 3 messages
-        roomFromBobPOV.sendTextMessage(messagesFromBob[0])
-        roomFromBobPOV.sendTextMessage(messagesFromBob[1])
-        roomFromBobPOV.sendTextMessage(messagesFromBob[2])
-
+        mTestHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[0], 1)
+        mTestHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[1], 1)
+        mTestHelper.sendTextMessage(roomFromBobPOV, messagesFromBob[2], 1)
         // Alice sends a message
-        roomFromAlicePOV.sendTextMessage(messagesFromAlice[1])
-
-        mTestHelper.await(lock)
-
-        bobTimeline.removeListener(bobEventsListener)
-        bobTimeline.dispose()
+        mTestHelper.sendTextMessage(roomFromAlicePOV, messagesFromAlice[1], 1)
 
         return cryptoTestData
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt
index 225a9af1ec..ba0dbda786 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt
@@ -31,7 +31,7 @@ import javax.inject.Inject
  * in the session DB, this class encapsulate this functionality
  */
 internal class CryptoSessionInfoProvider @Inject constructor(
-        @SessionDatabase private val monarchy: Monarchy,
+        @SessionDatabase private val monarchy: Monarchy
 ) {
 
     fun isRoomEncrypted(roomId: String): Boolean {

From 9e921d8b50e5fc003ab19dfb86c15603859e9680 Mon Sep 17 00:00:00 2001
From: Valere <valeref@matrix.org>
Date: Fri, 23 Oct 2020 17:49:42 +0200
Subject: [PATCH 06/23] safeguard for save after store closed

---
 .../android/sdk/internal/crypto/InboundGroupSessionStore.kt  | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

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 a57b66cd4e..06c667ee4a 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,6 +19,7 @@ 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.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
@@ -90,7 +91,9 @@ internal class InboundGroupSessionStore @Inject constructor(
         dirtySession.clear()
         cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
             Timber.v("## Inbound: getInboundGroupSession batching save of ${dirtySession.size}")
-            store.storeInboundGroupSessions(toSave)
+            tryOrNull {
+                store.storeInboundGroupSessions(toSave)
+            }
         }
     }
 }

From e149be9e0fe66a5fb89e8aeddac69b05f025c378 Mon Sep 17 00:00:00 2001
From: Valere <valeref@matrix.org>
Date: Mon, 26 Oct 2020 11:24:01 +0100
Subject: [PATCH 07/23] Offload Incoming Gossip to dedicated thread

---
 .../crypto/IncomingGossipingRequestManager.kt | 68 +++++++++++--------
 .../internal/crypto/store/IMXCryptoStore.kt   |  1 +
 .../crypto/store/db/RealmCryptoStore.kt       | 22 ++++++
 3 files changed, 63 insertions(+), 28 deletions(-)

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 8869e73432..5882205fca 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
@@ -38,6 +38,7 @@ 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
 import javax.inject.Inject
 
 @SessionScope
@@ -52,6 +53,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
         private val coroutineDispatchers: MatrixCoroutineDispatchers,
         private val cryptoCoroutineScope: CoroutineScope) {
 
+    private val executor = Executors.newSingleThreadExecutor()
     // list of IncomingRoomKeyRequests/IncomingRoomKeyRequestCancellations
     // we received in the current sync.
     private val receivedGossipingRequests = ArrayList<IncomingShareRequestCommon>()
@@ -108,8 +110,8 @@ internal class IncomingGossipingRequestManager @Inject constructor(
                             // ignore, it was sent by me as *
                             Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo")
                         } else {
-                            // save in DB
-                            cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
+//                            // save in DB
+//                            cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
                             receivedGossipingRequests.add(it)
                         }
                     }
@@ -119,7 +121,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
                             // ignore, it was sent by me as *
                             Timber.v("## GOSSIP onGossipingRequestEvent type ${event.type} ignore remote echo")
                         } else {
-                            cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
+//                            cryptoStore.storeIncomingGossipingRequest(it, ageLocalTs)
                             receivedGossipingRequests.add(it)
                         }
                     }
@@ -144,13 +146,8 @@ internal class IncomingGossipingRequestManager @Inject constructor(
     fun processReceivedGossipingRequests() {
         val roomKeyRequestsToProcess = receivedGossipingRequests.toList()
         receivedGossipingRequests.clear()
-        for (request in roomKeyRequestsToProcess) {
-            if (request is IncomingRoomKeyRequest) {
-                processIncomingRoomKeyRequest(request)
-            } else if (request is IncomingSecretShareRequest) {
-                processIncomingSecretShareRequest(request)
-            }
-        }
+
+        Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : ${roomKeyRequestsToProcess.size} request to process")
 
         var receivedRequestCancellations: List<IncomingRequestCancellation>? = null
 
@@ -161,27 +158,42 @@ internal class IncomingGossipingRequestManager @Inject constructor(
             }
         }
 
-        receivedRequestCancellations?.forEach { request ->
-            Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : m.room_key_request cancellation $request")
-            // we should probably only notify the app of cancellations we told it
-            // about, but we don't currently have a record of that, so we just pass
-            // everything through.
-            if (request.userId == credentials.userId && request.deviceId == credentials.deviceId) {
-                // ignore remote echo
-                return@forEach
-            }
-            val matchingIncoming = cryptoStore.getIncomingRoomKeyRequest(request.userId ?: "", request.deviceId ?: "", request.requestId ?: "")
-            if (matchingIncoming == null) {
-                // ignore that?
-                return@forEach
-            } else {
-                // If it was accepted from this device, keep the information, do not mark as cancelled
-                if (matchingIncoming.state != GossipingRequestState.ACCEPTED) {
-                    onRoomKeyRequestCancellation(request)
-                    cryptoStore.updateGossipingRequestState(request, GossipingRequestState.CANCELLED_BY_REQUESTER)
+
+
+        executor.execute {
+            cryptoStore.storeIncomingGossipingRequests(roomKeyRequestsToProcess)
+            for (request in roomKeyRequestsToProcess) {
+                if (request is IncomingRoomKeyRequest) {
+                    processIncomingRoomKeyRequest(request)
+                } else if (request is IncomingSecretShareRequest) {
+                    processIncomingSecretShareRequest(request)
                 }
             }
+
+            receivedRequestCancellations?.forEach { request ->
+                Timber.v("## CRYPTO | GOSSIP processReceivedGossipingRequests() : m.room_key_request cancellation $request")
+                // we should probably only notify the app of cancellations we told it
+                // about, but we don't currently have a record of that, so we just pass
+                // everything through.
+                if (request.userId == credentials.userId && request.deviceId == credentials.deviceId) {
+                    // ignore remote echo
+                    return@forEach
+                }
+                val matchingIncoming = cryptoStore.getIncomingRoomKeyRequest(request.userId ?: "", request.deviceId ?: "", request.requestId ?: "")
+                if (matchingIncoming == null) {
+                    // ignore that?
+                    return@forEach
+                } else {
+                    // If it was accepted from this device, keep the information, do not mark as cancelled
+                    if (matchingIncoming.state != GossipingRequestState.ACCEPTED) {
+                        onRoomKeyRequestCancellation(request)
+                        cryptoStore.updateGossipingRequestState(request, GossipingRequestState.CANCELLED_BY_REQUESTER)
+                    }
+                }
+            }
+
         }
+
     }
 
     private fun processIncomingRoomKeyRequest(request: IncomingRoomKeyRequest) {
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 0ae1e69124..a43faa2cd8 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
@@ -127,6 +127,7 @@ internal interface IMXCryptoStore {
 
     fun getPendingIncomingGossipingRequests(): List<IncomingShareRequestCommon>
     fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?)
+    fun storeIncomingGossipingRequests(request: List<IncomingShareRequestCommon>)
 //    fun getPendingIncomingSecretShareRequests(): List<IncomingSecretShareRequest>
 
     /**
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 b25349cba9..c0b538963d 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
@@ -1284,6 +1284,28 @@ internal class RealmCryptoStore @Inject constructor(
         }
     }
 
+    override fun storeIncomingGossipingRequests(requests: List<IncomingShareRequestCommon>) {
+        doRealmTransactionAsync(realmConfiguration) { realm ->
+            requests.forEach { request ->
+                // After a clear cache, we might have a
+                realm.createObject(IncomingGossipingRequestEntity::class.java).let {
+                    it.otherDeviceId = request.deviceId
+                    it.otherUserId = request.userId
+                    it.requestId = request.requestId ?: ""
+                    it.requestState = GossipingRequestState.PENDING
+                    it.localCreationTimestamp = request.localCreationTimestamp ?: System.currentTimeMillis()
+                    if (request is IncomingSecretShareRequest) {
+                        it.type = GossipRequestType.SECRET
+                        it.requestedInfoStr = request.secretName
+                    } else if (request is IncomingRoomKeyRequest) {
+                        it.type = GossipRequestType.KEY
+                        it.requestedInfoStr = request.requestBody?.toJson()
+                    }
+                }
+            }
+        }
+    }
+
 //    override fun getPendingIncomingSecretShareRequests(): List<IncomingSecretShareRequest> {
 //        return doRealmQueryAndCopyList(realmConfiguration) {
 //            it.where<GossipingEventEntity>()

From 2144879e7390bc0e63aaadab9c840961d6be8567 Mon Sep 17 00:00:00 2001
From: Valere <valeref@matrix.org>
Date: Mon, 26 Oct 2020 11:24:37 +0100
Subject: [PATCH 08/23] Offload sending keywithheld to computation

---
 .../algorithms/megolm/MXMegolmEncryption.kt   | 27 ++++++++++++-------
 .../megolm/MXMegolmEncryptionFactory.kt       | 10 +++++--
 2 files changed, 26 insertions(+), 11 deletions(-)

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 466722788d..74909f6297 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
@@ -16,6 +16,9 @@
 
 package org.matrix.android.sdk.internal.crypto.algorithms.megolm
 
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
 import org.matrix.android.sdk.api.MatrixCallback
 import org.matrix.android.sdk.api.auth.data.Credentials
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
@@ -39,6 +42,7 @@ import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
 import org.matrix.android.sdk.internal.task.TaskExecutor
 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.convertToUTF8
 import timber.log.Timber
 
@@ -54,7 +58,9 @@ internal class MXMegolmEncryption(
         private val sendToDeviceTask: SendToDeviceTask,
         private val messageEncrypter: MessageEncrypter,
         private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository,
-        private val taskExecutor: TaskExecutor
+        private val taskExecutor: TaskExecutor,
+        private val coroutineDispatchers: MatrixCoroutineDispatchers,
+        private val cryptoCoroutineScope: CoroutineScope
 ) : IMXEncrypting {
 
     // OutboundSessionInfo. Null if we haven't yet started setting one up. Note
@@ -84,15 +90,18 @@ internal class MXMegolmEncryption(
     }
 
     private fun notifyWithheldForSession(devices: MXUsersDevicesMap<WithHeldCode>, outboundSession: MXOutboundSessionInfo) {
-        mutableListOf<Pair<UserDevice, WithHeldCode>>().apply {
-            devices.forEach { userId, deviceId, withheldCode ->
-                this.add(UserDevice(userId, deviceId) to withheldCode)
+        // offload to computation thread
+        cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
+            mutableListOf<Pair<UserDevice, WithHeldCode>>().apply {
+                devices.forEach { userId, deviceId, withheldCode ->
+                    this.add(UserDevice(userId, deviceId) to withheldCode)
+                }
+            }.groupBy(
+                    { it.second },
+                    { it.first }
+            ).forEach { (code, targets) ->
+                notifyKeyWithHeld(targets, outboundSession.sessionId, olmDevice.deviceCurve25519Key, code)
             }
-        }.groupBy(
-                { it.second },
-                { it.first }
-        ).forEach { (code, targets) ->
-            notifyKeyWithHeld(targets, outboundSession.sessionId, olmDevice.deviceCurve25519Key, code)
         }
     }
 
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 8f651692fc..f0cc15fb63 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
@@ -16,6 +16,7 @@
 
 package org.matrix.android.sdk.internal.crypto.algorithms.megolm
 
+import kotlinx.coroutines.CoroutineScope
 import org.matrix.android.sdk.api.auth.data.Credentials
 import org.matrix.android.sdk.internal.crypto.DeviceListManager
 import org.matrix.android.sdk.internal.crypto.MXOlmDevice
@@ -26,6 +27,7 @@ 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.task.TaskExecutor
+import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
 import javax.inject.Inject
 
 internal class MXMegolmEncryptionFactory @Inject constructor(
@@ -38,7 +40,9 @@ internal class MXMegolmEncryptionFactory @Inject constructor(
         private val sendToDeviceTask: SendToDeviceTask,
         private val messageEncrypter: MessageEncrypter,
         private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository,
-        private val taskExecutor: TaskExecutor) {
+        private val taskExecutor: TaskExecutor,
+        private val coroutineDispatchers: MatrixCoroutineDispatchers,
+        private val cryptoCoroutineScope: CoroutineScope) {
 
     fun create(roomId: String): MXMegolmEncryption {
         return MXMegolmEncryption(
@@ -52,7 +56,9 @@ internal class MXMegolmEncryptionFactory @Inject constructor(
                 sendToDeviceTask,
                 messageEncrypter,
                 warnOnUnknownDevicesRepository,
-                taskExecutor
+                taskExecutor,
+                coroutineDispatchers,
+                cryptoCoroutineScope
         )
     }
 }

From dbb14e631305ece5956b66b9d85f53699dfea7dd Mon Sep 17 00:00:00 2001
From: Valere <valeref@matrix.org>
Date: Mon, 26 Oct 2020 11:24:53 +0100
Subject: [PATCH 09/23] offload megolm backup import off crypto thread

---
 .../sdk/internal/crypto/keysbackup/DefaultKeysBackupService.kt  | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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 bbd8ebd000..3abbf9b16e 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
@@ -690,7 +690,7 @@ internal class DefaultKeysBackupService @Inject constructor(
                 // Get backed up keys from the homeserver
                 val data = getKeys(sessionId, roomId, keysVersionResult.version!!)
 
-                withContext(coroutineDispatchers.crypto) {
+                withContext(coroutineDispatchers.computation) {
                     val sessionsData = ArrayList<MegolmSessionData>()
                     // Restore that data
                     var sessionsFromHsCount = 0

From 6d2b3a6940465d07c61b9c86813d92ed928ecada Mon Sep 17 00:00:00 2001
From: Valere <valeref@matrix.org>
Date: Mon, 26 Oct 2020 11:25:31 +0100
Subject: [PATCH 10/23] cleaning

---
 .../sdk/internal/crypto/IncomingGossipingRequestManager.kt    | 4 ----
 .../internal/crypto/algorithms/megolm/MXMegolmEncryption.kt   | 1 -
 2 files changed, 5 deletions(-)

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 5882205fca..6f135ceb49 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
@@ -158,8 +158,6 @@ internal class IncomingGossipingRequestManager @Inject constructor(
             }
         }
 
-
-
         executor.execute {
             cryptoStore.storeIncomingGossipingRequests(roomKeyRequestsToProcess)
             for (request in roomKeyRequestsToProcess) {
@@ -191,9 +189,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
                     }
                 }
             }
-
         }
-
     }
 
     private fun processIncomingRoomKeyRequest(request: IncomingRoomKeyRequest) {
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 74909f6297..a7c438bb31 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
@@ -17,7 +17,6 @@
 package org.matrix.android.sdk.internal.crypto.algorithms.megolm
 
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.launch
 import org.matrix.android.sdk.api.MatrixCallback
 import org.matrix.android.sdk.api.auth.data.Credentials

From bb000f77f867b534e3b43038de18af4cff4407d9 Mon Sep 17 00:00:00 2001
From: Valere <valeref@matrix.org>
Date: Mon, 26 Oct 2020 14:54:44 +0100
Subject: [PATCH 11/23] Offload self verif todevice off crypto thread

---
 .../crypto/verification/DefaultVerificationService.kt    | 9 +++++++--
 .../verification/IncomingVerificationRequestHandler.kt   | 2 ++
 2 files changed, 9 insertions(+), 2 deletions(-)

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 f3a794154c..7f02750359 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
@@ -125,7 +125,8 @@ internal class DefaultVerificationService @Inject constructor(
 
     // Event received from the sync
     fun onToDeviceEvent(event: Event) {
-        cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
+        Timber.d("## SAS onToDeviceEvent ${event.getClearType()}")
+        cryptoCoroutineScope.launch(coroutineDispatchers.dmVerif) {
             when (event.getClearType()) {
                 EventType.KEY_VERIFICATION_START         -> {
                     onStartRequestReceived(event)
@@ -159,7 +160,7 @@ internal class DefaultVerificationService @Inject constructor(
     }
 
     fun onRoomEvent(event: Event) {
-        cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
+        cryptoCoroutineScope.launch(coroutineDispatchers.dmVerif) {
             when (event.getClearType()) {
                 EventType.KEY_VERIFICATION_START  -> {
                     onRoomStartRequestReceived(event)
@@ -236,6 +237,7 @@ internal class DefaultVerificationService @Inject constructor(
     }
 
     private fun dispatchRequestAdded(tx: PendingVerificationRequest) {
+        Timber.v("## SAS dispatchRequestAdded txId:${tx.transactionId}")
         uiHandler.post {
             listeners.forEach {
                 try {
@@ -299,11 +301,14 @@ internal class DefaultVerificationService @Inject constructor(
         // We don't want to block here
         val otherDeviceId = validRequestInfo.fromDevice
 
+        Timber.v("## SAS onRequestReceived from $senderId and device $otherDeviceId, txId:${validRequestInfo.transactionId}")
+
         cryptoCoroutineScope.launch {
             if (checkKeysAreDownloaded(senderId, otherDeviceId) == null) {
                 Timber.e("## Verification device $otherDeviceId is not known")
             }
         }
+        Timber.v("## SAS onRequestReceived .. checkKeysAreDownloaded launched")
 
         // Remember this request
         val requestsForUser = pendingRequests.getOrPut(senderId) { mutableListOf() }
diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt
index 0a1b631344..e42eb6de6f 100644
--- a/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt
+++ b/vector/src/main/java/im/vector/app/features/crypto/verification/IncomingVerificationRequestHandler.kt
@@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationServic
 import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
 import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
 import org.matrix.android.sdk.api.util.toMatrixItem
+import timber.log.Timber
 import javax.inject.Inject
 import javax.inject.Singleton
 
@@ -116,6 +117,7 @@ class IncomingVerificationRequestHandler @Inject constructor(
     }
 
     override fun verificationRequestCreated(pr: PendingVerificationRequest) {
+        Timber.v("## SAS verificationRequestCreated ${pr.transactionId}")
         // For incoming request we should prompt (if not in activity where this request apply)
         if (pr.isIncoming) {
             val name = session?.getUser(pr.otherUserId)?.displayName

From 5a111af2fe50682fc018ac62345a1789f8d36d5c Mon Sep 17 00:00:00 2001
From: Valere <valeref@matrix.org>
Date: Tue, 27 Oct 2020 00:03:49 +0100
Subject: [PATCH 12/23] Fix / add close to IncomingRequestManager

---
 .../sdk/internal/crypto/DefaultCryptoService.kt |  2 +-
 .../crypto/IncomingGossipingRequestManager.kt   |  4 ++++
 .../java/im/vector/app/features/MainActivity.kt | 17 ++++++++++-------
 3 files changed, 15 insertions(+), 8 deletions(-)

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 0a18aba8ba..f4ec7acd88 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
@@ -386,7 +386,7 @@ internal class DefaultCryptoService @Inject constructor(
      */
     fun close() = runBlocking(coroutineDispatchers.crypto) {
         cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module"))
-
+        incomingGossipingRequestManager.close()
         olmDevice.release()
         cryptoStore.close()
     }
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 6f135ceb49..ae017b33f5 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
@@ -66,6 +66,10 @@ internal class IncomingGossipingRequestManager @Inject constructor(
         receivedGossipingRequests.addAll(cryptoStore.getPendingIncomingGossipingRequests())
     }
 
+    fun close() {
+        executor.shutdownNow()
+    }
+
     // Recently verified devices (map of deviceId and timestamp)
     private val recentlyVerifiedDevices = HashMap<String, Long>()
 
diff --git a/vector/src/main/java/im/vector/app/features/MainActivity.kt b/vector/src/main/java/im/vector/app/features/MainActivity.kt
index b5552e4d62..e553b5e0d3 100644
--- a/vector/src/main/java/im/vector/app/features/MainActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/MainActivity.kt
@@ -21,6 +21,7 @@ import android.content.Intent
 import android.os.Bundle
 import android.os.Parcelable
 import androidx.appcompat.app.AlertDialog
+import androidx.lifecycle.Lifecycle
 import com.bumptech.glide.Glide
 import im.vector.app.R
 import im.vector.app.core.di.ActiveSessionHolder
@@ -205,13 +206,15 @@ class MainActivity : VectorBaseActivity(), UnlockedActivity {
     }
 
     private fun displayError(failure: Throwable) {
-        AlertDialog.Builder(this)
-                .setTitle(R.string.dialog_title_error)
-                .setMessage(errorFormatter.toHumanReadable(failure))
-                .setPositiveButton(R.string.global_retry) { _, _ -> doCleanUp() }
-                .setNegativeButton(R.string.cancel) { _, _ -> startNextActivityAndFinish() }
-                .setCancelable(false)
-                .show()
+        if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
+            AlertDialog.Builder(this)
+                    .setTitle(R.string.dialog_title_error)
+                    .setMessage(errorFormatter.toHumanReadable(failure))
+                    .setPositiveButton(R.string.global_retry) { _, _ -> doCleanUp() }
+                    .setNegativeButton(R.string.cancel) { _, _ -> startNextActivityAndFinish() }
+                    .setCancelable(false)
+                    .show()
+        }
     }
 
     private fun startNextActivityAndFinish() {

From c2027be0ee50e32f0e67a19653722bdbbb9ccead Mon Sep 17 00:00:00 2001
From: Valere <valeref@matrix.org>
Date: Wed, 28 Oct 2020 17:40:30 +0100
Subject: [PATCH 13/23] Fix audit freeze, add export, and buffer gossip saves

---
 .../sdk/api/session/crypto/CryptoService.kt   |   7 +-
 .../internal/crypto/DefaultCryptoService.kt   |  30 ++-
 .../algorithms/megolm/MXMegolmEncryption.kt   |  10 +
 .../internal/crypto/store/IMXCryptoStore.kt   |   7 +-
 .../crypto/store/db/RealmCryptoStore.kt       | 116 +++++++--
 .../GossipingEventsEpoxyController.kt         | 235 ------------------
 .../GossipingEventsPaperTrailFragment.kt      |   9 +-
 .../GossipingEventsPaperTrailViewModel.kt     |  17 +-
 .../GossipingTrailPagedEpoxyController.kt     | 168 +++++++++++++
 .../IncomingKeyRequestListFragment.kt         |  10 +-
 .../IncomingKeyRequestPagedController.kt      |  64 +++++
 .../devtools/KeyRequestEpoxyController.kt     | 164 ------------
 .../devtools/KeyRequestListViewModel.kt       |  31 ++-
 .../settings/devtools/KeyRequestViewModel.kt  | 154 ++++++++++++
 .../settings/devtools/KeyRequestsFragment.kt  |  62 ++++-
 .../OutgoingKeyRequestListFragment.kt         |   6 +-
 .../OutgoingKeyRequestPagedController.kt      |  63 +++++
 .../layout/fragment_devtool_keyrequests.xml   |  20 +-
 vector/src/main/res/menu/menu_audit.xml       |  10 +
 vector/src/main/res/values/strings.xml        |   1 +
 20 files changed, 719 insertions(+), 465 deletions(-)
 delete mode 100644 vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsEpoxyController.kt
 create mode 100644 vector/src/main/java/im/vector/app/features/settings/devtools/GossipingTrailPagedEpoxyController.kt
 create mode 100644 vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestPagedController.kt
 delete mode 100644 vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestEpoxyController.kt
 create mode 100644 vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt
 create mode 100644 vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestPagedController.kt
 create mode 100644 vector/src/main/res/menu/menu_audit.xml

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt
index 121d9fb401..34be1b8d05 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt
@@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.crypto
 
 import android.content.Context
 import androidx.lifecycle.LiveData
+import androidx.paging.PagedList
 import org.matrix.android.sdk.api.MatrixCallback
 import org.matrix.android.sdk.api.listeners.ProgressListener
 import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
@@ -40,6 +41,7 @@ import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent
 import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
 import org.matrix.android.sdk.internal.crypto.model.rest.DevicesListResponse
 import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyRequestBody
+import kotlin.jvm.Throws
 
 interface CryptoService {
 
@@ -142,10 +144,13 @@ interface CryptoService {
     fun removeSessionListener(listener: NewSessionListener)
 
     fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
+    fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>>
 
     fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
+    fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>>
 
-    fun getGossipingEventsTrail(): List<Event>
+    fun getGossipingEventsTrail(): LiveData<PagedList<Event>>
+    fun getGossipingEvents(): List<Event>
 
     // For testing shared session
     fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int>
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 f4ec7acd88..07545c50bc 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
@@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.crypto
 import android.content.Context
 import androidx.annotation.VisibleForTesting
 import androidx.lifecycle.LiveData
+import androidx.paging.PagedList
 import com.squareup.moshi.Types
 import dagger.Lazy
 import kotlinx.coroutines.CancellationException
@@ -185,6 +186,8 @@ internal class DefaultCryptoService @Inject constructor(
         }
     }
 
+    val gossipingBuffer = mutableListOf<Event>()
+
     override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>) {
         setDeviceNameTask
                 .configureWith(SetDeviceNameTask.Params(deviceId, deviceName)) {
@@ -428,6 +431,13 @@ internal class DefaultCryptoService @Inject constructor(
                     incomingGossipingRequestManager.processReceivedGossipingRequests()
                 }
             }
+
+            tryOrNull {
+                gossipingBuffer.toList().let {
+                    cryptoStore.saveGossipingEvents(it)
+                }
+                gossipingBuffer.clear()
+            }
         }
     }
 
@@ -721,19 +731,19 @@ internal class DefaultCryptoService @Inject constructor(
         cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
             when (event.getClearType()) {
                 EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> {
-                    cryptoStore.saveGossipingEvent(event)
+                    gossipingBuffer.add(event)
                     // Keys are imported directly, not waiting for end of sync
                     onRoomKeyEvent(event)
                 }
                 EventType.REQUEST_SECRET,
                 EventType.ROOM_KEY_REQUEST                       -> {
                     // save audit trail
-                    cryptoStore.saveGossipingEvent(event)
+                    gossipingBuffer.add(event)
                     // Requests are stacked, and will be handled one by one at the end of the sync (onSyncComplete)
                     incomingGossipingRequestManager.onGossipingRequestEvent(event)
                 }
                 EventType.SEND_SECRET                            -> {
-                    cryptoStore.saveGossipingEvent(event)
+                    gossipingBuffer.add(event)
                     onSecretSendReceived(event)
                 }
                 EventType.ROOM_KEY_WITHHELD                      -> {
@@ -1254,14 +1264,26 @@ internal class DefaultCryptoService @Inject constructor(
         return cryptoStore.getOutgoingRoomKeyRequests()
     }
 
+    override fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>> {
+        return cryptoStore.getOutgoingRoomKeyRequestsPaged()
+    }
+
+    override fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>> {
+        return cryptoStore.getIncomingRoomKeyRequestsPaged()
+    }
+
     override fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> {
         return cryptoStore.getIncomingRoomKeyRequests()
     }
 
-    override fun getGossipingEventsTrail(): List<Event> {
+    override fun getGossipingEventsTrail(): LiveData<PagedList<Event>> {
         return cryptoStore.getGossipingEventsTrail()
     }
 
+    override fun getGossipingEvents(): List<Event> {
+        return cryptoStore.getGossipingEvents()
+    }
+
     override fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int> {
         return cryptoStore.getSharedWithInfo(roomId, sessionId)
     }
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 a7c438bb31..e55cf37118 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
@@ -22,6 +22,7 @@ import org.matrix.android.sdk.api.MatrixCallback
 import org.matrix.android.sdk.api.auth.data.Credentials
 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
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.internal.crypto.DeviceListManager
 import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
@@ -255,6 +256,15 @@ internal class MXMegolmEncryption(
         for ((userId, devicesToShareWith) in devicesByUser) {
             for ((deviceId) in devicesToShareWith) {
                 session.sharedWithHelper.markedSessionAsShared(userId, deviceId, chainIndex)
+                cryptoStore.saveGossipingEvent(Event(
+                        type = EventType.ROOM_KEY,
+                        senderId = credentials.userId,
+                        content = submap.apply {
+                            this["session_key"] = ""
+                            // we add a fake key for trail
+                            this["_dest"] = "$userId|$deviceId"
+                        }
+                ))
             }
         }
 
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 a43faa2cd8..74773384ae 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
@@ -17,6 +17,7 @@
 package org.matrix.android.sdk.internal.crypto.store
 
 import androidx.lifecycle.LiveData
+import androidx.paging.PagedList
 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.util.Optional
@@ -365,6 +366,7 @@ internal interface IMXCryptoStore {
     fun getOrAddOutgoingSecretShareRequest(secretName: String, recipients: Map<String, List<String>>): OutgoingSecretRequest?
 
     fun saveGossipingEvent(event: Event)
+    fun saveGossipingEvents(events: List<Event>)
 
     fun updateGossipingRequestState(request: IncomingShareRequestCommon, state: GossipingRequestState) {
         updateGossipingRequestState(
@@ -442,10 +444,13 @@ internal interface IMXCryptoStore {
     // Dev tools
 
     fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
+    fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>>
     fun getOutgoingSecretKeyRequests(): List<OutgoingSecretRequest>
     fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest?
     fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
-    fun getGossipingEventsTrail(): List<Event>
+    fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>>
+    fun getGossipingEventsTrail(): LiveData<PagedList<Event>>
+    fun getGossipingEvents(): List<Event>
 
     fun setDeviceKeysUploaded(uploaded: Boolean)
     fun getDeviceKeysUploaded(): Boolean
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 c0b538963d..7644ab5cc2 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
@@ -18,6 +18,8 @@ package org.matrix.android.sdk.internal.crypto.store.db
 
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.Transformations
+import androidx.paging.LivePagedListBuilder
+import androidx.paging.PagedList
 import com.zhuinden.monarchy.Monarchy
 import io.realm.Realm
 import io.realm.RealmConfiguration
@@ -62,6 +64,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.model.CryptoRoomEntityFie
 import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntity
 import org.matrix.android.sdk.internal.crypto.store.db.model.DeviceInfoEntityFields
 import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntity
+import org.matrix.android.sdk.internal.crypto.store.db.model.GossipingEventEntityFields
 import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntity
 import org.matrix.android.sdk.internal.crypto.store.db.model.IncomingGossipingRequestEntityFields
 import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity
@@ -998,7 +1001,50 @@ internal class RealmCryptoStore @Inject constructor(
         }
     }
 
-    override fun getGossipingEventsTrail(): List<Event> {
+    override fun getIncomingRoomKeyRequestsPaged(): LiveData<PagedList<IncomingRoomKeyRequest>> {
+        val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
+            realm.where<IncomingGossipingRequestEntity>()
+                    .equalTo(IncomingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
+                    .sort(IncomingGossipingRequestEntityFields.LOCAL_CREATION_TIMESTAMP, Sort.DESCENDING)
+        }
+        val dataSourceFactory = realmDataSourceFactory.map {
+            it.toIncomingGossipingRequest() as? IncomingRoomKeyRequest
+                    ?: IncomingRoomKeyRequest(
+                            requestBody = null,
+                            deviceId = "",
+                            userId = "",
+                            requestId = "",
+                            state = GossipingRequestState.NONE,
+                            localCreationTimestamp = 0
+                    )
+        }
+        return monarchy.findAllPagedWithChanges(realmDataSourceFactory,
+                LivePagedListBuilder(dataSourceFactory,
+                        PagedList.Config.Builder()
+                                .setPageSize(20)
+                                .setEnablePlaceholders(false)
+                                .setPrefetchDistance(1)
+                                .build())
+        )
+    }
+
+    override fun getGossipingEventsTrail(): LiveData<PagedList<Event>> {
+        val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
+            realm.where<GossipingEventEntity>().sort(GossipingEventEntityFields.AGE_LOCAL_TS, Sort.DESCENDING)
+        }
+        val dataSourceFactory = realmDataSourceFactory.map { it.toModel() }
+        val trail = monarchy.findAllPagedWithChanges(realmDataSourceFactory,
+                LivePagedListBuilder(dataSourceFactory,
+                        PagedList.Config.Builder()
+                                .setPageSize(20)
+                                .setEnablePlaceholders(false)
+                                .setPrefetchDistance(1)
+                                .build())
+        )
+        return trail
+    }
+
+    override fun getGossipingEvents(): List<Event> {
         return monarchy.fetchAllCopiedSync { realm ->
             realm.where<GossipingEventEntity>()
         }.map {
@@ -1066,24 +1112,43 @@ internal class RealmCryptoStore @Inject constructor(
         return request
     }
 
-    override fun saveGossipingEvent(event: Event) {
+    override fun saveGossipingEvents(events: List<Event>) {
         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>(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult)
-            decryptionErrorCode = event.mCryptoError?.name
-        }
-        doRealmTransaction(realmConfiguration) { realm ->
-            realm.insertOrUpdate(entity)
+        monarchy.writeAsync { realm ->
+            events.forEach { event ->
+                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>(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult)
+                    decryptionErrorCode = event.mCryptoError?.name
+                }
+                realm.insertOrUpdate(entity)
+            }
         }
     }
 
+    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>(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 ->
@@ -1439,6 +1504,27 @@ internal class RealmCryptoStore @Inject constructor(
                 .filterNotNull()
     }
 
+    override fun getOutgoingRoomKeyRequestsPaged(): LiveData<PagedList<OutgoingRoomKeyRequest>> {
+        val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
+            realm
+                    .where(OutgoingGossipingRequestEntity::class.java)
+                    .equalTo(OutgoingGossipingRequestEntityFields.TYPE_STR, GossipRequestType.KEY.name)
+        }
+        val dataSourceFactory = realmDataSourceFactory.map {
+            it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
+                    ?: OutgoingRoomKeyRequest(requestBody = null, requestId = "?", recipients = emptyMap(), state = OutgoingGossipingRequestState.CANCELLED)
+        }
+        val trail = monarchy.findAllPagedWithChanges(realmDataSourceFactory,
+                LivePagedListBuilder(dataSourceFactory,
+                        PagedList.Config.Builder()
+                                .setPageSize(20)
+                                .setEnablePlaceholders(false)
+                                .setPrefetchDistance(1)
+                                .build())
+        )
+        return trail
+    }
+
     override fun getCrossSigningInfo(userId: String): MXCrossSigningInfo? {
         return doWithRealm(realmConfiguration) { realm ->
             val crossSigningInfo = realm.where(CrossSigningInfoEntity::class.java)
diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsEpoxyController.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsEpoxyController.kt
deleted file mode 100644
index cf93bc14e7..0000000000
--- a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsEpoxyController.kt
+++ /dev/null
@@ -1,235 +0,0 @@
-/*
- * Copyright (c) 2020 New Vector Ltd
- *
- * 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 im.vector.app.features.settings.devtools
-
-import com.airbnb.epoxy.TypedEpoxyController
-import com.airbnb.mvrx.Fail
-import com.airbnb.mvrx.Loading
-import com.airbnb.mvrx.Success
-import com.airbnb.mvrx.Uninitialized
-import im.vector.app.R
-import im.vector.app.core.date.DateFormatKind
-import im.vector.app.core.date.VectorDateFormatter
-import im.vector.app.core.epoxy.loadingItem
-import im.vector.app.core.extensions.exhaustive
-import im.vector.app.core.resources.ColorProvider
-import im.vector.app.core.resources.StringProvider
-import im.vector.app.core.ui.list.GenericItem
-import im.vector.app.core.ui.list.genericFooterItem
-import im.vector.app.core.ui.list.genericItem
-import im.vector.app.core.ui.list.genericItemHeader
-import me.gujun.android.span.span
-import org.matrix.android.sdk.api.session.events.model.Event
-import org.matrix.android.sdk.api.session.events.model.EventType
-import org.matrix.android.sdk.api.session.events.model.toModel
-import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent
-import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent
-import org.matrix.android.sdk.internal.crypto.model.rest.ForwardedRoomKeyContent
-import org.matrix.android.sdk.internal.crypto.model.rest.GossipingToDeviceObject
-import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyShareRequest
-import org.matrix.android.sdk.internal.crypto.model.rest.SecretShareRequest
-import javax.inject.Inject
-
-class GossipingEventsEpoxyController @Inject constructor(
-        private val stringProvider: StringProvider,
-        private val vectorDateFormatter: VectorDateFormatter,
-        private val colorProvider: ColorProvider
-) : TypedEpoxyController<GossipingEventsPaperTrailState>() {
-
-    interface InteractionListener {
-        fun didTap(event: Event)
-    }
-
-    var interactionListener: InteractionListener? = null
-
-    override fun buildModels(data: GossipingEventsPaperTrailState?) {
-        when (val async = data?.events) {
-            is Uninitialized,
-            is Loading -> {
-                loadingItem {
-                    id("loadingOutgoing")
-                    loadingText(stringProvider.getString(R.string.loading))
-                }
-            }
-            is Fail    -> {
-                genericItem {
-                    id("failOutgoing")
-                    title(async.error.localizedMessage)
-                }
-            }
-            is Success -> {
-                val eventList = async.invoke()
-                if (eventList.isEmpty()) {
-                    genericFooterItem {
-                        id("empty")
-                        text(stringProvider.getString(R.string.no_result_placeholder))
-                    }
-                    return
-                }
-
-                eventList.forEachIndexed { _, event ->
-                    genericItem {
-                        id(event.hashCode())
-                        itemClickAction(GenericItem.Action("view").apply { perform = Runnable { interactionListener?.didTap(event) } })
-                        title(
-                                if (event.isEncrypted()) {
-                                    "${event.getClearType()} [encrypted]"
-                                } else {
-                                    event.type
-                                }
-                        )
-                        description(
-                                span {
-                                    +vectorDateFormatter.format(event.ageLocalTs, DateFormatKind.DEFAULT_DATE_AND_TIME)
-                                    span("\nfrom: ") {
-                                        textStyle = "bold"
-                                    }
-                                    +"${event.senderId}"
-                                    apply {
-                                        if (event.getClearType() == EventType.ROOM_KEY_REQUEST) {
-                                            val content = event.getClearContent().toModel<RoomKeyShareRequest>()
-                                            span("\nreqId:") {
-                                                textStyle = "bold"
-                                            }
-                                            +" ${content?.requestId}"
-                                            span("\naction:") {
-                                                textStyle = "bold"
-                                            }
-                                            +" ${content?.action}"
-                                            if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
-                                                span("\nsessionId:") {
-                                                    textStyle = "bold"
-                                                }
-                                                +" ${content.body?.sessionId}"
-                                            }
-                                            span("\nrequestedBy: ") {
-                                                textStyle = "bold"
-                                            }
-                                            +"${content?.requestingDeviceId}"
-                                        } else if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) {
-                                            val encryptedContent = event.content.toModel<OlmEventContent>()
-                                            val content = event.getClearContent().toModel<ForwardedRoomKeyContent>()
-                                            if (event.mxDecryptionResult == null) {
-                                                span("**Failed to Decrypt** ${event.mCryptoError}") {
-                                                    textColor = colorProvider.getColor(R.color.vector_error_color)
-                                                }
-                                            }
-                                            span("\nsessionId:") {
-                                                textStyle = "bold"
-                                            }
-                                            +" ${content?.sessionId}"
-                                            span("\nFrom Device (sender key):") {
-                                                textStyle = "bold"
-                                            }
-                                            +" ${encryptedContent?.senderKey}"
-                                        } else if (event.getClearType() == EventType.SEND_SECRET) {
-                                            val content = event.getClearContent().toModel<SecretSendEventContent>()
-
-                                            span("\nrequestId:") {
-                                                textStyle = "bold"
-                                            }
-                                            +" ${content?.requestId}"
-                                            span("\nFrom Device:") {
-                                                textStyle = "bold"
-                                            }
-                                            +" ${event.mxDecryptionResult?.payload?.get("sender_device")}"
-                                        } else if (event.getClearType() == EventType.REQUEST_SECRET) {
-                                            val content = event.getClearContent().toModel<SecretShareRequest>()
-                                            span("\nreqId:") {
-                                                textStyle = "bold"
-                                            }
-                                            +" ${content?.requestId}"
-                                            span("\naction:") {
-                                                textStyle = "bold"
-                                            }
-                                            +" ${content?.action}"
-                                            if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
-                                                span("\nsecretName:") {
-                                                    textStyle = "bold"
-                                                }
-                                                +" ${content.secretName}"
-                                            }
-                                            span("\nrequestedBy: ") {
-                                                textStyle = "bold"
-                                            }
-                                            +"${content?.requestingDeviceId}"
-                                        }
-                                    }
-                                }
-                        )
-                    }
-                }
-            }
-        }
-    }
-
-    private fun buildOutgoing(data: KeyRequestListViewState?) {
-        data?.outgoingRoomKeyRequests?.let { async ->
-            when (async) {
-                is Uninitialized,
-                is Loading -> {
-                    loadingItem {
-                        id("loadingOutgoing")
-                        loadingText(stringProvider.getString(R.string.loading))
-                    }
-                }
-                is Fail    -> {
-                    genericItem {
-                        id("failOutgoing")
-                        title(async.error.localizedMessage)
-                    }
-                }
-                is Success -> {
-                    if (async.invoke().isEmpty()) {
-                        genericFooterItem {
-                            id("empty")
-                            text(stringProvider.getString(R.string.no_result_placeholder))
-                        }
-                        return
-                    }
-
-                    val requestList = async.invoke().groupBy { it.roomId }
-
-                    requestList.forEach {
-                        genericItemHeader {
-                            id(it.key)
-                            text("roomId: ${it.key}")
-                        }
-                        it.value.forEach { roomKeyRequest ->
-                            genericItem {
-                                id(roomKeyRequest.requestId)
-                                title(roomKeyRequest.requestId)
-                                description(
-                                        span {
-                                            span("sessionId:\n") {
-                                                textStyle = "bold"
-                                            }
-                                            +"${roomKeyRequest.sessionId}"
-                                            span("\nstate:") {
-                                                textStyle = "bold"
-                                            }
-                                            +"\n${roomKeyRequest.state.name}"
-                                        }
-                                )
-                            }
-                        }
-                    }
-                }
-            }.exhaustive
-        }
-    }
-}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt
index e2c855a9e3..0ceb8e148d 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailFragment.kt
@@ -33,16 +33,19 @@ import javax.inject.Inject
 
 class GossipingEventsPaperTrailFragment @Inject constructor(
         val viewModelFactory: GossipingEventsPaperTrailViewModel.Factory,
-        private val epoxyController: GossipingEventsEpoxyController,
+        private val epoxyController: GossipingTrailPagedEpoxyController,
         private val colorProvider: ColorProvider
-) : VectorBaseFragment(), GossipingEventsEpoxyController.InteractionListener {
+) : VectorBaseFragment(), GossipingTrailPagedEpoxyController.InteractionListener {
 
     override fun getLayoutResId() = R.layout.fragment_generic_recycler
 
     private val viewModel: GossipingEventsPaperTrailViewModel by fragmentViewModel(GossipingEventsPaperTrailViewModel::class)
 
     override fun invalidate() = withState(viewModel) { state ->
-        epoxyController.setData(state)
+        state.events.invoke()?.let {
+            epoxyController.submitList(it)
+        }
+        Unit
     }
 
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt
index d903725b22..4249ef09fa 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsPaperTrailViewModel.kt
@@ -16,13 +16,12 @@
 
 package im.vector.app.features.settings.devtools
 
-import androidx.lifecycle.viewModelScope
+import androidx.paging.PagedList
 import com.airbnb.mvrx.Async
 import com.airbnb.mvrx.FragmentViewModelContext
 import com.airbnb.mvrx.Loading
 import com.airbnb.mvrx.MvRxState
 import com.airbnb.mvrx.MvRxViewModelFactory
-import com.airbnb.mvrx.Success
 import com.airbnb.mvrx.Uninitialized
 import com.airbnb.mvrx.ViewModelContext
 import com.squareup.inject.assisted.Assisted
@@ -30,12 +29,12 @@ import com.squareup.inject.assisted.AssistedInject
 import im.vector.app.core.platform.EmptyAction
 import im.vector.app.core.platform.EmptyViewEvents
 import im.vector.app.core.platform.VectorViewModel
-import kotlinx.coroutines.launch
 import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.rx.asObservable
 
 data class GossipingEventsPaperTrailState(
-        val events: Async<List<Event>> = Uninitialized
+        val events: Async<PagedList<Event>> = Uninitialized
 ) : MvRxState
 
 class GossipingEventsPaperTrailViewModel @AssistedInject constructor(@Assisted initialState: GossipingEventsPaperTrailState,
@@ -50,14 +49,10 @@ class GossipingEventsPaperTrailViewModel @AssistedInject constructor(@Assisted i
         setState {
             copy(events = Loading())
         }
-        viewModelScope.launch {
-            session.cryptoService().getGossipingEventsTrail().let {
-                val sorted = it.sortedByDescending { it.ageLocalTs }
-                setState {
-                    copy(events = Success(sorted))
+        session.cryptoService().getGossipingEventsTrail().asObservable()
+                .execute {
+                    copy(events = it)
                 }
-            }
-        }
     }
 
     override fun handle(action: EmptyAction) {}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingTrailPagedEpoxyController.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingTrailPagedEpoxyController.kt
new file mode 100644
index 0000000000..603c67d074
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingTrailPagedEpoxyController.kt
@@ -0,0 +1,168 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * 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 im.vector.app.features.settings.devtools
+
+import com.airbnb.epoxy.EpoxyModel
+import com.airbnb.epoxy.paging.PagedListEpoxyController
+import im.vector.app.R
+import im.vector.app.core.date.DateFormatKind
+import im.vector.app.core.date.VectorDateFormatter
+import im.vector.app.core.resources.ColorProvider
+import im.vector.app.core.resources.StringProvider
+import im.vector.app.core.ui.list.GenericItem
+import im.vector.app.core.ui.list.GenericItem_
+import im.vector.app.core.utils.createUIHandler
+import me.gujun.android.span.span
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent
+import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent
+import org.matrix.android.sdk.internal.crypto.model.rest.ForwardedRoomKeyContent
+import org.matrix.android.sdk.internal.crypto.model.rest.GossipingToDeviceObject
+import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyShareRequest
+import org.matrix.android.sdk.internal.crypto.model.rest.SecretShareRequest
+import javax.inject.Inject
+
+class GossipingTrailPagedEpoxyController @Inject constructor(
+        private val stringProvider: StringProvider,
+        private val vectorDateFormatter: VectorDateFormatter,
+        private val colorProvider: ColorProvider
+) : PagedListEpoxyController<Event>(
+        // Important it must match the PageList builder notify Looper
+        modelBuildingHandler = createUIHandler()
+) {
+
+    interface InteractionListener {
+        fun didTap(event: Event)
+    }
+
+    var interactionListener: InteractionListener? = null
+
+    override fun buildItemModel(currentPosition: Int, item: Event?): EpoxyModel<*> {
+        val event = item ?: return GenericItem_().apply { id(currentPosition) }
+        return GenericItem_().apply {
+            id(event.hashCode())
+            itemClickAction(GenericItem.Action("view").apply { perform = Runnable { interactionListener?.didTap(event) } })
+            title(
+                    if (event.isEncrypted()) {
+                        "${event.getClearType()} [encrypted]"
+                    } else {
+                        event.type
+                    }
+            )
+            description(
+                    span {
+                        +vectorDateFormatter.format(event.ageLocalTs, DateFormatKind.DEFAULT_DATE_AND_TIME)
+                        span("\nfrom: ") {
+                            textStyle = "bold"
+                        }
+                        +"${event.senderId}"
+                        apply {
+                            if (event.getClearType() == EventType.ROOM_KEY_REQUEST) {
+                                val content = event.getClearContent().toModel<RoomKeyShareRequest>()
+                                span("\nreqId:") {
+                                    textStyle = "bold"
+                                }
+                                +" ${content?.requestId}"
+                                span("\naction:") {
+                                    textStyle = "bold"
+                                }
+                                +" ${content?.action}"
+                                if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
+                                    span("\nsessionId:") {
+                                        textStyle = "bold"
+                                    }
+                                    +" ${content.body?.sessionId}"
+                                }
+                                span("\nrequestedBy: ") {
+                                    textStyle = "bold"
+                                }
+                                +"${content?.requestingDeviceId}"
+                            } else if (event.getClearType() == EventType.FORWARDED_ROOM_KEY) {
+                                val encryptedContent = event.content.toModel<OlmEventContent>()
+                                val content = event.getClearContent().toModel<ForwardedRoomKeyContent>()
+                                if (event.mxDecryptionResult == null) {
+                                    span("**Failed to Decrypt** ${event.mCryptoError}") {
+                                        textColor = colorProvider.getColor(R.color.vector_error_color)
+                                    }
+                                }
+                                span("\nsessionId:") {
+                                    textStyle = "bold"
+                                }
+                                +" ${content?.sessionId}"
+                                span("\nFrom Device (sender key):") {
+                                    textStyle = "bold"
+                                }
+                                +" ${encryptedContent?.senderKey}"
+                            } else if (event.getClearType() == EventType.ROOM_KEY) {
+                                // it's a bit of a fake event for trail reasons
+                                val content = event.getClearContent()
+                                span("\nsessionId:") {
+                                    textStyle = "bold"
+                                }
+                                +" ${content?.get("session_id")}"
+                                span("\nroomId:") {
+                                    textStyle = "bold"
+                                }
+                                +" ${content?.get("room_id")}"
+                                span("\nTo :") {
+                                    textStyle = "bold"
+                                }
+                                +" ${content?.get("_dest") ?: "me"}"
+                            } else if (event.getClearType() == EventType.SEND_SECRET) {
+                                val content = event.getClearContent().toModel<SecretSendEventContent>()
+
+                                span("\nrequestId:") {
+                                    textStyle = "bold"
+                                }
+                                +" ${content?.requestId}"
+                                span("\nFrom Device:") {
+                                    textStyle = "bold"
+                                }
+                                +" ${event.mxDecryptionResult?.payload?.get("sender_device")}"
+                            } else if (event.getClearType() == EventType.REQUEST_SECRET) {
+                                val content = event.getClearContent().toModel<SecretShareRequest>()
+                                span("\nreqId:") {
+                                    textStyle = "bold"
+                                }
+                                +" ${content?.requestId}"
+                                span("\naction:") {
+                                    textStyle = "bold"
+                                }
+                                +" ${content?.action}"
+                                if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
+                                    span("\nsecretName:") {
+                                        textStyle = "bold"
+                                    }
+                                    +" ${content.secretName}"
+                                }
+                                span("\nrequestedBy: ") {
+                                    textStyle = "bold"
+                                }
+                                +"${content?.requestingDeviceId}"
+                            } else if (event.getClearType() == EventType.ENCRYPTED) {
+                                span("**Failed to Decrypt** ${event.mCryptoError}") {
+                                        textColor = colorProvider.getColor(R.color.vector_error_color)
+                                    }
+                            }
+                        }
+                    }
+            )
+        }
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestListFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestListFragment.kt
index c7b95ddf78..35f46d9c74 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestListFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestListFragment.kt
@@ -24,14 +24,12 @@ import im.vector.app.R
 import im.vector.app.core.extensions.cleanup
 import im.vector.app.core.extensions.configureWith
 import im.vector.app.core.platform.VectorBaseFragment
-import im.vector.app.core.resources.ColorProvider
 import kotlinx.android.synthetic.main.fragment_generic_recycler.*
 import javax.inject.Inject
 
 class IncomingKeyRequestListFragment @Inject constructor(
         val viewModelFactory: KeyRequestListViewModel.Factory,
-        private val epoxyController: KeyRequestEpoxyController,
-        private val colorProvider: ColorProvider
+        private val epoxyController: IncomingKeyRequestPagedController
 ) : VectorBaseFragment() {
 
     override fun getLayoutResId() = R.layout.fragment_generic_recycler
@@ -39,8 +37,10 @@ class IncomingKeyRequestListFragment @Inject constructor(
     private val viewModel: KeyRequestListViewModel by fragmentViewModel(KeyRequestListViewModel::class)
 
     override fun invalidate() = withState(viewModel) { state ->
-        epoxyController.outgoing = false
-        epoxyController.setData(state)
+        state.incomingRequests.invoke()?.let {
+            epoxyController.submitList(it)
+        }
+        Unit
     }
 
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestPagedController.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestPagedController.kt
new file mode 100644
index 0000000000..1510d47180
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestPagedController.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * 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 im.vector.app.features.settings.devtools
+
+import com.airbnb.epoxy.EpoxyModel
+import com.airbnb.epoxy.paging.PagedListEpoxyController
+import im.vector.app.core.date.DateFormatKind
+import im.vector.app.core.date.VectorDateFormatter
+import im.vector.app.core.ui.list.GenericItem_
+import im.vector.app.core.utils.createUIHandler
+import me.gujun.android.span.span
+import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest
+import javax.inject.Inject
+
+class IncomingKeyRequestPagedController @Inject constructor(
+        private val vectorDateFormatter: VectorDateFormatter
+) : PagedListEpoxyController<IncomingRoomKeyRequest>(
+        // Important it must match the PageList builder notify Looper
+        modelBuildingHandler = createUIHandler()
+) {
+
+    interface InteractionListener {
+        // fun didTap(data: UserAccountData)
+    }
+
+    var interactionListener: InteractionListener? = null
+
+    override fun buildItemModel(currentPosition: Int, item: IncomingRoomKeyRequest?): EpoxyModel<*> {
+        val roomKeyRequest = item ?: return GenericItem_().apply { id(currentPosition) }
+
+        return GenericItem_().apply {
+            id(roomKeyRequest.requestId)
+            title(roomKeyRequest.requestId)
+            description(
+                    span {
+                        span("From user: ${roomKeyRequest.userId}")
+                        +vectorDateFormatter.format(roomKeyRequest.localCreationTimestamp, DateFormatKind.DEFAULT_DATE_AND_TIME)
+                        span("sessionId:") {
+                            textStyle = "bold"
+                        }
+                        span("\nFrom device:") {
+                            textStyle = "bold"
+                        }
+                        +"${roomKeyRequest.deviceId}"
+                        +"\n${roomKeyRequest.state.name}"
+                    }
+            )
+        }
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestEpoxyController.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestEpoxyController.kt
deleted file mode 100644
index 5907b55b31..0000000000
--- a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestEpoxyController.kt
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- * Copyright (c) 2020 New Vector Ltd
- *
- * 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 im.vector.app.features.settings.devtools
-
-import com.airbnb.epoxy.TypedEpoxyController
-import com.airbnb.mvrx.Fail
-import com.airbnb.mvrx.Loading
-import com.airbnb.mvrx.Success
-import com.airbnb.mvrx.Uninitialized
-import im.vector.app.R
-import im.vector.app.core.epoxy.loadingItem
-import im.vector.app.core.extensions.exhaustive
-import im.vector.app.core.resources.StringProvider
-import im.vector.app.core.ui.list.genericFooterItem
-import im.vector.app.core.ui.list.genericItem
-import im.vector.app.core.ui.list.genericItemHeader
-import me.gujun.android.span.span
-import javax.inject.Inject
-
-class KeyRequestEpoxyController @Inject constructor(
-        private val stringProvider: StringProvider
-) : TypedEpoxyController<KeyRequestListViewState>() {
-
-    interface InteractionListener {
-        // fun didTap(data: UserAccountData)
-    }
-
-    var outgoing = true
-
-    var interactionListener: InteractionListener? = null
-
-    override fun buildModels(data: KeyRequestListViewState?) {
-        if (outgoing) {
-            buildOutgoing(data)
-        } else {
-            buildIncoming(data)
-        }
-    }
-
-    private fun buildIncoming(data: KeyRequestListViewState?) {
-        data?.incomingRequests?.let { async ->
-            when (async) {
-                is Uninitialized,
-                is Loading -> {
-                    loadingItem {
-                        id("loadingOutgoing")
-                        loadingText(stringProvider.getString(R.string.loading))
-                    }
-                }
-                is Fail    -> {
-                    genericItem {
-                        id("failOutgoing")
-                        title(async.error.localizedMessage)
-                    }
-                }
-                is Success -> {
-                    if (async.invoke().isEmpty()) {
-                        genericFooterItem {
-                            id("empty")
-                            text(stringProvider.getString(R.string.no_result_placeholder))
-                        }
-                        return
-                    }
-                    val requestList = async.invoke().groupBy { it.userId }
-
-                    requestList.forEach {
-                        genericItemHeader {
-                            id(it.key)
-                            text("From user: ${it.key}")
-                        }
-                        it.value.forEach { roomKeyRequest ->
-                            genericItem {
-                                id(roomKeyRequest.requestId)
-                                title(roomKeyRequest.requestId)
-                                description(
-                                        span {
-                                            span("sessionId:") {
-                                                textStyle = "bold"
-                                            }
-                                            span("\nFrom device:") {
-                                                textStyle = "bold"
-                                            }
-                                            +"${roomKeyRequest.deviceId}"
-                                            +"\n${roomKeyRequest.state.name}"
-                                        }
-                                )
-                            }
-                        }
-                    }
-                }
-            }.exhaustive
-        }
-    }
-
-    private fun buildOutgoing(data: KeyRequestListViewState?) {
-        data?.outgoingRoomKeyRequests?.let { async ->
-            when (async) {
-                is Uninitialized,
-                is Loading -> {
-                    loadingItem {
-                        id("loadingOutgoing")
-                        loadingText(stringProvider.getString(R.string.loading))
-                    }
-                }
-                is Fail    -> {
-                    genericItem {
-                        id("failOutgoing")
-                        title(async.error.localizedMessage)
-                    }
-                }
-                is Success -> {
-                    if (async.invoke().isEmpty()) {
-                        genericFooterItem {
-                            id("empty")
-                            text(stringProvider.getString(R.string.no_result_placeholder))
-                        }
-                        return
-                    }
-
-                    val requestList = async.invoke().groupBy { it.roomId }
-
-                    requestList.forEach {
-                        genericItemHeader {
-                            id(it.key)
-                            text("roomId: ${it.key}")
-                        }
-                        it.value.forEach { roomKeyRequest ->
-                            genericItem {
-                                id(roomKeyRequest.requestId)
-                                title(roomKeyRequest.requestId)
-                                description(
-                                        span {
-                                            span("sessionId:\n") {
-                                                textStyle = "bold"
-                                            }
-                                            +"${roomKeyRequest.sessionId}"
-                                            span("\nstate:") {
-                                                textStyle = "bold"
-                                            }
-                                            +"\n${roomKeyRequest.state.name}"
-                                        }
-                                )
-                            }
-                        }
-                    }
-                }
-            }.exhaustive
-        }
-    }
-}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestListViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestListViewModel.kt
index 22093763e1..0b0b923a48 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestListViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestListViewModel.kt
@@ -17,11 +17,11 @@
 package im.vector.app.features.settings.devtools
 
 import androidx.lifecycle.viewModelScope
+import androidx.paging.PagedList
 import com.airbnb.mvrx.Async
 import com.airbnb.mvrx.FragmentViewModelContext
 import com.airbnb.mvrx.MvRxState
 import com.airbnb.mvrx.MvRxViewModelFactory
-import com.airbnb.mvrx.Success
 import com.airbnb.mvrx.Uninitialized
 import com.airbnb.mvrx.ViewModelContext
 import com.squareup.inject.assisted.Assisted
@@ -33,10 +33,11 @@ import kotlinx.coroutines.launch
 import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest
 import org.matrix.android.sdk.internal.crypto.OutgoingRoomKeyRequest
+import org.matrix.android.sdk.rx.asObservable
 
 data class KeyRequestListViewState(
-        val incomingRequests: Async<List<IncomingRoomKeyRequest>> = Uninitialized,
-        val outgoingRoomKeyRequests: Async<List<OutgoingRoomKeyRequest>> = Uninitialized
+        val incomingRequests: Async<PagedList<IncomingRoomKeyRequest>> = Uninitialized,
+        val outgoingRoomKeyRequests: Async<PagedList<OutgoingRoomKeyRequest>> = Uninitialized
 ) : MvRxState
 
 class KeyRequestListViewModel @AssistedInject constructor(@Assisted initialState: KeyRequestListViewState,
@@ -49,20 +50,16 @@ class KeyRequestListViewModel @AssistedInject constructor(@Assisted initialState
 
     fun refresh() {
         viewModelScope.launch {
-            session.cryptoService().getOutgoingRoomKeyRequests().let {
-                setState {
-                    copy(
-                            outgoingRoomKeyRequests = Success(it)
-                    )
-                }
-            }
-            session.cryptoService().getIncomingRoomKeyRequests().let {
-                setState {
-                    copy(
-                            incomingRequests = Success(it)
-                    )
-                }
-            }
+            session.cryptoService().getOutgoingRoomKeyRequestsPaged().asObservable()
+                    .execute {
+                        copy(outgoingRoomKeyRequests = it)
+                    }
+
+            session.cryptoService().getIncomingRoomKeyRequestsPaged()
+                    .asObservable()
+                    .execute {
+                        copy(incomingRequests = it)
+                    }
         }
     }
 
diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt
new file mode 100644
index 0000000000..be8778e1db
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * 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 im.vector.app.features.settings.devtools
+
+import android.net.Uri
+import androidx.lifecycle.viewModelScope
+import com.airbnb.mvrx.Async
+import com.airbnb.mvrx.Fail
+import com.airbnb.mvrx.FragmentViewModelContext
+import com.airbnb.mvrx.Loading
+import com.airbnb.mvrx.MvRxState
+import com.airbnb.mvrx.MvRxViewModelFactory
+import com.airbnb.mvrx.Success
+import com.airbnb.mvrx.Uninitialized
+import com.airbnb.mvrx.ViewModelContext
+import com.squareup.inject.assisted.Assisted
+import com.squareup.inject.assisted.AssistedInject
+import im.vector.app.core.platform.VectorViewEvents
+import im.vector.app.core.platform.VectorViewModel
+import im.vector.app.core.platform.VectorViewModelAction
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import me.gujun.android.span.span
+import org.matrix.android.sdk.api.session.Session
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent
+import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent
+import org.matrix.android.sdk.internal.crypto.model.rest.ForwardedRoomKeyContent
+import org.matrix.android.sdk.internal.crypto.model.rest.GossipingToDeviceObject
+import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyShareRequest
+import org.matrix.android.sdk.internal.crypto.model.rest.SecretShareRequest
+
+sealed class KeyRequestAction : VectorViewModelAction {
+    data class ExportAudit(val uri: Uri) : KeyRequestAction()
+}
+
+sealed class KeyRequestEvents : VectorViewEvents {
+    data class SaveAudit(val uri: Uri, val raw: String) : KeyRequestEvents()
+}
+
+data class KeyRequestViewState(
+        val exporting: Async<String> = Uninitialized
+) : MvRxState
+
+class KeyRequestViewModel @AssistedInject constructor(
+        @Assisted initialState: KeyRequestViewState,
+        private val session: Session)
+    : VectorViewModel<KeyRequestViewState, KeyRequestAction, KeyRequestEvents>(initialState) {
+
+    @AssistedInject.Factory
+    interface Factory {
+        fun create(initialState: KeyRequestViewState): KeyRequestViewModel
+    }
+
+    companion object : MvRxViewModelFactory<KeyRequestViewModel, KeyRequestViewState> {
+
+        @JvmStatic
+        override fun create(viewModelContext: ViewModelContext, state: KeyRequestViewState): KeyRequestViewModel? {
+            val fragment: KeyRequestsFragment = (viewModelContext as FragmentViewModelContext).fragment()
+            return fragment.viewModelFactory.create(state)
+        }
+    }
+
+    override fun handle(action: KeyRequestAction) {
+        when (action) {
+            is KeyRequestAction.ExportAudit -> {
+                setState {
+                    copy(exporting = Loading())
+                }
+                viewModelScope.launch(Dispatchers.IO) {
+                    try {
+                        // this can take long
+                        val eventList = session.cryptoService().getGossipingEvents()
+                        // clean it a bit to
+                        val stringBuilder = StringBuilder()
+                        eventList.forEach {
+                            val clearType = it.getClearType()
+                            stringBuilder.append("[${it.ageLocalTs}] : $clearType from:${it.senderId} - ")
+                            when (clearType) {
+                                EventType.ROOM_KEY_REQUEST   -> {
+                                    val content = it.getClearContent().toModel<RoomKeyShareRequest>()
+                                    stringBuilder.append("reqId:${content?.requestId}  action:${content?.action} ")
+                                    if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
+                                        stringBuilder.append("sessionId: ${content.body?.sessionId} ")
+                                    }
+                                    stringBuilder.append("requestedBy: ${content?.requestingDeviceId} ")
+                                    stringBuilder.append("\n")
+                                }
+                                EventType.FORWARDED_ROOM_KEY -> {
+                                    val encryptedContent = it.content.toModel<OlmEventContent>()
+                                    val content = it.getClearContent().toModel<ForwardedRoomKeyContent>()
+
+                                    stringBuilder.append("sessionId:${content?.sessionId}  From Device (sender key):${encryptedContent?.senderKey} ")
+                                    span("\nFrom Device (sender key):") {
+                                        textStyle = "bold"
+                                    }
+                                    stringBuilder.append("\n")
+                                }
+                                EventType.ROOM_KEY           -> {
+                                    val content = it.getClearContent()
+                                    stringBuilder.append("sessionId:${content?.get("session_id")} roomId:${content?.get("room_id")} dest:${content?.get("_dest") ?: "me"}")
+                                    stringBuilder.append("\n")
+                                }
+                                EventType.SEND_SECRET        -> {
+                                    val content = it.getClearContent().toModel<SecretSendEventContent>()
+                                    stringBuilder.append("requestId:${content?.requestId} From Device:${it.mxDecryptionResult?.payload?.get("sender_device")}")
+                                }
+                                EventType.REQUEST_SECRET     -> {
+                                    val content = it.getClearContent().toModel<SecretShareRequest>()
+                                    stringBuilder.append("reqId:${content?.requestId} action:${content?.action} ")
+                                    if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
+                                        stringBuilder.append("secretName:${content.secretName} ")
+                                    }
+                                    stringBuilder.append("requestedBy:${content?.requestingDeviceId}")
+                                    stringBuilder.append("\n")
+                                }
+                                EventType.ENCRYPTED          -> {
+                                    stringBuilder.append("Failed to Derypt \n")
+                                }
+                                else                         -> {
+                                    stringBuilder.append("?? \n")
+                                }
+                            }
+                        }
+                        val raw = stringBuilder.toString()
+                        setState {
+                            copy(exporting = Success(""))
+                        }
+                        _viewEvents.post(KeyRequestEvents.SaveAudit(action.uri, raw))
+                    } catch (error: Throwable) {
+                        setState {
+                            copy(exporting = Fail(error))
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt
index 08016c66ac..0ea0e9de31 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt
@@ -16,20 +16,30 @@
 
 package im.vector.app.features.settings.devtools
 
+import android.app.Activity
 import android.os.Bundle
+import android.view.MenuItem
 import android.view.View
+import androidx.core.view.isVisible
 import androidx.fragment.app.Fragment
 import androidx.viewpager2.adapter.FragmentStateAdapter
 import androidx.viewpager2.widget.ViewPager2
 import androidx.viewpager2.widget.ViewPager2.SCROLL_STATE_IDLE
+import com.airbnb.mvrx.Loading
+import com.airbnb.mvrx.fragmentViewModel
+import com.airbnb.mvrx.withState
 import com.google.android.material.tabs.TabLayoutMediator
 import im.vector.app.R
+import im.vector.app.core.extensions.registerStartForActivityResult
 import im.vector.app.core.platform.VectorBaseActivity
 import im.vector.app.core.platform.VectorBaseFragment
+import im.vector.app.core.utils.selectTxtFileToWrite
 import kotlinx.android.synthetic.main.fragment_devtool_keyrequests.*
+import org.matrix.android.sdk.api.extensions.tryOrNull
 import javax.inject.Inject
 
-class KeyRequestsFragment @Inject constructor() : VectorBaseFragment() {
+class KeyRequestsFragment @Inject constructor(
+        val viewModelFactory: KeyRequestViewModel.Factory) : VectorBaseFragment() {
 
     override fun getLayoutResId(): Int = R.layout.fragment_devtool_keyrequests
 
@@ -40,6 +50,10 @@ class KeyRequestsFragment @Inject constructor() : VectorBaseFragment() {
 
     private var mPagerAdapter: KeyReqPagerAdapter? = null
 
+    private val viewModel: KeyRequestViewModel by fragmentViewModel()
+
+    override fun getMenuRes(): Int = R.menu.menu_audit
+
     private val pageAdapterListener = object : ViewPager2.OnPageChangeCallback() {
         override fun onPageSelected(position: Int) {
             invalidateOptionsMenu()
@@ -53,6 +67,13 @@ class KeyRequestsFragment @Inject constructor() : VectorBaseFragment() {
         }
     }
 
+    override fun invalidate() = withState(viewModel) {
+        when (it.exporting) {
+            is Loading -> exportWaitingView.isVisible = true
+            else       -> exportWaitingView.isVisible = false
+        }
+    }
+
     override fun onDestroy() {
         invalidateOptionsMenu()
         super.onDestroy()
@@ -77,6 +98,23 @@ class KeyRequestsFragment @Inject constructor() : VectorBaseFragment() {
                 }
             }
         }.attach()
+
+        viewModel.observeViewEvents {
+            when (it) {
+                is KeyRequestEvents.SaveAudit -> {
+                    tryOrNull {
+                        val os = requireContext().contentResolver?.openOutputStream(it.uri)
+                        if (os == null) {
+                            false
+                        } else {
+                            os.write(it.raw.toByteArray())
+                            os.flush()
+                            true
+                        }
+                    }
+                }
+            }
+        }
     }
 
     override fun onDestroyView() {
@@ -85,6 +123,28 @@ class KeyRequestsFragment @Inject constructor() : VectorBaseFragment() {
         super.onDestroyView()
     }
 
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        if (item.itemId == R.id.audit_export) {
+            selectTxtFileToWrite(
+                    activity = requireActivity(),
+                    activityResultLauncher = epxortAuditForActivityResult,
+                    defaultFileName = "audit-export-json_${System.currentTimeMillis()}.txt",
+                    chooserHint = "Export Audit"
+            )
+            return true
+        }
+        return super.onOptionsItemSelected(item)
+    }
+
+    private val epxortAuditForActivityResult = registerStartForActivityResult { activityResult ->
+        if (activityResult.resultCode == Activity.RESULT_OK) {
+            val uri = activityResult.data?.data
+            if (uri != null) {
+                viewModel.handle(KeyRequestAction.ExportAudit(uri))
+            }
+        }
+    }
+
     private inner class KeyReqPagerAdapter(fa: Fragment) : FragmentStateAdapter(fa) {
         override fun getItemCount(): Int = 3
 
diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestListFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestListFragment.kt
index 60e73fb74d..a82b5dd6c9 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestListFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestListFragment.kt
@@ -24,21 +24,19 @@ import im.vector.app.R
 import im.vector.app.core.extensions.cleanup
 import im.vector.app.core.extensions.configureWith
 import im.vector.app.core.platform.VectorBaseFragment
-import im.vector.app.core.resources.ColorProvider
 import kotlinx.android.synthetic.main.fragment_generic_recycler.*
 import javax.inject.Inject
 
 class OutgoingKeyRequestListFragment @Inject constructor(
         val viewModelFactory: KeyRequestListViewModel.Factory,
-        private val epoxyController: KeyRequestEpoxyController,
-        private val colorProvider: ColorProvider
+        private val epoxyController: OutgoingKeyRequestPagedController
 ) : VectorBaseFragment() {
 
     override fun getLayoutResId() = R.layout.fragment_generic_recycler
     private val viewModel: KeyRequestListViewModel by fragmentViewModel(KeyRequestListViewModel::class)
 
     override fun invalidate() = withState(viewModel) { state ->
-        epoxyController.setData(state)
+        epoxyController.submitList(state.outgoingRoomKeyRequests.invoke())
     }
 
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestPagedController.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestPagedController.kt
new file mode 100644
index 0000000000..38c4c8153f
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestPagedController.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * 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 im.vector.app.features.settings.devtools
+
+import com.airbnb.epoxy.EpoxyModel
+import com.airbnb.epoxy.paging.PagedListEpoxyController
+import im.vector.app.core.ui.list.GenericItem_
+import im.vector.app.core.utils.createUIHandler
+import me.gujun.android.span.span
+import org.matrix.android.sdk.internal.crypto.OutgoingRoomKeyRequest
+import javax.inject.Inject
+
+class OutgoingKeyRequestPagedController @Inject constructor() : PagedListEpoxyController<OutgoingRoomKeyRequest>(
+        // Important it must match the PageList builder notify Looper
+        modelBuildingHandler = createUIHandler()
+) {
+
+    interface InteractionListener {
+        // fun didTap(data: UserAccountData)
+    }
+
+    var interactionListener: InteractionListener? = null
+
+    override fun buildItemModel(currentPosition: Int, item: OutgoingRoomKeyRequest?): EpoxyModel<*> {
+        val roomKeyRequest = item ?: return GenericItem_().apply { id(currentPosition) }
+
+        return GenericItem_().apply {
+            id(roomKeyRequest.requestId)
+            title(roomKeyRequest.requestId)
+            description(
+                    span {
+                        span("roomId:\n") {
+                            textStyle = "bold"
+                        }
+                        +"${roomKeyRequest.roomId}"
+
+                        span("sessionId:\n") {
+                            textStyle = "bold"
+                        }
+                        +"${roomKeyRequest.sessionId}"
+                        span("\nstate:") {
+                            textStyle = "bold"
+                        }
+                        +"\n${roomKeyRequest.state.name}"
+                    }
+            )
+        }
+    }
+}
diff --git a/vector/src/main/res/layout/fragment_devtool_keyrequests.xml b/vector/src/main/res/layout/fragment_devtool_keyrequests.xml
index ccd3cee660..dd0cbff1b1 100644
--- a/vector/src/main/res/layout/fragment_devtool_keyrequests.xml
+++ b/vector/src/main/res/layout/fragment_devtool_keyrequests.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
@@ -11,12 +11,24 @@
         android:id="@+id/devToolKeyRequestTabs"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
+        app:layout_constraintTop_toTopOf="parent"
         app:tabMode="scrollable" />
 
     <androidx.viewpager2.widget.ViewPager2
+        app:layout_constraintTop_toBottomOf="@id/devToolKeyRequestTabs"
+        app:layout_constraintBottom_toBottomOf="parent"
         android:id="@+id/devToolKeyRequestPager"
         android:layout_width="match_parent"
-        android:layout_height="0dp"
-        android:layout_weight="1" />
+        android:layout_height="0dp" />
 
-</LinearLayout>
\ No newline at end of file
+    <ProgressBar
+        android:id="@+id/exportWaitingView"
+        android:visibility="gone"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        android:layout_width="40dp"
+        android:layout_height="40dp"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/vector/src/main/res/menu/menu_audit.xml b/vector/src/main/res/menu/menu_audit.xml
new file mode 100644
index 0000000000..1c0d2f9989
--- /dev/null
+++ b/vector/src/main/res/menu/menu_audit.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item
+        android:id="@+id/audit_export"
+        android:enabled="true"
+        android:icon="@drawable/ic_material_save"
+        android:title="@string/settings_export_trail" />
+
+</menu>
\ No newline at end of file
diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
index dd461123cc..1178e5aeb4 100644
--- a/vector/src/main/res/values/strings.xml
+++ b/vector/src/main/res/values/strings.xml
@@ -2304,6 +2304,7 @@
     <string name="login_default_session_public_name">Element Android</string>
 
     <string name="settings_key_requests">Key Requests</string>
+    <string name="settings_export_trail">Export Audit</string>
 
     <string name="e2e_use_keybackup">Unlock encrypted messages history</string>
 

From a1ed9bb377b12b5317b86c951d6ea4bacd96d4c3 Mon Sep 17 00:00:00 2001
From: Valere <valeref@matrix.org>
Date: Wed, 28 Oct 2020 17:47:05 +0100
Subject: [PATCH 14/23] Fix / missing new line in secret trail

---
 .../vector/app/features/settings/devtools/KeyRequestViewModel.kt | 1 +
 1 file changed, 1 insertion(+)

diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt
index be8778e1db..aca374afd9 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt
@@ -119,6 +119,7 @@ class KeyRequestViewModel @AssistedInject constructor(
                                 EventType.SEND_SECRET        -> {
                                     val content = it.getClearContent().toModel<SecretSendEventContent>()
                                     stringBuilder.append("requestId:${content?.requestId} From Device:${it.mxDecryptionResult?.payload?.get("sender_device")}")
+                                    stringBuilder.append("\n")
                                 }
                                 EventType.REQUEST_SECRET     -> {
                                     val content = it.getClearContent().toModel<SecretShareRequest>()

From 794a0bb14b1b2682d6b071e705885ff2e9827666 Mon Sep 17 00:00:00 2001
From: Valere <valeref@matrix.org>
Date: Wed, 28 Oct 2020 17:48:55 +0100
Subject: [PATCH 15/23] Fix / bad ref to json in filename, it's txt

---
 .../app/features/settings/devtools/KeyRequestsFragment.kt       | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt
index 0ea0e9de31..3f4d24c6ea 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt
@@ -128,7 +128,7 @@ class KeyRequestsFragment @Inject constructor(
             selectTxtFileToWrite(
                     activity = requireActivity(),
                     activityResultLauncher = epxortAuditForActivityResult,
-                    defaultFileName = "audit-export-json_${System.currentTimeMillis()}.txt",
+                    defaultFileName = "audit-export_${System.currentTimeMillis()}.txt",
                     chooserHint = "Export Audit"
             )
             return true

From f190356934bdbe9f97b622a13b449722f02f9a98 Mon Sep 17 00:00:00 2001
From: Benoit Marty <benoit@matrix.org>
Date: Thu, 29 Oct 2020 13:44:47 +0100
Subject: [PATCH 16/23] Fix compilation warnings

---
 .../sdk/internal/crypto/IncomingGossipingRequestManager.kt    | 2 +-
 .../android/sdk/internal/crypto/store/IMXCryptoStore.kt       | 4 +++-
 2 files changed, 4 insertions(+), 2 deletions(-)

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 ae017b33f5..97ae0b9d83 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
@@ -105,7 +105,7 @@ internal class IncomingGossipingRequestManager @Inject constructor(
     fun onGossipingRequestEvent(event: Event) {
         Timber.v("## CRYPTO | GOSSIP onGossipingRequestEvent type ${event.type} from user ${event.senderId}")
         val roomKeyShare = event.getClearContent().toModel<GossipingDefaultContent>()
-        val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
+        // val ageLocalTs = event.unsignedData?.age?.let { System.currentTimeMillis() - it }
         when (roomKeyShare?.action) {
             GossipingToDeviceObject.ACTION_SHARE_REQUEST      -> {
                 if (event.getClearType() == EventType.REQUEST_SECRET) {
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 74773384ae..72d541d4df 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
@@ -127,8 +127,10 @@ internal interface IMXCryptoStore {
     fun getPendingIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
 
     fun getPendingIncomingGossipingRequests(): List<IncomingShareRequestCommon>
+
     fun storeIncomingGossipingRequest(request: IncomingShareRequestCommon, ageLocalTS: Long?)
-    fun storeIncomingGossipingRequests(request: List<IncomingShareRequestCommon>)
+
+    fun storeIncomingGossipingRequests(requests: List<IncomingShareRequestCommon>)
 //    fun getPendingIncomingSecretShareRequests(): List<IncomingSecretShareRequest>
 
     /**

From f48d4c021a36e9b07ddb5faeb55ef0570f913aa1 Mon Sep 17 00:00:00 2001
From: Benoit Marty <benoit@matrix.org>
Date: Thu, 29 Oct 2020 14:10:41 +0100
Subject: [PATCH 17/23] Cleanup and split long lines

---
 .../crypto/store/db/RealmCryptoStore.kt       |   4 +-
 .../settings/devtools/KeyRequestViewModel.kt  | 129 +++++++++---------
 .../settings/devtools/KeyRequestsFragment.kt  |  13 +-
 3 files changed, 70 insertions(+), 76 deletions(-)

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 7644ab5cc2..5d19e6d607 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
@@ -1124,7 +1124,7 @@ internal class RealmCryptoStore @Inject constructor(
                         content = ContentMapper.map(event.content)
                 ).apply {
                     sendState = SendState.SYNCED
-                    decryptionResultJson = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult)
+                    decryptionResultJson = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult)
                     decryptionErrorCode = event.mCryptoError?.name
                 }
                 realm.insertOrUpdate(entity)
@@ -1143,7 +1143,7 @@ internal class RealmCryptoStore @Inject constructor(
                     content = ContentMapper.map(event.content)
             ).apply {
                 sendState = SendState.SYNCED
-                decryptionResultJson = MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult)
+                decryptionResultJson = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).toJson(event.mxDecryptionResult)
                 decryptionErrorCode = event.mCryptoError?.name
             }
             realm.insertOrUpdate(entity)
diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt
index aca374afd9..e64f673309 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt
@@ -29,6 +29,7 @@ import com.airbnb.mvrx.Uninitialized
 import com.airbnb.mvrx.ViewModelContext
 import com.squareup.inject.assisted.Assisted
 import com.squareup.inject.assisted.AssistedInject
+import im.vector.app.core.extensions.exhaustive
 import im.vector.app.core.platform.VectorViewEvents
 import im.vector.app.core.platform.VectorViewModel
 import im.vector.app.core.platform.VectorViewModelAction
@@ -54,7 +55,7 @@ sealed class KeyRequestEvents : VectorViewEvents {
 }
 
 data class KeyRequestViewState(
-        val exporting: Async<String> = Uninitialized
+        val exporting: Async<Unit> = Uninitialized
 ) : MvRxState
 
 class KeyRequestViewModel @AssistedInject constructor(
@@ -78,77 +79,75 @@ class KeyRequestViewModel @AssistedInject constructor(
 
     override fun handle(action: KeyRequestAction) {
         when (action) {
-            is KeyRequestAction.ExportAudit -> {
-                setState {
-                    copy(exporting = Loading())
-                }
-                viewModelScope.launch(Dispatchers.IO) {
-                    try {
-                        // this can take long
-                        val eventList = session.cryptoService().getGossipingEvents()
-                        // clean it a bit to
-                        val stringBuilder = StringBuilder()
-                        eventList.forEach {
-                            val clearType = it.getClearType()
-                            stringBuilder.append("[${it.ageLocalTs}] : $clearType from:${it.senderId} - ")
-                            when (clearType) {
-                                EventType.ROOM_KEY_REQUEST   -> {
-                                    val content = it.getClearContent().toModel<RoomKeyShareRequest>()
-                                    stringBuilder.append("reqId:${content?.requestId}  action:${content?.action} ")
-                                    if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
-                                        stringBuilder.append("sessionId: ${content.body?.sessionId} ")
-                                    }
-                                    stringBuilder.append("requestedBy: ${content?.requestingDeviceId} ")
-                                    stringBuilder.append("\n")
-                                }
-                                EventType.FORWARDED_ROOM_KEY -> {
-                                    val encryptedContent = it.content.toModel<OlmEventContent>()
-                                    val content = it.getClearContent().toModel<ForwardedRoomKeyContent>()
+            is KeyRequestAction.ExportAudit -> exportAudit(action)
+        }.exhaustive
+    }
 
-                                    stringBuilder.append("sessionId:${content?.sessionId}  From Device (sender key):${encryptedContent?.senderKey} ")
-                                    span("\nFrom Device (sender key):") {
-                                        textStyle = "bold"
-                                    }
-                                    stringBuilder.append("\n")
+    private fun exportAudit(action: KeyRequestAction.ExportAudit) {
+        setState {
+            copy(exporting = Loading())
+        }
+        viewModelScope.launch(Dispatchers.IO) {
+            try {
+                // this can take long
+                val eventList = session.cryptoService().getGossipingEvents()
+                // clean it a bit to
+                val raw = buildString {
+                    eventList.forEach {
+                        val clearType = it.getClearType()
+                        append("[${it.ageLocalTs}] : $clearType from:${it.senderId} - ")
+                        when (clearType) {
+                            EventType.ROOM_KEY_REQUEST   -> {
+                                val content = it.getClearContent().toModel<RoomKeyShareRequest>()
+                                append("reqId:${content?.requestId} action:${content?.action} ")
+                                if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
+                                    append("sessionId: ${content.body?.sessionId} ")
                                 }
-                                EventType.ROOM_KEY           -> {
-                                    val content = it.getClearContent()
-                                    stringBuilder.append("sessionId:${content?.get("session_id")} roomId:${content?.get("room_id")} dest:${content?.get("_dest") ?: "me"}")
-                                    stringBuilder.append("\n")
-                                }
-                                EventType.SEND_SECRET        -> {
-                                    val content = it.getClearContent().toModel<SecretSendEventContent>()
-                                    stringBuilder.append("requestId:${content?.requestId} From Device:${it.mxDecryptionResult?.payload?.get("sender_device")}")
-                                    stringBuilder.append("\n")
-                                }
-                                EventType.REQUEST_SECRET     -> {
-                                    val content = it.getClearContent().toModel<SecretShareRequest>()
-                                    stringBuilder.append("reqId:${content?.requestId} action:${content?.action} ")
-                                    if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
-                                        stringBuilder.append("secretName:${content.secretName} ")
-                                    }
-                                    stringBuilder.append("requestedBy:${content?.requestingDeviceId}")
-                                    stringBuilder.append("\n")
-                                }
-                                EventType.ENCRYPTED          -> {
-                                    stringBuilder.append("Failed to Derypt \n")
-                                }
-                                else                         -> {
-                                    stringBuilder.append("?? \n")
+                                append("requestedBy: ${content?.requestingDeviceId}")
+                            }
+                            EventType.FORWARDED_ROOM_KEY -> {
+                                val encryptedContent = it.content.toModel<OlmEventContent>()
+                                val content = it.getClearContent().toModel<ForwardedRoomKeyContent>()
+
+                                append("sessionId:${content?.sessionId} From Device (sender key):${encryptedContent?.senderKey}")
+                                span("\nFrom Device (sender key):") {
+                                    textStyle = "bold"
                                 }
                             }
+                            EventType.ROOM_KEY           -> {
+                                val content = it.getClearContent()
+                                append("sessionId:${content?.get("session_id")} roomId:${content?.get("room_id")} dest:${content?.get("_dest") ?: "me"}")
+                            }
+                            EventType.SEND_SECRET        -> {
+                                val content = it.getClearContent().toModel<SecretSendEventContent>()
+                                append("requestId:${content?.requestId} From Device:${it.mxDecryptionResult?.payload?.get("sender_device")}")
+                            }
+                            EventType.REQUEST_SECRET     -> {
+                                val content = it.getClearContent().toModel<SecretShareRequest>()
+                                append("reqId:${content?.requestId} action:${content?.action} ")
+                                if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
+                                    append("secretName:${content.secretName} ")
+                                }
+                                append("requestedBy:${content?.requestingDeviceId}")
+                            }
+                            EventType.ENCRYPTED          -> {
+                                append("Failed to Decrypt")
+                            }
+                            else                         -> {
+                                append("??")
+                            }
                         }
-                        val raw = stringBuilder.toString()
-                        setState {
-                            copy(exporting = Success(""))
-                        }
-                        _viewEvents.post(KeyRequestEvents.SaveAudit(action.uri, raw))
-                    } catch (error: Throwable) {
-                        setState {
-                            copy(exporting = Fail(error))
-                        }
+                        append("\n")
                     }
                 }
+                setState {
+                    copy(exporting = Success(Unit))
+                }
+                _viewEvents.post(KeyRequestEvents.SaveAudit(action.uri, raw))
+            } catch (error: Throwable) {
+                setState {
+                    copy(exporting = Fail(error))
+                }
             }
         }
     }
diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt
index 3f4d24c6ea..ff91d9bba3 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestsFragment.kt
@@ -30,6 +30,7 @@ import com.airbnb.mvrx.fragmentViewModel
 import com.airbnb.mvrx.withState
 import com.google.android.material.tabs.TabLayoutMediator
 import im.vector.app.R
+import im.vector.app.core.extensions.exhaustive
 import im.vector.app.core.extensions.registerStartForActivityResult
 import im.vector.app.core.platform.VectorBaseActivity
 import im.vector.app.core.platform.VectorBaseFragment
@@ -103,17 +104,11 @@ class KeyRequestsFragment @Inject constructor(
             when (it) {
                 is KeyRequestEvents.SaveAudit -> {
                     tryOrNull {
-                        val os = requireContext().contentResolver?.openOutputStream(it.uri)
-                        if (os == null) {
-                            false
-                        } else {
-                            os.write(it.raw.toByteArray())
-                            os.flush()
-                            true
-                        }
+                        requireContext().contentResolver?.openOutputStream(it.uri)
+                                ?.use { os -> os.write(it.raw.toByteArray()) }
                     }
                 }
-            }
+            }.exhaustive
         }
     }
 

From f6ec7bc323813e83561428942e6a46d0558ae0b7 Mon Sep 17 00:00:00 2001
From: Benoit Marty <benoit@matrix.org>
Date: Thu, 29 Oct 2020 14:11:34 +0100
Subject: [PATCH 18/23] values-v21 does not exist anymore

---
 tools/check/check_code_quality.sh | 1 -
 1 file changed, 1 deletion(-)

diff --git a/tools/check/check_code_quality.sh b/tools/check/check_code_quality.sh
index e855440e81..0b4272cbfe 100755
--- a/tools/check/check_code_quality.sh
+++ b/tools/check/check_code_quality.sh
@@ -91,7 +91,6 @@ ${searchForbiddenStringsScript} ./tools/check/forbidden_strings_in_resources.txt
     ./vector/src/main/res/color \
     ./vector/src/main/res/layout \
     ./vector/src/main/res/values \
-    ./vector/src/main/res/values-v21 \
     ./vector/src/main/res/xml
 
 resultForbiddenStringInResource=$?

From 909ee2cc85dce43a169d606bd56dd741a2a4b6d4 Mon Sep 17 00:00:00 2001
From: Valere <valeref@matrix.org>
Date: Thu, 29 Oct 2020 14:57:17 +0100
Subject: [PATCH 19/23] Fix / bad format of trail item

---
 .../devtools/IncomingKeyRequestPagedController.kt | 15 +++++++++++----
 .../devtools/OutgoingKeyRequestPagedController.kt |  8 ++++----
 2 files changed, 15 insertions(+), 8 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestPagedController.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestPagedController.kt
index 1510d47180..c2bdcfbd04 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestPagedController.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devtools/IncomingKeyRequestPagedController.kt
@@ -47,16 +47,23 @@ class IncomingKeyRequestPagedController @Inject constructor(
             title(roomKeyRequest.requestId)
             description(
                     span {
-                        span("From user: ${roomKeyRequest.userId}")
-                        +vectorDateFormatter.format(roomKeyRequest.localCreationTimestamp, DateFormatKind.DEFAULT_DATE_AND_TIME)
-                        span("sessionId:") {
+                        span("From: ") {
                             textStyle = "bold"
                         }
+                        span("${roomKeyRequest.userId}")
+                        +vectorDateFormatter.format(roomKeyRequest.localCreationTimestamp, DateFormatKind.DEFAULT_DATE_AND_TIME)
+                        span("\nsessionId:") {
+                            textStyle = "bold"
+                        }
+                        +"${roomKeyRequest.requestBody?.sessionId}"
                         span("\nFrom device:") {
                             textStyle = "bold"
                         }
                         +"${roomKeyRequest.deviceId}"
-                        +"\n${roomKeyRequest.state.name}"
+                        span("\nstate: ") {
+                            textStyle = "bold"
+                        }
+                        +roomKeyRequest.state.name
                     }
             )
         }
diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestPagedController.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestPagedController.kt
index 38c4c8153f..c2a3bc9827 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestPagedController.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devtools/OutgoingKeyRequestPagedController.kt
@@ -43,19 +43,19 @@ class OutgoingKeyRequestPagedController @Inject constructor() : PagedListEpoxyCo
             title(roomKeyRequest.requestId)
             description(
                     span {
-                        span("roomId:\n") {
+                        span("roomId: ") {
                             textStyle = "bold"
                         }
                         +"${roomKeyRequest.roomId}"
 
-                        span("sessionId:\n") {
+                        span("\nsessionId: ") {
                             textStyle = "bold"
                         }
                         +"${roomKeyRequest.sessionId}"
-                        span("\nstate:") {
+                        span("\nstate: ") {
                             textStyle = "bold"
                         }
-                        +"\n${roomKeyRequest.state.name}"
+                        +roomKeyRequest.state.name
                     }
             )
         }

From e8dcdc71827ad2c579d9adbd7f2152564dbbc677 Mon Sep 17 00:00:00 2001
From: Benoit Marty <benoit@matrix.org>
Date: Thu, 29 Oct 2020 14:46:42 +0100
Subject: [PATCH 20/23] Format date in the exported trail

---
 .../settings/devtools/KeyRequestViewModel.kt      | 15 ++++++++++++++-
 1 file changed, 14 insertions(+), 1 deletion(-)

diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt
index e64f673309..4385cb2c09 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt
@@ -33,6 +33,7 @@ import im.vector.app.core.extensions.exhaustive
 import im.vector.app.core.platform.VectorViewEvents
 import im.vector.app.core.platform.VectorViewModel
 import im.vector.app.core.platform.VectorViewModelAction
+import im.vector.app.core.resources.DateProvider
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import me.gujun.android.span.span
@@ -45,6 +46,7 @@ import org.matrix.android.sdk.internal.crypto.model.rest.ForwardedRoomKeyContent
 import org.matrix.android.sdk.internal.crypto.model.rest.GossipingToDeviceObject
 import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyShareRequest
 import org.matrix.android.sdk.internal.crypto.model.rest.SecretShareRequest
+import org.threeten.bp.format.DateTimeFormatter
 
 sealed class KeyRequestAction : VectorViewModelAction {
     data class ExportAudit(val uri: Uri) : KeyRequestAction()
@@ -68,6 +70,10 @@ class KeyRequestViewModel @AssistedInject constructor(
         fun create(initialState: KeyRequestViewState): KeyRequestViewModel
     }
 
+    private val full24DateFormatter by lazy {
+        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")
+    }
+
     companion object : MvRxViewModelFactory<KeyRequestViewModel, KeyRequestViewState> {
 
         @JvmStatic
@@ -95,7 +101,7 @@ class KeyRequestViewModel @AssistedInject constructor(
                 val raw = buildString {
                     eventList.forEach {
                         val clearType = it.getClearType()
-                        append("[${it.ageLocalTs}] : $clearType from:${it.senderId} - ")
+                        append("[${getFormattedDate(it.ageLocalTs)}] $clearType from:${it.senderId} - ")
                         when (clearType) {
                             EventType.ROOM_KEY_REQUEST   -> {
                                 val content = it.getClearContent().toModel<RoomKeyShareRequest>()
@@ -151,4 +157,11 @@ class KeyRequestViewModel @AssistedInject constructor(
             }
         }
     }
+
+    private fun getFormattedDate(ageLocalTs: Long?): String {
+        return ageLocalTs
+                ?.let { DateProvider.toLocalDateTime(it) }
+                ?.let { full24DateFormatter.format(it) }
+                ?: "?"
+    }
 }

From 41e3ff381f2433b5f89cc578debb050283028091 Mon Sep 17 00:00:00 2001
From: Benoit Marty <benoitm@matrix.org>
Date: Thu, 29 Oct 2020 16:02:21 +0100
Subject: [PATCH 21/23] Rename method

---
 .../android/sdk/internal/crypto/CryptoSessionInfoProvider.kt    | 2 +-
 .../matrix/android/sdk/internal/crypto/DefaultCryptoService.kt  | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt
index ba0dbda786..e8a70615e1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoSessionInfoProvider.kt
@@ -47,7 +47,7 @@ internal class CryptoSessionInfoProvider @Inject constructor(
     /**
      * @param allActive if true return joined as well as invited, if false, only joined
      */
-     fun getRoomUserIdsForCrypto(roomId: String, allActive: Boolean): List<String> {
+     fun getRoomUserIds(roomId: String, allActive: Boolean): List<String> {
         var userIds: List<String> = emptyList()
         monarchy.doWithRealm { realm ->
             userIds = if (allActive) {
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 07545c50bc..326eac8f91 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
@@ -872,7 +872,7 @@ internal class DefaultCryptoService @Inject constructor(
     private fun getRoomUserIds(roomId: String): List<String> {
         val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser()
                 && shouldEncryptForInvitedMembers(roomId)
-        return cryptoSessionInfoProvider.getRoomUserIdsForCrypto(roomId, encryptForInvitedMembers)
+        return cryptoSessionInfoProvider.getRoomUserIds(roomId, encryptForInvitedMembers)
     }
 
     /**

From 077bcb3f2ac6e3a45eb76a6934c9f4a4dde5466a Mon Sep 17 00:00:00 2001
From: Benoit Marty <benoit@matrix.org>
Date: Thu, 29 Oct 2020 16:04:07 +0100
Subject: [PATCH 22/23] Export the key share trail in rageshake (opt-in)

---
 .../features/rageshake/BugReportActivity.kt   |  4 +
 .../app/features/rageshake/BugReporter.kt     | 20 ++++
 .../devtools/GossipingEventsSerializer.kt     | 92 +++++++++++++++++++
 .../settings/devtools/KeyRequestViewModel.kt  | 60 +-----------
 .../main/res/layout/activity_bug_report.xml   |  9 ++
 vector/src/main/res/values/strings.xml        |  1 +
 6 files changed, 127 insertions(+), 59 deletions(-)
 create mode 100644 vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsSerializer.kt

diff --git a/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt b/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt
index e26736de7e..3f38e4ef15 100755
--- a/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/rageshake/BugReportActivity.kt
@@ -69,6 +69,9 @@ class BugReportActivity : VectorBaseActivity() {
             bug_report_button_include_crash_logs.isChecked = false
             bug_report_button_include_crash_logs.isVisible = false
 
+            bug_report_button_include_key_share_history.isChecked = false
+            bug_report_button_include_key_share_history.isVisible = false
+
             // Keep the screenshot
         } else {
             supportActionBar?.setTitle(R.string.title_activity_bug_report)
@@ -121,6 +124,7 @@ class BugReportActivity : VectorBaseActivity() {
                 forSuggestion,
                 bug_report_button_include_logs.isChecked,
                 bug_report_button_include_crash_logs.isChecked,
+                bug_report_button_include_key_share_history.isChecked,
                 bug_report_button_include_screenshot.isChecked,
                 bug_report_edit_text.text.toString(),
                 object : BugReporter.IMXBugReportListener {
diff --git a/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt b/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt
index e7382a17e9..95fe16ea51 100755
--- a/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt
+++ b/vector/src/main/java/im/vector/app/features/rageshake/BugReporter.kt
@@ -33,6 +33,7 @@ import im.vector.app.core.extensions.getAllChildFragments
 import im.vector.app.core.extensions.toOnOff
 import im.vector.app.features.settings.VectorLocale
 import im.vector.app.features.settings.VectorPreferences
+import im.vector.app.features.settings.devtools.GossipingEventsSerializer
 import im.vector.app.features.settings.locale.SystemLocaleProvider
 import im.vector.app.features.themes.ThemeUtils
 import im.vector.app.features.version.VersionProvider
@@ -74,6 +75,7 @@ class BugReporter @Inject constructor(
         private const val LOG_CAT_FILENAME = "logcat.log"
         private const val LOG_CAT_SCREENSHOT_FILENAME = "screenshot.png"
         private const val CRASH_FILENAME = "crash.log"
+        private const val KEY_REQUESTS_FILENAME = "keyRequests.log"
 
         private const val BUFFER_SIZE = 1024 * 1024 * 50
     }
@@ -143,6 +145,7 @@ class BugReporter @Inject constructor(
      * @param forSuggestion     true to send a suggestion
      * @param withDevicesLogs   true to include the device log
      * @param withCrashLogs     true to include the crash logs
+     * @param withKeyRequestHistory true to include the crash logs
      * @param withScreenshot    true to include the screenshot
      * @param theBugDescription the bug description
      * @param listener          the listener
@@ -152,6 +155,7 @@ class BugReporter @Inject constructor(
                       forSuggestion: Boolean,
                       withDevicesLogs: Boolean,
                       withCrashLogs: Boolean,
+                      withKeyRequestHistory: Boolean,
                       withScreenshot: Boolean,
                       theBugDescription: String,
                       listener: IMXBugReportListener?) {
@@ -207,6 +211,22 @@ class BugReporter @Inject constructor(
                     }
                 }
 
+                activeSessionHolder.getSafeActiveSession()
+                            ?.takeIf { !mIsCancelled && withKeyRequestHistory }
+                            ?.cryptoService()
+                            ?.getGossipingEvents()
+                            ?.let { GossipingEventsSerializer().serialize(it) }
+                            ?.toByteArray()
+                            ?.let { rawByteArray ->
+                                File(context.cacheDir.absolutePath, KEY_REQUESTS_FILENAME)
+                                        .also {
+                                            it.outputStream()
+                                                .use { os -> os.write(rawByteArray) }
+                                        }
+                            }
+                            ?.let { compressFile(it) }
+                            ?.let { gzippedFiles.add(it) }
+
                 var deviceId = "undefined"
                 var userId = "undefined"
                 var olmVersion = "undefined"
diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsSerializer.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsSerializer.kt
new file mode 100644
index 0000000000..d18a6c2ba8
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/settings/devtools/GossipingEventsSerializer.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * 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 im.vector.app.features.settings.devtools
+
+import im.vector.app.core.resources.DateProvider
+import me.gujun.android.span.span
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent
+import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent
+import org.matrix.android.sdk.internal.crypto.model.rest.ForwardedRoomKeyContent
+import org.matrix.android.sdk.internal.crypto.model.rest.GossipingToDeviceObject
+import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyShareRequest
+import org.matrix.android.sdk.internal.crypto.model.rest.SecretShareRequest
+import org.threeten.bp.format.DateTimeFormatter
+
+class GossipingEventsSerializer {
+    private val full24DateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")
+
+    fun serialize(eventList: List<Event>): String {
+        return buildString {
+            eventList.forEach {
+                val clearType = it.getClearType()
+                append("[${getFormattedDate(it.ageLocalTs)}] $clearType from:${it.senderId} - ")
+                when (clearType) {
+                    EventType.ROOM_KEY_REQUEST   -> {
+                        val content = it.getClearContent().toModel<RoomKeyShareRequest>()
+                        append("reqId:${content?.requestId} action:${content?.action} ")
+                        if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
+                            append("sessionId: ${content.body?.sessionId} ")
+                        }
+                        append("requestedBy: ${content?.requestingDeviceId}")
+                    }
+                    EventType.FORWARDED_ROOM_KEY -> {
+                        val encryptedContent = it.content.toModel<OlmEventContent>()
+                        val content = it.getClearContent().toModel<ForwardedRoomKeyContent>()
+
+                        append("sessionId:${content?.sessionId} From Device (sender key):${encryptedContent?.senderKey}")
+                        span("\nFrom Device (sender key):") {
+                            textStyle = "bold"
+                        }
+                    }
+                    EventType.ROOM_KEY           -> {
+                        val content = it.getClearContent()
+                        append("sessionId:${content?.get("session_id")} roomId:${content?.get("room_id")} dest:${content?.get("_dest") ?: "me"}")
+                    }
+                    EventType.SEND_SECRET        -> {
+                        val content = it.getClearContent().toModel<SecretSendEventContent>()
+                        append("requestId:${content?.requestId} From Device:${it.mxDecryptionResult?.payload?.get("sender_device")}")
+                    }
+                    EventType.REQUEST_SECRET     -> {
+                        val content = it.getClearContent().toModel<SecretShareRequest>()
+                        append("reqId:${content?.requestId} action:${content?.action} ")
+                        if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
+                            append("secretName:${content.secretName} ")
+                        }
+                        append("requestedBy:${content?.requestingDeviceId}")
+                    }
+                    EventType.ENCRYPTED          -> {
+                        append("Failed to Decrypt")
+                    }
+                    else                         -> {
+                        append("??")
+                    }
+                }
+                append("\n")
+            }
+        }
+    }
+
+    private fun getFormattedDate(ageLocalTs: Long?): String {
+        return ageLocalTs
+                ?.let { DateProvider.toLocalDateTime(it) }
+                ?.let { full24DateFormatter.format(it) }
+                ?: "?"
+    }
+}
diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt
index 4385cb2c09..d49d3dc0b6 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt
@@ -70,10 +70,6 @@ class KeyRequestViewModel @AssistedInject constructor(
         fun create(initialState: KeyRequestViewState): KeyRequestViewModel
     }
 
-    private val full24DateFormatter by lazy {
-        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")
-    }
-
     companion object : MvRxViewModelFactory<KeyRequestViewModel, KeyRequestViewState> {
 
         @JvmStatic
@@ -98,54 +94,7 @@ class KeyRequestViewModel @AssistedInject constructor(
                 // this can take long
                 val eventList = session.cryptoService().getGossipingEvents()
                 // clean it a bit to
-                val raw = buildString {
-                    eventList.forEach {
-                        val clearType = it.getClearType()
-                        append("[${getFormattedDate(it.ageLocalTs)}] $clearType from:${it.senderId} - ")
-                        when (clearType) {
-                            EventType.ROOM_KEY_REQUEST   -> {
-                                val content = it.getClearContent().toModel<RoomKeyShareRequest>()
-                                append("reqId:${content?.requestId} action:${content?.action} ")
-                                if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
-                                    append("sessionId: ${content.body?.sessionId} ")
-                                }
-                                append("requestedBy: ${content?.requestingDeviceId}")
-                            }
-                            EventType.FORWARDED_ROOM_KEY -> {
-                                val encryptedContent = it.content.toModel<OlmEventContent>()
-                                val content = it.getClearContent().toModel<ForwardedRoomKeyContent>()
-
-                                append("sessionId:${content?.sessionId} From Device (sender key):${encryptedContent?.senderKey}")
-                                span("\nFrom Device (sender key):") {
-                                    textStyle = "bold"
-                                }
-                            }
-                            EventType.ROOM_KEY           -> {
-                                val content = it.getClearContent()
-                                append("sessionId:${content?.get("session_id")} roomId:${content?.get("room_id")} dest:${content?.get("_dest") ?: "me"}")
-                            }
-                            EventType.SEND_SECRET        -> {
-                                val content = it.getClearContent().toModel<SecretSendEventContent>()
-                                append("requestId:${content?.requestId} From Device:${it.mxDecryptionResult?.payload?.get("sender_device")}")
-                            }
-                            EventType.REQUEST_SECRET     -> {
-                                val content = it.getClearContent().toModel<SecretShareRequest>()
-                                append("reqId:${content?.requestId} action:${content?.action} ")
-                                if (content?.action == GossipingToDeviceObject.ACTION_SHARE_REQUEST) {
-                                    append("secretName:${content.secretName} ")
-                                }
-                                append("requestedBy:${content?.requestingDeviceId}")
-                            }
-                            EventType.ENCRYPTED          -> {
-                                append("Failed to Decrypt")
-                            }
-                            else                         -> {
-                                append("??")
-                            }
-                        }
-                        append("\n")
-                    }
-                }
+                val raw = GossipingEventsSerializer().serialize(eventList)
                 setState {
                     copy(exporting = Success(Unit))
                 }
@@ -157,11 +106,4 @@ class KeyRequestViewModel @AssistedInject constructor(
             }
         }
     }
-
-    private fun getFormattedDate(ageLocalTs: Long?): String {
-        return ageLocalTs
-                ?.let { DateProvider.toLocalDateTime(it) }
-                ?.let { full24DateFormatter.format(it) }
-                ?: "?"
-    }
 }
diff --git a/vector/src/main/res/layout/activity_bug_report.xml b/vector/src/main/res/layout/activity_bug_report.xml
index 3bfb1f5429..34169f44f8 100644
--- a/vector/src/main/res/layout/activity_bug_report.xml
+++ b/vector/src/main/res/layout/activity_bug_report.xml
@@ -125,6 +125,15 @@
                     android:checked="true"
                     android:text="@string/send_bug_report_include_crash_logs" />
 
+                <com.google.android.material.switchmaterial.SwitchMaterial
+                    android:id="@+id/bug_report_button_include_key_share_history"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_marginStart="10dp"
+                    android:layout_marginEnd="10dp"
+                    android:checked="false"
+                    android:text="@string/send_bug_report_include_key_share_history" />
+
                 <com.google.android.material.switchmaterial.SwitchMaterial
                     android:id="@+id/bug_report_button_include_screenshot"
                     android:layout_width="match_parent"
diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
index 1178e5aeb4..0d7f5f3961 100644
--- a/vector/src/main/res/values/strings.xml
+++ b/vector/src/main/res/values/strings.xml
@@ -192,6 +192,7 @@
 
     <string name="send_bug_report_include_logs">Send logs</string>
     <string name="send_bug_report_include_crash_logs">Send crash logs</string>
+    <string name="send_bug_report_include_key_share_history">Send key share requests history</string>
     <string name="send_bug_report_include_screenshot">Send screenshot</string>
     <string name="send_bug_report">Report bug</string>
     <string name="send_bug_report_description">Please describe the bug. What did you do? What did you expect to happen? What actually happened?</string>

From 47a90746952059741d258e319183a3bfe7cfb351 Mon Sep 17 00:00:00 2001
From: Benoit Marty <benoit@matrix.org>
Date: Fri, 30 Oct 2020 10:59:11 +0100
Subject: [PATCH 23/23] Cleanup

---
 .../features/settings/devtools/KeyRequestViewModel.kt | 11 -----------
 1 file changed, 11 deletions(-)

diff --git a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt
index d49d3dc0b6..a7d5e8f1ac 100644
--- a/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/settings/devtools/KeyRequestViewModel.kt
@@ -33,20 +33,9 @@ import im.vector.app.core.extensions.exhaustive
 import im.vector.app.core.platform.VectorViewEvents
 import im.vector.app.core.platform.VectorViewModel
 import im.vector.app.core.platform.VectorViewModelAction
-import im.vector.app.core.resources.DateProvider
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
-import me.gujun.android.span.span
 import org.matrix.android.sdk.api.session.Session
-import org.matrix.android.sdk.api.session.events.model.EventType
-import org.matrix.android.sdk.api.session.events.model.toModel
-import org.matrix.android.sdk.internal.crypto.model.event.OlmEventContent
-import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent
-import org.matrix.android.sdk.internal.crypto.model.rest.ForwardedRoomKeyContent
-import org.matrix.android.sdk.internal.crypto.model.rest.GossipingToDeviceObject
-import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyShareRequest
-import org.matrix.android.sdk.internal.crypto.model.rest.SecretShareRequest
-import org.threeten.bp.format.DateTimeFormatter
 
 sealed class KeyRequestAction : VectorViewModelAction {
     data class ExportAudit(val uri: Uri) : KeyRequestAction()