From 6318bf41f6abea95f4c92b2753a3d4421bfa6fb1 Mon Sep 17 00:00:00 2001
From: Benoit Marty <benoit@matrix.org>
Date: Wed, 20 Jul 2022 10:12:24 +0200
Subject: [PATCH] Import v1.4.27-RC2 from Element Android

---
 dependencies.gradle                           |  25 +-
 dependencies_groups.gradle                    |   5 +
 matrix-sdk-android/build.gradle               |  10 +-
 .../TestBuildVersionSdkIntProvider.kt         |   6 +-
 .../securestorage/SecretStoringUtilsTest.kt   |  96 ++++-
 .../android/sdk/common/TestMatrixComponent.kt |   8 +-
 .../sdk/session/space/SpaceCreationTest.kt    |  29 +-
 .../sdk/session/space/SpaceHierarchyTest.kt   |   7 +-
 .../java/org/matrix/android/sdk/api/Matrix.kt |  22 +-
 .../android/sdk/api/MatrixConfiguration.kt    |   5 +
 .../sdk/api/auth/data/LoginFlowResult.kt      |   3 +-
 .../android/sdk/api/auth/login/LoginWizard.kt |   6 +-
 .../android/sdk/api/debug/DebugService.kt     |  34 ++
 .../android/sdk/api/query/QueryStringValue.kt |  15 +-
 .../securestorage/SecretStoringUtils.kt       |  97 +++--
 .../api/securestorage/SecureStorageModule.kt  |  45 ++
 .../securestorage/SecureStorageService.kt     |   2 +-
 .../matrix/android/sdk/api/session/Session.kt |  14 +-
 .../sdk/api/session/account/AccountService.kt |   8 +-
 .../sdk/api/session/crypto/CryptoService.kt   |   2 -
 .../homeserver/HomeServerCapabilities.kt      |   7 +-
 .../sdk/api/session/room/RoomExtensions.kt    |   7 +-
 .../room/location/LocationSharingService.kt   |  46 ++
 .../location/UpdateLiveLocationShareResult.kt |  25 ++
 .../room/model/message/LocationInfo.kt        |   2 +-
 .../model/message/MessageLocationContent.kt   |   2 +-
 .../room/model/message/PollCreationInfo.kt    |   5 +-
 .../sdk/api/session/room/send/SendService.kt  |  18 -
 .../api/session/room/state/StateService.kt    |  23 +-
 .../room/state/StateServiceExtension.kt       |   2 +-
 .../sdk/api/session/widgets/WidgetService.kt  |   6 +-
 .../util}/BuildVersionSdkIntProvider.kt       |   4 +-
 .../DefaultBuildVersionSdkIntProvider.kt      |   4 +-
 .../android/sdk/internal/SessionManager.kt    |   7 +
 .../auth/DefaultAuthenticationService.kt      |   9 +-
 .../internal/auth/login/DefaultLoginWizard.kt |   5 +-
 .../auth/login/ResetPasswordMailConfirmed.kt  |  10 +-
 .../auth/version/HomeServerVersion.kt         |   1 +
 .../sdk/internal/auth/version/Versions.kt     |   9 +
 .../internal/crypto/DefaultCryptoService.kt   |   4 -
 .../internal/crypto/store/IMXCryptoStore.kt   |   1 -
 .../crypto/store/db/RealmCryptoStore.kt       |   8 -
 .../VerificationMessageProcessor.kt           |   2 +-
 .../sdk/internal/database/RealmKeysUtils.kt   |   8 +-
 .../database/RealmSessionStoreMigration.kt    |   4 +-
 .../database/helper/ThreadSummaryHelper.kt    |   2 +-
 .../internal/database/mapper/EventMapper.kt   |   2 +-
 .../mapper/HomeServerCapabilitiesMapper.kt    |   3 +-
 ...iveLocationShareAggregatedSummaryMapper.kt |   6 +-
 .../database/migration/MigrateSessionTo029.kt |   2 +-
 .../database/migration/MigrateSessionTo030.kt |   6 +-
 .../database/migration/MigrateSessionTo031.kt |  31 ++
 .../model/HomeServerCapabilitiesEntity.kt     |   3 +-
 ...cationShareAggregatedSummaryEntityQuery.kt |   3 +-
 .../android/sdk/internal/debug/DebugModule.kt |  29 ++
 .../sdk/internal/debug/DefaultDebugService.kt |  44 ++
 .../sdk/internal/di/MatrixComponent.kt        |   9 +-
 .../android/sdk/internal/di/NetworkModule.kt  |   5 +-
 .../network/httpclient/OkHttpClientUtil.kt    |  15 +
 .../DefaultSecureStorageService.kt            |   5 +-
 .../sdk/internal/session/DefaultSession.kt    |  21 +-
 .../sdk/internal/session/SessionComponent.kt  |   4 +-
 .../sdk/internal/session/SessionListeners.kt  |   3 +-
 .../sdk/internal/session/SessionModule.kt     |  24 +-
 .../session/account/ChangePasswordParams.kt   |  10 +-
 .../session/account/ChangePasswordTask.kt     |   5 +-
 .../session/account/DefaultAccountService.kt  |   4 +-
 .../GetHomeServerCapabilitiesTask.kt          |   2 +
 .../session/identity/IdentityModule.kt        |   6 +-
 .../session/permalinks/ViaParameterFinder.kt  |   2 +-
 .../pushers/DefaultConditionResolver.kt       |   3 +-
 .../EventRelationsAggregationProcessor.kt     |   2 +-
 .../sdk/internal/session/room/RoomModule.kt   |  30 ++
 .../LiveLocationAggregationProcessor.kt       |  31 +-
 .../location/CheckIfExistingActiveLiveTask.kt |  45 ++
 .../location/DefaultLocationSharingService.kt |  76 +++-
 .../GetActiveBeaconInfoForUserTask.kt         |  54 +++
 .../room/location/SendLiveLocationTask.kt     |  51 +++
 .../room/location/SendStaticLocationTask.kt   |  51 +++
 .../location/StartLiveLocationShareTask.kt    |  66 +++
 .../location/StopLiveLocationShareTask.kt     |  72 ++++
 .../membership/DefaultMembershipService.kt    |   2 +-
 .../room/membership/LoadRoomMembersTask.kt    |  46 +-
 .../room/membership/RoomMemberEventHandler.kt |  32 +-
 .../room/membership/leaving/LeaveRoomTask.kt  |   2 +-
 .../threads/FetchThreadTimelineTask.kt        |   3 +-
 .../session/room/send/DefaultSendService.kt   |  12 -
 .../room/send/LocalEchoEventFactory.kt        |   6 +-
 .../session/room/state/DefaultStateService.kt |  46 +-
 .../room/state/StateEventDataSource.kt        |  14 +-
 .../session/room/timeline/DefaultTimeline.kt  |  24 +-
 .../session/room/timeline/GetEventTask.kt     |   2 +-
 .../room/timeline/LiveRoomStateListener.kt    |   2 +-
 .../room/timeline/LoadTimelineStrategy.kt     |  11 +-
 .../session/room/timeline/TimelineChunk.kt    |  53 +--
 .../room/timeline/TokenChunkEventPersistor.kt |   4 +-
 .../room/version/DefaultRoomVersionService.kt |   2 +-
 .../session/space/DefaultSpaceService.kt      |   2 +-
 .../sync/handler/room/RoomSyncHandler.kt      |  14 +-
 .../handler/room/ThreadsAwarenessHandler.kt   |   6 +-
 .../session/widgets/DefaultWidgetService.kt   |   6 +-
 .../internal/session/widgets/WidgetManager.kt |   7 +-
 .../util/BackgroundDetectionObserver.kt       |   3 +-
 .../sdk/internal/util/system/SystemModule.kt  |   4 +-
 .../android/sdk/api/MatrixPatternsTest.kt     |  40 ++
 .../pushers/DefaultAddPusherTaskTest.kt       |   6 +
 .../LiveLocationAggregationProcessorTest.kt   | 405 ++++++++++++++++++
 .../poll/PollAggregationProcessorTest.kt      |  15 +-
 ...efaultCheckIfExistingActiveLiveTaskTest.kt | 105 +++++
 ...faultGetActiveBeaconInfoForUserTaskTest.kt |  75 ++++
 .../DefaultLocationSharingServiceTest.kt      | 268 ++++++++++++
 .../DefaultSendLiveLocationTaskTest.kt        |  79 ++++
 .../DefaultSendStaticLocationTaskTest.kt      |  78 ++++
 .../DefaultStartLiveLocationShareTaskTest.kt  | 114 +++++
 .../DefaultStopLiveLocationShareTaskTest.kt   | 167 ++++++++
 .../android/sdk/test/fakes/FakeClock.kt       |  27 ++
 .../test/fakes/FakeEventSenderProcessor.kt    |  30 ++
 .../FakeGetActiveBeaconInfoForUserTask.kt     |  34 ++
 .../test/fakes/FakeLocalEchoEventFactory.kt   | 106 +++++
 .../android/sdk/test/fakes/FakeMonarchy.kt    |  43 +-
 .../android/sdk/test/fakes/FakeRealm.kt       |  73 +++-
 .../sdk/test/fakes/FakeSendStateTask.kt       |  37 ++
 .../test/fakes/FakeStateEventDataSource.kt    |  49 +++
 .../android/sdk/test/fakes/FakeWorkManager.kt |  45 ++
 .../sdk/test/fakes/FakeWorkManagerProvider.kt |  30 ++
 .../DefaultBuildVersionSdkIntProviderTests.kt |  31 ++
 126 files changed, 3124 insertions(+), 421 deletions(-)
 rename matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/{internal/session/securestorage => }/TestBuildVersionSdkIntProvider.kt (78%)
 rename matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/{internal/session => api}/securestorage/SecretStoringUtilsTest.kt (62%)
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/debug/DebugService.kt
 rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/{internal/session => api}/securestorage/SecretStoringUtils.kt (82%)
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/securestorage/SecureStorageModule.kt
 rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/{session => }/securestorage/SecureStorageService.kt (93%)
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/UpdateLiveLocationShareResult.kt
 rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/{internal/util/system => api/util}/BuildVersionSdkIntProvider.kt (87%)
 rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/{internal/util/system => api/util}/DefaultBuildVersionSdkIntProvider.kt (85%)
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo031.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/debug/DebugModule.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/debug/DefaultDebugService.kt
 rename matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/{session => }/securestorage/DefaultSecureStorageService.kt (86%)
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/CheckIfExistingActiveLiveTask.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/GetActiveBeaconInfoForUserTask.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/SendLiveLocationTask.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/SendStaticLocationTask.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StartLiveLocationShareTask.kt
 create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt
 create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/MatrixPatternsTest.kt
 create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt
 create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultCheckIfExistingActiveLiveTaskTest.kt
 create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt
 create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt
 create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultSendLiveLocationTaskTest.kt
 create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultSendStaticLocationTaskTest.kt
 create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStartLiveLocationShareTaskTest.kt
 create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt
 create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeClock.kt
 create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventSenderProcessor.kt
 create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeGetActiveBeaconInfoForUserTask.kt
 create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeLocalEchoEventFactory.kt
 create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeSendStateTask.kt
 create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeStateEventDataSource.kt
 create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManager.kt
 create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeWorkManagerProvider.kt
 create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/util/DefaultBuildVersionSdkIntProviderTests.kt

