diff --git a/dependencies.gradle b/dependencies.gradle
index 451ad4449bc026db99e0f5852244dc49172b1e28..db9278b9757695e11478cb61a1bbae54046c6e33 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -13,7 +13,7 @@ ext.versions = [
 def gradle = "7.1.3"
 // Ref: https://kotlinlang.org/releases.html
 def kotlin = "1.6.21"
-def kotlinCoroutines = "1.6.2"
+def kotlinCoroutines = "1.6.3"
 def dagger = "2.42"
 def retrofit = "2.9.0"
 def arrow = "0.8.2"
@@ -21,20 +21,21 @@ def markwon = "4.6.2"
 def moshi = "1.13.0"
 def lifecycle = "2.4.1"
 def flowBinding = "1.2.0"
+def flipper = "0.151.1"
 def epoxy = "4.6.2"
-def mavericks = "2.6.1"
+def mavericks = "2.7.0"
 def glide = "4.13.2"
 def bigImageViewer = "1.8.1"
 def jjwt = "0.11.5"
 def vanniktechEmoji = "0.15.0"
 
+def fragment = "1.4.1"
+
 // Testing
-def mockk = "1.12.4"
+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
 def espresso = "3.4.0"
 def androidxTest = "1.4.0"
 def androidxOrchestrator = "1.4.1"
-
-
 ext.libs = [
         gradle      : [
                 'gradlePlugin'            : "com.android.tools.build:gradle:$gradle",
@@ -48,12 +49,16 @@ ext.libs = [
                 'coroutinesTest'          : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines"
         ],
         androidx    : [
+                'annotation'              : "androidx.annotation:annotation:1.4.0",
                 'activity'                : "androidx.activity:activity:1.4.0",
+                'annotations'             : "androidx.annotation:annotation:1.3.0",
                 'appCompat'               : "androidx.appcompat:appcompat:1.4.2",
+                'biometric'               : "androidx.biometric:biometric:1.1.0",
                 'core'                    : "androidx.core:core-ktx:1.8.0",
                 'recyclerview'            : "androidx.recyclerview:recyclerview:1.2.1",
                 'exifinterface'           : "androidx.exifinterface:exifinterface:1.3.3",
-                'fragmentKtx'             : "androidx.fragment:fragment-ktx:1.4.1",
+                'fragmentKtx'             : "androidx.fragment:fragment-ktx:$fragment",
+                'fragmentTesting'         : "androidx.fragment:fragment-testing:$fragment",
                 'constraintLayout'        : "androidx.constraintlayout:constraintlayout:2.1.4",
                 'work'                    : "androidx.work:work-runtime-ktx:2.7.1",
                 'autoFill'                : "androidx.autofill:autofill:1.1.0",
@@ -84,10 +89,16 @@ ext.libs = [
                 'dagger'                  : "com.google.dagger:dagger:$dagger",
                 'daggerCompiler'          : "com.google.dagger:dagger-compiler:$dagger",
                 'hilt'                    : "com.google.dagger:hilt-android:$dagger",
+                'hiltAndroidTesting'      : "com.google.dagger:hilt-android-testing:$dagger",
                 'hiltCompiler'            : "com.google.dagger:hilt-compiler:$dagger"
         ],
+        flipper     : [
+                'flipper'                 : "com.facebook.flipper:flipper:$flipper",
+                'flipperNetworkPlugin'    : "com.facebook.flipper:flipper-network-plugin:$flipper",
+        ],
         squareup    : [
                 'moshi'                  : "com.squareup.moshi:moshi:$moshi",
+                'moshiKt'                : "com.squareup.moshi:moshi-kotlin:$moshi",
                 'moshiKotlin'            : "com.squareup.moshi:moshi-kotlin-codegen:$moshi",
                 'retrofit'               : "com.squareup.retrofit2:retrofit:$retrofit",
                 'retrofitMoshi'          : "com.squareup.retrofit2:converter-moshi:$retrofit"
@@ -153,3 +164,5 @@ ext.libs = [
                 'junit'                  : "junit:junit:4.13.2"
         ]
 ]
+
+
diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle
index 59cefe7e899545202918938527213805f5982217..b785c7f50b71d941d22ebd9d8beabd29e73c1a07 100644
--- a/dependencies_groups.gradle
+++ b/dependencies_groups.gradle
@@ -9,6 +9,7 @@ ext.groups = [
                         'com.github.jetradarmobile',
                         'com.github.MatrixFrog',
                         'com.github.tapadoo',
+                        'com.github.UnifiedPush',
                         'com.github.vector-im',
                         'com.github.yalantis',
                         'com.github.Zhuinden',
@@ -31,6 +32,7 @@ ext.groups = [
                 ],
                 group: [
                         'com.android',
+                        'com.android.ndk.thirdparty',
                         'com.android.tools',
                         'com.google.firebase',
                         'com.google.testing.platform',
@@ -52,6 +54,7 @@ ext.groups = [
                         'com.dropbox.core',
                         'com.soywiz.korlibs.korte',
                         'com.facebook.fbjni',
+                        'com.facebook.flipper',
                         'com.facebook.fresco',
                         'com.facebook.infer.annotation',
                         'com.facebook.soloader',
@@ -93,6 +96,7 @@ ext.groups = [
                         'com.ibm.icu',
                         'com.jakewharton.android.repackaged',
                         'com.jakewharton.timber',
+                        'com.kgurgul.flipper',
                         'com.linkedin.dexmaker',
                         'com.mapbox.mapboxsdk',
                         'com.nulab-inc',
@@ -168,6 +172,7 @@ ext.groups = [
                         'org.glassfish.jaxb',
                         'org.hamcrest',
                         'org.jacoco',
+                        'org.java-websocket',
                         'org.jetbrains',
                         'org.jetbrains.dokka',
                         'org.jetbrains.intellij.deps',
diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle
index f6a7a294de26955a5ed883525332809aea9a93f4..a7cb1cad2fc016d84f240d069491f57bf544ef66 100644
--- a/matrix-sdk-android/build.gradle
+++ b/matrix-sdk-android/build.gradle
@@ -7,6 +7,10 @@ apply plugin: "org.jetbrains.dokka"
 // WARNING: always restore this line after importing code from Element Android (1/2)
 apply plugin: "com.vanniktech.maven.publish"
 
+if (project.hasProperty("coverage")) {
+    apply plugin: 'jacoco'
+}
+
 buildscript {
     repositories {
         // Do not use `mavenCentral()`, it prevents Dependabot from working properly
@@ -77,7 +81,9 @@ android {
 
     buildTypes {
         debug {
-            testCoverageEnabled true
+            if (project.hasProperty("coverage")) {
+                testCoverageEnabled = coverage.enableTestCoverage
+            }
             // Set to true to log privacy or sensible data, such as token
             buildConfigField "boolean", "LOG_PRIVATE_DATA", project.property("vector.debugPrivateData")
             // Set to BODY instead of NONE to enable logging
@@ -196,7 +202,7 @@ dependencies {
     implementation libs.apache.commonsImaging
 
     // Phone number https://github.com/google/libphonenumber
-    implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.50'
+    implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.51'
 
     testImplementation libs.tests.junit
     // Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/TestBuildVersionSdkIntProvider.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/TestBuildVersionSdkIntProvider.kt
similarity index 78%
rename from matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/TestBuildVersionSdkIntProvider.kt
rename to matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/TestBuildVersionSdkIntProvider.kt
index b08c88fb246da80217ee2c84e079053e56382319..d0d64491efaba62d3b6f740c33eb64ffe1f9f880 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/TestBuildVersionSdkIntProvider.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/TestBuildVersionSdkIntProvider.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2021 The Matrix.org Foundation C.I.C.
+ * 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.
@@ -14,9 +14,9 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.session.securestorage
+package org.matrix.android.sdk
 
-import org.matrix.android.sdk.internal.util.system.BuildVersionSdkIntProvider
+import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
 
 class TestBuildVersionSdkIntProvider : BuildVersionSdkIntProvider {
     var value: Int = 0
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtilsTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtilsTest.kt
similarity index 62%
rename from matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtilsTest.kt
rename to matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtilsTest.kt
index 6bcd12742b3d630a577f99355aca1e3064a74a60..14f985243cd528d78e00deee39161f8774603d1d 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtilsTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtilsTest.kt
@@ -14,40 +14,57 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.session.securestorage
+package org.matrix.android.sdk.api.securestorage
 
 import android.os.Build
+import android.util.Base64
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import io.mockk.clearAllMocks
+import io.mockk.every
+import io.mockk.spyk
+import org.amshove.kluent.invoking
 import org.amshove.kluent.shouldBeEqualTo
+import org.amshove.kluent.shouldBeInstanceOf
+import org.amshove.kluent.shouldNotThrow
+import org.amshove.kluent.shouldThrow
+import org.junit.Before
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.MethodSorters
-import org.matrix.android.sdk.InstrumentedTest
-import org.matrix.android.sdk.api.util.fromBase64
-import org.matrix.android.sdk.api.util.toBase64NoPadding
+import org.matrix.android.sdk.TestBuildVersionSdkIntProvider
 import java.io.ByteArrayOutputStream
+import java.security.KeyStore
+import java.security.KeyStoreException
 import java.util.UUID
 
 @RunWith(AndroidJUnit4::class)
 @FixMethodOrder(MethodSorters.JVM)
-class SecretStoringUtilsTest : InstrumentedTest {
+class SecretStoringUtilsTest {
 
+    private val context = InstrumentationRegistry.getInstrumentation().targetContext
     private val buildVersionSdkIntProvider = TestBuildVersionSdkIntProvider()
-    private val secretStoringUtils = SecretStoringUtils(context(), buildVersionSdkIntProvider)
+    private val keyStore = spyk(KeyStore.getInstance("AndroidKeyStore")).also { it.load(null) }
+    private val secretStoringUtils = SecretStoringUtils(context, keyStore, buildVersionSdkIntProvider)
 
     companion object {
         const val TEST_STR = "This is something I want to store safely!"
     }
 
+    @Before
+    fun setup() {
+        clearAllMocks()
+    }
+
     @Test
     fun testStringNominalCaseApi21() {
         val alias = generateAlias()
         buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP
         // Encrypt
-        val encrypted = secretStoringUtils.securelyStoreString(TEST_STR, alias)
+        val encrypted = secretStoringUtils.securelyStoreBytes(TEST_STR.toByteArray(), alias)
         // Decrypt
-        val decrypted = secretStoringUtils.loadSecureSecret(encrypted, alias)
+        val decrypted = String(secretStoringUtils.loadSecureSecretBytes(encrypted, alias))
         decrypted shouldBeEqualTo TEST_STR
         secretStoringUtils.safeDeleteKey(alias)
     }
@@ -57,9 +74,9 @@ class SecretStoringUtilsTest : InstrumentedTest {
         val alias = generateAlias()
         buildVersionSdkIntProvider.value = Build.VERSION_CODES.M
         // Encrypt
-        val encrypted = secretStoringUtils.securelyStoreString(TEST_STR, alias)
+        val encrypted = secretStoringUtils.securelyStoreBytes(TEST_STR.toByteArray(), alias)
         // Decrypt
-        val decrypted = secretStoringUtils.loadSecureSecret(encrypted, alias)
+        val decrypted = String(secretStoringUtils.loadSecureSecretBytes(encrypted, alias))
         decrypted shouldBeEqualTo TEST_STR
         secretStoringUtils.safeDeleteKey(alias)
     }
@@ -69,9 +86,9 @@ class SecretStoringUtilsTest : InstrumentedTest {
         val alias = generateAlias()
         buildVersionSdkIntProvider.value = Build.VERSION_CODES.R
         // Encrypt
-        val encrypted = secretStoringUtils.securelyStoreString(TEST_STR, alias)
+        val encrypted = secretStoringUtils.securelyStoreBytes(TEST_STR.toByteArray(), alias)
         // Decrypt
-        val decrypted = secretStoringUtils.loadSecureSecret(encrypted, alias)
+        val decrypted = String(secretStoringUtils.loadSecureSecretBytes(encrypted, alias))
         decrypted shouldBeEqualTo TEST_STR
         secretStoringUtils.safeDeleteKey(alias)
     }
@@ -81,13 +98,13 @@ class SecretStoringUtilsTest : InstrumentedTest {
         val alias = generateAlias()
         buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP
         // Encrypt
-        val encrypted = secretStoringUtils.securelyStoreString(TEST_STR, alias)
+        val encrypted = secretStoringUtils.securelyStoreBytes(TEST_STR.toByteArray(), alias)
 
         // Simulate a system upgrade
         buildVersionSdkIntProvider.value = Build.VERSION_CODES.M
 
         // Decrypt
-        val decrypted = secretStoringUtils.loadSecureSecret(encrypted, alias)
+        val decrypted = String(secretStoringUtils.loadSecureSecretBytes(encrypted, alias))
         decrypted shouldBeEqualTo TEST_STR
         secretStoringUtils.safeDeleteKey(alias)
     }
@@ -180,5 +197,56 @@ class SecretStoringUtilsTest : InstrumentedTest {
         secretStoringUtils.safeDeleteKey(alias)
     }
 
+    @Test
+    fun testEnsureKeyReturnsSymmetricKeyOnAndroidM() {
+        buildVersionSdkIntProvider.value = Build.VERSION_CODES.M
+        val alias = generateAlias()
+
+        val key = secretStoringUtils.ensureKey(alias)
+        key shouldBeInstanceOf KeyStore.SecretKeyEntry::class
+
+        secretStoringUtils.safeDeleteKey(alias)
+    }
+
+    @Test
+    fun testEnsureKeyReturnsPrivateKeyOnAndroidL() {
+        buildVersionSdkIntProvider.value = Build.VERSION_CODES.LOLLIPOP
+        val alias = generateAlias()
+
+        val key = secretStoringUtils.ensureKey(alias)
+        key shouldBeInstanceOf KeyStore.PrivateKeyEntry::class
+
+        secretStoringUtils.safeDeleteKey(alias)
+    }
+
+    @Test
+    fun testSafeDeleteCanHandleKeyStoreExceptions() {
+        every { keyStore.deleteEntry(any()) } throws KeyStoreException()
+
+        invoking { secretStoringUtils.safeDeleteKey(generateAlias()) } shouldNotThrow KeyStoreException::class
+    }
+
+    @Test
+    fun testLoadSecureSecretBytesWillThrowOnInvalidStreamFormat() {
+        invoking {
+            secretStoringUtils.loadSecureSecretBytes(byteArrayOf(255.toByte()), generateAlias())
+        } shouldThrow IllegalArgumentException::class
+    }
+
+    @Test
+    fun testLoadSecureSecretWillThrowOnInvalidStreamFormat() {
+        invoking {
+            secretStoringUtils.loadSecureSecret(byteArrayOf(255.toByte()).inputStream(), generateAlias())
+        } shouldThrow IllegalArgumentException::class
+    }
+
     private fun generateAlias() = UUID.randomUUID().toString()
 }
+
+private fun ByteArray.toBase64NoPadding(): String {
+    return Base64.encodeToString(this, Base64.NO_PADDING or Base64.NO_WRAP)
+}
+
+private fun String.fromBase64(): ByteArray {
+    return Base64.decode(this, Base64.DEFAULT)
+}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt
index daf6b73313fd900db73e8fde539321714fafba6e..6cf01d4ae238e2da5e5661c1db528cb73d9924eb 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/TestMatrixComponent.kt
@@ -20,7 +20,9 @@ import android.content.Context
 import dagger.BindsInstance
 import dagger.Component
 import org.matrix.android.sdk.api.MatrixConfiguration
+import org.matrix.android.sdk.api.securestorage.SecureStorageModule
 import org.matrix.android.sdk.internal.auth.AuthModule
+import org.matrix.android.sdk.internal.debug.DebugModule
 import org.matrix.android.sdk.internal.di.MatrixComponent
 import org.matrix.android.sdk.internal.di.MatrixModule
 import org.matrix.android.sdk.internal.di.MatrixScope
@@ -36,8 +38,10 @@ import org.matrix.android.sdk.internal.util.system.SystemModule
             NetworkModule::class,
             AuthModule::class,
             RawModule::class,
+            DebugModule::class,
             SettingsModule::class,
-            SystemModule::class
+            SystemModule::class,
+            SecureStorageModule::class,
         ]
 )
 @MatrixScope
@@ -49,7 +53,7 @@ internal interface TestMatrixComponent : MatrixComponent {
     interface Factory {
         fun create(
                 @BindsInstance context: Context,
-                @BindsInstance matrixConfiguration: MatrixConfiguration
+                @BindsInstance matrixConfiguration: MatrixConfiguration,
         ): TestMatrixComponent
     }
 }
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 0cc0ef57c411e7585ac56ed889e18603551f0d0f..38136ff5cee79fd5576e8063884218fca948c4ce 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
@@ -28,6 +28,7 @@ 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.query.QueryStringValue
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.room.getStateEvent
@@ -73,27 +74,37 @@ class SpaceCreationTest : InstrumentedTest {
         // assertEquals(topic, syncedSpace.asRoom().roomSummary()?., "Room topic should be set")
 
         assertNotNull("Space should be found by Id", syncedSpace)
-        val creationEvent = syncedSpace!!.asRoom().getStateEvent(EventType.STATE_ROOM_CREATE)
-        val createContent = creationEvent?.content.toModel<RoomCreateContent>()
+        val createContent = syncedSpace!!.asRoom()
+                .getStateEvent(EventType.STATE_ROOM_CREATE, QueryStringValue.IsEmpty)
+                ?.content
+                ?.toModel<RoomCreateContent>()
         assertEquals("Room type should be space", RoomType.SPACE, createContent?.type)
 
         var powerLevelsContent: PowerLevelsContent? = null
         commonTestHelper.waitWithLatch { latch ->
             commonTestHelper.retryPeriodicallyWithLatch(latch) {
-                val toModel = syncedSpace.asRoom().getStateEvent(EventType.STATE_ROOM_POWER_LEVELS)?.content.toModel<PowerLevelsContent>()
-                powerLevelsContent = toModel
-                toModel != null
+                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)
 
-        val guestAccess = syncedSpace.asRoom().getStateEvent(EventType.STATE_ROOM_GUEST_ACCESS)?.content
-                ?.toModel<RoomGuestAccessContent>()?.guestAccess
+        val guestAccess = syncedSpace.asRoom()
+                .getStateEvent(EventType.STATE_ROOM_GUEST_ACCESS, QueryStringValue.IsEmpty)
+                ?.content
+                ?.toModel<RoomGuestAccessContent>()
+                ?.guestAccess
 
         assertEquals("Public space room should be peekable by guest", GuestAccess.CanJoin, guestAccess)
 
-        val historyVisibility = syncedSpace.asRoom().getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY)?.content
-                ?.toModel<RoomHistoryVisibilityContent>()?.historyVisibility
+        val historyVisibility = syncedSpace.asRoom()
+                .getStateEvent(EventType.STATE_ROOM_HISTORY_VISIBILITY, QueryStringValue.IsEmpty)
+                ?.content
+                ?.toModel<RoomHistoryVisibilityContent>()
+                ?.historyVisibility
 
         assertEquals("Public space room should be world readable", RoomHistoryVisibility.WORLD_READABLE, historyVisibility)
     }
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 5396251438d56fbe6d230220f833f0851a0e78c2..63ca963479f2c0edb135aac507a861d700d90bb4 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
@@ -569,8 +569,9 @@ class SpaceHierarchyTest : InstrumentedTest {
         commonTestHelper.waitWithLatch {
             val room = bobSession.getRoom(bobRoomId)!!
             val currentPLContent = room
-                    .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS)
-                    ?.let { it.content.toModel<PowerLevelsContent>() }
+                    .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
+                    ?.content
+                    .toModel<PowerLevelsContent>()
 
             val newPowerLevelsContent = currentPLContent
                     ?.setUserPowerLevel(aliceSession.myUserId, Role.Admin.value)
@@ -583,7 +584,7 @@ class SpaceHierarchyTest : InstrumentedTest {
         commonTestHelper.waitWithLatch { latch ->
             commonTestHelper.retryPeriodicallyWithLatch(latch) {
                 val powerLevelsHelper = aliceSession.getRoom(bobRoomId)!!
-                        .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS)
+                        .getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
                         ?.content
                         ?.toModel<PowerLevelsContent>()
                         ?.let { PowerLevelsHelper(it) }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt
index 979201706b20cb838744b7764c8db745589239f7..953ebddcbf73fe4226e8e723750a5b2f2ef2061c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt
@@ -17,6 +17,8 @@
 package org.matrix.android.sdk.api
 
 import android.content.Context
+import android.os.Handler
+import android.os.Looper
 import androidx.lifecycle.ProcessLifecycleOwner
 import androidx.work.Configuration
 import androidx.work.WorkManager
@@ -25,10 +27,12 @@ import com.zhuinden.monarchy.Monarchy
 import org.matrix.android.sdk.BuildConfig
 import org.matrix.android.sdk.api.auth.AuthenticationService
 import org.matrix.android.sdk.api.auth.HomeServerHistoryService
+import org.matrix.android.sdk.api.debug.DebugService
 import org.matrix.android.sdk.api.legacy.LegacySessionImporter
 import org.matrix.android.sdk.api.network.ApiInterceptorListener
 import org.matrix.android.sdk.api.network.ApiPath
 import org.matrix.android.sdk.api.raw.RawService
+import org.matrix.android.sdk.api.securestorage.SecureStorageService
 import org.matrix.android.sdk.api.settings.LightweightSettingsStorage
 import org.matrix.android.sdk.internal.SessionManager
 import org.matrix.android.sdk.internal.di.DaggerMatrixComponent
@@ -54,6 +58,7 @@ class Matrix(context: Context, matrixConfiguration: MatrixConfiguration) {
     @Inject internal lateinit var legacySessionImporter: LegacySessionImporter
     @Inject internal lateinit var authenticationService: AuthenticationService
     @Inject internal lateinit var rawService: RawService
+    @Inject internal lateinit var debugService: DebugService
     @Inject internal lateinit var userAgentHolder: UserAgentHolder
     @Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver
     @Inject internal lateinit var olmManager: OlmManager
@@ -62,6 +67,9 @@ class Matrix(context: Context, matrixConfiguration: MatrixConfiguration) {
     @Inject internal lateinit var apiInterceptor: ApiInterceptor
     @Inject internal lateinit var matrixWorkerFactory: MatrixWorkerFactory
     @Inject internal lateinit var lightweightSettingsStorage: LightweightSettingsStorage
+    @Inject internal lateinit var secureStorageService: SecureStorageService
+
+    private val uiHandler = Handler(Looper.getMainLooper())
 
     init {
         val appContext = context.applicationContext
@@ -74,7 +82,9 @@ class Matrix(context: Context, matrixConfiguration: MatrixConfiguration) {
                     .build()
             WorkManager.initialize(appContext, configuration)
         }
-        ProcessLifecycleOwner.get().lifecycle.addObserver(backgroundDetectionObserver)
+        uiHandler.post {
+            ProcessLifecycleOwner.get().lifecycle.addObserver(backgroundDetectionObserver)
+        }
     }
 
     /**
@@ -93,6 +103,11 @@ class Matrix(context: Context, matrixConfiguration: MatrixConfiguration) {
      */
     fun rawService() = rawService
 
+    /**
+     * Return the DebugService.
+     */
+    fun debugService() = debugService
+
     /**
      * Return the LightweightSettingsStorage.
      */
@@ -108,6 +123,11 @@ class Matrix(context: Context, matrixConfiguration: MatrixConfiguration) {
      */
     fun legacySessionImporter() = legacySessionImporter
 
+    /**
+     * Returns the SecureStorageService used to encrypt and decrypt sensitive data.
+     */
+    fun secureStorageService(): SecureStorageService = secureStorageService
+
     /**
      * Get the worker factory. The returned value has to be provided to `WorkConfiguration.Builder()`.
      */
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 21106fba6c3d6d7f724310c1cf051961082200f5..893e90fb3efbafe59c44b5125522b082e63aadbf 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
@@ -17,6 +17,7 @@
 package org.matrix.android.sdk.api
 
 import okhttp3.ConnectionSpec
+import okhttp3.Interceptor
 import org.matrix.android.sdk.api.crypto.MXCryptoConfig
 import java.net.Proxy
 
@@ -65,4 +66,8 @@ data class MatrixConfiguration(
          * Thread messages default enable/disabled value.
          */
         val threadMessagesEnabledDefault: Boolean = false,
+        /**
+         * List of network interceptors, they will be added when building an OkHttp client.
+         */
+        val networkInterceptors: List<Interceptor> = emptyList(),
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt
index 7d1407c0d81a9186c7cb912ef0a21d251611f316..5b6c1897bf73236a023a15d9c7f1e52f31dee8c0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/LoginFlowResult.kt
@@ -21,5 +21,6 @@ data class LoginFlowResult(
         val ssoIdentityProviders: List<SsoIdentityProvider>?,
         val isLoginAndRegistrationSupported: Boolean,
         val homeServerUrl: String,
-        val isOutdatedHomeserver: Boolean
+        val isOutdatedHomeserver: Boolean,
+        val isLogoutDevicesSupported: Boolean
 )
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt
index 5b8d2328c769ef3c49d192eac13c232e4226bd56..145cdbdc228ad1f405a66f1b60c7f154614ff2eb 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/login/LoginWizard.kt
@@ -72,7 +72,9 @@ interface LoginWizard {
      * Confirm the new password, once the user has checked their email
      * When this method succeed, tha account password will be effectively modified.
      *
-     * @param newPassword the desired new password
+     * @param newPassword the desired new password.
+     * @param logoutAllDevices defaults to true, all devices will be logged out. False values will only be taken into account
+     * if [org.matrix.android.sdk.api.auth.data.LoginFlowResult.isLogoutDevicesSupported] is true.
      */
-    suspend fun resetPasswordMailConfirmed(newPassword: String)
+    suspend fun resetPasswordMailConfirmed(newPassword: String, logoutAllDevices: Boolean = true)
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/debug/DebugService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/debug/DebugService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d0cee08831d90160ab84c5713b7a886d68b31356
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/debug/DebugService.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.debug
+
+import io.realm.RealmConfiguration
+
+/**
+ * Useful methods to access to some private data managed by the SDK.
+ */
+interface DebugService {
+    /**
+     * Get all the available Realm Configuration.
+     */
+    fun getAllRealmConfigurations(): List<RealmConfiguration>
+
+    /**
+     * Prints out info on DB size to logcat.
+     */
+    fun logDbUsageInfo()
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt
index f08c86885d7e2179e40d26fe10d437e82ee2f772..d3f6ec2287f8e375f43788c2e1cab9bf45489de9 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/query/QueryStringValue.kt
@@ -16,6 +16,11 @@
 
 package org.matrix.android.sdk.api.query
 
+/**
+ * Only a subset of [QueryStringValue] are applicable to query the `stateKey` of a state event.
+ */
+sealed interface QueryStateEventValue
+
 /**
  * Basic query language. All these cases are mutually exclusive.
  */
@@ -33,22 +38,22 @@ sealed interface QueryStringValue {
     /**
      * The tested field has to be not null.
      */
-    object IsNotNull : QueryStringValue
+    object IsNotNull : QueryStringValue, QueryStateEventValue
 
     /**
      * The tested field has to be empty.
      */
-    object IsEmpty : QueryStringValue
+    object IsEmpty : QueryStringValue, QueryStateEventValue
 
     /**
-     * The tested field has to not empty.
+     * The tested field has to be not empty.
      */
-    object IsNotEmpty : QueryStringValue
+    object IsNotEmpty : QueryStringValue, QueryStateEventValue
 
     /**
      * Interface to check String content.
      */
-    sealed interface ContentQueryStringValue : QueryStringValue {
+    sealed interface ContentQueryStringValue : QueryStringValue, QueryStateEventValue {
         val string: String
         val case: Case
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtils.kt
similarity index 82%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtils.kt
index 8b35bd173ee35dc73993e43bb610c39d7b3ac928..bd2a1078b244702923b86f1e54715005cba9cead 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecretStoringUtils.kt
@@ -16,7 +16,7 @@
 
 @file:Suppress("DEPRECATION")
 
-package org.matrix.android.sdk.internal.session.securestorage
+package org.matrix.android.sdk.api.securestorage
 
 import android.annotation.SuppressLint
 import android.content.Context
@@ -25,7 +25,7 @@ import android.security.KeyPairGeneratorSpec
 import android.security.keystore.KeyGenParameterSpec
 import android.security.keystore.KeyProperties
 import androidx.annotation.RequiresApi
-import org.matrix.android.sdk.internal.util.system.BuildVersionSdkIntProvider
+import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
 import timber.log.Timber
 import java.io.ByteArrayInputStream
 import java.io.ByteArrayOutputStream
@@ -80,9 +80,11 @@ import javax.security.auth.x500.X500Principal
  * Important: Keys stored in the keystore can be wiped out (depends of the OS version, like for example if you
  * add a pin or change the schema); So you might and with a useless pile of bytes.
  */
-internal class SecretStoringUtils @Inject constructor(
+class SecretStoringUtils @Inject constructor(
         private val context: Context,
-        private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider
+        private val keyStore: KeyStore,
+        private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
+        private val keyNeedsUserAuthentication: Boolean = false,
 ) {
 
     companion object {
@@ -94,14 +96,24 @@ internal class SecretStoringUtils @Inject constructor(
         private const val FORMAT_1: Byte = 1
     }
 
-    private val keyStore: KeyStore by lazy {
-        KeyStore.getInstance(ANDROID_KEY_STORE).apply {
-            load(null)
+    private val secureRandom = SecureRandom()
+
+    /**
+     * Allows creation of the crypto keys associated witht he [alias] before encrypting some value with it.
+     * @return A [KeyStore.Entry] with the keys.
+     */
+    @SuppressLint("NewApi")
+    fun ensureKey(alias: String): KeyStore.Entry {
+        when {
+            buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> getOrGenerateSymmetricKeyForAliasM(alias)
+            else -> getOrGenerateKeyPairForAlias(alias).privateKey
         }
+        return keyStore.getEntry(alias, null)
     }
 
-    private val secureRandom = SecureRandom()
-
+    /**
+     * Deletes the key associated with the [keyAlias] and logs any [KeyStoreException] that could happen.
+     */
     fun safeDeleteKey(keyAlias: String) {
         try {
             keyStore.deleteEntry(keyAlias)
@@ -121,24 +133,24 @@ internal class SecretStoringUtils @Inject constructor(
      */
     @SuppressLint("NewApi")
     @Throws(Exception::class)
-    fun securelyStoreString(secret: String, keyAlias: String): ByteArray {
+    fun securelyStoreBytes(secret: ByteArray, keyAlias: String): ByteArray {
         return when {
-            buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> encryptStringM(secret, keyAlias)
-            else -> encryptString(secret, keyAlias)
+            buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> encryptBytesM(secret, keyAlias)
+            else -> encryptBytes(secret, keyAlias)
         }
     }
 
     /**
-     * Decrypt a secret that was encrypted by #securelyStoreString().
+     * Decrypt a secret that was encrypted by [securelyStoreBytes].
      */
     @SuppressLint("NewApi")
     @Throws(Exception::class)
-    fun loadSecureSecret(encrypted: ByteArray, keyAlias: String): String {
+    fun loadSecureSecretBytes(encrypted: ByteArray, keyAlias: String): ByteArray {
         encrypted.inputStream().use { inputStream ->
             // First get the format
             return when (val format = inputStream.read().toByte()) {
-                FORMAT_API_M -> decryptStringM(inputStream, keyAlias)
-                FORMAT_1 -> decryptString(inputStream, keyAlias)
+                FORMAT_API_M -> decryptBytesM(inputStream, keyAlias)
+                FORMAT_1 -> decryptBytes(inputStream, keyAlias)
                 else -> throw IllegalArgumentException("Unknown format $format")
             }
         }
@@ -162,6 +174,22 @@ internal class SecretStoringUtils @Inject constructor(
         }
     }
 
+    fun getEncryptCipher(alias: String): Cipher {
+        val key = when (val keyEntry = ensureKey(alias)) {
+            is KeyStore.SecretKeyEntry -> keyEntry.secretKey
+            is KeyStore.PrivateKeyEntry -> keyEntry.certificate.publicKey
+            else -> throw IllegalStateException("Unknown KeyEntry type.")
+        }
+        val cipherMode = when {
+            buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.M -> AES_MODE
+            else -> RSA_MODE
+        }
+        val cipher = Cipher.getInstance(cipherMode)
+        cipher.init(Cipher.ENCRYPT_MODE, key)
+        return cipher
+    }
+
+    @SuppressLint("NewApi")
     @RequiresApi(Build.VERSION_CODES.M)
     private fun getOrGenerateSymmetricKeyForAliasM(alias: String): SecretKey {
         val secretKeyEntry = (keyStore.getEntry(alias, null) as? KeyStore.SecretKeyEntry)
@@ -176,6 +204,13 @@ internal class SecretStoringUtils @Inject constructor(
                     .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                     .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                     .setKeySize(128)
+                    .apply {
+                        setUserAuthenticationRequired(keyNeedsUserAuthentication)
+                        if (buildVersionSdkIntProvider.get() >= Build.VERSION_CODES.N) {
+                            setInvalidatedByBiometricEnrollment(true)
+                        }
+                    }
+                    .setUserAuthenticationRequired(keyNeedsUserAuthentication)
                     .build()
             generator.init(keyGenSpec)
             return generator.generateKey()
@@ -216,19 +251,16 @@ internal class SecretStoringUtils @Inject constructor(
     }
 
     @RequiresApi(Build.VERSION_CODES.M)
-    private fun encryptStringM(text: String, keyAlias: String): ByteArray {
-        val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias)
-
-        val cipher = Cipher.getInstance(AES_MODE)
-        cipher.init(Cipher.ENCRYPT_MODE, secretKey)
+    private fun encryptBytesM(byteArray: ByteArray, keyAlias: String): ByteArray {
+        val cipher = getEncryptCipher(keyAlias)
         val iv = cipher.iv
         // we happen the iv to the final result
-        val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8))
+        val encryptedBytes: ByteArray = cipher.doFinal(byteArray)
         return formatMMake(iv, encryptedBytes)
     }
 
     @RequiresApi(Build.VERSION_CODES.M)
-    private fun decryptStringM(inputStream: InputStream, keyAlias: String): String {
+    private fun decryptBytesM(inputStream: InputStream, keyAlias: String): ByteArray {
         val (iv, encryptedText) = formatMExtract(inputStream)
 
         val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias)
@@ -237,10 +269,10 @@ internal class SecretStoringUtils @Inject constructor(
         val spec = GCMParameterSpec(128, iv)
         cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
 
-        return String(cipher.doFinal(encryptedText), Charsets.UTF_8)
+        return cipher.doFinal(encryptedText)
     }
 
-    private fun encryptString(text: String, keyAlias: String): ByteArray {
+    private fun encryptBytes(byteArray: ByteArray, keyAlias: String): ByteArray {
         // we generate a random symmetric key
         val key = ByteArray(16)
         secureRandom.nextBytes(key)
@@ -252,12 +284,12 @@ internal class SecretStoringUtils @Inject constructor(
         val cipher = Cipher.getInstance(AES_MODE)
         cipher.init(Cipher.ENCRYPT_MODE, sKey)
         val iv = cipher.iv
-        val encryptedBytes: ByteArray = cipher.doFinal(text.toByteArray(Charsets.UTF_8))
+        val encryptedBytes: ByteArray = cipher.doFinal(byteArray)
 
         return format1Make(encryptedKey, iv, encryptedBytes)
     }
 
-    private fun decryptString(inputStream: InputStream, keyAlias: String): String {
+    private fun decryptBytes(inputStream: InputStream, keyAlias: String): ByteArray {
         val (encryptedKey, iv, encrypted) = format1Extract(inputStream)
 
         // we need to decrypt the key
@@ -266,16 +298,13 @@ internal class SecretStoringUtils @Inject constructor(
         val spec = GCMParameterSpec(128, iv)
         cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sKeyBytes, "AES"), spec)
 
-        return String(cipher.doFinal(encrypted), Charsets.UTF_8)
+        return cipher.doFinal(encrypted)
     }
 
     @RequiresApi(Build.VERSION_CODES.M)
     @Throws(IOException::class)
     private fun saveSecureObjectM(keyAlias: String, output: OutputStream, writeObject: Any) {
-        val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias)
-
-        val cipher = Cipher.getInstance(AES_MODE)
-        cipher.init(Cipher.ENCRYPT_MODE, secretKey/*, spec*/)
+        val cipher = getEncryptCipher(keyAlias)
         val iv = cipher.iv
 
         val bos1 = ByteArrayOutputStream()
@@ -362,10 +391,8 @@ internal class SecretStoringUtils @Inject constructor(
 
     @Throws(Exception::class)
     private fun rsaEncrypt(alias: String, secret: ByteArray): ByteArray {
-        val privateKeyEntry = getOrGenerateKeyPairForAlias(alias)
         // Encrypt the text
-        val inputCipher = Cipher.getInstance(RSA_MODE)
-        inputCipher.init(Cipher.ENCRYPT_MODE, privateKeyEntry.certificate.publicKey)
+        val inputCipher = getEncryptCipher(alias)
 
         val outputStream = ByteArrayOutputStream()
         CipherOutputStream(outputStream, inputCipher).use {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecureStorageModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecureStorageModule.kt
new file mode 100644
index 0000000000000000000000000000000000000000..37a40fd677d79984a9d64215c58a83003be9a64a
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecureStorageModule.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.securestorage
+
+import android.content.Context
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import org.matrix.android.sdk.api.util.BuildVersionSdkIntProvider
+import org.matrix.android.sdk.api.util.DefaultBuildVersionSdkIntProvider
+import java.security.KeyStore
+
+@Module
+internal abstract class SecureStorageModule {
+
+    @Module
+    companion object {
+        @Provides
+        fun provideKeyStore(): KeyStore = KeyStore.getInstance("AndroidKeyStore").also { it.load(null) }
+
+        @Provides
+        fun provideSecretStoringUtils(
+                context: Context,
+                keyStore: KeyStore,
+                buildVersionSdkIntProvider: BuildVersionSdkIntProvider,
+        ): SecretStoringUtils = SecretStoringUtils(context, keyStore, buildVersionSdkIntProvider)
+    }
+
+    @Binds
+    abstract fun bindBuildVersionSdkIntProvider(provider: DefaultBuildVersionSdkIntProvider): BuildVersionSdkIntProvider
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SecureStorageService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecureStorageService.kt
similarity index 93%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SecureStorageService.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecureStorageService.kt
index 6b75c94cb2ef2f057bdf99f49a7e03b947f6ef5d..e217611d9691661ebe15f52e538fef91f4ea3423 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/securestorage/SecureStorageService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecureStorageService.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.api.session.securestorage
+package org.matrix.android.sdk.api.securestorage
 
 import java.io.InputStream
 import java.io.OutputStream
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt
index b3a629094cb20d79a751600b795c69c8ed6638a1..1b01239de5222f1b0deca3a3660c862ec987cf81 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt
@@ -17,6 +17,7 @@
 package org.matrix.android.sdk.api.session
 
 import androidx.annotation.MainThread
+import io.realm.RealmConfiguration
 import okhttp3.OkHttpClient
 import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.auth.data.SessionParams
@@ -46,7 +47,6 @@ import org.matrix.android.sdk.api.session.pushrules.PushRuleService
 import org.matrix.android.sdk.api.session.room.RoomDirectoryService
 import org.matrix.android.sdk.api.session.room.RoomService
 import org.matrix.android.sdk.api.session.search.SearchService
-import org.matrix.android.sdk.api.session.securestorage.SecureStorageService
 import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
 import org.matrix.android.sdk.api.session.signout.SignOutService
 import org.matrix.android.sdk.api.session.space.SpaceService
@@ -199,11 +199,6 @@ interface Session {
      */
     fun syncService(): SyncService
 
-    /**
-     * Returns the SecureStorageService associated with the session.
-     */
-    fun secureStorageService(): SecureStorageService
-
     /**
      * Returns the ProfileService associated with the session.
      */
@@ -334,7 +329,12 @@ interface Session {
     fun getUiaSsoFallbackUrl(authenticationSessionId: String): String
 
     /**
-     * Maintenance API, allows to print outs info on DB size to logcat.
+     * Debug API, will print out info on DB size to logcat.
      */
     fun logDbUsageInfo()
+
+    /**
+     * Debug API, return the list of all RealmConfiguration used by this session.
+     */
+    fun getRealmConfigurations(): List<RealmConfiguration>
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/account/AccountService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/account/AccountService.kt
index e3d52adfc58da34c9825536ebeaab226e5ea32a0..094c66f6f7bf11b868535442e22c2ff99c69a96d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/account/AccountService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/account/AccountService.kt
@@ -24,13 +24,13 @@ import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
 interface AccountService {
     /**
      * Ask the homeserver to change the password.
+     *
      * @param password Current password.
      * @param newPassword New password
+     * @param logoutAllDevices defaults to true, all devices will be logged out. False values will only be taken into account
+     * if [org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities.canControlLogoutDevices] is true.
      */
-    suspend fun changePassword(
-            password: String,
-            newPassword: String
-    )
+    suspend fun changePassword(password: String, newPassword: String, logoutAllDevices: Boolean = true)
 
     /**
      * Deactivate the account.
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 9cc87b6f718476c8b0ea5ee530926e9507688427..638da1180498472bb9e936248dcf045611c0a9b9 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
@@ -171,8 +171,6 @@ interface CryptoService {
     fun getSharedWithInfo(roomId: String?, sessionId: String): MXUsersDevicesMap<Int>
     fun getWithHeldMegolmSession(roomId: String, sessionId: String): RoomKeyWithHeldContent?
 
-    fun logDbUsageInfo()
-
     /**
      * Perform any background tasks that can be done before a message is ready to
      * send, in order to speed up sending of the message.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt
index c78fb9cf7928c372435b2930d6d0b8f3f9be3eda..b5d6d891e400a3f18e132c85dfbc9683efd8c6bb 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt
@@ -54,7 +54,12 @@ data class HomeServerCapabilities(
         /**
          * True if the home server support threading.
          */
-        val canUseThreading: Boolean = false
+        val canUseThreading: Boolean = false,
+
+        /**
+         * True if the home server supports controlling the logout of all devices when changing password.
+         */
+        val canControlLogoutDevices: Boolean = false
 ) {
 
     enum class RoomCapabilitySupport {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomExtensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomExtensions.kt
index 0e631427bd320aefbf516ca120f16b278a31071d..b30c60554f2811c19f9c046a2e711fe465be5ea8 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomExtensions.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomExtensions.kt
@@ -16,18 +16,21 @@
 
 package org.matrix.android.sdk.api.session.room
 
-import org.matrix.android.sdk.api.query.QueryStringValue
+import org.matrix.android.sdk.api.query.QueryStateEventValue
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
 
 /**
  * Get a TimelineEvent using the TimelineService of a Room.
+ * @param eventId The id of the event to retrieve
  */
 fun Room.getTimelineEvent(eventId: String): TimelineEvent? =
         timelineService().getTimelineEvent(eventId)
 
 /**
  * Get a StateEvent using the StateService of a Room.
+ * @param eventType The type of the event, see [org.matrix.android.sdk.api.session.events.model.EventType].
+ * @param stateKey the query which will be done on the stateKey.
  */
-fun Room.getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event? =
+fun Room.getStateEvent(eventType: String, stateKey: QueryStateEventValue): Event? =
         stateService().getStateEvent(eventType, stateKey)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt
index dd48d51f45a6a63ce67f4ac143e5581b6e85633c..ada3dc85d78a66291a9cd7e89af6fb66335ad30c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt
@@ -16,12 +16,58 @@
 
 package org.matrix.android.sdk.api.session.room.location
 
+import androidx.annotation.MainThread
 import androidx.lifecycle.LiveData
 import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
+import org.matrix.android.sdk.api.util.Cancelable
+import org.matrix.android.sdk.api.util.Optional
 
 /**
  * Manage all location sharing related features.
  */
 interface LocationSharingService {
+    /**
+     * Send a static location event to the room.
+     * @param latitude required latitude of the location
+     * @param longitude required longitude of the location
+     * @param uncertainty Accuracy of the location in meters
+     * @param isUserLocation indicates whether the location data corresponds to the user location or not (pinned location)
+     */
+    suspend fun sendStaticLocation(latitude: Double, longitude: Double, uncertainty: Double?, isUserLocation: Boolean): Cancelable
+
+    /**
+     * Send a live location event to the room.
+     * To get the beacon info event id, [startLiveLocationShare] must be called before sending live location updates.
+     * @param beaconInfoEventId event id of the initial beacon info state event
+     * @param latitude required latitude of the location
+     * @param longitude required longitude of the location
+     * @param uncertainty Accuracy of the location in meters
+     */
+    suspend fun sendLiveLocation(beaconInfoEventId: String, latitude: Double, longitude: Double, uncertainty: Double?): Cancelable
+
+    /**
+     * Starts sharing live location in the room.
+     * @param timeoutMillis timeout of the live in milliseconds
+     * @return the result of the update of the live
+     */
+    suspend fun startLiveLocationShare(timeoutMillis: Long): UpdateLiveLocationShareResult
+
+    /**
+     * Stops sharing live location in the room.
+     * @return the result of the update of the live
+     */
+    suspend fun stopLiveLocationShare(): UpdateLiveLocationShareResult
+
+    /**
+     * Returns a LiveData on the list of current running live location shares.
+     */
+    @MainThread
     fun getRunningLiveLocationShareSummaries(): LiveData<List<LiveLocationShareAggregatedSummary>>
+
+    /**
+     * Returns a LiveData on the live location share summary with the given eventId.
+     * @param beaconInfoEventId event id of the initial beacon info state event
+     */
+    @MainThread
+    fun getLiveLocationShareSummary(beaconInfoEventId: String): LiveData<Optional<LiveLocationShareAggregatedSummary>>
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/UpdateLiveLocationShareResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/UpdateLiveLocationShareResult.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6f8c03be4642a5a5859d4ca7075303164d5b5d0d
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/UpdateLiveLocationShareResult.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.room.location
+
+/**
+ * Represents the result of an update of live location share like a start or a stop.
+ */
+sealed interface UpdateLiveLocationShareResult {
+    data class Success(val beaconEventId: String) : UpdateLiveLocationShareResult
+    data class Failure(val error: Throwable) : UpdateLiveLocationShareResult
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt
index a1fd3bd2ecc5c3c2251568984db63ace128a96a1..e0a7846167e0e868ec213704ded41284e1b5ff26 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt
@@ -22,7 +22,7 @@ import com.squareup.moshi.JsonClass
 @JsonClass(generateAdapter = true)
 data class LocationInfo(
         /**
-         * Required. RFC5870 formatted geo uri 'geo:latitude,longitude;uncertainty' like 'geo:40.05,29.24;30' representing this location.
+         * Required. RFC5870 formatted geo uri 'geo:latitude,longitude;u=uncertainty' like 'geo:40.05,29.24;u=30' representing this location.
          */
         @Json(name = "uri") val geoUri: String? = null,
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt
index 0a66a6e400d5b3a75a7549ad15863890d379dd71..30420fd3c7028089852754fcb01fdab675ce4c8d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt
@@ -35,7 +35,7 @@ data class MessageLocationContent(
         @Json(name = "body") override val body: String,
 
         /**
-         * Required. RFC5870 formatted geo uri 'geo:latitude,longitude;uncertainty' like 'geo:40.05,29.24;30' representing this location.
+         * Required. RFC5870 formatted geo uri 'geo:latitude,longitude;u=uncertainty' like 'geo:40.05,29.24;u=30' representing this location.
          */
         @Json(name = "geo_uri") val geoUri: String,
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt
index 81b034a809b3debb3dd2faf8d13b241b0ae36c6b..ee31d5959ea93ff426d300c4ff70e1f6e10606e1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt
@@ -25,4 +25,7 @@ data class PollCreationInfo(
         @Json(name = "kind") val kind: PollType? = PollType.DISCLOSED_UNSTABLE,
         @Json(name = "max_selections") val maxSelections: Int = 1,
         @Json(name = "answers") val answers: List<PollAnswer>? = null
-)
+) {
+
+    fun isUndisclosed() = kind in listOf(PollType.UNDISCLOSED_UNSTABLE, PollType.UNDISCLOSED)
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
index 661c3be5bd01a703d70bcba346b44f853e50bc54..9cf062356f3e132159307746e4681d69d23cbf68 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
@@ -142,24 +142,6 @@ interface SendService {
      */
     fun resendMediaMessage(localEcho: TimelineEvent): Cancelable
 
-    /**
-     * Send a location event to the room.
-     * @param latitude required latitude of the location
-     * @param longitude required longitude of the location
-     * @param uncertainty Accuracy of the location in meters
-     * @param isUserLocation indicates whether the location data corresponds to the user location or not
-     */
-    fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?, isUserLocation: Boolean): Cancelable
-
-    /**
-     * Send a live location event to the room. beacon_info state event has to be sent before sending live location updates.
-     * @param beaconInfoEventId event id of the initial beacon info state event
-     * @param latitude required latitude of the location
-     * @param longitude required longitude of the location
-     * @param uncertainty Accuracy of the location in meters
-     */
-    fun sendLiveLocation(beaconInfoEventId: String, latitude: Double, longitude: Double, uncertainty: Double?): Cancelable
-
     /**
      * Remove this failed message from the timeline.
      * @param localEcho the unsent local echo
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt
index c79171f1564cb0e6c7960f9fd4b6080e806f2240..6ca63c2c49b75fafbd4a717564c0d9e02fb47a3f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateService.kt
@@ -18,7 +18,7 @@ package org.matrix.android.sdk.api.session.room.state
 
 import android.net.Uri
 import androidx.lifecycle.LiveData
-import org.matrix.android.sdk.api.query.QueryStringValue
+import org.matrix.android.sdk.api.query.QueryStateEventValue
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.room.model.GuestAccess
 import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
@@ -66,19 +66,6 @@ interface StateService {
      */
     suspend fun deleteAvatar()
 
-    /**
-     * Stops sharing live location in the room.
-     * @param userId user id
-     */
-    suspend fun stopLiveLocation(userId: String)
-
-    /**
-     * Returns beacon info state event of a user.
-     * @param userId user id who is sharing location
-     * @param filterOnlyLive filters only ongoing live location sharing beacons if true else ended event is included
-     */
-    suspend fun getLiveLocationBeaconInfo(userId: String, filterOnlyLive: Boolean): Event?
-
     /**
      * Send a state event to the room.
      * @param eventType The type of event to send.
@@ -93,28 +80,28 @@ interface StateService {
      * @param eventType An eventType.
      * @param stateKey the query which will be done on the stateKey
      */
-    fun getStateEvent(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): Event?
+    fun getStateEvent(eventType: String, stateKey: QueryStateEventValue): Event?
 
     /**
      * Get a live state event of the room.
      * @param eventType An eventType.
      * @param stateKey the query which will be done on the stateKey
      */
-    fun getStateEventLive(eventType: String, stateKey: QueryStringValue = QueryStringValue.NoCondition): LiveData<Optional<Event>>
+    fun getStateEventLive(eventType: String, stateKey: QueryStateEventValue): LiveData<Optional<Event>>
 
     /**
      * Get state events of the room.
      * @param eventTypes Set of eventType. If empty, all state events will be returned
      * @param stateKey the query which will be done on the stateKey
      */
-    fun getStateEvents(eventTypes: Set<String>, stateKey: QueryStringValue = QueryStringValue.NoCondition): List<Event>
+    fun getStateEvents(eventTypes: Set<String>, stateKey: QueryStateEventValue): List<Event>
 
     /**
      * Get live state events of the room.
      * @param eventTypes Set of eventType to observe. If empty, all state events will be observed
      * @param stateKey the query which will be done on the stateKey
      */
-    fun getStateEventsLive(eventTypes: Set<String>, stateKey: QueryStringValue = QueryStringValue.NoCondition): LiveData<List<Event>>
+    fun getStateEventsLive(eventTypes: Set<String>, stateKey: QueryStateEventValue): LiveData<List<Event>>
 
     suspend fun setJoinRulePublic()
     suspend fun setJoinRuleInviteOnly()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateServiceExtension.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateServiceExtension.kt
index 9e45fc126dbad4b82ddc4c3c8b011229c86d7ecc..6a9506fd9e09b296f7484d0acb1b6c3307362aa3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateServiceExtension.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/state/StateServiceExtension.kt
@@ -26,7 +26,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
  * Return true if a room can be joined by anyone (RoomJoinRules.PUBLIC).
  */
 fun StateService.isPublic(): Boolean {
-    return getStateEvent(EventType.STATE_ROOM_JOIN_RULES, QueryStringValue.NoCondition)
+    return getStateEvent(EventType.STATE_ROOM_JOIN_RULES, QueryStringValue.IsEmpty)
             ?.content
             ?.toModel<RoomJoinRulesContent>()
             ?.joinRules == RoomJoinRules.PUBLIC
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetService.kt
index 8ad6500d25bb6c7b366309a3151aa1fed2855dc4..c2094f46bd4881fce4058ca82500a1e8eaeb9a75 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/WidgetService.kt
@@ -17,7 +17,7 @@
 package org.matrix.android.sdk.api.session.widgets
 
 import androidx.lifecycle.LiveData
-import org.matrix.android.sdk.api.query.QueryStringValue
+import org.matrix.android.sdk.api.query.QueryStateEventValue
 import org.matrix.android.sdk.api.session.events.model.Content
 import org.matrix.android.sdk.api.session.widgets.model.Widget
 
@@ -49,7 +49,7 @@ interface WidgetService {
      */
     fun getRoomWidgets(
             roomId: String,
-            widgetId: QueryStringValue = QueryStringValue.NoCondition,
+            widgetId: QueryStateEventValue,
             widgetTypes: Set<String>? = null,
             excludedTypes: Set<String>? = null
     ): List<Widget>
@@ -70,7 +70,7 @@ interface WidgetService {
      */
     fun getRoomWidgetsLive(
             roomId: String,
-            widgetId: QueryStringValue = QueryStringValue.NoCondition,
+            widgetId: QueryStateEventValue,
             widgetTypes: Set<String>? = null,
             excludedTypes: Set<String>? = null
     ): LiveData<List<Widget>>
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/BuildVersionSdkIntProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/BuildVersionSdkIntProvider.kt
similarity index 87%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/BuildVersionSdkIntProvider.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/BuildVersionSdkIntProvider.kt
index 515656049a0fe1af25056af5f64aae720a0fd122..b7ea187ec508b7453f821973187eea8aa0b0e7e5 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/BuildVersionSdkIntProvider.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/BuildVersionSdkIntProvider.kt
@@ -14,9 +14,9 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.util.system
+package org.matrix.android.sdk.api.util
 
-internal interface BuildVersionSdkIntProvider {
+interface BuildVersionSdkIntProvider {
     /**
      * Return the current version of the Android SDK.
      */
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/DefaultBuildVersionSdkIntProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/DefaultBuildVersionSdkIntProvider.kt
similarity index 85%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/DefaultBuildVersionSdkIntProvider.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/DefaultBuildVersionSdkIntProvider.kt
index 806c6e97351b4289fdef0a758fc938228eaa2839..7f0024cafa8495a5bce7c3b1d21c7047eab1d2d3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/DefaultBuildVersionSdkIntProvider.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/DefaultBuildVersionSdkIntProvider.kt
@@ -14,12 +14,12 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.util.system
+package org.matrix.android.sdk.api.util
 
 import android.os.Build
 import javax.inject.Inject
 
-internal class DefaultBuildVersionSdkIntProvider @Inject constructor() :
+class DefaultBuildVersionSdkIntProvider @Inject constructor() :
         BuildVersionSdkIntProvider {
     override fun get() = Build.VERSION.SDK_INT
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/SessionManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/SessionManager.kt
index bd2dac9e3ccaef4f3c31870a2f33942f4f9e01e3..5f5bb1f9514d452645eadb80f5a639dc885ef44a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/SessionManager.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/SessionManager.kt
@@ -40,6 +40,13 @@ internal class SessionManager @Inject constructor(
         return getOrCreateSessionComponent(sessionParams)
     }
 
+    fun getLastSession(): Session? {
+        val sessionParams = sessionParamsStore.getLast()
+        return sessionParams?.let {
+            getOrCreateSession(it)
+        }
+    }
+
     fun getOrCreateSession(sessionParams: SessionParams): Session {
         return getOrCreateSessionComponent(sessionParams).session()
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt
index 61a423669c46ea9e43fae37217edadc743246712..9d6b018a672cc113b3cf5e92820ba9d41b3f92b0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt
@@ -40,6 +40,7 @@ import org.matrix.android.sdk.internal.auth.login.DefaultLoginWizard
 import org.matrix.android.sdk.internal.auth.login.DirectLoginTask
 import org.matrix.android.sdk.internal.auth.registration.DefaultRegistrationWizard
 import org.matrix.android.sdk.internal.auth.version.Versions
+import org.matrix.android.sdk.internal.auth.version.doesServerSupportLogoutDevices
 import org.matrix.android.sdk.internal.auth.version.isLoginAndRegistrationSupportedBySdk
 import org.matrix.android.sdk.internal.auth.version.isSupportedBySdk
 import org.matrix.android.sdk.internal.di.Unauthenticated
@@ -73,10 +74,7 @@ internal class DefaultAuthenticationService @Inject constructor(
     }
 
     override fun getLastAuthenticatedSession(): Session? {
-        val sessionParams = sessionParamsStore.getLast()
-        return sessionParams?.let {
-            sessionManager.getOrCreateSession(it)
-        }
+        return sessionManager.getLastSession()
     }
 
     override suspend fun getLoginFlowOfSession(sessionId: String): LoginFlowResult {
@@ -295,7 +293,8 @@ internal class DefaultAuthenticationService @Inject constructor(
                 ssoIdentityProviders = loginFlowResponse.flows.orEmpty().firstOrNull { it.type == LoginFlowTypes.SSO }?.ssoIdentityProvider,
                 isLoginAndRegistrationSupported = versions.isLoginAndRegistrationSupportedBySdk(),
                 homeServerUrl = homeServerUrl,
-                isOutdatedHomeserver = !versions.isSupportedBySdk()
+                isOutdatedHomeserver = !versions.isSupportedBySdk(),
+                isLogoutDevicesSupported = versions.doesServerSupportLogoutDevices()
         )
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt
index 20b056f1c79bcd3300a09d281e89e8c7a122e5d2..656a4f671bd46488bedd98cebaca32363a8f3fca 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt
@@ -121,12 +121,13 @@ internal class DefaultLoginWizard(
                 .also { pendingSessionStore.savePendingSessionData(it) }
     }
 
-    override suspend fun resetPasswordMailConfirmed(newPassword: String) {
+    override suspend fun resetPasswordMailConfirmed(newPassword: String, logoutAllDevices: Boolean) {
         val resetPasswordData = pendingSessionData.resetPasswordData ?: throw IllegalStateException("Developer error - Must call resetPassword first")
         val param = ResetPasswordMailConfirmed.create(
                 pendingSessionData.clientSecret,
                 resetPasswordData.addThreePidRegistrationResponse.sid,
-                newPassword
+                newPassword,
+                logoutAllDevices
         )
 
         executeRequest(null) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/ResetPasswordMailConfirmed.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/ResetPasswordMailConfirmed.kt
index 4e0c000f87f4e769451907448b2a39f832da195d..01481f70dcc5b102aa20902fd20a616d35ec1e85 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/ResetPasswordMailConfirmed.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/ResetPasswordMailConfirmed.kt
@@ -30,13 +30,17 @@ internal data class ResetPasswordMailConfirmed(
 
         // the new password
         @Json(name = "new_password")
-        val newPassword: String? = null
+        val newPassword: String? = null,
+
+        @Json(name = "logout_devices")
+        val logoutDevices: Boolean? = null
 ) {
     companion object {
-        fun create(clientSecret: String, sid: String, newPassword: String): ResetPasswordMailConfirmed {
+        fun create(clientSecret: String, sid: String, newPassword: String, logoutDevices: Boolean?): ResetPasswordMailConfirmed {
             return ResetPasswordMailConfirmed(
                     auth = AuthParams.createForResetPassword(clientSecret, sid),
-                    newPassword = newPassword
+                    newPassword = newPassword,
+                    logoutDevices = logoutDevices
             )
         }
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt
index cd38b68a85ec10a9b4101fc9c44061c6d2b80674..75639c6a218d97e04fe1f2a3212d2eb75a980168 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/HomeServerVersion.kt
@@ -58,6 +58,7 @@ internal data class HomeServerVersion(
         val r0_4_0 = HomeServerVersion(major = 0, minor = 4, patch = 0)
         val r0_5_0 = HomeServerVersion(major = 0, minor = 5, patch = 0)
         val r0_6_0 = HomeServerVersion(major = 0, minor = 6, patch = 0)
+        val r0_6_1 = HomeServerVersion(major = 0, minor = 6, patch = 1)
         val v1_3_0 = HomeServerVersion(major = 1, minor = 3, patch = 0)
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt
index cee4b12138ed45063314d4c0a8399ba44efd01ee..915b25134b2f0f687a7a443796db800f50c6e0d2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt
@@ -111,6 +111,15 @@ private fun Versions.doesServerSeparatesAddAndBind(): Boolean {
             unstableFeatures?.get(FEATURE_SEPARATE_ADD_AND_BIND) ?: false
 }
 
+/**
+ * Indicate if the server supports MSC2457 `logout_devices` parameter when setting a new password.
+ *
+ * @return true if logout_devices is supported
+ */
+internal fun Versions.doesServerSupportLogoutDevices(): Boolean {
+    return getMaxVersion() >= HomeServerVersion.r0_6_1
+}
+
 private fun Versions.getMaxVersion(): HomeServerVersion {
     return supportedVersions
             ?.mapNotNull { HomeServerVersion.parse(it) }
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 719f366518fd685e41ebe0f75dda37bdd3321e9d..e0bcde2296369032d1140b01a51e0cfa831f1a55 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
@@ -1298,10 +1298,6 @@ internal class DefaultCryptoService @Inject constructor(
         return cryptoStore.getWithHeldMegolmSession(roomId, sessionId)
     }
 
-    override fun logDbUsageInfo() {
-        cryptoStore.logDbUsageInfo()
-    }
-
     override fun prepareToEncrypt(roomId: String, callback: MatrixCallback<Unit>) {
         cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
             Timber.tag(loggerTag.value).d("prepareToEncrypt() roomId:$roomId Check room members up to date")
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 b18de34329452f229e429a1027dd257939440bf3..b5b8d8e97446d08d5cf5494738824f3066f0dd49 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
@@ -513,6 +513,5 @@ internal interface IMXCryptoStore {
     fun setDeviceKeysUploaded(uploaded: Boolean)
     fun areDeviceKeysUploaded(): Boolean
     fun tidyUpDataBase()
-    fun logDbUsageInfo()
     fun getOutgoingRoomKeyRequests(inStates: Set<OutgoingRoomKeyRequestState>): List<OutgoingKeyRequest>
 }
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 c56e4d320b82ec41cb6a9fb22f42ba9c9c0aaccd..028d8f73f9556bcdb8002704e8d107b3e36d8e84 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
@@ -88,7 +88,6 @@ import org.matrix.android.sdk.internal.crypto.store.db.query.get
 import org.matrix.android.sdk.internal.crypto.store.db.query.getById
 import org.matrix.android.sdk.internal.crypto.store.db.query.getOrCreate
 import org.matrix.android.sdk.internal.crypto.util.RequestIdHelper
-import org.matrix.android.sdk.internal.database.tools.RealmDebugTools
 import org.matrix.android.sdk.internal.di.CryptoDatabase
 import org.matrix.android.sdk.internal.di.DeviceId
 import org.matrix.android.sdk.internal.di.MoshiProvider
@@ -1709,11 +1708,4 @@ internal class RealmCryptoStore @Inject constructor(
             // Can we do something for WithHeldSessionEntity?
         }
     }
-
-    /**
-     * Prints out database info.
-     */
-    override fun logDbUsageInfo() {
-        RealmDebugTools(realmConfiguration).logInfo("Crypto")
-    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt
index 9f123f0c08638163b26a6c3288793c29656fb693..821663bcff1f8255576454bb39e4a4ccc33dc9d3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt
@@ -62,7 +62,7 @@ internal class VerificationMessageProcessor @Inject constructor(
         // If the request is in the future by more than 5 minutes or more than 10 minutes in the past,
         // the message should be ignored by the receiver.
 
-        if (event.ageLocalTs != null && !VerificationService.isValidRequest(event.ageLocalTs, clock.epochMillis())) return Unit.also {
+        if (!VerificationService.isValidRequest(event.ageLocalTs, clock.epochMillis())) return Unit.also {
             Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated age:$event.ageLocalTs ms")
         }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmKeysUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmKeysUtils.kt
index b3a039d11927f9badf9b04d7df0980acd4c964ba..86355ceaa848eba98adeef3924635a6858e71ad7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmKeysUtils.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmKeysUtils.kt
@@ -21,7 +21,7 @@ import androidx.core.content.edit
 import io.realm.Realm
 import io.realm.RealmConfiguration
 import org.matrix.android.sdk.BuildConfig
-import org.matrix.android.sdk.internal.session.securestorage.SecretStoringUtils
+import org.matrix.android.sdk.api.securestorage.SecretStoringUtils
 import timber.log.Timber
 import java.security.SecureRandom
 import javax.inject.Inject
@@ -40,7 +40,7 @@ import javax.inject.Inject
  */
 internal class RealmKeysUtils @Inject constructor(
         context: Context,
-        private val secretStoringUtils: SecretStoringUtils
+        private val secretStoringUtils: SecretStoringUtils,
 ) {
 
     private val rng = SecureRandom()
@@ -71,7 +71,7 @@ internal class RealmKeysUtils @Inject constructor(
     private fun createAndSaveKeyForDatabase(alias: String): ByteArray {
         val key = generateKeyForRealm()
         val encodedKey = Base64.encodeToString(key, Base64.NO_PADDING)
-        val toStore = secretStoringUtils.securelyStoreString(encodedKey, alias)
+        val toStore = secretStoringUtils.securelyStoreBytes(encodedKey.toByteArray(), alias)
         sharedPreferences.edit {
             putString("${ENCRYPTED_KEY_PREFIX}_$alias", Base64.encodeToString(toStore, Base64.NO_PADDING))
         }
@@ -85,7 +85,7 @@ internal class RealmKeysUtils @Inject constructor(
     private fun extractKeyForDatabase(alias: String): ByteArray {
         val encryptedB64 = sharedPreferences.getString("${ENCRYPTED_KEY_PREFIX}_$alias", null)
         val encryptedKey = Base64.decode(encryptedB64, Base64.NO_PADDING)
-        val b64 = secretStoringUtils.loadSecureSecret(encryptedKey, alias)
+        val b64 = secretStoringUtils.loadSecureSecretBytes(encryptedKey, alias)
         return Base64.decode(b64, Base64.NO_PADDING)
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
index 6a8589bc5e26bafdd377cab9b15a055b6e7223e9..665567bf2a4d3ebdf10ee4d3f8bf3e6b3157e0ce 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt
@@ -48,6 +48,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo027
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo028
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo029
 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo030
+import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo031
 import org.matrix.android.sdk.internal.util.Normalizer
 import timber.log.Timber
 import javax.inject.Inject
@@ -62,7 +63,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
     override fun equals(other: Any?) = other is RealmSessionStoreMigration
     override fun hashCode() = 1000
 
-    val schemaVersion = 30L
+    val schemaVersion = 31L
 
     override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
         Timber.d("Migrating Realm Session from $oldVersion to $newVersion")
@@ -97,5 +98,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
         if (oldVersion < 28) MigrateSessionTo028(realm).perform()
         if (oldVersion < 29) MigrateSessionTo029(realm).perform()
         if (oldVersion < 30) MigrateSessionTo030(realm).perform()
+        if (oldVersion < 31) MigrateSessionTo031(realm).perform()
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt
index 79a99cdfac4c282d5d9877fed8ba7278a930ab59..0a6d4bf833285e5312630fdb373faa39e448b6a0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt
@@ -271,7 +271,7 @@ private fun HashMap<String, RoomMemberContent?>.addSenderState(realm: Realm, roo
  * Create an EventEntity for the root thread event or get an existing one.
  */
 private fun createEventEntity(realm: Realm, roomId: String, event: Event, currentTimeMillis: Long): EventEntity {
-    val ageLocalTs = event.unsignedData?.age?.let { currentTimeMillis - it }
+    val ageLocalTs = currentTimeMillis - (event.unsignedData?.age ?: 0)
     return event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
 }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt
index 5b60c536425155562f3cfad3528e5a3793eb9f7b..0f0a847c783f3836b7d8fe223f9bd48ff2e19f80 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt
@@ -130,7 +130,7 @@ internal fun EventEntity.asDomain(castJsonNumbers: Boolean = false): Event {
 internal fun Event.toEntity(
         roomId: String,
         sendState: SendState,
-        ageLocalTs: Long?,
+        ageLocalTs: Long,
         contentToInject: String? = null
 ): EventEntity {
     return EventMapper.map(this, roomId).apply {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt
index 20af43530c4821a11892dc1fd8d37c8dbb1a5da3..184a0108b910191cacf7380f48b7bf96f03230db 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt
@@ -42,7 +42,8 @@ internal object HomeServerCapabilitiesMapper {
                 lastVersionIdentityServerSupported = entity.lastVersionIdentityServerSupported,
                 defaultIdentityServerUrl = entity.defaultIdentityServerUrl,
                 roomVersions = mapRoomVersion(entity.roomVersionsJson),
-                canUseThreading = entity.canUseThreading
+                canUseThreading = entity.canUseThreading,
+                canControlLogoutDevices = entity.canControlLogoutDevices
         )
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapper.kt
index 9460e4c6ba228a5264826819c88fc9e760b2d349..4a4c730a0be7fd2b7b2f2a4dfaf299498634917d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapper.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/LiveLocationShareAggregatedSummaryMapper.kt
@@ -16,15 +16,17 @@
 
 package org.matrix.android.sdk.internal.database.mapper
 
+import com.zhuinden.monarchy.Monarchy
 import org.matrix.android.sdk.api.session.events.model.toModel
 import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
 import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
 import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
 import javax.inject.Inject
 
-internal class LiveLocationShareAggregatedSummaryMapper @Inject constructor() {
+internal class LiveLocationShareAggregatedSummaryMapper @Inject constructor() :
+        Monarchy.Mapper<LiveLocationShareAggregatedSummary, LiveLocationShareAggregatedSummaryEntity> {
 
-    fun map(entity: LiveLocationShareAggregatedSummaryEntity): LiveLocationShareAggregatedSummary {
+    override fun map(entity: LiveLocationShareAggregatedSummaryEntity): LiveLocationShareAggregatedSummary {
         return LiveLocationShareAggregatedSummary(
                 userId = entity.userId,
                 isActive = entity.isActive,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo029.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo029.kt
index aebca11c2bb3b409057d09de82dbe8d508d42604..17dc0f7c8270487595fe7031e1ac948d2e5cb91c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo029.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo029.kt
@@ -25,7 +25,7 @@ import org.matrix.android.sdk.internal.util.database.RealmMigrator
  * Migrating to:
  * Live location sharing aggregated summary: adding new field userId.
  */
-internal class MigrateSessionTo029(realm: DynamicRealm) : RealmMigrator(realm, 28) {
+internal class MigrateSessionTo029(realm: DynamicRealm) : RealmMigrator(realm, 29) {
 
     override fun doMigrate(realm: DynamicRealm) {
         realm.schema.get("LiveLocationShareAggregatedSummaryEntity")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo030.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo030.kt
index b9c611f5dde8c0d209229c60a3b8ba70759e3554..5d24b1433c63ba92b13e74fa148a7c3452dc25c2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo030.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo030.kt
@@ -52,6 +52,10 @@ internal class MigrateSessionTo030(realm: DynamicRealm) : RealmMigrator(realm, 3
             timelineEvents.deleteAllFromRealm()
         }
         chunks.deleteAllFromRealm()
-        Timber.d("MigrateSessionTo030: $nbOfDeletedChunks deleted chunk(s), $nbOfDeletedTimelineEvents deleted TimelineEvent(s) and $nbOfDeletedEvents deleted Event(s).")
+        Timber.d(
+                "MigrateSessionTo030: $nbOfDeletedChunks deleted chunk(s)," +
+                        " $nbOfDeletedTimelineEvents deleted TimelineEvent(s)" +
+                        " and $nbOfDeletedEvents deleted Event(s)."
+        )
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo031.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo031.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e278b7475670e46916e1d9ef2ae05e93504f8476
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo031.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.database.migration
+
+import io.realm.DynamicRealm
+import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
+import org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabilities
+import org.matrix.android.sdk.internal.util.database.RealmMigrator
+
+internal class MigrateSessionTo031(realm: DynamicRealm) : RealmMigrator(realm, 31) {
+
+    override fun doMigrate(realm: DynamicRealm) {
+        realm.schema.get("HomeServerCapabilitiesEntity")
+                ?.addField(HomeServerCapabilitiesEntityFields.CAN_CONTROL_LOGOUT_DEVICES, Boolean::class.java)
+                ?.forceRefreshOfHomeServerCapabilities()
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt
index 47a83f0ed998fbbd27713416a302b90bde103809..9d90973f8abded22d4ccb9b017badfc5a8351ed7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt
@@ -29,7 +29,8 @@ internal open class HomeServerCapabilitiesEntity(
         var lastVersionIdentityServerSupported: Boolean = false,
         var defaultIdentityServerUrl: String? = null,
         var lastUpdatedTimestamp: Long = 0L,
-        var canUseThreading: Boolean = false
+        var canUseThreading: Boolean = false,
+        var canControlLogoutDevices: Boolean = false
 ) : RealmObject() {
 
     companion object
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt
index 7dfeb6884abf247224c8561a2e6ab6b8a7b2d743..d69f251f6ffbf769227e29b722d0ce93a52fd72c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt
@@ -76,7 +76,7 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.findActiveLiveIn
         realm: Realm,
         roomId: String,
         userId: String,
-        ignoredEventId: String
+        ignoredEventId: String,
 ): List<LiveLocationShareAggregatedSummaryEntity> {
     return LiveLocationShareAggregatedSummaryEntity
             .whereRoomId(realm, roomId = roomId)
@@ -84,6 +84,7 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.findActiveLiveIn
             .equalTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true)
             .notEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, ignoredEventId)
             .findAll()
+            .toList()
 }
 
 /**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/debug/DebugModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/debug/DebugModule.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f392d39d7b7b53150bae04b4e5eaa85028a23614
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/debug/DebugModule.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.debug
+
+import dagger.Binds
+import dagger.Module
+import org.matrix.android.sdk.api.debug.DebugService
+
+@Module
+internal abstract class DebugModule {
+
+    @Binds
+    abstract fun bindDebugService(service: DefaultDebugService): DebugService
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/debug/DefaultDebugService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/debug/DefaultDebugService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..3f2e6fafc880bd9081968afd64bfe7fbbf3000f0
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/debug/DefaultDebugService.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.debug
+
+import io.realm.RealmConfiguration
+import org.matrix.android.sdk.api.debug.DebugService
+import org.matrix.android.sdk.internal.SessionManager
+import org.matrix.android.sdk.internal.database.tools.RealmDebugTools
+import org.matrix.android.sdk.internal.di.AuthDatabase
+import org.matrix.android.sdk.internal.di.GlobalDatabase
+import javax.inject.Inject
+
+internal class DefaultDebugService @Inject constructor(
+        @AuthDatabase private val realmConfigurationAuth: RealmConfiguration,
+        @GlobalDatabase private val realmConfigurationGlobal: RealmConfiguration,
+        private val sessionManager: SessionManager,
+) : DebugService {
+
+    override fun getAllRealmConfigurations(): List<RealmConfiguration> {
+        return sessionManager.getLastSession()?.getRealmConfigurations().orEmpty() +
+                realmConfigurationAuth +
+                realmConfigurationGlobal
+    }
+
+    override fun logDbUsageInfo() {
+        RealmDebugTools(realmConfigurationAuth).logInfo("Auth")
+        RealmDebugTools(realmConfigurationGlobal).logInfo("Global")
+        sessionManager.getLastSession()?.logDbUsageInfo()
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt
index 095916643cb8d5e7fc35e54a72d1e65cfa3af103..44ec90ed40f3c08aeecd5b3ed2dd8ab91df865f5 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/MatrixComponent.kt
@@ -28,10 +28,13 @@ import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.auth.AuthenticationService
 import org.matrix.android.sdk.api.auth.HomeServerHistoryService
 import org.matrix.android.sdk.api.raw.RawService
+import org.matrix.android.sdk.api.securestorage.SecureStorageModule
+import org.matrix.android.sdk.api.securestorage.SecureStorageService
 import org.matrix.android.sdk.api.settings.LightweightSettingsStorage
 import org.matrix.android.sdk.internal.SessionManager
 import org.matrix.android.sdk.internal.auth.AuthModule
 import org.matrix.android.sdk.internal.auth.SessionParamsStore
+import org.matrix.android.sdk.internal.debug.DebugModule
 import org.matrix.android.sdk.internal.raw.RawModule
 import org.matrix.android.sdk.internal.session.MockHttpInterceptor
 import org.matrix.android.sdk.internal.session.TestInterceptor
@@ -49,9 +52,11 @@ import java.io.File
             NetworkModule::class,
             AuthModule::class,
             RawModule::class,
+            DebugModule::class,
             SettingsModule::class,
             SystemModule::class,
-            NoOpTestModule::class
+            NoOpTestModule::class,
+            SecureStorageModule::class,
         ]
 )
 @MatrixScope
@@ -94,6 +99,8 @@ internal interface MatrixComponent {
 
     fun sessionManager(): SessionManager
 
+    fun secureStorageService(): SecureStorageService
+
     fun matrixWorkerFactory(): MatrixWorkerFactory
 
     fun inject(matrix: Matrix)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt
index 862cf463b2c2ce7e1316c4bda26dea59a09d9c97..b5b46a3f5ae5b89c82364f2765ce9ddd84fed033 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/NetworkModule.kt
@@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.MatrixConfiguration
 import org.matrix.android.sdk.internal.network.ApiInterceptor
 import org.matrix.android.sdk.internal.network.TimeOutInterceptor
 import org.matrix.android.sdk.internal.network.UserAgentInterceptor
+import org.matrix.android.sdk.internal.network.httpclient.applyMatrixConfiguration
 import org.matrix.android.sdk.internal.network.interceptors.CurlLoggingInterceptor
 import org.matrix.android.sdk.internal.network.interceptors.FormattedJsonHttpLogger
 import java.util.Collections
@@ -92,11 +93,9 @@ internal object NetworkModule {
                     if (BuildConfig.LOG_PRIVATE_DATA) {
                         addInterceptor(curlLoggingInterceptor)
                     }
-                    matrixConfiguration.proxy?.let {
-                        proxy(it)
-                    }
                 }
                 .connectionSpecs(Collections.singletonList(spec))
+                .applyMatrixConfiguration(matrixConfiguration)
                 .build()
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/httpclient/OkHttpClientUtil.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/httpclient/OkHttpClientUtil.kt
index 3920c3b527bd0c7c65654d5269682dc62ccf747f..1c395c2d615fd276c0870d92870243774283f2b2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/httpclient/OkHttpClientUtil.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/httpclient/OkHttpClientUtil.kt
@@ -17,6 +17,7 @@
 package org.matrix.android.sdk.internal.network.httpclient
 
 import okhttp3.OkHttpClient
+import org.matrix.android.sdk.api.MatrixConfiguration
 import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
 import org.matrix.android.sdk.internal.network.AccessTokenInterceptor
 import org.matrix.android.sdk.internal.network.interceptors.CurlLoggingInterceptor
@@ -51,3 +52,17 @@ internal fun OkHttpClient.Builder.addSocketFactory(homeServerConnectionConfig: H
 
     return this
 }
+
+internal fun OkHttpClient.Builder.applyMatrixConfiguration(matrixConfiguration: MatrixConfiguration): OkHttpClient.Builder {
+    matrixConfiguration.proxy?.let {
+        proxy(it)
+    }
+
+    // Move networkInterceptors provided in the configuration after all the others
+    interceptors().removeAll(matrixConfiguration.networkInterceptors)
+    matrixConfiguration.networkInterceptors.forEach {
+        addInterceptor(it)
+    }
+
+    return this
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/DefaultSecureStorageService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/securestorage/DefaultSecureStorageService.kt
similarity index 86%
rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/DefaultSecureStorageService.kt
rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/securestorage/DefaultSecureStorageService.kt
index ef8133dd15cfd6574018be8e27411bbbefdc0469..8f6605d65702f0bc7dfa5820d2f3c2f939bbce9f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/DefaultSecureStorageService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/securestorage/DefaultSecureStorageService.kt
@@ -14,9 +14,10 @@
  * limitations under the License.
  */
 
-package org.matrix.android.sdk.internal.session.securestorage
+package org.matrix.android.sdk.internal.securestorage
 
-import org.matrix.android.sdk.api.session.securestorage.SecureStorageService
+import org.matrix.android.sdk.api.securestorage.SecretStoringUtils
+import org.matrix.android.sdk.api.securestorage.SecureStorageService
 import java.io.InputStream
 import java.io.OutputStream
 import javax.inject.Inject
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt
index 32269c9afddbdbe0bf751e38c1743b9b5da7eedf..7c50a0ff84ede28af924cbcf0c1856206b8e46fa 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt
@@ -55,7 +55,6 @@ import org.matrix.android.sdk.api.session.pushrules.PushRuleService
 import org.matrix.android.sdk.api.session.room.RoomDirectoryService
 import org.matrix.android.sdk.api.session.room.RoomService
 import org.matrix.android.sdk.api.session.search.SearchService
-import org.matrix.android.sdk.api.session.securestorage.SecureStorageService
 import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
 import org.matrix.android.sdk.api.session.signout.SignOutService
 import org.matrix.android.sdk.api.session.space.SpaceService
@@ -71,6 +70,9 @@ import org.matrix.android.sdk.internal.auth.SSO_UIA_FALLBACK_PATH
 import org.matrix.android.sdk.internal.auth.SessionParamsStore
 import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
 import org.matrix.android.sdk.internal.database.tools.RealmDebugTools
+import org.matrix.android.sdk.internal.di.ContentScannerDatabase
+import org.matrix.android.sdk.internal.di.CryptoDatabase
+import org.matrix.android.sdk.internal.di.IdentityDatabase
 import org.matrix.android.sdk.internal.di.SessionDatabase
 import org.matrix.android.sdk.internal.di.SessionId
 import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate
@@ -88,6 +90,9 @@ internal class DefaultSession @Inject constructor(
         override val sessionId: String,
         override val coroutineDispatchers: MatrixCoroutineDispatchers,
         @SessionDatabase private val realmConfiguration: RealmConfiguration,
+        @CryptoDatabase private val realmConfigurationCrypto: RealmConfiguration,
+        @IdentityDatabase private val realmConfigurationIdentity: RealmConfiguration,
+        @ContentScannerDatabase private val realmConfigurationContentScanner: RealmConfiguration,
         private val lifecycleObservers: Set<@JvmSuppressWildcards SessionLifecycleObserver>,
         private val sessionListeners: SessionListeners,
         private val roomService: Lazy<RoomService>,
@@ -105,7 +110,6 @@ internal class DefaultSession @Inject constructor(
         private val cryptoService: Lazy<DefaultCryptoService>,
         private val defaultFileService: Lazy<FileService>,
         private val permalinkService: Lazy<PermalinkService>,
-        private val secureStorageService: Lazy<SecureStorageService>,
         private val profileService: Lazy<ProfileService>,
         private val syncService: Lazy<SyncService>,
         private val mediaService: Lazy<MediaService>,
@@ -214,7 +218,6 @@ internal class DefaultSession @Inject constructor(
     override fun eventService(): EventService = eventService.get()
     override fun termsService(): TermsService = termsService.get()
     override fun syncService(): SyncService = syncService.get()
-    override fun secureStorageService(): SecureStorageService = secureStorageService.get()
     override fun profileService(): ProfileService = profileService.get()
     override fun presenceService(): PresenceService = presenceService.get()
     override fun accountService(): AccountService = accountService.get()
@@ -265,5 +268,17 @@ internal class DefaultSession @Inject constructor(
 
     override fun logDbUsageInfo() {
         RealmDebugTools(realmConfiguration).logInfo("Session")
+        RealmDebugTools(realmConfigurationCrypto).logInfo("Crypto")
+        RealmDebugTools(realmConfigurationIdentity).logInfo("Identity")
+        RealmDebugTools(realmConfigurationContentScanner).logInfo("ContentScanner")
+    }
+
+    override fun getRealmConfigurations(): List<RealmConfiguration> {
+        return listOf(
+                realmConfiguration,
+                realmConfigurationCrypto,
+                realmConfigurationIdentity,
+                realmConfigurationContentScanner,
+        )
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt
index f01451b6887e4b1b5e85b14ed5be44ada7664676..d3cae3ac2d84cc85bb474608f9daa910408fdd36 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt
@@ -20,6 +20,7 @@ import dagger.BindsInstance
 import dagger.Component
 import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.auth.data.SessionParams
+import org.matrix.android.sdk.api.securestorage.SecureStorageModule
 import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.internal.crypto.CryptoModule
 import org.matrix.android.sdk.internal.crypto.crosssigning.UpdateTrustWorker
@@ -98,7 +99,8 @@ import org.matrix.android.sdk.internal.util.system.SystemModule
             ThirdPartyModule::class,
             SpaceModule::class,
             PresenceModule::class,
-            RequestModule::class
+            RequestModule::class,
+            SecureStorageModule::class,
         ]
 )
 @SessionScope
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionListeners.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionListeners.kt
index 756b9cef836e5d4cd6dced030e1723f53c46352c..0dae24e64b878d936c85c1e35cf817a43850f16f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionListeners.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionListeners.kt
@@ -19,12 +19,13 @@ package org.matrix.android.sdk.internal.session
 import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.session.Session
 import timber.log.Timber
+import java.util.concurrent.CopyOnWriteArraySet
 import javax.inject.Inject
 
 @SessionScope
 internal class SessionListeners @Inject constructor() {
 
-    private val listeners = mutableSetOf<Session.Listener>()
+    private val listeners = CopyOnWriteArraySet<Session.Listener>()
 
     fun addListener(listener: Session.Listener) {
         synchronized(listeners) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt
index 2c2317de0d0ed7a6bd06526c109e1a9626d216b4..f8a52f0b7ed5ac51f4473a32f9a6b607fde41d7b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt
@@ -41,7 +41,6 @@ import org.matrix.android.sdk.api.session.events.EventService
 import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilitiesService
 import org.matrix.android.sdk.api.session.openid.OpenIdService
 import org.matrix.android.sdk.api.session.permalinks.PermalinkService
-import org.matrix.android.sdk.api.session.securestorage.SecureStorageService
 import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageService
 import org.matrix.android.sdk.api.session.typing.TypingUsersTracker
 import org.matrix.android.sdk.api.util.md5
@@ -73,6 +72,7 @@ import org.matrix.android.sdk.internal.network.PreferredNetworkCallbackStrategy
 import org.matrix.android.sdk.internal.network.RetrofitFactory
 import org.matrix.android.sdk.internal.network.httpclient.addAccessTokenInterceptor
 import org.matrix.android.sdk.internal.network.httpclient.addSocketFactory
+import org.matrix.android.sdk.internal.network.httpclient.applyMatrixConfiguration
 import org.matrix.android.sdk.internal.network.interceptors.CurlLoggingInterceptor
 import org.matrix.android.sdk.internal.network.token.AccessTokenProvider
 import org.matrix.android.sdk.internal.network.token.HomeserverAccessTokenProvider
@@ -92,7 +92,6 @@ import org.matrix.android.sdk.internal.session.room.prune.RedactionEventProcesso
 import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
 import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessorCoroutine
 import org.matrix.android.sdk.internal.session.room.tombstone.RoomTombstoneEventProcessor
-import org.matrix.android.sdk.internal.session.securestorage.DefaultSecureStorageService
 import org.matrix.android.sdk.internal.session.typing.DefaultTypingUsersTracker
 import org.matrix.android.sdk.internal.session.user.accountdata.DefaultSessionAccountDataService
 import org.matrix.android.sdk.internal.session.widgets.DefaultWidgetURLFormatter
@@ -212,7 +211,7 @@ internal abstract class SessionModule {
         @UnauthenticatedWithCertificate
         fun providesOkHttpClientWithCertificate(
                 @Unauthenticated okHttpClient: OkHttpClient,
-                homeServerConnectionConfig: HomeServerConnectionConfig
+                homeServerConnectionConfig: HomeServerConnectionConfig,
         ): OkHttpClient {
             return okHttpClient
                     .newBuilder()
@@ -228,7 +227,8 @@ internal abstract class SessionModule {
                 @UnauthenticatedWithCertificate okHttpClient: OkHttpClient,
                 @Authenticated accessTokenProvider: AccessTokenProvider,
                 @SessionId sessionId: String,
-                @MockHttpInterceptor testInterceptor: TestInterceptor?
+                @MockHttpInterceptor testInterceptor: TestInterceptor?,
+                matrixConfiguration: MatrixConfiguration,
         ): OkHttpClient {
             return okHttpClient
                     .newBuilder()
@@ -239,6 +239,7 @@ internal abstract class SessionModule {
                             addInterceptor(testInterceptor)
                         }
                     }
+                    .applyMatrixConfiguration(matrixConfiguration)
                     .build()
         }
 
@@ -248,11 +249,13 @@ internal abstract class SessionModule {
         @UnauthenticatedWithCertificateWithProgress
         fun providesProgressOkHttpClient(
                 @UnauthenticatedWithCertificate okHttpClient: OkHttpClient,
-                downloadProgressInterceptor: DownloadProgressInterceptor
+                downloadProgressInterceptor: DownloadProgressInterceptor,
+                matrixConfiguration: MatrixConfiguration,
         ): OkHttpClient {
-            return okHttpClient.newBuilder()
+            return okHttpClient
+                    .newBuilder()
                     .apply {
-                        // Remove the previous CurlLoggingInterceptor, to add it after the accessTokenInterceptor
+                        // Remove the previous CurlLoggingInterceptor, to add it after the downloadProgressInterceptor
                         val existingCurlInterceptors = interceptors().filterIsInstance<CurlLoggingInterceptor>()
                         interceptors().removeAll(existingCurlInterceptors)
 
@@ -262,7 +265,9 @@ internal abstract class SessionModule {
                         existingCurlInterceptors.forEach {
                             addInterceptor(it)
                         }
-                    }.build()
+                    }
+                    .applyMatrixConfiguration(matrixConfiguration)
+                    .build()
         }
 
         @JvmStatic
@@ -360,9 +365,6 @@ internal abstract class SessionModule {
     @IntoSet
     abstract fun bindEventSenderProcessorAsSessionLifecycleObserver(processor: EventSenderProcessorCoroutine): SessionLifecycleObserver
 
-    @Binds
-    abstract fun bindSecureStorageService(service: DefaultSecureStorageService): SecureStorageService
-
     @Binds
     abstract fun bindHomeServerCapabilitiesService(service: DefaultHomeServerCapabilitiesService): HomeServerCapabilitiesService
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordParams.kt
index 1b95820918e3d830e4b14e5b96d3b079004e2137..f6778327d9f53073cc1c45c0b92f07b55585d8f2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordParams.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordParams.kt
@@ -29,13 +29,17 @@ internal data class ChangePasswordParams(
         val auth: UserPasswordAuth? = null,
 
         @Json(name = "new_password")
-        val newPassword: String? = null
+        val newPassword: String? = null,
+
+        @Json(name = "logout_devices")
+        val logoutDevices: Boolean = true
 ) {
     companion object {
-        fun create(userId: String, oldPassword: String, newPassword: String): ChangePasswordParams {
+        fun create(userId: String, oldPassword: String, newPassword: String, logoutAllDevices: Boolean): ChangePasswordParams {
             return ChangePasswordParams(
                     auth = UserPasswordAuth(user = userId, password = oldPassword),
-                    newPassword = newPassword
+                    newPassword = newPassword,
+                    logoutDevices = logoutAllDevices
             )
         }
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordTask.kt
index 7b21ba2e63b918a37cbbd6a53f0e2668d74b2a10..e767950ff747bca0425d60103b8ba59ebe28963f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/ChangePasswordTask.kt
@@ -26,7 +26,8 @@ import javax.inject.Inject
 internal interface ChangePasswordTask : Task<ChangePasswordTask.Params, Unit> {
     data class Params(
             val password: String,
-            val newPassword: String
+            val newPassword: String,
+            val logoutAllDevices: Boolean
     )
 }
 
@@ -37,7 +38,7 @@ internal class DefaultChangePasswordTask @Inject constructor(
 ) : ChangePasswordTask {
 
     override suspend fun execute(params: ChangePasswordTask.Params) {
-        val changePasswordParams = ChangePasswordParams.create(userId, params.password, params.newPassword)
+        val changePasswordParams = ChangePasswordParams.create(userId, params.password, params.newPassword, params.logoutAllDevices)
         try {
             executeRequest(globalErrorReceiver) {
                 accountAPI.changePassword(changePasswordParams)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DefaultAccountService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DefaultAccountService.kt
index bb830a5e419a1bc42e906c0b0bbd9c4442d481b0..9d03ec479b3a5c8d16a9e6ae82ed4840af332639 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DefaultAccountService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DefaultAccountService.kt
@@ -25,8 +25,8 @@ internal class DefaultAccountService @Inject constructor(
         private val deactivateAccountTask: DeactivateAccountTask
 ) : AccountService {
 
-    override suspend fun changePassword(password: String, newPassword: String) {
-        changePasswordTask.execute(ChangePasswordTask.Params(password, newPassword))
+    override suspend fun changePassword(password: String, newPassword: String, logoutAllDevices: Boolean) {
+        changePasswordTask.execute(ChangePasswordTask.Params(password, newPassword, logoutAllDevices))
     }
 
     override suspend fun deactivateAccount(eraseAllData: Boolean, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
index d22da8f6f2102a4f6ce0cae9f318f06cbc5f117c..add69dd8c7d90e387909626283f3bb6358454b8a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt
@@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.extensions.orFalse
 import org.matrix.android.sdk.api.extensions.orTrue
 import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
 import org.matrix.android.sdk.internal.auth.version.Versions
+import org.matrix.android.sdk.internal.auth.version.doesServerSupportLogoutDevices
 import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreads
 import org.matrix.android.sdk.internal.auth.version.isLoginAndRegistrationSupportedBySdk
 import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity
@@ -142,6 +143,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
 
             if (getVersionResult != null) {
                 homeServerCapabilitiesEntity.lastVersionIdentityServerSupported = getVersionResult.isLoginAndRegistrationSupportedBySdk()
+                homeServerCapabilitiesEntity.canControlLogoutDevices = getVersionResult.doesServerSupportLogoutDevices()
             }
 
             if (getWellknownResult != null && getWellknownResult is WellknownResult.Prompt) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityModule.kt
index 464ae96e3a4eda60f5fd7125767d257117e7bbe0..33d81648954bf7a8864ce07e74808ed9dce3eccd 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityModule.kt
@@ -21,6 +21,7 @@ import dagger.Module
 import dagger.Provides
 import io.realm.RealmConfiguration
 import okhttp3.OkHttpClient
+import org.matrix.android.sdk.api.MatrixConfiguration
 import org.matrix.android.sdk.api.session.identity.IdentityService
 import org.matrix.android.sdk.internal.database.RealmKeysUtils
 import org.matrix.android.sdk.internal.di.AuthenticatedIdentity
@@ -29,6 +30,7 @@ import org.matrix.android.sdk.internal.di.SessionFilesDirectory
 import org.matrix.android.sdk.internal.di.UnauthenticatedWithCertificate
 import org.matrix.android.sdk.internal.di.UserMd5
 import org.matrix.android.sdk.internal.network.httpclient.addAccessTokenInterceptor
+import org.matrix.android.sdk.internal.network.httpclient.applyMatrixConfiguration
 import org.matrix.android.sdk.internal.network.token.AccessTokenProvider
 import org.matrix.android.sdk.internal.session.SessionModule
 import org.matrix.android.sdk.internal.session.SessionScope
@@ -49,11 +51,13 @@ internal abstract class IdentityModule {
         @AuthenticatedIdentity
         fun providesOkHttpClient(
                 @UnauthenticatedWithCertificate okHttpClient: OkHttpClient,
-                @AuthenticatedIdentity accessTokenProvider: AccessTokenProvider
+                @AuthenticatedIdentity accessTokenProvider: AccessTokenProvider,
+                matrixConfiguration: MatrixConfiguration,
         ): OkHttpClient {
             return okHttpClient
                     .newBuilder()
                     .addAccessTokenInterceptor(accessTokenProvider)
+                    .applyMatrixConfiguration(matrixConfiguration)
                     .build()
         }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/ViaParameterFinder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/ViaParameterFinder.kt
index edc45fe94549b839c2e0b8096af61d38d539aaea..5fb20bb259830b842dc0d10851326174de8e70d1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/ViaParameterFinder.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/ViaParameterFinder.kt
@@ -101,7 +101,7 @@ internal class ViaParameterFinder @Inject constructor(
     }
 
     fun userCanInvite(userId: String, roomId: String): Boolean {
-        val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition)
+        val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
                 ?.content?.toModel<PowerLevelsContent>()
                 ?.let { PowerLevelsHelper(it) }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultConditionResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultConditionResolver.kt
index ace5ee0b9a17d0448e8243deed3f27dfcc83d02d..c2310f4fdafcad997399072a61b8e76f58b7aeb0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultConditionResolver.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultConditionResolver.kt
@@ -15,6 +15,7 @@
  */
 package org.matrix.android.sdk.internal.session.pushers
 
+import org.matrix.android.sdk.api.query.QueryStringValue
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.toModel
@@ -55,7 +56,7 @@ internal class DefaultConditionResolver @Inject constructor(
         val roomId = event.roomId ?: return false
         val room = roomGetter.getRoom(roomId) ?: return false
 
-        val powerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS)
+        val powerLevelsContent = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
                 ?.content
                 ?.toModel<PowerLevelsContent>()
                 ?: PowerLevelsContent()
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
index bb43d90328b90c4b4b6cb9b9865e0df55bf4aa44..24d4975eb9a905d53cfac917043dbc93c464334a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt
@@ -386,7 +386,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(
     }
 
     private fun getPowerLevelsHelper(roomId: String): PowerLevelsHelper? {
-        return stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition)
+        return stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
                 ?.content?.toModel<PowerLevelsContent>()
                 ?.let { PowerLevelsHelper(it) }
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
index f3845f1f15c93911224c72b339367387088912a6..c4d37d124b2f2d664762205f25742d79ba315dd2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
@@ -51,6 +51,18 @@ import org.matrix.android.sdk.internal.session.room.directory.DefaultSetRoomDire
 import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask
 import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask
 import org.matrix.android.sdk.internal.session.room.directory.SetRoomDirectoryVisibilityTask
+import org.matrix.android.sdk.internal.session.room.location.CheckIfExistingActiveLiveTask
+import org.matrix.android.sdk.internal.session.room.location.DefaultCheckIfExistingActiveLiveTask
+import org.matrix.android.sdk.internal.session.room.location.DefaultGetActiveBeaconInfoForUserTask
+import org.matrix.android.sdk.internal.session.room.location.DefaultSendLiveLocationTask
+import org.matrix.android.sdk.internal.session.room.location.DefaultSendStaticLocationTask
+import org.matrix.android.sdk.internal.session.room.location.DefaultStartLiveLocationShareTask
+import org.matrix.android.sdk.internal.session.room.location.DefaultStopLiveLocationShareTask
+import org.matrix.android.sdk.internal.session.room.location.GetActiveBeaconInfoForUserTask
+import org.matrix.android.sdk.internal.session.room.location.SendLiveLocationTask
+import org.matrix.android.sdk.internal.session.room.location.SendStaticLocationTask
+import org.matrix.android.sdk.internal.session.room.location.StartLiveLocationShareTask
+import org.matrix.android.sdk.internal.session.room.location.StopLiveLocationShareTask
 import org.matrix.android.sdk.internal.session.room.membership.DefaultLoadRoomMembersTask
 import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask
 import org.matrix.android.sdk.internal.session.room.membership.admin.DefaultMembershipAdminTask
@@ -299,4 +311,22 @@ internal abstract class RoomModule {
 
     @Binds
     abstract fun bindFetchThreadSummariesTask(task: DefaultFetchThreadSummariesTask): FetchThreadSummariesTask
+
+    @Binds
+    abstract fun bindStartLiveLocationShareTask(task: DefaultStartLiveLocationShareTask): StartLiveLocationShareTask
+
+    @Binds
+    abstract fun bindStopLiveLocationShareTask(task: DefaultStopLiveLocationShareTask): StopLiveLocationShareTask
+
+    @Binds
+    abstract fun bindSendStaticLocationTask(task: DefaultSendStaticLocationTask): SendStaticLocationTask
+
+    @Binds
+    abstract fun bindSendLiveLocationTask(task: DefaultSendLiveLocationTask): SendLiveLocationTask
+
+    @Binds
+    abstract fun bindGetActiveBeaconInfoForUserTask(task: DefaultGetActiveBeaconInfoForUserTask): GetActiveBeaconInfoForUserTask
+
+    @Binds
+    abstract fun bindCheckIfExistingActiveLiveTask(task: DefaultCheckIfExistingActiveLiveTask): CheckIfExistingActiveLiveTask
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt
index 05bde8f83f9214beda1bb6d9ac86e83c4000791d..921749122b3e511dcf7246ab05f3fc9161d75d03 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt
@@ -36,16 +36,22 @@ import timber.log.Timber
 import java.util.concurrent.TimeUnit
 import javax.inject.Inject
 
-// TODO add unit tests
+/**
+ * Aggregates all live location sharing related events in local database.
+ */
 internal class LiveLocationAggregationProcessor @Inject constructor(
         @SessionId private val sessionId: String,
         private val workManagerProvider: WorkManagerProvider,
         private val clock: Clock,
 ) {
 
-    fun handleBeaconInfo(realm: Realm, event: Event, content: MessageBeaconInfoContent, roomId: String, isLocalEcho: Boolean) {
+    /**
+     * Handle the content of a beacon info.
+     * @return true if it has been processed, false if ignored.
+     */
+    fun handleBeaconInfo(realm: Realm, event: Event, content: MessageBeaconInfoContent, roomId: String, isLocalEcho: Boolean): Boolean {
         if (event.senderId.isNullOrEmpty() || isLocalEcho) {
-            return
+            return false
         }
 
         val isLive = content.isLive.orTrue()
@@ -58,7 +64,7 @@ internal class LiveLocationAggregationProcessor @Inject constructor(
 
         if (targetEventId.isNullOrEmpty()) {
             Timber.w("no target event id found for the beacon content")
-            return
+            return false
         }
 
         val aggregatedSummary = LiveLocationShareAggregatedSummaryEntity.getOrCreate(
@@ -83,6 +89,8 @@ internal class LiveLocationAggregationProcessor @Inject constructor(
         } else {
             cancelDeactivationAfterTimeout(targetEventId, roomId)
         }
+
+        return true
     }
 
     private fun scheduleDeactivationAfterTimeout(eventId: String, roomId: String, endOfLiveTimestampMillis: Long?) {
@@ -110,6 +118,10 @@ internal class LiveLocationAggregationProcessor @Inject constructor(
         workManagerProvider.workManager.cancelUniqueWork(workName)
     }
 
+    /**
+     * Handle the content of a beacon location data.
+     * @return true if it has been processed, false if ignored.
+     */
     fun handleBeaconLocationData(
             realm: Realm,
             event: Event,
@@ -117,14 +129,14 @@ internal class LiveLocationAggregationProcessor @Inject constructor(
             roomId: String,
             relatedEventId: String?,
             isLocalEcho: Boolean
-    ) {
+    ): Boolean {
         if (event.senderId.isNullOrEmpty() || isLocalEcho) {
-            return
+            return false
         }
 
         if (relatedEventId.isNullOrEmpty()) {
             Timber.w("no related event id found for the live location content")
-            return
+            return false
         }
 
         val aggregatedSummary = LiveLocationShareAggregatedSummaryEntity.getOrCreate(
@@ -139,9 +151,12 @@ internal class LiveLocationAggregationProcessor @Inject constructor(
                 ?.getBestTimestampMillis()
                 ?: 0
 
-        if (updatedLocationTimestamp.isMoreRecentThan(currentLocationTimestamp)) {
+        return if (updatedLocationTimestamp.isMoreRecentThan(currentLocationTimestamp)) {
             Timber.d("updating last location of the summary of id=$relatedEventId")
             aggregatedSummary.lastLocationContent = ContentMapper.map(content.toContent())
+            true
+        } else {
+            false
         }
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/CheckIfExistingActiveLiveTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/CheckIfExistingActiveLiveTask.kt
new file mode 100644
index 0000000000000000000000000000000000000000..228a046f531e9a491c7be47e7b6efa9e997978f2
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/CheckIfExistingActiveLiveTask.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.session.room.location
+
+import org.matrix.android.sdk.api.extensions.orFalse
+import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
+import org.matrix.android.sdk.internal.task.Task
+import javax.inject.Inject
+
+internal interface CheckIfExistingActiveLiveTask : Task<CheckIfExistingActiveLiveTask.Params, Boolean> {
+    data class Params(
+            val roomId: String,
+    )
+}
+
+internal class DefaultCheckIfExistingActiveLiveTask @Inject constructor(
+        private val getActiveBeaconInfoForUserTask: GetActiveBeaconInfoForUserTask,
+) : CheckIfExistingActiveLiveTask {
+
+    override suspend fun execute(params: CheckIfExistingActiveLiveTask.Params): Boolean {
+        val getActiveBeaconTaskParams = GetActiveBeaconInfoForUserTask.Params(
+                roomId = params.roomId
+        )
+        return getActiveBeaconInfoForUserTask.execute(getActiveBeaconTaskParams)
+                ?.getClearContent()
+                ?.toModel<MessageBeaconInfoContent>()
+                ?.isLive
+                .orFalse()
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt
index 8cf6fcdfbf4e43389e62f2165e35157555e96b69..20320cad230919b30ddfe0225e18999b69e4b265 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt
@@ -17,21 +17,31 @@
 package org.matrix.android.sdk.internal.session.room.location
 
 import androidx.lifecycle.LiveData
+import androidx.lifecycle.Transformations
 import com.zhuinden.monarchy.Monarchy
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import org.matrix.android.sdk.api.session.room.location.LocationSharingService
+import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult
 import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
+import org.matrix.android.sdk.api.util.Cancelable
+import org.matrix.android.sdk.api.util.Optional
+import org.matrix.android.sdk.api.util.toOptional
 import org.matrix.android.sdk.internal.database.mapper.LiveLocationShareAggregatedSummaryMapper
 import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
 import org.matrix.android.sdk.internal.database.query.findRunningLiveInRoom
+import org.matrix.android.sdk.internal.database.query.where
 import org.matrix.android.sdk.internal.di.SessionDatabase
 
-// TODO add unit tests
 internal class DefaultLocationSharingService @AssistedInject constructor(
         @Assisted private val roomId: String,
         @SessionDatabase private val monarchy: Monarchy,
+        private val sendStaticLocationTask: SendStaticLocationTask,
+        private val sendLiveLocationTask: SendLiveLocationTask,
+        private val startLiveLocationShareTask: StartLiveLocationShareTask,
+        private val stopLiveLocationShareTask: StopLiveLocationShareTask,
+        private val checkIfExistingActiveLiveTask: CheckIfExistingActiveLiveTask,
         private val liveLocationShareAggregatedSummaryMapper: LiveLocationShareAggregatedSummaryMapper,
 ) : LocationSharingService {
 
@@ -40,10 +50,72 @@ internal class DefaultLocationSharingService @AssistedInject constructor(
         fun create(roomId: String): DefaultLocationSharingService
     }
 
+    override suspend fun sendStaticLocation(latitude: Double, longitude: Double, uncertainty: Double?, isUserLocation: Boolean): Cancelable {
+        val params = SendStaticLocationTask.Params(
+                roomId = roomId,
+                latitude = latitude,
+                longitude = longitude,
+                uncertainty = uncertainty,
+                isUserLocation = isUserLocation,
+        )
+        return sendStaticLocationTask.execute(params)
+    }
+
+    override suspend fun sendLiveLocation(beaconInfoEventId: String, latitude: Double, longitude: Double, uncertainty: Double?): Cancelable {
+        val params = SendLiveLocationTask.Params(
+                beaconInfoEventId = beaconInfoEventId,
+                roomId = roomId,
+                latitude = latitude,
+                longitude = longitude,
+                uncertainty = uncertainty,
+        )
+        return sendLiveLocationTask.execute(params)
+    }
+
+    override suspend fun startLiveLocationShare(timeoutMillis: Long): UpdateLiveLocationShareResult {
+        // Ensure to stop any active live before starting a new one
+        if (checkIfExistingActiveLive()) {
+            val result = stopLiveLocationShare()
+            if (result is UpdateLiveLocationShareResult.Failure) {
+                return result
+            }
+        }
+        val params = StartLiveLocationShareTask.Params(
+                roomId = roomId,
+                timeoutMillis = timeoutMillis
+        )
+        return startLiveLocationShareTask.execute(params)
+    }
+
+    private suspend fun checkIfExistingActiveLive(): Boolean {
+        val params = CheckIfExistingActiveLiveTask.Params(
+                roomId = roomId
+        )
+        return checkIfExistingActiveLiveTask.execute(params)
+    }
+
+    override suspend fun stopLiveLocationShare(): UpdateLiveLocationShareResult {
+        val params = StopLiveLocationShareTask.Params(
+                roomId = roomId,
+        )
+        return stopLiveLocationShareTask.execute(params)
+    }
+
     override fun getRunningLiveLocationShareSummaries(): LiveData<List<LiveLocationShareAggregatedSummary>> {
         return monarchy.findAllMappedWithChanges(
                 { LiveLocationShareAggregatedSummaryEntity.findRunningLiveInRoom(it, roomId = roomId) },
-                { liveLocationShareAggregatedSummaryMapper.map(it) }
+                liveLocationShareAggregatedSummaryMapper
         )
     }
+
+    override fun getLiveLocationShareSummary(beaconInfoEventId: String): LiveData<Optional<LiveLocationShareAggregatedSummary>> {
+        return Transformations.map(
+                monarchy.findAllMappedWithChanges(
+                        { LiveLocationShareAggregatedSummaryEntity.where(it, roomId = roomId, eventId = beaconInfoEventId) },
+                        liveLocationShareAggregatedSummaryMapper
+                )
+        ) {
+            it.firstOrNull().toOptional()
+        }
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/GetActiveBeaconInfoForUserTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/GetActiveBeaconInfoForUserTask.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a8d955af1dd0652641763d745f3b88996aa5f434
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/GetActiveBeaconInfoForUserTask.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.session.room.location
+
+import org.matrix.android.sdk.api.extensions.orFalse
+import org.matrix.android.sdk.api.query.QueryStringValue
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
+import org.matrix.android.sdk.internal.di.UserId
+import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
+import org.matrix.android.sdk.internal.task.Task
+import javax.inject.Inject
+
+internal interface GetActiveBeaconInfoForUserTask : Task<GetActiveBeaconInfoForUserTask.Params, Event?> {
+    data class Params(
+            val roomId: String,
+    )
+}
+
+internal class DefaultGetActiveBeaconInfoForUserTask @Inject constructor(
+        @UserId private val userId: String,
+        private val stateEventDataSource: StateEventDataSource,
+) : GetActiveBeaconInfoForUserTask {
+
+    override suspend fun execute(params: GetActiveBeaconInfoForUserTask.Params): Event? {
+        return EventType.STATE_ROOM_BEACON_INFO
+                .mapNotNull {
+                    stateEventDataSource.getStateEvent(
+                            roomId = params.roomId,
+                            eventType = it,
+                            stateKey = QueryStringValue.Equals(userId)
+                    )
+                }
+                .firstOrNull { beaconInfoEvent ->
+                    beaconInfoEvent.getClearContent()?.toModel<MessageBeaconInfoContent>()?.isLive.orFalse()
+                }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/SendLiveLocationTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/SendLiveLocationTask.kt
new file mode 100644
index 0000000000000000000000000000000000000000..bebd9c774a2fa865449ba5bf62e73240db97092f
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/SendLiveLocationTask.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.session.room.location
+
+import org.matrix.android.sdk.api.util.Cancelable
+import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
+import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
+import org.matrix.android.sdk.internal.task.Task
+import javax.inject.Inject
+
+internal interface SendLiveLocationTask : Task<SendLiveLocationTask.Params, Cancelable> {
+    data class Params(
+            val roomId: String,
+            val beaconInfoEventId: String,
+            val latitude: Double,
+            val longitude: Double,
+            val uncertainty: Double?,
+    )
+}
+
+internal class DefaultSendLiveLocationTask @Inject constructor(
+        private val localEchoEventFactory: LocalEchoEventFactory,
+        private val eventSenderProcessor: EventSenderProcessor,
+) : SendLiveLocationTask {
+
+    override suspend fun execute(params: SendLiveLocationTask.Params): Cancelable {
+        val event = localEchoEventFactory.createLiveLocationEvent(
+                beaconInfoEventId = params.beaconInfoEventId,
+                roomId = params.roomId,
+                latitude = params.latitude,
+                longitude = params.longitude,
+                uncertainty = params.uncertainty,
+        )
+        localEchoEventFactory.createLocalEcho(event)
+        return eventSenderProcessor.postEvent(event)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/SendStaticLocationTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/SendStaticLocationTask.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e08b82f3d4893589718a41769f5e953f03ea07aa
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/SendStaticLocationTask.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.session.room.location
+
+import org.matrix.android.sdk.api.util.Cancelable
+import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
+import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
+import org.matrix.android.sdk.internal.task.Task
+import javax.inject.Inject
+
+internal interface SendStaticLocationTask : Task<SendStaticLocationTask.Params, Cancelable> {
+    data class Params(
+            val roomId: String,
+            val latitude: Double,
+            val longitude: Double,
+            val uncertainty: Double?,
+            val isUserLocation: Boolean
+    )
+}
+
+internal class DefaultSendStaticLocationTask @Inject constructor(
+        private val localEchoEventFactory: LocalEchoEventFactory,
+        private val eventSenderProcessor: EventSenderProcessor,
+) : SendStaticLocationTask {
+
+    override suspend fun execute(params: SendStaticLocationTask.Params): Cancelable {
+        val event = localEchoEventFactory.createStaticLocationEvent(
+                roomId = params.roomId,
+                latitude = params.latitude,
+                longitude = params.longitude,
+                uncertainty = params.uncertainty,
+                isUserLocation = params.isUserLocation
+        )
+        localEchoEventFactory.createLocalEcho(event)
+        return eventSenderProcessor.postEvent(event)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b943c279778b1ef043b7bc4222ee306fb791ac90
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.session.room.location
+
+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.room.location.UpdateLiveLocationShareResult
+import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
+import org.matrix.android.sdk.internal.di.UserId
+import org.matrix.android.sdk.internal.session.room.state.SendStateTask
+import org.matrix.android.sdk.internal.task.Task
+import org.matrix.android.sdk.internal.util.time.Clock
+import javax.inject.Inject
+
+internal interface StartLiveLocationShareTask : Task<StartLiveLocationShareTask.Params, UpdateLiveLocationShareResult> {
+    data class Params(
+            val roomId: String,
+            val timeoutMillis: Long,
+    )
+}
+
+internal class DefaultStartLiveLocationShareTask @Inject constructor(
+        @UserId private val userId: String,
+        private val clock: Clock,
+        private val sendStateTask: SendStateTask,
+) : StartLiveLocationShareTask {
+
+    override suspend fun execute(params: StartLiveLocationShareTask.Params): UpdateLiveLocationShareResult {
+        val beaconContent = MessageBeaconInfoContent(
+                timeout = params.timeoutMillis,
+                isLive = true,
+                unstableTimestampMillis = clock.epochMillis()
+        ).toContent()
+        val eventType = EventType.STATE_ROOM_BEACON_INFO.first()
+        val sendStateTaskParams = SendStateTask.Params(
+                roomId = params.roomId,
+                stateKey = userId,
+                eventType = eventType,
+                body = beaconContent
+        )
+        return try {
+            val eventId = sendStateTask.executeRetry(sendStateTaskParams, 3)
+            if (eventId.isNotEmpty()) {
+                UpdateLiveLocationShareResult.Success(eventId)
+            } else {
+                UpdateLiveLocationShareResult.Failure(Exception("empty event id for new state event"))
+            }
+        } catch (error: Throwable) {
+            UpdateLiveLocationShareResult.Failure(error)
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt
new file mode 100644
index 0000000000000000000000000000000000000000..da5fd76940dbffba32cfc02bc99bd519af62f06b
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.session.room.location
+
+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.room.location.UpdateLiveLocationShareResult
+import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
+import org.matrix.android.sdk.internal.session.room.state.SendStateTask
+import org.matrix.android.sdk.internal.task.Task
+import javax.inject.Inject
+
+internal interface StopLiveLocationShareTask : Task<StopLiveLocationShareTask.Params, UpdateLiveLocationShareResult> {
+    data class Params(
+            val roomId: String,
+    )
+}
+
+internal class DefaultStopLiveLocationShareTask @Inject constructor(
+        private val sendStateTask: SendStateTask,
+        private val getActiveBeaconInfoForUserTask: GetActiveBeaconInfoForUserTask,
+) : StopLiveLocationShareTask {
+
+    override suspend fun execute(params: StopLiveLocationShareTask.Params): UpdateLiveLocationShareResult {
+        val beaconInfoStateEvent = getActiveLiveLocationBeaconInfoForUser(params.roomId) ?: return getResultForIncorrectBeaconInfoEvent()
+        val stateKey = beaconInfoStateEvent.stateKey ?: return getResultForIncorrectBeaconInfoEvent()
+        val content = beaconInfoStateEvent.getClearContent()?.toModel<MessageBeaconInfoContent>() ?: return getResultForIncorrectBeaconInfoEvent()
+        val updatedContent = content.copy(isLive = false).toContent()
+        val sendStateTaskParams = SendStateTask.Params(
+                roomId = params.roomId,
+                stateKey = stateKey,
+                eventType = EventType.STATE_ROOM_BEACON_INFO.first(),
+                body = updatedContent
+        )
+        return try {
+            val eventId = sendStateTask.executeRetry(sendStateTaskParams, 3)
+            if (eventId.isNotEmpty()) {
+                UpdateLiveLocationShareResult.Success(eventId)
+            } else {
+                UpdateLiveLocationShareResult.Failure(Exception("empty event id for new state event"))
+            }
+        } catch (error: Throwable) {
+            UpdateLiveLocationShareResult.Failure(error)
+        }
+    }
+
+    private fun getResultForIncorrectBeaconInfoEvent() =
+            UpdateLiveLocationShareResult.Failure(Exception("incorrect last beacon info event"))
+
+    private suspend fun getActiveLiveLocationBeaconInfoForUser(roomId: String): Event? {
+        val params = GetActiveBeaconInfoForUserTask.Params(
+                roomId = roomId
+        )
+        return getActiveBeaconInfoForUserTask.execute(params)
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt
index 005d7f26db4f9fe86aeebcc56fe834862e6f0656..ef89ca33a723f71b52f60b82479573ad505fb40d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/DefaultMembershipService.kt
@@ -58,7 +58,7 @@ internal class DefaultMembershipService @AssistedInject constructor(
     }
 
     override suspend fun loadRoomMembersIfNeeded() {
-        val params = LoadRoomMembersTask.Params(roomId, Membership.LEAVE)
+        val params = LoadRoomMembersTask.Params(roomId, excludeMembership = Membership.LEAVE)
         loadRoomMembersTask.execute(params)
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt
index 15d0889255155dadf49bbae7c156d9ccc4a551e1..7052eb23e247c99c4d195a0850588e3fdf85f334 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt
@@ -43,6 +43,7 @@ import org.matrix.android.sdk.internal.session.sync.SyncTokenStore
 import org.matrix.android.sdk.internal.task.Task
 import org.matrix.android.sdk.internal.util.awaitTransaction
 import org.matrix.android.sdk.internal.util.time.Clock
+import timber.log.Timber
 import java.util.concurrent.TimeUnit
 import javax.inject.Inject
 
@@ -105,32 +106,37 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(
     }
 
     private suspend fun insertInDb(response: RoomMembersResponse, roomId: String) {
+        val chunks = response.roomMemberEvents.chunked(500)
+        chunks.forEach { roomMemberEvents ->
+            monarchy.awaitTransaction { realm ->
+                Timber.v("Insert ${roomMemberEvents.size} member events in room $roomId")
+                // We ignore all the already known members
+                val now = clock.epochMillis()
+                for (roomMemberEvent in roomMemberEvents) {
+                    if (roomMemberEvent.eventId == null || roomMemberEvent.stateKey == null || roomMemberEvent.type == null) {
+                        continue
+                    }
+                    val ageLocalTs = now - (roomMemberEvent.unsignedData?.age ?: 0)
+                    val eventEntity = roomMemberEvent.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
+                    CurrentStateEventEntity.getOrCreate(
+                            realm,
+                            roomId,
+                            roomMemberEvent.stateKey,
+                            roomMemberEvent.type
+                    ).apply {
+                        eventId = roomMemberEvent.eventId
+                        root = eventEntity
+                    }
+                    roomMemberEventHandler.handle(realm, roomId, roomMemberEvent, false)
+                }
+            }
+        }
         monarchy.awaitTransaction { realm ->
-            // We ignore all the already known members
             val roomEntity = RoomEntity.where(realm, roomId).findFirst()
                     ?: realm.createObject(roomId)
-            val now = clock.epochMillis()
-            for (roomMemberEvent in response.roomMemberEvents) {
-                if (roomMemberEvent.eventId == null || roomMemberEvent.stateKey == null || roomMemberEvent.type == null) {
-                    continue
-                }
-                val ageLocalTs = roomMemberEvent.unsignedData?.age?.let { now - it }
-                val eventEntity = roomMemberEvent.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
-                CurrentStateEventEntity.getOrCreate(
-                        realm,
-                        roomId,
-                        roomMemberEvent.stateKey,
-                        roomMemberEvent.type
-                ).apply {
-                    eventId = roomMemberEvent.eventId
-                    root = eventEntity
-                }
-                roomMemberEventHandler.handle(realm, roomId, roomMemberEvent, false)
-            }
             roomEntity.membersLoadStatus = RoomMembersLoadStatusType.LOADED
             roomSummaryUpdater.update(realm, roomId, updateMembers = true)
         }
-
         if (cryptoSessionInfoProvider.isRoomEncrypted(roomId)) {
             deviceListManager.onRoomMembersLoadedFor(roomId)
         }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEventHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEventHandler.kt
index 1e36e9c6da52a350429d74038a7be2d9f6623cdf..fd6552525e92883a9e09e8f4b6f0aa5fb2abffb2 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEventHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomMemberEventHandler.kt
@@ -83,23 +83,21 @@ internal class RoomMemberEventHandler @Inject constructor(
             userId: String,
             roomMember: RoomMemberContent
     ) {
-        val roomMemberEntity = RoomMemberEntityFactory.create(
-                roomId,
-                userId,
-                roomMember,
-                // When an update is happening, insertOrUpdate replace existing values with null if they are not provided,
-                // but we want to preserve presence record value and not replace it with null
-                getExistingPresenceState(realm, roomId, userId)
-        )
-        realm.insertOrUpdate(roomMemberEntity)
-    }
-
-    /**
-     * Get the already existing presence state for a specific user & room in order NOT to be replaced in RoomMemberSummaryEntity
-     * by NULL value.
-     */
-    private fun getExistingPresenceState(realm: Realm, roomId: String, userId: String): UserPresenceEntity? {
-        return RoomMemberSummaryEntity.where(realm, roomId, userId).findFirst()?.userPresenceEntity
+        val existingRoomMemberSummary = RoomMemberSummaryEntity.where(realm, roomId, userId).findFirst()
+        if (existingRoomMemberSummary != null) {
+            existingRoomMemberSummary.displayName = roomMember.displayName
+            existingRoomMemberSummary.avatarUrl = roomMember.avatarUrl
+            existingRoomMemberSummary.membership = roomMember.membership
+        } else {
+            val presenceEntity = UserPresenceEntity.where(realm, userId).findFirst()
+            val roomMemberEntity = RoomMemberEntityFactory.create(
+                    roomId,
+                    userId,
+                    roomMember,
+                    presenceEntity
+            )
+            realm.insert(roomMemberEntity)
+        }
     }
 
     private fun saveUserEntityLocallyIfNecessary(
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/leaving/LeaveRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/leaving/LeaveRoomTask.kt
index 1b836e36a681da6db9d3031050c7b761cc25726f..dd28bbcc73dd1e62626c73171488b4ee264670aa 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/leaving/LeaveRoomTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/leaving/LeaveRoomTask.kt
@@ -60,7 +60,7 @@ internal class DefaultLeaveRoomTask @Inject constructor(
         val roomCreateStateEvent = stateEventDataSource.getStateEvent(
                 roomId = roomId,
                 eventType = EventType.STATE_ROOM_CREATE,
-                stateKey = QueryStringValue.NoCondition
+                stateKey = QueryStringValue.IsEmpty,
         )
         // Server is not cleaning predecessor rooms, so we also try to left them
         val predecessorRoomId = roomCreateStateEvent?.getClearContent()?.toModel<RoomCreateContent>()?.predecessor?.roomId
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
index bad734173e84c33455141c27db2111549b3a738a..bac810f424704bb3604d25d07dd044410c4198bc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt
@@ -209,7 +209,8 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor(
      * Create an EventEntity to be added in the TimelineEventEntity.
      */
     private fun createEventEntity(roomId: String, event: Event, realm: Realm): EventEntity {
-        val ageLocalTs = event.unsignedData?.age?.let { clock.epochMillis() - it }
+        val now = clock.epochMillis()
+        val ageLocalTs = now - (event.unsignedData?.age ?: 0)
         return event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
index fc78abcfd93bd355147e81fd847167c8ce87cb67..418000abed9037e598550f14533a94234b111fa0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
@@ -129,18 +129,6 @@ internal class DefaultSendService @AssistedInject constructor(
                 .let { sendEvent(it) }
     }
 
-    override fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?, isUserLocation: Boolean): Cancelable {
-        return localEchoEventFactory.createLocationEvent(roomId, latitude, longitude, uncertainty, isUserLocation)
-                .also { createLocalEcho(it) }
-                .let { sendEvent(it) }
-    }
-
-    override fun sendLiveLocation(beaconInfoEventId: String, latitude: Double, longitude: Double, uncertainty: Double?): Cancelable {
-        return localEchoEventFactory.createLiveLocationEvent(beaconInfoEventId, roomId, latitude, longitude, uncertainty)
-                .also { createLocalEcho(it) }
-                .let { sendEvent(it) }
-    }
-
     override fun redactEvent(event: Event, reason: String?): Cancelable {
         // TODO manage media/attachements?
         val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
index 3b9ca44d18cb34826a2f5ed9518c5a74a3c94674..f52500de1b58638ba3d0c26021007653e7e10d78 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
@@ -244,7 +244,7 @@ internal class LocalEchoEventFactory @Inject constructor(
         )
     }
 
-    fun createLocationEvent(
+    fun createStaticLocationEvent(
             roomId: String,
             latitude: Double,
             longitude: Double,
@@ -708,7 +708,7 @@ internal class LocalEchoEventFactory @Inject constructor(
     }
 
     /**
-     * Returns RFC5870 formatted geo uri 'geo:latitude,longitude;uncertainty' like 'geo:40.05,29.24;30'
+     * Returns RFC5870 formatted geo uri 'geo:latitude,longitude;u=uncertainty' like 'geo:40.05,29.24;u=30'
      * Uncertainty of the location is in meters and not required.
      */
     private fun buildGeoUri(latitude: Double, longitude: Double, uncertainty: Double?): String {
@@ -718,7 +718,7 @@ internal class LocalEchoEventFactory @Inject constructor(
             append(",")
             append(longitude)
             uncertainty?.let {
-                append(";")
+                append(";u=")
                 append(it)
             }
         }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt
index 2a980f32863e77ac2a4bf38d3d2330599a449942..ad47b82428ca86ec961aa575fd1341559a4ac47f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/DefaultStateService.kt
@@ -21,32 +21,27 @@ import androidx.lifecycle.LiveData
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
-import org.matrix.android.sdk.api.extensions.orFalse
-import org.matrix.android.sdk.api.query.QueryStringValue
+import org.matrix.android.sdk.api.query.QueryStateEventValue
 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.room.model.GuestAccess
 import org.matrix.android.sdk.api.session.room.model.RoomCanonicalAliasContent
 import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
 import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
 import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry
 import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
-import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
 import org.matrix.android.sdk.api.session.room.state.StateService
 import org.matrix.android.sdk.api.util.JsonDict
 import org.matrix.android.sdk.api.util.MimeTypes
 import org.matrix.android.sdk.api.util.Optional
 import org.matrix.android.sdk.internal.session.content.FileUploader
-import org.matrix.android.sdk.internal.session.permalinks.ViaParameterFinder
 
 internal class DefaultStateService @AssistedInject constructor(
         @Assisted private val roomId: String,
         private val stateEventDataSource: StateEventDataSource,
         private val sendStateTask: SendStateTask,
         private val fileUploader: FileUploader,
-        private val viaParameterFinder: ViaParameterFinder
 ) : StateService {
 
     @AssistedFactory
@@ -54,19 +49,19 @@ internal class DefaultStateService @AssistedInject constructor(
         fun create(roomId: String): DefaultStateService
     }
 
-    override fun getStateEvent(eventType: String, stateKey: QueryStringValue): Event? {
+    override fun getStateEvent(eventType: String, stateKey: QueryStateEventValue): Event? {
         return stateEventDataSource.getStateEvent(roomId, eventType, stateKey)
     }
 
-    override fun getStateEventLive(eventType: String, stateKey: QueryStringValue): LiveData<Optional<Event>> {
+    override fun getStateEventLive(eventType: String, stateKey: QueryStateEventValue): LiveData<Optional<Event>> {
         return stateEventDataSource.getStateEventLive(roomId, eventType, stateKey)
     }
 
-    override fun getStateEvents(eventTypes: Set<String>, stateKey: QueryStringValue): List<Event> {
+    override fun getStateEvents(eventTypes: Set<String>, stateKey: QueryStateEventValue): List<Event> {
         return stateEventDataSource.getStateEvents(roomId, eventTypes, stateKey)
     }
 
-    override fun getStateEventsLive(eventTypes: Set<String>, stateKey: QueryStringValue): LiveData<List<Event>> {
+    override fun getStateEventsLive(eventTypes: Set<String>, stateKey: QueryStateEventValue): LiveData<List<Event>> {
         return stateEventDataSource.getStateEventsLive(roomId, eventTypes, stateKey)
     }
 
@@ -190,35 +185,4 @@ internal class DefaultStateService @AssistedInject constructor(
         }
         updateJoinRule(RoomJoinRules.RESTRICTED, null, allowEntries)
     }
-
-    override suspend fun stopLiveLocation(userId: String) {
-        getLiveLocationBeaconInfo(userId, true)?.let { beaconInfoStateEvent ->
-            beaconInfoStateEvent.getClearContent()?.toModel<MessageBeaconInfoContent>()?.let { content ->
-                val updatedContent = content.copy(isLive = false).toContent()
-
-                beaconInfoStateEvent.stateKey?.let {
-                    sendStateEvent(
-                            eventType = EventType.STATE_ROOM_BEACON_INFO.first(),
-                            body = updatedContent,
-                            stateKey = it
-                    )
-                }
-            }
-        }
-    }
-
-    override suspend fun getLiveLocationBeaconInfo(userId: String, filterOnlyLive: Boolean): Event? {
-        return EventType.STATE_ROOM_BEACON_INFO
-                .mapNotNull {
-                    stateEventDataSource.getStateEvent(
-                            roomId = roomId,
-                            eventType = it,
-                            stateKey = QueryStringValue.Equals(userId)
-                    )
-                }
-                .firstOrNull { beaconInfoEvent ->
-                    !filterOnlyLive ||
-                            beaconInfoEvent.getClearContent()?.toModel<MessageBeaconInfoContent>()?.isLive.orFalse()
-                }
-    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/StateEventDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/StateEventDataSource.kt
index 18c709adf2d2b3f1ac470ec672bd525632715203..9971ce3ccc536f9abb409cc272a67b05237ab036 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/StateEventDataSource.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/state/StateEventDataSource.kt
@@ -22,6 +22,7 @@ import com.zhuinden.monarchy.Monarchy
 import io.realm.Realm
 import io.realm.RealmQuery
 import io.realm.kotlin.where
+import org.matrix.android.sdk.api.query.QueryStateEventValue
 import org.matrix.android.sdk.api.query.QueryStringValue
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.util.Optional
@@ -40,13 +41,13 @@ internal class StateEventDataSource @Inject constructor(
         private val queryStringValueProcessor: QueryStringValueProcessor
 ) {
 
-    fun getStateEvent(roomId: String, eventType: String, stateKey: QueryStringValue): Event? {
+    fun getStateEvent(roomId: String, eventType: String, stateKey: QueryStateEventValue): Event? {
         return realmSessionProvider.withRealm { realm ->
             buildStateEventQuery(realm, roomId, setOf(eventType), stateKey).findFirst()?.root?.asDomain()
         }
     }
 
-    fun getStateEventLive(roomId: String, eventType: String, stateKey: QueryStringValue): LiveData<Optional<Event>> {
+    fun getStateEventLive(roomId: String, eventType: String, stateKey: QueryStateEventValue): LiveData<Optional<Event>> {
         val liveData = monarchy.findAllMappedWithChanges(
                 { realm -> buildStateEventQuery(realm, roomId, setOf(eventType), stateKey) },
                 { it.root?.asDomain() }
@@ -56,7 +57,7 @@ internal class StateEventDataSource @Inject constructor(
         }
     }
 
-    fun getStateEvents(roomId: String, eventTypes: Set<String>, stateKey: QueryStringValue): List<Event> {
+    fun getStateEvents(roomId: String, eventTypes: Set<String>, stateKey: QueryStateEventValue): List<Event> {
         return realmSessionProvider.withRealm { realm ->
             buildStateEventQuery(realm, roomId, eventTypes, stateKey)
                     .findAll()
@@ -66,7 +67,7 @@ internal class StateEventDataSource @Inject constructor(
         }
     }
 
-    fun getStateEventsLive(roomId: String, eventTypes: Set<String>, stateKey: QueryStringValue): LiveData<List<Event>> {
+    fun getStateEventsLive(roomId: String, eventTypes: Set<String>, stateKey: QueryStateEventValue): LiveData<List<Event>> {
         val liveData = monarchy.findAllMappedWithChanges(
                 { realm -> buildStateEventQuery(realm, roomId, eventTypes, stateKey) },
                 { it.root?.asDomain() }
@@ -80,7 +81,7 @@ internal class StateEventDataSource @Inject constructor(
             realm: Realm,
             roomId: String,
             eventTypes: Set<String>,
-            stateKey: QueryStringValue
+            stateKey: QueryStateEventValue
     ): RealmQuery<CurrentStateEventEntity> {
         return with(queryStringValueProcessor) {
             realm.where<CurrentStateEventEntity>()
@@ -90,7 +91,8 @@ internal class StateEventDataSource @Inject constructor(
                             `in`(CurrentStateEventEntityFields.TYPE, eventTypes.toTypedArray())
                         }
                     }
-                    .process(CurrentStateEventEntityFields.STATE_KEY, stateKey)
+                    // It's OK to cast stateKey as QueryStringValue
+                    .process(CurrentStateEventEntityFields.STATE_KEY, stateKey as QueryStringValue)
         }
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
index 7795a56cbf44ce60b6a66962f05d0892dcf1b1bb..4eaac67e5ae3662ddfe4632e18cef9446e533deb 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt
@@ -20,6 +20,7 @@ import io.realm.Realm
 import io.realm.RealmConfiguration
 import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
 import kotlinx.coroutines.SupervisorJob
 import kotlinx.coroutines.android.asCoroutineDispatcher
 import kotlinx.coroutines.cancelChildren
@@ -33,6 +34,7 @@ import kotlinx.coroutines.withContext
 import okhttp3.internal.closeQuietly
 import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.api.session.room.model.Membership
 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
@@ -115,6 +117,7 @@ internal class DefaultTimeline(
     )
 
     private var strategy: LoadTimelineStrategy = buildStrategy(LoadTimelineStrategy.Mode.Live)
+    private var startTimelineJob: Job? = null
 
     override val isLive: Boolean
         get() = !getPaginationState(Timeline.Direction.FORWARDS).hasMoreToLoad
@@ -142,7 +145,7 @@ internal class DefaultTimeline(
         timelineScope.launch {
             loadRoomMembersIfNeeded()
         }
-        timelineScope.launch {
+        startTimelineJob = timelineScope.launch {
             sequencer.post {
                 if (isStarted.compareAndSet(false, true)) {
                     isFromThreadTimeline = rootThreadEventId != null
@@ -173,8 +176,10 @@ internal class DefaultTimeline(
 
     override fun restartWithEventId(eventId: String?) {
         timelineScope.launch {
-            openAround(eventId, rootThreadEventId)
-            postSnapshot()
+            sequencer.post {
+                openAround(eventId, rootThreadEventId)
+                postSnapshot()
+            }
         }
     }
 
@@ -184,6 +189,7 @@ internal class DefaultTimeline(
 
     override fun paginate(direction: Timeline.Direction, count: Int) {
         timelineScope.launch {
+            startTimelineJob?.join()
             val postSnapshot = loadMore(count, direction, fetchOnServerIfNeeded = true)
             if (postSnapshot) {
                 postSnapshot()
@@ -192,6 +198,7 @@ internal class DefaultTimeline(
     }
 
     override suspend fun awaitPaginate(direction: Timeline.Direction, count: Int): List<TimelineEvent> {
+        startTimelineJob?.join()
         withContext(timelineDispatcher) {
             loadMore(count, direction, fetchOnServerIfNeeded = true)
         }
@@ -278,6 +285,7 @@ internal class DefaultTimeline(
                 direction = Timeline.Direction.BACKWARDS,
                 fetchOnServerIfNeeded = false
         )
+
         Timber.v("$baseLogMessage finished")
     }
 
@@ -311,9 +319,11 @@ internal class DefaultTimeline(
 
     private fun onLimitedTimeline() {
         timelineScope.launch {
-            initPaginationStates(null)
-            loadMore(settings.initialSize, Timeline.Direction.BACKWARDS, false)
-            postSnapshot()
+            sequencer.post {
+                initPaginationStates(null)
+                loadMore(settings.initialSize, Timeline.Direction.BACKWARDS, false)
+                postSnapshot()
+            }
         }
     }
 
@@ -388,7 +398,7 @@ internal class DefaultTimeline(
     }
 
     private suspend fun loadRoomMembersIfNeeded() {
-        val loadRoomMembersParam = LoadRoomMembersTask.Params(roomId)
+        val loadRoomMembersParam = LoadRoomMembersTask.Params(roomId, excludeMembership = Membership.LEAVE)
         try {
             loadRoomMembersTask.execute(loadRoomMembersParam)
         } catch (failure: Throwable) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt
index aef9e24c8b912d7a80dc02a082a55fe81c839a35..7c662444e4bb10cbf28db56acfda64a8f524518b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt
@@ -61,7 +61,7 @@ internal class DefaultGetEventTask @Inject constructor(
                     }
         }
 
-        event.ageLocalTs = event.unsignedData?.age?.let { clock.epochMillis() - it }
+        event.ageLocalTs = clock.epochMillis() - (event.unsignedData?.age ?: 0)
 
         return event
     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LiveRoomStateListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LiveRoomStateListener.kt
index b2692bf8051c7924a3c1c6573447c85b4bad48eb..b78ad45938ca61a7819ab7098f0fb766dd93240b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LiveRoomStateListener.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LiveRoomStateListener.kt
@@ -46,7 +46,7 @@ internal class LiveRoomStateListener(
         stateEventDataSource.getStateEventsLive(
                 roomId = roomId,
                 eventTypes = setOf(EventType.STATE_ROOM_MEMBER),
-                stateKey = QueryStringValue.NoCondition,
+                stateKey = QueryStringValue.IsNotNull,
         )
     }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
index c5d4d346fdf8b82dbf7b4f490fd118468d305a41..d81a115676fd8659529780275d0ed868783895e5 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt
@@ -22,6 +22,7 @@ import io.realm.Realm
 import io.realm.RealmConfiguration
 import io.realm.RealmResults
 import io.realm.kotlin.createObject
+import io.realm.kotlin.executeTransactionAwait
 import kotlinx.coroutines.CompletableDeferred
 import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.extensions.orFalse
@@ -265,7 +266,7 @@ internal class LoadTimelineStrategy constructor(
         }
     }
 
-    private fun getChunkEntity(realm: Realm): RealmResults<ChunkEntity> {
+    private suspend fun getChunkEntity(realm: Realm): RealmResults<ChunkEntity> {
         return when (mode) {
             is Mode.Live -> {
                 ChunkEntity.where(realm, roomId)
@@ -289,8 +290,8 @@ internal class LoadTimelineStrategy constructor(
      * Clear any existing thread chunk entity and create a new one, with the
      * rootThreadEventId included.
      */
-    private fun recreateThreadChunkEntity(realm: Realm, rootThreadEventId: String) {
-        realm.executeTransaction {
+    private suspend fun recreateThreadChunkEntity(realm: Realm, rootThreadEventId: String) {
+        realm.executeTransactionAwait {
             // Lets delete the chunk and start a new one
             ChunkEntity.findLastForwardChunkOfThread(it, roomId, rootThreadEventId)?.deleteAndClearThreadEvents()?.let {
                 Timber.i("###THREADS LoadTimelineStrategy [onStart] thread chunk cleared..")
@@ -309,8 +310,8 @@ internal class LoadTimelineStrategy constructor(
     /**
      * Clear any existing thread chunk.
      */
-    private fun clearThreadChunkEntity(realm: Realm, rootThreadEventId: String) {
-        realm.executeTransaction {
+    private suspend fun clearThreadChunkEntity(realm: Realm, rootThreadEventId: String) {
+        realm.executeTransactionAwait {
             ChunkEntity.findLastForwardChunkOfThread(it, roomId, rootThreadEventId)?.deleteAndClearThreadEvents()?.let {
                 Timber.i("###THREADS LoadTimelineStrategy [onStop] thread chunk cleared..")
             }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
index e13f3f454fb0af42327164318f449a255053b38f..7fa36969b16e823d7c67a5bf01b553c87187a8fb 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt
@@ -490,38 +490,11 @@ internal class TimelineChunk(
     private fun handleDatabaseChangeSet(results: RealmResults<TimelineEventEntity>, changeSet: OrderedCollectionChangeSet) {
         val insertions = changeSet.insertionRanges
         for (range in insertions) {
-            // Check if the insertion's displayIndices match our expectations - or skip this insertion.
-            // Inconsistencies (missing messages) can happen otherwise if we get insertions before having loaded all timeline events of the chunk.
-            if (builtEvents.isNotEmpty()) {
-                // Check consistency to item before insertions
-                if (range.startIndex > 0) {
-                    val firstInsertion = results[range.startIndex]!!
-                    val lastBeforeInsertion = builtEvents[range.startIndex - 1]
-                    if (firstInsertion.displayIndex + 1 != lastBeforeInsertion.displayIndex) {
-                        Timber.i(
-                                "handleDatabaseChangeSet: skip insertion at ${range.startIndex}/${builtEvents.size}, " +
-                                        "displayIndex mismatch at ${range.startIndex}: ${firstInsertion.displayIndex} -> ${lastBeforeInsertion.displayIndex}"
-                        )
-                        continue
-                    }
-                }
-                // Check consistency to item after insertions
-                if (range.startIndex < builtEvents.size) {
-                    val lastInsertion = results[range.startIndex + range.length - 1]!!
-                    val firstAfterInsertion = builtEvents[range.startIndex]
-                    if (firstAfterInsertion.displayIndex + 1 != lastInsertion.displayIndex) {
-                        Timber.i(
-                                "handleDatabaseChangeSet: skip insertion at ${range.startIndex}/${builtEvents.size}, " +
-                                        "displayIndex mismatch at ${range.startIndex + range.length}: " +
-                                        "${firstAfterInsertion.displayIndex} -> ${lastInsertion.displayIndex}"
-                        )
-                        continue
-                    }
-                }
-            }
+            if (!validateInsertion(range, results)) continue
             val newItems = results
                     .subList(range.startIndex, range.startIndex + range.length)
                     .map { it.buildAndDecryptIfNeeded() }
+
             builtEventsIndexes.entries.filter { it.value >= range.startIndex }.forEach { it.setValue(it.value + range.length) }
             newItems.mapIndexed { index, timelineEvent ->
                 if (timelineEvent.root.type == EventType.STATE_ROOM_CREATE) {
@@ -536,12 +509,9 @@ internal class TimelineChunk(
         for (range in modifications) {
             for (modificationIndex in (range.startIndex until range.startIndex + range.length)) {
                 val updatedEntity = results[modificationIndex] ?: continue
-                val displayIndex = builtEventsIndexes[updatedEntity.eventId]
-                if (displayIndex == null) {
-                    continue
-                }
+                val builtEventIndex = builtEventsIndexes[updatedEntity.eventId] ?: continue
                 try {
-                    builtEvents[displayIndex] = updatedEntity.buildAndDecryptIfNeeded()
+                    builtEvents[builtEventIndex] = updatedEntity.buildAndDecryptIfNeeded()
                 } catch (failure: Throwable) {
                     Timber.v("Fail to update items at index: $modificationIndex")
                 }
@@ -558,6 +528,21 @@ internal class TimelineChunk(
         }
     }
 
+    private fun validateInsertion(range: OrderedCollectionChangeSet.Range, results: RealmResults<TimelineEventEntity>): Boolean {
+        // Insertion can only happen from LastForward chunk after a sync.
+        if (isLastForward.get()) {
+            val firstBuiltEvent = builtEvents.firstOrNull()
+            if (firstBuiltEvent != null) {
+                val lastInsertion = results[range.startIndex + range.length - 1] ?: return false
+                if (firstBuiltEvent.displayIndex + 1 != lastInsertion.displayIndex) {
+                    Timber.v("There is no continuation in the chunk, chunk is not fully loaded yet, skip insert.")
+                    return false
+                }
+            }
+        }
+        return true
+    }
+
     private fun getNextDisplayIndex(direction: Timeline.Direction): Int? {
         if (timelineEventEntities.isEmpty()) {
             return null
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt
index fd1703dbc8a507a0886f5d5bfff211b91541e895..ea22f8cd789c16f55d6a1bd5302449ad2c94f708 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt
@@ -142,7 +142,7 @@ internal class TokenChunkEventPersistor @Inject constructor(
         val now = clock.epochMillis()
 
         stateEvents?.forEach { stateEvent ->
-            val ageLocalTs = stateEvent.unsignedData?.age?.let { now - it }
+            val ageLocalTs = now - (stateEvent.unsignedData?.age ?: 0)
             val stateEventEntity = stateEvent.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
             currentChunk.addStateEvent(roomId, stateEventEntity, direction)
             if (stateEvent.type == EventType.STATE_ROOM_MEMBER && stateEvent.stateKey != null) {
@@ -155,7 +155,7 @@ internal class TokenChunkEventPersistor @Inject constructor(
                 if (event.eventId == null || event.senderId == null) {
                     return@forEach
                 }
-                val ageLocalTs = event.unsignedData?.age?.let { now - it }
+                val ageLocalTs = now - (event.unsignedData?.age ?: 0)
                 val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
                 if (event.type == EventType.STATE_ROOM_MEMBER && event.stateKey != null) {
                     val contentToUse = if (direction == PaginationDirection.BACKWARDS) {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/DefaultRoomVersionService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/DefaultRoomVersionService.kt
index dc12c3209b2811398a41c35ec2009ac0074b5f08..0bde3a11d29861a4290a1950d284e9836c695a31 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/DefaultRoomVersionService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/version/DefaultRoomVersionService.kt
@@ -71,7 +71,7 @@ internal class DefaultRoomVersionService @AssistedInject constructor(
     }
 
     override fun userMayUpgradeRoom(userId: String): Boolean {
-        val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition)
+        val powerLevelsHelper = stateEventDataSource.getStateEvent(roomId, EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
                 ?.content?.toModel<PowerLevelsContent>()
                 ?.let { PowerLevelsHelper(it) }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt
index c08d9389a87b5f5841b93eea8cd475b85f0d2bf9..d2f1b3202b049da22a5d84c6d8516c2418b26c52 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt
@@ -252,7 +252,7 @@ internal class DefaultSpaceService @Inject constructor(
             val powerLevelsEvent = stateEventDataSource.getStateEvent(
                     roomId = parentSpaceId,
                     eventType = EventType.STATE_ROOM_POWER_LEVELS,
-                    stateKey = QueryStringValue.NoCondition
+                    stateKey = QueryStringValue.IsEmpty
             )
             val powerLevelsContent = powerLevelsEvent?.content?.toModel<PowerLevelsContent>()
                     ?: throw UnsupportedOperationException("Cannot add canonical child, missing powerlevel")
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt
index f99fe964102fa06a7ad9bacc93f9d1625e68af3f..30e1ec6679347ca37b5c6790d198f60452f98994 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt
@@ -244,7 +244,7 @@ internal class RoomSyncHandler @Inject constructor(
                 if (event.eventId == null || event.stateKey == null || event.type == null) {
                     continue
                 }
-                val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it }
+                val ageLocalTs = syncLocalTimestampMillis - (event.unsignedData?.age ?: 0)
                 val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType)
                 Timber.v("## received state event ${event.type} and key ${event.stateKey}")
                 CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
@@ -306,7 +306,7 @@ internal class RoomSyncHandler @Inject constructor(
                 if (event.stateKey == null || event.type == null) {
                     return@forEach
                 }
-                val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it }
+                val ageLocalTs = syncLocalTimestampMillis - (event.unsignedData?.age ?: 0)
                 val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType)
                 CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
                     eventId = eventEntity.eventId
@@ -336,7 +336,7 @@ internal class RoomSyncHandler @Inject constructor(
             if (event.eventId == null || event.stateKey == null || event.type == null) {
                 continue
             }
-            val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it }
+            val ageLocalTs = syncLocalTimestampMillis - (event.unsignedData?.age ?: 0)
             val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType)
             CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
                 eventId = event.eventId
@@ -348,7 +348,7 @@ internal class RoomSyncHandler @Inject constructor(
             if (event.eventId == null || event.senderId == null || event.type == null) {
                 continue
             }
-            val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it }
+            val ageLocalTs = syncLocalTimestampMillis - (event.unsignedData?.age ?: 0)
             val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType)
             if (event.stateKey != null) {
                 CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
@@ -401,7 +401,10 @@ internal class RoomSyncHandler @Inject constructor(
         for (rawEvent in eventList) {
             // It's annoying roomId is not there, but lot of code rely on it.
             // And had to do it now as copy would delete all decryption results..
-            val event = rawEvent.copy(roomId = roomId)
+            val ageLocalTs = syncLocalTimestampMillis - (rawEvent.unsignedData?.age ?: 0)
+            val event = rawEvent.copy(roomId = roomId).also {
+                it.ageLocalTs = ageLocalTs
+            }
             if (event.eventId == null || event.senderId == null || event.type == null) {
                 continue
             }
@@ -423,7 +426,6 @@ internal class RoomSyncHandler @Inject constructor(
                 contentToInject = threadsAwarenessHandler.makeEventThreadAware(realm, roomId, event)
             }
 
-            val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it }
             val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs, contentToInject).copyToRealmOrIgnore(realm, insertType)
             if (event.stateKey != null) {
                 CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt
index 8c7557a5b8611f156d8f970457fc0c2e79ea584f..70553359ff1a521ce6fadc486019afa02454c7d1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt
@@ -53,6 +53,7 @@ import org.matrix.android.sdk.internal.session.permalinks.PermalinkFactory
 import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
 import org.matrix.android.sdk.internal.session.room.timeline.GetEventTask
 import org.matrix.android.sdk.internal.util.awaitTransaction
+import org.matrix.android.sdk.internal.util.time.Clock
 import javax.inject.Inject
 
 /**
@@ -64,7 +65,8 @@ internal class ThreadsAwarenessHandler @Inject constructor(
         private val permalinkFactory: PermalinkFactory,
         @SessionDatabase private val monarchy: Monarchy,
         private val lightweightSettingsStorage: LightweightSettingsStorage,
-        private val getEventTask: GetEventTask
+        private val getEventTask: GetEventTask,
+        private val clock: Clock,
 ) {
 
     // This caching is responsible to improve the performance when we receive a root event
@@ -120,7 +122,7 @@ internal class ThreadsAwarenessHandler @Inject constructor(
     private suspend fun fetchThreadsEvents(threadsToFetch: Map<String, String>) {
         val eventEntityList = threadsToFetch.mapNotNull { (eventId, roomId) ->
             fetchEvent(eventId, roomId)?.let {
-                it.toEntity(roomId, SendState.SYNCED, it.ageLocalTs)
+                it.toEntity(roomId, SendState.SYNCED, it.ageLocalTs ?: clock.epochMillis())
             }
         }
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetService.kt
index 7b2edf2dbf3f22b189a5018e07ee1015c36a377d..9c59c11345aff2b47b4abe3c274ab0425ace7484 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/DefaultWidgetService.kt
@@ -17,7 +17,7 @@
 package org.matrix.android.sdk.internal.session.widgets
 
 import androidx.lifecycle.LiveData
-import org.matrix.android.sdk.api.query.QueryStringValue
+import org.matrix.android.sdk.api.query.QueryStateEventValue
 import org.matrix.android.sdk.api.session.events.model.Content
 import org.matrix.android.sdk.api.session.widgets.WidgetPostAPIMediator
 import org.matrix.android.sdk.api.session.widgets.WidgetService
@@ -43,7 +43,7 @@ internal class DefaultWidgetService @Inject constructor(
 
     override fun getRoomWidgets(
             roomId: String,
-            widgetId: QueryStringValue,
+            widgetId: QueryStateEventValue,
             widgetTypes: Set<String>?,
             excludedTypes: Set<String>?
     ): List<Widget> {
@@ -56,7 +56,7 @@ internal class DefaultWidgetService @Inject constructor(
 
     override fun getRoomWidgetsLive(
             roomId: String,
-            widgetId: QueryStringValue,
+            widgetId: QueryStateEventValue,
             widgetTypes: Set<String>?,
             excludedTypes: Set<String>?
     ): LiveData<List<Widget>> {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt
index 3f7db93b97d23efe45721238f63c3b03c641a8d7..37a329af34f54137d2877ad98384d4eb4f784b12 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/WidgetManager.kt
@@ -21,6 +21,7 @@ import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.LifecycleRegistry
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.Transformations
+import org.matrix.android.sdk.api.query.QueryStateEventValue
 import org.matrix.android.sdk.api.query.QueryStringValue
 import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.api.session.SessionLifecycleObserver
@@ -71,7 +72,7 @@ internal class WidgetManager @Inject constructor(
 
     fun getRoomWidgetsLive(
             roomId: String,
-            widgetId: QueryStringValue = QueryStringValue.NoCondition,
+            widgetId: QueryStateEventValue,
             widgetTypes: Set<String>? = null,
             excludedTypes: Set<String>? = null
     ): LiveData<List<Widget>> {
@@ -88,7 +89,7 @@ internal class WidgetManager @Inject constructor(
 
     fun getRoomWidgets(
             roomId: String,
-            widgetId: QueryStringValue = QueryStringValue.NoCondition,
+            widgetId: QueryStateEventValue,
             widgetTypes: Set<String>? = null,
             excludedTypes: Set<String>? = null
     ): List<Widget> {
@@ -199,7 +200,7 @@ internal class WidgetManager @Inject constructor(
         val powerLevelsEvent = stateEventDataSource.getStateEvent(
                 roomId = roomId,
                 eventType = EventType.STATE_ROOM_POWER_LEVELS,
-                stateKey = QueryStringValue.NoCondition
+                stateKey = QueryStringValue.IsEmpty
         )
         val powerLevelsContent = powerLevelsEvent?.content?.toModel<PowerLevelsContent>() ?: return false
         return PowerLevelsHelper(powerLevelsContent).isUserAllowedToSend(userId, true, EventType.STATE_ROOM_WIDGET_LEGACY)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/BackgroundDetectionObserver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/BackgroundDetectionObserver.kt
index 2dd16d83750a26aecf2a9cd4c67b9082cce14736..901d0eca8fb98b1028f5e93210097480507ff75c 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/BackgroundDetectionObserver.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/BackgroundDetectionObserver.kt
@@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.util
 import androidx.lifecycle.DefaultLifecycleObserver
 import androidx.lifecycle.LifecycleOwner
 import timber.log.Timber
+import java.util.concurrent.CopyOnWriteArraySet
 
 internal interface BackgroundDetectionObserver : DefaultLifecycleObserver {
     val isInBackground: Boolean
@@ -37,7 +38,7 @@ internal class DefaultBackgroundDetectionObserver : BackgroundDetectionObserver
     override var isInBackground: Boolean = true
         private set
 
-    private val listeners = LinkedHashSet<BackgroundDetectionObserver.Listener>()
+    private val listeners = CopyOnWriteArraySet<BackgroundDetectionObserver.Listener>()
 
     override fun register(listener: BackgroundDetectionObserver.Listener) {
         listeners.add(listener)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/SystemModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/SystemModule.kt
index 396d12f369c8fb611444f1549dfbc2504830339e..8c7d7704eda3115058b697264c257bf392971180 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/SystemModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/system/SystemModule.kt
@@ -18,6 +18,8 @@ package org.matrix.android.sdk.internal.util.system
 
 import dagger.Binds
 import dagger.Module
+import org.matrix.android.sdk.api.securestorage.SecureStorageService
+import org.matrix.android.sdk.internal.securestorage.DefaultSecureStorageService
 import org.matrix.android.sdk.internal.util.time.Clock
 import org.matrix.android.sdk.internal.util.time.DefaultClock
 
@@ -25,7 +27,7 @@ import org.matrix.android.sdk.internal.util.time.DefaultClock
 internal abstract class SystemModule {
 
     @Binds
-    abstract fun bindBuildVersionSdkIntProvider(provider: DefaultBuildVersionSdkIntProvider): BuildVersionSdkIntProvider
+    abstract fun bindSecureStorageService(service: DefaultSecureStorageService): SecureStorageService
 
     @Binds
     abstract fun bindClock(clock: DefaultClock): Clock
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/MatrixPatternsTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/MatrixPatternsTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0d0450adc2b9aab323a55c97a3627d0ddb8fbd81
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/MatrixPatternsTest.kt
@@ -0,0 +1,40 @@
+/*
+ * 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
+
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.Test
+
+class MatrixPatternsTest {
+
+    @Test
+    fun `given user id cases, when checking isUserId, then returns expected`() {
+        val cases = listOf(
+                UserIdCase("foobar", isUserId = false),
+                UserIdCase("@foobar", isUserId = false),
+                UserIdCase("foobar@matrix.org", isUserId = false),
+                UserIdCase("@foobar: matrix.org", isUserId = false),
+                UserIdCase("@foobar:matrix.org", isUserId = true),
+        )
+
+        cases.forEach { (input, expected) ->
+            MatrixPatterns.isUserId(input) shouldBeEqualTo expected
+        }
+    }
+}
+
+private data class UserIdCase(val input: String, val isUserId: Boolean)
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt
index 32b1d44fb9a5429c25cf1187f358507085b5175e..dac33069f348732ec667531b993025c5e1ebbad6 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt
@@ -23,10 +23,12 @@ import org.amshove.kluent.shouldBeEqualTo
 import org.junit.Test
 import org.matrix.android.sdk.api.session.pushers.PusherState
 import org.matrix.android.sdk.internal.database.model.PusherEntity
+import org.matrix.android.sdk.internal.database.model.PusherEntityFields
 import org.matrix.android.sdk.test.fakes.FakeGlobalErrorReceiver
 import org.matrix.android.sdk.test.fakes.FakeMonarchy
 import org.matrix.android.sdk.test.fakes.FakePushersAPI
 import org.matrix.android.sdk.test.fakes.FakeRequestExecutor
+import org.matrix.android.sdk.test.fakes.givenEqualTo
 import java.net.SocketException
 
 private val A_JSON_PUSHER = JsonPusher(
@@ -56,6 +58,7 @@ class DefaultAddPusherTaskTest {
     @Test
     fun `given no persisted pusher when adding Pusher then updates api and inserts result with Registered state`() {
         monarchy.givenWhereReturns<PusherEntity>(result = null)
+                .givenEqualTo(PusherEntityFields.PUSH_KEY, A_JSON_PUSHER.pushKey)
 
         runTest { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
 
@@ -71,6 +74,7 @@ class DefaultAddPusherTaskTest {
     fun `given a persisted pusher when adding Pusher then updates api and mutates persisted result with Registered state`() {
         val realmResult = PusherEntity(appDisplayName = null)
         monarchy.givenWhereReturns(result = realmResult)
+                .givenEqualTo(PusherEntityFields.PUSH_KEY, A_JSON_PUSHER.pushKey)
 
         runTest { addPusherTask.execute(AddPusherTask.Params(A_JSON_PUSHER)) }
 
@@ -84,6 +88,7 @@ class DefaultAddPusherTaskTest {
     fun `given a persisted push entity and SetPush API fails when adding Pusher then mutates persisted result with Failed registration state and rethrows`() {
         val realmResult = PusherEntity()
         monarchy.givenWhereReturns(result = realmResult)
+                .givenEqualTo(PusherEntityFields.PUSH_KEY, A_JSON_PUSHER.pushKey)
         pushersAPI.givenSetPusherErrors(SocketException())
 
         assertFailsWith<SocketException> {
@@ -96,6 +101,7 @@ class DefaultAddPusherTaskTest {
     @Test
     fun `given no persisted push entity and SetPush API fails when adding Pusher then rethrows error`() {
         monarchy.givenWhereReturns<PusherEntity>(result = null)
+                .givenEqualTo(PusherEntityFields.PUSH_KEY, A_JSON_PUSHER.pushKey)
         pushersAPI.givenSetPusherErrors(SocketException())
 
         assertFailsWith<SocketException> {
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..933087af2b6027fb264ed584973dd882095fc85a
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt
@@ -0,0 +1,405 @@
+/*
+ * 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.session.room.aggregation.livelocation
+
+import androidx.work.ExistingWorkPolicy
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.Test
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.UnsignedData
+import org.matrix.android.sdk.api.session.events.model.toContent
+import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.room.model.message.LocationInfo
+import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
+import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
+import org.matrix.android.sdk.internal.database.mapper.ContentMapper
+import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
+import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields
+import org.matrix.android.sdk.test.fakes.FakeClock
+import org.matrix.android.sdk.test.fakes.FakeRealm
+import org.matrix.android.sdk.test.fakes.FakeWorkManagerProvider
+import org.matrix.android.sdk.test.fakes.givenEqualTo
+import org.matrix.android.sdk.test.fakes.givenFindAll
+import org.matrix.android.sdk.test.fakes.givenFindFirst
+import org.matrix.android.sdk.test.fakes.givenNotEqualTo
+
+private const val A_SESSION_ID = "session_id"
+private const val A_SENDER_ID = "sender_id"
+private const val AN_EVENT_ID = "event_id"
+private const val A_ROOM_ID = "room_id"
+private const val A_TIMESTAMP = 1654689143L
+private const val A_TIMEOUT_MILLIS = 15 * 60 * 1000L
+private const val A_LATITUDE = 40.05
+private const val A_LONGITUDE = 29.24
+private const val A_UNCERTAINTY = 30.0
+private const val A_GEO_URI = "geo:$A_LATITUDE,$A_LONGITUDE;u=$A_UNCERTAINTY"
+
+internal class LiveLocationAggregationProcessorTest {
+
+    private val fakeWorkManagerProvider = FakeWorkManagerProvider()
+    private val fakeClock = FakeClock()
+    private val fakeRealm = FakeRealm()
+    private val fakeQuery = fakeRealm.givenWhere<LiveLocationShareAggregatedSummaryEntity>()
+
+    private val liveLocationAggregationProcessor = LiveLocationAggregationProcessor(
+            sessionId = A_SESSION_ID,
+            workManagerProvider = fakeWorkManagerProvider.instance,
+            clock = fakeClock
+    )
+
+    @Test
+    fun `given beacon info when it is local echo then it is ignored`() {
+        val event = Event(senderId = A_SENDER_ID)
+        val beaconInfo = MessageBeaconInfoContent()
+
+        val result = liveLocationAggregationProcessor.handleBeaconInfo(
+                realm = fakeRealm.instance,
+                event = event,
+                content = beaconInfo,
+                roomId = A_ROOM_ID,
+                isLocalEcho = true
+        )
+
+        result shouldBeEqualTo false
+    }
+
+    private data class IgnoredBeaconInfoEvent(
+            val event: Event,
+            val beaconInfo: MessageBeaconInfoContent
+    )
+
+    @Test
+    fun `given beacon info and event when some values are missing then it is ignored`() {
+        val ignoredInfoEvents = listOf(
+                // missing senderId
+                IgnoredBeaconInfoEvent(
+                        event = Event(eventId = AN_EVENT_ID, senderId = null),
+                        beaconInfo = MessageBeaconInfoContent()
+                ),
+                // empty senderId
+                IgnoredBeaconInfoEvent(
+                        event = Event(eventId = AN_EVENT_ID, senderId = ""),
+                        beaconInfo = MessageBeaconInfoContent()
+                ),
+                // beacon is live and no eventId
+                IgnoredBeaconInfoEvent(
+                        event = Event(eventId = null, senderId = A_SENDER_ID),
+                        beaconInfo = MessageBeaconInfoContent(isLive = true)
+                ),
+                // beacon is live and eventId is empty
+                IgnoredBeaconInfoEvent(
+                        event = Event(eventId = "", senderId = A_SENDER_ID),
+                        beaconInfo = MessageBeaconInfoContent(isLive = true)
+                ),
+                // beacon is not live and replaced event id is null
+                IgnoredBeaconInfoEvent(
+                        event = Event(
+                                eventId = AN_EVENT_ID,
+                                senderId = A_SENDER_ID,
+                                unsignedData = UnsignedData(
+                                        age = 123,
+                                        replacesState = null
+                                )
+                        ),
+                        beaconInfo = MessageBeaconInfoContent(isLive = false)
+                ),
+                // beacon is not live and replaced event id is empty
+                IgnoredBeaconInfoEvent(
+                        event = Event(
+                                eventId = AN_EVENT_ID,
+                                senderId = A_SENDER_ID,
+                                unsignedData = UnsignedData(
+                                        age = 123,
+                                        replacesState = ""
+                                )
+                        ),
+                        beaconInfo = MessageBeaconInfoContent(isLive = false)
+                ),
+        )
+
+        ignoredInfoEvents.forEach {
+            val result = liveLocationAggregationProcessor.handleBeaconInfo(
+                    realm = fakeRealm.instance,
+                    event = it.event,
+                    content = it.beaconInfo,
+                    roomId = A_ROOM_ID,
+                    isLocalEcho = false
+            )
+
+            result shouldBeEqualTo false
+        }
+    }
+
+    @Test
+    fun `given beacon info and existing entity when beacon content is correct and active then it is aggregated`() {
+        val event = Event(
+                senderId = A_SENDER_ID,
+                eventId = AN_EVENT_ID
+        )
+        val beaconInfo = MessageBeaconInfoContent(
+                isLive = true,
+                unstableTimestampMillis = A_TIMESTAMP,
+                timeout = A_TIMEOUT_MILLIS
+        )
+        fakeClock.givenEpoch(A_TIMESTAMP + 5000)
+        fakeWorkManagerProvider.fakeWorkManager.expectEnqueueUniqueWork()
+        val aggregatedEntity = givenLastSummaryQueryReturns(eventId = AN_EVENT_ID, roomId = A_ROOM_ID)
+        val previousEntities = givenActiveSummaryListQueryReturns(
+                listOf(
+                        LiveLocationShareAggregatedSummaryEntity(
+                                eventId = "${AN_EVENT_ID}1",
+                                roomId = A_ROOM_ID,
+                                userId = A_SENDER_ID,
+                                isActive = true
+                        )
+                )
+        )
+
+        val result = liveLocationAggregationProcessor.handleBeaconInfo(
+                realm = fakeRealm.instance,
+                event = event,
+                content = beaconInfo,
+                roomId = A_ROOM_ID,
+                isLocalEcho = false
+        )
+
+        result shouldBeEqualTo true
+        aggregatedEntity.eventId shouldBeEqualTo AN_EVENT_ID
+        aggregatedEntity.roomId shouldBeEqualTo A_ROOM_ID
+        aggregatedEntity.userId shouldBeEqualTo A_SENDER_ID
+        aggregatedEntity.isActive shouldBeEqualTo true
+        aggregatedEntity.endOfLiveTimestampMillis shouldBeEqualTo A_TIMESTAMP + A_TIMEOUT_MILLIS
+        aggregatedEntity.lastLocationContent shouldBeEqualTo null
+        previousEntities.forEach { entity ->
+            entity.isActive shouldBeEqualTo false
+        }
+        fakeWorkManagerProvider.fakeWorkManager.verifyEnqueueUniqueWork(
+                workName = DeactivateLiveLocationShareWorker.getWorkName(eventId = AN_EVENT_ID, roomId = A_ROOM_ID),
+                policy = ExistingWorkPolicy.REPLACE
+        )
+    }
+
+    @Test
+    fun `given beacon info and existing entity when beacon content is correct and inactive then it is aggregated`() {
+        val unsignedData = UnsignedData(
+                age = 123,
+                replacesState = AN_EVENT_ID
+        )
+        val event = Event(
+                senderId = A_SENDER_ID,
+                eventId = "",
+                unsignedData = unsignedData
+        )
+        val beaconInfo = MessageBeaconInfoContent(
+                isLive = false,
+                unstableTimestampMillis = A_TIMESTAMP,
+                timeout = A_TIMEOUT_MILLIS
+        )
+        fakeClock.givenEpoch(A_TIMESTAMP + 5000)
+        fakeWorkManagerProvider.fakeWorkManager.expectCancelUniqueWork()
+        val aggregatedEntity = givenLastSummaryQueryReturns(eventId = AN_EVENT_ID, roomId = A_ROOM_ID)
+        val previousEntities = givenActiveSummaryListQueryReturns(
+                listOf(
+                        LiveLocationShareAggregatedSummaryEntity(
+                                eventId = "${AN_EVENT_ID}1",
+                                roomId = A_ROOM_ID,
+                                userId = A_SENDER_ID,
+                                isActive = true
+                        )
+                )
+
+        )
+
+        val result = liveLocationAggregationProcessor.handleBeaconInfo(
+                realm = fakeRealm.instance,
+                event = event,
+                content = beaconInfo,
+                roomId = A_ROOM_ID,
+                isLocalEcho = false
+        )
+
+        result shouldBeEqualTo true
+        aggregatedEntity.eventId shouldBeEqualTo AN_EVENT_ID
+        aggregatedEntity.roomId shouldBeEqualTo A_ROOM_ID
+        aggregatedEntity.userId shouldBeEqualTo A_SENDER_ID
+        aggregatedEntity.isActive shouldBeEqualTo false
+        aggregatedEntity.endOfLiveTimestampMillis shouldBeEqualTo A_TIMESTAMP + A_TIMEOUT_MILLIS
+        aggregatedEntity.lastLocationContent shouldBeEqualTo null
+        previousEntities.forEach { entity ->
+            entity.isActive shouldBeEqualTo false
+        }
+        fakeWorkManagerProvider.fakeWorkManager.verifyCancelUniqueWork(
+                workName = DeactivateLiveLocationShareWorker.getWorkName(eventId = AN_EVENT_ID, roomId = A_ROOM_ID)
+        )
+    }
+
+    @Test
+    fun `given beacon location data when it is local echo then it is ignored`() {
+        val event = Event(senderId = A_SENDER_ID)
+        val beaconLocationData = MessageBeaconLocationDataContent()
+
+        val result = liveLocationAggregationProcessor.handleBeaconLocationData(
+                realm = fakeRealm.instance,
+                event = event,
+                content = beaconLocationData,
+                roomId = A_ROOM_ID,
+                relatedEventId = AN_EVENT_ID,
+                isLocalEcho = true
+        )
+
+        result shouldBeEqualTo false
+    }
+
+    private data class IgnoredBeaconLocationDataEvent(
+            val event: Event,
+            val beaconLocationData: MessageBeaconLocationDataContent
+    )
+
+    @Test
+    fun `given event and beacon location data when some values are missing then it is ignored`() {
+        val ignoredLocationDataEvents = listOf(
+                // missing sender id
+                IgnoredBeaconLocationDataEvent(
+                        event = Event(eventId = AN_EVENT_ID),
+                        beaconLocationData = MessageBeaconLocationDataContent()
+                ),
+                // empty sender id
+                IgnoredBeaconLocationDataEvent(
+                        event = Event(eventId = AN_EVENT_ID, senderId = ""),
+                        beaconLocationData = MessageBeaconLocationDataContent()
+                ),
+        )
+
+        ignoredLocationDataEvents.forEach {
+            val result = liveLocationAggregationProcessor.handleBeaconLocationData(
+                    realm = fakeRealm.instance,
+                    event = it.event,
+                    content = it.beaconLocationData,
+                    roomId = A_ROOM_ID,
+                    relatedEventId = "",
+                    isLocalEcho = false
+            )
+            result shouldBeEqualTo false
+        }
+    }
+
+    @Test
+    fun `given beacon location data when relatedEventId is null or empty then it is ignored`() {
+        val event = Event(senderId = A_SENDER_ID)
+        val beaconLocationData = MessageBeaconLocationDataContent()
+
+        listOf(null, "").forEach {
+            val result = liveLocationAggregationProcessor.handleBeaconLocationData(
+                    realm = fakeRealm.instance,
+                    event = event,
+                    content = beaconLocationData,
+                    roomId = A_ROOM_ID,
+                    relatedEventId = it,
+                    isLocalEcho = false
+            )
+            result shouldBeEqualTo false
+        }
+    }
+
+    @Test
+    fun `given beacon location data when location is less recent than the saved one then it is ignored`() {
+        val event = Event(eventId = AN_EVENT_ID, senderId = A_SENDER_ID)
+        val beaconLocationData = MessageBeaconLocationDataContent(
+                unstableTimestampMillis = A_TIMESTAMP - 60_000
+        )
+        val lastBeaconLocationContent = MessageBeaconLocationDataContent(
+                unstableTimestampMillis = A_TIMESTAMP
+        )
+        givenLastSummaryQueryReturns(
+                eventId = AN_EVENT_ID,
+                roomId = A_ROOM_ID,
+                beaconLocationContent = lastBeaconLocationContent
+        )
+
+        val result = liveLocationAggregationProcessor.handleBeaconLocationData(
+                realm = fakeRealm.instance,
+                event = event,
+                content = beaconLocationData,
+                roomId = A_ROOM_ID,
+                relatedEventId = AN_EVENT_ID,
+                isLocalEcho = false
+        )
+
+        result shouldBeEqualTo false
+    }
+
+    @Test
+    fun `given beacon location data when location is more recent than the saved one then it is aggregated`() {
+        val event = Event(eventId = AN_EVENT_ID, senderId = A_SENDER_ID)
+        val locationInfo = LocationInfo(geoUri = A_GEO_URI)
+        val beaconLocationData = MessageBeaconLocationDataContent(
+                unstableTimestampMillis = A_TIMESTAMP,
+                unstableLocationInfo = locationInfo
+        )
+        val lastBeaconLocationContent = MessageBeaconLocationDataContent(
+                unstableTimestampMillis = A_TIMESTAMP - 60_000
+        )
+        val entity = givenLastSummaryQueryReturns(
+                eventId = AN_EVENT_ID,
+                roomId = A_ROOM_ID,
+                beaconLocationContent = lastBeaconLocationContent
+        )
+
+        val result = liveLocationAggregationProcessor.handleBeaconLocationData(
+                realm = fakeRealm.instance,
+                event = event,
+                content = beaconLocationData,
+                roomId = A_ROOM_ID,
+                relatedEventId = AN_EVENT_ID,
+                isLocalEcho = false
+        )
+
+        result shouldBeEqualTo true
+        val savedLocationData = ContentMapper.map(entity.lastLocationContent).toModel<MessageBeaconLocationDataContent>()
+        savedLocationData?.getBestTimestampMillis() shouldBeEqualTo A_TIMESTAMP
+        savedLocationData?.getBestLocationInfo()?.geoUri shouldBeEqualTo A_GEO_URI
+    }
+
+    private fun givenLastSummaryQueryReturns(
+            eventId: String,
+            roomId: String,
+            beaconLocationContent: MessageBeaconLocationDataContent? = null
+    ): LiveLocationShareAggregatedSummaryEntity {
+        val result = LiveLocationShareAggregatedSummaryEntity(
+                eventId = eventId,
+                roomId = roomId,
+                lastLocationContent = ContentMapper.map(beaconLocationContent?.toContent())
+        )
+        fakeQuery
+                .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, eventId)
+                .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.ROOM_ID, roomId)
+                .givenFindFirst(result)
+        return result
+    }
+
+    private fun givenActiveSummaryListQueryReturns(
+            summaryList: List<LiveLocationShareAggregatedSummaryEntity>
+    ): List<LiveLocationShareAggregatedSummaryEntity> {
+        fakeQuery
+                .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.ROOM_ID, A_ROOM_ID)
+                .givenNotEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, AN_EVENT_ID)
+                .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.USER_ID, A_SENDER_ID)
+                .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true)
+                .givenFindAll(summaryList)
+        return summaryList
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessorTest.kt
index 837bbeea268b60defcf9da04a6437c3b501db454..3044ca5d436e6945027ccd2712a09ea1bffc5442 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessorTest.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessorTest.kt
@@ -19,8 +19,6 @@ package org.matrix.android.sdk.internal.session.room.aggregation.poll
 import io.mockk.every
 import io.mockk.mockk
 import io.realm.RealmList
-import io.realm.RealmModel
-import io.realm.RealmQuery
 import org.amshove.kluent.shouldBeFalse
 import org.amshove.kluent.shouldBeTrue
 import org.junit.Before
@@ -46,6 +44,8 @@ import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsT
 import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_TIMELINE_EVENT
 import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollEventsTestData.A_USER_ID_1
 import org.matrix.android.sdk.test.fakes.FakeRealm
+import org.matrix.android.sdk.test.fakes.givenEqualTo
+import org.matrix.android.sdk.test.fakes.givenFindFirst
 
 class PollAggregationProcessorTest {
 
@@ -135,14 +135,11 @@ class PollAggregationProcessorTest {
         pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, event).shouldBeFalse()
     }
 
-    private inline fun <reified T : RealmModel> RealmQuery<T>.givenEqualTo(fieldName: String, value: String, result: RealmQuery<T>) {
-        every { equalTo(fieldName, value) } returns result
-    }
-
     private fun mockEventAnnotationsSummaryEntity() {
-        val queryResult = realm.givenWhereReturns(result = EventAnnotationsSummaryEntity())
-        queryResult.givenEqualTo(EventAnnotationsSummaryEntityFields.ROOM_ID, A_POLL_REPLACE_EVENT.roomId!!, queryResult)
-        queryResult.givenEqualTo(EventAnnotationsSummaryEntityFields.EVENT_ID, A_POLL_REPLACE_EVENT.eventId!!, queryResult)
+        realm.givenWhere<EventAnnotationsSummaryEntity>()
+                .givenFindFirst(EventAnnotationsSummaryEntity())
+                .givenEqualTo(EventAnnotationsSummaryEntityFields.ROOM_ID, A_POLL_REPLACE_EVENT.roomId!!)
+                .givenEqualTo(EventAnnotationsSummaryEntityFields.EVENT_ID, A_POLL_REPLACE_EVENT.eventId!!)
     }
 
     private fun mockRoom(
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultCheckIfExistingActiveLiveTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultCheckIfExistingActiveLiveTaskTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..3198392eabecbc1d592b63911f3787279d0aecbd
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultCheckIfExistingActiveLiveTaskTest.kt
@@ -0,0 +1,105 @@
+/*
+ * 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.session.room.location
+
+import io.mockk.unmockkAll
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.After
+import org.junit.Test
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.session.events.model.toContent
+import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
+import org.matrix.android.sdk.test.fakes.FakeGetActiveBeaconInfoForUserTask
+
+private const val A_USER_ID = "user-id"
+private const val A_ROOM_ID = "room-id"
+private const val A_TIMEOUT = 15_000L
+private const val AN_EPOCH = 1655210176L
+
+@ExperimentalCoroutinesApi
+class DefaultCheckIfExistingActiveLiveTaskTest {
+
+    private val fakeGetActiveBeaconInfoForUserTask = FakeGetActiveBeaconInfoForUserTask()
+
+    private val defaultCheckIfExistingActiveLiveTask = DefaultCheckIfExistingActiveLiveTask(
+            getActiveBeaconInfoForUserTask = fakeGetActiveBeaconInfoForUserTask
+    )
+
+    @After
+    fun tearDown() {
+        unmockkAll()
+    }
+
+    @Test
+    fun `given parameters and existing active live event when calling the task then result is true`() = runTest {
+        val params = CheckIfExistingActiveLiveTask.Params(
+                roomId = A_ROOM_ID
+        )
+        val currentStateEvent = Event(
+                stateKey = A_USER_ID,
+                content = MessageBeaconInfoContent(
+                        timeout = A_TIMEOUT,
+                        isLive = true,
+                        unstableTimestampMillis = AN_EPOCH
+                ).toContent()
+        )
+        fakeGetActiveBeaconInfoForUserTask.givenExecuteReturns(currentStateEvent)
+
+        val result = defaultCheckIfExistingActiveLiveTask.execute(params)
+
+        result shouldBeEqualTo true
+        val expectedGetActiveBeaconParams = GetActiveBeaconInfoForUserTask.Params(
+                roomId = params.roomId
+        )
+        fakeGetActiveBeaconInfoForUserTask.verifyExecute(expectedGetActiveBeaconParams)
+    }
+
+    @Test
+    fun `given parameters and no existing active live event when calling the task then result is false`() = runTest {
+        val params = CheckIfExistingActiveLiveTask.Params(
+                roomId = A_ROOM_ID
+        )
+        val inactiveEvents = listOf(
+                // no event
+                null,
+                // null content
+                Event(
+                        stateKey = A_USER_ID,
+                        content = null
+                ),
+                // inactive live
+                Event(
+                        stateKey = A_USER_ID,
+                        content = MessageBeaconInfoContent(
+                                timeout = A_TIMEOUT,
+                                isLive = false,
+                                unstableTimestampMillis = AN_EPOCH
+                        ).toContent()
+                )
+        )
+
+        inactiveEvents.forEach { currentStateEvent ->
+            fakeGetActiveBeaconInfoForUserTask.givenExecuteReturns(currentStateEvent)
+
+            val result = defaultCheckIfExistingActiveLiveTask.execute(params)
+
+            result shouldBeEqualTo false
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..588bfaa9796a68ae4d9e523d7f206897a97c5e71
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt
@@ -0,0 +1,75 @@
+/*
+ * 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.session.room.location
+
+import io.mockk.unmockkAll
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.After
+import org.junit.Test
+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.room.model.message.MessageBeaconInfoContent
+import org.matrix.android.sdk.test.fakes.FakeStateEventDataSource
+
+private const val A_USER_ID = "user-id"
+private const val A_ROOM_ID = "room-id"
+private const val A_TIMEOUT = 15_000L
+private const val AN_EPOCH = 1655210176L
+
+@ExperimentalCoroutinesApi
+class DefaultGetActiveBeaconInfoForUserTaskTest {
+
+    private val fakeStateEventDataSource = FakeStateEventDataSource()
+
+    private val defaultGetActiveBeaconInfoForUserTask = DefaultGetActiveBeaconInfoForUserTask(
+            userId = A_USER_ID,
+            stateEventDataSource = fakeStateEventDataSource.instance
+    )
+
+    @After
+    fun tearDown() {
+        unmockkAll()
+    }
+
+    @Test
+    fun `given parameters and no error when calling the task then result is computed`() = runTest {
+        val currentStateEvent = Event(
+                stateKey = A_USER_ID,
+                content = MessageBeaconInfoContent(
+                        timeout = A_TIMEOUT,
+                        isLive = true,
+                        unstableTimestampMillis = AN_EPOCH
+                ).toContent()
+        )
+        fakeStateEventDataSource.givenGetStateEventReturns(currentStateEvent)
+        val params = GetActiveBeaconInfoForUserTask.Params(
+                roomId = A_ROOM_ID
+        )
+
+        val result = defaultGetActiveBeaconInfoForUserTask.execute(params)
+
+        result shouldBeEqualTo currentStateEvent
+        fakeStateEventDataSource.verifyGetStateEvent(
+                roomId = params.roomId,
+                eventType = EventType.STATE_ROOM_BEACON_INFO.first(),
+                stateKey = A_USER_ID
+        )
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..de9120653160a866563253f94ae3d8e9794bea71
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt
@@ -0,0 +1,268 @@
+/*
+ * 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.session.room.location
+
+import androidx.arch.core.util.Function
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.Transformations
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.slot
+import io.mockk.unmockkAll
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult
+import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
+import org.matrix.android.sdk.api.util.Cancelable
+import org.matrix.android.sdk.api.util.Optional
+import org.matrix.android.sdk.api.util.toOptional
+import org.matrix.android.sdk.internal.database.mapper.LiveLocationShareAggregatedSummaryMapper
+import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
+import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields
+import org.matrix.android.sdk.test.fakes.FakeMonarchy
+import org.matrix.android.sdk.test.fakes.givenEqualTo
+import org.matrix.android.sdk.test.fakes.givenIsNotEmpty
+import org.matrix.android.sdk.test.fakes.givenIsNotNull
+
+private const val A_ROOM_ID = "room_id"
+private const val AN_EVENT_ID = "event_id"
+private const val A_LATITUDE = 1.4
+private const val A_LONGITUDE = 40.0
+private const val AN_UNCERTAINTY = 5.0
+private const val A_TIMEOUT = 15_000L
+
+@ExperimentalCoroutinesApi
+internal class DefaultLocationSharingServiceTest {
+
+    private val fakeMonarchy = FakeMonarchy()
+    private val sendStaticLocationTask = mockk<SendStaticLocationTask>()
+    private val sendLiveLocationTask = mockk<SendLiveLocationTask>()
+    private val startLiveLocationShareTask = mockk<StartLiveLocationShareTask>()
+    private val stopLiveLocationShareTask = mockk<StopLiveLocationShareTask>()
+    private val checkIfExistingActiveLiveTask = mockk<CheckIfExistingActiveLiveTask>()
+    private val fakeLiveLocationShareAggregatedSummaryMapper = mockk<LiveLocationShareAggregatedSummaryMapper>()
+
+    private val defaultLocationSharingService = DefaultLocationSharingService(
+            roomId = A_ROOM_ID,
+            monarchy = fakeMonarchy.instance,
+            sendStaticLocationTask = sendStaticLocationTask,
+            sendLiveLocationTask = sendLiveLocationTask,
+            startLiveLocationShareTask = startLiveLocationShareTask,
+            stopLiveLocationShareTask = stopLiveLocationShareTask,
+            checkIfExistingActiveLiveTask = checkIfExistingActiveLiveTask,
+            liveLocationShareAggregatedSummaryMapper = fakeLiveLocationShareAggregatedSummaryMapper
+    )
+
+    @Before
+    fun setUp() {
+        mockkStatic("androidx.lifecycle.Transformations")
+    }
+
+    @After
+    fun tearDown() {
+        unmockkAll()
+    }
+
+    @Test
+    fun `static location can be sent`() = runTest {
+        val isUserLocation = true
+        val cancelable = mockk<Cancelable>()
+        coEvery { sendStaticLocationTask.execute(any()) } returns cancelable
+
+        val result = defaultLocationSharingService.sendStaticLocation(
+                latitude = A_LATITUDE,
+                longitude = A_LONGITUDE,
+                uncertainty = AN_UNCERTAINTY,
+                isUserLocation = isUserLocation
+        )
+
+        result shouldBeEqualTo cancelable
+        val expectedParams = SendStaticLocationTask.Params(
+                roomId = A_ROOM_ID,
+                latitude = A_LATITUDE,
+                longitude = A_LONGITUDE,
+                uncertainty = AN_UNCERTAINTY,
+                isUserLocation = isUserLocation,
+        )
+        coVerify { sendStaticLocationTask.execute(expectedParams) }
+    }
+
+    @Test
+    fun `live location can be sent`() = runTest {
+        val cancelable = mockk<Cancelable>()
+        coEvery { sendLiveLocationTask.execute(any()) } returns cancelable
+
+        val result = defaultLocationSharingService.sendLiveLocation(
+                beaconInfoEventId = AN_EVENT_ID,
+                latitude = A_LATITUDE,
+                longitude = A_LONGITUDE,
+                uncertainty = AN_UNCERTAINTY
+        )
+
+        result shouldBeEqualTo cancelable
+        val expectedParams = SendLiveLocationTask.Params(
+                roomId = A_ROOM_ID,
+                beaconInfoEventId = AN_EVENT_ID,
+                latitude = A_LATITUDE,
+                longitude = A_LONGITUDE,
+                uncertainty = AN_UNCERTAINTY
+        )
+        coVerify { sendLiveLocationTask.execute(expectedParams) }
+    }
+
+    @Test
+    fun `given existing active live can be stopped when starting a live then the current live is stopped and the new live is started`() = runTest {
+        coEvery { checkIfExistingActiveLiveTask.execute(any()) } returns true
+        coEvery { stopLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success("stopped-event-id")
+        coEvery { startLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
+
+        val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT)
+
+        result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
+        val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params(
+                roomId = A_ROOM_ID
+        )
+        coVerify { checkIfExistingActiveLiveTask.execute(expectedCheckExistingParams) }
+        val expectedStopParams = StopLiveLocationShareTask.Params(
+                roomId = A_ROOM_ID
+        )
+        coVerify { stopLiveLocationShareTask.execute(expectedStopParams) }
+        val expectedStartParams = StartLiveLocationShareTask.Params(
+                roomId = A_ROOM_ID,
+                timeoutMillis = A_TIMEOUT
+        )
+        coVerify { startLiveLocationShareTask.execute(expectedStartParams) }
+    }
+
+    @Test
+    fun `given existing active live cannot be stopped when starting a live then the result is failure`() = runTest {
+        coEvery { checkIfExistingActiveLiveTask.execute(any()) } returns true
+        val error = Throwable()
+        coEvery { stopLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Failure(error)
+
+        val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT)
+
+        result shouldBeEqualTo UpdateLiveLocationShareResult.Failure(error)
+        val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params(
+                roomId = A_ROOM_ID
+        )
+        coVerify { checkIfExistingActiveLiveTask.execute(expectedCheckExistingParams) }
+        val expectedStopParams = StopLiveLocationShareTask.Params(
+                roomId = A_ROOM_ID
+        )
+        coVerify { stopLiveLocationShareTask.execute(expectedStopParams) }
+    }
+
+    @Test
+    fun `given no existing active live when starting a live then the new live is started`() = runTest {
+        coEvery { checkIfExistingActiveLiveTask.execute(any()) } returns false
+        coEvery { startLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
+
+        val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT)
+
+        result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
+        val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params(
+                roomId = A_ROOM_ID
+        )
+        coVerify { checkIfExistingActiveLiveTask.execute(expectedCheckExistingParams) }
+        val expectedStartParams = StartLiveLocationShareTask.Params(
+                roomId = A_ROOM_ID,
+                timeoutMillis = A_TIMEOUT
+        )
+        coVerify { startLiveLocationShareTask.execute(expectedStartParams) }
+    }
+
+    @Test
+    fun `live location share can be stopped`() = runTest {
+        coEvery { stopLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
+
+        val result = defaultLocationSharingService.stopLiveLocationShare()
+
+        result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
+        val expectedParams = StopLiveLocationShareTask.Params(
+                roomId = A_ROOM_ID
+        )
+        coVerify { stopLiveLocationShareTask.execute(expectedParams) }
+    }
+
+    @Test
+    fun `livedata of live summaries is correctly computed`() {
+        val entity = LiveLocationShareAggregatedSummaryEntity()
+        val summary = LiveLocationShareAggregatedSummary(
+                userId = "",
+                isActive = true,
+                endOfLiveTimestampMillis = 123,
+                lastLocationDataContent = null
+        )
+
+        fakeMonarchy.givenWhere<LiveLocationShareAggregatedSummaryEntity>()
+                .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.ROOM_ID, A_ROOM_ID)
+                .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true)
+                .givenIsNotEmpty(LiveLocationShareAggregatedSummaryEntityFields.USER_ID)
+                .givenIsNotNull(LiveLocationShareAggregatedSummaryEntityFields.LAST_LOCATION_CONTENT)
+        fakeMonarchy.givenFindAllMappedWithChangesReturns(
+                realmEntities = listOf(entity),
+                mappedResult = listOf(summary),
+                fakeLiveLocationShareAggregatedSummaryMapper
+        )
+
+        val result = defaultLocationSharingService.getRunningLiveLocationShareSummaries().value
+
+        result shouldBeEqualTo listOf(summary)
+    }
+
+    @Test
+    fun `given an event id when getting livedata on corresponding live summary then it is correctly computed`() {
+        val entity = LiveLocationShareAggregatedSummaryEntity()
+        val summary = LiveLocationShareAggregatedSummary(
+                userId = "",
+                isActive = true,
+                endOfLiveTimestampMillis = 123,
+                lastLocationDataContent = null
+        )
+
+        fakeMonarchy.givenWhere<LiveLocationShareAggregatedSummaryEntity>()
+                .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.ROOM_ID, A_ROOM_ID)
+                .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, AN_EVENT_ID)
+        val liveData = fakeMonarchy.givenFindAllMappedWithChangesReturns(
+                realmEntities = listOf(entity),
+                mappedResult = listOf(summary),
+                fakeLiveLocationShareAggregatedSummaryMapper
+        )
+        val mapper = slot<Function<List<LiveLocationShareAggregatedSummary>, Optional<LiveLocationShareAggregatedSummary>>>()
+        every {
+            Transformations.map(
+                    liveData,
+                    capture(mapper)
+            )
+        } answers {
+            val value = secondArg<Function<List<LiveLocationShareAggregatedSummary>, Optional<LiveLocationShareAggregatedSummary>>>().apply(listOf(summary))
+            MutableLiveData(value)
+        }
+
+        val result = defaultLocationSharingService.getLiveLocationShareSummary(AN_EVENT_ID).value
+
+        result shouldBeEqualTo summary.toOptional()
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultSendLiveLocationTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultSendLiveLocationTaskTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..423c6800549ab4c96fb21ed80afcfb44ccc83c82
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultSendLiveLocationTaskTest.kt
@@ -0,0 +1,79 @@
+/*
+ * 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.session.room.location
+
+import io.mockk.mockk
+import io.mockk.unmockkAll
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.After
+import org.junit.Test
+import org.matrix.android.sdk.api.util.Cancelable
+import org.matrix.android.sdk.test.fakes.FakeEventSenderProcessor
+import org.matrix.android.sdk.test.fakes.FakeLocalEchoEventFactory
+
+private const val A_ROOM_ID = "room_id"
+private const val AN_EVENT_ID = "event_id"
+private const val A_LATITUDE = 1.4
+private const val A_LONGITUDE = 44.0
+private const val AN_UNCERTAINTY = 5.0
+
+@ExperimentalCoroutinesApi
+internal class DefaultSendLiveLocationTaskTest {
+
+    private val fakeLocalEchoEventFactory = FakeLocalEchoEventFactory()
+    private val fakeEventSenderProcessor = FakeEventSenderProcessor()
+
+    private val defaultSendLiveLocationTask = DefaultSendLiveLocationTask(
+            localEchoEventFactory = fakeLocalEchoEventFactory.instance,
+            eventSenderProcessor = fakeEventSenderProcessor
+    )
+
+    @After
+    fun tearDown() {
+        unmockkAll()
+    }
+
+    @Test
+    fun `given parameters when calling the task then it is correctly executed`() = runTest {
+        val params = SendLiveLocationTask.Params(
+                roomId = A_ROOM_ID,
+                beaconInfoEventId = AN_EVENT_ID,
+                latitude = A_LATITUDE,
+                longitude = A_LONGITUDE,
+                uncertainty = AN_UNCERTAINTY
+        )
+        val event = fakeLocalEchoEventFactory.givenCreateLiveLocationEvent(
+                withLocalEcho = true
+        )
+        val cancelable = mockk<Cancelable>()
+        fakeEventSenderProcessor.givenPostEventReturns(event, cancelable)
+
+        val result = defaultSendLiveLocationTask.execute(params)
+
+        result shouldBeEqualTo cancelable
+        fakeLocalEchoEventFactory.verifyCreateLiveLocationEvent(
+                roomId = params.roomId,
+                beaconInfoEventId = params.beaconInfoEventId,
+                latitude = params.latitude,
+                longitude = params.longitude,
+                uncertainty = params.uncertainty
+        )
+        fakeLocalEchoEventFactory.verifyCreateLocalEcho(event)
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultSendStaticLocationTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultSendStaticLocationTaskTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..cfde568b71ba15d09685f1aa3f9aeadba310c143
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultSendStaticLocationTaskTest.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.session.room.location
+
+import io.mockk.mockk
+import io.mockk.unmockkAll
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.After
+import org.junit.Test
+import org.matrix.android.sdk.api.util.Cancelable
+import org.matrix.android.sdk.test.fakes.FakeEventSenderProcessor
+import org.matrix.android.sdk.test.fakes.FakeLocalEchoEventFactory
+
+private const val A_ROOM_ID = "room_id"
+private const val A_LATITUDE = 1.4
+private const val A_LONGITUDE = 44.0
+private const val AN_UNCERTAINTY = 5.0
+
+@ExperimentalCoroutinesApi
+internal class DefaultSendStaticLocationTaskTest {
+
+    private val fakeLocalEchoEventFactory = FakeLocalEchoEventFactory()
+    private val fakeEventSenderProcessor = FakeEventSenderProcessor()
+
+    private val defaultSendStaticLocationTask = DefaultSendStaticLocationTask(
+            localEchoEventFactory = fakeLocalEchoEventFactory.instance,
+            eventSenderProcessor = fakeEventSenderProcessor
+    )
+
+    @After
+    fun tearDown() {
+        unmockkAll()
+    }
+
+    @Test
+    fun `given parameters when calling the task then it is correctly executed`() = runTest {
+        val params = SendStaticLocationTask.Params(
+                roomId = A_ROOM_ID,
+                latitude = A_LATITUDE,
+                longitude = A_LONGITUDE,
+                uncertainty = AN_UNCERTAINTY,
+                isUserLocation = true
+        )
+        val event = fakeLocalEchoEventFactory.givenCreateStaticLocationEvent(
+                withLocalEcho = true
+        )
+        val cancelable = mockk<Cancelable>()
+        fakeEventSenderProcessor.givenPostEventReturns(event, cancelable)
+
+        val result = defaultSendStaticLocationTask.execute(params)
+
+        result shouldBeEqualTo cancelable
+        fakeLocalEchoEventFactory.verifyCreateStaticLocationEvent(
+                roomId = params.roomId,
+                latitude = params.latitude,
+                longitude = params.longitude,
+                uncertainty = params.uncertainty,
+                isUserLocation = params.isUserLocation
+        )
+        fakeLocalEchoEventFactory.verifyCreateLocalEcho(event)
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..909ba5d048eb93e39e130a9f59c33dd9fb5c2c01
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt
@@ -0,0 +1,114 @@
+/*
+ * 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.session.room.location
+
+import io.mockk.unmockkAll
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.amshove.kluent.shouldBeEqualTo
+import org.amshove.kluent.shouldBeInstanceOf
+import org.junit.After
+import org.junit.Test
+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.room.location.UpdateLiveLocationShareResult
+import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
+import org.matrix.android.sdk.internal.session.room.state.SendStateTask
+import org.matrix.android.sdk.test.fakes.FakeClock
+import org.matrix.android.sdk.test.fakes.FakeSendStateTask
+
+private const val A_USER_ID = "user-id"
+private const val A_ROOM_ID = "room-id"
+private const val AN_EVENT_ID = "event-id"
+private const val A_TIMEOUT = 15_000L
+private const val AN_EPOCH = 1655210176L
+
+@ExperimentalCoroutinesApi
+internal class DefaultStartLiveLocationShareTaskTest {
+
+    private val fakeClock = FakeClock()
+    private val fakeSendStateTask = FakeSendStateTask()
+
+    private val defaultStartLiveLocationShareTask = DefaultStartLiveLocationShareTask(
+            userId = A_USER_ID,
+            clock = fakeClock,
+            sendStateTask = fakeSendStateTask
+    )
+
+    @After
+    fun tearDown() {
+        unmockkAll()
+    }
+
+    @Test
+    fun `given parameters and no error when calling the task then result is success`() = runTest {
+        val params = StartLiveLocationShareTask.Params(
+                roomId = A_ROOM_ID,
+                timeoutMillis = A_TIMEOUT
+        )
+        fakeClock.givenEpoch(AN_EPOCH)
+        fakeSendStateTask.givenExecuteRetryReturns(AN_EVENT_ID)
+
+        val result = defaultStartLiveLocationShareTask.execute(params)
+
+        result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
+        val expectedBeaconContent = MessageBeaconInfoContent(
+                timeout = params.timeoutMillis,
+                isLive = true,
+                unstableTimestampMillis = AN_EPOCH
+        ).toContent()
+        val expectedParams = SendStateTask.Params(
+                roomId = params.roomId,
+                stateKey = A_USER_ID,
+                eventType = EventType.STATE_ROOM_BEACON_INFO.first(),
+                body = expectedBeaconContent
+        )
+        fakeSendStateTask.verifyExecuteRetry(
+                params = expectedParams,
+                remainingRetry = 3
+        )
+    }
+
+    @Test
+    fun `given parameters and an empty returned event id when calling the task then result is failure`() = runTest {
+        val params = StartLiveLocationShareTask.Params(
+                roomId = A_ROOM_ID,
+                timeoutMillis = A_TIMEOUT
+        )
+        fakeClock.givenEpoch(AN_EPOCH)
+        fakeSendStateTask.givenExecuteRetryReturns("")
+
+        val result = defaultStartLiveLocationShareTask.execute(params)
+
+        result shouldBeInstanceOf UpdateLiveLocationShareResult.Failure::class
+    }
+
+    @Test
+    fun `given parameters and error during event sending when calling the task then result is failure`() = runTest {
+        val params = StartLiveLocationShareTask.Params(
+                roomId = A_ROOM_ID,
+                timeoutMillis = A_TIMEOUT
+        )
+        fakeClock.givenEpoch(AN_EPOCH)
+        val error = Throwable()
+        fakeSendStateTask.givenExecuteRetryThrows(error)
+
+        val result = defaultStartLiveLocationShareTask.execute(params)
+
+        result shouldBeEqualTo UpdateLiveLocationShareResult.Failure(error)
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..1abf179ccf3bacec5c676c73d8b765a0eab6819e
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt
@@ -0,0 +1,167 @@
+/*
+ * 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.session.room.location
+
+import io.mockk.unmockkAll
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.amshove.kluent.shouldBeEqualTo
+import org.amshove.kluent.shouldBeInstanceOf
+import org.junit.After
+import org.junit.Test
+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.room.location.UpdateLiveLocationShareResult
+import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
+import org.matrix.android.sdk.internal.session.room.state.SendStateTask
+import org.matrix.android.sdk.test.fakes.FakeGetActiveBeaconInfoForUserTask
+import org.matrix.android.sdk.test.fakes.FakeSendStateTask
+
+private const val A_USER_ID = "user-id"
+private const val A_ROOM_ID = "room-id"
+private const val AN_EVENT_ID = "event-id"
+private const val A_TIMEOUT = 15_000L
+private const val AN_EPOCH = 1655210176L
+
+@ExperimentalCoroutinesApi
+class DefaultStopLiveLocationShareTaskTest {
+
+    private val fakeSendStateTask = FakeSendStateTask()
+    private val fakeGetActiveBeaconInfoForUserTask = FakeGetActiveBeaconInfoForUserTask()
+
+    private val defaultStopLiveLocationShareTask = DefaultStopLiveLocationShareTask(
+            sendStateTask = fakeSendStateTask,
+            getActiveBeaconInfoForUserTask = fakeGetActiveBeaconInfoForUserTask
+    )
+
+    @After
+    fun tearDown() {
+        unmockkAll()
+    }
+
+    @Test
+    fun `given parameters and no error when calling the task then result is success`() = runTest {
+        val params = StopLiveLocationShareTask.Params(roomId = A_ROOM_ID)
+        val currentStateEvent = Event(
+                stateKey = A_USER_ID,
+                content = MessageBeaconInfoContent(
+                        timeout = A_TIMEOUT,
+                        isLive = true,
+                        unstableTimestampMillis = AN_EPOCH
+                ).toContent()
+        )
+        fakeGetActiveBeaconInfoForUserTask.givenExecuteReturns(currentStateEvent)
+        fakeSendStateTask.givenExecuteRetryReturns(AN_EVENT_ID)
+
+        val result = defaultStopLiveLocationShareTask.execute(params)
+
+        result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
+        val expectedBeaconContent = MessageBeaconInfoContent(
+                timeout = A_TIMEOUT,
+                isLive = false,
+                unstableTimestampMillis = AN_EPOCH
+        ).toContent()
+        val expectedSendParams = SendStateTask.Params(
+                roomId = params.roomId,
+                stateKey = A_USER_ID,
+                eventType = EventType.STATE_ROOM_BEACON_INFO.first(),
+                body = expectedBeaconContent
+        )
+        fakeSendStateTask.verifyExecuteRetry(
+                params = expectedSendParams,
+                remainingRetry = 3
+        )
+        val expectedGetBeaconParams = GetActiveBeaconInfoForUserTask.Params(
+                roomId = params.roomId
+        )
+        fakeGetActiveBeaconInfoForUserTask.verifyExecute(
+                expectedGetBeaconParams
+        )
+    }
+
+    @Test
+    fun `given parameters and an incorrect current state event when calling the task then result is failure`() = runTest {
+        val incorrectCurrentStateEvents = listOf(
+                // no event
+                null,
+                // no stateKey
+                Event(
+                        stateKey = null,
+                        content = MessageBeaconInfoContent(
+                                timeout = A_TIMEOUT,
+                                isLive = true,
+                                unstableTimestampMillis = AN_EPOCH
+                        ).toContent()
+                ),
+                // null content
+                Event(
+                        stateKey = A_USER_ID,
+                        content = null
+                )
+        )
+
+        incorrectCurrentStateEvents.forEach { currentStateEvent ->
+            fakeGetActiveBeaconInfoForUserTask.givenExecuteReturns(currentStateEvent)
+            fakeSendStateTask.givenExecuteRetryReturns(AN_EVENT_ID)
+            val params = StopLiveLocationShareTask.Params(roomId = A_ROOM_ID)
+
+            val result = defaultStopLiveLocationShareTask.execute(params)
+
+            result shouldBeInstanceOf UpdateLiveLocationShareResult.Failure::class
+        }
+    }
+
+    @Test
+    fun `given parameters and an empty returned event id when calling the task then result is failure`() = runTest {
+        val params = StopLiveLocationShareTask.Params(roomId = A_ROOM_ID)
+        val currentStateEvent = Event(
+                stateKey = A_USER_ID,
+                content = MessageBeaconInfoContent(
+                        timeout = A_TIMEOUT,
+                        isLive = true,
+                        unstableTimestampMillis = AN_EPOCH
+                ).toContent()
+        )
+        fakeGetActiveBeaconInfoForUserTask.givenExecuteReturns(currentStateEvent)
+        fakeSendStateTask.givenExecuteRetryReturns("")
+
+        val result = defaultStopLiveLocationShareTask.execute(params)
+
+        result shouldBeInstanceOf UpdateLiveLocationShareResult.Failure::class
+    }
+
+    @Test
+    fun `given parameters and error during event sending when calling the task then result is failure`() = runTest {
+        val params = StopLiveLocationShareTask.Params(roomId = A_ROOM_ID)
+        val currentStateEvent = Event(
+                stateKey = A_USER_ID,
+                content = MessageBeaconInfoContent(
+                        timeout = A_TIMEOUT,
+                        isLive = true,
+                        unstableTimestampMillis = AN_EPOCH
+                ).toContent()
+        )
+        fakeGetActiveBeaconInfoForUserTask.givenExecuteReturns(currentStateEvent)
+        val error = Throwable()
+        fakeSendStateTask.givenExecuteRetryThrows(error)
+
+        val result = defaultStopLiveLocationShareTask.execute(params)
+
+        result shouldBeEqualTo UpdateLiveLocationShareResult.Failure(error)
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeClock.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeClock.kt
new file mode 100644
index 0000000000000000000000000000000000000000..febf94f4cf59c31fddee5b8b23386acd051dbee6
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeClock.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.test.fakes
+
+import io.mockk.every
+import io.mockk.mockk
+import org.matrix.android.sdk.internal.util.time.Clock
+
+internal class FakeClock : Clock by mockk() {
+    fun givenEpoch(epoch: Long) {
+        every { epochMillis() } returns epoch
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventSenderProcessor.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventSenderProcessor.kt
new file mode 100644
index 0000000000000000000000000000000000000000..fbdcf5bfd7cf6f0bf6ba97d0e726afb35c0af6b1
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventSenderProcessor.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.test.fakes
+
+import io.mockk.every
+import io.mockk.mockk
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.api.util.Cancelable
+import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
+
+internal class FakeEventSenderProcessor : EventSenderProcessor by mockk() {
+
+    fun givenPostEventReturns(event: Event, cancelable: Cancelable) {
+        every { postEvent(event) } returns cancelable
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeGetActiveBeaconInfoForUserTask.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeGetActiveBeaconInfoForUserTask.kt
new file mode 100644
index 0000000000000000000000000000000000000000..dc4a48908a6e3e1de8732c9a105eab5d6eef4e30
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeGetActiveBeaconInfoForUserTask.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.test.fakes
+
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.mockk
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.internal.session.room.location.GetActiveBeaconInfoForUserTask
+
+internal class FakeGetActiveBeaconInfoForUserTask : GetActiveBeaconInfoForUserTask by mockk() {
+
+    fun givenExecuteReturns(event: Event?) {
+        coEvery { execute(any()) } returns event
+    }
+
+    fun verifyExecute(params: GetActiveBeaconInfoForUserTask.Params) {
+        coVerify { execute(params) }
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeLocalEchoEventFactory.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeLocalEchoEventFactory.kt
new file mode 100644
index 0000000000000000000000000000000000000000..50ec85f14a2e186c3033c3f3f508111157ef5c60
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeLocalEchoEventFactory.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.test.fakes
+
+import io.mockk.every
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.runs
+import io.mockk.verify
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
+
+internal class FakeLocalEchoEventFactory {
+
+    val instance = mockk<LocalEchoEventFactory>()
+
+    fun givenCreateStaticLocationEvent(withLocalEcho: Boolean): Event {
+        val event = Event()
+        every {
+            instance.createStaticLocationEvent(
+                    roomId = any(),
+                    latitude = any(),
+                    longitude = any(),
+                    uncertainty = any(),
+                    isUserLocation = any()
+            )
+        } returns event
+
+        if (withLocalEcho) {
+            every { instance.createLocalEcho(event) } just runs
+        }
+        return event
+    }
+
+    fun givenCreateLiveLocationEvent(withLocalEcho: Boolean): Event {
+        val event = Event()
+        every {
+            instance.createLiveLocationEvent(
+                    beaconInfoEventId = any(),
+                    roomId = any(),
+                    latitude = any(),
+                    longitude = any(),
+                    uncertainty = any()
+            )
+        } returns event
+
+        if (withLocalEcho) {
+            every { instance.createLocalEcho(event) } just runs
+        }
+        return event
+    }
+
+    fun verifyCreateStaticLocationEvent(
+            roomId: String,
+            latitude: Double,
+            longitude: Double,
+            uncertainty: Double?,
+            isUserLocation: Boolean
+    ) {
+        verify {
+            instance.createStaticLocationEvent(
+                    roomId = roomId,
+                    latitude = latitude,
+                    longitude = longitude,
+                    uncertainty = uncertainty,
+                    isUserLocation = isUserLocation
+            )
+        }
+    }
+
+    fun verifyCreateLiveLocationEvent(
+            roomId: String,
+            beaconInfoEventId: String,
+            latitude: Double,
+            longitude: Double,
+            uncertainty: Double?
+    ) {
+        verify {
+            instance.createLiveLocationEvent(
+                    roomId = roomId,
+                    beaconInfoEventId = beaconInfoEventId,
+                    latitude = latitude,
+                    longitude = longitude,
+                    uncertainty = uncertainty
+            )
+        }
+    }
+
+    fun verifyCreateLocalEcho(event: Event) {
+        verify { instance.createLocalEcho(event) }
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt
index 0a22ef899664023c842b35034aee27c5c233acb5..d77084fe3b0fdb33e81f5ffc9e5482dacc2b3565 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt
@@ -16,40 +16,65 @@
 
 package org.matrix.android.sdk.test.fakes
 
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
 import com.zhuinden.monarchy.Monarchy
 import io.mockk.MockKVerificationScope
 import io.mockk.coEvery
 import io.mockk.every
 import io.mockk.mockk
 import io.mockk.mockkStatic
-import io.mockk.verify
+import io.mockk.slot
 import io.realm.Realm
 import io.realm.RealmModel
 import io.realm.RealmQuery
-import io.realm.kotlin.where
 import org.matrix.android.sdk.internal.util.awaitTransaction
 
 internal class FakeMonarchy {
 
     val instance = mockk<Monarchy>()
-    private val realm = mockk<Realm>(relaxed = true)
+    private val fakeRealm = FakeRealm()
 
     init {
         mockkStatic("org.matrix.android.sdk.internal.util.MonarchyKt")
         coEvery {
             instance.awaitTransaction(any<suspend (Realm) -> Any>())
         } coAnswers {
-            secondArg<suspend (Realm) -> Any>().invoke(realm)
+            secondArg<suspend (Realm) -> Any>().invoke(fakeRealm.instance)
         }
     }
 
-    inline fun <reified T : RealmModel> givenWhereReturns(result: T?) {
-        val queryResult = mockk<RealmQuery<T>>(relaxed = true)
-        every { queryResult.findFirst() } returns result
-        every { realm.where<T>() } returns queryResult
+    inline fun <reified T : RealmModel> givenWhere(): RealmQuery<T> {
+        return fakeRealm.givenWhere()
+    }
+
+    inline fun <reified T : RealmModel> givenWhereReturns(result: T?): RealmQuery<T> {
+        return fakeRealm.givenWhere<T>()
+                .givenFindFirst(result)
     }
 
     inline fun <reified T : RealmModel> verifyInsertOrUpdate(crossinline verification: MockKVerificationScope.() -> T) {
-        verify { realm.insertOrUpdate(verification()) }
+        fakeRealm.verifyInsertOrUpdate(verification)
+    }
+
+    inline fun <reified R, reified T : RealmModel> givenFindAllMappedWithChangesReturns(
+            realmEntities: List<T>,
+            mappedResult: List<R>,
+            mapper: Monarchy.Mapper<R, T>
+    ): LiveData<List<R>> {
+        every { mapper.map(any()) } returns mockk()
+        val monarchyQuery = slot<Monarchy.Query<T>>()
+        val monarchyMapper = slot<Monarchy.Mapper<R, T>>()
+        val result = MutableLiveData(mappedResult)
+        every {
+            instance.findAllMappedWithChanges(capture(monarchyQuery), capture(monarchyMapper))
+        } answers {
+            monarchyQuery.captured.createQuery(fakeRealm.instance)
+            realmEntities.forEach {
+                monarchyMapper.captured.map(it)
+            }
+            result
+        }
+        return result
     }
 }
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt
index c07f8e187363e934a79ca52e16ae224f1ecff85f..0ebff8727889dd42832674f3e5ab2bbacfa39843 100644
--- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt
@@ -16,21 +16,84 @@
 
 package org.matrix.android.sdk.test.fakes
 
+import io.mockk.MockKVerificationScope
 import io.mockk.every
 import io.mockk.mockk
+import io.mockk.verify
 import io.realm.Realm
 import io.realm.RealmModel
 import io.realm.RealmQuery
+import io.realm.RealmResults
 import io.realm.kotlin.where
 
 internal class FakeRealm {
 
     val instance = mockk<Realm>(relaxed = true)
 
-    inline fun <reified T : RealmModel> givenWhereReturns(result: T?): RealmQuery<T> {
-        val queryResult = mockk<RealmQuery<T>>()
-        every { queryResult.findFirst() } returns result
-        every { instance.where<T>() } returns queryResult
-        return queryResult
+    inline fun <reified T : RealmModel> givenWhere(): RealmQuery<T> {
+        val query = mockk<RealmQuery<T>>()
+        every { instance.where<T>() } returns query
+        return query
     }
+
+    inline fun <reified T : RealmModel> verifyInsertOrUpdate(crossinline verification: MockKVerificationScope.() -> T) {
+        verify { instance.insertOrUpdate(verification()) }
+    }
+}
+
+inline fun <reified T : RealmModel> RealmQuery<T>.givenFindFirst(
+        result: T?
+): RealmQuery<T> {
+    every { findFirst() } returns result
+    return this
+}
+
+inline fun <reified T : RealmModel> RealmQuery<T>.givenFindAll(
+        result: List<T>
+): RealmQuery<T> {
+    val realmResults = mockk<RealmResults<T>>()
+    result.forEachIndexed { index, t ->
+        every { realmResults[index] } returns t
+    }
+    every { realmResults.size } returns result.size
+    every { findAll() } returns realmResults
+    return this
+}
+
+inline fun <reified T : RealmModel> RealmQuery<T>.givenEqualTo(
+        fieldName: String,
+        value: String
+): RealmQuery<T> {
+    every { equalTo(fieldName, value) } returns this
+    return this
+}
+
+inline fun <reified T : RealmModel> RealmQuery<T>.givenEqualTo(
+        fieldName: String,
+        value: Boolean
+): RealmQuery<T> {
+    every { equalTo(fieldName, value) } returns this
+    return this
+}
+
+inline fun <reified T : RealmModel> RealmQuery<T>.givenNotEqualTo(
+        fieldName: String,
+        value: String
+): RealmQuery<T> {
+    every { notEqualTo(fieldName, value) } returns this
+    return this
+}
+
+inline fun <reified T : RealmModel> RealmQuery<T>.givenIsNotEmpty(
+        fieldName: String
+): RealmQuery<T> {
+    every { isNotEmpty(fieldName) } returns this
+    return this
+}
+
+inline fun <reified T : RealmModel> RealmQuery<T>.givenIsNotNull(
+        fieldName: String
+): RealmQuery<T> {
+    every { isNotNull(fieldName) } returns this
+    return this
 }
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSendStateTask.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSendStateTask.kt
new file mode 100644
index 0000000000000000000000000000000000000000..08a25be93ed372a6e4f63d161fe43d79e53bf9e1
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSendStateTask.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.test.fakes
+
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.mockk
+import org.matrix.android.sdk.internal.session.room.state.SendStateTask
+
+internal class FakeSendStateTask : SendStateTask by mockk() {
+
+    fun givenExecuteRetryReturns(eventId: String) {
+        coEvery { executeRetry(any(), any()) } returns eventId
+    }
+
+    fun givenExecuteRetryThrows(error: Throwable) {
+        coEvery { executeRetry(any(), any()) } throws error
+    }
+
+    fun verifyExecuteRetry(params: SendStateTask.Params, remainingRetry: Int) {
+        coVerify { executeRetry(params, remainingRetry) }
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeStateEventDataSource.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeStateEventDataSource.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ca03316fa728a0ae34652496b106c1b3c7c0b9ba
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeStateEventDataSource.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.test.fakes
+
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import org.matrix.android.sdk.api.query.QueryStringValue
+import org.matrix.android.sdk.api.session.events.model.Event
+import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
+
+internal class FakeStateEventDataSource {
+
+    val instance: StateEventDataSource = mockk()
+
+    fun givenGetStateEventReturns(event: Event?) {
+        every {
+            instance.getStateEvent(
+                    roomId = any(),
+                    eventType = any(),
+                    stateKey = any()
+            )
+        } returns event
+    }
+
+    fun verifyGetStateEvent(roomId: String, eventType: String, stateKey: String) {
+        verify {
+            instance.getStateEvent(
+                    roomId = roomId,
+                    eventType = eventType,
+                    stateKey = QueryStringValue.Equals(stateKey)
+            )
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManager.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManager.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b29d015a431b2c4661be5fe735eb8189f7f9966a
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManager.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.test.fakes
+
+import androidx.work.ExistingWorkPolicy
+import androidx.work.OneTimeWorkRequest
+import androidx.work.WorkManager
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+
+class FakeWorkManager {
+
+    val instance = mockk<WorkManager>()
+
+    fun expectEnqueueUniqueWork() {
+        every { instance.enqueueUniqueWork(any(), any(), any<OneTimeWorkRequest>()) } returns mockk()
+    }
+
+    fun verifyEnqueueUniqueWork(workName: String, policy: ExistingWorkPolicy) {
+        verify { instance.enqueueUniqueWork(workName, policy, any<OneTimeWorkRequest>()) }
+    }
+
+    fun expectCancelUniqueWork() {
+        every { instance.cancelUniqueWork(any()) } returns mockk()
+    }
+
+    fun verifyCancelUniqueWork(workName: String) {
+        verify { instance.cancelUniqueWork(workName) }
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManagerProvider.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManagerProvider.kt
new file mode 100644
index 0000000000000000000000000000000000000000..51ff24c01d24efab7c53a24ca95b37a749b83d13
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManagerProvider.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.test.fakes
+
+import io.mockk.every
+import io.mockk.mockk
+import org.matrix.android.sdk.internal.di.WorkManagerProvider
+
+internal class FakeWorkManagerProvider(
+        val fakeWorkManager: FakeWorkManager = FakeWorkManager(),
+) {
+
+    val instance = mockk<WorkManagerProvider>().also {
+        every { it.workManager } returns fakeWorkManager.instance
+    }
+}
diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/util/DefaultBuildVersionSdkIntProviderTests.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/util/DefaultBuildVersionSdkIntProviderTests.kt
new file mode 100644
index 0000000000000000000000000000000000000000..c118cf07a16e9690bafd7847da6c610939d52c6a
--- /dev/null
+++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/util/DefaultBuildVersionSdkIntProviderTests.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.util
+
+import android.os.Build
+import org.amshove.kluent.shouldBeEqualTo
+import org.junit.Test
+import org.matrix.android.sdk.api.util.DefaultBuildVersionSdkIntProvider
+
+class DefaultBuildVersionSdkIntProviderTests {
+
+    @Test
+    fun getReturnsCurrentVersionFromBuild_Version_SDK_INT() {
+        val provider = DefaultBuildVersionSdkIntProvider()
+        provider.get() shouldBeEqualTo Build.VERSION.SDK_INT
+    }
+}