diff --git a/dependencies.gradle b/dependencies.gradle
index 9641a63f263b3ac5cb21a96cd3cef9625299beb3..3bf3ab746d8753ec420b11dd0cd6cd741679f9d4 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -1,36 +1,37 @@
 ext.versions = [
 
         'minSdk'            : 21,
-        'compileSdk'        : 31,
-        'targetSdk'         : 31,
+        'compileSdk'        : 32,
+        'targetSdk'         : 32,
         'sourceCompat'      : JavaVersion.VERSION_11,
         'targetCompat'      : JavaVersion.VERSION_11,
 ]
 
-
-// Pinned to 7.1.3 because of https://github.com/vector-im/element-android/issues/6142
-// Please test carefully before upgrading again.
-def gradle = "7.1.3"
+def gradle = "7.2.2"
 // Ref: https://kotlinlang.org/releases.html
-def kotlin = "1.6.21"
+def kotlin = "1.7.20"
 def kotlinCoroutines = "1.6.4"
-def dagger = "2.42"
+def dagger = "2.44"
 def appDistribution = "16.0.0-beta04"
 def retrofit = "2.9.0"
 def arrow = "0.8.2"
 def markwon = "4.6.2"
-def moshi = "1.13.0"
+def moshi = "1.14.0"
 def lifecycle = "2.5.1"
 def flowBinding = "1.2.0"
 def flipper = "0.164.0"
 def epoxy = "4.6.2"
 def mavericks = "2.7.0"
-def glide = "4.13.2"
+def glide = "4.14.1"
 def bigImageViewer = "1.8.1"
 def jjwt = "0.11.5"
-def vanniktechEmoji = "0.15.0"
+// Temporary version to unblock #6929. Once 0.16.0 is released we should use it, and revert
+// the whole commit which set version 0.16.0-SNAPSHOT
+def vanniktechEmoji = "0.16.0-SNAPSHOT"
+
+def sentry = "6.4.1"
 
-def fragment = "1.5.2"
+def fragment = "1.5.3"
 
 // Testing
 def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819
@@ -51,7 +52,7 @@ ext.libs = [
         ],
         androidx    : [
                 'activity'                : "androidx.activity:activity:1.5.1",
-                'appCompat'               : "androidx.appcompat:appcompat:1.4.2",
+                'appCompat'               : "androidx.appcompat:appcompat:1.5.1",
                 'biometric'               : "androidx.biometric:biometric:1.1.0",
                 'core'                    : "androidx.core:core-ktx:1.8.0",
                 'recyclerview'            : "androidx.recyclerview:recyclerview:1.2.1",
@@ -86,7 +87,7 @@ ext.libs = [
                 'appdistributionApi'      : "com.google.firebase:firebase-appdistribution-api-ktx:$appDistribution",
                 'appdistribution'         : "com.google.firebase:firebase-appdistribution:$appDistribution",
                 // Phone number https://github.com/google/libphonenumber
-                'phonenumber'             : "com.googlecode.libphonenumber:libphonenumber:8.12.55"
+                'phonenumber'             : "com.googlecode.libphonenumber:libphonenumber:8.12.56"
         ],
         dagger      : [
                 'dagger'                  : "com.google.dagger:dagger:$dagger",
@@ -100,7 +101,7 @@ ext.libs = [
                 'flipperNetworkPlugin'    : "com.facebook.flipper:flipper-network-plugin:$flipper",
         ],
         element     : [
-                'opusencoder'             : "io.element.android:opusencoder:1.0.4",
+                'opusencoder'             : "io.element.android:opusencoder:1.1.0",
         ],
         squareup    : [
                 'moshi'                  : "com.squareup.moshi:moshi:$moshi",
@@ -120,6 +121,7 @@ ext.libs = [
         markwon     : [
                 'core'                   : "io.noties.markwon:core:$markwon",
                 'extLatex'               : "io.noties.markwon:ext-latex:$markwon",
+                'imageGlide'             : "io.noties.markwon:image-glide:$markwon",
                 'inlineParser'           : "io.noties.markwon:inline-parser:$markwon",
                 'html'                   : "io.noties.markwon:html:$markwon"
         ],
@@ -165,10 +167,13 @@ ext.libs = [
         apache      : [
                 'commonsImaging'         : "org.apache.sanselan:sanselan:0.97-incubator"
         ],
+        sentry: [
+                'sentryAndroid'         : "io.sentry:sentry-android:$sentry"
+        ],
         tests       : [
                 'kluent'                 : "org.amshove.kluent:kluent-android:1.68",
                 'timberJunitRule'        : "net.lachlanmckee:timber-junit-rule:1.0.1",
-                'junit'                  : "junit:junit:4.13.2"
+                'junit'                  : "junit:junit:4.13.2",
         ]
 ]
 
diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle
index 433bc5356829ae66e491a3e2ea6b46698715dbe1..cdab6172d1068cf9c58980673baae54fadab9b9a 100644
--- a/dependencies_groups.gradle
+++ b/dependencies_groups.gradle
@@ -38,10 +38,18 @@ ext.groups = [
                         'com.google.testing.platform',
                 ]
         ],
+        snapshot: [
+                regex: [
+                ],
+                group: [
+                        'com.vanniktech',
+                ]
+        ],
         mavenCentral: [
                 regex: [
                 ],
                 group: [
+                        'app.cash.paparazzi',
                         'ch.qos.logback',
                         'com.adevinta.android',
                         'com.airbnb.android',
@@ -118,7 +126,7 @@ ext.groups = [
                         'com.sun.xml.bind.mvn',
                         'com.sun.xml.fastinfoset',
                         'com.thoughtworks.qdox',
-                        'com.vanniktech',
+                        // 'com.vanniktech',
                         'commons-cli',
                         'commons-codec',
                         'commons-io',
@@ -140,14 +148,18 @@ ext.groups = [
                         'io.opencensus',
                         'io.reactivex.rxjava2',
                         'io.realm',
+                        'io.sentry',
                         'it.unimi.dsi',
                         'jakarta.activation',
                         'jakarta.xml.bind',
+                        'javax.activation',
                         'javax.annotation',
                         'javax.inject',
+                        'javax.xml.bind',
                         'jline',
                         'jp.wasabeef',
                         'junit',
+                        'kxml2',
                         'me.saket',
                         'net.bytebuddy',
                         'net.java',
@@ -176,11 +188,13 @@ ext.groups = [
                         'org.hamcrest',
                         'org.jacoco',
                         'org.java-websocket',
+                        'org.jcodec',
                         'org.jetbrains',
                         'org.jetbrains.dokka',
                         'org.jetbrains.intellij.deps',
                         'org.jetbrains.kotlin',
                         'org.jetbrains.kotlinx',
+                        'org.jetbrains.trove4j',
                         'org.json',
                         'org.jsoup',
                         'org.junit',
@@ -197,7 +211,6 @@ ext.groups = [
                         'org.ow2.asm',
                         'org.ow2.asm',
                         'org.reactivestreams',
-                        'org.robolectric',
                         'org.slf4j',
                         'org.sonatype.oss',
                         'org.testng',
diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle
index 9dbc5f42a3f6ef8680b89c35e8dbcec4d426091e..9836e1c8bb285132b6c9b3e298fbba3afef654a5 100644
--- a/matrix-sdk-android/build.gradle
+++ b/matrix-sdk-android/build.gradle
@@ -224,6 +224,8 @@ dependencies {
     androidTestImplementation libs.mockk.mockkAndroid
     androidTestImplementation libs.androidx.coreTesting
     androidTestImplementation libs.jetbrains.coroutinesAndroid
+    androidTestImplementation libs.jetbrains.coroutinesTest
+
     // Plant Timber tree for test
     androidTestImplementation libs.tests.timberJunitRule
 
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt
index 260e8dbe058bb7d73ee87d1409fcd61cfa8cde8b..403f697778e28864e5cf38cd4d08edaa58af116f 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/ChangePasswordTest.kt
@@ -43,9 +43,7 @@ class ChangePasswordTest : InstrumentedTest {
         val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = false))
 
         // Change password
-        commonTestHelper.runBlockingTest {
-            session.accountService().changePassword(TestConstants.PASSWORD, NEW_PASSWORD)
-        }
+        session.accountService().changePassword(TestConstants.PASSWORD, NEW_PASSWORD)
 
         // Try to login with the previous password, it will fail
         val throwable = commonTestHelper.logAccountWithError(session.myUserId, TestConstants.PASSWORD)
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt
index 0b21f85742d513bfac201c648958a4990e8b099f..bb5618b81633ae560e2b2cd9228cd477e3d2cba7 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt
@@ -40,26 +40,24 @@ import kotlin.coroutines.resume
 class DeactivateAccountTest : InstrumentedTest {
 
     @Test
-    fun deactivateAccountTest() = runSessionTest(context(), false /* session will be deactivated */) { commonTestHelper ->
+    fun deactivateAccountTest() = runSessionTest(context(), autoSignoutOnClose = false /* session will be deactivated */) { commonTestHelper ->
         val session = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true))
 
         // Deactivate the account
-        commonTestHelper.runBlockingTest {
-            session.accountService().deactivateAccount(
-                    eraseAllData = false,
-                    userInteractiveAuthInterceptor = object : UserInteractiveAuthInterceptor {
-                        override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
-                            promise.resume(
-                                    UserPasswordAuth(
-                                            user = session.myUserId,
-                                            password = TestConstants.PASSWORD,
-                                            session = flowResponse.session
-                                    )
-                            )
-                        }
+        session.accountService().deactivateAccount(
+                eraseAllData = false,
+                userInteractiveAuthInterceptor = object : UserInteractiveAuthInterceptor {
+                    override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
+                        promise.resume(
+                                UserPasswordAuth(
+                                        user = session.myUserId,
+                                        password = TestConstants.PASSWORD,
+                                        session = flowResponse.session
+                                )
+                        )
                     }
-            )
-        }
+                }
+        )
 
         // Try to login on the previous account, it will fail (M_USER_DEACTIVATED)
         val throwable = commonTestHelper.logAccountWithError(session.myUserId, TestConstants.PASSWORD)
@@ -74,23 +72,19 @@ class DeactivateAccountTest : InstrumentedTest {
         // Try to create an account with the deactivate account user id, it will fail (M_USER_IN_USE)
         val hs = commonTestHelper.createHomeServerConfig()
 
-        commonTestHelper.runBlockingTest {
-            commonTestHelper.matrix.authenticationService.getLoginFlow(hs)
-        }
+        commonTestHelper.matrix.authenticationService.getLoginFlow(hs)
 
         var accountCreationError: Throwable? = null
-        commonTestHelper.runBlockingTest {
-            try {
-                commonTestHelper.matrix.authenticationService
-                        .getRegistrationWizard()
-                        .createAccount(
-                                session.myUserId.substringAfter("@").substringBefore(":"),
-                                TestConstants.PASSWORD,
-                                null
-                        )
-            } catch (failure: Throwable) {
-                accountCreationError = failure
-            }
+        try {
+            commonTestHelper.matrix.authenticationService
+                    .getRegistrationWizard()
+                    .createAccount(
+                            session.myUserId.substringAfter("@").substringBefore(":"),
+                            TestConstants.PASSWORD,
+                            null
+                    )
+        } catch (failure: Throwable) {
+            accountCreationError = failure
         }
 
         // Test the error
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 43f42a3ed460c08d009a1d5d67ea846aee584bf8..eeb2def5827876275de455c482a8090c7b32d6d6 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
@@ -19,23 +19,22 @@ package org.matrix.android.sdk.common
 import android.content.Context
 import android.net.Uri
 import android.util.Log
-import androidx.lifecycle.Observer
 import androidx.test.internal.runner.junit4.statement.UiThreadStatement
-import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.Job
-import kotlinx.coroutines.SupervisorJob
 import kotlinx.coroutines.cancel
 import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.withTimeout
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.withContext
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertNotNull
 import org.junit.Assert.assertTrue
 import org.matrix.android.sdk.api.MatrixCallback
 import org.matrix.android.sdk.api.MatrixConfiguration
+import org.matrix.android.sdk.api.SyncConfig
 import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
 import org.matrix.android.sdk.api.auth.registration.RegistrationResult
 import org.matrix.android.sdk.api.crypto.MXCryptoConfig
@@ -51,12 +50,12 @@ import org.matrix.android.sdk.api.session.room.send.SendState
 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.api.session.sync.SyncState
 import timber.log.Timber
 import java.util.UUID
-import java.util.concurrent.CancellationException
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
 
 /**
  * This class exposes methods to be used in common cases
@@ -65,34 +64,42 @@ import java.util.concurrent.TimeUnit
 class CommonTestHelper internal constructor(context: Context, val cryptoConfig: MXCryptoConfig? = null) {
 
     companion object {
-        internal fun runSessionTest(context: Context, autoSignoutOnClose: Boolean = true, block: (CommonTestHelper) -> Unit) {
-            val testHelper = CommonTestHelper(context)
-            return try {
-                block(testHelper)
-            } finally {
-                if (autoSignoutOnClose) {
-                    testHelper.cleanUpOpenedSessions()
+
+        @OptIn(ExperimentalCoroutinesApi::class)
+        internal fun runSessionTest(context: Context, cryptoConfig: MXCryptoConfig? = null, autoSignoutOnClose: Boolean = true, block: suspend CoroutineScope.(CommonTestHelper) -> Unit) {
+            val testHelper = CommonTestHelper(context, cryptoConfig)
+            return runTest(dispatchTimeoutMs = TestConstants.timeOutMillis) {
+                try {
+                    withContext(Dispatchers.Default) {
+                        block(testHelper)
+                    }
+                } finally {
+                    if (autoSignoutOnClose) {
+                        testHelper.cleanUpOpenedSessions()
+                    }
                 }
             }
         }
 
-        internal fun runCryptoTest(context: Context, autoSignoutOnClose: Boolean = true,
-                                   cryptoConfig: MXCryptoConfig? = null,
-                                   block: (CryptoTestHelper, CommonTestHelper) -> Unit) {
+        @OptIn(ExperimentalCoroutinesApi::class)
+        internal fun runCryptoTest(context: Context, cryptoConfig: MXCryptoConfig? = null,  autoSignoutOnClose: Boolean = true, block: suspend CoroutineScope.(CryptoTestHelper, CommonTestHelper) -> Unit) {
             val testHelper = CommonTestHelper(context, cryptoConfig)
             val cryptoTestHelper = CryptoTestHelper(testHelper)
-            return try {
-                block(cryptoTestHelper, testHelper)
-            } finally {
-                if (autoSignoutOnClose) {
-                    testHelper.cleanUpOpenedSessions()
+            return runTest(dispatchTimeoutMs = TestConstants.timeOutMillis) {
+                try {
+                    withContext(Dispatchers.Default) {
+                        block(cryptoTestHelper, testHelper)
+                    }
+                } finally {
+                    if (autoSignoutOnClose) {
+                        testHelper.cleanUpOpenedSessions()
+                    }
                 }
             }
         }
     }
 
     internal val matrix: TestMatrix
-    private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
     private var accountNumber = 0
 
     private val trackedSessions = mutableListOf<Session>()
@@ -107,6 +114,7 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig:
                     MatrixConfiguration(
                             applicationFlavor = "TestFlavor",
                             roomDisplayNameFallbackProvider = TestRoomDisplayNameFallbackProvider(),
+                            syncConfig = SyncConfig(longPollTimeout = 5_000L),
                             cryptoConfig = cryptoConfig ?: MXCryptoConfig()
                     )
             )
@@ -114,19 +122,17 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig:
         matrix = _matrix!!
     }
 
-    fun createAccount(userNamePrefix: String, testParams: SessionTestParams): Session {
+    suspend fun createAccount(userNamePrefix: String, testParams: SessionTestParams): Session {
         return createAccount(userNamePrefix, TestConstants.PASSWORD, testParams)
     }
 
-    fun logIntoAccount(userId: String, testParams: SessionTestParams): Session {
+    suspend fun logIntoAccount(userId: String, testParams: SessionTestParams): Session {
         return logIntoAccount(userId, TestConstants.PASSWORD, testParams)
     }
 
-    fun cleanUpOpenedSessions() {
+    suspend fun cleanUpOpenedSessions() {
         trackedSessions.forEach {
-            runBlockingTest {
-                it.signOutService().signOut(true)
-            }
+            it.signOutService().signOut(true)
         }
         trackedSessions.clear()
     }
@@ -140,27 +146,10 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig:
                 .build()
     }
 
-    /**
-     * This methods init the event stream and check for initial sync
-     *
-     * @param session the session to sync
-     */
-    fun syncSession(session: Session, timeout: Long = TestConstants.timeOutMillis * 10) {
-        val lock = CountDownLatch(1)
-        coroutineScope.launch {
-            session.syncService().startSync(true)
-            val syncLiveData = session.syncService().getSyncStateLive()
-            val syncObserver = object : Observer<SyncState> {
-                override fun onChanged(t: SyncState?) {
-                    if (session.syncService().hasAlreadySynced()) {
-                        lock.countDown()
-                        syncLiveData.removeObserver(this)
-                    }
-                }
-            }
-            syncLiveData.observeForever(syncObserver)
-        }
-        await(lock, timeout)
+    suspend fun syncSession(session: Session, timeout: Long = TestConstants.timeOutMillis * 10) {
+        session.syncService().startSync(true)
+        val syncLiveData = session.syncService().getSyncStateLive()
+        syncLiveData.first(timeout) { session.syncService().hasAlreadySynced() }
     }
 
     /**
@@ -168,22 +157,11 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig:
      *
      * @param session the session to sync
      */
-    fun clearCacheAndSync(session: Session, timeout: Long = TestConstants.timeOutMillis) {
-        waitWithLatch(timeout) { latch ->
-            session.clearCache()
-            val syncLiveData = session.syncService().getSyncStateLive()
-            val syncObserver = object : Observer<SyncState> {
-                override fun onChanged(t: SyncState?) {
-                    if (session.syncService().hasAlreadySynced()) {
-                        Timber.v("Clear cache and synced")
-                        syncLiveData.removeObserver(this)
-                        latch.countDown()
-                    }
-                }
-            }
-            syncLiveData.observeForever(syncObserver)
-            session.syncService().startSync(true)
-        }
+    suspend fun clearCacheAndSync(session: Session, timeout: Long = TestConstants.timeOutMillis) {
+        session.clearCache()
+        syncSession(session, timeout)
+        session.syncService().getSyncStateLive().first(timeout) { session.syncService().hasAlreadySynced() }
+        Timber.v("Clear cache and synced")
     }
 
     /**
@@ -193,7 +171,7 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig:
      * @param message the message to send
      * @param nbOfMessages the number of time the message will be sent
      */
-    fun sendTextMessage(room: Room, message: String, nbOfMessages: Int, timeout: Long = TestConstants.timeOutMillis): List<TimelineEvent> {
+    suspend fun sendTextMessage(room: Room, message: String, nbOfMessages: Int, timeout: Long = TestConstants.timeOutMillis): List<TimelineEvent> {
         val timeline = room.timelineService().createTimeline(null, TimelineSettings(10))
         timeline.start()
         val sentEvents = sendTextMessagesBatched(timeline, room, message, nbOfMessages, timeout)
@@ -206,66 +184,72 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig:
     /**
      * Will send nb of messages provided by count parameter but waits every 10 messages to avoid gap in sync
      */
-    private fun sendTextMessagesBatched(timeline: Timeline, room: Room, message: String, count: Int, timeout: Long, rootThreadEventId: String? = null): List<TimelineEvent> {
+    private suspend fun sendTextMessagesBatched(timeline: Timeline, room: Room, message: String, count: Int, timeout: Long, rootThreadEventId: String? = null): List<TimelineEvent> {
         val sentEvents = ArrayList<TimelineEvent>(count)
         (1 until count + 1)
                 .map { "$message #$it" }
                 .chunked(10)
                 .forEach { batchedMessages ->
-                    batchedMessages.forEach { formattedMessage ->
-                        if (rootThreadEventId != null) {
-                            room.relationService().replyInThread(
-                                    rootThreadEventId = rootThreadEventId,
-                                    replyInThreadText = formattedMessage
-                            )
-                        } else {
-                            room.sendService().sendTextMessage(formattedMessage)
-                        }
-                    }
-                    waitWithLatch(timeout) { latch ->
-                        val timelineListener = object : Timeline.Listener {
-
-                            override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
-                                val allSentMessages = snapshot
-                                        .filter { it.root.sendState == SendState.SYNCED }
-                                        .filter { it.root.getClearType() == EventType.MESSAGE }
-                                        .filter { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(message) == true }
-
-                                val hasSyncedAllBatchedMessages = allSentMessages
-                                        .map {
-                                            it.root.getClearContent().toModel<MessageContent>()?.body
+                    waitFor(
+                            continueWhen = {
+                                wrapWithTimeout(timeout) {
+                                    suspendCoroutine<Unit> { continuation ->
+                                        val timelineListener = object : Timeline.Listener {
+
+                                            override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
+                                                val allSentMessages = snapshot
+                                                        .filter { it.root.sendState == SendState.SYNCED }
+                                                        .filter { it.root.getClearType() == EventType.MESSAGE }
+                                                        .filter { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(message) == true }
+
+                                                val hasSyncedAllBatchedMessages = allSentMessages
+                                                        .map {
+                                                            it.root.getClearContent().toModel<MessageContent>()?.body
+                                                        }
+                                                        .containsAll(batchedMessages)
+
+                                                if (allSentMessages.size == count) {
+                                                    sentEvents.addAll(allSentMessages)
+                                                }
+                                                if (hasSyncedAllBatchedMessages) {
+                                                    timeline.removeListener(this)
+                                                    continuation.resume(Unit)
+                                                }
+                                            }
                                         }
-                                        .containsAll(batchedMessages)
-
-                                if (allSentMessages.size == count) {
-                                    sentEvents.addAll(allSentMessages)
+                                        timeline.addListener(timelineListener)
+                                    }
                                 }
-                                if (hasSyncedAllBatchedMessages) {
-                                    timeline.removeListener(this)
-                                    latch.countDown()
+                            },
+                            action = {
+                                batchedMessages.forEach { formattedMessage ->
+                                    if (rootThreadEventId != null) {
+                                        room.relationService().replyInThread(
+                                                rootThreadEventId = rootThreadEventId,
+                                                replyInThreadText = formattedMessage
+                                        )
+                                    } else {
+                                        room.sendService().sendTextMessage(formattedMessage)
+                                    }
                                 }
                             }
-                        }
-                        timeline.addListener(timelineListener)
-                    }
+                    )
                 }
         return sentEvents
     }
 
-    fun waitForAndAcceptInviteInRoom(otherSession: Session, roomID: String) {
-        waitWithLatch { latch ->
-            retryPeriodicallyWithLatch(latch) {
-                val roomSummary = otherSession.getRoomSummary(roomID)
-                (roomSummary != null && roomSummary.membership == Membership.INVITE).also {
-                    if (it) {
-                        Log.v("# TEST", "${otherSession.myUserId} can see the invite")
-                    }
+    suspend fun waitForAndAcceptInviteInRoom(otherSession: Session, roomID: String) {
+        retryPeriodically {
+            val roomSummary = otherSession.getRoomSummary(roomID)
+            (roomSummary != null && roomSummary.membership == Membership.INVITE).also {
+                if (it) {
+                    Log.v("# TEST", "${otherSession.myUserId} can see the invite")
                 }
             }
         }
 
         // not sure why it's taking so long :/
-        runBlockingTest(90_000) {
+        wrapWithTimeout(90_000) {
             Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $roomID")
             try {
                 otherSession.roomService().joinRoom(roomID)
@@ -275,11 +259,9 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig:
         }
 
         Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...")
-        waitWithLatch {
-            retryPeriodicallyWithLatch(it) {
-                val roomSummary = otherSession.getRoomSummary(roomID)
-                roomSummary != null && roomSummary.membership == Membership.JOIN
-            }
+        retryPeriodically {
+            val roomSummary = otherSession.getRoomSummary(roomID)
+            roomSummary != null && roomSummary.membership == Membership.JOIN
         }
     }
 
@@ -289,7 +271,7 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig:
      * @param message the message to send
      * @param numberOfMessages the number of time the message will be sent
      */
-    fun replyInThreadMessage(
+    suspend fun replyInThreadMessage(
             room: Room,
             message: String,
             numberOfMessages: Int,
@@ -307,15 +289,7 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig:
 
     // PRIVATE METHODS *****************************************************************************
 
-    /**
-     * Creates a unique account
-     *
-     * @param userNamePrefix the user name prefix
-     * @param password the password
-     * @param testParams test params about the session
-     * @return the session associated with the newly created account
-     */
-    private fun createAccount(
+    private suspend fun createAccount(
             userNamePrefix: String,
             password: String,
             testParams: SessionTestParams
@@ -333,15 +307,7 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig:
         }
     }
 
-    /**
-     * Logs into an existing account
-     *
-     * @param userId the userId to log in
-     * @param password the password to log in
-     * @param testParams test params about the session
-     * @return the session associated with the existing account
-     */
-    fun logIntoAccount(
+    suspend fun logIntoAccount(
             userId: String,
             password: String,
             testParams: SessionTestParams
@@ -353,32 +319,25 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig:
         }
     }
 
-    /**
-     * Create an account and a dedicated session
-     *
-     * @param userName the account username
-     * @param password the password
-     * @param sessionTestParams parameters for the test
-     */
-    private fun createAccountAndSync(
+    private suspend fun createAccountAndSync(
             userName: String,
             password: String,
             sessionTestParams: SessionTestParams
     ): Session {
         val hs = createHomeServerConfig()
 
-        runBlockingTest {
+        wrapWithTimeout(TestConstants.timeOutMillis) {
             matrix.authenticationService.getLoginFlow(hs)
         }
 
-        runBlockingTest(timeout = 60_000) {
+        wrapWithTimeout(60_000L) {
             matrix.authenticationService
                     .getRegistrationWizard()
                     .createAccount(userName, password, null)
         }
 
         // Perform dummy step
-        val registrationResult = runBlockingTest(timeout = 60_000) {
+        val registrationResult = wrapWithTimeout(timeout = 60_000) {
             matrix.authenticationService
                     .getRegistrationWizard()
                     .dummy()
@@ -393,29 +352,14 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig:
         return session
     }
 
-    /**
-     * Start an account login
-     *
-     * @param userName the account username
-     * @param password the password
-     * @param sessionTestParams session test params
-     */
-    private fun logAccountAndSync(
-            userName: String,
-            password: String,
-            sessionTestParams: SessionTestParams
-    ): Session {
+    private suspend fun logAccountAndSync(userName: String, password: String, sessionTestParams: SessionTestParams): Session {
         val hs = createHomeServerConfig()
 
-        runBlockingTest {
-            matrix.authenticationService.getLoginFlow(hs)
-        }
+        matrix.authenticationService.getLoginFlow(hs)
 
-        val session = runBlockingTest {
-            matrix.authenticationService
-                    .getLoginWizard()
-                    .login(userName, password, "myDevice")
-        }
+        val session = matrix.authenticationService
+                .getLoginWizard()
+                .login(userName, password, "myDevice")
         session.open()
         if (sessionTestParams.withInitialSync) {
             syncSession(session)
@@ -430,25 +374,21 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig:
      * @param userName the account username
      * @param password the password
      */
-    fun logAccountWithError(
+    suspend fun logAccountWithError(
             userName: String,
             password: String
     ): Throwable {
         val hs = createHomeServerConfig()
 
-        runBlockingTest {
-            matrix.authenticationService.getLoginFlow(hs)
-        }
+        matrix.authenticationService.getLoginFlow(hs)
 
         var requestFailure: Throwable? = null
-        runBlockingTest {
-            try {
-                matrix.authenticationService
-                        .getLoginWizard()
-                        .login(userName, password, "myDevice")
-            } catch (failure: Throwable) {
-                requestFailure = failure
-            }
+        try {
+            matrix.authenticationService
+                    .getLoginWizard()
+                    .login(userName, password, "myDevice")
+        } catch (failure: Throwable) {
+            requestFailure = failure
         }
 
         assertNotNull(requestFailure)
@@ -484,65 +424,48 @@ class CommonTestHelper internal constructor(context: Context, val cryptoConfig:
         )
     }
 
-    suspend fun retryPeriodicallyWithLatch(latch: CountDownLatch, condition: (() -> Boolean)) {
-        while (true) {
-            try {
-                delay(1000)
-            } catch (ex: CancellationException) {
-                // the job was canceled, just stop
-                return
-            }
-            if (condition()) {
-                latch.countDown()
-                return
+    suspend fun retryPeriodically(timeout: Long = TestConstants.timeOutMillis, predicate: suspend () -> Boolean) {
+        wrapWithTimeout(timeout) {
+            while (!predicate()) {
+                runBlocking { delay(500) }
             }
         }
     }
 
-    fun waitWithLatch(timeout: Long? = TestConstants.timeOutMillis, dispatcher: CoroutineDispatcher = Dispatchers.Main, block: suspend (CountDownLatch) -> Unit) {
-        val latch = CountDownLatch(1)
-        val job = coroutineScope.launch(dispatcher) {
-            block(latch)
-        }
-        await(latch, timeout, job)
-    }
-
-    fun <T> runBlockingTest(timeout: Long = TestConstants.timeOutMillis, block: suspend () -> T): T {
-        return runBlocking {
-            withTimeout(timeout) {
-                block()
+    suspend fun <T> waitForCallback(timeout: Long = TestConstants.timeOutMillis, block: (MatrixCallback<T>) -> Unit): T {
+        return wrapWithTimeout(timeout) {
+            suspendCoroutine { continuation ->
+                val callback = object : MatrixCallback<T> {
+                    override fun onSuccess(data: T) {
+                        continuation.resume(data)
+                    }
+                }
+                block(callback)
             }
         }
     }
 
-    // Transform a method with a MatrixCallback to a synchronous method
-    inline fun <reified T> doSync(timeout: Long? = TestConstants.timeOutMillis, block: (MatrixCallback<T>) -> Unit): T {
-        val lock = CountDownLatch(1)
-        var result: T? = null
-
-        val callback = object : TestMatrixCallback<T>(lock) {
-            override fun onSuccess(data: T) {
-                result = data
-                super.onSuccess(data)
+    suspend fun <T> waitForCallbackError(timeout: Long = TestConstants.timeOutMillis, block: (MatrixCallback<T>) -> Unit): Throwable {
+        return wrapWithTimeout(timeout) {
+            suspendCoroutine { continuation ->
+                val callback = object : MatrixCallback<T> {
+                    override fun onFailure(failure: Throwable) {
+                        continuation.resume(failure)
+                    }
+                }
+                block(callback)
             }
         }
-
-        block.invoke(callback)
-
-        await(lock, timeout)
-
-        assertNotNull(result)
-        return result!!
     }
 
     /**
      * Clear all provided sessions
      */
-    fun Iterable<Session>.signOutAndClose() = forEach { signOutAndClose(it) }
+    suspend fun Iterable<Session>.signOutAndClose() = forEach { signOutAndClose(it) }
 
-    fun signOutAndClose(session: Session) {
+    suspend fun signOutAndClose(session: Session) {
         trackedSessions.remove(session)
-        runBlockingTest(timeout = 60_000) {
+        wrapWithTimeout(timeout = 60_000L) {
             session.signOutService().signOut(true)
         }
         // no need signout will close
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt
index 41d0d3a7e86810386ee6ddfa2bcf43d2517195e0..8cd5bee5698945ccfa401a3a672e6560a50a2e71 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt
@@ -32,7 +32,7 @@ data class CryptoTestData(
     val thirdSession: Session?
         get() = sessions.getOrNull(2)
 
-    fun cleanUp(testHelper: CommonTestHelper) {
+    suspend fun cleanUp(testHelper: CommonTestHelper) {
         sessions.forEach {
             testHelper.signOutAndClose(it)
         }
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 210ce906928ac8c161777018ed0a20da835ccc51..74292daf150adde20e826faf1a11e22730307ce0 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
@@ -16,19 +16,15 @@
 
 package org.matrix.android.sdk.common
 
-import android.os.SystemClock
 import android.util.Log
-import androidx.lifecycle.Observer
 import org.amshove.kluent.fail
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertNotNull
-import org.junit.Assert.assertNull
 import org.junit.Assert.assertTrue
 import org.matrix.android.sdk.api.auth.UIABaseAuth
 import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
 import org.matrix.android.sdk.api.auth.UserPasswordAuth
 import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
-import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
 import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
 import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.session.Session
@@ -46,22 +42,16 @@ import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerific
 import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction
 import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
 import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
-import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
-import org.matrix.android.sdk.api.session.events.model.toContent
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.getRoom
-import org.matrix.android.sdk.api.session.room.Room
 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.RoomSummary
 import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
 import org.matrix.android.sdk.api.session.room.model.message.MessageContent
 import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
 import org.matrix.android.sdk.api.session.securestorage.EmptyKeySigner
 import org.matrix.android.sdk.api.session.securestorage.KeyRef
-import org.matrix.android.sdk.api.util.Optional
-import org.matrix.android.sdk.api.util.awaitCallback
 import org.matrix.android.sdk.api.util.toBase64NoPadding
 import java.util.UUID
 import kotlin.coroutines.Continuation
@@ -77,30 +67,19 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
     /**
      * @return alice session
      */
-    fun doE2ETestWithAliceInARoom(encryptedRoom: Boolean = true, roomHistoryVisibility: RoomHistoryVisibility? = null): CryptoTestData {
+    suspend fun doE2ETestWithAliceInARoom(encryptedRoom: Boolean = true, roomHistoryVisibility: RoomHistoryVisibility? = null): CryptoTestData {
         val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
 
-        val roomId = testHelper.runBlockingTest {
-            aliceSession.roomService().createRoom(CreateRoomParams().apply {
-                historyVisibility = roomHistoryVisibility
-                name = "MyRoom"
-            })
-        }
+        val roomId = aliceSession.roomService().createRoom(CreateRoomParams().apply {
+            historyVisibility = roomHistoryVisibility
+            name = "MyRoom"
+        })
         if (encryptedRoom) {
-            testHelper.waitWithLatch { latch ->
-                val room = aliceSession.getRoom(roomId)!!
-                room.roomCryptoService().enableEncryption()
-                val roomSummaryLive = room.getRoomSummaryLive()
-                val roomSummaryObserver = object : Observer<Optional<RoomSummary>> {
-                    override fun onChanged(roomSummary: Optional<RoomSummary>) {
-                        if (roomSummary.getOrNull()?.isEncrypted.orFalse()) {
-                            roomSummaryLive.removeObserver(this)
-                            latch.countDown()
-                        }
-                    }
-                }
-                roomSummaryLive.observeForever(roomSummaryObserver)
-            }
+            val room = aliceSession.getRoom(roomId)!!
+            waitFor(
+                    continueWhen = { room.onMain { getRoomSummaryLive() }.first { it.getOrNull()?.isEncrypted.orFalse() } },
+                    action = { room.roomCryptoService().enableEncryption() }
+            )
         }
         return CryptoTestData(roomId, listOf(aliceSession))
     }
@@ -108,7 +87,7 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
     /**
      * @return alice and bob sessions
      */
-    fun doE2ETestWithAliceAndBobInARoom(encryptedRoom: Boolean = true, roomHistoryVisibility: RoomHistoryVisibility? = null): CryptoTestData {
+    suspend fun doE2ETestWithAliceAndBobInARoom(encryptedRoom: Boolean = true, roomHistoryVisibility: RoomHistoryVisibility? = null): CryptoTestData {
         val cryptoTestData = doE2ETestWithAliceInARoom(encryptedRoom, roomHistoryVisibility)
         val aliceSession = cryptoTestData.firstSession
         val aliceRoomId = cryptoTestData.roomId
@@ -117,36 +96,23 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
 
         val bobSession = testHelper.createAccount(TestConstants.USER_BOB, defaultSessionParams)
 
-        testHelper.waitWithLatch { latch ->
-            val bobRoomSummariesLive = bobSession.roomService().getRoomSummariesLive(roomSummaryQueryParams { })
-            val newRoomObserver = object : Observer<List<RoomSummary>> {
-                override fun onChanged(t: List<RoomSummary>?) {
-                    if (t?.isNotEmpty() == true) {
-                        bobRoomSummariesLive.removeObserver(this)
-                        latch.countDown()
-                    }
-                }
-            }
-            bobRoomSummariesLive.observeForever(newRoomObserver)
-            aliceRoom.membershipService().invite(bobSession.myUserId)
-        }
+        waitFor(
+                continueWhen = { bobSession.roomService().onMain { getRoomSummariesLive(roomSummaryQueryParams { }) }.first { it.isNotEmpty() } },
+                action = { aliceRoom.membershipService().invite(bobSession.myUserId) }
+        )
 
-        testHelper.waitWithLatch { latch ->
-            val bobRoomSummariesLive = bobSession.roomService().getRoomSummariesLive(roomSummaryQueryParams { })
-            val roomJoinedObserver = object : Observer<List<RoomSummary>> {
-                override fun onChanged(t: List<RoomSummary>?) {
-                    if (bobSession.getRoom(aliceRoomId)
-                                    ?.membershipService()
-                                    ?.getRoomMember(bobSession.myUserId)
-                                    ?.membership == Membership.JOIN) {
-                        bobRoomSummariesLive.removeObserver(this)
-                        latch.countDown()
+        waitFor(
+                continueWhen = {
+                    bobSession.roomService().onMain { getRoomSummariesLive(roomSummaryQueryParams { }) }.first {
+                        bobSession.getRoom(aliceRoomId)
+                                ?.membershipService()
+                                ?.getRoomMember(bobSession.myUserId)
+                                ?.membership == Membership.JOIN
                     }
-                }
-            }
-            bobRoomSummariesLive.observeForever(roomJoinedObserver)
-            bobSession.roomService().joinRoom(aliceRoomId)
-        }
+                },
+                action = { bobSession.roomService().joinRoom(aliceRoomId) }
+        )
+
         // Ensure bob can send messages to the room
 //        val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!!
 //        assertNotNull(roomFromBobPOV.powerLevels)
@@ -155,46 +121,10 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
         return CryptoTestData(aliceRoomId, listOf(aliceSession, bobSession))
     }
 
-    /**
-     * @return Alice, Bob and Sam session
-     */
-    fun doE2ETestWithAliceAndBobAndSamInARoom(): CryptoTestData {
-        val cryptoTestData = doE2ETestWithAliceAndBobInARoom()
-        val aliceSession = cryptoTestData.firstSession
-        val aliceRoomId = cryptoTestData.roomId
-
-        val room = aliceSession.getRoom(aliceRoomId)!!
-
-        val samSession = createSamAccountAndInviteToTheRoom(room)
-
-        // wait the initial sync
-        SystemClock.sleep(1000)
-
-        return CryptoTestData(aliceRoomId, listOf(aliceSession, cryptoTestData.secondSession!!, samSession))
-    }
-
-    /**
-     * Create Sam account and invite him in the room. He will accept the invitation
-     * @Return Sam session
-     */
-    fun createSamAccountAndInviteToTheRoom(room: Room): Session {
-        val samSession = testHelper.createAccount(TestConstants.USER_SAM, defaultSessionParams)
-
-        testHelper.runBlockingTest {
-            room.membershipService().invite(samSession.myUserId, null)
-        }
-
-        testHelper.runBlockingTest {
-            samSession.roomService().joinRoom(room.roomId, null, emptyList())
-        }
-
-        return samSession
-    }
-
     /**
      * @return Alice and Bob sessions
      */
-    fun doE2ETestWithAliceAndBobInARoomWithEncryptedMessages(): CryptoTestData {
+    suspend fun doE2ETestWithAliceAndBobInARoomWithEncryptedMessages(): CryptoTestData {
         val cryptoTestData = doE2ETestWithAliceAndBobInARoom()
         val aliceSession = cryptoTestData.firstSession
         val aliceRoomId = cryptoTestData.roomId
@@ -235,49 +165,20 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
         return cryptoTestData
     }
 
-    private fun ensureEventReceived(roomId: String, eventId: String, session: Session, andCanDecrypt: Boolean) {
-        testHelper.waitWithLatch { latch ->
-            testHelper.retryPeriodicallyWithLatch(latch) {
-                val timeLineEvent = session.getRoom(roomId)?.timelineService()?.getTimelineEvent(eventId)
-                if (andCanDecrypt) {
-                    timeLineEvent != null &&
-                            timeLineEvent.isEncrypted() &&
-                            timeLineEvent.root.getClearType() == EventType.MESSAGE
-                } else {
-                    timeLineEvent != null
-                }
+    private suspend fun ensureEventReceived(roomId: String, eventId: String, session: Session, andCanDecrypt: Boolean) {
+        testHelper.retryPeriodically {
+            val timeLineEvent = session.getRoom(roomId)?.timelineService()?.getTimelineEvent(eventId)
+            if (andCanDecrypt) {
+                timeLineEvent != null &&
+                        timeLineEvent.isEncrypted() &&
+                        timeLineEvent.root.getClearType() == EventType.MESSAGE
+            } else {
+                timeLineEvent != null
             }
         }
     }
 
-    fun checkEncryptedEvent(event: Event, roomId: String, clearMessage: String, senderSession: Session) {
-        assertEquals(EventType.ENCRYPTED, event.type)
-        assertNotNull(event.content)
-
-        val eventWireContent = event.content.toContent()
-        assertNotNull(eventWireContent)
-
-        assertNull(eventWireContent["body"])
-        assertEquals(MXCRYPTO_ALGORITHM_MEGOLM, eventWireContent["algorithm"])
-
-        assertNotNull(eventWireContent["ciphertext"])
-        assertNotNull(eventWireContent["session_id"])
-        assertNotNull(eventWireContent["sender_key"])
-
-        assertEquals(senderSession.sessionParams.deviceId, eventWireContent["device_id"])
-
-        assertNotNull(event.eventId)
-        assertEquals(roomId, event.roomId)
-        assertEquals(EventType.MESSAGE, event.getClearType())
-        // TODO assertTrue(event.getAge() < 10000)
-
-        val eventContent = event.toContent()
-        assertNotNull(eventContent)
-        assertEquals(clearMessage, eventContent["body"])
-        assertEquals(senderSession.myUserId, event.senderId)
-    }
-
-    fun createFakeMegolmBackupAuthData(): MegolmBackupAuthData {
+    private fun createFakeMegolmBackupAuthData(): MegolmBackupAuthData {
         return MegolmBackupAuthData(
                 publicKey = "abcdefg",
                 signatures = mapOf("something" to mapOf("ed25519:something" to "hijklmnop"))
@@ -292,44 +193,35 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
         )
     }
 
-    fun createDM(alice: Session, bob: Session): String {
-        var roomId: String = ""
-        testHelper.waitWithLatch { latch ->
-            roomId = alice.roomService().createDirectRoom(bob.myUserId)
-            val bobRoomSummariesLive = bob.roomService().getRoomSummariesLive(roomSummaryQueryParams { })
-            val newRoomObserver = object : Observer<List<RoomSummary>> {
-                override fun onChanged(t: List<RoomSummary>?) {
-                    if (t?.any { it.roomId == roomId }.orFalse()) {
-                        bobRoomSummariesLive.removeObserver(this)
-                        latch.countDown()
-                    }
-                }
-            }
-            bobRoomSummariesLive.observeForever(newRoomObserver)
-        }
-
-        testHelper.waitWithLatch { latch ->
-            val bobRoomSummariesLive = bob.roomService().getRoomSummariesLive(roomSummaryQueryParams { })
-            val newRoomObserver = object : Observer<List<RoomSummary>> {
-                override fun onChanged(t: List<RoomSummary>?) {
-                    if (bob.getRoom(roomId)
-                                    ?.membershipService()
-                                    ?.getRoomMember(bob.myUserId)
-                                    ?.membership == Membership.JOIN) {
-                        bobRoomSummariesLive.removeObserver(this)
-                        latch.countDown()
-                    }
-                }
-            }
-            bobRoomSummariesLive.observeForever(newRoomObserver)
-            bob.roomService().joinRoom(roomId)
-        }
+    suspend fun createDM(alice: Session, bob: Session): String {
+        var roomId = ""
+        waitFor(
+                continueWhen = {
+                    bob.roomService()
+                            .onMain { getRoomSummariesLive(roomSummaryQueryParams { }) }
+                            .first { it.any { it.roomId == roomId }.orFalse() }
+                },
+                action = { roomId = alice.roomService().createDirectRoom(bob.myUserId) }
+        )
 
+        waitFor(
+                continueWhen = {
+                    bob.roomService()
+                            .onMain { getRoomSummariesLive(roomSummaryQueryParams { }) }
+                            .first {
+                                bob.getRoom(roomId)
+                                        ?.membershipService()
+                                        ?.getRoomMember(bob.myUserId)
+                                        ?.membership == Membership.JOIN
+                            }
+                },
+                action = { bob.roomService().joinRoom(roomId) }
+        )
         return roomId
     }
 
-    fun initializeCrossSigning(session: Session) {
-        testHelper.doSync<Unit> {
+    suspend fun initializeCrossSigning(session: Session) {
+        testHelper.waitForCallback<Unit> {
             session.cryptoService().crossSigningService()
                     .initializeCrossSigning(
                             object : UserInteractiveAuthInterceptor {
@@ -350,57 +242,55 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
     /**
      * Initialize cross-signing, set up megolm backup and save all in 4S
      */
-    fun bootstrapSecurity(session: Session) {
+    suspend fun bootstrapSecurity(session: Session) {
         initializeCrossSigning(session)
         val ssssService = session.sharedSecretStorageService()
-        testHelper.runBlockingTest {
-            val keyInfo = ssssService.generateKey(
-                    UUID.randomUUID().toString(),
-                    null,
-                    "ssss_key",
-                    EmptyKeySigner()
-            )
-            ssssService.setDefaultKey(keyInfo.keyId)
+        val keyInfo = ssssService.generateKey(
+                UUID.randomUUID().toString(),
+                null,
+                "ssss_key",
+                EmptyKeySigner()
+        )
+        ssssService.setDefaultKey(keyInfo.keyId)
 
-            ssssService.storeSecret(
-                    MASTER_KEY_SSSS_NAME,
-                    session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master!!,
-                    listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec))
-            )
+        ssssService.storeSecret(
+                MASTER_KEY_SSSS_NAME,
+                session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.master!!,
+                listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec))
+        )
 
-            ssssService.storeSecret(
-                    SELF_SIGNING_KEY_SSSS_NAME,
-                    session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned!!,
-                    listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec))
-            )
+        ssssService.storeSecret(
+                SELF_SIGNING_KEY_SSSS_NAME,
+                session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.selfSigned!!,
+                listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec))
+        )
+
+        ssssService.storeSecret(
+                USER_SIGNING_KEY_SSSS_NAME,
+                session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user!!,
+                listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec))
+        )
 
+        // set up megolm backup
+        val creationInfo = testHelper.waitForCallback<MegolmBackupCreationInfo> {
+            session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
+        }
+        val version = testHelper.waitForCallback<KeysVersion> {
+            session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it)
+        }
+        // Save it for gossiping
+        session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version)
+
+        extractCurveKeyFromRecoveryKey(creationInfo.recoveryKey)?.toBase64NoPadding()?.let { secret ->
             ssssService.storeSecret(
-                    USER_SIGNING_KEY_SSSS_NAME,
-                    session.cryptoService().crossSigningService().getCrossSigningPrivateKeys()!!.user!!,
+                    KEYBACKUP_SECRET_SSSS_NAME,
+                    secret,
                     listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec))
             )
-
-            // set up megolm backup
-            val creationInfo = awaitCallback<MegolmBackupCreationInfo> {
-                session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
-            }
-            val version = awaitCallback<KeysVersion> {
-                session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it)
-            }
-            // Save it for gossiping
-            session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version)
-
-            extractCurveKeyFromRecoveryKey(creationInfo.recoveryKey)?.toBase64NoPadding()?.let { secret ->
-                ssssService.storeSecret(
-                        KEYBACKUP_SECRET_SSSS_NAME,
-                        secret,
-                        listOf(KeyRef(keyInfo.keyId, keyInfo.keySpec))
-                )
-            }
         }
     }
 
-    fun verifySASCrossSign(alice: Session, bob: Session, roomId: String) {
+    suspend fun verifySASCrossSign(alice: Session, bob: Session, roomId: String) {
         assertTrue(alice.cryptoService().crossSigningService().canCrossSign())
         assertTrue(bob.cryptoService().crossSigningService().canCrossSign())
 
@@ -415,30 +305,26 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
                 roomId = roomId
         ).transactionId
 
-        testHelper.waitWithLatch {
-            testHelper.retryPeriodicallyWithLatch(it) {
-                bobVerificationService.getExistingVerificationRequests(alice.myUserId).firstOrNull {
-                    it.requestInfo?.fromDevice == alice.sessionParams.deviceId
-                } != null
-            }
+        testHelper.retryPeriodically {
+            bobVerificationService.getExistingVerificationRequests(alice.myUserId).firstOrNull {
+                it.requestInfo?.fromDevice == alice.sessionParams.deviceId
+            } != null
         }
         val incomingRequest = bobVerificationService.getExistingVerificationRequests(alice.myUserId).first {
             it.requestInfo?.fromDevice == alice.sessionParams.deviceId
         }
-        bobVerificationService.readyPendingVerification(listOf(VerificationMethod.SAS), alice.myUserId, incomingRequest.transactionId!!)
+        bobVerificationService.readyPendingVerificationInDMs(listOf(VerificationMethod.SAS), alice.myUserId, roomId, incomingRequest.transactionId!!)
 
         var requestID: String? = null
         // wait for it to be readied
-        testHelper.waitWithLatch {
-            testHelper.retryPeriodicallyWithLatch(it) {
-                val outgoingRequest = aliceVerificationService.getExistingVerificationRequests(bob.myUserId)
-                        .firstOrNull { it.localId == localId }
-                if (outgoingRequest?.isReady == true) {
-                    requestID = outgoingRequest.transactionId!!
-                    true
-                } else {
-                    false
-                }
+        testHelper.retryPeriodically {
+            val outgoingRequest = aliceVerificationService.getExistingVerificationRequests(bob.myUserId)
+                    .firstOrNull { it.localId == localId }
+            if (outgoingRequest?.isReady == true) {
+                requestID = outgoingRequest.transactionId!!
+                true
+            } else {
+                false
             }
         }
 
@@ -454,23 +340,19 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
         var alicePovTx: OutgoingSasVerificationTransaction? = null
         var bobPovTx: IncomingSasVerificationTransaction? = null
 
-        testHelper.waitWithLatch {
-            testHelper.retryPeriodicallyWithLatch(it) {
-                alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID!!) as? OutgoingSasVerificationTransaction
-                Log.v("TEST", "== alicePovTx is ${alicePovTx?.uxState}")
-                alicePovTx?.state == VerificationTxState.ShortCodeReady
-            }
+        testHelper.retryPeriodically {
+            alicePovTx = aliceVerificationService.getExistingTransaction(bob.myUserId, requestID!!) as? OutgoingSasVerificationTransaction
+            Log.v("TEST", "== alicePovTx is ${alicePovTx?.uxState}")
+            alicePovTx?.state == VerificationTxState.ShortCodeReady
         }
         // wait for alice to get the ready
-        testHelper.waitWithLatch {
-            testHelper.retryPeriodicallyWithLatch(it) {
-                bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID!!) as? IncomingSasVerificationTransaction
-                Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}")
-                if (bobPovTx?.state == VerificationTxState.OnStarted) {
-                    bobPovTx?.performAccept()
-                }
-                bobPovTx?.state == VerificationTxState.ShortCodeReady
+        testHelper.retryPeriodically {
+            bobPovTx = bobVerificationService.getExistingTransaction(alice.myUserId, requestID!!) as? IncomingSasVerificationTransaction
+            Log.v("TEST", "== bobPovTx is ${alicePovTx?.uxState}")
+            if (bobPovTx?.state == VerificationTxState.OnStarted) {
+                bobPovTx?.performAccept()
             }
+            bobPovTx?.state == VerificationTxState.ShortCodeReady
         }
 
         assertEquals("SAS code do not match", alicePovTx!!.getDecimalCodeRepresentation(), bobPovTx!!.getDecimalCodeRepresentation())
@@ -478,38 +360,30 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
         bobPovTx!!.userHasVerifiedShortCode()
         alicePovTx!!.userHasVerifiedShortCode()
 
-        testHelper.waitWithLatch {
-            testHelper.retryPeriodicallyWithLatch(it) {
-                alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId)
-            }
+        testHelper.retryPeriodically {
+            alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId)
         }
 
-        testHelper.waitWithLatch {
-            testHelper.retryPeriodicallyWithLatch(it) {
-                alice.cryptoService().crossSigningService().isUserTrusted(bob.myUserId)
-            }
+        testHelper.retryPeriodically {
+            bob.cryptoService().crossSigningService().isUserTrusted(alice.myUserId)
         }
     }
 
-    fun doE2ETestWithManyMembers(numberOfMembers: Int): CryptoTestData {
+    suspend fun doE2ETestWithManyMembers(numberOfMembers: Int): CryptoTestData {
         val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
         aliceSession.cryptoService().setWarnOnUnknownDevices(false)
 
-        val roomId = testHelper.runBlockingTest {
-            aliceSession.roomService().createRoom(CreateRoomParams().apply { name = "MyRoom" })
-        }
+        val roomId = aliceSession.roomService().createRoom(CreateRoomParams().apply { name = "MyRoom" })
         val room = aliceSession.getRoom(roomId)!!
 
-        testHelper.runBlockingTest {
-            room.roomCryptoService().enableEncryption()
-        }
+        room.roomCryptoService().enableEncryption()
 
         val sessions = mutableListOf(aliceSession)
         for (index in 1 until numberOfMembers) {
             val session = testHelper.createAccount("User_$index", defaultSessionParams)
-            testHelper.runBlockingTest(timeout = 600_000) { room.membershipService().invite(session.myUserId, null) }
+            room.membershipService().invite(session.myUserId, null)
             println("TEST -> " + session.myUserId + " invited")
-            testHelper.runBlockingTest { session.roomService().joinRoom(room.roomId, null, emptyList()) }
+            session.roomService().joinRoom(room.roomId, null, emptyList())
             println("TEST -> " + session.myUserId + " joined")
             sessions.add(session)
         }
@@ -517,48 +391,43 @@ class CryptoTestHelper(val testHelper: CommonTestHelper) {
         return CryptoTestData(roomId, sessions)
     }
 
-    fun ensureCanDecrypt(sentEventIds: List<String>, session: Session, e2eRoomID: String, messagesText: List<String>) {
+    suspend fun ensureCanDecrypt(sentEventIds: List<String>, session: Session, e2eRoomID: String, messagesText: List<String>) {
         sentEventIds.forEachIndexed { index, sentEventId ->
-            testHelper.waitWithLatch { latch ->
-                testHelper.retryPeriodicallyWithLatch(latch) {
-                    val event = session.getRoom(e2eRoomID)!!.timelineService().getTimelineEvent(sentEventId)!!.root
-                    testHelper.runBlockingTest {
-                        try {
-                            session.cryptoService().decryptEvent(event, "").let { result ->
-                                event.mxDecryptionResult = OlmDecryptionResult(
-                                        payload = result.clearEvent,
-                                        senderKey = result.senderCurve25519Key,
-                                        keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
-                                        forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
-                                        isSafe = result.isSafe
-                                )
-                            }
-                        } catch (error: MXCryptoError) {
-                            // nop
-                        }
+            testHelper.retryPeriodically {
+                val event = session.getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(sentEventId)?.root
+                        ?: return@retryPeriodically false
+                try {
+                    session.cryptoService().decryptEvent(event, "").let { result ->
+                        event.mxDecryptionResult = OlmDecryptionResult(
+                                payload = result.clearEvent,
+                                senderKey = result.senderCurve25519Key,
+                                keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
+                                forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain,
+                                isSafe = result.isSafe
+                        )
                     }
-                    Log.v("TEST", "ensureCanDecrypt ${event.getClearType()} is ${event.getClearContent()}")
-                    event.getClearType() == EventType.MESSAGE &&
-                            messagesText[index] == event.getClearContent()?.toModel<MessageContent>()?.body
+                } catch (error: MXCryptoError) {
+                    // nop
                 }
+                Log.v("TEST", "ensureCanDecrypt ${event.getClearType()} is ${event.getClearContent()}")
+                event.getClearType() == EventType.MESSAGE &&
+                        messagesText[index] == event.getClearContent()?.toModel<MessageContent>()?.body
             }
         }
     }
 
-    fun ensureCannotDecrypt(sentEventIds: List<String>, session: Session, e2eRoomID: String, expectedError: MXCryptoError.ErrorType? = null) {
+    suspend fun ensureCannotDecrypt(sentEventIds: List<String>, session: Session, e2eRoomID: String, expectedError: MXCryptoError.ErrorType? = null) {
         sentEventIds.forEach { sentEventId ->
             val event = session.getRoom(e2eRoomID)!!.timelineService().getTimelineEvent(sentEventId)!!.root
-            testHelper.runBlockingTest {
-                try {
-                    session.cryptoService().decryptEvent(event, "")
-                    fail("Should not be able to decrypt event")
-                } catch (error: MXCryptoError) {
-                    val errorType = (error as? MXCryptoError.Base)?.errorType
-                    if (expectedError == null) {
-                        assertNotNull(errorType)
-                    } else {
-                        assertEquals("Unexpected reason", expectedError, errorType)
-                    }
+            try {
+                session.cryptoService().decryptEvent(event, "")
+                fail("Should not be able to decrypt event")
+            } catch (error: MXCryptoError) {
+                val errorType = (error as? MXCryptoError.Base)?.errorType
+                if (expectedError == null) {
+                    assertNotNull(errorType)
+                } else {
+                    assertEquals("Unexpected reason", expectedError, errorType)
                 }
             }
         }
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestExtensions.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestExtensions.kt
new file mode 100644
index 0000000000000000000000000000000000000000..8f89d42ac07211fa9e83ee2ce533a1f3c24879c6
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestExtensions.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2022 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.common
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.Observer
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.withTimeout
+import kotlin.coroutines.resume
+
+suspend fun <T, R> T.onMain(block: T.() -> R): R {
+    return withContext(Dispatchers.Main) {
+        block(this@onMain)
+    }
+}
+
+suspend fun <T> LiveData<T>.first(timeout: Long = TestConstants.timeOutMillis, predicate: (T) -> Boolean): T {
+    return wrapWithTimeout(timeout) {
+        withContext(Dispatchers.Main) {
+            suspendCancellableCoroutine { continuation ->
+                val observer = object : Observer<T> {
+                    override fun onChanged(data: T) {
+                        if (predicate(data)) {
+                            removeObserver(this)
+                            continuation.resume(data)
+                        }
+                    }
+                }
+                observeForever(observer)
+                continuation.invokeOnCancellation { removeObserver(observer) }
+            }
+        }
+    }
+}
+
+suspend fun <T> waitFor(continueWhen: suspend () -> T, action: suspend () -> Unit) {
+    coroutineScope {
+        val deferred = async { continueWhen() }
+        action()
+        deferred.await()
+    }
+}
+
+suspend fun <T> wrapWithTimeout(timeout: Long = TestConstants.timeOutMillis, block: suspend () -> T): T {
+    val deferred = coroutineScope {
+        async { block() }
+    }
+    return withTimeout(timeout) { deferred.await() }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/DecryptRedactedEventTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/DecryptRedactedEventTest.kt
index a48b45a1f53fb78b819c310679fc274dc90aee20..4e1efbb700bdbe3d2ea700701f88be323b04e940 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/DecryptRedactedEventTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/DecryptRedactedEventTest.kt
@@ -46,30 +46,26 @@ class DecryptRedactedEventTest : InstrumentedTest {
         roomALicePOV.sendService().redactEvent(timelineEvent.root, redactionReason)
 
         // get the event from bob
-        testHelper.waitWithLatch {
-            testHelper.retryPeriodicallyWithLatch(it) {
-                bobSession.getRoom(e2eRoomID)?.getTimelineEvent(timelineEvent.eventId)?.root?.isRedacted() == true
-            }
+        testHelper.retryPeriodically {
+            bobSession.getRoom(e2eRoomID)?.getTimelineEvent(timelineEvent.eventId)?.root?.isRedacted() == true
         }
 
         val eventBobPov = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(timelineEvent.eventId)!!
 
-        testHelper.runBlockingTest {
-            try {
-                val result = bobSession.cryptoService().decryptEvent(eventBobPov.root, "")
-                Assert.assertEquals(
-                        "Unexpected redacted reason",
-                        redactionReason,
-                        result.clearEvent.toModel<Event>()?.unsignedData?.redactedEvent?.content?.get("reason")
-                )
-                Assert.assertEquals(
-                        "Unexpected Redacted event id",
-                        timelineEvent.eventId,
-                        result.clearEvent.toModel<Event>()?.unsignedData?.redactedEvent?.redacts
-                )
-            } catch (failure: Throwable) {
-                Assert.fail("Should not throw when decrypting a redacted event")
-            }
+        try {
+            val result = bobSession.cryptoService().decryptEvent(eventBobPov.root, "")
+            Assert.assertEquals(
+                    "Unexpected redacted reason",
+                    redactionReason,
+                    result.clearEvent.toModel<Event>()?.unsignedData?.redactedEvent?.content?.get("reason")
+            )
+            Assert.assertEquals(
+                    "Unexpected Redacted event id",
+                    timelineEvent.eventId,
+                    result.clearEvent.toModel<Event>()?.unsignedData?.redactedEvent?.redacts
+            )
+        } catch (failure: Throwable) {
+            Assert.fail("Should not throw when decrypting a redacted event")
         }
     }
 }
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2EShareKeysConfigTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2EShareKeysConfigTest.kt
index 32d63a1934241c23e99f0a6bdb7a54436923914b..cbbc4dc74e9874d065c4f9c97fcb69dc0302edfd 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2EShareKeysConfigTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2EShareKeysConfigTest.kt
@@ -40,7 +40,6 @@ import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
 import org.matrix.android.sdk.common.CryptoTestData
 import org.matrix.android.sdk.common.SessionTestParams
 import org.matrix.android.sdk.common.TestConstants
-import org.matrix.android.sdk.common.TestMatrixCallback
 
 @RunWith(JUnit4::class)
 @FixMethodOrder(MethodSorters.JVM)
@@ -57,18 +56,14 @@ class E2EShareKeysConfigTest : InstrumentedTest {
     fun ensureKeysAreNotSharedIfOptionDisabled() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
         val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true))
         aliceSession.cryptoService().enableShareKeyOnInvite(false)
-        val roomId = commonTestHelper.runBlockingTest {
-            aliceSession.roomService().createRoom(CreateRoomParams().apply {
-                historyVisibility = RoomHistoryVisibility.SHARED
-                name = "MyRoom"
-                enableEncryption()
-            })
-        }
-
-        commonTestHelper.waitWithLatch { latch ->
-            commonTestHelper.retryPeriodicallyWithLatch(latch) {
-                aliceSession.roomService().getRoomSummary(roomId)?.isEncrypted == true
-            }
+        val roomId = aliceSession.roomService().createRoom(CreateRoomParams().apply {
+            historyVisibility = RoomHistoryVisibility.SHARED
+            name = "MyRoom"
+            enableEncryption()
+        })
+
+        commonTestHelper.retryPeriodically {
+            aliceSession.roomService().getRoomSummary(roomId)?.isEncrypted == true
         }
         val roomAlice = aliceSession.roomService().getRoom(roomId)!!
 
@@ -81,9 +76,7 @@ class E2EShareKeysConfigTest : InstrumentedTest {
         val bobSession = commonTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams(withInitialSync = true))
 
         // Let alice invite bob
-        commonTestHelper.runBlockingTest {
-            roomAlice.membershipService().invite(bobSession.myUserId)
-        }
+        roomAlice.membershipService().invite(bobSession.myUserId)
 
         commonTestHelper.waitForAndAcceptInviteInRoom(bobSession, roomId)
 
@@ -114,9 +107,7 @@ class E2EShareKeysConfigTest : InstrumentedTest {
         val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true))
 
         // Let alice invite sam
-        commonTestHelper.runBlockingTest {
-            roomAlice.membershipService().invite(samSession.myUserId)
-        }
+        roomAlice.membershipService().invite(samSession.myUserId)
 
         commonTestHelper.waitForAndAcceptInviteInRoom(samSession, roomId)
 
@@ -135,7 +126,7 @@ class E2EShareKeysConfigTest : InstrumentedTest {
     }
 
     @Test
-    fun ifSharingDisabledOnAliceSideBobShouldNotShareAliceHistoty() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
+    fun ifSharingDisabledOnAliceSideBobShouldNotShareAliceHistory() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
         val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(roomHistoryVisibility = RoomHistoryVisibility.SHARED)
         val aliceSession = testData.firstSession.also {
             it.cryptoService().enableShareKeyOnInvite(false)
@@ -162,7 +153,7 @@ class E2EShareKeysConfigTest : InstrumentedTest {
     }
 
     @Test
-    fun ifSharingEnabledOnAliceSideBobShouldShareAliceHistoty() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
+    fun ifSharingEnabledOnAliceSideBobShouldShareAliceHistory() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
         val testData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(roomHistoryVisibility = RoomHistoryVisibility.SHARED)
         val aliceSession = testData.firstSession.also {
             it.cryptoService().enableShareKeyOnInvite(true)
@@ -186,7 +177,7 @@ class E2EShareKeysConfigTest : InstrumentedTest {
                 fromBobSharable.map { it.root.getClearContent()?.get("body") as String })
     }
 
-    private fun commonAliceAndBobSendMessages(commonTestHelper: CommonTestHelper, aliceSession: Session, testData: CryptoTestData, bobSession: Session): Triple<List<TimelineEvent>, List<TimelineEvent>, Session> {
+    private suspend fun commonAliceAndBobSendMessages(commonTestHelper: CommonTestHelper, aliceSession: Session, testData: CryptoTestData, bobSession: Session): Triple<List<TimelineEvent>, List<TimelineEvent>, Session> {
         val fromAliceNotSharable = commonTestHelper.sendTextMessage(aliceSession.getRoom(testData.roomId)!!, "Hello from alice", 1)
         val fromBobSharable = commonTestHelper.sendTextMessage(bobSession.getRoom(testData.roomId)!!, "Hello from bob", 1)
 
@@ -195,9 +186,7 @@ class E2EShareKeysConfigTest : InstrumentedTest {
         val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true))
 
         // Let bob invite sam
-        commonTestHelper.runBlockingTest {
-            bobSession.getRoom(testData.roomId)!!.membershipService().invite(samSession.myUserId)
-        }
+        bobSession.getRoom(testData.roomId)!!.membershipService().invite(samSession.myUserId)
 
         commonTestHelper.waitForAndAcceptInviteInRoom(samSession, testData.roomId)
         return Triple(fromAliceNotSharable, fromBobSharable, samSession)
@@ -209,18 +198,14 @@ class E2EShareKeysConfigTest : InstrumentedTest {
     fun testBackupFlagIsCorrect() = runCryptoTest(context()) { cryptoTestHelper, commonTestHelper ->
         val aliceSession = commonTestHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(withInitialSync = true))
         aliceSession.cryptoService().enableShareKeyOnInvite(false)
-        val roomId = commonTestHelper.runBlockingTest {
-            aliceSession.roomService().createRoom(CreateRoomParams().apply {
-                historyVisibility = RoomHistoryVisibility.SHARED
-                name = "MyRoom"
-                enableEncryption()
-            })
-        }
-
-        commonTestHelper.waitWithLatch { latch ->
-            commonTestHelper.retryPeriodicallyWithLatch(latch) {
-                aliceSession.roomService().getRoomSummary(roomId)?.isEncrypted == true
-            }
+        val roomId = aliceSession.roomService().createRoom(CreateRoomParams().apply {
+            historyVisibility = RoomHistoryVisibility.SHARED
+            name = "MyRoom"
+            enableEncryption()
+        })
+
+        commonTestHelper.retryPeriodically {
+            aliceSession.roomService().getRoomSummary(roomId)?.isEncrypted == true
         }
         val roomAlice = aliceSession.roomService().getRoom(roomId)!!
 
@@ -232,18 +217,15 @@ class E2EShareKeysConfigTest : InstrumentedTest {
         Log.v("#E2E TEST", "Create and start key backup for bob ...")
         val keysBackupService = aliceSession.cryptoService().keysBackupService()
         val keyBackupPassword = "FooBarBaz"
-        val megolmBackupCreationInfo = commonTestHelper.doSync<MegolmBackupCreationInfo> {
+        val megolmBackupCreationInfo = commonTestHelper.waitForCallback<MegolmBackupCreationInfo> {
             keysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it)
         }
-        val version = commonTestHelper.doSync<KeysVersion> {
+        val version = commonTestHelper.waitForCallback<KeysVersion> {
             keysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it)
         }
 
-        commonTestHelper.waitWithLatch { latch ->
-            keysBackupService.backupAllGroupSessions(
-                    null,
-                    TestMatrixCallback(latch, true)
-            )
+        commonTestHelper.waitForCallback<Unit> {
+            keysBackupService.backupAllGroupSessions(null, it)
         }
 
         // signout
@@ -253,11 +235,11 @@ class E2EShareKeysConfigTest : InstrumentedTest {
         newAliceSession.cryptoService().enableShareKeyOnInvite(true)
 
         newAliceSession.cryptoService().keysBackupService().let { kbs ->
-            val keyVersionResult = commonTestHelper.doSync<KeysVersionResult?> {
+            val keyVersionResult = commonTestHelper.waitForCallback<KeysVersionResult?> {
                 kbs.getVersion(version.version, it)
             }
 
-            val importedResult = commonTestHelper.doSync<ImportRoomKeysResult> {
+            val importedResult = commonTestHelper.waitForCallback<ImportRoomKeysResult> {
                 kbs.restoreKeyBackupWithPassword(
                         keyVersionResult!!,
                         keyBackupPassword,
@@ -276,9 +258,7 @@ class E2EShareKeysConfigTest : InstrumentedTest {
         val samSession = commonTestHelper.createAccount(TestConstants.USER_SAM, SessionTestParams(withInitialSync = true))
 
         // Let alice invite sam
-        commonTestHelper.runBlockingTest {
-            newAliceSession.getRoom(roomId)!!.membershipService().invite(samSession.myUserId)
-        }
+        newAliceSession.getRoom(roomId)!!.membershipService().invite(samSession.myUserId)
 
         commonTestHelper.waitForAndAcceptInviteInRoom(samSession, roomId)
 
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeConfigTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeConfigTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..8b12092b794fbb1a070285770a6ee733ecde4a39
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeConfigTest.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2022 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
+
+import androidx.test.filters.LargeTest
+import org.amshove.kluent.shouldBe
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.junit.runners.MethodSorters
+import org.matrix.android.sdk.InstrumentedTest
+import org.matrix.android.sdk.api.session.crypto.MXCryptoError
+import org.matrix.android.sdk.api.session.getRoom
+import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
+import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
+
+@RunWith(JUnit4::class)
+@FixMethodOrder(MethodSorters.JVM)
+@LargeTest
+class E2eeConfigTest : InstrumentedTest {
+
+    @Test
+    fun testBlacklistUnverifiedDefault() = runCryptoTest(context()) { cryptoTestHelper, _ ->
+        val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
+
+        cryptoTestData.firstSession.cryptoService().getGlobalBlacklistUnverifiedDevices() shouldBe false
+        cryptoTestData.firstSession.cryptoService().isRoomBlacklistUnverifiedDevices(cryptoTestData.roomId) shouldBe false
+        cryptoTestData.secondSession!!.cryptoService().getGlobalBlacklistUnverifiedDevices() shouldBe false
+        cryptoTestData.secondSession!!.cryptoService().isRoomBlacklistUnverifiedDevices(cryptoTestData.roomId) shouldBe false
+    }
+
+    @Test
+    fun testCantDecryptIfGlobalUnverified() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
+        val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
+
+        cryptoTestData.firstSession.cryptoService().setGlobalBlacklistUnverifiedDevices(true)
+
+        val roomAlicePOV = cryptoTestData.firstSession.roomService().getRoom(cryptoTestData.roomId)!!
+
+        val sentMessage = testHelper.sendTextMessage(roomAlicePOV, "you are blocked", 1).first()
+
+        val roomBobPOV = cryptoTestData.secondSession!!.roomService().getRoom(cryptoTestData.roomId)!!
+        // ensure other received
+        testHelper.retryPeriodically {
+            roomBobPOV.timelineService().getTimelineEvent(sentMessage.eventId) != null
+        }
+
+        cryptoTestHelper.ensureCannotDecrypt(listOf(sentMessage.eventId), cryptoTestData.secondSession!!, cryptoTestData.roomId)
+    }
+
+    @Test
+    fun testCanDecryptIfGlobalUnverifiedAndUserTrusted() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
+        val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
+
+        cryptoTestHelper.initializeCrossSigning(cryptoTestData.firstSession)
+        cryptoTestHelper.initializeCrossSigning(cryptoTestData.secondSession!!)
+
+        cryptoTestHelper.verifySASCrossSign(cryptoTestData.firstSession, cryptoTestData.secondSession!!, cryptoTestData.roomId)
+
+        cryptoTestData.firstSession.cryptoService().setGlobalBlacklistUnverifiedDevices(true)
+
+        val roomAlicePOV = cryptoTestData.firstSession.roomService().getRoom(cryptoTestData.roomId)!!
+
+        val sentMessage = testHelper.sendTextMessage(roomAlicePOV, "you can read", 1).first()
+
+        val roomBobPOV = cryptoTestData.secondSession!!.roomService().getRoom(cryptoTestData.roomId)!!
+        // ensure other received
+        testHelper.retryPeriodically {
+            roomBobPOV.timelineService().getTimelineEvent(sentMessage.eventId) != null
+        }
+
+        cryptoTestHelper.ensureCanDecrypt(
+                listOf(sentMessage.eventId),
+                cryptoTestData.secondSession!!,
+                cryptoTestData.roomId,
+                listOf(sentMessage.getLastMessageContent()!!.body)
+        )
+    }
+
+    @Test
+    fun testCantDecryptIfPerRoomUnverified() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
+        val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true)
+
+        val roomAlicePOV = cryptoTestData.firstSession.roomService().getRoom(cryptoTestData.roomId)!!
+
+        val beforeMessage = testHelper.sendTextMessage(roomAlicePOV, "you can read", 1).first()
+
+        val roomBobPOV = cryptoTestData.secondSession!!.roomService().getRoom(cryptoTestData.roomId)!!
+        // ensure other received
+        testHelper.retryPeriodically {
+            roomBobPOV.timelineService().getTimelineEvent(beforeMessage.eventId) != null
+        }
+
+        cryptoTestHelper.ensureCanDecrypt(
+                listOf(beforeMessage.eventId),
+                cryptoTestData.secondSession!!,
+                cryptoTestData.roomId,
+                listOf(beforeMessage.getLastMessageContent()!!.body)
+        )
+
+        cryptoTestData.firstSession.cryptoService().setRoomBlockUnverifiedDevices(cryptoTestData.roomId, true)
+
+        val afterMessage = testHelper.sendTextMessage(roomAlicePOV, "you are blocked", 1).first()
+
+        // ensure received
+        testHelper.retryPeriodically {
+            cryptoTestData.secondSession?.getRoom(cryptoTestData.roomId)?.timelineService()?.getTimelineEvent(afterMessage.eventId)?.root != null
+        }
+
+        cryptoTestHelper.ensureCannotDecrypt(
+                listOf(afterMessage.eventId),
+                cryptoTestData.secondSession!!,
+                cryptoTestData.roomId,
+                MXCryptoError.ErrorType.KEYS_WITHHELD
+        )
+    }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt
index 410fb4f5d4778be9e415eeca3b933eec41605805..a36ba8ac028cedbd26edcbf3e38f4615b4c242b3 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeSanityTests.kt
@@ -18,17 +18,25 @@ package org.matrix.android.sdk.internal.crypto
 
 import android.util.Log
 import androidx.test.filters.LargeTest
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
 import kotlinx.coroutines.delay
+import kotlinx.coroutines.suspendCancellableCoroutine
 import org.amshove.kluent.fail
 import org.amshove.kluent.internal.assertEquals
 import org.junit.Assert
 import org.junit.FixMethodOrder
-import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
 import org.junit.runners.MethodSorters
 import org.matrix.android.sdk.InstrumentedTest
+import org.matrix.android.sdk.api.auth.UIABaseAuth
+import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
+import org.matrix.android.sdk.api.auth.UserPasswordAuth
+import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
 import org.matrix.android.sdk.api.crypto.MXCryptoConfig
 import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
@@ -56,12 +64,12 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
 import org.matrix.android.sdk.common.CommonTestHelper
 import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
 import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
-import org.matrix.android.sdk.common.RetryTestRule
 import org.matrix.android.sdk.common.SessionTestParams
 import org.matrix.android.sdk.common.TestConstants
-import org.matrix.android.sdk.common.TestMatrixCallback
 import org.matrix.android.sdk.mustFail
-import java.util.concurrent.CountDownLatch
+import timber.log.Timber
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.resume
 
 // @Ignore("This test fails with an unhandled exception thrown from a coroutine which terminates the entire test run.")
 @RunWith(JUnit4::class)
@@ -69,8 +77,6 @@ import java.util.concurrent.CountDownLatch
 @LargeTest
 class E2eeSanityTests : InstrumentedTest {
 
-    @get:Rule val rule = RetryTestRule(3)
-
     /**
      * Simple test that create an e2ee room.
      * Some new members are added, and a message is sent.
@@ -103,10 +109,8 @@ class E2eeSanityTests : InstrumentedTest {
         Log.v("#E2E TEST", "All accounts created")
         // we want to invite them in the room
         otherAccounts.forEach {
-            testHelper.runBlockingTest {
-                Log.v("#E2E TEST", "Alice invites ${it.myUserId}")
-                aliceRoomPOV.membershipService().invite(it.myUserId)
-            }
+            Log.v("#E2E TEST", "Alice invites ${it.myUserId}")
+            aliceRoomPOV.membershipService().invite(it.myUserId)
         }
 
         // All user should accept invite
@@ -128,14 +132,12 @@ class E2eeSanityTests : InstrumentedTest {
 
         // All should be able to decrypt
         otherAccounts.forEach { otherSession ->
-            testHelper.waitWithLatch { latch ->
-                testHelper.retryPeriodicallyWithLatch(latch) {
-                    val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!)
-                    timeLineEvent != null &&
-                            timeLineEvent.isEncrypted() &&
-                            timeLineEvent.root.getClearType() == EventType.MESSAGE &&
-                            timeLineEvent.root.mxDecryptionResult?.isSafe == true
-                }
+            testHelper.retryPeriodically {
+                val timeLineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!)
+                timeLineEvent != null &&
+                        timeLineEvent.isEncrypted() &&
+                        timeLineEvent.root.getClearType() == EventType.MESSAGE &&
+                        timeLineEvent.root.mxDecryptionResult?.isSafe == true
             }
         }
 
@@ -146,10 +148,8 @@ class E2eeSanityTests : InstrumentedTest {
                 }
 
         newAccount.forEach {
-            testHelper.runBlockingTest {
-                Log.v("#E2E TEST", "Alice invites ${it.myUserId}")
-                aliceRoomPOV.membershipService().invite(it.myUserId)
-            }
+            Log.v("#E2E TEST", "Alice invites ${it.myUserId}")
+            aliceRoomPOV.membershipService().invite(it.myUserId)
         }
 
         newAccount.forEach {
@@ -159,21 +159,17 @@ class E2eeSanityTests : InstrumentedTest {
         ensureMembersHaveJoined(testHelper, aliceSession, newAccount, e2eRoomID)
 
         // wait a bit
-        testHelper.runBlockingTest {
-            delay(3_000)
-        }
+        delay(3_000)
 
         // check that messages are encrypted (uisi)
         newAccount.forEach { otherSession ->
-            testHelper.waitWithLatch { latch ->
-                testHelper.retryPeriodicallyWithLatch(latch) {
-                    val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!).also {
-                        Log.v("#E2E TEST", "Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}")
-                    }
-                    timelineEvent != null &&
-                            timelineEvent.root.getClearType() == EventType.ENCRYPTED &&
-                            timelineEvent.root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID
+            testHelper.retryPeriodically {
+                val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId!!).also {
+                    Log.v("#E2E TEST", "Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}")
                 }
+                timelineEvent != null &&
+                        timelineEvent.root.getClearType() == EventType.ENCRYPTED &&
+                        timelineEvent.root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID
             }
         }
 
@@ -185,15 +181,13 @@ class E2eeSanityTests : InstrumentedTest {
 
         // new members should be able to decrypt it
         newAccount.forEach { otherSession ->
-            testHelper.waitWithLatch { latch ->
-                testHelper.retryPeriodicallyWithLatch(latch) {
-                    val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(secondSentEventId!!).also {
-                        Log.v("#E2E TEST", "Second Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}")
-                    }
-                    timelineEvent != null &&
-                            timelineEvent.root.getClearType() == EventType.MESSAGE &&
-                            secondMessage == timelineEvent.root.getClearContent().toModel<MessageContent>()?.body
+            testHelper.retryPeriodically {
+                val timelineEvent = otherSession.getRoom(e2eRoomID)?.getTimelineEvent(secondSentEventId!!).also {
+                    Log.v("#E2E TEST", "Second Event seen by new user ${it?.root?.getClearType()}|${it?.root?.mCryptoError}")
                 }
+                timelineEvent != null &&
+                        timelineEvent.root.getClearType() == EventType.MESSAGE &&
+                        secondMessage == timelineEvent.root.getClearContent().toModel<MessageContent>()?.body
             }
         }
     }
@@ -229,10 +223,10 @@ class E2eeSanityTests : InstrumentedTest {
         Log.v("#E2E TEST", "Create and start key backup for bob ...")
         val bobKeysBackupService = bobSession.cryptoService().keysBackupService()
         val keyBackupPassword = "FooBarBaz"
-        val megolmBackupCreationInfo = testHelper.doSync<MegolmBackupCreationInfo> {
+        val megolmBackupCreationInfo = testHelper.waitForCallback<MegolmBackupCreationInfo> {
             bobKeysBackupService.prepareKeysBackupVersion(keyBackupPassword, null, it)
         }
-        val version = testHelper.doSync<KeysVersion> {
+        val version = testHelper.waitForCallback<KeysVersion> {
             bobKeysBackupService.createKeysBackupVersion(megolmBackupCreationInfo, it)
         }
         Log.v("#E2E TEST", "... Key backup started and enabled for bob")
@@ -248,32 +242,21 @@ class E2eeSanityTests : InstrumentedTest {
                 sentEventIds.add(it)
             }
 
-            testHelper.waitWithLatch { latch ->
-                testHelper.retryPeriodicallyWithLatch(latch) {
-                    val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
-                    timeLineEvent != null &&
-                            timeLineEvent.isEncrypted() &&
-                            timeLineEvent.root.getClearType() == EventType.MESSAGE
-                }
+            testHelper.retryPeriodically {
+                val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
+                timeLineEvent != null &&
+                        timeLineEvent.isEncrypted() &&
+                        timeLineEvent.root.getClearType() == EventType.MESSAGE
             }
             // we want more so let's discard the session
             aliceSession.cryptoService().discardOutboundSession(e2eRoomID)
-
-            testHelper.runBlockingTest {
-                delay(1_000)
-            }
         }
         Log.v("#E2E TEST", "Bob received all and can decrypt")
 
         // Let's wait a bit to be sure that bob has backed up the session
 
         Log.v("#E2E TEST", "Force key backup for Bob...")
-        testHelper.waitWithLatch { latch ->
-            bobKeysBackupService.backupAllGroupSessions(
-                    null,
-                    TestMatrixCallback(latch, true)
-            )
-        }
+        testHelper.waitForCallback<Unit> { bobKeysBackupService.backupAllGroupSessions(null, it) }
         Log.v("#E2E TEST", "... Key backup done for Bob")
 
         // Now lets logout both alice and bob to ensure that we won't have any gossiping
@@ -284,9 +267,7 @@ class E2eeSanityTests : InstrumentedTest {
         testHelper.signOutAndClose(bobSession)
         Log.v("#E2E TEST", "..Logout alice and bob...")
 
-        testHelper.runBlockingTest {
-            delay(1_000)
-        }
+        delay(1_000)
 
         // Create a new session for bob
         Log.v("#E2E TEST", "Create a new session for Bob")
@@ -295,14 +276,11 @@ class E2eeSanityTests : InstrumentedTest {
         // check that bob can't currently decrypt
         Log.v("#E2E TEST", "check that bob can't currently decrypt")
         sentEventIds.forEach { sentEventId ->
-            testHelper.waitWithLatch { latch ->
-                testHelper.retryPeriodicallyWithLatch(latch) {
-                    val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)?.also {
-                        Log.v("#E2E TEST", "Event seen by new user ${it.root.getClearType()}|${it.root.mCryptoError}")
-                    }
-                    timelineEvent != null &&
-                            timelineEvent.root.getClearType() == EventType.ENCRYPTED
+            testHelper.retryPeriodically {
+                val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)?.also {
+                    Log.v("#E2E TEST", "Event seen by new user ${it.root.getClearType()}|${it.root.mCryptoError}")
                 }
+                timelineEvent != null && timelineEvent.root.getClearType() == EventType.ENCRYPTED
             }
         }
         // after initial sync events are not decrypted, so we have to try manually
@@ -311,11 +289,11 @@ class E2eeSanityTests : InstrumentedTest {
         // Let's now import keys from backup
 
         newBobSession.cryptoService().keysBackupService().let { kbs ->
-            val keyVersionResult = testHelper.doSync<KeysVersionResult?> {
+            val keyVersionResult = testHelper.waitForCallback<KeysVersionResult?> {
                 kbs.getVersion(version.version, it)
             }
 
-            val importedResult = testHelper.doSync<ImportRoomKeysResult> {
+            val importedResult = testHelper.waitForCallback<ImportRoomKeysResult> {
                 kbs.restoreKeyBackupWithPassword(
                         keyVersionResult!!,
                         keyBackupPassword,
@@ -335,9 +313,7 @@ class E2eeSanityTests : InstrumentedTest {
         // Check key trust
         sentEventIds.forEach { sentEventId ->
             val timelineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)!!
-            val result = testHelper.runBlockingTest {
-                newBobSession.cryptoService().decryptEvent(timelineEvent.root, "")
-            }
+            val result = newBobSession.cryptoService().decryptEvent(timelineEvent.root, "")
             assertEquals("Keys from history should be deniable", false, result.isSafe)
         }
     }
@@ -366,13 +342,11 @@ class E2eeSanityTests : InstrumentedTest {
                 sentEventIds.add(it)
             }
 
-            testHelper.waitWithLatch { latch ->
-                testHelper.retryPeriodicallyWithLatch(latch) {
-                    val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
-                    timeLineEvent != null &&
-                            timeLineEvent.isEncrypted() &&
-                            timeLineEvent.root.getClearType() == EventType.MESSAGE
-                }
+            testHelper.retryPeriodically {
+                val timeLineEvent = bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
+                timeLineEvent != null &&
+                        timeLineEvent.isEncrypted() &&
+                        timeLineEvent.root.getClearType() == EventType.MESSAGE
             }
         }
 
@@ -396,27 +370,24 @@ class E2eeSanityTests : InstrumentedTest {
         }
 
         // Ensure that new bob still can't decrypt (keys must have been withheld)
-        // as per new config we won't request to alice, so ignore following test
 //        sentEventIds.forEach { sentEventId ->
 //            val megolmSessionId = newBobSession.getRoom(e2eRoomID)!!
 //                    .getTimelineEvent(sentEventId)!!
 //                    .root.content.toModel<EncryptedEventContent>()!!.sessionId
-//            testHelper.waitWithLatch { latch ->
-//                testHelper.retryPeriodicallyWithLatch(latch) {
-//                    val aliceReply = newBobSession.cryptoService().getOutgoingRoomKeyRequests()
-//                            .first {
-//                                it.sessionId == megolmSessionId &&
-//                                        it.roomId == e2eRoomID
-//                            }
-//                            .results.also {
-//                                Log.w("##TEST", "result list is $it")
-//                            }
-//                            .firstOrNull { it.userId == aliceSession.myUserId }
-//                            ?.result
-//                    aliceReply != null &&
-//                            aliceReply is RequestResult.Failure &&
-//                            WithHeldCode.UNAUTHORISED == aliceReply.code
-//                }
+//            testHelper.retryPeriodically {
+//                val aliceReply = newBobSession.cryptoService().getOutgoingRoomKeyRequests()
+//                        .first {
+//                            it.sessionId == megolmSessionId &&
+//                                    it.roomId == e2eRoomID
+//                        }
+//                        .results.also {
+//                            Log.w("##TEST", "result list is $it")
+//                        }
+//                        .firstOrNull { it.userId == aliceSession.myUserId }
+//                        ?.result
+//                aliceReply != null &&
+//                        aliceReply is RequestResult.Failure &&
+//                        WithHeldCode.UNAUTHORISED == aliceReply.code
 //            }
 //        }
 
@@ -460,13 +431,11 @@ class E2eeSanityTests : InstrumentedTest {
         firstMessage.let { text ->
             firstEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!
 
-            testHelper.waitWithLatch { latch ->
-                testHelper.retryPeriodicallyWithLatch(latch) {
-                    val timeLineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId)
-                    timeLineEvent != null &&
-                            timeLineEvent.isEncrypted() &&
-                            timeLineEvent.root.getClearType() == EventType.MESSAGE
-                }
+            testHelper.retryPeriodically {
+                val timeLineEvent = bobSessionWithBetterKey.getRoom(e2eRoomID)?.getTimelineEvent(firstEventId)
+                timeLineEvent != null &&
+                        timeLineEvent.isEncrypted() &&
+                        timeLineEvent.root.getClearType() == EventType.MESSAGE
             }
         }
 
@@ -488,13 +457,11 @@ class E2eeSanityTests : InstrumentedTest {
         secondMessage.let { text ->
             secondEventId = sendMessageInRoom(testHelper, aliceRoomPOV, text)!!
 
-            testHelper.waitWithLatch { latch ->
-                testHelper.retryPeriodicallyWithLatch(latch) {
-                    val timeLineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId)
-                    timeLineEvent != null &&
-                            timeLineEvent.isEncrypted() &&
-                            timeLineEvent.root.getClearType() == EventType.MESSAGE
-                }
+            testHelper.retryPeriodically {
+                val timeLineEvent = newBobSession.getRoom(e2eRoomID)?.getTimelineEvent(secondEventId)
+                timeLineEvent != null &&
+                        timeLineEvent.isEncrypted() &&
+                        timeLineEvent.root.getClearType() == EventType.MESSAGE
             }
         }
 
@@ -508,18 +475,14 @@ class E2eeSanityTests : InstrumentedTest {
         Assert.assertTrue("Should be the same session id", firstSessionId == secondSessionId)
 
         // Confirm we can decrypt one but not the other
-        testHelper.runBlockingTest {
-            mustFail(message = "Should not be able to decrypt event") {
-                newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
-            }
+        mustFail(message = "Should not be able to decrypt event") {
+            newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
         }
 
-        testHelper.runBlockingTest {
-            try {
-                newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "")
-            } catch (error: MXCryptoError) {
-                fail("Should be able to decrypt event")
-            }
+        try {
+            newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "")
+        } catch (error: MXCryptoError) {
+            fail("Should be able to decrypt event")
         }
 
         // Now let's verify bobs session, and re-request keys
@@ -538,50 +501,42 @@ class E2eeSanityTests : InstrumentedTest {
 
         // old session should have shared the key at earliest known index now
         // we should be able to decrypt both
-        testHelper.waitWithLatch {
-            testHelper.retryPeriodicallyWithLatch(it) {
-                val canDecryptFirst = try {
-                    testHelper.runBlockingTest {
-                        newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
-                    }
-                    true
-                } catch (error: MXCryptoError) {
-                    false
-                }
-                val canDecryptSecond = try {
-                    testHelper.runBlockingTest {
-                        newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "")
-                    }
-                    true
-                } catch (error: MXCryptoError) {
-                    false
-                }
-                canDecryptFirst && canDecryptSecond
+        testHelper.retryPeriodically {
+            val canDecryptFirst = try {
+                newBobSession.cryptoService().decryptEvent(firstEventNewBobPov.root, "")
+                true
+            } catch (error: MXCryptoError) {
+                false
             }
+            val canDecryptSecond = try {
+                newBobSession.cryptoService().decryptEvent(secondEventNewBobPov.root, "")
+                true
+            } catch (error: MXCryptoError) {
+                false
+            }
+            canDecryptFirst && canDecryptSecond
         }
     }
 
-    private fun sendMessageInRoom(testHelper: CommonTestHelper, aliceRoomPOV: Room, text: String): String? {
-        aliceRoomPOV.sendService().sendTextMessage(text)
+    private suspend fun sendMessageInRoom(testHelper: CommonTestHelper, aliceRoomPOV: Room, text: String): String? {
         var sentEventId: String? = null
-        testHelper.waitWithLatch(4 * TestConstants.timeOutMillis) { latch ->
-            val timeline = aliceRoomPOV.timelineService().createTimeline(null, TimelineSettings(60))
-            timeline.start()
-            testHelper.retryPeriodicallyWithLatch(latch) {
-                val decryptedMsg = timeline.getSnapshot()
-                        .filter { it.root.getClearType() == EventType.MESSAGE }
-                        .also { list ->
-                            val message = list.joinToString(",", "[", "]") { "${it.root.type}|${it.root.sendState}" }
-                            Log.v("#E2E TEST", "Timeline snapshot is $message")
-                        }
-                        .filter { it.root.sendState == SendState.SYNCED }
-                        .firstOrNull { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(text) == true }
-                sentEventId = decryptedMsg?.eventId
-                decryptedMsg != null
-            }
+        aliceRoomPOV.sendService().sendTextMessage(text)
 
-            timeline.dispose()
+        val timeline = aliceRoomPOV.timelineService().createTimeline(null, TimelineSettings(60))
+        timeline.start()
+        testHelper.retryPeriodically {
+            val decryptedMsg = timeline.getSnapshot()
+                    .filter { it.root.getClearType() == EventType.MESSAGE }
+                    .also { list ->
+                        val message = list.joinToString(",", "[", "]") { "${it.root.type}|${it.root.sendState}" }
+                        Log.v("#E2E TEST", "Timeline snapshot is $message")
+                    }
+                    .filter { it.root.sendState == SendState.SYNCED }
+                    .firstOrNull { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(text) == true }
+            sentEventId = decryptedMsg?.eventId
+            decryptedMsg != null
         }
+        timeline.dispose()
         return sentEventId
     }
 
@@ -598,106 +553,35 @@ class E2eeSanityTests : InstrumentedTest {
 
         val aliceNewSession = testHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true))
 
-        val oldCompleteLatch = CountDownLatch(1)
-        lateinit var oldCode: String
-        aliceSession.cryptoService().verificationService().addListener(object : VerificationService.Listener {
-
-            override fun verificationRequestUpdated(pr: PendingVerificationRequest) {
-                val readyInfo = pr.readyInfo
-                if (readyInfo != null) {
-                    aliceSession.cryptoService().verificationService().beginKeyVerification(
-                            VerificationMethod.SAS,
-                            aliceSession.myUserId,
-                            readyInfo.fromDevice,
-                            readyInfo.transactionId
-
-                    )
-                }
-            }
-
-            override fun transactionUpdated(tx: VerificationTransaction) {
-                Log.d("##TEST", "exitsingPov: $tx")
-                val sasTx = tx as OutgoingSasVerificationTransaction
-                when (sasTx.uxState) {
-                    OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> {
-                        // for the test we just accept?
-                        oldCode = sasTx.getDecimalCodeRepresentation()
-                        sasTx.userHasVerifiedShortCode()
-                    }
-                    OutgoingSasVerificationTransaction.UxState.VERIFIED -> {
-                        // we can release this latch?
-                        oldCompleteLatch.countDown()
-                    }
-                    else -> Unit
-                }
-            }
-        })
-
-        val newCompleteLatch = CountDownLatch(1)
-        lateinit var newCode: String
-        aliceNewSession.cryptoService().verificationService().addListener(object : VerificationService.Listener {
-
-            override fun verificationRequestCreated(pr: PendingVerificationRequest) {
-                // let's ready
-                aliceNewSession.cryptoService().verificationService().readyPendingVerification(
-                        listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
-                        aliceSession.myUserId,
-                        pr.transactionId!!
-                )
-            }
-
-            var matchOnce = true
-            override fun transactionUpdated(tx: VerificationTransaction) {
-                Log.d("##TEST", "newPov: $tx")
-
-                val sasTx = tx as IncomingSasVerificationTransaction
-                when (sasTx.uxState) {
-                    IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> {
-                        // no need to accept as there was a request first it will auto accept
-                    }
-                    IncomingSasVerificationTransaction.UxState.SHOW_SAS -> {
-                        if (matchOnce) {
-                            sasTx.userHasVerifiedShortCode()
-                            newCode = sasTx.getDecimalCodeRepresentation()
-                            matchOnce = false
-                        }
-                    }
-                    IncomingSasVerificationTransaction.UxState.VERIFIED -> {
-                        newCompleteLatch.countDown()
-                    }
-                    else -> Unit
-                }
-            }
-        })
-
+        val deferredOldCode = aliceSession.cryptoService().verificationService().readOldVerificationCodeAsync(this, aliceSession.myUserId)
+        val deferredNewCode = aliceNewSession.cryptoService().verificationService().readNewVerificationCodeAsync(this, aliceSession.myUserId)
         // initiate self verification
         aliceSession.cryptoService().verificationService().requestKeyVerification(
                 listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
                 aliceNewSession.myUserId,
                 listOf(aliceNewSession.sessionParams.deviceId!!)
         )
-        testHelper.await(oldCompleteLatch)
-        testHelper.await(newCompleteLatch)
+
+        val (oldCode, newCode) = awaitAll(deferredOldCode, deferredNewCode)
+
         assertEquals("Decimal code should have matched", oldCode, newCode)
 
         // Assert that devices are verified
-        val newDeviceFromOldPov: CryptoDeviceInfo? = aliceSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceNewSession.sessionParams.deviceId)
-        val oldDeviceFromNewPov: CryptoDeviceInfo? = aliceSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceSession.sessionParams.deviceId)
+        val newDeviceFromOldPov: CryptoDeviceInfo? =
+                aliceSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceNewSession.sessionParams.deviceId)
+        val oldDeviceFromNewPov: CryptoDeviceInfo? =
+                aliceSession.cryptoService().getCryptoDeviceInfo(aliceSession.myUserId, aliceSession.sessionParams.deviceId)
 
         Assert.assertTrue("new device should be verified from old point of view", newDeviceFromOldPov!!.isVerified)
         Assert.assertTrue("old device should be verified from new point of view", oldDeviceFromNewPov!!.isVerified)
 
         // wait for secret gossiping to happen
-        testHelper.waitWithLatch { latch ->
-            testHelper.retryPeriodicallyWithLatch(latch) {
-                aliceNewSession.cryptoService().crossSigningService().allPrivateKeysKnown()
-            }
+        testHelper.retryPeriodically {
+            aliceNewSession.cryptoService().crossSigningService().allPrivateKeysKnown()
         }
 
-        testHelper.waitWithLatch { latch ->
-            testHelper.retryPeriodicallyWithLatch(latch) {
-                aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo() != null
-            }
+        testHelper.retryPeriodically {
+            aliceNewSession.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo() != null
         }
 
         assertEquals(
@@ -730,27 +614,191 @@ class E2eeSanityTests : InstrumentedTest {
         )
     }
 
-    private fun ensureMembersHaveJoined(testHelper: CommonTestHelper, aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String) {
-        testHelper.waitWithLatch { latch ->
-            testHelper.retryPeriodicallyWithLatch(latch) {
-                otherAccounts.map {
-                    aliceSession.roomService().getRoomMember(it.myUserId, e2eRoomID)?.membership
-                }.all {
-                    it == Membership.JOIN
+    @Test
+    fun test_EncryptionDoesNotHinderVerification() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
+        val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
+
+        val aliceSession = cryptoTestData.firstSession
+        val bobSession = cryptoTestData.secondSession
+
+        val aliceAuthParams = UserPasswordAuth(
+                user = aliceSession.myUserId,
+                password = TestConstants.PASSWORD
+        )
+        val bobAuthParams = UserPasswordAuth(
+                user = bobSession!!.myUserId,
+                password = TestConstants.PASSWORD
+        )
+
+        testHelper.waitForCallback {
+            aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
+                override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
+                    promise.resume(aliceAuthParams)
+                }
+            }, it)
+        }
+
+        testHelper.waitForCallback {
+            bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
+                override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
+                    promise.resume(bobAuthParams)
                 }
+            }, it)
+        }
+
+        // add a second session for bob but not cross signed
+
+        val secondBobSession = testHelper.logIntoAccount(bobSession.myUserId, SessionTestParams(true))
+
+        aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(true)
+
+        // The two bob session should not be able to decrypt any message
+
+        val roomFromAlicePOV = aliceSession.getRoom(cryptoTestData.roomId)!!
+        Timber.v("#TEST: Send a first message that should be withheld")
+        val sentEvent = sendMessageInRoom(testHelper, roomFromAlicePOV, "Hello")!!
+
+        // wait for it to be synced back the other side
+        Timber.v("#TEST: Wait for message to be synced back")
+        testHelper.retryPeriodically {
+            bobSession.roomService().getRoom(cryptoTestData.roomId)?.timelineService()?.getTimelineEvent(sentEvent) != null
+        }
+
+        testHelper.retryPeriodically {
+            secondBobSession.roomService().getRoom(cryptoTestData.roomId)?.timelineService()?.getTimelineEvent(sentEvent) != null
+        }
+
+        // bob should not be able to decrypt
+        Timber.v("#TEST: Ensure cannot be decrytped")
+        cryptoTestHelper.ensureCannotDecrypt(listOf(sentEvent), bobSession, cryptoTestData.roomId)
+        cryptoTestHelper.ensureCannotDecrypt(listOf(sentEvent), secondBobSession, cryptoTestData.roomId)
+
+        // let's try to verify, it should work even if bob devices are untrusted
+        Timber.v("#TEST: Do the verification")
+        cryptoTestHelper.verifySASCrossSign(aliceSession, bobSession, cryptoTestData.roomId)
+
+        Timber.v("#TEST: Send a second message, outbound session should have rotated and only bob 1rst session should decrypt")
+
+        val secondEvent = sendMessageInRoom(testHelper, roomFromAlicePOV, "World")!!
+        Timber.v("#TEST: Wait for message to be synced back")
+        testHelper.retryPeriodically {
+            bobSession.roomService().getRoom(cryptoTestData.roomId)?.timelineService()?.getTimelineEvent(secondEvent) != null
+        }
+
+        testHelper.retryPeriodically {
+            secondBobSession.roomService().getRoom(cryptoTestData.roomId)?.timelineService()?.getTimelineEvent(secondEvent) != null
+        }
+
+        cryptoTestHelper.ensureCanDecrypt(listOf(secondEvent), bobSession, cryptoTestData.roomId, listOf("World"))
+        cryptoTestHelper.ensureCannotDecrypt(listOf(secondEvent), secondBobSession, cryptoTestData.roomId)
+    }
+
+    private suspend fun VerificationService.readOldVerificationCodeAsync(scope: CoroutineScope, userId: String): Deferred<String> {
+        return scope.async {
+            suspendCancellableCoroutine { continuation ->
+                var oldCode: String? = null
+                val listener = object : VerificationService.Listener {
+
+                    override fun verificationRequestUpdated(pr: PendingVerificationRequest) {
+                        val readyInfo = pr.readyInfo
+                        if (readyInfo != null) {
+                            beginKeyVerification(
+                                    VerificationMethod.SAS,
+                                    userId,
+                                    readyInfo.fromDevice,
+                                    readyInfo.transactionId
+
+                            )
+                        }
+                    }
+
+                    override fun transactionUpdated(tx: VerificationTransaction) {
+                        Log.d("##TEST", "exitsingPov: $tx")
+                        val sasTx = tx as OutgoingSasVerificationTransaction
+                        when (sasTx.uxState) {
+                            OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> {
+                                // for the test we just accept?
+                                oldCode = sasTx.getDecimalCodeRepresentation()
+                                sasTx.userHasVerifiedShortCode()
+                            }
+                            OutgoingSasVerificationTransaction.UxState.VERIFIED -> {
+                                removeListener(this)
+                                // we can release this latch?
+                                continuation.resume(oldCode!!)
+                            }
+                            else -> Unit
+                        }
+                    }
+                }
+                addListener(listener)
+                continuation.invokeOnCancellation { removeListener(listener) }
             }
         }
     }
 
-    private fun ensureIsDecrypted(testHelper: CommonTestHelper, sentEventIds: List<String>, session: Session, e2eRoomID: String) {
-        testHelper.waitWithLatch { latch ->
-            sentEventIds.forEach { sentEventId ->
-                testHelper.retryPeriodicallyWithLatch(latch) {
-                    val timeLineEvent = session.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
-                    timeLineEvent != null &&
-                            timeLineEvent.isEncrypted() &&
-                            timeLineEvent.root.getClearType() == EventType.MESSAGE
+    private suspend fun VerificationService.readNewVerificationCodeAsync(scope: CoroutineScope, userId: String): Deferred<String> {
+        return scope.async {
+            suspendCancellableCoroutine { continuation ->
+                var newCode: String? = null
+
+                val listener = object : VerificationService.Listener {
+
+                    override fun verificationRequestCreated(pr: PendingVerificationRequest) {
+                        // let's ready
+                        readyPendingVerification(
+                                listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW),
+                                userId,
+                                pr.transactionId!!
+                        )
+                    }
+
+                    var matchOnce = true
+                    override fun transactionUpdated(tx: VerificationTransaction) {
+                        Log.d("##TEST", "newPov: $tx")
+
+                        val sasTx = tx as IncomingSasVerificationTransaction
+                        when (sasTx.uxState) {
+                            IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> {
+                                // no need to accept as there was a request first it will auto accept
+                            }
+                            IncomingSasVerificationTransaction.UxState.SHOW_SAS -> {
+                                if (matchOnce) {
+                                    sasTx.userHasVerifiedShortCode()
+                                    newCode = sasTx.getDecimalCodeRepresentation()
+                                    matchOnce = false
+                                }
+                            }
+                            IncomingSasVerificationTransaction.UxState.VERIFIED -> {
+                                removeListener(this)
+                                continuation.resume(newCode!!)
+                            }
+                            else -> Unit
+                        }
+                    }
                 }
+                addListener(listener)
+                continuation.invokeOnCancellation { removeListener(listener) }
+            }
+        }
+    }
+
+    private suspend fun ensureMembersHaveJoined(testHelper: CommonTestHelper, aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String) {
+        testHelper.retryPeriodically {
+            otherAccounts.map {
+                aliceSession.roomService().getRoomMember(it.myUserId, e2eRoomID)?.membership
+            }.all {
+                it == Membership.JOIN
+            }
+        }
+    }
+
+    private suspend fun ensureIsDecrypted(testHelper: CommonTestHelper, sentEventIds: List<String>, session: Session, e2eRoomID: String) {
+        sentEventIds.forEach { sentEventId ->
+            testHelper.retryPeriodically {
+                val timeLineEvent = session.getRoom(e2eRoomID)?.getTimelineEvent(sentEventId)
+                timeLineEvent != null &&
+                        timeLineEvent.isEncrypted() &&
+                        timeLineEvent.root.getClearType() == EventType.MESSAGE
             }
         }
     }
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt
index 4b44aab18b8b0030a48e279d7b3539282b80eaa5..91e0026c93d72b2ccaae21b12fad19a46f966b20 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/E2eeShareKeysHistoryTest.kt
@@ -41,8 +41,8 @@ import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibilityConten
 import org.matrix.android.sdk.api.session.room.model.shouldShareHistory
 import org.matrix.android.sdk.common.CommonTestHelper
 import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
-import org.matrix.android.sdk.common.CryptoTestHelper
 import org.matrix.android.sdk.common.SessionTestParams
+import org.matrix.android.sdk.common.wrapWithTimeout
 
 @RunWith(JUnit4::class)
 @FixMethodOrder(MethodSorters.JVM)
@@ -102,8 +102,7 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
                 Log.v("#E2E TEST", "Alice sent message to roomId: $e2eRoomID")
 
                 // Bob should be able to decrypt the message
-                testHelper.waitWithLatch { latch ->
-                    testHelper.retryPeriodicallyWithLatch(latch) {
+                testHelper.retryPeriodically {
                         val timelineEvent = bobSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!)
                         (timelineEvent != null &&
                                 timelineEvent.isEncrypted() &&
@@ -113,7 +112,6 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
                                 Log.v("#E2E TEST", "Bob can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}")
                             }
                         }
-                    }
                 }
 
                 // Create a new user
@@ -123,10 +121,8 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
                 Log.v("#E2E TEST", "Aris user created")
 
                 // Alice invites new user to the room
-                testHelper.runBlockingTest {
-                    Log.v("#E2E TEST", "Alice invites ${arisSession.myUserId}")
-                    aliceRoomPOV.membershipService().invite(arisSession.myUserId)
-                }
+                Log.v("#E2E TEST", "Alice invites ${arisSession.myUserId}")
+                aliceRoomPOV.membershipService().invite(arisSession.myUserId)
 
                 waitForAndAcceptInviteInRoom(arisSession, e2eRoomID, testHelper)
 
@@ -139,8 +135,7 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
                     null
                     -> {
                         // Aris should be able to decrypt the message
-                        testHelper.waitWithLatch { latch ->
-                            testHelper.retryPeriodicallyWithLatch(latch) {
+                        testHelper.retryPeriodically {
                                 val timelineEvent = arisSession.roomService().getRoom(e2eRoomID)?.timelineService()?.getTimelineEvent(aliceMessageId!!)
                                 (timelineEvent != null &&
                                         timelineEvent.isEncrypted() &&
@@ -151,19 +146,16 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
                                                 Log.v("#E2E TEST", "Aris can decrypt the message: ${timelineEvent?.root?.getDecryptedTextSummary()}")
                                             }
                                         }
-                            }
                         }
                     }
                     RoomHistoryVisibility.INVITED,
                     RoomHistoryVisibility.JOINED -> {
                         // Aris should not even be able to get the message
-                        testHelper.waitWithLatch { latch ->
-                            testHelper.retryPeriodicallyWithLatch(latch) {
-                                val timelineEvent = arisSession.roomService().getRoom(e2eRoomID)
-                                        ?.timelineService()
-                                        ?.getTimelineEvent(aliceMessageId!!)
-                                timelineEvent == null
-                            }
+                        testHelper.retryPeriodically {
+                            val timelineEvent = arisSession.roomService().getRoom(e2eRoomID)
+                                    ?.timelineService()
+                                    ?.getTimelineEvent(aliceMessageId!!)
+                            timelineEvent == null
                         }
                     }
                 }
@@ -241,10 +233,7 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
     private fun testRotationDueToVisibilityChange(
             initRoomHistoryVisibility: RoomHistoryVisibility,
             nextRoomHistoryVisibility: RoomHistoryVisibilityContent
-    ) {
-        val testHelper = CommonTestHelper(context())
-        val cryptoTestHelper = CryptoTestHelper(testHelper)
-
+    ) = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
         val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(true, initRoomHistoryVisibility)
         val e2eRoomID = cryptoTestData.roomId
 
@@ -270,96 +259,84 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
         // Bob should be able to decrypt the message
         var firstAliceMessageMegolmSessionId: String? = null
         val bobRoomPov = bobSession.roomService().getRoom(e2eRoomID)
-        testHelper.waitWithLatch { latch ->
-            testHelper.retryPeriodicallyWithLatch(latch) {
+        testHelper.retryPeriodically {
+            val timelineEvent = bobRoomPov
+                    ?.timelineService()
+                    ?.getTimelineEvent(aliceMessageId!!)
+            (timelineEvent != null &&
+                    timelineEvent.isEncrypted() &&
+                    timelineEvent.root.getClearType() == EventType.MESSAGE).also {
+                if (it) {
+                    firstAliceMessageMegolmSessionId = timelineEvent?.root?.content?.get("session_id") as? String
+                    Log.v(
+                            "#E2E TEST",
+                            "Bob can decrypt the message (sid:$firstAliceMessageMegolmSessionId): ${timelineEvent?.root?.getDecryptedTextSummary()}"
+                    )
+                }
+            }
+        }
+
+        Assert.assertNotNull("megolm session id can't be null", firstAliceMessageMegolmSessionId)
+
+        var secondAliceMessageSessionId: String? = null
+        sendMessageInRoom(aliceRoomPOV, "Other msg", testHelper)?.let { secondMessage ->
+            testHelper.retryPeriodically {
                 val timelineEvent = bobRoomPov
                         ?.timelineService()
-                        ?.getTimelineEvent(aliceMessageId!!)
+                        ?.getTimelineEvent(secondMessage)
                 (timelineEvent != null &&
                         timelineEvent.isEncrypted() &&
                         timelineEvent.root.getClearType() == EventType.MESSAGE).also {
                     if (it) {
-                        firstAliceMessageMegolmSessionId = timelineEvent?.root?.content?.get("session_id") as? String
+                        secondAliceMessageSessionId = timelineEvent?.root?.content?.get("session_id") as? String
                         Log.v(
                                 "#E2E TEST",
-                                "Bob can decrypt the message (sid:$firstAliceMessageMegolmSessionId): ${timelineEvent?.root?.getDecryptedTextSummary()}"
+                                "Bob can decrypt the message (sid:$secondAliceMessageSessionId): ${timelineEvent?.root?.getDecryptedTextSummary()}"
                         )
                     }
                 }
             }
         }
-
-        Assert.assertNotNull("megolm session id can't be null", firstAliceMessageMegolmSessionId)
-
-        var secondAliceMessageSessionId: String? = null
-        sendMessageInRoom(aliceRoomPOV, "Other msg", testHelper)?.let { secondMessage ->
-            testHelper.waitWithLatch { latch ->
-                testHelper.retryPeriodicallyWithLatch(latch) {
-                    val timelineEvent = bobRoomPov
-                            ?.timelineService()
-                            ?.getTimelineEvent(secondMessage)
-                    (timelineEvent != null &&
-                            timelineEvent.isEncrypted() &&
-                            timelineEvent.root.getClearType() == EventType.MESSAGE).also {
-                        if (it) {
-                            secondAliceMessageSessionId = timelineEvent?.root?.content?.get("session_id") as? String
-                            Log.v(
-                                    "#E2E TEST",
-                                    "Bob can decrypt the message (sid:$secondAliceMessageSessionId): ${timelineEvent?.root?.getDecryptedTextSummary()}"
-                            )
-                        }
-                    }
-                }
-            }
-        }
         assertEquals("No rotation needed session should be the same", firstAliceMessageMegolmSessionId, secondAliceMessageSessionId)
         Log.v("#E2E TEST ROTATION", "No rotation needed yet")
 
         // Let's change the room history visibility
-        testHelper.runBlockingTest {
-            aliceRoomPOV.stateService()
-                    .sendStateEvent(
-                            eventType = EventType.STATE_ROOM_HISTORY_VISIBILITY,
-                            stateKey = "",
-                            body = RoomHistoryVisibilityContent(
-                                    historyVisibilityStr = nextRoomHistoryVisibility.historyVisibilityStr
-                            ).toContent()
-                    )
-        }
+        aliceRoomPOV.stateService()
+                .sendStateEvent(
+                        eventType = EventType.STATE_ROOM_HISTORY_VISIBILITY,
+                        stateKey = "",
+                        body = RoomHistoryVisibilityContent(
+                                historyVisibilityStr = nextRoomHistoryVisibility.historyVisibilityStr
+                        ).toContent()
+                )
 
         // ensure that the state did synced down
-        testHelper.waitWithLatch { latch ->
-            testHelper.retryPeriodicallyWithLatch(latch) {
-                aliceRoomPOV.stateService().getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty)?.content
-                        ?.toModel<RoomHistoryVisibilityContent>()?.historyVisibility == nextRoomHistoryVisibility.historyVisibility
-            }
+        testHelper.retryPeriodically {
+            aliceRoomPOV.stateService().getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty)?.content
+                    ?.toModel<RoomHistoryVisibilityContent>()?.historyVisibility == nextRoomHistoryVisibility.historyVisibility
         }
 
-        testHelper.waitWithLatch { latch ->
-            testHelper.retryPeriodicallyWithLatch(latch) {
-                val roomVisibility = aliceSession.getRoom(e2eRoomID)!!
-                        .stateService()
-                        .getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty)
-                        ?.content
-                        ?.toModel<RoomHistoryVisibilityContent>()
-                Log.v("#E2E TEST ROTATION", "Room visibility changed from: ${initRoomHistoryVisibility.name} to: ${roomVisibility?.historyVisibility?.name}")
-                roomVisibility?.historyVisibility == nextRoomHistoryVisibility.historyVisibility
-            }
+        testHelper.retryPeriodically {
+            val roomVisibility = aliceSession.getRoom(e2eRoomID)!!
+                    .stateService()
+                    .getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty)
+                    ?.content
+                    ?.toModel<RoomHistoryVisibilityContent>()
+            Log.v("#E2E TEST ROTATION", "Room visibility changed from: ${initRoomHistoryVisibility.name} to: ${roomVisibility?.historyVisibility?.name}")
+            roomVisibility?.historyVisibility == nextRoomHistoryVisibility.historyVisibility
         }
 
         var aliceThirdMessageSessionId: String? = null
         sendMessageInRoom(aliceRoomPOV, "Message after visibility change", testHelper)?.let { thirdMessage ->
-            testHelper.waitWithLatch { latch ->
-                testHelper.retryPeriodicallyWithLatch(latch) {
-                    val timelineEvent = bobRoomPov
-                            ?.timelineService()
-                            ?.getTimelineEvent(thirdMessage)
-                    (timelineEvent != null &&
-                            timelineEvent.isEncrypted() &&
-                            timelineEvent.root.getClearType() == EventType.MESSAGE).also {
-                        if (it) {
-                            aliceThirdMessageSessionId = timelineEvent?.root?.content?.get("session_id") as? String
-                        }
+            testHelper.retryPeriodically {
+                val timelineEvent = bobRoomPov
+                        ?.timelineService()
+                        ?.getTimelineEvent(thirdMessage)
+                (timelineEvent != null &&
+                        timelineEvent.isEncrypted() &&
+                        timelineEvent.root.getClearType() == EventType.MESSAGE).also {
+                    if (it) {
+                        aliceThirdMessageSessionId = timelineEvent?.root?.content?.get("session_id") as? String
                     }
                 }
             }
@@ -379,38 +356,34 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
         cryptoTestData.cleanUp(testHelper)
     }
 
-    private fun sendMessageInRoom(aliceRoomPOV: Room, text: String, testHelper: CommonTestHelper): String? {
+    private suspend fun sendMessageInRoom(aliceRoomPOV: Room, text: String, testHelper: CommonTestHelper): String? {
         return testHelper.sendTextMessage(aliceRoomPOV, text, 1).firstOrNull()?.let {
             Log.v("#E2E TEST", "Message sent with session ${it.root.content?.get("session_id")}")
             return it.eventId
         }
     }
 
-    private fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String, testHelper: CommonTestHelper) {
-        testHelper.waitWithLatch { latch ->
-            testHelper.retryPeriodicallyWithLatch(latch) {
-                otherAccounts.map {
-                    aliceSession.roomService().getRoomMember(it.myUserId, e2eRoomID)?.membership
-                }.all {
-                    it == Membership.JOIN
-                }
+    private suspend fun ensureMembersHaveJoined(aliceSession: Session, otherAccounts: List<Session>, e2eRoomID: String, testHelper: CommonTestHelper) {
+        testHelper.retryPeriodically {
+            otherAccounts.map {
+                aliceSession.roomService().getRoomMember(it.myUserId, e2eRoomID)?.membership
+            }.all {
+                it == Membership.JOIN
             }
         }
     }
 
-    private fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String, testHelper: CommonTestHelper) {
-        testHelper.waitWithLatch { latch ->
-            testHelper.retryPeriodicallyWithLatch(latch) {
-                val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID)
-                (roomSummary != null && roomSummary.membership == Membership.INVITE).also {
-                    if (it) {
-                        Log.v("#E2E TEST", "${otherSession.myUserId} can see the invite from alice")
-                    }
+    private suspend fun waitForAndAcceptInviteInRoom(otherSession: Session, e2eRoomID: String, testHelper: CommonTestHelper) {
+        testHelper.retryPeriodically {
+            val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID)
+            (roomSummary != null && roomSummary.membership == Membership.INVITE).also {
+                if (it) {
+                    Log.v("#E2E TEST", "${otherSession.myUserId} can see the invite from alice")
                 }
             }
         }
 
-        testHelper.runBlockingTest(60_000) {
+        wrapWithTimeout(60_000) {
             Log.v("#E2E TEST", "${otherSession.myUserId} tries to join room $e2eRoomID")
             try {
                 otherSession.roomService().joinRoom(e2eRoomID)
@@ -420,11 +393,9 @@ class E2eeShareKeysHistoryTest : InstrumentedTest {
         }
 
         Log.v("#E2E TEST", "${otherSession.myUserId} waiting for join echo ...")
-        testHelper.waitWithLatch {
-            testHelper.retryPeriodicallyWithLatch(it) {
-                val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID)
-                roomSummary != null && roomSummary.membership == Membership.JOIN
-            }
+        testHelper.retryPeriodically {
+            val roomSummary = otherSession.roomService().getRoomSummary(e2eRoomID)
+            roomSummary != null && roomSummary.membership == Membership.JOIN
         }
     }
 }
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt
index e8e7b1d708444b9589884837a559a8e0ca442e9f..5c817443ce6adc0d9d828c2d964ce0d877f2d692 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/PreShareKeysTest.kt
@@ -52,15 +52,13 @@ class PreShareKeysTest : InstrumentedTest {
         Log.d("#Test", "Room Key Received from alice $preShareCount")
 
         // Force presharing of new outbound key
-        testHelper.doSync<Unit> {
+        testHelper.waitForCallback<Unit> {
             aliceSession.cryptoService().prepareToEncrypt(e2eRoomID, it)
         }
 
-        testHelper.waitWithLatch { latch ->
-            testHelper.retryPeriodicallyWithLatch(latch) {
-                val newKeysCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys()
-                newKeysCount > preShareCount
-            }
+        testHelper.retryPeriodically {
+            val newKeysCount = bobSession.cryptoService().keysBackupService().getTotalNumbersOfKeys()
+            newKeysCount > preShareCount
         }
 
         val aliceCryptoStore = (aliceSession.cryptoService() as DefaultCryptoService).cryptoStoreForTesting
@@ -85,10 +83,8 @@ class PreShareKeysTest : InstrumentedTest {
         val sentEvent = testHelper.sendTextMessage(aliceSession.getRoom(e2eRoomID)!!, "Allo", 1).first()
 
         assertEquals("Unexpected megolm session", megolmSessionId, sentEvent.root.content.toModel<EncryptedEventContent>()?.sessionId)
-        testHelper.waitWithLatch { latch ->
-            testHelper.retryPeriodicallyWithLatch(latch) {
-                bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEvent.eventId)?.root?.getClearType() == EventType.MESSAGE
-            }
+        testHelper.retryPeriodically {
+            bobSession.getRoom(e2eRoomID)?.getTimelineEvent(sentEvent.eventId)?.root?.getClearType() == EventType.MESSAGE
         }
     }
 }
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt
index 130c8d13f9909a242ee33e12118f8b0ee437125f..889cc9a562ae6bf2003ba67a478b43855817a734 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt
@@ -17,7 +17,8 @@
 package org.matrix.android.sdk.internal.crypto
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import org.amshove.kluent.shouldBe
+import kotlinx.coroutines.suspendCancellableCoroutine
+import org.amshove.kluent.shouldBeEqualTo
 import org.junit.Assert
 import org.junit.Before
 import org.junit.FixMethodOrder
@@ -46,7 +47,6 @@ import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm
 import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm
 import org.matrix.olm.OlmSession
 import timber.log.Timber
-import java.util.concurrent.CountDownLatch
 import kotlin.coroutines.Continuation
 import kotlin.coroutines.resume
 
@@ -102,69 +102,37 @@ class UnwedgingTest : InstrumentedTest {
         val bobTimeline = roomFromBobPOV.timelineService().createTimeline(null, TimelineSettings(20))
         bobTimeline.start()
 
-        val bobFinalLatch = CountDownLatch(1)
-        val bobHasThreeDecryptedEventsListener = object : Timeline.Listener {
-            override fun onTimelineFailure(throwable: Throwable) {
-                // noop
-            }
-
-            override fun onNewTimelineEvents(eventIds: List<String>) {
-                // noop
-            }
-
-            override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
-                val decryptedEventReceivedByBob = snapshot.filter { it.root.type == EventType.ENCRYPTED }
-                Timber.d("Bob can now decrypt ${decryptedEventReceivedByBob.size} messages")
-                if (decryptedEventReceivedByBob.size == 3) {
-                    if (decryptedEventReceivedByBob[0].root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID) {
-                        bobFinalLatch.countDown()
-                    }
-                }
-            }
-        }
-        bobTimeline.addListener(bobHasThreeDecryptedEventsListener)
-
-        var latch = CountDownLatch(1)
-        var bobEventsListener = createEventListener(latch, 1)
-        bobTimeline.addListener(bobEventsListener)
         messagesReceivedByBob = emptyList()
 
         // - Alice sends a 1st message with a 1st megolm session
         roomFromAlicePOV.sendService().sendTextMessage("First message")
 
         // Wait for the message to be received by Bob
-        testHelper.await(latch)
-        bobTimeline.removeListener(bobEventsListener)
+        messagesReceivedByBob = bobTimeline.waitForMessages(expectedCount = 1)
 
-        messagesReceivedByBob.size shouldBe 1
+        messagesReceivedByBob.size shouldBeEqualTo 1
         val firstMessageSession = messagesReceivedByBob[0].root.content.toModel<EncryptedEventContent>()!!.sessionId!!
 
         //  - Store the olm session between A&B devices
         // Let us pickle our session with bob here so we can later unpickle it
         // and wedge our session.
         val sessionIdsForBob = aliceCryptoStore.getDeviceSessionIds(bobSession.cryptoService().getMyDevice().identityKey()!!)
-        sessionIdsForBob!!.size shouldBe 1
+        sessionIdsForBob!!.size shouldBeEqualTo 1
         val olmSession = aliceCryptoStore.getDeviceSession(sessionIdsForBob.first(), bobSession.cryptoService().getMyDevice().identityKey()!!)!!
 
         val oldSession = serializeForRealm(olmSession.olmSession)
 
         aliceSession.cryptoService().discardOutboundSession(roomFromAlicePOV.roomId)
-        Thread.sleep(6_000)
 
-        latch = CountDownLatch(1)
-        bobEventsListener = createEventListener(latch, 2)
-        bobTimeline.addListener(bobEventsListener)
         messagesReceivedByBob = emptyList()
-
         Timber.i("## CRYPTO | testUnwedging:  Alice sends a 2nd message with a 2nd megolm session")
         // - Alice sends a 2nd message with a 2nd megolm session
         roomFromAlicePOV.sendService().sendTextMessage("Second message")
 
         // Wait for the message to be received by Bob
-        testHelper.await(latch)
-        bobTimeline.removeListener(bobEventsListener)
+        messagesReceivedByBob = bobTimeline.waitForMessages(expectedCount = 2)
 
-        messagesReceivedByBob.size shouldBe 2
+        messagesReceivedByBob.size shouldBeEqualTo 2
         // Session should have changed
         val secondMessageSession = messagesReceivedByBob[0].root.content.toModel<EncryptedEventContent>()!!.sessionId!!
         Assert.assertNotEquals(firstMessageSession, secondMessageSession)
@@ -177,25 +145,18 @@ class UnwedgingTest : InstrumentedTest {
                 bobSession.cryptoService().getMyDevice().identityKey()!!
         )
         olmDevice.clearOlmSessionCache()
-        Thread.sleep(6_000)
 
         // Force new session, and key share
         aliceSession.cryptoService().discardOutboundSession(roomFromAlicePOV.roomId)
 
+        Timber.i("## CRYPTO | testUnwedging: Alice sends a 3rd message with a 3rd megolm session but a wedged olm session")
+        // - Alice sends a 3rd message with a 3rd megolm session but a wedged olm session
+        roomFromAlicePOV.sendService().sendTextMessage("Third message")
+        // Bob should not be able to decrypt, because the session key could not be sent
         // Wait for the message to be received by Bob
-        testHelper.waitWithLatch {
-            bobEventsListener = createEventListener(it, 3)
-            bobTimeline.addListener(bobEventsListener)
-            messagesReceivedByBob = emptyList()
-
-            Timber.i("## CRYPTO | testUnwedging: Alice sends a 3rd message with a 3rd megolm session but a wedged olm session")
-            // - Alice sends a 3rd message with a 3rd megolm session but a wedged olm session
-            roomFromAlicePOV.sendService().sendTextMessage("Third message")
-            // Bob should not be able to decrypt, because the session key could not be sent
-        }
-        bobTimeline.removeListener(bobEventsListener)
+        messagesReceivedByBob = bobTimeline.waitForMessages(expectedCount = 3)
 
-        messagesReceivedByBob.size shouldBe 3
+        messagesReceivedByBob.size shouldBeEqualTo 3
 
         val thirdMessageSession = messagesReceivedByBob[0].root.content.toModel<EncryptedEventContent>()!!.sessionId!!
         Timber.i("## CRYPTO | testUnwedging: third message session ID $thirdMessageSession")
@@ -205,11 +166,11 @@ class UnwedgingTest : InstrumentedTest {
         Assert.assertEquals(EventType.MESSAGE, messagesReceivedByBob[1].root.getClearType())
         Assert.assertEquals(EventType.MESSAGE, messagesReceivedByBob[2].root.getClearType())
         // Bob Should not be able to decrypt last message, because session could not be sent as the olm channel was wedged
-        testHelper.await(bobFinalLatch)
-        bobTimeline.removeListener(bobHasThreeDecryptedEventsListener)
+
+        Assert.assertTrue(messagesReceivedByBob[0].root.mCryptoError == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID)
 
         // It's a trick to force key request on fail to decrypt
-        testHelper.doSync<Unit> {
+        testHelper.waitForCallback<Unit> {
             bobSession.cryptoService().crossSigningService()
                     .initializeCrossSigning(
                             object : UserInteractiveAuthInterceptor {
@@ -227,24 +188,22 @@ class UnwedgingTest : InstrumentedTest {
         }
 
         // Wait until we received back the key
-        testHelper.waitWithLatch {
-            testHelper.retryPeriodicallyWithLatch(it) {
-                // we should get back the key and be able to decrypt
-                val result = testHelper.runBlockingTest {
-                    tryOrNull {
-                        bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "")
-                    }
-                }
-                Timber.i("## CRYPTO | testUnwedging: decrypt result  ${result?.clearEvent}")
-                result != null
+        testHelper.retryPeriodically {
+            // we should get back the key and be able to decrypt
+            val result = tryOrNull {
+                bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "")
             }
+            Timber.i("## CRYPTO | testUnwedging: decrypt result  ${result?.clearEvent}")
+            result != null
         }
 
         bobTimeline.dispose()
     }
+}
 
-    private fun createEventListener(latch: CountDownLatch, expectedNumberOfMessages: Int): Timeline.Listener {
-        return object : Timeline.Listener {
+private suspend fun Timeline.waitForMessages(expectedCount: Int): List<TimelineEvent> {
+    return suspendCancellableCoroutine { continuation ->
+        val listener = object : Timeline.Listener {
             override fun onTimelineFailure(throwable: Throwable) {
                 // noop
             }
@@ -254,12 +213,16 @@ class UnwedgingTest : InstrumentedTest {
             }
 
             override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
-                messagesReceivedByBob = snapshot.filter { it.root.type == EventType.ENCRYPTED }
+                val messagesReceived = snapshot.filter { it.root.type == EventType.ENCRYPTED }
 
-                if (messagesReceivedByBob.size == expectedNumberOfMessages) {
-                    latch.countDown()
+                if (messagesReceived.size == expectedCount) {
+                    removeListener(this)
+                    continuation.resume(messagesReceived)
                 }
             }
         }
+
+        addListener(listener)
+        continuation.invokeOnCancellation { removeListener(listener) }
     }
 }
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt
index ef3fdfeeda301d6f214a1d3e8ebdead50363e457..c4fb89693421e4c36edd99e17860dd33217bdd25 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt
@@ -25,7 +25,6 @@ import org.junit.Assert.assertNull
 import org.junit.Assert.assertTrue
 import org.junit.Assert.fail
 import org.junit.FixMethodOrder
-import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
@@ -42,20 +41,20 @@ import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
 import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
 import org.matrix.android.sdk.common.SessionTestParams
 import org.matrix.android.sdk.common.TestConstants
+import timber.log.Timber
 import kotlin.coroutines.Continuation
 import kotlin.coroutines.resume
 
 @RunWith(AndroidJUnit4::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 @LargeTest
-@Ignore
 class XSigningTest : InstrumentedTest {
 
     @Test
     fun test_InitializeAndStoreKeys() = runSessionTest(context()) { testHelper ->
         val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
 
-        testHelper.doSync<Unit> {
+        testHelper.waitForCallback<Unit> {
             aliceSession.cryptoService().crossSigningService()
                     .initializeCrossSigning(object : UserInteractiveAuthInterceptor {
                         override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
@@ -101,14 +100,14 @@ class XSigningTest : InstrumentedTest {
                 password = TestConstants.PASSWORD
         )
 
-        testHelper.doSync<Unit> {
+        testHelper.waitForCallback<Unit> {
             aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
                 override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
                     promise.resume(aliceAuthParams)
                 }
             }, it)
         }
-        testHelper.doSync<Unit> {
+        testHelper.waitForCallback<Unit> {
             bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
                 override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
                     promise.resume(bobAuthParams)
@@ -117,7 +116,7 @@ class XSigningTest : InstrumentedTest {
         }
 
         // Check that alice can see bob keys
-        testHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> { aliceSession.cryptoService().downloadKeys(listOf(bobSession.myUserId), true, it) }
+        testHelper.waitForCallback<MXUsersDevicesMap<CryptoDeviceInfo>> { aliceSession.cryptoService().downloadKeys(listOf(bobSession.myUserId), true, it) }
 
         val bobKeysFromAlicePOV = aliceSession.cryptoService().crossSigningService().getUserCrossSigningKeys(bobSession.myUserId)
         assertNotNull("Alice can see bob Master key", bobKeysFromAlicePOV!!.masterKey())
@@ -154,14 +153,14 @@ class XSigningTest : InstrumentedTest {
                 password = TestConstants.PASSWORD
         )
 
-        testHelper.doSync<Unit> {
+        testHelper.waitForCallback<Unit> {
             aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
                 override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
                     promise.resume(aliceAuthParams)
                 }
             }, it)
         }
-        testHelper.doSync<Unit> {
+        testHelper.waitForCallback<Unit> {
             bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
                 override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
                     promise.resume(bobAuthParams)
@@ -171,12 +170,12 @@ class XSigningTest : InstrumentedTest {
 
         // Check that alice can see bob keys
         val bobUserId = bobSession.myUserId
-        testHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> { aliceSession.cryptoService().downloadKeys(listOf(bobUserId), true, it) }
+        testHelper.waitForCallback<MXUsersDevicesMap<CryptoDeviceInfo>> { aliceSession.cryptoService().downloadKeys(listOf(bobUserId), true, it) }
 
         val bobKeysFromAlicePOV = aliceSession.cryptoService().crossSigningService().getUserCrossSigningKeys(bobUserId)
         assertTrue("Bob keys from alice pov should not be trusted", bobKeysFromAlicePOV?.isTrusted() == false)
 
-        testHelper.doSync<Unit> { aliceSession.cryptoService().crossSigningService().trustUser(bobUserId, it) }
+        testHelper.waitForCallback<Unit> { aliceSession.cryptoService().crossSigningService().trustUser(bobUserId, it) }
 
         // Now bobs logs in on a new device and verifies it
         // We will want to test that in alice POV, this new device would be trusted by cross signing
@@ -185,7 +184,7 @@ class XSigningTest : InstrumentedTest {
         val bobSecondDeviceId = bobSession2.sessionParams.deviceId!!
 
         // Check that bob first session sees the new login
-        val data = testHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> {
+        val data = testHelper.waitForCallback<MXUsersDevicesMap<CryptoDeviceInfo>> {
             bobSession.cryptoService().downloadKeys(listOf(bobUserId), true, it)
         }
 
@@ -197,12 +196,12 @@ class XSigningTest : InstrumentedTest {
         assertNotNull("Bob Second device should be known and persisted from first", bobSecondDevicePOVFirstDevice)
 
         // Manually mark it as trusted from first session
-        testHelper.doSync<Unit> {
+        testHelper.waitForCallback<Unit> {
             bobSession.cryptoService().crossSigningService().trustDevice(bobSecondDeviceId, it)
         }
 
         // Now alice should cross trust bob's second device
-        val data2 = testHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> {
+        val data2 = testHelper.waitForCallback<MXUsersDevicesMap<CryptoDeviceInfo>> {
             aliceSession.cryptoService().downloadKeys(listOf(bobUserId), true, it)
         }
 
@@ -214,4 +213,104 @@ class XSigningTest : InstrumentedTest {
         val result = aliceSession.cryptoService().crossSigningService().checkDeviceTrust(bobUserId, bobSecondDeviceId, null)
         assertTrue("Bob second device should be trusted from alice POV", result.isCrossSignedVerified())
     }
+
+    @Test
+    fun testWarnOnCrossSigningReset() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
+        val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom()
+
+        val aliceSession = cryptoTestData.firstSession
+        val bobSession = cryptoTestData.secondSession
+
+        val aliceAuthParams = UserPasswordAuth(
+                user = aliceSession.myUserId,
+                password = TestConstants.PASSWORD
+        )
+        val bobAuthParams = UserPasswordAuth(
+                user = bobSession!!.myUserId,
+                password = TestConstants.PASSWORD
+        )
+
+        testHelper.waitForCallback<Unit> {
+            aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
+                override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
+                    promise.resume(aliceAuthParams)
+                }
+            }, it)
+        }
+        testHelper.waitForCallback<Unit> {
+            bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
+                override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
+                    promise.resume(bobAuthParams)
+                }
+            }, it)
+        }
+
+        cryptoTestHelper.verifySASCrossSign(aliceSession, bobSession, cryptoTestData.roomId)
+
+        testHelper.retryPeriodically {
+            aliceSession.cryptoService().crossSigningService().isUserTrusted(bobSession.myUserId)
+        }
+
+        testHelper.retryPeriodically {
+            aliceSession.cryptoService().crossSigningService().checkUserTrust(bobSession.myUserId).isVerified()
+        }
+
+        aliceSession.cryptoService()
+        // Ensure also that bob device is trusted
+        testHelper.retryPeriodically {
+            val deviceInfo = aliceSession.cryptoService().getUserDevices(bobSession.myUserId).firstOrNull()
+            Timber.v("#TEST device:${deviceInfo?.shortDebugString()} trust ${deviceInfo?.trustLevel}")
+            deviceInfo?.trustLevel?.crossSigningVerified == true
+        }
+
+        val currentBobMSK = aliceSession.cryptoService().crossSigningService()
+                .getUserCrossSigningKeys(bobSession.myUserId)!!
+                .masterKey()!!.unpaddedBase64PublicKey!!
+
+        testHelper.waitForCallback<Unit> {
+            bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
+                override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
+                    promise.resume(bobAuthParams)
+                }
+            }, it)
+        }
+
+        testHelper.retryPeriodically {
+            val newBobMsk = aliceSession.cryptoService().crossSigningService()
+                    .getUserCrossSigningKeys(bobSession.myUserId)
+                    ?.masterKey()?.unpaddedBase64PublicKey
+            newBobMsk != null && newBobMsk != currentBobMSK
+        }
+
+        // trick to force event to sync
+        bobSession.roomService().getRoom(cryptoTestData.roomId)!!.typingService().userIsTyping()
+
+        // assert that bob is not trusted anymore from alice s
+        testHelper.retryPeriodically {
+            val trust = aliceSession.cryptoService().crossSigningService().checkUserTrust(bobSession.myUserId)
+            !trust.isVerified()
+        }
+
+        // trick to force event to sync
+        bobSession.roomService().getRoom(cryptoTestData.roomId)!!.typingService().userStopsTyping()
+        bobSession.roomService().getRoom(cryptoTestData.roomId)!!.typingService().userIsTyping()
+
+        testHelper.retryPeriodically {
+            val info = aliceSession.cryptoService().crossSigningService().getUserCrossSigningKeys(bobSession.myUserId)
+            info?.wasTrustedOnce == true
+        }
+
+        // trick to force event to sync
+        bobSession.roomService().getRoom(cryptoTestData.roomId)!!.typingService().userStopsTyping()
+        bobSession.roomService().getRoom(cryptoTestData.roomId)!!.typingService().userIsTyping()
+
+        testHelper.retryPeriodically {
+            !aliceSession.cryptoService().crossSigningService().isUserTrusted(bobSession.myUserId)
+        }
+
+        // Ensure also that bob device are not trusted
+        testHelper.retryPeriodically {
+            aliceSession.cryptoService().getUserDevices(bobSession.myUserId).first().trustLevel?.crossSigningVerified != true
+        }
+    }
 }
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt
index 5f26fda94618019f119284305fa27a2360fbf50a..42a04dbe3f9003a3330416c469a796e0df0f1152 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/encryption/EncryptionTest.kt
@@ -17,7 +17,7 @@
 package org.matrix.android.sdk.internal.crypto.encryption
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.suspendCancellableCoroutine
 import org.amshove.kluent.shouldBe
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -34,54 +34,59 @@ import org.matrix.android.sdk.api.session.room.send.SendState
 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.common.CommonTestHelper
 import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
 import org.matrix.android.sdk.common.CryptoTestHelper
-import java.util.concurrent.CountDownLatch
+import org.matrix.android.sdk.common.waitFor
+import kotlin.coroutines.resume
 
 @RunWith(AndroidJUnit4::class)
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 class EncryptionTest : InstrumentedTest {
 
     @Test
-    fun test_EncryptionEvent() {
-        runCryptoTest(context()) { cryptoTestHelper, testHelper ->
-            performTest(cryptoTestHelper, testHelper, roomShouldBeEncrypted = false) { room ->
-                // Send an encryption Event as an Event (and not as a state event)
-                room.sendService().sendEvent(
-                        eventType = EventType.STATE_ROOM_ENCRYPTION,
-                        content = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent()
-                )
-            }
+    fun test_EncryptionEvent() = runCryptoTest(context()) { cryptoTestHelper, _ ->
+        performTest(cryptoTestHelper, roomShouldBeEncrypted = false) { room ->
+            // Send an encryption Event as an Event (and not as a state event)
+            room.sendService().sendEvent(
+                    eventType = EventType.STATE_ROOM_ENCRYPTION,
+                    content = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent()
+            )
         }
     }
 
     @Test
-    fun test_EncryptionStateEvent() {
-        runCryptoTest(context()) { cryptoTestHelper, testHelper ->
-            performTest(cryptoTestHelper, testHelper, roomShouldBeEncrypted = true) { room ->
-                runBlocking {
-                    // Send an encryption Event as a State Event
-                    room.stateService().sendStateEvent(
-                            eventType = EventType.STATE_ROOM_ENCRYPTION,
-                            stateKey = "",
-                            body = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent()
-                    )
-                }
-            }
+    fun test_EncryptionStateEvent() = runCryptoTest(context()) { cryptoTestHelper, _ ->
+        performTest(cryptoTestHelper, roomShouldBeEncrypted = true) { room ->
+            // Send an encryption Event as a State Event
+            room.stateService().sendStateEvent(
+                    eventType = EventType.STATE_ROOM_ENCRYPTION,
+                    stateKey = "",
+                    body = EncryptionEventContent(algorithm = MXCRYPTO_ALGORITHM_MEGOLM).toContent()
+            )
         }
     }
 
-    private fun performTest(cryptoTestHelper: CryptoTestHelper, testHelper: CommonTestHelper, roomShouldBeEncrypted: Boolean, action: (Room) -> Unit) {
+    private suspend fun performTest(cryptoTestHelper: CryptoTestHelper, roomShouldBeEncrypted: Boolean, action: suspend (Room) -> Unit) {
         val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(encryptedRoom = false)
-
         val aliceSession = cryptoTestData.firstSession
         val room = aliceSession.getRoom(cryptoTestData.roomId)!!
 
         room.roomCryptoService().isEncrypted() shouldBe false
 
         val timeline = room.timelineService().createTimeline(null, TimelineSettings(10))
-        val latch = CountDownLatch(1)
+        timeline.start()
+        waitFor(
+                continueWhen = { timeline.waitForEncryptedMessages() },
+                action = { action.invoke(room) }
+        )
+        timeline.dispose()
+
+        room.roomCryptoService().isEncrypted() shouldBe roomShouldBeEncrypted
+    }
+}
+
+private suspend fun Timeline.waitForEncryptedMessages() {
+    suspendCancellableCoroutine<Unit> { continuation ->
         val timelineListener = object : Timeline.Listener {
             override fun onTimelineFailure(throwable: Throwable) {
             }
@@ -96,20 +101,12 @@ class EncryptionTest : InstrumentedTest {
                         .filter { it.root.getClearType() == EventType.STATE_ROOM_ENCRYPTION }
 
                 if (newMessages.isNotEmpty()) {
-                    timeline.removeListener(this)
-                    latch.countDown()
+                    removeListener(this)
+                    continuation.resume(Unit)
                 }
             }
         }
-        timeline.start()
-        timeline.addListener(timelineListener)
-
-        action.invoke(room)
-        testHelper.await(latch)
-        timeline.dispose()
-        testHelper.waitWithLatch {
-            room.roomCryptoService().isEncrypted() shouldBe roomShouldBeEncrypted
-            it.countDown()
-        }
+        addListener(timelineListener)
+        continuation.invokeOnCancellation { removeListener(timelineListener) }
     }
 }
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt
index df0b10ea6d9143e49b856c5d268fe19413ab6715..8e001b84d38836dc57e3c790959b8133f4c6c93a 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt
@@ -62,14 +62,12 @@ class KeyShareTests : InstrumentedTest {
         Log.v("#TEST", "=======> AliceSession 1 is ${aliceSession.sessionParams.deviceId}")
 
         // Create an encrypted room and add a message
-        val roomId = commonTestHelper.runBlockingTest {
-            aliceSession.roomService().createRoom(
-                    CreateRoomParams().apply {
-                        visibility = RoomDirectoryVisibility.PRIVATE
-                        enableEncryption()
-                    }
-            )
-        }
+        val roomId = aliceSession.roomService().createRoom(
+                CreateRoomParams().apply {
+                    visibility = RoomDirectoryVisibility.PRIVATE
+                    enableEncryption()
+                }
+        )
         val room = aliceSession.getRoom(roomId)
         assertNotNull(room)
         Thread.sleep(4_000)
@@ -93,10 +91,8 @@ class KeyShareTests : InstrumentedTest {
         assertNotNull(receivedEvent)
         assert(receivedEvent!!.isEncrypted())
 
-        commonTestHelper.runBlockingTest {
-            mustFail {
-                aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
-            }
+        mustFail {
+            aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
         }
 
         val outgoingRequestsBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
@@ -110,15 +106,13 @@ class KeyShareTests : InstrumentedTest {
 
         var outGoingRequestId: String? = null
 
-        commonTestHelper.waitWithLatch { latch ->
-            commonTestHelper.retryPeriodicallyWithLatch(latch) {
-                aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
-                        .let {
-                            val outgoing = it.firstOrNull { it.sessionId == eventMegolmSessionId }
-                            outGoingRequestId = outgoing?.requestId
-                            outgoing != null
-                        }
-            }
+        commonTestHelper.retryPeriodically {
+            aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
+                    .let {
+                        val outgoing = it.firstOrNull { it.sessionId == eventMegolmSessionId }
+                        outGoingRequestId = outgoing?.requestId
+                        outgoing != null
+                    }
         }
         Log.v("#TEST", "=======> Outgoing requet Id is $outGoingRequestId")
 
@@ -130,47 +124,41 @@ class KeyShareTests : InstrumentedTest {
 
         // The first session should see an incoming request
         // the request should be refused, because the device is not trusted
-        commonTestHelper.waitWithLatch { latch ->
-            commonTestHelper.retryPeriodicallyWithLatch(latch) {
-                // DEBUG LOGS
-                aliceSession.cryptoService().getIncomingRoomKeyRequests().let {
-                    Log.v("#TEST", "Incoming request Session 1 (looking for $outGoingRequestId)")
-                    Log.v("#TEST", "=========================")
-                    it.forEach { keyRequest ->
-                        Log.v(
-                                "#TEST",
-                                "[ts${keyRequest.localCreationTimestamp}] requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}"
-                        )
-                    }
-                    Log.v("#TEST", "=========================")
+        commonTestHelper.retryPeriodically {
+            // DEBUG LOGS
+            aliceSession.cryptoService().getIncomingRoomKeyRequests().let {
+                Log.v("#TEST", "Incoming request Session 1 (looking for $outGoingRequestId)")
+                Log.v("#TEST", "=========================")
+                it.forEach { keyRequest ->
+                    Log.v(
+                            "#TEST",
+                            "[ts${keyRequest.localCreationTimestamp}] requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}"
+                    )
                 }
-
-                val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId }
-                incoming != null
+                Log.v("#TEST", "=========================")
             }
-        }
 
-        commonTestHelper.waitWithLatch { latch ->
-            commonTestHelper.retryPeriodicallyWithLatch(latch) {
-                // DEBUG LOGS
-                aliceSession2.cryptoService().getOutgoingRoomKeyRequests().forEach { keyRequest ->
-                    Log.v("#TEST", "=========================")
-                    Log.v("#TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}")
-                    Log.v("#TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}")
-                    Log.v("#TEST", "=========================")
-                }
+            val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId }
+            incoming != null
+        }
 
-                val outgoing = aliceSession2.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId }
-                val reply = outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
-                val resultCode = (reply?.result as? RequestResult.Failure)?.code
-                resultCode == WithHeldCode.UNVERIFIED
+        commonTestHelper.retryPeriodically {
+            // DEBUG LOGS
+            aliceSession2.cryptoService().getOutgoingRoomKeyRequests().forEach { keyRequest ->
+                Log.v("#TEST", "=========================")
+                Log.v("#TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}")
+                Log.v("#TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}")
+                Log.v("#TEST", "=========================")
             }
+
+            val outgoing = aliceSession2.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId }
+            val reply = outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
+            val resultCode = (reply?.result as? RequestResult.Failure)?.code
+            resultCode == WithHeldCode.UNVERIFIED
         }
 
-        commonTestHelper.runBlockingTest {
-            mustFail {
-                aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
-            }
+        mustFail {
+            aliceSession2.cryptoService().decryptEvent(receivedEvent.root, "foo")
         }
 
         // Mark the device as trusted
@@ -228,12 +216,10 @@ class KeyShareTests : InstrumentedTest {
         // As it was share previously alice should accept to reshare
         bobSession.cryptoService().reRequestRoomKeyForEvent(sentEvent.root)
 
-        commonTestHelper.waitWithLatch { latch ->
-            commonTestHelper.retryPeriodicallyWithLatch(latch) {
-                val outgoing = bobSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
-                val aliceReply = outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
-                aliceReply != null && aliceReply.result is RequestResult.Success
-            }
+        commonTestHelper.retryPeriodically {
+            val outgoing = bobSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
+            val aliceReply = outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
+            aliceReply != null && aliceReply.result is RequestResult.Success
         }
     }
 
@@ -254,12 +240,10 @@ class KeyShareTests : InstrumentedTest {
         val aliceNewSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true))
 
         // we wait for alice first session to be aware of that session?
-        commonTestHelper.waitWithLatch { latch ->
-            commonTestHelper.retryPeriodicallyWithLatch(latch) {
-                val newSession = aliceSession.cryptoService().getUserDevices(aliceSession.myUserId)
-                        .firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId }
-                newSession != null
-            }
+        commonTestHelper.retryPeriodically {
+            val newSession = aliceSession.cryptoService().getUserDevices(aliceSession.myUserId)
+                    .firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId }
+            newSession != null
         }
         val sentEvent = commonTestHelper.sendTextMessage(roomFromAlice, "Hello", 1).first()
         val sentEventMegolmSession = sentEvent.root.content.toModel<EncryptedEventContent>()!!.sessionId!!
@@ -267,13 +251,11 @@ class KeyShareTests : InstrumentedTest {
         // As it was share previously alice should accept to reshare
         aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvent.root)
 
-        commonTestHelper.waitWithLatch { latch ->
-            commonTestHelper.retryPeriodicallyWithLatch(latch) {
-                val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
-                val ownDeviceReply =
-                        outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
-                ownDeviceReply != null && ownDeviceReply.result is RequestResult.Success
-            }
+        commonTestHelper.retryPeriodically {
+            val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
+            val ownDeviceReply =
+                    outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
+            ownDeviceReply != null && ownDeviceReply.result is RequestResult.Success
         }
     }
 
@@ -300,12 +282,10 @@ class KeyShareTests : InstrumentedTest {
         commonTestHelper.syncSession(aliceNewSession)
 
         // we wait bob first session to be aware of that session?
-        commonTestHelper.waitWithLatch { latch ->
-            commonTestHelper.retryPeriodicallyWithLatch(latch) {
-                val newSession = bobSession.cryptoService().getUserDevices(aliceSession.myUserId)
-                        .firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId }
-                newSession != null
-            }
+        commonTestHelper.retryPeriodically {
+            val newSession = bobSession.cryptoService().getUserDevices(aliceSession.myUserId)
+                    .firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId }
+            newSession != null
         }
 
         val newEvent = commonTestHelper.sendTextMessage(roomFromBob, "The New", 1).first()
@@ -327,26 +307,22 @@ class KeyShareTests : InstrumentedTest {
         aliceNewSession.cryptoService().enableKeyGossiping(true)
         aliceNewSession.cryptoService().reRequestRoomKeyForEvent(newEvent.root)
 
-        commonTestHelper.waitWithLatch { latch ->
-            commonTestHelper.retryPeriodicallyWithLatch(latch) {
-                val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
-                val ownDeviceReply = outgoing?.results
-                        ?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
-                val result = ownDeviceReply?.result
-                Log.v("TEST", "own device result is $result")
-                result != null && result is RequestResult.Failure && result.code == WithHeldCode.UNVERIFIED
-            }
+        commonTestHelper.retryPeriodically {
+            val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
+            val ownDeviceReply = outgoing?.results
+                    ?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
+            val result = ownDeviceReply?.result
+            Log.v("TEST", "own device result is $result")
+            result != null && result is RequestResult.Failure && result.code == WithHeldCode.UNVERIFIED
         }
 
-        commonTestHelper.waitWithLatch { latch ->
-            commonTestHelper.retryPeriodicallyWithLatch(latch) {
-                val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
-                val bobDeviceReply = outgoing?.results
-                        ?.firstOrNull { it.userId == bobSession.myUserId && it.fromDevice == bobSession.sessionParams.deviceId }
-                val result = bobDeviceReply?.result
-                Log.v("TEST", "bob device result is $result")
-                result != null && result is RequestResult.Success && result.chainIndex > 0
-            }
+        commonTestHelper.retryPeriodically {
+            val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
+            val bobDeviceReply = outgoing?.results
+                    ?.firstOrNull { it.userId == bobSession.myUserId && it.fromDevice == bobSession.sessionParams.deviceId }
+            val result = bobDeviceReply?.result
+            Log.v("TEST", "bob device result is $result")
+            result != null && result is RequestResult.Success && result.chainIndex > 0
         }
 
         // it's a success but still can't decrypt first message
@@ -363,21 +339,19 @@ class KeyShareTests : InstrumentedTest {
         // Let's now try to request
         aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvents.first().root)
 
-        commonTestHelper.waitWithLatch { latch ->
-            commonTestHelper.retryPeriodicallyWithLatch(latch) {
-                // DEBUG LOGS
-                aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().forEach { keyRequest ->
-                    Log.v("TEST", "=========================")
-                    Log.v("TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}")
-                    Log.v("TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}")
-                    Log.v("TEST", "=========================")
-                }
-                val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
-                val ownDeviceReply =
-                        outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
-                val result = ownDeviceReply?.result
-                result != null && result is RequestResult.Success && result.chainIndex == 0
+        commonTestHelper.retryPeriodically {
+            // DEBUG LOGS
+            aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().forEach { keyRequest ->
+                Log.v("TEST", "=========================")
+                Log.v("TEST", "requestId ${keyRequest.requestId}, for sessionId ${keyRequest.requestBody?.sessionId}")
+                Log.v("TEST", "replies -> ${keyRequest.results.joinToString { it.toString() }}")
+                Log.v("TEST", "=========================")
             }
+            val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
+            val ownDeviceReply =
+                    outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
+            val result = ownDeviceReply?.result
+            result != null && result is RequestResult.Success && result.chainIndex == 0
         }
 
         // now the new session should be able to decrypt all!
@@ -389,13 +363,11 @@ class KeyShareTests : InstrumentedTest {
         )
 
         // Additional test, can we check that bob replied successfully but with a ratcheted key
-        commonTestHelper.waitWithLatch { latch ->
-            commonTestHelper.retryPeriodicallyWithLatch(latch) {
-                val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
-                val bobReply = outgoing?.results?.firstOrNull { it.userId == bobSession.myUserId }
-                val result = bobReply?.result
-                result != null && result is RequestResult.Success && result.chainIndex == 3
-            }
+        commonTestHelper.retryPeriodically {
+            val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
+            val bobReply = outgoing?.results?.firstOrNull { it.userId == bobSession.myUserId }
+            val result = bobReply?.result
+            result != null && result is RequestResult.Success && result.chainIndex == 3
         }
 
         commonTestHelper.signOutAndClose(aliceNewSession)
@@ -423,12 +395,10 @@ class KeyShareTests : InstrumentedTest {
         val aliceNewSession = commonTestHelper.logIntoAccount(aliceSession.myUserId, SessionTestParams(true))
 
         // we wait bob first session to be aware of that session?
-        commonTestHelper.waitWithLatch { latch ->
-            commonTestHelper.retryPeriodicallyWithLatch(latch) {
-                val newSession = bobSession.cryptoService().getUserDevices(aliceSession.myUserId)
-                        .firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId }
-                newSession != null
-            }
+        commonTestHelper.retryPeriodically {
+            val newSession = bobSession.cryptoService().getUserDevices(aliceSession.myUserId)
+                    .firstOrNull { it.deviceId == aliceNewSession.sessionParams.deviceId }
+            newSession != null
         }
 
         val newEvent = commonTestHelper.sendTextMessage(roomFromBob, "The New", 1).first()
@@ -462,14 +432,12 @@ class KeyShareTests : InstrumentedTest {
         aliceNewSession.cryptoService().reRequestRoomKeyForEvent(sentEvents.first().root)
 
         // Should get a reply from bob and not from alice
-        commonTestHelper.waitWithLatch { latch ->
-            commonTestHelper.retryPeriodicallyWithLatch(latch) {
-                //  Log.d("#TEST", "outgoing key requests :${aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().joinToString { it.sessionId ?: "?" }}")
-                val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
-                val bobReply = outgoing?.results?.firstOrNull { it.userId == bobSession.myUserId }
-                val result = bobReply?.result
-                result != null && result is RequestResult.Success && result.chainIndex == 3
-            }
+        commonTestHelper.retryPeriodically {
+            //  Log.d("#TEST", "outgoing key requests :${aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().joinToString { it.sessionId ?: "?" }}")
+            val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
+            val bobReply = outgoing?.results?.firstOrNull { it.userId == bobSession.myUserId }
+            val result = bobReply?.result
+            result != null && result is RequestResult.Success && result.chainIndex == 3
         }
 
         val outgoingReq = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
@@ -482,14 +450,12 @@ class KeyShareTests : InstrumentedTest {
         aliceSession.syncService().startSync(true)
 
         // We should now get a reply from first session
-        commonTestHelper.waitWithLatch { latch ->
-            commonTestHelper.retryPeriodicallyWithLatch(latch) {
-                val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
-                val ownDeviceReply =
-                        outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
-                val result = ownDeviceReply?.result
-                result != null && result is RequestResult.Success && result.chainIndex == 0
-            }
+        commonTestHelper.retryPeriodically {
+            val outgoing = aliceNewSession.cryptoService().getOutgoingRoomKeyRequests().firstOrNull { it.sessionId == sentEventMegolmSession }
+            val ownDeviceReply =
+                    outgoing?.results?.firstOrNull { it.userId == aliceSession.myUserId && it.fromDevice == aliceSession.sessionParams.deviceId }
+            val result = ownDeviceReply?.result
+            result != null && result is RequestResult.Success && result.chainIndex == 0
         }
 
         // It should be in sent then cancel
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt
index 910a349b403ac108b7f7809dfde68600a6bb690c..b55ddbc97065684813bbc158ca870071e9f4c25f 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/WithHeldTests.kt
@@ -81,10 +81,8 @@ class WithHeldTests : InstrumentedTest {
         val timelineEvent = testHelper.sendTextMessage(roomAlicePOV, "Hello Bob", 1).first()
 
         // await for bob unverified session to get the message
-        testHelper.waitWithLatch { latch ->
-            testHelper.retryPeriodicallyWithLatch(latch) {
-                bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(timelineEvent.eventId) != null
-            }
+        testHelper.retryPeriodically {
+            bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(timelineEvent.eventId) != null
         }
 
         val eventBobPOV = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(timelineEvent.eventId)!!
@@ -96,60 +94,52 @@ class WithHeldTests : InstrumentedTest {
 
         // Bob should not be able to decrypt because the keys is withheld
         // .. might need to wait a bit for stability?
-        testHelper.runBlockingTest {
-            mustFail(
-                    message = "This session should not be able to decrypt",
-                    failureBlock = { failure ->
-                        val type = (failure as MXCryptoError.Base).errorType
-                        val technicalMessage = failure.technicalMessage
-                        Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
-                        Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage)
-                    }
-            ) {
-                bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
-            }
+        mustFail(
+                message = "This session should not be able to decrypt",
+                failureBlock = { failure ->
+                    val type = (failure as MXCryptoError.Base).errorType
+                    val technicalMessage = failure.technicalMessage
+                    Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
+                    Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage)
+                }
+        ) {
+            bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
         }
 
         // Let's see if the reply we got from bob first session is unverified
-        testHelper.waitWithLatch { latch ->
-            testHelper.retryPeriodicallyWithLatch(latch) {
-                bobUnverifiedSession.cryptoService().getOutgoingRoomKeyRequests()
-                        .firstOrNull { it.sessionId == megolmSessionId }
-                        ?.results
-                        ?.firstOrNull { it.fromDevice == bobSession.sessionParams.deviceId }
-                        ?.result
-                        ?.let {
-                            it as? RequestResult.Failure
-                        }
-                        ?.code == WithHeldCode.UNVERIFIED
-            }
+        testHelper.retryPeriodically {
+            bobUnverifiedSession.cryptoService().getOutgoingRoomKeyRequests()
+                    .firstOrNull { it.sessionId == megolmSessionId }
+                    ?.results
+                    ?.firstOrNull { it.fromDevice == bobSession.sessionParams.deviceId }
+                    ?.result
+                    ?.let {
+                        it as? RequestResult.Failure
+                    }
+                    ?.code == WithHeldCode.UNVERIFIED
         }
         // enable back sending to unverified
         aliceSession.cryptoService().setGlobalBlacklistUnverifiedDevices(false)
 
         val secondEvent = testHelper.sendTextMessage(roomAlicePOV, "Verify your device!!", 1).first()
 
-        testHelper.waitWithLatch { latch ->
-            testHelper.retryPeriodicallyWithLatch(latch) {
-                val ev = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(secondEvent.eventId)
-                // wait until it's decrypted
-                ev?.root?.getClearType() == EventType.MESSAGE
-            }
+        testHelper.retryPeriodically {
+            val ev = bobUnverifiedSession.getRoom(roomId)?.getTimelineEvent(secondEvent.eventId)
+            // wait until it's decrypted
+            ev?.root?.getClearType() == EventType.MESSAGE
         }
 
         // Previous message should still be undecryptable (partially withheld session)
         // .. might need to wait a bit for stability?
-        testHelper.runBlockingTest {
-            mustFail(
-                    message = "This session should not be able to decrypt",
-                    failureBlock = { failure ->
-                        val type = (failure as MXCryptoError.Base).errorType
-                        val technicalMessage = failure.technicalMessage
-                        Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
-                        Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage)
-                    }) {
-                bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
-            }
+        mustFail(
+                message = "This session should not be able to decrypt",
+                failureBlock = { failure ->
+                    val type = (failure as MXCryptoError.Base).errorType
+                    val technicalMessage = failure.technicalMessage
+                    Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
+                    Assert.assertEquals("Cause should be unverified", WithHeldCode.UNVERIFIED.value, technicalMessage)
+                }) {
+            bobUnverifiedSession.cryptoService().decryptEvent(eventBobPOV.root, "")
         }
     }
 
@@ -181,26 +171,22 @@ class WithHeldTests : InstrumentedTest {
         val eventId = testHelper.sendTextMessage(roomAlicePov, "first message", 1).first().eventId
 
         // await for bob session to get the message
-        testHelper.waitWithLatch { latch ->
-            testHelper.retryPeriodicallyWithLatch(latch) {
-                bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId) != null
-            }
+        testHelper.retryPeriodically {
+            bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId) != null
         }
 
         // Previous message should still be undecryptable (partially withheld session)
         val eventBobPOV = bobSession.getRoom(testData.roomId)?.getTimelineEvent(eventId)
         // .. might need to wait a bit for stability?
-        testHelper.runBlockingTest {
-            mustFail(
-                    message = "This session should not be able to decrypt",
-                    failureBlock = { failure ->
-                        val type = (failure as MXCryptoError.Base).errorType
-                        val technicalMessage = failure.technicalMessage
-                        Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
-                        Assert.assertEquals("Cause should be unverified", WithHeldCode.NO_OLM.value, technicalMessage)
-                    }) {
-                bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "")
-            }
+        mustFail(
+                message = "This session should not be able to decrypt",
+                failureBlock = { failure ->
+                    val type = (failure as MXCryptoError.Base).errorType
+                    val technicalMessage = failure.technicalMessage
+                    Assert.assertEquals("Error should be withheld", MXCryptoError.ErrorType.KEYS_WITHHELD, type)
+                    Assert.assertEquals("Cause should be unverified", WithHeldCode.NO_OLM.value, technicalMessage)
+                }) {
+            bobSession.cryptoService().decryptEvent(eventBobPOV!!.root, "")
         }
 
         // Ensure that alice has marked the session to be shared with bob
@@ -220,10 +206,8 @@ class WithHeldTests : InstrumentedTest {
 
         // Check that the
         // await for bob SecondSession session to get the message
-        testHelper.waitWithLatch { latch ->
-            testHelper.retryPeriodicallyWithLatch(latch) {
-                bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(secondMessageId) != null
-            }
+        testHelper.retryPeriodically {
+            bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(secondMessageId) != null
         }
 
         val chainIndex2 = aliceSession.cryptoService().getSharedWithInfo(testData.roomId, sessionId).getObject(
@@ -265,27 +249,21 @@ class WithHeldTests : InstrumentedTest {
         var sessionId: String? = null
         // Check that the
         // await for bob SecondSession session to get the message
-        testHelper.waitWithLatch { latch ->
-            testHelper.retryPeriodicallyWithLatch(latch) {
-                val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(eventId)?.also {
-                    // try to decrypt and force key request
-                    tryOrNull {
-                        testHelper.runBlockingTest {
-                            bobSecondSession.cryptoService().decryptEvent(it.root, "")
-                        }
-                    }
+        testHelper.retryPeriodically {
+            val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimelineEvent(eventId)?.also {
+                // try to decrypt and force key request
+                tryOrNull {
+                    bobSecondSession.cryptoService().decryptEvent(it.root, "")
                 }
-                sessionId = timeLineEvent?.root?.content?.toModel<EncryptedEventContent>()?.sessionId
-                timeLineEvent != null
             }
+            sessionId = timeLineEvent?.root?.content?.toModel<EncryptedEventContent>()?.sessionId
+            timeLineEvent != null
         }
 
         // Check that bob second session requested the key
-        testHelper.waitWithLatch { latch ->
-            testHelper.retryPeriodicallyWithLatch(latch) {
-                val wc = bobSecondSession.cryptoService().getWithHeldMegolmSession(roomAlicePov.roomId, sessionId!!)
-                wc?.code == WithHeldCode.UNAUTHORISED
-            }
+        testHelper.retryPeriodically {
+            val wc = bobSecondSession.cryptoService().getWithHeldMegolmSession(roomAlicePov.roomId, sessionId!!)
+            wc?.code == WithHeldCode.UNAUTHORISED
         }
     }
 }
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt
index cf201611a0c0aa02d591a99d72fab3c13d273a92..8679cf3c998f7ba4a98f1d1eedf8aca55f9f09dd 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupScenarioData.kt
@@ -30,7 +30,7 @@ internal data class KeysBackupScenarioData(
         val prepareKeysBackupDataResult: PrepareKeysBackupDataResult,
         val aliceSession2: Session
 ) {
-    fun cleanUp(testHelper: CommonTestHelper) {
+    suspend fun cleanUp(testHelper: CommonTestHelper) {
         cryptoTestData.cleanUp(testHelper)
         testHelper.signOutAndClose(aliceSession2)
     }
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt
index 2439119f01c8eb1ddcd0abd8cc91d103cf2903fe..01c03b8001615cc1d0fbac7d7a7bf88b773b2d2e 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTest.kt
@@ -18,10 +18,10 @@ package org.matrix.android.sdk.internal.crypto.keysbackup
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
+import kotlinx.coroutines.suspendCancellableCoroutine
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertNotNull
-import org.junit.Assert.assertNull
 import org.junit.Assert.assertTrue
 import org.junit.FixMethodOrder
 import org.junit.Rule
@@ -48,9 +48,11 @@ import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
 import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
 import org.matrix.android.sdk.common.RetryTestRule
 import org.matrix.android.sdk.common.TestConstants
-import org.matrix.android.sdk.common.TestMatrixCallback
+import org.matrix.android.sdk.common.waitFor
+import java.security.InvalidParameterException
 import java.util.Collections
 import java.util.concurrent.CountDownLatch
+import kotlin.coroutines.resume
 
 @RunWith(AndroidJUnit4::class)
 @FixMethodOrder(MethodSorters.JVM)
@@ -116,7 +118,7 @@ class KeysBackupTest : InstrumentedTest {
 
         assertFalse(keysBackup.isEnabled())
 
-        val megolmBackupCreationInfo = testHelper.doSync<MegolmBackupCreationInfo> {
+        val megolmBackupCreationInfo = testHelper.waitForCallback<MegolmBackupCreationInfo> {
             keysBackup.prepareKeysBackupVersion(null, null, it)
         }
 
@@ -133,7 +135,6 @@ class KeysBackupTest : InstrumentedTest {
      */
     @Test
     fun createKeysBackupVersionTest() = runCryptoTest(context()) { cryptoTestHelper, testHelper ->
-
         val bobSession = testHelper.createAccount(TestConstants.USER_BOB, KeysBackupTestConstants.defaultSessionParams)
         cryptoTestHelper.initializeCrossSigning(bobSession)
 
@@ -143,14 +144,14 @@ class KeysBackupTest : InstrumentedTest {
 
         assertFalse(keysBackup.isEnabled())
 
-        val megolmBackupCreationInfo = testHelper.doSync<MegolmBackupCreationInfo> {
+        val megolmBackupCreationInfo = testHelper.waitForCallback<MegolmBackupCreationInfo> {
             keysBackup.prepareKeysBackupVersion(null, null, it)
         }
 
         assertFalse(keysBackup.isEnabled())
 
         // Create the version
-        val version = testHelper.doSync<KeysVersion> {
+        val version = testHelper.waitForCallback<KeysVersion> {
             keysBackup.createKeysBackupVersion(megolmBackupCreationInfo, it)
         }
 
@@ -158,10 +159,10 @@ class KeysBackupTest : InstrumentedTest {
         assertTrue(keysBackup.isEnabled())
 
         // Check that it's signed with MSK
-        val versionResult = testHelper.doSync<KeysVersionResult?> {
+        val versionResult = testHelper.waitForCallback<KeysVersionResult?> {
             keysBackup.getVersion(version.version, it)
         }
-        val trust = testHelper.doSync<KeysBackupVersionTrust> {
+        val trust = testHelper.waitForCallback<KeysBackupVersionTrust> {
             keysBackup.getKeysBackupTrust(versionResult!!, it)
         }
 
@@ -257,7 +258,7 @@ class KeysBackupTest : InstrumentedTest {
 
         var lastBackedUpKeysProgress = 0
 
-        testHelper.doSync<Unit> {
+        testHelper.waitForCallback<Unit> {
             keysBackup.backupAllGroupSessions(object : ProgressListener {
                 override fun onProgress(progress: Int, total: Int) {
                     assertEquals(nbOfKeys, total)
@@ -299,7 +300,7 @@ class KeysBackupTest : InstrumentedTest {
         val keyBackupCreationInfo = keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup).megolmBackupCreationInfo
 
         // - Check encryptGroupSession() returns stg
-        val keyBackupData = testHelper.runBlockingTest { keysBackup.encryptGroupSession(session) }
+        val keyBackupData = keysBackup.encryptGroupSession(session)
         assertNotNull(keyBackupData)
         assertNotNull(keyBackupData!!.sessionData)
 
@@ -334,7 +335,7 @@ class KeysBackupTest : InstrumentedTest {
         val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
 
         // - Restore the e2e backup from the homeserver
-        val importRoomKeysResult = testHelper.doSync<ImportRoomKeysResult> {
+        val importRoomKeysResult = testHelper.waitForCallback<ImportRoomKeysResult> {
             testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(
                     testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
                     testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey,
@@ -379,7 +380,7 @@ class KeysBackupTest : InstrumentedTest {
 //        assertTrue(unsentRequest != null || sentRequest != null)
 //
 //        // - Restore the e2e backup from the homeserver
-//        val importRoomKeysResult = mTestHelper.doSync<ImportRoomKeysResult> {
+//        val importRoomKeysResult = mTestHelper.doSyncSuspending<> {  }<ImportRoomKeysResult> {
 //            testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
 //                    testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey,
 //                    null,
@@ -429,7 +430,7 @@ class KeysBackupTest : InstrumentedTest {
         assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState())
 
         // - Trust the backup from the new device
-        testHelper.doSync<Unit> {
+        testHelper.waitForCallback<Unit> {
             testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersion(
                     testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
                     true,
@@ -445,14 +446,14 @@ class KeysBackupTest : InstrumentedTest {
         assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled())
 
         // - Retrieve the last version from the server
-        val keysVersionResult = testHelper.doSync<KeysBackupLastVersionResult> {
+        val keysVersionResult = testHelper.waitForCallback<KeysBackupLastVersionResult> {
             testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it)
         }.toKeysVersionResult()
 
         // - It must be the same
         assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version)
 
-        val keysBackupVersionTrust = testHelper.doSync<KeysBackupVersionTrust> {
+        val keysBackupVersionTrust = testHelper.waitForCallback<KeysBackupVersionTrust> {
             testData.aliceSession2.cryptoService().keysBackupService().getKeysBackupTrust(keysVersionResult, it)
         }
 
@@ -489,7 +490,7 @@ class KeysBackupTest : InstrumentedTest {
         assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState())
 
         // - Trust the backup from the new device with the recovery key
-        testHelper.doSync<Unit> {
+        testHelper.waitForCallback<Unit> {
             testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithRecoveryKey(
                     testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
                     testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey,
@@ -505,14 +506,14 @@ class KeysBackupTest : InstrumentedTest {
         assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled())
 
         // - Retrieve the last version from the server
-        val keysVersionResult = testHelper.doSync<KeysBackupLastVersionResult> {
+        val keysVersionResult = testHelper.waitForCallback<KeysBackupLastVersionResult> {
             testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it)
         }.toKeysVersionResult()
 
         // - It must be the same
         assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version)
 
-        val keysBackupVersionTrust = testHelper.doSync<KeysBackupVersionTrust> {
+        val keysBackupVersionTrust = testHelper.waitForCallback<KeysBackupVersionTrust> {
             testData.aliceSession2.cryptoService().keysBackupService().getKeysBackupTrust(keysVersionResult, it)
         }
 
@@ -547,13 +548,13 @@ class KeysBackupTest : InstrumentedTest {
         assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState())
 
         // - Try to trust the backup from the new device with a wrong recovery key
-        val latch = CountDownLatch(1)
-        testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithRecoveryKey(
-                testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
-                "Bad recovery key",
-                TestMatrixCallback(latch, false)
-        )
-        testHelper.await(latch)
+        testHelper.waitForCallbackError<Unit> {
+            testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithRecoveryKey(
+                    testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
+                    "Bad recovery key",
+                    it
+            )
+        }
 
         // - The new device must still see the previous backup as not trusted
         assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion)
@@ -591,7 +592,7 @@ class KeysBackupTest : InstrumentedTest {
         assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState())
 
         // - Trust the backup from the new device with the password
-        testHelper.doSync<Unit> {
+        testHelper.waitForCallback<Unit> {
             testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithPassphrase(
                     testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
                     password,
@@ -607,14 +608,14 @@ class KeysBackupTest : InstrumentedTest {
         assertTrue(testData.aliceSession2.cryptoService().keysBackupService().isEnabled())
 
         // - Retrieve the last version from the server
-        val keysVersionResult = testHelper.doSync<KeysBackupLastVersionResult> {
+        val keysVersionResult = testHelper.waitForCallback<KeysBackupLastVersionResult> {
             testData.aliceSession2.cryptoService().keysBackupService().getCurrentVersion(it)
         }.toKeysVersionResult()
 
         // - It must be the same
         assertEquals(testData.prepareKeysBackupDataResult.version, keysVersionResult!!.version)
 
-        val keysBackupVersionTrust = testHelper.doSync<KeysBackupVersionTrust> {
+        val keysBackupVersionTrust = testHelper.waitForCallback<KeysBackupVersionTrust> {
             testData.aliceSession2.cryptoService().keysBackupService().getKeysBackupTrust(keysVersionResult, it)
         }
 
@@ -652,13 +653,13 @@ class KeysBackupTest : InstrumentedTest {
         assertEquals(KeysBackupState.NotTrusted, testData.aliceSession2.cryptoService().keysBackupService().getState())
 
         // - Try to trust the backup from the new device with a wrong password
-        val latch = CountDownLatch(1)
-        testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithPassphrase(
-                testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
-                badPassword,
-                TestMatrixCallback(latch, false)
-        )
-        testHelper.await(latch)
+        testHelper.waitForCallbackError<Unit> {
+            testData.aliceSession2.cryptoService().keysBackupService().trustKeysBackupVersionWithPassphrase(
+                    testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
+                    badPassword,
+                    it
+            )
+        }
 
         // - The new device must still see the previous backup as not trusted
         assertNotNull(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion)
@@ -679,26 +680,21 @@ class KeysBackupTest : InstrumentedTest {
         val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
 
         val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
+        val keysBackupService = testData.aliceSession2.cryptoService().keysBackupService()
 
         // - Try to restore the e2e backup with a wrong recovery key
-        val latch2 = CountDownLatch(1)
-        var importRoomKeysResult: ImportRoomKeysResult? = null
-        testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
-                "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
-                null,
-                null,
-                null,
-                object : TestMatrixCallback<ImportRoomKeysResult>(latch2, false) {
-                    override fun onSuccess(data: ImportRoomKeysResult) {
-                        importRoomKeysResult = data
-                        super.onSuccess(data)
-                    }
-                }
-        )
-        testHelper.await(latch2)
+        val importRoomKeysResult = testHelper.waitForCallbackError<ImportRoomKeysResult> {
+            keysBackupService.restoreKeysWithRecoveryKey(
+                    keysBackupService.keysBackupVersion!!,
+                    "EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
+                    null,
+                    null,
+                    null,
+                    it
+            )
+        }
 
-        // onSuccess may not have been called
-        assertNull(importRoomKeysResult)
+        assertTrue(importRoomKeysResult is InvalidParameterException)
     }
 
     /**
@@ -718,7 +714,7 @@ class KeysBackupTest : InstrumentedTest {
         // - Restore the e2e backup with the password
         val steps = ArrayList<StepProgressListener.Step>()
 
-        val importRoomKeysResult = testHelper.doSync<ImportRoomKeysResult> {
+        val importRoomKeysResult = testHelper.waitForCallback<ImportRoomKeysResult> {
             testData.aliceSession2.cryptoService().keysBackupService().restoreKeyBackupWithPassword(
                     testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
                     password,
@@ -771,26 +767,21 @@ class KeysBackupTest : InstrumentedTest {
         val wrongPassword = "passw0rd"
 
         val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(password)
+        val keysBackupService = testData.aliceSession2.cryptoService().keysBackupService()
 
         // - Try to restore the e2e backup with a wrong password
-        val latch2 = CountDownLatch(1)
-        var importRoomKeysResult: ImportRoomKeysResult? = null
-        testData.aliceSession2.cryptoService().keysBackupService().restoreKeyBackupWithPassword(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
-                wrongPassword,
-                null,
-                null,
-                null,
-                object : TestMatrixCallback<ImportRoomKeysResult>(latch2, false) {
-                    override fun onSuccess(data: ImportRoomKeysResult) {
-                        importRoomKeysResult = data
-                        super.onSuccess(data)
-                    }
-                }
-        )
-        testHelper.await(latch2)
+        val importRoomKeysResult = testHelper.waitForCallbackError<ImportRoomKeysResult> {
+            keysBackupService.restoreKeyBackupWithPassword(
+                    keysBackupService.keysBackupVersion!!,
+                    wrongPassword,
+                    null,
+                    null,
+                    null,
+                    it
+            )
+        }
 
-        // onSuccess may not have been called
-        assertNull(importRoomKeysResult)
+        assertTrue(importRoomKeysResult is InvalidParameterException)
     }
 
     /**
@@ -808,7 +799,7 @@ class KeysBackupTest : InstrumentedTest {
         val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(password)
 
         // - Restore the e2e backup with the recovery key.
-        val importRoomKeysResult = testHelper.doSync<ImportRoomKeysResult> {
+        val importRoomKeysResult = testHelper.waitForCallback<ImportRoomKeysResult> {
             testData.aliceSession2.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(
                     testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
                     testData.prepareKeysBackupDataResult.megolmBackupCreationInfo.recoveryKey,
@@ -833,26 +824,21 @@ class KeysBackupTest : InstrumentedTest {
         val keysBackupTestHelper = KeysBackupTestHelper(testHelper, cryptoTestHelper)
 
         val testData = keysBackupTestHelper.createKeysBackupScenarioWithPassword(null)
+        val keysBackupService = testData.aliceSession2.cryptoService().keysBackupService()
 
         // - Try to restore the e2e backup with a password
-        val latch2 = CountDownLatch(1)
-        var importRoomKeysResult: ImportRoomKeysResult? = null
-        testData.aliceSession2.cryptoService().keysBackupService().restoreKeyBackupWithPassword(testData.aliceSession2.cryptoService().keysBackupService().keysBackupVersion!!,
-                "password",
-                null,
-                null,
-                null,
-                object : TestMatrixCallback<ImportRoomKeysResult>(latch2, false) {
-                    override fun onSuccess(data: ImportRoomKeysResult) {
-                        importRoomKeysResult = data
-                        super.onSuccess(data)
-                    }
-                }
-        )
-        testHelper.await(latch2)
+        val importRoomKeysResult = testHelper.waitForCallbackError<ImportRoomKeysResult> {
+            keysBackupService.restoreKeyBackupWithPassword(
+                    keysBackupService.keysBackupVersion!!,
+                    "password",
+                    null,
+                    null,
+                    null,
+                    it
+            )
+        }
 
-        // onSuccess may not have been called
-        assertNull(importRoomKeysResult)
+        assertTrue(importRoomKeysResult is IllegalStateException)
     }
 
     /**
@@ -874,12 +860,12 @@ class KeysBackupTest : InstrumentedTest {
         keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup)
 
         // Get key backup version from the homeserver
-        val keysVersionResult = testHelper.doSync<KeysBackupLastVersionResult> {
+        val keysVersionResult = testHelper.waitForCallback<KeysBackupLastVersionResult> {
             keysBackup.getCurrentVersion(it)
         }.toKeysVersionResult()
 
         // - Check the returned KeyBackupVersion is trusted
-        val keysBackupVersionTrust = testHelper.doSync<KeysBackupVersionTrust> {
+        val keysBackupVersionTrust = testHelper.waitForCallback<KeysBackupVersionTrust> {
             keysBackup.getKeysBackupTrust(keysVersionResult!!, it)
         }
 
@@ -918,34 +904,39 @@ class KeysBackupTest : InstrumentedTest {
         assertFalse(keysBackup.isEnabled())
 
         // Wait for keys backup to be finished
-        val latch0 = CountDownLatch(1)
         var count = 0
-        keysBackup.addListener(object : KeysBackupStateListener {
-            override fun onStateChange(newState: KeysBackupState) {
-                // Check the backup completes
-                if (newState == KeysBackupState.ReadyToBackUp) {
-                    count++
-
-                    if (count == 2) {
-                        // Remove itself from the list of listeners
-                        keysBackup.removeListener(this)
-
-                        latch0.countDown()
+        waitFor(
+                continueWhen = {
+                    suspendCancellableCoroutine<Unit> { continuation ->
+                        val listener = object : KeysBackupStateListener {
+                            override fun onStateChange(newState: KeysBackupState) {
+                                // Check the backup completes
+                                if (newState == KeysBackupState.ReadyToBackUp) {
+                                    count++
+
+                                    if (count == 2) {
+                                        // Remove itself from the list of listeners
+                                        keysBackup.removeListener(this)
+                                        continuation.resume(Unit)
+                                    }
+                                }
+                            }
+                        }
+                        keysBackup.addListener(listener)
+                        continuation.invokeOnCancellation { keysBackup.removeListener(listener) }
                     }
-                }
-            }
-        })
-
-        // - Make alice back up her keys to her homeserver
-        keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup)
+                },
+                action = {
+                    // - Make alice back up her keys to her homeserver
+                    keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup)
+                },
+        )
 
         assertTrue(keysBackup.isEnabled())
 
-        testHelper.await(latch0)
-
         // - Create a new backup with fake data on the homeserver, directly using the rest client
         val megolmBackupCreationInfo = cryptoTestHelper.createFakeMegolmBackupCreationInfo()
-        testHelper.doSync<KeysVersion> {
+        testHelper.waitForCallback<KeysVersion> {
             (keysBackup as DefaultKeysBackupService).createFakeKeysBackupVersion(megolmBackupCreationInfo, it)
         }
 
@@ -953,9 +944,7 @@ class KeysBackupTest : InstrumentedTest {
         (cryptoTestData.firstSession.cryptoService().keysBackupService() as DefaultKeysBackupService).store.resetBackupMarkers()
 
         // - Make alice back up all her keys again
-        val latch2 = CountDownLatch(1)
-        keysBackup.backupAllGroupSessions(null, TestMatrixCallback(latch2, false))
-        testHelper.await(latch2)
+        testHelper.waitForCallbackError<Unit> { keysBackup.backupAllGroupSessions(null, it) }
 
         // -> That must fail and her backup state must be WrongBackUpVersion
         assertEquals(KeysBackupState.WrongBackUpVersion, keysBackup.getState())
@@ -991,7 +980,7 @@ class KeysBackupTest : InstrumentedTest {
         keysBackupTestHelper.prepareAndCreateKeysBackupData(keysBackup)
 
         // Wait for keys backup to finish by asking again to backup keys.
-        testHelper.doSync<Unit> {
+        testHelper.waitForCallback<Unit> {
             keysBackup.backupAllGroupSessions(null, it)
         }
 
@@ -1016,19 +1005,7 @@ class KeysBackupTest : InstrumentedTest {
 
         val stateObserver2 = StateObserver(keysBackup2)
 
-        var isSuccessful = false
-        val latch2 = CountDownLatch(1)
-        keysBackup2.backupAllGroupSessions(
-                null,
-                object : TestMatrixCallback<Unit>(latch2, false) {
-                    override fun onSuccess(data: Unit) {
-                        isSuccessful = true
-                        super.onSuccess(data)
-                    }
-                })
-        testHelper.await(latch2)
-
-        assertFalse(isSuccessful)
+        testHelper.waitForCallbackError<Unit> { keysBackup2.backupAllGroupSessions(null, it) }
 
         // Backup state must be NotTrusted
         assertEquals("Backup state must be NotTrusted", KeysBackupState.NotTrusted, keysBackup2.getState())
@@ -1042,24 +1019,25 @@ class KeysBackupTest : InstrumentedTest {
         )
 
         // -> Backup should automatically enable on the new device
-        val latch4 = CountDownLatch(1)
-        keysBackup2.addListener(object : KeysBackupStateListener {
-            override fun onStateChange(newState: KeysBackupState) {
-                // Check the backup completes
-                if (keysBackup2.getState() == KeysBackupState.ReadyToBackUp) {
-                    // Remove itself from the list of listeners
-                    keysBackup2.removeListener(this)
-
-                    latch4.countDown()
+        suspendCancellableCoroutine<Unit> { continuation ->
+            val listener = object : KeysBackupStateListener {
+                override fun onStateChange(newState: KeysBackupState) {
+                    // Check the backup completes
+                    if (keysBackup2.getState() == KeysBackupState.ReadyToBackUp) {
+                        // Remove itself from the list of listeners
+                        keysBackup2.removeListener(this)
+                        continuation.resume(Unit)
+                    }
                 }
             }
-        })
-        testHelper.await(latch4)
+            keysBackup2.addListener(listener)
+            continuation.invokeOnCancellation { keysBackup2.removeListener(listener) }
+        }
 
         // -> It must use the same backup version
         assertEquals(oldKeyBackupVersion, aliceSession2.cryptoService().keysBackupService().currentBackupVersion)
 
-        testHelper.doSync<Unit> {
+        testHelper.waitForCallback<Unit> {
             aliceSession2.cryptoService().keysBackupService().backupAllGroupSessions(null, it)
         }
 
@@ -1092,7 +1070,7 @@ class KeysBackupTest : InstrumentedTest {
         assertTrue(keysBackup.isEnabled())
 
         // Delete the backup
-        testHelper.doSync<Unit> { keysBackup.deleteBackup(keyBackupCreationInfo.version, it) }
+        testHelper.waitForCallback<Unit> { keysBackup.deleteBackup(keyBackupCreationInfo.version, it) }
 
         // Backup is now disabled
         assertFalse(keysBackup.isEnabled())
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt
index 2cc2b506b925322552f247fbb6f002c0e889fbbd..10abf93bcb071948fb4e900f234a49d52470b3ea 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/keysbackup/KeysBackupTestHelper.kt
@@ -16,6 +16,7 @@
 
 package org.matrix.android.sdk.internal.crypto.keysbackup
 
+import kotlinx.coroutines.suspendCancellableCoroutine
 import org.junit.Assert
 import org.matrix.android.sdk.api.listeners.ProgressListener
 import org.matrix.android.sdk.api.session.Session
@@ -29,7 +30,7 @@ import org.matrix.android.sdk.common.CryptoTestHelper
 import org.matrix.android.sdk.common.assertDictEquals
 import org.matrix.android.sdk.common.assertListEquals
 import org.matrix.android.sdk.internal.crypto.MegolmSessionData
-import java.util.concurrent.CountDownLatch
+import kotlin.coroutines.resume
 
 internal class KeysBackupTestHelper(
         private val testHelper: CommonTestHelper,
@@ -47,7 +48,7 @@ internal class KeysBackupTestHelper(
      *
      * @param password optional password
      */
-    fun createKeysBackupScenarioWithPassword(password: String?): KeysBackupScenarioData {
+    suspend fun createKeysBackupScenarioWithPassword(password: String?): KeysBackupScenarioData {
         val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoomWithEncryptedMessages()
 
         waitForKeybackUpBatching()
@@ -64,7 +65,7 @@ internal class KeysBackupTestHelper(
 
         var lastProgress = 0
         var lastTotal = 0
-        testHelper.doSync<Unit> {
+        testHelper.waitForCallback<Unit> {
             keysBackup.backupAllGroupSessions(object : ProgressListener {
                 override fun onProgress(progress: Int, total: Int) {
                     lastProgress = progress
@@ -97,13 +98,13 @@ internal class KeysBackupTestHelper(
         )
     }
 
-    fun prepareAndCreateKeysBackupData(
+    suspend fun prepareAndCreateKeysBackupData(
             keysBackup: KeysBackupService,
             password: String? = null
     ): PrepareKeysBackupDataResult {
         val stateObserver = StateObserver(keysBackup)
 
-        val megolmBackupCreationInfo = testHelper.doSync<MegolmBackupCreationInfo> {
+        val megolmBackupCreationInfo = testHelper.waitForCallback<MegolmBackupCreationInfo> {
             keysBackup.prepareKeysBackupVersion(password, null, it)
         }
 
@@ -112,7 +113,7 @@ internal class KeysBackupTestHelper(
         Assert.assertFalse("Key backup should not be enabled before creation", keysBackup.isEnabled())
 
         // Create the version
-        val keysVersion = testHelper.doSync<KeysVersion> {
+        val keysVersion = testHelper.waitForCallback<KeysVersion> {
             keysBackup.createKeysBackupVersion(megolmBackupCreationInfo, it)
         }
 
@@ -129,25 +130,26 @@ internal class KeysBackupTestHelper(
      * As KeysBackup is doing asynchronous call to update its internal state, this method help to wait for the
      * KeysBackup object to be in the specified state
      */
-    fun waitForKeysBackupToBeInState(session: Session, state: KeysBackupState) {
+    suspend fun waitForKeysBackupToBeInState(session: Session, state: KeysBackupState) {
         // If already in the wanted state, return
-        if (session.cryptoService().keysBackupService().getState() == state) {
+        val keysBackupService = session.cryptoService().keysBackupService()
+        if (keysBackupService.getState() == state) {
             return
         }
 
         // Else observe state changes
-        val latch = CountDownLatch(1)
-
-        session.cryptoService().keysBackupService().addListener(object : KeysBackupStateListener {
-            override fun onStateChange(newState: KeysBackupState) {
-                if (newState == state) {
-                    session.cryptoService().keysBackupService().removeListener(this)
-                    latch.countDown()
+        suspendCancellableCoroutine<Unit> { continuation ->
+            val listener = object : KeysBackupStateListener {
+                override fun onStateChange(newState: KeysBackupState) {
+                    if (newState == state) {
+                        keysBackupService.removeListener(this)
+                        continuation.resume(Unit)
+                    }
                 }
             }
-        })
-
-        testHelper.await(latch)
+            keysBackupService.addListener(listener)
+            continuation.invokeOnCancellation { keysBackupService.removeListener(listener) }
+        }
     }
 
     fun assertKeysEquals(keys1: MegolmSessionData?, keys2: MegolmSessionData?) {
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/replayattack/ReplayAttackTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/replayattack/ReplayAttackTest.kt
index 53cf802b91e9978a09906cff4fbc7e757db675fd..0dfecffbded67ed21b0698b145df5aa5c5a7e63c 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/replayattack/ReplayAttackTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/replayattack/ReplayAttackTest.kt
@@ -58,18 +58,16 @@ class ReplayAttackTest : InstrumentedTest {
         val fakeEventWithTheSameIndex =
                 sentEvents[0].copy(eventId = fakeEventId, root = sentEvents[0].root.copy(eventId = fakeEventId))
 
-        testHelper.runBlockingTest {
-            // Lets assume we are from the main timelineId
-            val timelineId = "timelineId"
-            // Lets decrypt the original event
-            aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId)
-            // Lets decrypt the fake event that will have the same message index
-            val exception = assertFailsWith<MXCryptoError.Base> {
-                // An exception should be thrown while the same index would have been used for the previous decryption
-                aliceSession.cryptoService().decryptEvent(fakeEventWithTheSameIndex.root, timelineId)
-            }
-            assertEquals(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, exception.errorType)
+        // Lets assume we are from the main timelineId
+        val timelineId = "timelineId"
+        // Lets decrypt the original event
+        aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId)
+        // Lets decrypt the fake event that will have the same message index
+        val exception = assertFailsWith<MXCryptoError.Base> {
+            // An exception should be thrown while the same index would have been used for the previous decryption
+            aliceSession.cryptoService().decryptEvent(fakeEventWithTheSameIndex.root, timelineId)
         }
+        assertEquals(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, exception.errorType)
         cryptoTestData.cleanUp(testHelper)
     }
 
@@ -93,17 +91,15 @@ class ReplayAttackTest : InstrumentedTest {
         Assert.assertTrue("Message should be sent", sentEvents.size == 1)
         assertEquals(sentEvents.size, 1)
 
-        testHelper.runBlockingTest {
-            // Lets assume we are from the main timelineId
-            val timelineId = "timelineId"
-            // Lets decrypt the original event
+        // Lets assume we are from the main timelineId
+        val timelineId = "timelineId"
+        // Lets decrypt the original event
+        aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId)
+        try {
+            // Lets try to decrypt the same event
             aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId)
-            try {
-                // Lets try to decrypt the same event
-                aliceSession.cryptoService().decryptEvent(sentEvents[0].root, timelineId)
-            } catch (ex: Throwable) {
-                fail("Shouldn't throw a decryption error for same event")
-            }
+        } catch (ex: Throwable) {
+            fail("Shouldn't throw a decryption error for same event")
         }
     }
 }
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt
index c8be6aae74ec8a6bcb24a9a5ce4e36e238c591d7..0467d082a33f2952237a8b1309f93acfe2e0485d 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/ssss/QuadSTests.kt
@@ -16,7 +16,6 @@
 
 package org.matrix.android.sdk.internal.crypto.ssss
 
-import androidx.lifecycle.Observer
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertNotNull
@@ -37,12 +36,12 @@ import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec
 import org.matrix.android.sdk.api.session.securestorage.SecretStorageKeyContent
 import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageError
 import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo
-import org.matrix.android.sdk.api.util.Optional
 import org.matrix.android.sdk.api.util.toBase64NoPadding
-import org.matrix.android.sdk.common.CommonTestHelper
 import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
 import org.matrix.android.sdk.common.SessionTestParams
 import org.matrix.android.sdk.common.TestConstants
+import org.matrix.android.sdk.common.first
+import org.matrix.android.sdk.common.onMain
 import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorageService
 
 @RunWith(AndroidJUnit4::class)
@@ -64,22 +63,14 @@ class QuadSTests : InstrumentedTest {
 
         val TEST_KEY_ID = "my.test.Key"
 
-        testHelper.runBlockingTest {
-            quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner)
-        }
+        quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner)
 
-        var accountData: UserAccountDataEvent? = null
         // Assert Account data is updated
-        testHelper.waitWithLatch {
-            val liveAccountData = aliceSession.accountDataService().getLiveUserAccountDataEvent("${DefaultSharedSecretStorageService.KEY_ID_BASE}.$TEST_KEY_ID")
-            val accountDataObserver = Observer<Optional<UserAccountDataEvent>?> { t ->
-                if (t?.getOrNull()?.type == "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$TEST_KEY_ID") {
-                    accountData = t.getOrNull()
-                }
-                it.countDown()
-            }
-            liveAccountData.observeForever(accountDataObserver)
-        }
+        val accountData = aliceSession.accountDataService()
+                .onMain { getLiveUserAccountDataEvent("${DefaultSharedSecretStorageService.KEY_ID_BASE}.$TEST_KEY_ID") }
+                .first { it.getOrNull()?.type == "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$TEST_KEY_ID" }
+                .getOrNull()
+
         assertNotNull("Key should be stored in account data", accountData)
         val parsed = SecretStorageKeyContent.fromJson(accountData!!.content)
         assertNotNull("Key Content cannot be parsed", parsed)
@@ -87,20 +78,13 @@ class QuadSTests : InstrumentedTest {
         assertEquals("Unexpected key name", "Test Key", parsed.name)
         assertNull("Key was not generated from passphrase", parsed.passphrase)
 
-        var defaultKeyAccountData: UserAccountDataEvent? = null
+        quadS.setDefaultKey(TEST_KEY_ID)
+        val defaultKeyAccountData = aliceSession.accountDataService()
+                .onMain { getLiveUserAccountDataEvent(DefaultSharedSecretStorageService.DEFAULT_KEY_ID) }
+                .first { it.getOrNull()?.type == DefaultSharedSecretStorageService.DEFAULT_KEY_ID }
+                .getOrNull()
+
         // Set as default key
-        testHelper.waitWithLatch { latch ->
-            quadS.setDefaultKey(TEST_KEY_ID)
-            val liveDefAccountData =
-                    aliceSession.accountDataService().getLiveUserAccountDataEvent(DefaultSharedSecretStorageService.DEFAULT_KEY_ID)
-            val accountDefDataObserver = Observer<Optional<UserAccountDataEvent>?> { t ->
-                if (t?.getOrNull()?.type == DefaultSharedSecretStorageService.DEFAULT_KEY_ID) {
-                    defaultKeyAccountData = t.getOrNull()!!
-                    latch.countDown()
-                }
-            }
-            liveDefAccountData.observeForever(accountDefDataObserver)
-        }
         assertNotNull(defaultKeyAccountData?.content)
         assertEquals("Unexpected default key ${defaultKeyAccountData?.content}", TEST_KEY_ID, defaultKeyAccountData?.content?.get("key"))
 
@@ -112,21 +96,19 @@ class QuadSTests : InstrumentedTest {
 
         val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
         val keyId = "My.Key"
-        val info = generatedSecret(testHelper, aliceSession, keyId, true)
+        val info = generatedSecret(aliceSession, keyId, true)
 
         val keySpec = RawBytesKeySpec.fromRecoveryKey(info.recoveryKey)
 
         // Store a secret
         val clearSecret = "42".toByteArray().toBase64NoPadding()
-        testHelper.runBlockingTest {
-            aliceSession.sharedSecretStorageService().storeSecret(
-                    "secret.of.life",
-                    clearSecret,
-                    listOf(KeyRef(null, keySpec)) // default key
-            )
-        }
+        aliceSession.sharedSecretStorageService().storeSecret(
+                "secret.of.life",
+                clearSecret,
+                listOf(KeyRef(null, keySpec)) // default key
+        )
 
-        val secretAccountData = assertAccountData(testHelper, aliceSession, "secret.of.life")
+        val secretAccountData = assertAccountData(aliceSession, "secret.of.life")
 
         val encryptedContent = secretAccountData.content["encrypted"] as? Map<*, *>
         assertNotNull("Element should be encrypted", encryptedContent)
@@ -139,13 +121,11 @@ class QuadSTests : InstrumentedTest {
 
         // Try to decrypt??
 
-        val decryptedSecret = testHelper.runBlockingTest {
-            aliceSession.sharedSecretStorageService().getSecret(
-                    "secret.of.life",
-                    null, // default key
-                    keySpec!!
-            )
-        }
+        val decryptedSecret = aliceSession.sharedSecretStorageService().getSecret(
+                "secret.of.life",
+                null, // default key
+                keySpec!!
+        )
 
         assertEquals("Secret mismatch", clearSecret, decryptedSecret)
     }
@@ -159,14 +139,10 @@ class QuadSTests : InstrumentedTest {
 
         val TEST_KEY_ID = "my.test.Key"
 
-        testHelper.runBlockingTest {
-            quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner)
-        }
+        quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner)
 
         // Test that we don't need to wait for an account data sync to access directly the keyid from DB
-        testHelper.runBlockingTest {
-            quadS.setDefaultKey(TEST_KEY_ID)
-        }
+        quadS.setDefaultKey(TEST_KEY_ID)
     }
 
     @Test
@@ -174,22 +150,20 @@ class QuadSTests : InstrumentedTest {
 
         val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
         val keyId1 = "Key.1"
-        val key1Info = generatedSecret(testHelper, aliceSession, keyId1, true)
+        val key1Info = generatedSecret(aliceSession, keyId1, true)
         val keyId2 = "Key2"
-        val key2Info = generatedSecret(testHelper, aliceSession, keyId2, true)
+        val key2Info = generatedSecret(aliceSession, keyId2, true)
 
         val mySecretText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
 
-        testHelper.runBlockingTest {
-            aliceSession.sharedSecretStorageService().storeSecret(
-                    "my.secret",
-                    mySecretText.toByteArray().toBase64NoPadding(),
-                    listOf(
-                            KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)),
-                            KeyRef(keyId2, RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey))
-                    )
-            )
-        }
+        aliceSession.sharedSecretStorageService().storeSecret(
+                "my.secret",
+                mySecretText.toByteArray().toBase64NoPadding(),
+                listOf(
+                        KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)),
+                        KeyRef(keyId2, RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey))
+                )
+        )
 
         val accountDataEvent = aliceSession.accountDataService().getUserAccountDataEvent("my.secret")
         val encryptedContent = accountDataEvent?.content?.get("encrypted") as? Map<*, *>
@@ -200,21 +174,17 @@ class QuadSTests : InstrumentedTest {
         assertNotNull(encryptedContent?.get(keyId2))
 
         // Assert that can decrypt with both keys
-        testHelper.runBlockingTest {
-            aliceSession.sharedSecretStorageService().getSecret(
-                    "my.secret",
-                    keyId1,
-                    RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)!!
-            )
-        }
-
-        testHelper.runBlockingTest {
-            aliceSession.sharedSecretStorageService().getSecret(
-                    "my.secret",
-                    keyId2,
-                    RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)!!
-            )
-        }
+        aliceSession.sharedSecretStorageService().getSecret(
+                "my.secret",
+                keyId1,
+                RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)!!
+        )
+
+        aliceSession.sharedSecretStorageService().getSecret(
+                "my.secret",
+                keyId2,
+                RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)!!
+        )
     }
 
     @Test
@@ -224,104 +194,84 @@ class QuadSTests : InstrumentedTest {
         val aliceSession = testHelper.createAccount(TestConstants.USER_ALICE, SessionTestParams(true))
         val keyId1 = "Key.1"
         val passphrase = "The good pass phrase"
-        val key1Info = generatedSecretFromPassphrase(testHelper, aliceSession, passphrase, keyId1, true)
+        val key1Info = generatedSecretFromPassphrase(aliceSession, passphrase, keyId1, true)
 
         val mySecretText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
 
-        testHelper.runBlockingTest {
-            aliceSession.sharedSecretStorageService().storeSecret(
-                    "my.secret",
-                    mySecretText.toByteArray().toBase64NoPadding(),
-                    listOf(KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)))
-            )
-        }
+        aliceSession.sharedSecretStorageService().storeSecret(
+                "my.secret",
+                mySecretText.toByteArray().toBase64NoPadding(),
+                listOf(KeyRef(keyId1, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)))
+        )
 
-        testHelper.runBlockingTest {
-            try {
-                aliceSession.sharedSecretStorageService().getSecret(
-                        "my.secret",
-                        keyId1,
-                        RawBytesKeySpec.fromPassphrase(
-                                "A bad passphrase",
-                                key1Info.content?.passphrase?.salt ?: "",
-                                key1Info.content?.passphrase?.iterations ?: 0,
-                                null
-                        )
-                )
-            } catch (throwable: Throwable) {
-                assert(throwable is SharedSecretStorageError.BadMac)
-            }
-        }
-
-        // Now try with correct key
-        testHelper.runBlockingTest {
+        try {
             aliceSession.sharedSecretStorageService().getSecret(
                     "my.secret",
                     keyId1,
                     RawBytesKeySpec.fromPassphrase(
-                            passphrase,
+                            "A bad passphrase",
                             key1Info.content?.passphrase?.salt ?: "",
                             key1Info.content?.passphrase?.iterations ?: 0,
                             null
                     )
             )
+        } catch (throwable: Throwable) {
+            assert(throwable is SharedSecretStorageError.BadMac)
         }
+
+        // Now try with correct key
+        aliceSession.sharedSecretStorageService().getSecret(
+                "my.secret",
+                keyId1,
+                RawBytesKeySpec.fromPassphrase(
+                        passphrase,
+                        key1Info.content?.passphrase?.salt ?: "",
+                        key1Info.content?.passphrase?.iterations ?: 0,
+                        null
+                )
+        )
     }
 
-    private fun assertAccountData(testHelper: CommonTestHelper, session: Session, type: String): UserAccountDataEvent {
-        var accountData: UserAccountDataEvent? = null
-        testHelper.waitWithLatch {
-            val liveAccountData = session.accountDataService().getLiveUserAccountDataEvent(type)
-            val accountDataObserver = Observer<Optional<UserAccountDataEvent>?> { t ->
-                if (t?.getOrNull()?.type == type) {
-                    accountData = t.getOrNull()
-                    it.countDown()
-                }
-            }
-            liveAccountData.observeForever(accountDataObserver)
-        }
+    private suspend fun assertAccountData(session: Session, type: String): UserAccountDataEvent {
+        val accountData = session.accountDataService()
+                .onMain { getLiveUserAccountDataEvent(type) }
+                .first { it.getOrNull()?.type == type }
+                .getOrNull()
+
         assertNotNull("Account Data type:$type should be found", accountData)
         return accountData!!
     }
 
-    private fun generatedSecret(testHelper: CommonTestHelper, session: Session, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo {
+    private suspend fun generatedSecret(session: Session, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo {
         val quadS = session.sharedSecretStorageService()
 
-        val creationInfo = testHelper.runBlockingTest {
-            quadS.generateKey(keyId, null, keyId, emptyKeySigner)
-        }
+        val creationInfo = quadS.generateKey(keyId, null, keyId, emptyKeySigner)
 
-        assertAccountData(testHelper, session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId")
+        assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId")
 
         if (asDefault) {
-            testHelper.runBlockingTest {
-                quadS.setDefaultKey(keyId)
-            }
-            assertAccountData(testHelper, session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID)
+            quadS.setDefaultKey(keyId)
+            assertAccountData(session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID)
         }
 
         return creationInfo
     }
 
-    private fun generatedSecretFromPassphrase(testHelper: CommonTestHelper, session: Session, passphrase: String, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo {
+    private suspend fun generatedSecretFromPassphrase(session: Session, passphrase: String, keyId: String, asDefault: Boolean = true): SsssKeyCreationInfo {
         val quadS = session.sharedSecretStorageService()
 
-        val creationInfo = testHelper.runBlockingTest {
-            quadS.generateKeyWithPassphrase(
-                    keyId,
-                    keyId,
-                    passphrase,
-                    emptyKeySigner,
-                    null
-            )
-        }
+        val creationInfo = quadS.generateKeyWithPassphrase(
+                keyId,
+                keyId,
+                passphrase,
+                emptyKeySigner,
+                null
+        )
 
-        assertAccountData(testHelper, session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId")
+        assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId")
         if (asDefault) {
-            testHelper.runBlockingTest {
-                quadS.setDefaultKey(keyId)
-            }
-            assertAccountData(testHelper, session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID)
+            quadS.setDefaultKey(keyId)
+            assertAccountData(session, DefaultSharedSecretStorageService.DEFAULT_KEY_ID)
         }
 
         return creationInfo
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt
index 1bffbeeeaa9dd9d22eba064e57cef491a35c275e..fd2136edd5f0dabd612e3c3dc81a27f599afddd7 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/SASTest.kt
@@ -547,23 +547,19 @@ class SASTest : InstrumentedTest {
 
         var requestID: String? = null
 
-        testHelper.waitWithLatch {
-            testHelper.retryPeriodicallyWithLatch(it) {
-                val prAlicePOV = aliceVerificationService.getExistingVerificationRequests(bobSession.myUserId).firstOrNull()
-                requestID = prAlicePOV?.transactionId
-                Log.v("TEST", "== alicePOV is $prAlicePOV")
-                prAlicePOV?.transactionId != null && prAlicePOV.localId == req.localId
-            }
+        testHelper.retryPeriodically {
+            val prAlicePOV = aliceVerificationService.getExistingVerificationRequests(bobSession.myUserId).firstOrNull()
+            requestID = prAlicePOV?.transactionId
+            Log.v("TEST", "== alicePOV is $prAlicePOV")
+            prAlicePOV?.transactionId != null && prAlicePOV.localId == req.localId
         }
 
         Log.v("TEST", "== requestID is $requestID")
 
-        testHelper.waitWithLatch {
-            testHelper.retryPeriodicallyWithLatch(it) {
-                val prBobPOV = bobVerificationService.getExistingVerificationRequests(aliceSession.myUserId).firstOrNull()
-                Log.v("TEST", "== prBobPOV is $prBobPOV")
-                prBobPOV?.transactionId == requestID
-            }
+        testHelper.retryPeriodically {
+            val prBobPOV = bobVerificationService.getExistingVerificationRequests(aliceSession.myUserId).firstOrNull()
+            Log.v("TEST", "== prBobPOV is $prBobPOV")
+            prBobPOV?.transactionId == requestID
         }
 
         bobVerificationService.readyPendingVerification(
@@ -573,12 +569,10 @@ class SASTest : InstrumentedTest {
         )
 
         // wait for alice to get the ready
-        testHelper.waitWithLatch {
-            testHelper.retryPeriodicallyWithLatch(it) {
-                val prAlicePOV = aliceVerificationService.getExistingVerificationRequests(bobSession.myUserId).firstOrNull()
-                Log.v("TEST", "== prAlicePOV is $prAlicePOV")
-                prAlicePOV?.transactionId == requestID && prAlicePOV?.isReady != null
-            }
+        testHelper.retryPeriodically {
+            val prAlicePOV = aliceVerificationService.getExistingVerificationRequests(bobSession.myUserId).firstOrNull()
+            Log.v("TEST", "== prAlicePOV is $prAlicePOV")
+            prAlicePOV?.transactionId == requestID && prAlicePOV?.isReady != null
         }
 
         // Start concurrent!
@@ -602,20 +596,16 @@ class SASTest : InstrumentedTest {
         var alicePovTx: SasVerificationTransaction?
         var bobPovTx: SasVerificationTransaction?
 
-        testHelper.waitWithLatch {
-            testHelper.retryPeriodicallyWithLatch(it) {
-                alicePovTx = aliceVerificationService.getExistingTransaction(bobSession.myUserId, requestID!!) as? SasVerificationTransaction
-                Log.v("TEST", "== alicePovTx is $alicePovTx")
-                alicePovTx?.state == VerificationTxState.ShortCodeReady
-            }
+        testHelper.retryPeriodically {
+            alicePovTx = aliceVerificationService.getExistingTransaction(bobSession.myUserId, requestID!!) as? SasVerificationTransaction
+            Log.v("TEST", "== alicePovTx is $alicePovTx")
+            alicePovTx?.state == VerificationTxState.ShortCodeReady
         }
         // wait for alice to get the ready
-        testHelper.waitWithLatch {
-            testHelper.retryPeriodicallyWithLatch(it) {
-                bobPovTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, requestID!!) as? SasVerificationTransaction
-                Log.v("TEST", "== bobPovTx is $bobPovTx")
-                bobPovTx?.state == VerificationTxState.ShortCodeReady
-            }
+        testHelper.retryPeriodically {
+            bobPovTx = bobVerificationService.getExistingTransaction(aliceSession.myUserId, requestID!!) as? SasVerificationTransaction
+            Log.v("TEST", "== bobPovTx is $bobPovTx")
+            bobPovTx?.state == VerificationTxState.ShortCodeReady
         }
     }
 }
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt
index 3f229069653067c43b4e0b13c1d5c95d72519994..4ecfe5be8f31ca3f5741e5aa029401f82ee2c5e2 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt
@@ -164,7 +164,7 @@ class VerificationTest : InstrumentedTest {
         val aliceSession = cryptoTestData.firstSession
         val bobSession = cryptoTestData.secondSession!!
 
-        testHelper.doSync<Unit> { callback ->
+        testHelper.waitForCallback<Unit> { callback ->
             aliceSession.cryptoService().crossSigningService()
                     .initializeCrossSigning(
                             object : UserInteractiveAuthInterceptor {
@@ -181,7 +181,7 @@ class VerificationTest : InstrumentedTest {
                     )
         }
 
-        testHelper.doSync<Unit> { callback ->
+        testHelper.waitForCallback<Unit> { callback ->
             bobSession.cryptoService().crossSigningService()
                     .initializeCrossSigning(
                             object : UserInteractiveAuthInterceptor {
@@ -261,7 +261,11 @@ class VerificationTest : InstrumentedTest {
 
         val aliceSessionToVerify = testHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
         val aliceSessionThatVerifies = testHelper.logIntoAccount(aliceSessionToVerify.myUserId, TestConstants.PASSWORD, defaultSessionParams)
-        val aliceSessionThatReceivesCanceledEvent = testHelper.logIntoAccount(aliceSessionToVerify.myUserId, TestConstants.PASSWORD, defaultSessionParams)
+        val aliceSessionThatReceivesCanceledEvent = testHelper.logIntoAccount(
+                aliceSessionToVerify.myUserId,
+                TestConstants.PASSWORD,
+                defaultSessionParams
+        )
 
         val verificationMethods = listOf(VerificationMethod.SAS, VerificationMethod.QR_CODE_SCAN, VerificationMethod.QR_CODE_SHOW)
 
@@ -286,11 +290,9 @@ class VerificationTest : InstrumentedTest {
                 otherDevices = listOfNotNull(aliceSessionThatVerifies.sessionParams.deviceId, aliceSessionThatReceivesCanceledEvent.sessionParams.deviceId),
         )
 
-        testHelper.waitWithLatch { latch ->
-            testHelper.retryPeriodicallyWithLatch(latch) {
-                val requests = serviceOfUserWhoReceivesCancellation.getExistingVerificationRequests(aliceSessionToVerify.myUserId)
-                requests.any { it.cancelConclusion == CancelCode.AcceptedByAnotherDevice }
-            }
+        testHelper.retryPeriodically {
+            val requests = serviceOfUserWhoReceivesCancellation.getExistingVerificationRequests(aliceSessionToVerify.myUserId)
+            requests.any { it.cancelConclusion == CancelCode.AcceptedByAnotherDevice }
         }
 
         testHelper.signOutAndClose(aliceSessionToVerify)
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt
index 59b3b1453229c135340bba60a27f831b0ca78c22..656e00bcbdf89404e5a91f545150f3b3599550b7 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineSimpleBackPaginationTest.kt
@@ -17,7 +17,7 @@
 package org.matrix.android.sdk.session.room.timeline
 
 import androidx.test.filters.LargeTest
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.suspendCancellableCoroutine
 import org.amshove.kluent.internal.assertEquals
 import org.junit.FixMethodOrder
 import org.junit.Ignore
@@ -35,6 +35,9 @@ import org.matrix.android.sdk.api.session.room.timeline.Timeline
 import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
 import org.matrix.android.sdk.common.CommonTestHelper.Companion.runCryptoTest
 import org.matrix.android.sdk.common.TestConstants
+import org.matrix.android.sdk.common.waitFor
+import org.matrix.android.sdk.common.wrapWithTimeout
+import kotlin.coroutines.resume
 
 @RunWith(JUnit4::class)
 @FixMethodOrder(MethodSorters.JVM)
@@ -69,30 +72,36 @@ class TimelineSimpleBackPaginationTest : InstrumentedTest {
         val bobTimeline = roomFromBobPOV.timelineService().createTimeline(null, TimelineSettings(30))
         bobTimeline.start()
 
-        commonTestHelper.waitWithLatch(timeout = TestConstants.timeOutMillis * 10) {
-            val listener = object : Timeline.Listener {
+        waitFor(
+                continueWhen = {
+                    wrapWithTimeout(timeout = TestConstants.timeOutMillis * 10) {
+                        suspendCancellableCoroutine<Unit> { continuation ->
+                            val listener = object : Timeline.Listener {
 
-                override fun onStateUpdated(direction: Timeline.Direction, state: Timeline.PaginationState) {
-                    if (direction == Timeline.Direction.FORWARDS) {
-                        return
+                                override fun onStateUpdated(direction: Timeline.Direction, state: Timeline.PaginationState) {
+                                    if (direction == Timeline.Direction.FORWARDS) {
+                                        return
+                                    }
+                                    if (state.hasMoreToLoad && !state.loading) {
+                                        bobTimeline.paginate(Timeline.Direction.BACKWARDS, 30)
+                                    } else if (!state.hasMoreToLoad) {
+                                        bobTimeline.removeListener(this)
+                                        continuation.resume(Unit)
+                                    }
+                                }
+                            }
+                            bobTimeline.addListener(listener)
+                            continuation.invokeOnCancellation { bobTimeline.removeListener(listener) }
+                        }
                     }
-                    if (state.hasMoreToLoad && !state.loading) {
-                        bobTimeline.paginate(Timeline.Direction.BACKWARDS, 30)
-                    } else if (!state.hasMoreToLoad) {
-                        bobTimeline.removeListener(this)
-                        it.countDown()
-                    }
-                }
-            }
-            bobTimeline.addListener(listener)
-            bobTimeline.paginate(Timeline.Direction.BACKWARDS, 30)
-        }
+                },
+                action = { bobTimeline.paginate(Timeline.Direction.BACKWARDS, 30) }
+        )
+
         assertEquals(false, bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS))
         assertEquals(false, bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS))
 
-        val onlySentEvents = runBlocking {
-            bobTimeline.getSnapshot()
-        }
+        val onlySentEvents = bobTimeline.getSnapshot()
                 .filter {
                     it.root.isTextMessage()
                 }.filter {
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt
index 7c97426c391a675f4c2659b030e12ef214162eb6..6ef90193d8010f76a15f86c6bd996fdf3dc8859a 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/search/SearchMessagesTest.kt
@@ -85,9 +85,7 @@ class SearchMessagesTest : InstrumentedTest {
                 2
         )
 
-        val data = commonTestHelper.runBlockingTest {
-            block.invoke(cryptoTestData)
-        }
+        val data = block.invoke(cryptoTestData)
 
         assertTrue(data.results?.size == 2)
         assertTrue(
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt
index 2cd579df24dfec8056199cf1c48cc539656b6d3d..df131cc19aa542c43516a5e351ab4fa09782a6b0 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceCreationTest.kt
@@ -55,15 +55,11 @@ class SpaceCreationTest : InstrumentedTest {
         val session = commonTestHelper.createAccount("Hubble", SessionTestParams(true))
         val roomName = "My Space"
         val topic = "A public space for test"
-        var spaceId: String = ""
-        commonTestHelper.runBlockingTest {
-            spaceId = session.spaceService().createSpace(roomName, topic, null, true)
-        }
+        val spaceId = session.spaceService().createSpace(roomName, topic, null, true)
 
-        commonTestHelper.waitWithLatch {
-            commonTestHelper.retryPeriodicallyWithLatch(it) {
-                session.spaceService().getSpace(spaceId)?.asRoom()?.roomSummary()?.name != null
-            }
+        commonTestHelper.retryPeriodically {
+            val roomSummary = session.spaceService().getSpace(spaceId)?.asRoom()?.roomSummary()
+            roomSummary?.name == roomName && roomSummary.topic == topic
         }
 
         val syncedSpace = session.spaceService().getSpace(spaceId)
@@ -79,14 +75,12 @@ class SpaceCreationTest : InstrumentedTest {
         assertEquals("Room type should be space", RoomType.SPACE, createContent?.type)
 
         var powerLevelsContent: PowerLevelsContent? = null
-        commonTestHelper.waitWithLatch { latch ->
-            commonTestHelper.retryPeriodicallyWithLatch(latch) {
-                powerLevelsContent = syncedSpace.asRoom()
-                        .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
-                        ?.content
-                        ?.toModel<PowerLevelsContent>()
-                powerLevelsContent != null
-            }
+        commonTestHelper.retryPeriodically {
+            powerLevelsContent = syncedSpace.asRoom()
+                    .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
+                    ?.content
+                    ?.toModel<PowerLevelsContent>()
+            powerLevelsContent != null
         }
         assertEquals("Space-rooms should be created with a power level for events_default of 100", 100, powerLevelsContent?.eventsDefault)
 
@@ -116,19 +110,13 @@ class SpaceCreationTest : InstrumentedTest {
 
         val roomName = "My Space"
         val topic = "A public space for test"
-        val spaceId: String
-        runBlocking {
-            spaceId = aliceSession.spaceService().createSpace(roomName, topic, null, true)
-            // wait a bit to let the summary update it self :/
-            delay(400)
-        }
+        val spaceId = aliceSession.spaceService().createSpace(roomName, topic, null, true)
+        // wait a bit to let the summary update it self :/
+        delay(400)
 
         // Try to join from bob, it's a public space no need to invite
 
-        val joinResult: JoinSpaceResult
-        runBlocking {
-            joinResult = bobSession.spaceService().joinSpace(spaceId)
-        }
+        val joinResult = bobSession.spaceService().joinSpace(spaceId)
 
         assertEquals(JoinSpaceResult.Success, joinResult)
 
@@ -152,43 +140,24 @@ class SpaceCreationTest : InstrumentedTest {
         val syncedSpace = aliceSession.spaceService().getSpace(spaceId)
 
         // create a room
-        var firstChild: String? = null
-        commonTestHelper.waitWithLatch {
-            firstChild = aliceSession.roomService().createRoom(CreateRoomParams().apply {
-                this.name = "FirstRoom"
-                this.topic = "Description of first room"
-                this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT
-            })
-            it.countDown()
-        }
+        val firstChild: String = aliceSession.roomService().createRoom(CreateRoomParams().apply {
+            this.name = "FirstRoom"
+            this.topic = "Description of first room"
+            this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT
+        })
 
-        commonTestHelper.waitWithLatch {
-            syncedSpace?.addChildren(firstChild!!, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "a", suggested = true)
-            it.countDown()
-        }
+        syncedSpace?.addChildren(firstChild, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "a", suggested = true)
 
-        var secondChild: String? = null
-        commonTestHelper.waitWithLatch {
-            secondChild = aliceSession.roomService().createRoom(CreateRoomParams().apply {
-                this.name = "SecondRoom"
-                this.topic = "Description of second room"
-                this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT
-            })
-            it.countDown()
-        }
+        val secondChild = aliceSession.roomService().createRoom(CreateRoomParams().apply {
+            this.name = "SecondRoom"
+            this.topic = "Description of second room"
+            this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT
+        })
 
-        commonTestHelper.waitWithLatch {
-            syncedSpace?.addChildren(secondChild!!, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "b", suggested = true)
-            it.countDown()
-        }
+        syncedSpace?.addChildren(secondChild, listOf(aliceSession.sessionParams.homeServerHost ?: ""), "b", suggested = true)
 
         // Try to join from bob, it's a public space no need to invite
-        var joinResult: JoinSpaceResult? = null
-        commonTestHelper.waitWithLatch {
-            joinResult = bobSession.spaceService().joinSpace(spaceId)
-            // wait a bit to let the summary update it self :/
-            it.countDown()
-        }
+        val joinResult = bobSession.spaceService().joinSpace(spaceId)
 
         assertEquals(JoinSpaceResult.Success, joinResult)
 
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt
index 18645fd6d9ce82cf5323a03a56db9d17140a56f2..abe9af5e3859b9c915f75c96c18dac9d72ff7a2f 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/space/SpaceHierarchyTest.kt
@@ -17,8 +17,6 @@
 package org.matrix.android.sdk.session.space
 
 import android.util.Log
-import androidx.lifecycle.Observer
-import kotlinx.coroutines.runBlocking
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertTrue
 import org.junit.FixMethodOrder
@@ -39,16 +37,17 @@ import org.matrix.android.sdk.api.session.getRoomSummary
 import org.matrix.android.sdk.api.session.room.getStateEvent
 import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
 import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry
-import org.matrix.android.sdk.api.session.room.model.RoomSummary
 import org.matrix.android.sdk.api.session.room.model.RoomType
 import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
 import org.matrix.android.sdk.api.session.room.model.create.RestrictedRoomPreset
 import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
 import org.matrix.android.sdk.api.session.room.powerlevels.Role
 import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
-import org.matrix.android.sdk.common.CommonTestHelper
 import org.matrix.android.sdk.common.CommonTestHelper.Companion.runSessionTest
 import org.matrix.android.sdk.common.SessionTestParams
+import org.matrix.android.sdk.common.first
+import org.matrix.android.sdk.common.onMain
+import org.matrix.android.sdk.common.waitFor
 
 @RunWith(JUnit4::class)
 @FixMethodOrder(MethodSorters.JVM)
@@ -60,40 +59,28 @@ class SpaceHierarchyTest : InstrumentedTest {
         val session = commonTestHelper.createAccount("John", SessionTestParams(true))
         val spaceName = "My Space"
         val topic = "A public space for test"
-        var spaceId = ""
-        commonTestHelper.runBlockingTest {
-            spaceId = session.spaceService().createSpace(spaceName, topic, null, true)
-        }
+        val spaceId = session.spaceService().createSpace(spaceName, topic, null, true)
 
         val syncedSpace = session.spaceService().getSpace(spaceId)
 
-        var roomId = ""
-        commonTestHelper.runBlockingTest {
-            roomId = session.roomService().createRoom(CreateRoomParams().apply { name = "General" })
-        }
+        val roomId = session.roomService().createRoom(CreateRoomParams().apply { name = "General" })
 
         val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
 
-        commonTestHelper.runBlockingTest {
-            syncedSpace!!.addChildren(roomId, viaServers, null, true)
-        }
+        syncedSpace!!.addChildren(roomId, viaServers, null, true)
 
-        commonTestHelper.runBlockingTest {
-            session.spaceService().setSpaceParent(roomId, spaceId, true, viaServers)
-        }
+        session.spaceService().setSpaceParent(roomId, spaceId, true, viaServers)
 
-        commonTestHelper.waitWithLatch { latch ->
-            commonTestHelper.retryPeriodicallyWithLatch(latch) {
-                val parents = session.getRoom(roomId)?.roomSummary()?.spaceParents
-                val canonicalParents = session.getRoom(roomId)?.roomSummary()?.spaceParents?.filter { it.canonical == true }
-                parents?.forEach {
-                    Log.d("## TEST", "parent : $it")
-                }
-                parents?.size == 1 &&
-                        parents.first().roomSummary?.name == spaceName &&
-                        canonicalParents?.size == 1 &&
-                        canonicalParents.first().roomSummary?.name == spaceName
+        commonTestHelper.retryPeriodically {
+            val parents = session.getRoom(roomId)?.roomSummary()?.spaceParents
+            val canonicalParents = session.getRoom(roomId)?.roomSummary()?.spaceParents?.filter { it.canonical == true }
+            parents?.forEach {
+                Log.d("## TEST", "parent : $it")
             }
+            parents?.size == 1 &&
+                    parents.first().roomSummary?.name == spaceName &&
+                    canonicalParents?.size == 1 &&
+                    canonicalParents.first().roomSummary?.name == spaceName
         }
     }
 
@@ -169,7 +156,6 @@ class SpaceHierarchyTest : InstrumentedTest {
         val session = commonTestHelper.createAccount("John", SessionTestParams(true))
 
         val spaceAInfo = createPublicSpace(
-                commonTestHelper,
                 session, "SpaceA",
                 listOf(
                         Triple("A1", true /*auto-join*/, true/*canonical*/),
@@ -178,7 +164,6 @@ class SpaceHierarchyTest : InstrumentedTest {
         )
 
         /* val spaceBInfo = */ createPublicSpace(
-            commonTestHelper,
             session, "SpaceB",
             listOf(
                     Triple("B1", true /*auto-join*/, true/*canonical*/),
@@ -188,7 +173,6 @@ class SpaceHierarchyTest : InstrumentedTest {
     )
 
         val spaceCInfo = createPublicSpace(
-                commonTestHelper,
                 session, "SpaceC",
                 listOf(
                         Triple("C1", true /*auto-join*/, true/*canonical*/),
@@ -199,22 +183,12 @@ class SpaceHierarchyTest : InstrumentedTest {
         // add C as a subspace of A
         val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId)
         val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
-        commonTestHelper.runBlockingTest {
-            spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
-            session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers)
-        }
+        spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
+        session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers)
 
         // Create orphan rooms
-
-        var orphan1 = ""
-        commonTestHelper.runBlockingTest {
-            orphan1 = session.roomService().createRoom(CreateRoomParams().apply { name = "O1" })
-        }
-
-        var orphan2 = ""
-        commonTestHelper.runBlockingTest {
-            orphan2 = session.roomService().createRoom(CreateRoomParams().apply { name = "O2" })
-        }
+        val orphan1 = session.roomService().createRoom(CreateRoomParams().apply { name = "O1" })
+        val orphan2 = session.roomService().createRoom(CreateRoomParams().apply { name = "O2" })
 
         val allRooms = session.roomService().getRoomSummaries(roomSummaryQueryParams { excludeType = listOf(RoomType.SPACE) })
 
@@ -235,15 +209,15 @@ class SpaceHierarchyTest : InstrumentedTest {
         assertTrue("A1 should be a grand child of A", aChildren.any { it.name == "C2" })
 
         // Add a non canonical child and check that it does not appear as orphan
-        commonTestHelper.runBlockingTest {
-            val a3 = session.roomService().createRoom(CreateRoomParams().apply { name = "A3" })
-            spaceA!!.addChildren(a3, viaServers, null, false)
-        }
+        val a3 = session.roomService().createRoom(CreateRoomParams().apply { name = "A3" })
+        spaceA.addChildren(a3, viaServers, null, false)
+
+        val orphansUpdate = session.roomService().onMain {
+            getRoomSummariesLive(roomSummaryQueryParams {
+                spaceFilter = SpaceFilter.OrphanRooms
+            })
+        }.first { it.size == 2 }
 
-        Thread.sleep(6_000)
-        val orphansUpdate = session.roomService().getRoomSummaries(roomSummaryQueryParams {
-            spaceFilter = SpaceFilter.OrphanRooms
-        })
         assertEquals("Unexpected number of orphan rooms ${orphansUpdate.map { it.name }}", 2, orphansUpdate.size)
     }
 
@@ -253,7 +227,6 @@ class SpaceHierarchyTest : InstrumentedTest {
         val session = commonTestHelper.createAccount("John", SessionTestParams(true))
 
         val spaceAInfo = createPublicSpace(
-                commonTestHelper,
                 session, "SpaceA",
                 listOf(
                         Triple("A1", true /*auto-join*/, true/*canonical*/),
@@ -262,7 +235,6 @@ class SpaceHierarchyTest : InstrumentedTest {
         )
 
         val spaceCInfo = createPublicSpace(
-                commonTestHelper,
                 session, "SpaceC",
                 listOf(
                         Triple("C1", true /*auto-join*/, true/*canonical*/),
@@ -273,16 +245,12 @@ class SpaceHierarchyTest : InstrumentedTest {
         // add C as a subspace of A
         val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId)
         val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
-        commonTestHelper.runBlockingTest {
-            spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
-            session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers)
-        }
+        spaceA!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
+        session.spaceService().setSpaceParent(spaceCInfo.spaceId, spaceAInfo.spaceId, true, viaServers)
 
         // add back A as subspace of C
-        commonTestHelper.runBlockingTest {
-            val spaceC = session.spaceService().getSpace(spaceCInfo.spaceId)
-            spaceC!!.addChildren(spaceAInfo.spaceId, viaServers, null, true)
-        }
+        val spaceC = session.spaceService().getSpace(spaceCInfo.spaceId)
+        spaceC!!.addChildren(spaceAInfo.spaceId, viaServers, null, true)
 
         // A -> C -> A
 
@@ -300,7 +268,6 @@ class SpaceHierarchyTest : InstrumentedTest {
         val session = commonTestHelper.createAccount("John", SessionTestParams(true))
 
         val spaceAInfo = createPublicSpace(
-                commonTestHelper,
                 session,
                 "SpaceA",
                 listOf(
@@ -310,7 +277,6 @@ class SpaceHierarchyTest : InstrumentedTest {
         )
 
         val spaceBInfo = createPublicSpace(
-                commonTestHelper,
                 session,
                 "SpaceB",
                 listOf(
@@ -323,13 +289,10 @@ class SpaceHierarchyTest : InstrumentedTest {
         // add B as a subspace of A
         val spaceA = session.spaceService().getSpace(spaceAInfo.spaceId)
         val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
-        commonTestHelper.runBlockingTest {
-            spaceA!!.addChildren(spaceBInfo.spaceId, viaServers, null, true)
-            session.spaceService().setSpaceParent(spaceBInfo.spaceId, spaceAInfo.spaceId, true, viaServers)
-        }
+        spaceA!!.addChildren(spaceBInfo.spaceId, viaServers, null, true)
+        session.spaceService().setSpaceParent(spaceBInfo.spaceId, spaceAInfo.spaceId, true, viaServers)
 
         val spaceCInfo = createPublicSpace(
-                commonTestHelper,
                 session,
                 "SpaceC",
                 listOf(
@@ -338,52 +301,39 @@ class SpaceHierarchyTest : InstrumentedTest {
                 )
         )
 
-        commonTestHelper.waitWithLatch { latch ->
-
-            val flatAChildren = session.roomService().getFlattenRoomSummaryChildrenOfLive(spaceAInfo.spaceId)
-            val childObserver = object : Observer<List<RoomSummary>> {
-                override fun onChanged(children: List<RoomSummary>?) {
-//                    Log.d("## TEST", "Space A flat children update : ${children?.map { it.name }}")
-                    System.out.println("## TEST | Space A flat children update : ${children?.map { it.name }}")
-                    if (children?.any { it.name == "C1" } == true && children.any { it.name == "C2" }) {
-                        // B1 has been added live!
-                        latch.countDown()
-                        flatAChildren.removeObserver(this)
+        val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId)
+        waitFor(
+                continueWhen = {
+                    session.roomService().onMain { getFlattenRoomSummaryChildrenOfLive(spaceAInfo.spaceId) }.first { children ->
+                        println("## TEST | Space A flat children update : ${children.map { it.name }}")
+                        children.any { it.name == "C1" } && children.any { it.name == "C2" }
                     }
+                },
+                action = {
+                    // add C as subspace of B
+                    spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
                 }
-            }
-
-            flatAChildren.observeForever(childObserver)
-
-            // add C as subspace of B
-            val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId)
-            spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
-
-            // C1 and C2 should be in flatten child of A now
-        }
+        )
+        // C1 and C2 should be in flatten child of A now
 
         // Test part one of the rooms
 
         val bRoomId = spaceBInfo.roomIds.first()
 
-        commonTestHelper.waitWithLatch { latch ->
-            val flatAChildren = session.roomService().getFlattenRoomSummaryChildrenOfLive(spaceAInfo.spaceId)
-            val childObserver = object : Observer<List<RoomSummary>> {
-                override fun onChanged(children: List<RoomSummary>?) {
-                    System.out.println("## TEST | Space A flat children update : ${children?.map { it.name }}")
-                    if (children?.any { it.roomId == bRoomId } == false) {
-                        // B1 has been added live!
-                        latch.countDown()
-                        flatAChildren.removeObserver(this)
+        waitFor(
+                continueWhen = {
+                    // The room should have disappear from flat children
+                    session.roomService().onMain { getFlattenRoomSummaryChildrenOfLive(spaceAInfo.spaceId) }.first { children ->
+                        println("## TEST | Space A flat children update : ${children.map { it.name }}")
+                        !children.any { it.roomId == bRoomId }
                     }
+                },
+                action = {
+                    // part from b room
+                    session.roomService().leaveRoom(bRoomId)
                 }
-            }
+        )
 
-            // The room should have disapear from flat children
-            flatAChildren.observeForever(childObserver)
-            // part from b room
-            session.roomService().leaveRoom(bRoomId)
-        }
         commonTestHelper.signOutAndClose(session)
     }
 
@@ -392,68 +342,57 @@ class SpaceHierarchyTest : InstrumentedTest {
             val roomIds: List<String>
     )
 
-    private fun createPublicSpace(
-            commonTestHelper: CommonTestHelper,
+    private suspend fun createPublicSpace(
             session: Session,
             spaceName: String,
             childInfo: List<Triple<String, Boolean, Boolean?>>
             /** Name, auto-join, canonical*/
     ): TestSpaceCreationResult {
-        var spaceId = ""
-        var roomIds: List<String> = emptyList()
-        commonTestHelper.runBlockingTest {
-            spaceId = session.spaceService().createSpace(spaceName, "Test Topic", null, true)
-            val syncedSpace = session.spaceService().getSpace(spaceId)
-            val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
-
-            roomIds = childInfo.map { entry ->
-                session.roomService().createRoom(CreateRoomParams().apply { name = entry.first })
-            }
-            roomIds.forEachIndexed { index, roomId ->
-                syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second)
-                val canonical = childInfo[index].third
-                if (canonical != null) {
-                    session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers)
-                }
+        val spaceId = session.spaceService().createSpace(spaceName, "Test Topic", null, true)
+        val syncedSpace = session.spaceService().getSpace(spaceId)
+        val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
+
+        val roomIds = childInfo.map { entry ->
+            session.roomService().createRoom(CreateRoomParams().apply { name = entry.first })
+        }
+        roomIds.forEachIndexed { index, roomId ->
+            syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second)
+            val canonical = childInfo[index].third
+            if (canonical != null) {
+                session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers)
             }
         }
         return TestSpaceCreationResult(spaceId, roomIds)
     }
 
-    private fun createPrivateSpace(
-            commonTestHelper: CommonTestHelper,
+    private suspend fun createPrivateSpace(
             session: Session,
             spaceName: String,
             childInfo: List<Triple<String, Boolean, Boolean?>>
             /** Name, auto-join, canonical*/
     ): TestSpaceCreationResult {
-        var spaceId = ""
-        var roomIds: List<String> = emptyList()
-        commonTestHelper.runBlockingTest {
-            spaceId = session.spaceService().createSpace(spaceName, "My Private Space", null, false)
-            val syncedSpace = session.spaceService().getSpace(spaceId)
-            val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
-            roomIds =
-                    childInfo.map { entry ->
-                        val homeServerCapabilities = session
-                                .homeServerCapabilitiesService()
-                                .getHomeServerCapabilities()
-                        session.roomService().createRoom(CreateRoomParams().apply {
-                            name = entry.first
-                            this.featurePreset = RestrictedRoomPreset(
-                                    homeServerCapabilities,
-                                    listOf(
-                                            RoomJoinRulesAllowEntry.restrictedToRoom(spaceId)
-                                    )
-                            )
-                        })
-                    }
-            roomIds.forEachIndexed { index, roomId ->
-                syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second)
-                val canonical = childInfo[index].third
-                if (canonical != null) {
-                    session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers)
-                }
+        val spaceId = session.spaceService().createSpace(spaceName, "My Private Space", null, false)
+        val syncedSpace = session.spaceService().getSpace(spaceId)
+        val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
+        val roomIds = childInfo.map { entry ->
+            val homeServerCapabilities = session
+                    .homeServerCapabilitiesService()
+                    .getHomeServerCapabilities()
+            session.roomService().createRoom(CreateRoomParams().apply {
+                name = entry.first
+                this.featurePreset = RestrictedRoomPreset(
+                        homeServerCapabilities,
+                        listOf(
+                                RoomJoinRulesAllowEntry.restrictedToRoom(spaceId)
+                        )
+                )
+            })
+        }
+        roomIds.forEachIndexed { index, roomId ->
+            syncedSpace!!.addChildren(roomId, viaServers, null, childInfo[index].second)
+            val canonical = childInfo[index].third
+            if (canonical != null) {
+                session.spaceService().setSpaceParent(roomId, spaceId, canonical, viaServers)
             }
         }
         return TestSpaceCreationResult(spaceId, roomIds)
@@ -464,7 +403,6 @@ class SpaceHierarchyTest : InstrumentedTest {
         val session = commonTestHelper.createAccount("John", SessionTestParams(true))
 
         /* val spaceAInfo = */ createPublicSpace(
-            commonTestHelper,
             session, "SpaceA",
             listOf(
                     Triple("A1", true /*auto-join*/, true/*canonical*/),
@@ -473,7 +411,6 @@ class SpaceHierarchyTest : InstrumentedTest {
     )
 
         val spaceBInfo = createPublicSpace(
-                commonTestHelper,
                 session, "SpaceB",
                 listOf(
                         Triple("B1", true /*auto-join*/, true/*canonical*/),
@@ -483,7 +420,6 @@ class SpaceHierarchyTest : InstrumentedTest {
         )
 
         val spaceCInfo = createPublicSpace(
-                commonTestHelper,
                 session, "SpaceC",
                 listOf(
                         Triple("C1", true /*auto-join*/, true/*canonical*/),
@@ -494,10 +430,8 @@ class SpaceHierarchyTest : InstrumentedTest {
         val viaServers = listOf(session.sessionParams.homeServerHost ?: "")
 
         // add C as subspace of B
-        runBlocking {
-            val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId)
-            spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
-        }
+        val spaceB = session.spaceService().getSpace(spaceBInfo.spaceId)
+        spaceB!!.addChildren(spaceCInfo.spaceId, viaServers, null, true)
 
 //        Thread.sleep(4_000)
         // + A
@@ -507,11 +441,9 @@ class SpaceHierarchyTest : InstrumentedTest {
         //   + C
         //     + c1, c2
 
-        commonTestHelper.waitWithLatch { latch ->
-            commonTestHelper.retryPeriodicallyWithLatch(latch) {
-                val rootSpaces = commonTestHelper.runBlockingTest { session.spaceService().getRootSpaceSummaries() }
-                rootSpaces.size == 2
-            }
+        commonTestHelper.retryPeriodically {
+            val rootSpaces = session.spaceService().getRootSpaceSummaries()
+            rootSpaces.size == 2
         }
     }
 
@@ -521,7 +453,6 @@ class SpaceHierarchyTest : InstrumentedTest {
         val bobSession = commonTestHelper.createAccount("Bib", SessionTestParams(true))
 
         val spaceAInfo = createPrivateSpace(
-                commonTestHelper,
                 aliceSession, "Private Space A",
                 listOf(
                         Triple("General", true /*suggested*/, true/*canonical*/),
@@ -529,85 +460,58 @@ class SpaceHierarchyTest : InstrumentedTest {
                 )
         )
 
-        commonTestHelper.runBlockingTest {
-            aliceSession.getRoom(spaceAInfo.spaceId)!!.membershipService().invite(bobSession.myUserId, null)
-        }
+        aliceSession.getRoom(spaceAInfo.spaceId)!!.membershipService().invite(bobSession.myUserId, null)
 
-        commonTestHelper.runBlockingTest {
-            bobSession.roomService().joinRoom(spaceAInfo.spaceId, null, emptyList())
-        }
+        bobSession.roomService().joinRoom(spaceAInfo.spaceId, null, emptyList())
 
-        var bobRoomId = ""
-        commonTestHelper.runBlockingTest {
-            bobRoomId = bobSession.roomService().createRoom(CreateRoomParams().apply { name = "A Bob Room" })
-            bobSession.getRoom(bobRoomId)!!.membershipService().invite(aliceSession.myUserId)
-        }
+        val bobRoomId = bobSession.roomService().createRoom(CreateRoomParams().apply { name = "A Bob Room" })
+        bobSession.getRoom(bobRoomId)!!.membershipService().invite(aliceSession.myUserId)
 
-        commonTestHelper.runBlockingTest {
-            aliceSession.roomService().joinRoom(bobRoomId)
-        }
+        aliceSession.roomService().joinRoom(bobRoomId)
 
-        commonTestHelper.waitWithLatch { latch ->
-            commonTestHelper.retryPeriodicallyWithLatch(latch) {
-                aliceSession.getRoomSummary(bobRoomId)?.membership?.isActive() == true
-            }
+        commonTestHelper.retryPeriodically {
+            aliceSession.getRoomSummary(bobRoomId)?.membership?.isActive() == true
         }
 
-        commonTestHelper.runBlockingTest {
-            bobSession.spaceService().setSpaceParent(bobRoomId, spaceAInfo.spaceId, false, listOf(bobSession.sessionParams.homeServerHost ?: ""))
-        }
+        bobSession.spaceService().setSpaceParent(bobRoomId, spaceAInfo.spaceId, false, listOf(bobSession.sessionParams.homeServerHost ?: ""))
 
-        commonTestHelper.waitWithLatch { latch ->
-            commonTestHelper.retryPeriodicallyWithLatch(latch) {
-                val stateEvent = aliceSession.getRoom(bobRoomId)!!.getStateEvent(EventType.STATE_SPACE_PARENT, QueryStringValue.Equals(spaceAInfo.spaceId))
-                stateEvent != null
-            }
+        commonTestHelper.retryPeriodically {
+            val stateEvent = aliceSession.getRoom(bobRoomId)!!.getStateEvent(EventType.STATE_SPACE_PARENT, QueryStringValue.Equals(spaceAInfo.spaceId))
+            stateEvent != null
         }
 
         // This should be an invalid space parent relation, because no opposite child and bob is not admin of the space
-        commonTestHelper.runBlockingTest {
-            // we can see the state event
-            // but it is not valid and room is not in hierarchy
-            assertTrue("Bob Room should not be listed as a child of the space", aliceSession.getRoomSummary(bobRoomId)?.flattenParentIds?.isEmpty() == true)
-        }
+        // we can see the state event
+        // but it is not valid and room is not in hierarchy
+        assertTrue("Bob Room should not be listed as a child of the space", aliceSession.getRoomSummary(bobRoomId)?.flattenParentIds?.isEmpty() == true)
 
         // Let's now try to make alice admin of the room
 
-        commonTestHelper.waitWithLatch {
-            val room = bobSession.getRoom(bobRoomId)!!
-            val currentPLContent = room
-                    .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
-                    ?.content
-                    .toModel<PowerLevelsContent>()
+        val room = bobSession.getRoom(bobRoomId)!!
+        val currentPLContent = room
+                .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
+                ?.content
+                .toModel<PowerLevelsContent>()
 
-            val newPowerLevelsContent = currentPLContent
-                    ?.setUserPowerLevel(aliceSession.myUserId, Role.Admin.value)
-                    ?.toContent()
+        val newPowerLevelsContent = currentPLContent
+                ?.setUserPowerLevel(aliceSession.myUserId, Role.Admin.value)
+                ?.toContent()
 
-            room.stateService().sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, stateKey = "", newPowerLevelsContent!!)
-            it.countDown()
-        }
+        room.stateService().sendStateEvent(EventType.STATE_ROOM_POWER_LEVELS, stateKey = "", newPowerLevelsContent!!)
 
-        commonTestHelper.waitWithLatch { latch ->
-            commonTestHelper.retryPeriodicallyWithLatch(latch) {
-                val powerLevelsHelper = aliceSession.getRoom(bobRoomId)!!
-                        .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
-                        ?.content
-                        ?.toModel<PowerLevelsContent>()
-                        ?.let { PowerLevelsHelper(it) }
-                powerLevelsHelper!!.isUserAllowedToSend(aliceSession.myUserId, true, EventType.STATE_SPACE_PARENT)
-            }
+        commonTestHelper.retryPeriodically {
+            val powerLevelsHelper = aliceSession.getRoom(bobRoomId)!!
+                    .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
+                    ?.content
+                    ?.toModel<PowerLevelsContent>()
+                    ?.let { PowerLevelsHelper(it) }
+            powerLevelsHelper!!.isUserAllowedToSend(aliceSession.myUserId, true, EventType.STATE_SPACE_PARENT)
         }
 
-        commonTestHelper.waitWithLatch {
-            aliceSession.spaceService().setSpaceParent(bobRoomId, spaceAInfo.spaceId, false, listOf(bobSession.sessionParams.homeServerHost ?: ""))
-            it.countDown()
-        }
+        aliceSession.spaceService().setSpaceParent(bobRoomId, spaceAInfo.spaceId, false, listOf(bobSession.sessionParams.homeServerHost ?: ""))
 
-        commonTestHelper.waitWithLatch { latch ->
-            commonTestHelper.retryPeriodicallyWithLatch(latch) {
-                bobSession.getRoomSummary(bobRoomId)?.flattenParentIds?.contains(spaceAInfo.spaceId) == true
-            }
+        commonTestHelper.retryPeriodically {
+            bobSession.getRoomSummary(bobRoomId)?.flattenParentIds?.contains(spaceAInfo.spaceId) == true
         }
     }
 
@@ -616,7 +520,6 @@ class SpaceHierarchyTest : InstrumentedTest {
         val aliceSession = commonTestHelper.createAccount("Alice", SessionTestParams(true))
 
         val spaceAInfo = createPublicSpace(
-                commonTestHelper,
                 aliceSession, "SpaceA",
                 listOf(
                         Triple("A1", true /*auto-join*/, true/*canonical*/),
@@ -625,7 +528,6 @@ class SpaceHierarchyTest : InstrumentedTest {
         )
 
         val spaceBInfo = createPublicSpace(
-                commonTestHelper,
                 aliceSession, "SpaceB",
                 listOf(
                         Triple("B1", true /*auto-join*/, true/*canonical*/),
@@ -641,51 +543,39 @@ class SpaceHierarchyTest : InstrumentedTest {
 
         val spaceA = aliceSession.spaceService().getSpace(spaceAInfo.spaceId)
         val spaceB = aliceSession.spaceService().getSpace(spaceBInfo.spaceId)
-        commonTestHelper.runBlockingTest {
-            spaceA!!.addChildren(B1roomId, viaServers, null, true)
-        }
+        spaceA!!.addChildren(B1roomId, viaServers, null, true)
 
-        commonTestHelper.waitWithLatch { latch ->
-            commonTestHelper.retryPeriodicallyWithLatch(latch) {
-                val roomSummary = aliceSession.getRoomSummary(B1roomId)
-                roomSummary != null &&
-                        roomSummary.directParentNames.size == 2 &&
-                        roomSummary.directParentNames.contains(spaceA!!.spaceSummary()!!.name) &&
-                        roomSummary.directParentNames.contains(spaceB!!.spaceSummary()!!.name)
-            }
+        commonTestHelper.retryPeriodically {
+            val roomSummary = aliceSession.getRoomSummary(B1roomId)
+            roomSummary != null &&
+                    roomSummary.directParentNames.size == 2 &&
+                    roomSummary.directParentNames.contains(spaceA.spaceSummary()!!.name) &&
+                    roomSummary.directParentNames.contains(spaceB!!.spaceSummary()!!.name)
         }
 
-        commonTestHelper.waitWithLatch { latch ->
-            commonTestHelper.retryPeriodicallyWithLatch(latch) {
-                val roomSummary = aliceSession.getRoomSummary(spaceAInfo.roomIds.first())
-                roomSummary != null &&
-                        roomSummary.directParentNames.size == 1 &&
-                        roomSummary.directParentNames.contains(spaceA!!.spaceSummary()!!.name)
-            }
+        commonTestHelper.retryPeriodically {
+            val roomSummary = aliceSession.getRoomSummary(spaceAInfo.roomIds.first())
+            roomSummary != null &&
+                    roomSummary.directParentNames.size == 1 &&
+                    roomSummary.directParentNames.contains(spaceA.spaceSummary()!!.name)
         }
 
         val newAName = "FooBar"
-        commonTestHelper.runBlockingTest {
-            spaceA!!.asRoom().stateService().updateName(newAName)
-        }
+        spaceA.asRoom().stateService().updateName(newAName)
 
-        commonTestHelper.waitWithLatch { latch ->
-            commonTestHelper.retryPeriodicallyWithLatch(latch) {
-                val roomSummary = aliceSession.getRoomSummary(B1roomId)
-                roomSummary != null &&
-                        roomSummary.directParentNames.size == 2 &&
-                        roomSummary.directParentNames.contains(newAName) &&
-                        roomSummary.directParentNames.contains(spaceB!!.spaceSummary()!!.name)
-            }
+        commonTestHelper.retryPeriodically {
+            val roomSummary = aliceSession.getRoomSummary(B1roomId)
+            roomSummary != null &&
+                    roomSummary.directParentNames.size == 2 &&
+                    roomSummary.directParentNames.contains(newAName) &&
+                    roomSummary.directParentNames.contains(spaceB!!.spaceSummary()!!.name)
         }
 
-        commonTestHelper.waitWithLatch { latch ->
-            commonTestHelper.retryPeriodicallyWithLatch(latch) {
-                val roomSummary = aliceSession.getRoomSummary(spaceAInfo.roomIds.first())
-                roomSummary != null &&
-                        roomSummary.directParentNames.size == 1 &&
-                        roomSummary.directParentNames.contains(newAName)
-            }
+        commonTestHelper.retryPeriodically {
+            val roomSummary = aliceSession.getRoomSummary(spaceAInfo.roomIds.first())
+            roomSummary != null &&
+                    roomSummary.directParentNames.size == 1 &&
+                    roomSummary.directParentNames.contains(newAName)
         }
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt
index 893e90fb3efbafe59c44b5125522b082e63aadbf..711956361797bf47068b030c31c272ee08c55f10 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixConfiguration.kt
@@ -70,4 +70,8 @@ data class MatrixConfiguration(
          * List of network interceptors, they will be added when building an OkHttp client.
          */
         val networkInterceptors: List<Interceptor> = emptyList(),
+        /**
+         * Sync configuration.
+         */
+        val syncConfig: SyncConfig = SyncConfig(),
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/SyncConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/SyncConfig.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a9753e2407f7dd334b5dda410e72a3068bc4189c
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/SyncConfig.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api
+
+data class SyncConfig(
+        /**
+         * Time to keep sync connection alive for before making another request in milliseconds.
+         */
+        val longPollTimeout: Long = 30_000L,
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtils.kt
index e701e0f3ba0c4505c73a9633a6bbf78515b84e7e..234a8eee9861f5577720c55fd60c3a39241ac4f6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtils.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtils.kt
@@ -131,11 +131,10 @@ class SecretStoringUtils @Inject constructor(
      *
      * The secret is encrypted using the following method: AES/GCM/NoPadding
      */
-    @SuppressLint("NewApi")
     @Throws(Exception::class)
     fun securelyStoreBytes(secret: ByteArray, keyAlias: String): ByteArray {
         return when {
-            buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> encryptBytesM(secret, keyAlias)
+            buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.M) -> encryptBytesM(secret, keyAlias)
             else -> encryptBytes(secret, keyAlias)
         }
     }
@@ -156,10 +155,9 @@ class SecretStoringUtils @Inject constructor(
         }
     }
 
-    @SuppressLint("NewApi")
     fun securelyStoreObject(any: Any, keyAlias: String, output: OutputStream) {
         when {
-            buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> saveSecureObjectM(keyAlias, output, any)
+            buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.M) -> saveSecureObjectM(keyAlias, output, any)
             else -> saveSecureObject(keyAlias, output, any)
         }
     }
@@ -189,7 +187,6 @@ class SecretStoringUtils @Inject constructor(
         return cipher
     }
 
-    @SuppressLint("NewApi")
     @RequiresApi(Build.VERSION_CODES.M)
     private fun getOrGenerateSymmetricKeyForAliasM(alias: String): SecretKey {
         val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry)
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 e0e662c7893f994c058c40bc20202b3254dd4a86..d2aa8020e8257abbd48807a884a9450afa7c02c5 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
@@ -61,6 +61,8 @@ interface CryptoService {
 
     fun isRoomBlacklistUnverifiedDevices(roomId: String?): Boolean
 
+    fun getLiveBlockUnverifiedDevices(roomId: String): LiveData<Boolean>
+
     fun setWarnOnUnknownDevices(warn: Boolean)
 
     fun setDeviceVerification(trustLevel: DeviceTrustLevel, userId: String, deviceId: String)
@@ -77,6 +79,8 @@ interface CryptoService {
 
     fun setGlobalBlacklistUnverifiedDevices(block: Boolean)
 
+    fun getLiveGlobalCryptoConfig(): LiveData<GlobalCryptoConfig>
+
     /**
      * Enable or disable key gossiping.
      * Default is true.
@@ -100,7 +104,7 @@ interface CryptoService {
      */
     fun isShareKeysOnInviteEnabled(): Boolean
 
-    fun setRoomUnBlacklistUnverifiedDevices(roomId: String)
+    fun setRoomUnBlockUnverifiedDevices(roomId: String)
 
     fun getDeviceTrackingStatus(userId: String): Int
 
@@ -112,7 +116,7 @@ interface CryptoService {
 
     suspend fun exportRoomKeys(password: String): ByteArray
 
-    fun setRoomBlacklistUnverifiedDevices(roomId: String)
+    fun setRoomBlockUnverifiedDevices(roomId: String, block: Boolean)
 
     fun getCryptoDeviceInfo(userId: String, deviceId: String?): CryptoDeviceInfo?
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/GlobalCryptoConfig.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/GlobalCryptoConfig.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6405652a6846acc0037743a42fec2258d18362aa
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/GlobalCryptoConfig.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.crypto
+
+data class GlobalCryptoConfig(
+        val globalBlockUnverifiedDevices: Boolean,
+        val globalEnableKeyGossiping: Boolean,
+        val enableKeyForwardingOnInvite: Boolean,
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/MXCrossSigningInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/MXCrossSigningInfo.kt
index 9604decd62928492065c808c8cb6caf2a6c41431..30a2cfd719261636942e9157cacfd119c272e551 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/MXCrossSigningInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/MXCrossSigningInfo.kt
@@ -18,7 +18,8 @@ package org.matrix.android.sdk.api.session.crypto.crosssigning
 
 data class MXCrossSigningInfo(
         val userId: String,
-        val crossSigningKeys: List<CryptoCrossSigningKey>
+        val crossSigningKeys: List<CryptoCrossSigningKey>,
+        val wasTrustedOnce: Boolean
 ) {
 
     fun isTrusted(): Boolean = masterKey()?.trustLevel?.isVerified() == true &&
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/DeviceInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/DeviceInfo.kt
index b144069b9947b49f5eeeb10959eab881c53f763d..500d0160028fc9bedf2cc0834dbed461305f12f8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/DeviceInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/DeviceInfo.kt
@@ -52,9 +52,17 @@ data class DeviceInfo(
          * The last ip address.
          */
         @Json(name = "last_seen_ip")
-        val lastSeenIp: String? = null
+        val lastSeenIp: String? = null,
+
+        @Json(name = "org.matrix.msc3852.last_seen_user_agent")
+        val unstableLastSeenUserAgent: String? = null,
+
+        @Json(name = "last_seen_user_agent")
+        val lastSeenUserAgent: String? = null,
 ) : DatedObject {
 
     override val date: Long
         get() = lastSeenTs ?: 0
+
+    fun getBestLastSeenUserAgent() = lastSeenUserAgent ?: unstableLastSeenUserAgent
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/UserVerificationLevel.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/UserVerificationLevel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e3c7057b6b6aaa109b0d1636a76fa8d3e3645f8e
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/model/UserVerificationLevel.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.crypto.model
+
+enum class UserVerificationLevel {
+
+    VERIFIED_ALL_DEVICES_TRUSTED,
+
+    VERIFIED_WITH_DEVICES_UNTRUSTED,
+
+    UNVERIFIED_BUT_WAS_PREVIOUSLY,
+
+    WAS_NEVER_VERIFIED,
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt
index 84c25776e74ca5bea65cc617b434e809d1dc0a69..3ad4f3a87f08c7c03113541477989c7ec65670da 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt
@@ -128,4 +128,17 @@ object EventType {
                 type == CALL_REJECT ||
                 type == CALL_REPLACES
     }
+
+    fun isVerificationEvent(type: String): Boolean {
+        return when (type) {
+            KEY_VERIFICATION_START,
+            KEY_VERIFICATION_ACCEPT,
+            KEY_VERIFICATION_KEY,
+            KEY_VERIFICATION_MAC,
+            KEY_VERIFICATION_CANCEL,
+            KEY_VERIFICATION_DONE,
+            KEY_VERIFICATION_READY -> true
+            else -> false
+        }
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/BuildVersionSdkIntProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/BuildVersionSdkIntProvider.kt
index 900a2e237f175da98dfd44f7ccae9745c54e0a10..c8c328c92ca570f73e8a1314ab82c54e62e8d789 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/BuildVersionSdkIntProvider.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/BuildVersionSdkIntProvider.kt
@@ -16,6 +16,8 @@
 
 package org.matrix.android.sdk.api.util
 
+import androidx.annotation.ChecksSdkIntAtLeast
+
 interface BuildVersionSdkIntProvider {
     /**
      * Return the current version of the Android SDK.
@@ -26,9 +28,13 @@ interface BuildVersionSdkIntProvider {
      * Checks the if the current OS version is equal or greater than [version].
      * @return A `non-null` result if true, `null` otherwise.
      */
+    @ChecksSdkIntAtLeast(parameter = 0, lambda = 1)
     fun <T> whenAtLeast(version: Int, result: () -> T): T? {
         return if (get() >= version) {
             result()
         } else null
     }
+
+    @ChecksSdkIntAtLeast(parameter = 0)
+    fun isAtLeast(version: Int) = get() >= version
 }
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 322f297ac30ae61625f2df17ededcf13e89ac431..9c3e0ba1c588be1261e842632180ace421220cb2 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
@@ -40,6 +40,7 @@ import org.matrix.android.sdk.api.failure.Failure
 import org.matrix.android.sdk.api.listeners.ProgressListener
 import org.matrix.android.sdk.api.logger.LoggerTag
 import org.matrix.android.sdk.api.session.crypto.CryptoService
+import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig
 import org.matrix.android.sdk.api.session.crypto.MXCryptoError
 import org.matrix.android.sdk.api.session.crypto.NewSessionListener
 import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest
@@ -855,10 +856,12 @@ internal class DefaultCryptoService @Inject constructor(
      * Handle a key event.
      *
      * @param event the key event.
+     * @param acceptUnrequested, if true it will force to accept unrequested keys.
      */
     private fun onRoomKeyEvent(event: Event, acceptUnrequested: Boolean = false) {
         val roomKeyContent = event.getDecryptedContent().toModel<RoomKeyContent>() ?: return
-        Timber.tag(loggerTag.value).i("onRoomKeyEvent(forceAccept:$acceptUnrequested) from: ${event.senderId} type<${event.getClearType()}> , sessionId<${roomKeyContent.sessionId}>")
+        Timber.tag(loggerTag.value)
+                .i("onRoomKeyEvent(f:$acceptUnrequested) from: ${event.senderId} type<${event.getClearType()}> , session<${roomKeyContent.sessionId}>")
         if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) {
             Timber.tag(loggerTag.value).e("onRoomKeyEvent() : missing fields")
             return
@@ -1161,6 +1164,10 @@ internal class DefaultCryptoService @Inject constructor(
         return cryptoStore.getGlobalBlacklistUnverifiedDevices()
     }
 
+    override fun getLiveGlobalCryptoConfig(): LiveData<GlobalCryptoConfig> {
+        return cryptoStore.getLiveGlobalCryptoConfig()
+    }
+
     /**
      * Tells whether the client should encrypt messages only for the verified devices
      * in this room.
@@ -1169,39 +1176,28 @@ internal class DefaultCryptoService @Inject constructor(
      * @param roomId the room id
      * @return true if the client should encrypt messages only for the verified devices.
      */
-// TODO add this info in CryptoRoomEntity?
     override fun isRoomBlacklistUnverifiedDevices(roomId: String?): Boolean {
-        return roomId?.let { cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(it) }
+        return roomId?.let { cryptoStore.getBlockUnverifiedDevices(roomId) }
                 ?: false
     }
 
     /**
-     * Manages the room black-listing for unverified devices.
+     * A live status regarding sharing keys for unverified devices in this room.
      *
-     * @param roomId the room id
-     * @param add true to add the room id to the list, false to remove it.
+     * @return Live status
      */
-    private fun setRoomBlacklistUnverifiedDevices(roomId: String, add: Boolean) {
-        val roomIds = cryptoStore.getRoomsListBlacklistUnverifiedDevices().toMutableList()
-
-        if (add) {
-            if (roomId !in roomIds) {
-                roomIds.add(roomId)
-            }
-        } else {
-            roomIds.remove(roomId)
-        }
-
-        cryptoStore.setRoomsListBlacklistUnverifiedDevices(roomIds)
+    override fun getLiveBlockUnverifiedDevices(roomId: String): LiveData<Boolean> {
+        return cryptoStore.getLiveBlockUnverifiedDevices(roomId)
     }
 
     /**
      * Add this room to the ones which don't encrypt messages to unverified devices.
      *
      * @param roomId the room id
+     * @param block if true will block sending keys to unverified devices
      */
-    override fun setRoomBlacklistUnverifiedDevices(roomId: String) {
-        setRoomBlacklistUnverifiedDevices(roomId, true)
+    override fun setRoomBlockUnverifiedDevices(roomId: String, block: Boolean) {
+        cryptoStore.blockUnverifiedDevicesInRoom(roomId, block)
     }
 
     /**
@@ -1209,8 +1205,8 @@ internal class DefaultCryptoService @Inject constructor(
      *
      * @param roomId the room id
      */
-    override fun setRoomUnBlacklistUnverifiedDevices(roomId: String) {
-        setRoomBlacklistUnverifiedDevices(roomId, false)
+    override fun setRoomUnBlockUnverifiedDevices(roomId: String) {
+        setRoomBlockUnverifiedDevices(roomId, false)
     }
 
     /**
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 48b46523048c2d24a00c6c8f67f8767108c4534b..faadf339e970c148689658136fe0e562b3c6c6b8 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
@@ -603,6 +603,7 @@ internal class MXOlmDevice @Inject constructor(
      * @param keysClaimed Other keys the sender claims.
      * @param exportFormat true if the megolm keys are in export format
      * @param sharedHistory MSC3061, this key is sharable on invite
+     * @param trusted True if the key is coming from a trusted source
      * @return true if the operation succeeds.
      */
     fun addInboundGroupSession(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt
index e2ddd5d19f9598e6d9b31c1f1ec32c9144e41473..d9fd5f10ce60994985bb5bdf54e8c16a93e663cb 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/IMXDecrypting.kt
@@ -41,6 +41,7 @@ internal interface IMXDecrypting {
      *
      * @param event the key event.
      * @param defaultKeysBackupService the keys backup service
+     * @param forceAccept the keys backup service
      */
     fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService, forceAccept: Boolean = false) {}
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt
index 5354cbff3bc52753981d3ed25d4fe22ba2987bc2..64bd52dd3b0dbabafc4f4e29b8c3b2d790336ff2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/MXMegolmDecryption.kt
@@ -187,6 +187,7 @@ internal class MXMegolmDecryption(
      *
      * @param event the key event.
      * @param defaultKeysBackupService the keys backup service
+     * @param forceAccept if true will force to accept the forwarded key
      */
     override fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService, forceAccept: Boolean) {
         Timber.tag(loggerTag.value).v("onRoomKeyEvent(${event.getSenderKey()})")
@@ -274,7 +275,8 @@ internal class MXMegolmDecryption(
 
             if (!shouldAcceptForward) {
                 Timber.tag(loggerTag.value)
-                        .w("Ignoring forwarded_room_key device:$eventSenderKey, ownVerified:{$isOwnDevice&&$isDeviceVerified}, fromInitiator:$isFromSessionInitiator")
+                        .w("Ignoring forwarded_room_key device:$eventSenderKey, ownVerified:{$isOwnDevice&&$isDeviceVerified}," +
+                                " fromInitiator:$isFromSessionInitiator")
                 return
             }
         } else {
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 fca6fab66c27bbdc0d402dabf5561b4e62d3514c..0b7af9f4d760029baf0d243538c61e62178b412c 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
@@ -31,6 +31,8 @@ import org.matrix.android.sdk.api.session.events.model.Content
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.content.RoomKeyWithHeldContent
 import org.matrix.android.sdk.api.session.events.model.content.WithHeldCode
+import org.matrix.android.sdk.api.session.room.model.message.MessageContent
+import org.matrix.android.sdk.api.session.room.model.message.MessageType
 import org.matrix.android.sdk.internal.crypto.DeviceListManager
 import org.matrix.android.sdk.internal.crypto.InboundGroupSessionHolder
 import org.matrix.android.sdk.internal.crypto.MXOlmDevice
@@ -92,7 +94,18 @@ internal class MXMegolmEncryption(
     ): Content {
         val ts = clock.epochMillis()
         Timber.tag(loggerTag.value).v("encryptEventContent : getDevicesInRoom")
-        val devices = getDevicesInRoom(userIds)
+
+        /**
+         * When using in-room messages and the room has encryption enabled,
+         * clients should ensure that encryption does not hinder the verification.
+         * For example, if the verification messages are encrypted, clients must ensure that all the recipient’s
+         * unverified devices receive the keys necessary to decrypt the messages,
+         * even if they would normally not be given the keys to decrypt messages in the room.
+         */
+        val shouldSendToUnverified = isVerificationEvent(eventType, eventContent)
+
+        val devices = getDevicesInRoom(userIds, forceDistributeToUnverified = shouldSendToUnverified)
+
         Timber.tag(loggerTag.value).d("encrypt event in room=$roomId - devices count in room ${devices.allowedDevices.toDebugCount()}")
         Timber.tag(loggerTag.value).v("encryptEventContent ${clock.epochMillis() - ts}: getDevicesInRoom ${devices.allowedDevices.toDebugString()}")
         val outboundSession = ensureOutboundSession(devices.allowedDevices)
@@ -107,6 +120,11 @@ internal class MXMegolmEncryption(
                 }
     }
 
+    private fun isVerificationEvent(eventType: String, eventContent: Content) =
+            EventType.isVerificationEvent(eventType) ||
+                    (eventType == EventType.MESSAGE &&
+                            eventContent.get(MessageContent.MSG_TYPE_JSON_KEY) == MessageType.MSGTYPE_VERIFICATION_REQUEST)
+
     private fun notifyWithheldForSession(devices: MXUsersDevicesMap<WithHeldCode>, outboundSession: MXOutboundSessionInfo) {
         // offload to computation thread
         cryptoCoroutineScope.launch(coroutineDispatchers.computation) {
@@ -416,15 +434,17 @@ internal class MXMegolmEncryption(
      * This method must be called in getDecryptingThreadHandler() thread.
      *
      * @param userIds the user ids whose devices must be checked.
+     * @param forceDistributeToUnverified If true the unverified devices will be included in valid recipients even if
+     * such devices are blocked in crypto settings
      */
-    private suspend fun getDevicesInRoom(userIds: List<String>): DeviceInRoomInfo {
+    private suspend fun getDevicesInRoom(userIds: List<String>, forceDistributeToUnverified: Boolean = false): DeviceInRoomInfo {
         // We are happy to use a cached version here: we assume that if we already
         // have a list of the user's devices, then we already share an e2e room
         // with them, which means that they will have announced any new devices via
         // an m.new_device.
         val keys = deviceListManager.downloadKeys(userIds, false)
         val encryptToVerifiedDevicesOnly = cryptoStore.getGlobalBlacklistUnverifiedDevices() ||
-                cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(roomId)
+                cryptoStore.getBlockUnverifiedDevices(roomId)
 
         val devicesInRoom = DeviceInRoomInfo()
         val unknownDevices = MXUsersDevicesMap<CryptoDeviceInfo>()
@@ -444,7 +464,7 @@ internal class MXMegolmEncryption(
                     continue
                 }
 
-                if (!deviceInfo.isVerified && encryptToVerifiedDevicesOnly) {
+                if (!deviceInfo.isVerified && encryptToVerifiedDevicesOnly && !forceDistributeToUnverified) {
                     devicesInRoom.withHeldDevices.setObject(userId, deviceId, WithHeldCode.UNVERIFIED)
                     continue
                 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/UnRequestedForwardManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/UnRequestedForwardManager.kt
index 42629b617e6dd8e1aeb59f239e1e7a03d374c4a4..9235cd2abf5e46edf192beb59f8d8c12d5bb08d0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/UnRequestedForwardManager.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/megolm/UnRequestedForwardManager.kt
@@ -31,7 +31,7 @@ import java.util.concurrent.Executors
 import javax.inject.Inject
 import kotlin.math.abs
 
-private val INVITE_VALIDITY_TIME_WINDOW_MILLIS = 10 * 60_000
+private const val INVITE_VALIDITY_TIME_WINDOW_MILLIS = 10 * 60_000
 
 @SessionScope
 internal class UnRequestedForwardManager @Inject constructor(
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 d405bdce273ce9ecc4753f6132736a5fca2c9ef2..f4796155c62d5cb397fc1bb1136569baaf8701a9 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
@@ -60,7 +60,7 @@ import javax.inject.Inject
 
 @SessionScope
 internal class DefaultCrossSigningService @Inject constructor(
-        @UserId private val userId: String,
+        @UserId private val myUserId: String,
         @SessionId private val sessionId: String,
         private val cryptoStore: IMXCryptoStore,
         private val deviceListManager: DeviceListManager,
@@ -127,7 +127,7 @@ internal class DefaultCrossSigningService @Inject constructor(
                 }
 
                 // Recover local trust in case private key are there?
-                setUserKeysAsTrusted(userId, checkUserTrust(userId).isVerified())
+                setUserKeysAsTrusted(myUserId, checkUserTrust(myUserId).isVerified())
             }
         } catch (e: Throwable) {
             // Mmm this kind of a big issue
@@ -167,9 +167,13 @@ internal class DefaultCrossSigningService @Inject constructor(
                 }
 
                 override fun onSuccess(data: InitializeCrossSigningTask.Result) {
-                    val crossSigningInfo = MXCrossSigningInfo(userId, listOf(data.masterKeyInfo, data.userKeyInfo, data.selfSignedKeyInfo))
+                    val crossSigningInfo = MXCrossSigningInfo(
+                            myUserId,
+                            listOf(data.masterKeyInfo, data.userKeyInfo, data.selfSignedKeyInfo),
+                            true
+                    )
                     cryptoStore.setMyCrossSigningInfo(crossSigningInfo)
-                    setUserKeysAsTrusted(userId, true)
+                    setUserKeysAsTrusted(myUserId, true)
                     cryptoStore.storePrivateKeysInfo(data.masterKeyPK, data.userKeyPK, data.selfSigningKeyPK)
                     crossSigningOlm.masterPkSigning = OlmPkSigning().apply { initWithSeed(data.masterKeyPK.fromBase64()) }
                     crossSigningOlm.userPkSigning = OlmPkSigning().apply { initWithSeed(data.userKeyPK.fromBase64()) }
@@ -266,7 +270,7 @@ internal class DefaultCrossSigningService @Inject constructor(
             uskKeyPrivateKey: String?,
             sskPrivateKey: String?
     ): UserTrustResult {
-        val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return UserTrustResult.CrossSigningNotConfigured(userId)
+        val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return UserTrustResult.CrossSigningNotConfigured(myUserId)
 
         var masterKeyIsTrusted = false
         var userKeyIsTrusted = false
@@ -330,7 +334,7 @@ internal class DefaultCrossSigningService @Inject constructor(
             val checkSelfTrust = checkSelfTrust()
             if (checkSelfTrust.isVerified()) {
                 cryptoStore.storePrivateKeysInfo(masterKeyPrivateKey, uskKeyPrivateKey, sskPrivateKey)
-                setUserKeysAsTrusted(userId, true)
+                setUserKeysAsTrusted(myUserId, true)
             }
             return checkSelfTrust
         }
@@ -351,7 +355,7 @@ internal class DefaultCrossSigningService @Inject constructor(
      * .
      */
     override fun isUserTrusted(otherUserId: String): Boolean {
-        return cryptoStore.getCrossSigningInfo(userId)?.isTrusted() == true
+        return cryptoStore.getCrossSigningInfo(otherUserId)?.isTrusted() == true
     }
 
     override fun isCrossSigningVerified(): Boolean {
@@ -363,7 +367,7 @@ internal class DefaultCrossSigningService @Inject constructor(
      */
     override fun checkUserTrust(otherUserId: String): UserTrustResult {
         Timber.v("## CrossSigning  checkUserTrust for $otherUserId")
-        if (otherUserId == userId) {
+        if (otherUserId == myUserId) {
             return checkSelfTrust()
         }
         // I trust a user if I trust his master key
@@ -371,16 +375,14 @@ internal class DefaultCrossSigningService @Inject constructor(
         // TODO what if the master key is signed by a device key that i have verified
 
         // First let's get my user key
-        val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId)
-
-        checkOtherMSKTrusted(myCrossSigningInfo, cryptoStore.getCrossSigningInfo(otherUserId))
+        val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(myUserId)
 
-        return UserTrustResult.Success
+        return checkOtherMSKTrusted(myCrossSigningInfo, cryptoStore.getCrossSigningInfo(otherUserId))
     }
 
     fun checkOtherMSKTrusted(myCrossSigningInfo: MXCrossSigningInfo?, otherInfo: MXCrossSigningInfo?): UserTrustResult {
         val myUserKey = myCrossSigningInfo?.userKey()
-                ?: return UserTrustResult.CrossSigningNotConfigured(userId)
+                ?: return UserTrustResult.CrossSigningNotConfigured(myUserId)
 
         if (!myCrossSigningInfo.isTrusted()) {
             return UserTrustResult.KeysNotTrusted(myCrossSigningInfo)
@@ -391,7 +393,7 @@ internal class DefaultCrossSigningService @Inject constructor(
                 ?: return UserTrustResult.UnknownCrossSignatureInfo(otherInfo?.userId ?: "")
 
         val masterKeySignaturesMadeByMyUserKey = otherMasterKey.signatures
-                ?.get(userId) // Signatures made by me
+                ?.get(myUserId) // Signatures made by me
                 ?.get("ed25519:${myUserKey.unpaddedBase64PublicKey}")
 
         if (masterKeySignaturesMadeByMyUserKey.isNullOrBlank()) {
@@ -417,9 +419,9 @@ internal class DefaultCrossSigningService @Inject constructor(
         // 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 myCrossSigningInfo = cryptoStore.getCrossSigningInfo(myUserId)
 
-        return checkSelfTrust(myCrossSigningInfo, cryptoStore.getUserDeviceList(userId))
+        return checkSelfTrust(myCrossSigningInfo, cryptoStore.getUserDeviceList(myUserId))
     }
 
     fun checkSelfTrust(myCrossSigningInfo: MXCrossSigningInfo?, myDevices: List<CryptoDeviceInfo>?): UserTrustResult {
@@ -429,7 +431,7 @@ internal class DefaultCrossSigningService @Inject constructor(
 //        val myCrossSigningInfo = cryptoStore.getCrossSigningInfo(userId)
 
         val myMasterKey = myCrossSigningInfo?.masterKey()
-                ?: return UserTrustResult.CrossSigningNotConfigured(userId)
+                ?: return UserTrustResult.CrossSigningNotConfigured(myUserId)
 
         // Is the master key trusted
         // 1) check if I know the private key
@@ -453,7 +455,7 @@ internal class DefaultCrossSigningService @Inject constructor(
             olmPkSigning?.releaseSigning()
         } else {
             // Maybe it's signed by a locally trusted device?
-            myMasterKey.signatures?.get(userId)?.forEach { (key, value) ->
+            myMasterKey.signatures?.get(myUserId)?.forEach { (key, value) ->
                 val potentialDeviceId = key.removePrefix("ed25519:")
                 val potentialDevice = myDevices?.firstOrNull { it.deviceId == potentialDeviceId } // cryptoStore.getUserDevice(userId, potentialDeviceId)
                 if (potentialDevice != null && potentialDevice.isVerified) {
@@ -475,14 +477,14 @@ internal class DefaultCrossSigningService @Inject constructor(
         }
 
         val myUserKey = myCrossSigningInfo.userKey()
-                ?: return UserTrustResult.CrossSigningNotConfigured(userId)
+                ?: return UserTrustResult.CrossSigningNotConfigured(myUserId)
 
         val userKeySignaturesMadeByMyMasterKey = myUserKey.signatures
-                ?.get(userId) // Signatures made by me
+                ?.get(myUserId) // Signatures made by me
                 ?.get("ed25519:${myMasterKey.unpaddedBase64PublicKey}")
 
         if (userKeySignaturesMadeByMyMasterKey.isNullOrBlank()) {
-            Timber.d("## CrossSigning  checkUserTrust false for $userId, USK not signed by MSK")
+            Timber.d("## CrossSigning  checkUserTrust false for $myUserId, USK not signed by MSK")
             return UserTrustResult.KeyNotSigned(myUserKey)
         }
 
@@ -498,14 +500,14 @@ internal class DefaultCrossSigningService @Inject constructor(
         }
 
         val mySSKey = myCrossSigningInfo.selfSigningKey()
-                ?: return UserTrustResult.CrossSigningNotConfigured(userId)
+                ?: return UserTrustResult.CrossSigningNotConfigured(myUserId)
 
         val ssKeySignaturesMadeByMyMasterKey = mySSKey.signatures
-                ?.get(userId) // Signatures made by me
+                ?.get(myUserId) // Signatures made by me
                 ?.get("ed25519:${myMasterKey.unpaddedBase64PublicKey}")
 
         if (ssKeySignaturesMadeByMyMasterKey.isNullOrBlank()) {
-            Timber.d("## CrossSigning  checkUserTrust false for $userId, SSK not signed by MSK")
+            Timber.d("## CrossSigning  checkUserTrust false for $myUserId, SSK not signed by MSK")
             return UserTrustResult.KeyNotSigned(mySSKey)
         }
 
@@ -555,14 +557,14 @@ internal class DefaultCrossSigningService @Inject constructor(
 
     override fun trustUser(otherUserId: String, callback: MatrixCallback<Unit>) {
         cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
-            Timber.d("## CrossSigning - Mark user $userId as trusted ")
+            Timber.d("## CrossSigning - Mark user $otherUserId as trusted ")
             // We should have this user keys
             val otherMasterKeys = getUserCrossSigningKeys(otherUserId)?.masterKey()
             if (otherMasterKeys == null) {
                 callback.onFailure(Throwable("## CrossSigning - Other master signing key is not known"))
                 return@launch
             }
-            val myKeys = getUserCrossSigningKeys(userId)
+            val myKeys = getUserCrossSigningKeys(myUserId)
             if (myKeys == null) {
                 callback.onFailure(Throwable("## CrossSigning - CrossSigning is not setup for this account"))
                 return@launch
@@ -586,16 +588,22 @@ internal class DefaultCrossSigningService @Inject constructor(
             }
 
             cryptoStore.setUserKeysAsTrusted(otherUserId, true)
-            // TODO update local copy with new signature directly here? kind of local echo of trust?
 
-            Timber.d("## CrossSigning - Upload signature of $userId MSK signed by USK")
+            Timber.d("## CrossSigning - Upload signature of $otherUserId MSK signed by USK")
             val uploadQuery = UploadSignatureQueryBuilder()
-                    .withSigningKeyInfo(otherMasterKeys.copyForSignature(userId, userPubKey, newSignature))
+                    .withSigningKeyInfo(otherMasterKeys.copyForSignature(myUserId, userPubKey, newSignature))
                     .build()
             uploadSignaturesTask.configureWith(UploadSignaturesTask.Params(uploadQuery)) {
                 this.executionThread = TaskThread.CRYPTO
                 this.callback = callback
             }.executeBy(taskExecutor)
+
+            // Local echo for device cross trust, to avoid having to wait for a notification of key change
+            cryptoStore.getUserDeviceList(otherUserId)?.forEach { device ->
+                val updatedTrust = checkDeviceTrust(device.userId, device.deviceId, device.trustLevel?.isLocallyVerified() ?: false)
+                Timber.v("## CrossSigning - update trust for device ${device.deviceId} of user $otherUserId , verified=$updatedTrust")
+                cryptoStore.setDeviceTrust(device.userId, device.deviceId, updatedTrust.isCrossSignedVerified(), updatedTrust.isLocallyVerified())
+            }
         }
     }
 
@@ -604,20 +612,20 @@ internal class DefaultCrossSigningService @Inject constructor(
             cryptoStore.markMyMasterKeyAsLocallyTrusted(true)
             checkSelfTrust()
             // re-verify all trusts
-            onUsersDeviceUpdate(listOf(userId))
+            onUsersDeviceUpdate(listOf(myUserId))
         }
     }
 
     override fun trustDevice(deviceId: String, callback: MatrixCallback<Unit>) {
         cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
             // This device should be yours
-            val device = cryptoStore.getUserDevice(userId, deviceId)
+            val device = cryptoStore.getUserDevice(myUserId, deviceId)
             if (device == null) {
                 callback.onFailure(IllegalArgumentException("This device [$deviceId] is not known, or not yours"))
                 return@launch
             }
 
-            val myKeys = getUserCrossSigningKeys(userId)
+            val myKeys = getUserCrossSigningKeys(myUserId)
             if (myKeys == null) {
                 callback.onFailure(Throwable("CrossSigning is not setup for this account"))
                 return@launch
@@ -639,7 +647,7 @@ internal class DefaultCrossSigningService @Inject constructor(
             }
             val toUpload = device.copy(
                     signatures = mapOf(
-                            userId
+                            myUserId
                                     to
                                     mapOf(
                                             "ed25519:$ssPubKey" to newSignature
@@ -661,8 +669,8 @@ internal class DefaultCrossSigningService @Inject constructor(
         val otherDevice = cryptoStore.getUserDevice(otherUserId, otherDeviceId)
                 ?: return DeviceTrustResult.UnknownDevice(otherDeviceId)
 
-        val myKeys = getUserCrossSigningKeys(userId)
-                ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(userId))
+        val myKeys = getUserCrossSigningKeys(myUserId)
+                ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(myUserId))
 
         if (!myKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(myKeys))
 
@@ -717,7 +725,7 @@ internal class DefaultCrossSigningService @Inject constructor(
 
     fun checkDeviceTrust(myKeys: MXCrossSigningInfo?, otherKeys: MXCrossSigningInfo?, otherDevice: CryptoDeviceInfo): DeviceTrustResult {
         val locallyTrusted = otherDevice.trustLevel?.isLocallyVerified()
-        myKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(userId))
+        myKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(myUserId))
 
         if (!myKeys.isTrusted()) return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.KeysNotTrusted(myKeys))
 
@@ -805,7 +813,7 @@ internal class DefaultCrossSigningService @Inject constructor(
         cryptoStore.setUserKeysAsTrusted(otherUserId, trusted)
         // If it's me, recheck trust of all users and devices?
         val users = ArrayList<String>()
-        if (otherUserId == userId && currentTrust != trusted) {
+        if (otherUserId == myUserId && currentTrust != trusted) {
             // notify key requester
             outgoingKeyRequestManager.onSelfCrossSigningTrustChanged(trusted)
             cryptoStore.updateUsersTrust {
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 6d845ec59e9d6728ba79d86c115a8c01c11758f5..fffc6707d73b6f80de58f3506f1b046fb4f5f867 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
@@ -161,6 +161,7 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
             // i have all the new trusts, update DB
             trusts.forEach {
                 val verified = it.value?.isVerified() == true
+                Timber.v("[$myUserId] ## CrossSigning - Updating user trust: ${it.key} to $verified")
                 updateCrossSigningKeysTrust(cryptoRealm, it.key, verified)
             }
 
@@ -259,21 +260,27 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
         cryptoRealm.where(CrossSigningInfoEntity::class.java)
                 .equalTo(CrossSigningInfoEntityFields.USER_ID, userId)
                 .findFirst()
-                ?.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) {
-                            info.trustLevelEntity = cryptoRealm.createObject(TrustLevelEntity::class.java).also {
-                                it.locallyVerified = verified
-                                it.crossSignedVerified = verified
+                ?.let { userKeyInfo ->
+                    userKeyInfo
+                            .crossSigningKeys
+                            .forEach { key ->
+                                // optimization to avoid trigger updates when there is no change..
+                                if (key.trustLevelEntity?.isVerified() != verified) {
+                                    Timber.d("## CrossSigning - Trust change for $userId : $verified")
+                                    val level = key.trustLevelEntity
+                                    if (level == null) {
+                                        key.trustLevelEntity = cryptoRealm.createObject(TrustLevelEntity::class.java).also {
+                                            it.locallyVerified = verified
+                                            it.crossSignedVerified = verified
+                                        }
+                                    } else {
+                                        level.locallyVerified = verified
+                                        level.crossSignedVerified = verified
+                                    }
+                                }
                             }
-                        } else {
-                            level.locallyVerified = verified
-                            level.crossSignedVerified = verified
-                        }
+                    if (verified) {
+                        userKeyInfo.wasUserVerifiedOnce = true
                     }
                 }
     }
@@ -299,8 +306,18 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
                     getCrossSigningInfo(cryptoRealm, userId)?.isTrusted() == true
                 }
 
+        val resetTrust = listToCheck
+                .filter { userId ->
+                    val crossSigningInfo = getCrossSigningInfo(cryptoRealm, userId)
+                    crossSigningInfo?.isTrusted() != true && crossSigningInfo?.wasTrustedOnce == true
+                }
+
         return if (allTrustedUserIds.isEmpty()) {
-            RoomEncryptionTrustLevel.Default
+            if (resetTrust.isEmpty()) {
+                RoomEncryptionTrustLevel.Default
+            } else {
+                RoomEncryptionTrustLevel.Warning
+            }
         } else {
             // If one of the verified user as an untrusted device -> warning
             // If all devices of all verified users are trusted -> green
@@ -327,11 +344,15 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
                         if (hasWarning) {
                             RoomEncryptionTrustLevel.Warning
                         } else {
-                            if (listToCheck.size == allTrustedUserIds.size) {
-                                // all users are trusted and all devices are verified
-                                RoomEncryptionTrustLevel.Trusted
+                            if (resetTrust.isEmpty()) {
+                                if (listToCheck.size == allTrustedUserIds.size) {
+                                    // all users are trusted and all devices are verified
+                                    RoomEncryptionTrustLevel.Trusted
+                                } else {
+                                    RoomEncryptionTrustLevel.Default
+                                }
                             } else {
-                                RoomEncryptionTrustLevel.Default
+                                RoomEncryptionTrustLevel.Warning
                             }
                         }
                     }
@@ -344,7 +365,8 @@ internal class UpdateTrustWorker(context: Context, params: WorkerParameters, ses
                 userId = userId,
                 crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull {
                     crossSigningKeysMapper.map(userId, it)
-                }
+                },
+                wasTrustedOnce = xsignInfo.wasUserVerifiedOnce
         )
     }
 
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 56eba2524901878d04092919be5ed1c8e67e3f77..21e33423657ec8e4f0973c63eea0b1777dc3a5e5 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
@@ -18,6 +18,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.GlobalCryptoConfig
 import org.matrix.android.sdk.api.session.crypto.NewSessionListener
 import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest
 import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState
@@ -120,11 +121,26 @@ internal interface IMXCryptoStore {
     fun getRoomsListBlacklistUnverifiedDevices(): List<String>
 
     /**
-     * Updates the rooms ids list in which the messages are not encrypted for the unverified devices.
+     * A live status regarding sharing keys for unverified devices in this room.
      *
-     * @param roomIds the room ids list
+     * @return Live status
      */
-    fun setRoomsListBlacklistUnverifiedDevices(roomIds: List<String>)
+    fun getLiveBlockUnverifiedDevices(roomId: String): LiveData<Boolean>
+
+    /**
+     * Tell if unverified devices should be blacklisted when sending keys.
+     *
+     * @return true if should not send keys to unverified devices
+     */
+    fun getBlockUnverifiedDevices(roomId: String): Boolean
+
+    /**
+     * Define if encryption keys should be sent to unverified devices in this room.
+     *
+     * @param roomId the roomId
+     * @param block if true will not send keys to unverified devices
+     */
+    fun blockUnverifiedDevicesInRoom(roomId: String, block: Boolean)
 
     /**
      * Get the current keys backup version.
@@ -516,6 +532,9 @@ internal interface IMXCryptoStore {
     fun getCrossSigningPrivateKeys(): PrivateKeysInfo?
     fun getLiveCrossSigningPrivateKeys(): LiveData<Optional<PrivateKeysInfo>>
 
+    fun getGlobalCryptoConfig(): GlobalCryptoConfig
+    fun getLiveGlobalCryptoConfig(): LiveData<GlobalCryptoConfig>
+
     fun saveBackupRecoveryKey(recoveryKey: String?, version: String?)
     fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo?
 
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 3b8fa4cacdee6d382bed60099502578f90133a7c..e97cf437c68836363a5e0d5ce2637d9703e740c3 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
@@ -29,6 +29,7 @@ import io.realm.kotlin.where
 import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
 import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.logger.LoggerTag
+import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig
 import org.matrix.android.sdk.api.session.crypto.NewSessionListener
 import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest
 import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState
@@ -445,6 +446,38 @@ internal class RealmCryptoStore @Inject constructor(
         }
     }
 
+    override fun getGlobalCryptoConfig(): GlobalCryptoConfig {
+        return doWithRealm(realmConfiguration) { realm ->
+            realm.where<CryptoMetadataEntity>().findFirst()
+                    ?.let {
+                        GlobalCryptoConfig(
+                                globalBlockUnverifiedDevices = it.globalBlacklistUnverifiedDevices,
+                                globalEnableKeyGossiping = it.globalEnableKeyGossiping,
+                                enableKeyForwardingOnInvite = it.enableKeyForwardingOnInvite
+                        )
+                    } ?: GlobalCryptoConfig(false, false, false)
+        }
+    }
+
+    override fun getLiveGlobalCryptoConfig(): LiveData<GlobalCryptoConfig> {
+        val liveData = monarchy.findAllMappedWithChanges(
+                { realm: Realm ->
+                    realm
+                            .where<CryptoMetadataEntity>()
+                },
+                {
+                    GlobalCryptoConfig(
+                            globalBlockUnverifiedDevices = it.globalBlacklistUnverifiedDevices,
+                            globalEnableKeyGossiping = it.globalEnableKeyGossiping,
+                            enableKeyForwardingOnInvite = it.enableKeyForwardingOnInvite
+                    )
+                }
+        )
+        return Transformations.map(liveData) {
+            it.firstOrNull() ?: GlobalCryptoConfig(false, false, false)
+        }
+    }
+
     override fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) {
         Timber.v("## CRYPTO | *** storePrivateKeysInfo ${msk != null}, ${usk != null}, ${ssk != null}")
         doRealmTransaction(realmConfiguration) { realm ->
@@ -1053,25 +1086,6 @@ internal class RealmCryptoStore @Inject constructor(
         } ?: false
     }
 
-    override fun setRoomsListBlacklistUnverifiedDevices(roomIds: List<String>) {
-        doRealmTransaction(realmConfiguration) {
-            // Reset all
-            it.where<CryptoRoomEntity>()
-                    .findAll()
-                    .forEach { room ->
-                        room.blacklistUnverifiedDevices = false
-                    }
-
-            // Enable those in the list
-            it.where<CryptoRoomEntity>()
-                    .`in`(CryptoRoomEntityFields.ROOM_ID, roomIds.toTypedArray())
-                    .findAll()
-                    .forEach { room ->
-                        room.blacklistUnverifiedDevices = true
-                    }
-        }
-    }
-
     override fun getRoomsListBlacklistUnverifiedDevices(): List<String> {
         return doWithRealm(realmConfiguration) {
             it.where<CryptoRoomEntity>()
@@ -1083,6 +1097,37 @@ internal class RealmCryptoStore @Inject constructor(
         }
     }
 
+    override fun getLiveBlockUnverifiedDevices(roomId: String): LiveData<Boolean> {
+        val liveData = monarchy.findAllMappedWithChanges(
+                { realm: Realm ->
+                    realm.where<CryptoRoomEntity>()
+                            .equalTo(CryptoRoomEntityFields.ROOM_ID, roomId)
+                },
+                {
+                    it.blacklistUnverifiedDevices
+                }
+        )
+        return Transformations.map(liveData) {
+            it.firstOrNull() ?: false
+        }
+    }
+
+    override fun getBlockUnverifiedDevices(roomId: String): Boolean {
+        return doWithRealm(realmConfiguration) { realm ->
+            realm.where<CryptoRoomEntity>()
+                    .equalTo(CryptoRoomEntityFields.ROOM_ID, roomId)
+                    .findFirst()
+                    ?.blacklistUnverifiedDevices ?: false
+        }
+    }
+
+    override fun blockUnverifiedDevicesInRoom(roomId: String, block: Boolean) {
+        doRealmTransaction(realmConfiguration) { realm ->
+            CryptoRoomEntity.getById(realm, roomId)
+                    ?.blacklistUnverifiedDevices = block
+        }
+    }
+
     override fun getDeviceTrackingStatuses(): Map<String, Int> {
         return doWithRealm(realmConfiguration) {
             it.where<UserEntity>()
@@ -1611,7 +1656,8 @@ internal class RealmCryptoStore @Inject constructor(
                 userId = userId,
                 crossSigningKeys = xsignInfo.crossSigningKeys.mapNotNull {
                     crossSigningKeysMapper.map(userId, it)
-                }
+                },
+                wasTrustedOnce = xsignInfo.wasUserVerifiedOnce
         )
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt
index 426d50a54fc997c1398e1d25874974b23d2c8d3d..de2b74308dd278883fe4effe3682ba10d150d74a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt
@@ -35,6 +35,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo
 import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo016
 import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo017
 import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo018
+import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo019
 import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
 import org.matrix.android.sdk.internal.util.time.Clock
 import javax.inject.Inject
@@ -49,7 +50,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(
         private val clock: Clock,
 ) : MatrixRealmMigration(
         dbName = "Crypto",
-        schemaVersion = 18L,
+        schemaVersion = 19L,
 ) {
     /**
      * Forces all RealmCryptoStoreMigration instances to be equal.
@@ -77,5 +78,6 @@ internal class RealmCryptoStoreMigration @Inject constructor(
         if (oldVersion < 16) MigrateCryptoTo016(realm).perform()
         if (oldVersion < 17) MigrateCryptoTo017(realm).perform()
         if (oldVersion < 18) MigrateCryptoTo018(realm).perform()
+        if (oldVersion < 19) MigrateCryptoTo019(realm).perform()
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo019.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo019.kt
new file mode 100644
index 0000000000000000000000000000000000000000..9d2eb60a600879508134c866f982897eb54e06e2
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo019.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2022 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.store.db.migration
+
+import io.realm.DynamicRealm
+import io.realm.DynamicRealmObject
+import org.matrix.android.sdk.api.session.crypto.crosssigning.KeyUsage
+import org.matrix.android.sdk.internal.crypto.store.db.model.CrossSigningInfoEntityFields
+import org.matrix.android.sdk.internal.crypto.store.db.model.KeyInfoEntityFields
+import org.matrix.android.sdk.internal.crypto.store.db.model.TrustLevelEntityFields
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+/**
+ * This migration is adding support for trusted flags on megolm sessions.
+ * We can't really assert the trust of existing keys, so for the sake of simplicity we are going to
+ * mark existing keys as safe.
+ * This migration can take long depending on the account
+ */
+internal class MigrateCryptoTo019(realm: DynamicRealm) : RealmMigrator(realm, 18) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.get("CrossSigningInfoEntity")
+                ?.addField(CrossSigningInfoEntityFields.WAS_USER_VERIFIED_ONCE, Boolean::class.java)
+                ?.transform { dynamicObject ->
+
+                    val knowKeys = dynamicObject.getList(CrossSigningInfoEntityFields.CROSS_SIGNING_KEYS.`$`)
+                    val msk = knowKeys.firstOrNull {
+                        it.getList(KeyInfoEntityFields.USAGES.`$`, String::class.java).orEmpty().contains(KeyUsage.MASTER.value)
+                    }
+                    val ssk = knowKeys.firstOrNull {
+                        it.getList(KeyInfoEntityFields.USAGES.`$`, String::class.java).orEmpty().contains(KeyUsage.SELF_SIGNING.value)
+                    }
+                    val isTrusted = isDynamicKeyInfoTrusted(msk?.get<DynamicRealmObject>(KeyInfoEntityFields.TRUST_LEVEL_ENTITY.`$`)) &&
+                            isDynamicKeyInfoTrusted(ssk?.get<DynamicRealmObject>(KeyInfoEntityFields.TRUST_LEVEL_ENTITY.`$`))
+
+                    dynamicObject.setBoolean(CrossSigningInfoEntityFields.WAS_USER_VERIFIED_ONCE, isTrusted)
+                }
+    }
+
+    private fun isDynamicKeyInfoTrusted(keyInfo: DynamicRealmObject?): Boolean {
+        if (keyInfo == null) return false
+        return !keyInfo.isNull(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED) && keyInfo.getBoolean(TrustLevelEntityFields.CROSS_SIGNED_VERIFIED) &&
+                !keyInfo.isNull(TrustLevelEntityFields.LOCALLY_VERIFIED) && keyInfo.getBoolean(TrustLevelEntityFields.LOCALLY_VERIFIED)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CrossSigningInfoEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CrossSigningInfoEntity.kt
index 5aba9bb9ba627b82efb756e7367a2a65a164288d..033b7662c53efeef468b8ffcc08aa597bdbc23bd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CrossSigningInfoEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/CrossSigningInfoEntity.kt
@@ -25,6 +25,7 @@ import org.matrix.android.sdk.internal.extensions.clearWith
 internal open class CrossSigningInfoEntity(
         @PrimaryKey
         var userId: String? = null,
+        var wasUserVerifiedOnce: Boolean = false,
         var crossSigningKeys: RealmList<KeyInfoEntity> = RealmList()
 ) : RealmObject() {
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ComputeUserAgentUseCase.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ComputeUserAgentUseCase.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6eb4d5b1042263b359c87a41481674ea967e2b26
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ComputeUserAgentUseCase.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.network
+
+import android.content.Context
+import android.os.Build
+import org.matrix.android.sdk.BuildConfig
+import org.matrix.android.sdk.api.extensions.tryOrNull
+import javax.inject.Inject
+
+class ComputeUserAgentUseCase @Inject constructor(
+        private val context: Context,
+) {
+
+    /**
+     * Create an user agent with the application version.
+     * Ex: Element/1.5.0 (Xiaomi Mi 9T; Android 11; RKQ1.200826.002; Flavour GooglePlay; MatrixAndroidSdk2 1.5.0)
+     *
+     * @param flavorDescription the flavor description
+     */
+    fun execute(flavorDescription: String): String {
+        val appPackageName = context.applicationContext.packageName
+        val pm = context.packageManager
+
+        val appName = tryOrNull { pm.getApplicationLabel(pm.getApplicationInfo(appPackageName, 0)).toString() }
+                ?.takeIf {
+                    it.matches("\\A\\p{ASCII}*\\z".toRegex())
+                }
+                ?: run {
+                    // Use appPackageName instead of appName if appName is null or contains any non-ASCII character
+                    appPackageName
+                }
+        val appVersion = tryOrNull { pm.getPackageInfo(context.applicationContext.packageName, 0).versionName } ?: FALLBACK_APP_VERSION
+
+        val deviceManufacturer = Build.MANUFACTURER
+        val deviceModel = Build.MODEL
+        val androidVersion = Build.VERSION.RELEASE
+        val deviceBuildId = Build.DISPLAY
+        val matrixSdkVersion = BuildConfig.SDK_VERSION
+
+        return buildString {
+            append(appName)
+            append("/")
+            append(appVersion)
+            append(" (")
+            append(deviceManufacturer)
+            append(" ")
+            append(deviceModel)
+            append("; ")
+            append("Android ")
+            append(androidVersion)
+            append("; ")
+            append(deviceBuildId)
+            append("; ")
+            append("Flavour ")
+            append(flavorDescription)
+            append("; ")
+            append("MatrixAndroidSdk2 ")
+            append(matrixSdkVersion)
+            append(")")
+        }
+    }
+
+    companion object {
+        const val FALLBACK_APP_VERSION = "0.0.0"
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentHolder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentHolder.kt
index 28d96dfce7363f2ed58402109467c959557bf862..4e8326127777b83bc203f1797ee843516ffbb0f6 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentHolder.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/UserAgentHolder.kt
@@ -16,73 +16,20 @@
 
 package org.matrix.android.sdk.internal.network
 
-import android.content.Context
-import org.matrix.android.sdk.BuildConfig
 import org.matrix.android.sdk.api.MatrixConfiguration
 import org.matrix.android.sdk.internal.di.MatrixScope
-import timber.log.Timber
 import javax.inject.Inject
 
 @MatrixScope
 internal class UserAgentHolder @Inject constructor(
-        private val context: Context,
-        matrixConfiguration: MatrixConfiguration
+        matrixConfiguration: MatrixConfiguration,
+        computeUserAgentUseCase: ComputeUserAgentUseCase,
 ) {
 
     var userAgent: String = ""
         private set
 
     init {
-        setApplicationFlavor(matrixConfiguration.applicationFlavor)
-    }
-
-    /**
-     * Create an user agent with the application version.
-     * Ex: Element/1.0.0 (Linux; U; Android 6.0.1; SM-A510F Build/MMB29; Flavour GPlay; MatrixAndroidSdk2 1.0)
-     *
-     * @param flavorDescription the flavor description
-     */
-    private fun setApplicationFlavor(flavorDescription: String) {
-        var appName = ""
-        var appVersion = ""
-
-        try {
-            val appPackageName = context.applicationContext.packageName
-            val pm = context.packageManager
-            val appInfo = pm.getApplicationInfo(appPackageName, 0)
-            appName = pm.getApplicationLabel(appInfo).toString()
-
-            val pkgInfo = pm.getPackageInfo(context.applicationContext.packageName, 0)
-            appVersion = pkgInfo.versionName ?: ""
-
-            // Use appPackageName instead of appName if appName contains any non-ASCII character
-            if (!appName.matches("\\A\\p{ASCII}*\\z".toRegex())) {
-                appName = appPackageName
-            }
-        } catch (e: Exception) {
-            Timber.e(e, "## initUserAgent() : failed")
-        }
-
-        val systemUserAgent = System.getProperty("http.agent")
-
-        // cannot retrieve the application version
-        if (appName.isEmpty() || appVersion.isEmpty()) {
-            if (null == systemUserAgent) {
-                userAgent = "Java" + System.getProperty("java.version")
-            }
-            return
-        }
-
-        // if there is no user agent or cannot parse it
-        if (null == systemUserAgent || systemUserAgent.lastIndexOf(")") == -1 || !systemUserAgent.contains("(")) {
-            userAgent = (appName + "/" + appVersion + " ( Flavour " + flavorDescription +
-                    "; MatrixAndroidSdk2 " + BuildConfig.SDK_VERSION + ")")
-        } else {
-            // update
-            userAgent = appName + "/" + appVersion + " " +
-                    systemUserAgent.substring(systemUserAgent.indexOf("("), systemUserAgent.lastIndexOf(")") - 1) +
-                    "; Flavour " + flavorDescription +
-                    "; MatrixAndroidSdk2 " + BuildConfig.SDK_VERSION + ")"
-        }
+        userAgent = computeUserAgentUseCase.execute(matrixConfiguration.applicationFlavor)
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorThread.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorThread.kt
deleted file mode 100644
index 55363a725103e5cb93183c96cec1dc20aab8ec16..0000000000000000000000000000000000000000
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/queue/EventSenderProcessorThread.kt
+++ /dev/null
@@ -1,235 +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.session.room.send.queue
-
-import kotlinx.coroutines.CancellationException
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
-import org.matrix.android.sdk.api.auth.data.SessionParams
-import org.matrix.android.sdk.api.auth.data.sessionId
-import org.matrix.android.sdk.api.extensions.tryOrNull
-import org.matrix.android.sdk.api.failure.Failure
-import org.matrix.android.sdk.api.failure.isLimitExceededError
-import org.matrix.android.sdk.api.failure.isTokenError
-import org.matrix.android.sdk.api.session.Session
-import org.matrix.android.sdk.api.session.crypto.CryptoService
-import org.matrix.android.sdk.api.session.events.model.Event
-import org.matrix.android.sdk.api.session.sync.SyncState
-import org.matrix.android.sdk.api.util.Cancelable
-import org.matrix.android.sdk.internal.session.SessionScope
-import org.matrix.android.sdk.internal.task.TaskExecutor
-import timber.log.Timber
-import java.io.IOException
-import java.util.Timer
-import java.util.TimerTask
-import java.util.concurrent.LinkedBlockingQueue
-import javax.inject.Inject
-import kotlin.concurrent.schedule
-
-/**
- * A simple ever running thread unique for that session responsible of sending events in order.
- * Each send is retried 3 times, if there is no network (e.g if cannot ping homeserver) it will wait and
- * periodically test reachability before resume (does not count as a retry)
- *
- * If the app is killed before all event were sent, on next wakeup the scheduled events will be re posted
- */
-@Deprecated("You should know use EventSenderProcessorCoroutine instead")
-@SessionScope
-internal class EventSenderProcessorThread @Inject constructor(
-        private val cryptoService: CryptoService,
-        private val sessionParams: SessionParams,
-        private val queuedTaskFactory: QueuedTaskFactory,
-        private val taskExecutor: TaskExecutor,
-        private val memento: QueueMemento
-) : Thread("Matrix-SENDER_THREAD_SID_${sessionParams.credentials.sessionId()}"), EventSenderProcessor {
-
-    private fun markAsManaged(task: QueuedTask) {
-        memento.track(task)
-    }
-
-    private fun markAsFinished(task: QueuedTask) {
-        memento.unTrack(task)
-    }
-
-    override fun onSessionStarted(session: Session) {
-        start()
-    }
-
-    override fun onSessionStopped(session: Session) {
-        interrupt()
-    }
-
-    override fun start() {
-        super.start()
-        // We should check for sending events not handled because app was killed
-        // But we should be careful of only took those that was submitted to us, because if it's
-        // for example it's a media event it is handled by some worker and he will handle it
-        // This is a bit fragile :/
-        // also some events cannot be retried manually by users, e.g reactions
-        // they were previously relying on workers to do the work :/ and was expected to always finally succeed
-        // Also some echos are not to be resent like redaction echos (fake event created for aggregation)
-
-        tryOrNull {
-            taskExecutor.executorScope.launch {
-                Timber.d("## Send relaunched pending events on restart")
-                memento.restoreTasks(this@EventSenderProcessorThread)
-            }
-        }
-    }
-
-    // API
-    override fun postEvent(event: Event): Cancelable {
-        return postEvent(event, event.roomId?.let { cryptoService.isRoomEncrypted(it) } ?: false)
-    }
-
-    override fun postEvent(event: Event, encrypt: Boolean): Cancelable {
-        val task = queuedTaskFactory.createSendTask(event, encrypt)
-        return postTask(task)
-    }
-
-    override fun postRedaction(redactionLocalEcho: Event, reason: String?): Cancelable {
-        return postRedaction(redactionLocalEcho.eventId!!, redactionLocalEcho.redacts!!, redactionLocalEcho.roomId!!, reason)
-    }
-
-    override fun postRedaction(redactionLocalEchoId: String, eventToRedactId: String, roomId: String, reason: String?): Cancelable {
-        val task = queuedTaskFactory.createRedactTask(redactionLocalEchoId, eventToRedactId, roomId, reason)
-        return postTask(task)
-    }
-
-    override fun postTask(task: QueuedTask): Cancelable {
-        // non blocking add to queue
-        sendingQueue.add(task)
-        markAsManaged(task)
-        return task
-    }
-
-    override fun cancel(eventId: String, roomId: String) {
-        (currentTask as? SendEventQueuedTask)
-                ?.takeIf { it.event.eventId == eventId && it.event.roomId == roomId }
-                ?.cancel()
-    }
-
-    companion object {
-        private const val RETRY_WAIT_TIME_MS = 10_000L
-    }
-
-    private var currentTask: QueuedTask? = null
-
-    private var sendingQueue = LinkedBlockingQueue<QueuedTask>()
-
-    private var networkAvailableLock = Object()
-    private var canReachServer = true
-    private var retryNoNetworkTask: TimerTask? = null
-
-    override fun run() {
-        Timber.v("## SendThread started")
-        try {
-            while (!isInterrupted) {
-                Timber.v("## SendThread wait for task to process")
-                val task = sendingQueue.take()
-                        .also { currentTask = it }
-                Timber.v("## SendThread Found task to process $task")
-
-                if (task.isCancelled()) {
-                    Timber.v("## SendThread send cancelled for $task")
-                    // we do not execute this one
-                    continue
-                }
-                // we check for network connectivity
-                while (!canReachServer) {
-                    Timber.v("## SendThread cannot reach server")
-                    // schedule to retry
-                    waitForNetwork()
-                    // if thread as been killed meanwhile
-//                    if (state == State.KILLING) break
-                }
-                Timber.v("## Server is Reachable")
-                // so network is available
-
-                runBlocking {
-                    retryLoop@ while (task.retryCount.get() < 3) {
-                        try {
-                            // SendPerformanceProfiler.startStage(task.event.eventId!!, SendPerformanceProfiler.Stages.SEND_WORKER)
-                            Timber.v("## SendThread retryLoop for $task retryCount ${task.retryCount}")
-                            task.execute()
-                            // sendEventTask.execute(SendEventTask.Params(task.event, task.encrypt, cryptoService))
-                            // SendPerformanceProfiler.stopStage(task.event.eventId, SendPerformanceProfiler.Stages.SEND_WORKER)
-                            break@retryLoop
-                        } catch (exception: Throwable) {
-                            when {
-                                exception is IOException || exception is Failure.NetworkConnection -> {
-                                    canReachServer = false
-                                    if (task.retryCount.getAndIncrement() >= 3) task.onTaskFailed()
-                                    while (!canReachServer) {
-                                        Timber.v("## SendThread retryLoop cannot reach server")
-                                        // schedule to retry
-                                        waitForNetwork()
-                                    }
-                                }
-                                (exception.isLimitExceededError()) -> {
-                                    if (task.retryCount.getAndIncrement() >= 3) task.onTaskFailed()
-                                    Timber.v("## SendThread retryLoop retryable error for $task reason: ${exception.localizedMessage}")
-                                    // wait a bit
-                                    // Todo if its a quota exception can we get timout?
-                                    sleep(3_000)
-                                    continue@retryLoop
-                                }
-                                exception.isTokenError() -> {
-                                    Timber.v("## SendThread retryLoop retryable TOKEN error, interrupt")
-                                    // we can exit the loop
-                                    task.onTaskFailed()
-                                    throw InterruptedException()
-                                }
-                                exception is CancellationException -> {
-                                    Timber.v("## SendThread task has been cancelled")
-                                    break@retryLoop
-                                }
-                                else -> {
-                                    Timber.v("## SendThread retryLoop Un-Retryable error, try next task")
-                                    // this task is in error, check next one?
-                                    task.onTaskFailed()
-                                    break@retryLoop
-                                }
-                            }
-                        }
-                    }
-                }
-                markAsFinished(task)
-            }
-        } catch (interruptionException: InterruptedException) {
-            // will be thrown is thread is interrupted while seeping
-            interrupt()
-            Timber.v("## InterruptedException!! ${interruptionException.localizedMessage}")
-        }
-//        state = State.KILLED
-        // is this needed?
-        retryNoNetworkTask?.cancel()
-        Timber.w("## SendThread finished")
-    }
-
-    private fun waitForNetwork() {
-        retryNoNetworkTask = Timer(SyncState.NoNetwork.toString(), false).schedule(RETRY_WAIT_TIME_MS) {
-            synchronized(networkAvailableLock) {
-                canReachServer = HomeServerAvailabilityChecker(sessionParams).check().also {
-                    Timber.v("## SendThread checkHostAvailable $it")
-                }
-                networkAvailableLock.notify()
-            }
-        }
-        synchronized(networkAvailableLock) { networkAvailableLock.wait() }
-    }
-}
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 b47b2156552ad344ce24d2c14f34b6d883f92d0b..d3f2a3f044925be19ef70e9a7bac51eed622e841 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
@@ -30,6 +30,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.SharedFlow
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
+import org.matrix.android.sdk.api.MatrixConfiguration
 import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.failure.Failure
 import org.matrix.android.sdk.api.failure.isTokenError
@@ -52,7 +53,6 @@ import javax.inject.Inject
 import kotlin.concurrent.schedule
 
 private const val RETRY_WAIT_TIME_MS = 10_000L
-private const val DEFAULT_LONG_POOL_TIMEOUT = 30_000L
 
 private val loggerTag = LoggerTag("SyncThread", LoggerTag.SYNC)
 
@@ -61,7 +61,8 @@ internal class SyncThread @Inject constructor(
         private val networkConnectivityChecker: NetworkConnectivityChecker,
         private val backgroundDetectionObserver: BackgroundDetectionObserver,
         private val activeCallHandler: ActiveCallHandler,
-        private val lightweightSettingsStorage: DefaultLightweightSettingsStorage
+        private val lightweightSettingsStorage: DefaultLightweightSettingsStorage,
+        private val matrixConfiguration: MatrixConfiguration,
 ) : Thread("Matrix-SyncThread"), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {
 
     private var state: SyncState = SyncState.Idle
@@ -181,7 +182,7 @@ internal class SyncThread @Inject constructor(
                 val timeout = when {
                     previousSyncResponseHasToDevice -> 0L /* Force timeout to 0 */
                     afterPause -> 0L /* No timeout after a pause */
-                    else -> DEFAULT_LONG_POOL_TIMEOUT
+                    else -> matrixConfiguration.syncConfig.longPollTimeout
                 }
                 Timber.tag(loggerTag.value).d("Execute sync request with timeout $timeout")
                 val presence = lightweightSettingsStorage.getSyncPresenceStatus()
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/UnRequestedKeysManagerTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/UnRequestedKeysManagerTest.kt
index 950093760a11b874950f06357d320a590a454653..5b41ff6da07a54cd03592a33da19eb20864d3496 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/UnRequestedKeysManagerTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/UnRequestedKeysManagerTest.kt
@@ -53,8 +53,10 @@ class UnRequestedKeysManagerTest {
             ),
             signatures = mapOf(
                     aliceMxId to mapOf(
-                            "ed25519:$device1Id" to "bPOAqM40+QSMgeEzUbYbPSZZccDDMUG00lCNdSXCoaS1gKKBGkSEaHO1OcibISIabjLYzmhp9mgtivz32fbABQ",
-                            "ed25519:Ru4ni66dbQ6FZgUoHyyBtmjKecOHMvMSsSBZ2SABtt0" to "owzUsQ4Pvn35uEIc5FdVnXVRPzsVYBV8uJRUSqr4y8r5tp0DvrMArtJukKETgYEAivcZMT1lwNihHIN9xh06DA"
+                            "ed25519:$device1Id"
+                                    to "bPOAqM40+QSMgeEzUbYbPSZZccDDMUG00lCNdSXCoaS1gKKBGkSEaHO1OcibISIabjLYzmhp9mgtivz32fbABQ",
+                            "ed25519:Ru4ni66dbQ6FZgUoHyyBtmjKecOHMvMSsSBZ2SABtt0"
+                                    to "owzUsQ4Pvn35uEIc5FdVnXVRPzsVYBV8uJRUSqr4y8r5tp0DvrMArtJukKETgYEAivcZMT1lwNihHIN9xh06DA"
                     )
             ),
             unsigned = UnsignedDeviceInfo(deviceDisplayName = "Element Web"),
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/network/ComputeUserAgentUseCaseTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/network/ComputeUserAgentUseCaseTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..9ed6f28d7e22cd5bae3436db318fff951b0ccacc
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/network/ComputeUserAgentUseCaseTest.kt
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2022 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.network
+
+import android.content.Context
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+import android.os.Build
+import io.mockk.every
+import io.mockk.mockk
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.Before
+import org.junit.Test
+import org.matrix.android.sdk.BuildConfig
+import java.lang.Exception
+
+private const val A_PACKAGE_NAME = "org.matrix.sdk"
+private const val AN_APP_NAME = "Element"
+private const val A_NON_ASCII_APP_NAME = "Élement"
+private const val AN_APP_VERSION = "1.5.1"
+private const val A_FLAVOUR = "GooglePlay"
+
+class ComputeUserAgentUseCaseTest {
+
+    private val context = mockk<Context>()
+    private val packageManager = mockk<PackageManager>()
+    private val applicationInfo = mockk<ApplicationInfo>()
+    private val packageInfo = mockk<PackageInfo>()
+
+    private val computeUserAgentUseCase = ComputeUserAgentUseCase(context)
+
+    @Before
+    fun setUp() {
+        every { context.applicationContext } returns context
+        every { context.packageName } returns A_PACKAGE_NAME
+        every { context.packageManager } returns packageManager
+        every { packageManager.getApplicationInfo(any(), any()) } returns applicationInfo
+        every { packageManager.getPackageInfo(any<String>(), any()) } returns packageInfo
+    }
+
+    @Test
+    fun `given a non-null app name and app version when computing user agent then returns expected user agent`() {
+        // Given
+        givenAppName(AN_APP_NAME)
+        givenAppVersion(AN_APP_VERSION)
+
+        // When
+        val result = computeUserAgentUseCase.execute(A_FLAVOUR)
+
+        // Then
+        val expectedUserAgent = constructExpectedUserAgent(AN_APP_NAME, AN_APP_VERSION)
+        result shouldBeEqualTo expectedUserAgent
+    }
+
+    @Test
+    fun `given a null app name when computing user agent then returns user agent with package name instead of app name`() {
+        // Given
+        givenAppName(null)
+        givenAppVersion(AN_APP_VERSION)
+
+        // When
+        val result = computeUserAgentUseCase.execute(A_FLAVOUR)
+
+        // Then
+        val expectedUserAgent = constructExpectedUserAgent(A_PACKAGE_NAME, AN_APP_VERSION)
+        result shouldBeEqualTo expectedUserAgent
+    }
+
+    @Test
+    fun `given a non-ascii app name when computing user agent then returns user agent with package name instead of app name`() {
+        // Given
+        givenAppName(A_NON_ASCII_APP_NAME)
+        givenAppVersion(AN_APP_VERSION)
+
+        // When
+        val result = computeUserAgentUseCase.execute(A_FLAVOUR)
+
+        // Then
+        val expectedUserAgent = constructExpectedUserAgent(A_PACKAGE_NAME, AN_APP_VERSION)
+        result shouldBeEqualTo expectedUserAgent
+    }
+
+    @Test
+    fun `given a null app version when computing user agent then returns user agent with a fallback app version`() {
+        // Given
+        givenAppName(AN_APP_NAME)
+        givenAppVersion(null)
+
+        // When
+        val result = computeUserAgentUseCase.execute(A_FLAVOUR)
+
+        // Then
+        val expectedUserAgent = constructExpectedUserAgent(AN_APP_NAME, ComputeUserAgentUseCase.FALLBACK_APP_VERSION)
+        result shouldBeEqualTo expectedUserAgent
+    }
+
+    private fun constructExpectedUserAgent(appName: String, appVersion: String): String {
+        return buildString {
+            append(appName)
+            append("/")
+            append(appVersion)
+            append(" (")
+            append(Build.MANUFACTURER)
+            append(" ")
+            append(Build.MODEL)
+            append("; ")
+            append("Android ")
+            append(Build.VERSION.RELEASE)
+            append("; ")
+            append(Build.DISPLAY)
+            append("; ")
+            append("Flavour ")
+            append(A_FLAVOUR)
+            append("; ")
+            append("MatrixAndroidSdk2 ")
+            append(BuildConfig.SDK_VERSION)
+            append(")")
+        }
+    }
+
+    private fun givenAppName(deviceName: String?) {
+        if (deviceName == null) {
+            every { packageManager.getApplicationLabel(any()) } throws Exception("Cannot retrieve application name")
+        } else if (!deviceName.matches("\\A\\p{ASCII}*\\z".toRegex())) {
+            every { packageManager.getApplicationLabel(any()) } returns A_PACKAGE_NAME
+        } else {
+            every { packageManager.getApplicationLabel(any()) } returns deviceName
+        }
+    }
+
+    private fun givenAppVersion(appVersion: String?) {
+        packageInfo.versionName = appVersion
+    }
+}