diff --git a/dependencies.gradle b/dependencies.gradle
index 451ad444..db9278b9 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 59cefe7e..b785c7f5 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 f6a7a294..a7cb1cad 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 b08c88fb..d0d64491 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 6bcd1274..14f98524 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 daf6b733..6cf01d4a 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 0cc0ef57..38136ff5 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 53962514..63ca9634 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 97920170..953ebddc 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 21106fba..893e90fb 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 7d1407c0..5b6c1897 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 5b8d2328..145cdbdc 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 00000000..d0cee088
--- /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 f08c8688..d3f6ec22 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 8b35bd17..bd2a1078 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 00000000..37a40fd6
--- /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 6b75c94c..e217611d 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 b3a62909..1b01239d 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 e3d52adf..094c66f6 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 9cc87b6f..638da118 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 c78fb9cf..b5d6d891 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 0e631427..b30c6055 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 dd48d51f..ada3dc85 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 00000000..6f8c03be
--- /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 a1fd3bd2..e0a78461 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 0a66a6e4..30420fd3 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 81b034a8..ee31d595 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 661c3be5..9cf06235 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 c79171f1..6ca63c2c 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 9e45fc12..6a9506fd 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 8ad6500d..c2094f46 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 51565604..b7ea187e 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 806c6e97..7f0024ca 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 bd2dac9e..5f5bb1f9 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 61a42366..9d6b018a 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 20b056f1..656a4f67 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 4e0c000f..01481f70 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 cd38b68a..75639c6a 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 cee4b121..915b2513 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 719f3665..e0bcde22 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 b18de343..b5b8d8e9 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 c56e4d32..028d8f73 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 9f123f0c..821663bc 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 b3a039d1..86355cea 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 6a8589bc..665567bf 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 79a99cdf..0a6d4bf8 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 5b60c536..0f0a847c 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 20af4353..184a0108 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 9460e4c6..4a4c730a 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 aebca11c..17dc0f7c 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 b9c611f5..5d24b143 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 00000000..e278b747
--- /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 47a83f0e..9d90973f 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 7dfeb688..d69f251f 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 00000000..f392d39d
--- /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 00000000..3f2e6faf
--- /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 09591664..44ec90ed 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 862cf463..b5b46a3f 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 3920c3b5..1c395c2d 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 ef8133dd..8f6605d6 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 32269c9a..7c50a0ff 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 f01451b6..d3cae3ac 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 756b9cef..0dae24e6 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 2c2317de..f8a52f0b 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 1b958209..f6778327 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 7b21ba2e..e767950f 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 bb830a5e..9d03ec47 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 d22da8f6..add69dd8 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 464ae96e..33d81648 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 edc45fe9..5fb20bb2 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 ace5ee0b..c2310f4f 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 bb43d903..24d4975e 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 f3845f1f..c4d37d12 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 05bde8f8..92174912 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 00000000..228a046f
--- /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 8cf6fcdf..20320cad 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 00000000..a8d955af
--- /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 00000000..bebd9c77
--- /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 00000000..e08b82f3
--- /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 00000000..b943c279
--- /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 00000000..da5fd769
--- /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 005d7f26..ef89ca33 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 15d08892..7052eb23 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 1e36e9c6..fd655252 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 1b836e36..dd28bbcc 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 bad73417..bac810f4 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 fc78abcf..418000ab 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 3b9ca44d..f52500de 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 2a980f32..ad47b824 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 18c709ad..9971ce3c 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 7795a56c..4eaac67e 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 aef9e24c..7c662444 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 b2692bf8..b78ad459 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 c5d4d346..d81a1156 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 e13f3f45..7fa36969 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 fd1703db..ea22f8cd 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 dc12c320..0bde3a11 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 c08d9389..d2f1b320 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 f99fe964..30e1ec66 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 8c7557a5..70553359 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 7b2edf2d..9c59c113 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 3f7db93b..37a329af 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 2dd16d83..901d0eca 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 396d12f3..8c7d7704 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 00000000..0d0450ad
--- /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 32b1d44f..dac33069 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 00000000..933087af
--- /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 837bbeea..3044ca5d 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 00000000..3198392e
--- /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 00000000..588bfaa9
--- /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 00000000..de912065
--- /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 00000000..423c6800
--- /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 00000000..cfde568b
--- /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 00000000..909ba5d0
--- /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 00000000..1abf179c
--- /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 00000000..febf94f4
--- /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 00000000..fbdcf5bf
--- /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 00000000..dc4a4890
--- /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 00000000..50ec85f1
--- /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 0a22ef89..d77084fe 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 c07f8e18..0ebff872 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 00000000..08a25be9
--- /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 00000000..ca03316f
--- /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 00000000..b29d015a
--- /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 00000000..51ff24c0
--- /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 00000000..c118cf07
--- /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
+    }
+}
-- 
GitLab