diff --git a/dependencies.gradle b/dependencies.gradle
index 47090d4732fc1f10b10c9235a1865938ab3c90c6..8cc7b3b26085b9df53a71bfc59d0ffcd63ecd6b0 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -1,8 +1,8 @@
 ext.versions = [
 
         'minSdk'            : 21,
-        'compileSdk'        : 30,
-        'targetSdk'         : 30,
+        'compileSdk'        : 31,
+        'targetSdk'         : 31,
         'sourceCompat'      : JavaVersion.VERSION_11,
         'targetCompat'      : JavaVersion.VERSION_11,
 ]
@@ -11,13 +11,13 @@ def gradle = "7.0.3"
 // Ref: https://kotlinlang.org/releases.html
 def kotlin = "1.5.31"
 def kotlinCoroutines = "1.5.2"
-def dagger = "2.40"
+def dagger = "2.40.1"
 def retrofit = "2.9.0"
 def arrow = "0.8.2"
 def markwon = "4.6.2"
 def moshi = "1.12.0"
-def lifecycle = "2.2.0"
-def rxBinding = "3.1.0"
+def lifecycle = "2.4.0"
+def flowBinding = "1.2.0"
 def epoxy = "4.6.2"
 def mavericks = "2.4.0"
 def glide = "4.12.0"
@@ -26,7 +26,7 @@ def jjwt = "0.11.2"
 def vanniktechEmoji = "0.8.0"
 
 // Testing
-def mockk = "1.12.0"
+def mockk = "1.12.1"
 def espresso = "3.4.0"
 def androidxTest = "1.4.0"
 
@@ -41,22 +41,23 @@ ext.libs = [
         jetbrains   : [
                 'coroutinesCore'          : "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinCoroutines",
                 'coroutinesAndroid'       : "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinCoroutines",
-                'coroutinesRx2'           : "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$kotlinCoroutines"
+                'coroutinesRx2'           : "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$kotlinCoroutines",
+                'coroutinesTest'          : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines"
         ],
         androidx    : [
                 'appCompat'               : "androidx.appcompat:appcompat:1.3.1",
-                'core'                    : "androidx.core:core-ktx:1.6.0",
+                'core'                    : "androidx.core:core-ktx:1.7.0",
                 'recyclerview'            : "androidx.recyclerview:recyclerview:1.2.1",
                 'exifinterface'           : "androidx.exifinterface:exifinterface:1.3.3",
                 'fragmentKtx'             : "androidx.fragment:fragment-ktx:1.3.6",
                 'constraintLayout'        : "androidx.constraintlayout:constraintlayout:2.1.1",
-                'work'                    : "androidx.work:work-runtime-ktx:2.6.0",
+                'work'                    : "androidx.work:work-runtime-ktx:2.7.0",
                 'autoFill'                : "androidx.autofill:autofill:1.1.0",
                 'preferenceKtx'           : "androidx.preference:preference-ktx:1.1.1",
                 'junit'                   : "androidx.test.ext:junit:1.1.3",
-                'lifecycleExtensions'     : "androidx.lifecycle:lifecycle-extensions:$lifecycle",
-                'lifecycleJava8'          : "androidx.lifecycle:lifecycle-common-java8:$lifecycle",
-                'lifecycleLivedata'       : "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1",
+                'lifecycleCommon'         : "androidx.lifecycle:lifecycle-common:$lifecycle",
+                'lifecycleLivedata'       : "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle",
+                'lifecycleProcess'        : "androidx.lifecycle:lifecycle-process:$lifecycle",
                 'datastore'               : "androidx.datastore:datastore:1.0.0",
                 'datastorepreferences'    : "androidx.datastore:datastore-preferences:1.0.0",
                 'pagingRuntimeKtx'        : "androidx.paging:paging-runtime-ktx:2.1.2",
@@ -102,7 +103,6 @@ ext.libs = [
                 'epoxyProcessor'         : "com.airbnb.android:epoxy-processor:$epoxy",
                 'epoxyPaging'            : "com.airbnb.android:epoxy-paging:$epoxy",
                 'mavericks'              : "com.airbnb.android:mavericks:$mavericks",
-                'mavericksRx'            : "com.airbnb.android:mavericks-rxjava2:$mavericks",
                 'mavericksTesting'       : "com.airbnb.android:mavericks-testing:$mavericks"
         ],
         mockk      : [
@@ -115,13 +115,13 @@ ext.libs = [
                 'bigImageViewer'         : "com.github.piasy:BigImageViewer:$bigImageViewer",
                 'glideImageLoader'       : "com.github.piasy:GlideImageLoader:$bigImageViewer",
                 'progressPieIndicator'   : "com.github.piasy:ProgressPieIndicator:$bigImageViewer",
-                'glideImageViewFactory'  : "com.github.piasy:GlideImageViewFactory:$bigImageViewer"
+                'glideImageViewFactory'  : "com.github.piasy:GlideImageViewFactory:$bigImageViewer",
+                'flowBinding'            : "io.github.reactivecircus.flowbinding:flowbinding-android:$flowBinding",
+                'flowBindingAppcompat'   : "io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowBinding",
+                'flowBindingMaterial'    : "io.github.reactivecircus.flowbinding:flowbinding-material:$flowBinding"
         ],
         jakewharton : [
-                'timber'                 : "com.jakewharton.timber:timber:5.0.1",
-                'rxbinding'              : "com.jakewharton.rxbinding3:rxbinding:$rxBinding",
-                'rxbindingAppcompat'     : "com.jakewharton.rxbinding3:rxbinding-appcompat:$rxBinding",
-                'rxbindingMaterial'      : "com.jakewharton.rxbinding3:rxbinding-material:$rxBinding"
+                'timber'                 : "com.jakewharton.timber:timber:5.0.1"
         ],
         jsonwebtoken: [
                 'jjwtApi'                : "io.jsonwebtoken:jjwt-api:$jjwt",
diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle
index 98724c5b7f596c1868433b35f7cc39c682420b45..a96ca6c88a2ef3fda65e7fd1a80617d02b360481 100644
--- a/matrix-sdk-android/build.gradle
+++ b/matrix-sdk-android/build.gradle
@@ -11,7 +11,7 @@ buildscript {
         mavenCentral()
     }
     dependencies {
-        classpath "io.realm:realm-gradle-plugin:10.8.0"
+        classpath "io.realm:realm-gradle-plugin:10.8.1"
     }
 }
 
@@ -47,6 +47,7 @@ android {
     }
 
     testOptions {
+        // Comment to run on Android 12
         execution 'ANDROIDX_TEST_ORCHESTRATOR'
     }
 
@@ -109,8 +110,9 @@ dependencies {
     implementation libs.androidx.appCompat
     implementation libs.androidx.core
 
-    implementation libs.androidx.lifecycleExtensions
-    implementation  libs.androidx.lifecycleJava8
+    // Lifecycle
+    implementation libs.androidx.lifecycleCommon
+    implementation libs.androidx.lifecycleProcess
 
     // Network
     implementation libs.squareup.retrofit
@@ -159,10 +161,10 @@ dependencies {
     implementation libs.apache.commonsImaging
 
     // Phone number https://github.com/google/libphonenumber
-    implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.36'
+    implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.37'
 
     testImplementation libs.tests.junit
-    testImplementation 'org.robolectric:robolectric:4.6.1'
+    testImplementation 'org.robolectric:robolectric:4.7'
     //testImplementation 'org.robolectric:shadows-support-v4:3.0'
     // Note: version sticks to 1.9.2 due to https://github.com/mockk/mockk/issues/281
     testImplementation libs.mockk.mockk
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixUrls.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixUrls.kt
index dc4e0f152dfba463ca293aeba132c77ae542a26e..4a41eaec4a18b37cdd7cd43a829de18953dcf180 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixUrls.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixUrls.kt
@@ -20,7 +20,18 @@ package org.matrix.android.sdk.api
  * This class contains pattern to match Matrix Url, aka mxc urls
  */
 object MatrixUrls {
+    /**
+     * "mxc" scheme, including "://". So "mxc://"
+     */
     const val MATRIX_CONTENT_URI_SCHEME = "mxc://"
 
+    /**
+     * Return true if the String starts with "mxc://"
+     */
     fun String.isMxcUrl() = startsWith(MATRIX_CONTENT_URI_SCHEME)
+
+    /**
+     * Remove the "mxc://" prefix. No op if the String is not a Mxc URL
+     */
+    fun String.removeMxcPrefix() = removePrefix(MATRIX_CONTENT_URI_SCHEME)
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt
index b2035bb2eb5ddb27c9c826f4f1acc26e28d1d51a..13a26c89c19e4fe4bbf129a1a3100d929f8aa78d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt
@@ -18,6 +18,8 @@ package org.matrix.android.sdk.api.failure
 
 import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
 import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.api.session.contentscanner.ContentScannerError
+import org.matrix.android.sdk.api.session.contentscanner.ScanFailure
 import org.matrix.android.sdk.internal.di.MoshiProvider
 import java.io.IOException
 import javax.net.ssl.HttpsURLConnection
@@ -100,3 +102,19 @@ fun Throwable.isRegistrationAvailabilityError(): Boolean {
             error.code == MatrixError.M_INVALID_USERNAME ||
             error.code == MatrixError.M_EXCLUSIVE)
 }
+
+/**
+ * Try to convert to a ScanFailure. Return null in the cases it's not possible
+ */
+fun Throwable.toScanFailure(): ScanFailure? {
+    return if (this is Failure.OtherServerError) {
+        tryOrNull {
+            MoshiProvider.providesMoshi()
+                    .adapter(ContentScannerError::class.java)
+                    .fromJson(errorBody)
+        }
+                ?.let { ScanFailure(it, httpCode, this) }
+    } else {
+        null
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushEvents.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushEvents.kt
new file mode 100644
index 0000000000000000000000000000000000000000..466e345cad5bd77969492df9854ccfca31b91f79
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushEvents.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.matrix.android.sdk.api.pushrules
+
+import org.matrix.android.sdk.api.pushrules.rest.PushRule
+import org.matrix.android.sdk.api.session.events.model.Event
+
+data class PushEvents(
+        val matchedEvents: List<Pair<Event, PushRule>>,
+        val roomsJoined: Collection<String>,
+        val roomsLeft: Collection<String>,
+        val redactedEventIds: List<String>
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt
index 1d0acf38fa9c628822a9d64c7e1a00caf0c2c811..88268f0f86e682fb735ffd66abeea99a22976ba0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushRuleService.kt
@@ -51,11 +51,7 @@ interface PushRuleService {
 //    fun fulfilledBingRule(event: Event, rules: List<PushRule>): PushRule?
 
     interface PushRuleListener {
-        fun onMatchRule(event: Event, actions: List<Action>)
-        fun onRoomJoined(roomId: String)
-        fun onRoomLeft(roomId: String)
-        fun onEventRedacted(redactedEventId: String)
-        fun batchFinish()
+        fun onEvents(pushEvents: PushEvents)
     }
 
     fun getKeywords(): LiveData<Set<String>>
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 dfe43aed6f4bd3b4574f452856288ac4ef06127e..3f817ec4d2b2b864b979b82fe6fd0c9585736cfd 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
@@ -31,6 +31,7 @@ import org.matrix.android.sdk.api.session.cache.CacheService
 import org.matrix.android.sdk.api.session.call.CallSignalingService
 import org.matrix.android.sdk.api.session.content.ContentUploadStateTracker
 import org.matrix.android.sdk.api.session.content.ContentUrlResolver
+import org.matrix.android.sdk.api.session.contentscanner.ContentScannerService
 import org.matrix.android.sdk.api.session.crypto.CryptoService
 import org.matrix.android.sdk.api.session.events.EventService
 import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker
@@ -192,6 +193,11 @@ interface Session :
      */
     fun cryptoService(): CryptoService
 
+    /**
+     * Returns the ContentScannerService associated with the session
+     */
+    fun contentScannerService(): ContentScannerService
+
     /**
      * Returns the identity service associated with the session
      */
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUrlResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUrlResolver.kt
index 36c471bb2b516411adc28732bee0f4f38c556351..3dd096e1446af39a99718fe29d8a938b7c3ef9dc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUrlResolver.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUrlResolver.kt
@@ -16,6 +16,8 @@
 
 package org.matrix.android.sdk.api.session.content
 
+import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
+
 /**
  * This interface defines methods for accessing content from the current session.
  */
@@ -39,6 +41,15 @@ interface ContentUrlResolver {
      */
     fun resolveFullSize(contentUrl: String?): String?
 
+    /**
+     * Get the ResolvedMethod to download a URL
+     *
+     * @param contentUrl  the Matrix media content URI (in the form of "mxc://...").
+     * @param elementToDecrypt Encryption data may be required if you use a content scanner
+     * @return the Method to access resource, or null if invalid
+     */
+    fun resolveForDownload(contentUrl: String?, elementToDecrypt: ElementToDecrypt? = null): ResolvedMethod?
+
     /**
      * Get the actual URL for accessing the thumbnail image of a given Matrix media content URI.
      *
@@ -49,4 +60,9 @@ interface ContentUrlResolver {
      * @return the URL to access the described resource, or null if the url is invalid.
      */
     fun resolveThumbnail(contentUrl: String?, width: Int, height: Int, method: ThumbnailMethod): String?
+
+    sealed class ResolvedMethod {
+        data class GET(val url: String) : ResolvedMethod()
+        data class POST(val url: String, val jsonBody: String) : ResolvedMethod()
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/contentscanner/ContentScannerError.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/contentscanner/ContentScannerError.kt
new file mode 100644
index 0000000000000000000000000000000000000000..cef5d41f2cd305f73137d6223c73b18ed7c63ece
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/contentscanner/ContentScannerError.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.contentscanner
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class ContentScannerError(
+        @Json(name = "info") val info: String? = null,
+        @Json(name = "reason") val reason: String? = null
+) {
+    companion object {
+        // 502 The server failed to request media from the media repo.
+        const val REASON_MCS_MEDIA_REQUEST_FAILED = "MCS_MEDIA_REQUEST_FAILED"
+
+        /* 400 The server failed to decrypt the encrypted media downloaded from the media repo.*/
+        const val REASON_MCS_MEDIA_FAILED_TO_DECRYPT = "MCS_MEDIA_FAILED_TO_DECRYPT"
+
+        /* 403 The server scanned the downloaded media but the antivirus script returned a non-zero exit code.*/
+        const val REASON_MCS_MEDIA_NOT_CLEAN = "MCS_MEDIA_NOT_CLEAN"
+
+        /* 403 The provided encrypted_body could not be decrypted. The client should request the public key of the server and then retry (once).*/
+        const val REASON_MCS_BAD_DECRYPTION = "MCS_BAD_DECRYPTION"
+
+        /* 400 The request body contains malformed JSON.*/
+        const val REASON_MCS_MALFORMED_JSON = "MCS_MALFORMED_JSON"
+    }
+}
+
+class ScanFailure(val error: ContentScannerError, val httpCode: Int, cause: Throwable? = null) : Throwable(cause = cause)
+
+// For Glide, which deals with Exception and not with Throwable
+fun ScanFailure.toException() = Exception(this)
+fun Throwable.toScanFailure() = this.cause as? ScanFailure
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/contentscanner/ContentScannerService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/contentscanner/ContentScannerService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..1dd7bab01c5fa732c0edde2adea143db910deff2
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/contentscanner/ContentScannerService.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.contentscanner
+
+import androidx.lifecycle.LiveData
+import org.matrix.android.sdk.api.util.Optional
+import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
+
+interface ContentScannerService {
+
+    val serverPublicKey: String?
+
+    fun getContentScannerServer(): String?
+    fun setScannerUrl(url: String?)
+    fun enableScanner(enabled: Boolean)
+    fun isScannerEnabled(): Boolean
+    fun getLiveStatusForFile(mxcUrl: String, fetchIfNeeded: Boolean = true, fileInfo: ElementToDecrypt? = null): LiveData<Optional<ScanStatusInfo>>
+    fun getCachedScanResultForFile(mxcUrl: String): ScanStatusInfo?
+
+    /**
+     * Get the current public curve25519 key that the AV server is advertising.
+     * @param callback on success callback containing the server public key
+     */
+    suspend fun getServerPublicKey(forceDownload: Boolean = false): String?
+    suspend fun getScanResultForAttachment(mxcUrl: String, fileInfo: ElementToDecrypt? = null): ScanStatusInfo
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/contentscanner/ScanState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/contentscanner/ScanState.kt
new file mode 100644
index 0000000000000000000000000000000000000000..da209080ac31c8c673b10fa360a972dfdc1ba8e5
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/contentscanner/ScanState.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.contentscanner
+
+enum class ScanState {
+    TRUSTED,
+    INFECTED,
+    UNKNOWN,
+    IN_PROGRESS
+}
+
+data class ScanStatusInfo(
+        val state: ScanState,
+        val scanDateTimestamp: Long?,
+        val humanReadableMessage: String?
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt
index d0ce55070e65e7754f9935e45f8cf818bd75db13..a39ca5b4f487ca55dd9440c8d786944f20e0a6f3 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt
@@ -102,6 +102,9 @@ object EventType {
     // Relation Events
     const val REACTION = "m.reaction"
 
+    // Poll
+    const val POLL_START = "org.matrix.msc3381.poll.start"
+
     // Unwedging
     internal const val DUMMY = "m.dummy"
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollContent.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ef2fd1867a7a7ec4d7fadd94acfad7bc8fcff2d5
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollContent.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.room.model.message
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class MessagePollContent(
+    @Json(name = "org.matrix.msc3381.poll.start") val pollCreationInfo: PollCreationInfo? = null
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollAnswer.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollAnswer.kt
new file mode 100644
index 0000000000000000000000000000000000000000..8f5ff53c85cf8329e996c7d400a895327563701f
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollAnswer.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.room.model.message
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class PollAnswer(
+        @Json(name = "id") val id: String? = null,
+        @Json(name = "org.matrix.msc1767.text") val answer: String? = null
+)
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
new file mode 100644
index 0000000000000000000000000000000000000000..e652514b92c7f7635611a41d00acf16796d66b2d
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.room.model.message
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class PollCreationInfo(
+    @Json(name = "question") val question: PollQuestion? = null,
+    @Json(name = "kind") val kind: String? = "org.matrix.msc3381.poll.disclosed",
+    @Json(name = "max_selections") val maxSelections: Int = 1,
+    @Json(name = "answers") val answers: List<PollAnswer>? = null
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollQuestion.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollQuestion.kt
new file mode 100644
index 0000000000000000000000000000000000000000..76025f745e6346cac622dbca8a429c2a530ab8d3
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollQuestion.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.api.session.room.model.message
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+data class PollQuestion(
+        @Json(name = "org.matrix.msc1767.text") val question: String? = null
+)
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 6ae42de90cdd7573cf240f4b134e4d7ec2bc25d0..a2b38b66061b9ff683bc9c2e207c8ae5894c7a91 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
@@ -20,7 +20,6 @@ import org.matrix.android.sdk.api.session.content.ContentAttachmentData
 import org.matrix.android.sdk.api.session.events.model.Content
 import org.matrix.android.sdk.api.session.events.model.Event
 import org.matrix.android.sdk.api.session.room.model.message.MessageType
-import org.matrix.android.sdk.api.session.room.model.message.OptionItem
 import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
 import org.matrix.android.sdk.api.util.Cancelable
 
@@ -84,10 +83,10 @@ interface SendService {
     /**
      * Send a poll to the room.
      * @param question the question
-     * @param options list of (label, value)
+     * @param options list of options
      * @return a [Cancelable]
      */
-    fun sendPoll(question: String, options: List<OptionItem>): Cancelable
+    fun sendPoll(question: String, options: List<String>): Cancelable
 
     /**
      * Method to send a poll response.
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt
index 4a6462477dc653eb91d1c02487494876642602ef..86cb10bfe80637c7ae58537d2850f62e91fa6bed 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt
@@ -28,8 +28,10 @@ import org.matrix.android.sdk.api.session.room.model.EventAnnotationsSummary
 import org.matrix.android.sdk.api.session.room.model.ReadReceipt
 import org.matrix.android.sdk.api.session.room.model.message.MessageContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
+import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
 import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
 import org.matrix.android.sdk.api.session.room.sender.SenderInfo
+import org.matrix.android.sdk.api.util.ContentUtils
 import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromReply
 
 /**
@@ -131,20 +133,6 @@ fun TimelineEvent.getLastMessageContent(): MessageContent? {
     }
 }
 
-/**
- * Get last Message body, after a possible edition
- */
-fun TimelineEvent.getLastMessageBody(): String? {
-    val lastMessageContent = getLastMessageContent()
-
-    if (lastMessageContent != null) {
-        return lastMessageContent.newContent?.toModel<MessageContent>()?.body
-                ?: lastMessageContent.body
-    }
-
-    return null
-}
-
 /**
  * Returns true if it's a reply
  */
@@ -156,11 +144,25 @@ fun TimelineEvent.isEdition(): Boolean {
     return root.isEdition()
 }
 
-fun TimelineEvent.getTextEditableContent(): String? {
-    val lastContent = getLastMessageContent()
+/**
+ * Get the latest message body, after a possible edition, stripping the reply prefix if necessary
+ */
+fun TimelineEvent.getTextEditableContent(): String {
+    val lastContentBody = getLastMessageContent()?.body ?: return ""
     return if (isReply()) {
-        return extractUsefulTextFromReply(lastContent?.body ?: "")
+        extractUsefulTextFromReply(lastContentBody)
     } else {
-        lastContent?.body ?: ""
+        lastContentBody
     }
 }
+
+/**
+ * Get the latest displayable content.
+ * Will take care to hide spoiler text
+ */
+fun MessageContent.getTextDisplayableContent(): String {
+    return newContent?.toModel<MessageTextContent>()?.matrixFormattedBody?.let { ContentUtils.formatSpoilerTextFromHtml(it) }
+            ?: newContent?.toModel<MessageContent>()?.body
+            ?: (this as MessageTextContent?)?.matrixFormattedBody?.let { ContentUtils.formatSpoilerTextFromHtml(it) }
+            ?: body
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/ContentUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/ContentUtils.kt
index 1a00b85ff4e3d04934ae448f65ddeb3e849f7895..e453cb2df51e26fcb794f26f2cbb5f4d38c7b0ef 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/ContentUtils.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/ContentUtils.kt
@@ -15,6 +15,8 @@
  */
 package org.matrix.android.sdk.api.util
 
+import org.matrix.android.sdk.internal.util.unescapeHtml
+
 object ContentUtils {
     fun extractUsefulTextFromReply(repliedBody: String): String {
         val lines = repliedBody.lines()
@@ -44,4 +46,15 @@ object ContentUtils {
         }
         return repliedBody
     }
+
+    @Suppress("RegExpRedundantEscape")
+    fun formatSpoilerTextFromHtml(formattedBody: String): String {
+        // var reason = "",
+        // can capture the spoiler reason for better formatting? ex. { reason = it.value;  ">"}
+        return formattedBody.replace("(?<=<span data-mx-spoiler)=\\\".+?\\\">".toRegex(), ">")
+                .replace("(?<=<span data-mx-spoiler>).+?(?=</span>)".toRegex()) { SPOILER_CHAR.repeat(it.value.length) }
+                .unescapeHtml()
+    }
+
+    private const val SPOILER_CHAR = "â–ˆ"
 }
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 c746ad863a276fe189cb684fea9688cdd5f1a72e..934d61de45f99dde9255e22cdeb9b470e159f4e6 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
@@ -51,6 +51,11 @@ internal class SessionManager @Inject constructor(private val matrixComponent: M
         }
     }
 
+    fun stopSession(sessionId: String) {
+        val sessionComponent = sessionComponents[sessionId] ?: throw RuntimeException("You don't have a session for id $sessionId")
+        sessionComponent.session().stopSync()
+    }
+
     fun getOrCreateSessionComponent(sessionParams: SessionParams): SessionComponent {
         return sessionComponents.getOrPut(sessionParams.credentials.sessionId()) {
             DaggerSessionComponent
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 b72cff3cf1d8f100c86f3bcca3b40ec81f91629b..058395113837ddc975a864c71d3aba04ae33b7d5 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
@@ -33,6 +33,7 @@ import org.matrix.android.sdk.internal.auth.registration.AddThreePidRegistration
 import org.matrix.android.sdk.internal.auth.registration.RegisterAddThreePidTask
 import org.matrix.android.sdk.internal.network.executeRequest
 import org.matrix.android.sdk.internal.session.content.DefaultContentUrlResolver
+import org.matrix.android.sdk.internal.session.contentscanner.DisabledContentScannerService
 
 internal class DefaultLoginWizard(
         private val authAPI: AuthAPI,
@@ -44,7 +45,7 @@ internal class DefaultLoginWizard(
 
     private val getProfileTask: GetProfileTask = DefaultGetProfileTask(
             authAPI,
-            DefaultContentUrlResolver(pendingSessionData.homeServerConnectionConfig)
+            DefaultContentUrlResolver(pendingSessionData.homeServerConnectionConfig, DisabledContentScannerService())
     )
 
     override suspend fun getProfileInfo(matrixId: String): LoginProfileInfo {
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXKey.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXKey.kt
index f71c5079b3ad1c56c3b77a65b1e81275fc612f77..9f425eee7ff44baee02a69d911be8737d7b23c20 100755
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXKey.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/MXKey.kt
@@ -38,14 +38,22 @@ data class MXKey(
         /**
          * signature user Id to [deviceid][signature]
          */
-        private val signatures: Map<String, Map<String, String>>
+        private val signatures: Map<String, Map<String, String>>,
+
+        /**
+         * We have to store the original json because it can contain other fields
+         * that we don't support yet but they would be needed to check signatures
+         */
+        private val rawMap: JsonDict
 ) {
 
     /**
      * @return the signed data map
      */
     fun signalableJSONDictionary(): Map<String, Any> {
-        return mapOf("key" to value)
+        return rawMap.filter {
+            it.key != "signatures" && it.key != "unsigned"
+        }
     }
 
     /**
@@ -82,6 +90,7 @@ data class MXKey(
          * <pre>
          *   "signed_curve25519:AAAAFw": {
          *     "key": "IjwIcskng7YjYcn0tS8TUOT2OHHtBSfMpcfIczCgXj4",
+         *     "fallback" : true|false
          *     "signatures": {
          *       "@userId:matrix.org": {
          *         "ed25519:GMJRREOASV": "EUjp6pXzK9u3SDFR\/qLbzpOi3bEREeI6qMnKzXu992HsfuDDZftfJfiUXv9b\/Hqq1og4qM\/vCQJGTHAWMmgkCg"
@@ -107,7 +116,8 @@ data class MXKey(
                                     type = components[0],
                                     keyId = components[1],
                                     value = params["key"] as String,
-                                    signatures = params["signatures"] as Map<String, Map<String, String>>
+                                    signatures = params["signatures"] as Map<String, Map<String, String>>,
+                                    rawMap = params
                             )
                         }
                     }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/DbQualifiers.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/DbQualifiers.kt
index 49e155add8e7e75c96662a92ef733527b76174f6..8b78cd0d427ef05999f57c3bef23b7d09fdedbbf 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/DbQualifiers.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/DbQualifiers.kt
@@ -37,3 +37,7 @@ internal annotation class CryptoDatabase
 @Qualifier
 @Retention(AnnotationRetention.RUNTIME)
 internal annotation class IdentityDatabase
+
+@Qualifier
+@Retention(AnnotationRetention.RUNTIME)
+internal annotation class ContentScannerDatabase
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt
index 361a306d4f38ae2206d6513cd9da71d3a67e9ee3..1ab1042129f72743a1b42667723360eaf622d7eb 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/NetworkConstants.kt
@@ -38,6 +38,9 @@ internal object NetworkConstants {
     // Integration
     const val URI_INTEGRATION_MANAGER_PATH = "_matrix/integrations/v1/"
 
+    // Content scanner
+    const val URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE = "_matrix/media_proxy/unstable/"
+
     // Federation
     const val URI_FEDERATION_PATH = "_matrix/federation/v1/"
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt
index 46c596787652d6a245744c44602dd7d78e46d056..14dfc097cfc2300447f6e329c5a8827d7a867c14 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt
@@ -23,8 +23,10 @@ import androidx.core.content.FileProvider
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.completeWith
 import kotlinx.coroutines.withContext
+import okhttp3.MediaType.Companion.toMediaType
 import okhttp3.OkHttpClient
 import okhttp3.Request
+import okhttp3.RequestBody.Companion.toRequestBody
 import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
 import org.matrix.android.sdk.api.failure.Failure
 import org.matrix.android.sdk.api.session.content.ContentUrlResolver
@@ -118,12 +120,24 @@ internal class DefaultFileService @Inject constructor(
                 val cachedFiles = getFiles(url, fileName, mimeType, elementToDecrypt != null)
 
                 if (!cachedFiles.file.exists()) {
-                    val resolvedUrl = contentUrlResolver.resolveFullSize(url) ?: throw IllegalArgumentException("url is null")
+                    val resolvedUrl = contentUrlResolver.resolveForDownload(url, elementToDecrypt) ?: throw IllegalArgumentException("url is null")
+
+                    val request = when (resolvedUrl) {
+                        is ContentUrlResolver.ResolvedMethod.GET -> {
+                            Request.Builder()
+                                    .url(resolvedUrl.url)
+                                    .header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER, url)
+                                    .build()
+                        }
 
-                    val request = Request.Builder()
-                            .url(resolvedUrl)
-                            .header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER, url)
-                            .build()
+                        is ContentUrlResolver.ResolvedMethod.POST -> {
+                            Request.Builder()
+                                    .url(resolvedUrl.url)
+                                    .header(DOWNLOAD_PROGRESS_INTERCEPTOR_HEADER, url)
+                                    .post(resolvedUrl.jsonBody.toRequestBody("application/json".toMediaType()))
+                                    .build()
+                        }
+                    }
 
                     val response = try {
                         okHttpClient.newCall(request).execute()
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 c52462612aeb9a566fcb6a2fcdb1c4cee5eeb8af..c07ff48cf48569dd3703bb69cb36c5d61c5ba16a 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
@@ -35,6 +35,7 @@ import org.matrix.android.sdk.api.session.cache.CacheService
 import org.matrix.android.sdk.api.session.call.CallSignalingService
 import org.matrix.android.sdk.api.session.content.ContentUploadStateTracker
 import org.matrix.android.sdk.api.session.content.ContentUrlResolver
+import org.matrix.android.sdk.api.session.contentscanner.ContentScannerService
 import org.matrix.android.sdk.api.session.crypto.CryptoService
 import org.matrix.android.sdk.api.session.events.EventService
 import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker
@@ -124,6 +125,7 @@ internal class DefaultSession @Inject constructor(
         private val _sharedSecretStorageService: Lazy<SharedSecretStorageService>,
         private val accountService: Lazy<AccountService>,
         private val eventService: Lazy<EventService>,
+        private val contentScannerService: Lazy<ContentScannerService>,
         private val identityService: IdentityService,
         private val integrationManagerService: IntegrationManagerService,
         private val thirdPartyService: Lazy<ThirdPartyService>,
@@ -174,8 +176,8 @@ internal class DefaultSession @Inject constructor(
             lifecycleObservers.forEach {
                 it.onSessionStarted(this)
             }
-            sessionListeners.dispatch { _, listener ->
-                listener.onSessionStarted(this)
+            dispatchTo(sessionListeners) { session, listener ->
+                listener.onSessionStarted(session)
             }
         }
     }
@@ -217,8 +219,8 @@ internal class DefaultSession @Inject constructor(
         // timelineEventDecryptor.destroy()
         uiHandler.post {
             lifecycleObservers.forEach { it.onSessionStopped(this) }
-            sessionListeners.dispatch { _, listener ->
-                listener.onSessionStopped(this)
+            dispatchTo(sessionListeners) { session, listener ->
+                listener.onSessionStopped(session)
             }
         }
         cryptoService.get().close()
@@ -249,8 +251,8 @@ internal class DefaultSession @Inject constructor(
             lifecycleObservers.forEach {
                 it.onClearCache(this)
             }
-            sessionListeners.dispatch { _, listener ->
-                listener.onClearCache(this)
+            dispatchTo(sessionListeners) { session, listener ->
+                listener.onClearCache(session)
             }
         }
         withContext(NonCancellable) {
@@ -260,8 +262,8 @@ internal class DefaultSession @Inject constructor(
     }
 
     override fun onGlobalError(globalError: GlobalError) {
-        sessionListeners.dispatch { _, listener ->
-            listener.onGlobalError(this, globalError)
+        dispatchTo(sessionListeners) { session, listener ->
+            listener.onGlobalError(session, globalError)
         }
     }
 
@@ -275,6 +277,8 @@ internal class DefaultSession @Inject constructor(
 
     override fun cryptoService(): CryptoService = cryptoService.get()
 
+    override fun contentScannerService(): ContentScannerService = contentScannerService.get()
+
     override fun identityService() = identityService
 
     override fun fileService(): FileService = defaultFileService.get()
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 bc8a70753086901243df739a70aef66e6f97432d..76e5d84e56622b1753098998f103e7e744699be1 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
@@ -36,6 +36,7 @@ import org.matrix.android.sdk.internal.session.cache.CacheModule
 import org.matrix.android.sdk.internal.session.call.CallModule
 import org.matrix.android.sdk.internal.session.content.ContentModule
 import org.matrix.android.sdk.internal.session.content.UploadContentWorker
+import org.matrix.android.sdk.internal.session.contentscanner.ContentScannerModule
 import org.matrix.android.sdk.internal.session.filter.FilterModule
 import org.matrix.android.sdk.internal.session.group.GetGroupDataWorker
 import org.matrix.android.sdk.internal.session.group.GroupModule
@@ -94,6 +95,7 @@ import org.matrix.android.sdk.internal.util.system.SystemModule
             AccountModule::class,
             FederationModule::class,
             CallModule::class,
+            ContentScannerModule::class,
             SearchModule::class,
             ThirdPartyModule::class,
             SpaceModule::class,
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 d5c661b1e474d4f979df60103f7b147d2c75dfc6..756b9cef836e5d4cd6dced030e1723f53c46352c 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
@@ -18,15 +18,11 @@ package org.matrix.android.sdk.internal.session
 
 import org.matrix.android.sdk.api.extensions.tryOrNull
 import org.matrix.android.sdk.api.session.Session
-import org.matrix.android.sdk.internal.SessionManager
-import org.matrix.android.sdk.internal.di.SessionId
 import timber.log.Timber
 import javax.inject.Inject
 
 @SessionScope
-internal class SessionListeners @Inject constructor(
-        @SessionId private val sessionId: String,
-        private val sessionManager: SessionManager) {
+internal class SessionListeners @Inject constructor() {
 
     private val listeners = mutableSetOf<Session.Listener>()
 
@@ -42,18 +38,19 @@ internal class SessionListeners @Inject constructor(
         }
     }
 
-    fun dispatch(block: (Session, Session.Listener) -> Unit) {
+    fun dispatch(session: Session, block: (Session, Session.Listener) -> Unit) {
         synchronized(listeners) {
-            val session = getSession() ?: return Unit.also {
-                Timber.w("You don't have any attached session")
-            }
             listeners.forEach {
                 tryOrNull { block(session, it) }
             }
         }
     }
+}
 
-    private fun getSession(): Session? {
-        return sessionManager.getSessionComponent(sessionId)?.session()
+internal fun Session?.dispatchTo(sessionListeners: SessionListeners, block: (Session, Session.Listener) -> Unit) {
+    if (this == null) {
+        Timber.w("You don't have any attached session")
+        return
     }
+    sessionListeners.dispatch(this, block)
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountTask.kt
index 1a8e80ab683dcb875cabc216f76548bcdca21475..752856b931aac41d6e16e5d08e2b6098bb1cba47 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountTask.kt
@@ -44,7 +44,7 @@ internal class DefaultDeactivateAccountTask @Inject constructor(
 
     override suspend fun execute(params: DeactivateAccountTask.Params) {
         val deactivateAccountParams = DeactivateAccountParams.create(params.userAuthParam, params.eraseAllData)
-
+        cleanupSession.stopActiveTasks()
         val canCleanup = try {
             executeRequest(globalErrorReceiver) {
                 accountAPI.deactivate(deactivateAccountParams)
@@ -71,7 +71,7 @@ internal class DefaultDeactivateAccountTask @Inject constructor(
             runCatching { identityDisconnectTask.execute(Unit) }
                     .onFailure { Timber.w(it, "Unable to disconnect identity server") }
 
-            cleanupSession.handle()
+            cleanupSession.cleanup()
         }
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cleanup/CleanupSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cleanup/CleanupSession.kt
index e8d3eb1a78d40411f122808be5d6a5077322ad24..c42141a0aa2bfce08df6f2dd506f1c5ed3eeffc1 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cleanup/CleanupSession.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/cleanup/CleanupSession.kt
@@ -50,20 +50,26 @@ internal class CleanupSession @Inject constructor(
         @CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration,
         @UserMd5 private val userMd5: String
 ) {
-    suspend fun handle() {
-        val sessionRealmCount = Realm.getGlobalInstanceCount(realmSessionConfiguration)
-        val cryptoRealmCount = Realm.getGlobalInstanceCount(realmCryptoConfiguration)
-        Timber.d("Realm instance ($sessionRealmCount - $cryptoRealmCount)")
-
-        Timber.d("Cleanup: delete session params...")
-        sessionParamsStore.delete(sessionId)
 
+    fun stopActiveTasks() {
         Timber.d("Cleanup: cancel pending works...")
         workManagerProvider.cancelAllWorks()
 
+        Timber.d("Cleanup: stop session...")
+        sessionManager.stopSession(sessionId)
+    }
+
+    suspend fun cleanup() {
+        val sessionRealmCount = Realm.getGlobalInstanceCount(realmSessionConfiguration)
+        val cryptoRealmCount = Realm.getGlobalInstanceCount(realmCryptoConfiguration)
+        Timber.d("Realm instance ($sessionRealmCount - $cryptoRealmCount)")
+
         Timber.d("Cleanup: release session...")
         sessionManager.releaseSession(sessionId)
 
+        Timber.d("Cleanup: delete session params...")
+        sessionParamsStore.delete(sessionId)
+
         Timber.d("Cleanup: clear session data...")
         clearSessionDataTask.execute(Unit)
 
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUrlResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUrlResolver.kt
index 5c8cf99dc6c584d18e0b0db2e0a61c2142df5007..660ab8726f7f0646f61fdf20e2868e16fc2764fc 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUrlResolver.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUrlResolver.kt
@@ -16,20 +16,45 @@
 
 package org.matrix.android.sdk.internal.session.content
 
-import org.matrix.android.sdk.api.MatrixUrls
 import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl
+import org.matrix.android.sdk.api.MatrixUrls.removeMxcPrefix
 import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
 import org.matrix.android.sdk.api.session.content.ContentUrlResolver
+import org.matrix.android.sdk.api.session.contentscanner.ContentScannerService
+import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
 import org.matrix.android.sdk.internal.network.NetworkConstants
+import org.matrix.android.sdk.internal.session.contentscanner.ScanEncryptorUtils
+import org.matrix.android.sdk.internal.session.contentscanner.model.toJson
 import org.matrix.android.sdk.internal.util.ensureTrailingSlash
 import javax.inject.Inject
 
-internal class DefaultContentUrlResolver @Inject constructor(homeServerConnectionConfig: HomeServerConnectionConfig) : ContentUrlResolver {
+internal class DefaultContentUrlResolver @Inject constructor(
+        homeServerConnectionConfig: HomeServerConnectionConfig,
+        private val scannerService: ContentScannerService
+) : ContentUrlResolver {
 
     private val baseUrl = homeServerConnectionConfig.homeServerUriBase.toString().ensureTrailingSlash()
 
     override val uploadUrl = baseUrl + NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "upload"
 
+    override fun resolveForDownload(contentUrl: String?, elementToDecrypt: ElementToDecrypt?): ContentUrlResolver.ResolvedMethod? {
+        return if (scannerService.isScannerEnabled() && elementToDecrypt != null) {
+            val baseUrl = scannerService.getContentScannerServer()
+            val sep = if (baseUrl?.endsWith("/") == true) "" else "/"
+
+            val url = baseUrl + sep + NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE + "download_encrypted"
+
+            ContentUrlResolver.ResolvedMethod.POST(
+                    url = url,
+                    jsonBody = ScanEncryptorUtils
+                            .getDownloadBodyAndEncryptIfNeeded(scannerService.serverPublicKey, contentUrl ?: "", elementToDecrypt)
+                            .toJson()
+            )
+        } else {
+            resolveFullSize(contentUrl)?.let { ContentUrlResolver.ResolvedMethod.GET(it) }
+        }
+    }
+
     override fun resolveFullSize(contentUrl: String?): String? {
         return contentUrl
                 // do not allow non-mxc content URLs
@@ -37,7 +62,7 @@ internal class DefaultContentUrlResolver @Inject constructor(homeServerConnectio
                 ?.let {
                     resolve(
                             contentUrl = it,
-                            prefix = NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "download/"
+                            toThumbnail = false
                     )
                 }
     }
@@ -49,16 +74,27 @@ internal class DefaultContentUrlResolver @Inject constructor(homeServerConnectio
                 ?.let {
                     resolve(
                             contentUrl = it,
-                            prefix = NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "thumbnail/",
+                            toThumbnail = true,
                             params = "?width=$width&height=$height&method=${method.value}"
                     )
                 }
     }
 
     private fun resolve(contentUrl: String,
-                        prefix: String,
-                        params: String = ""): String? {
-        var serverAndMediaId = contentUrl.removePrefix(MatrixUrls.MATRIX_CONTENT_URI_SCHEME)
+                        toThumbnail: Boolean,
+                        params: String = ""): String {
+        var serverAndMediaId = contentUrl.removeMxcPrefix()
+
+        val apiPath = if (scannerService.isScannerEnabled()) {
+            NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE
+        } else {
+            NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0
+        }
+        val prefix = if (toThumbnail) {
+            apiPath + "thumbnail/"
+        } else {
+            apiPath + "download/"
+        }
         val fragmentOffset = serverAndMediaId.indexOf("#")
         var fragment = ""
         if (fragmentOffset >= 0) {
@@ -66,6 +102,11 @@ internal class DefaultContentUrlResolver @Inject constructor(homeServerConnectio
             serverAndMediaId = serverAndMediaId.substring(0, fragmentOffset)
         }
 
-        return baseUrl + prefix + serverAndMediaId + params + fragment
+        val resolvedUrl = if (scannerService.isScannerEnabled()) {
+            scannerService.getContentScannerServer()!!.ensureTrailingSlash()
+        } else {
+            baseUrl
+        }
+        return resolvedUrl + prefix + serverAndMediaId + params + fragment
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/ContentScannerApi.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/ContentScannerApi.kt
new file mode 100644
index 0000000000000000000000000000000000000000..46f1705806810bf1a048b8796d39cc5ef1bc4ae6
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/ContentScannerApi.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner
+
+import okhttp3.ResponseBody
+import org.matrix.android.sdk.internal.network.NetworkConstants
+import org.matrix.android.sdk.internal.session.contentscanner.model.DownloadBody
+import org.matrix.android.sdk.internal.session.contentscanner.model.ScanResponse
+import org.matrix.android.sdk.internal.session.contentscanner.model.ServerPublicKeyResponse
+import retrofit2.http.Body
+import retrofit2.http.GET
+import retrofit2.http.POST
+import retrofit2.http.Path
+
+/**
+ * https://github.com/matrix-org/matrix-content-scanner
+ */
+internal interface ContentScannerApi {
+
+    @POST(NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE + "download_encrypted")
+    suspend fun downloadEncrypted(@Body info: DownloadBody): ResponseBody
+
+    @POST(NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE + "scan_encrypted")
+    suspend fun scanFile(@Body info: DownloadBody): ScanResponse
+
+    @GET(NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE + "public_key")
+    suspend fun getServerPublicKey(): ServerPublicKeyResponse
+
+    @GET(NetworkConstants.URI_API_PREFIX_PATH_MEDIA_PROXY_UNSTABLE + "scan/{domain}/{mediaId}")
+    suspend fun scanMedia(@Path(value = "domain") domain: String, @Path(value = "mediaId") mediaId: String): ScanResponse
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/ContentScannerApiProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/ContentScannerApiProvider.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d8548bb238dba56b9fd7cd1befea783a09b902df
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/ContentScannerApiProvider.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner
+
+import org.matrix.android.sdk.internal.session.SessionScope
+import javax.inject.Inject
+
+@SessionScope
+internal class ContentScannerApiProvider @Inject constructor() {
+    var contentScannerApi: ContentScannerApi? = null
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/ContentScannerModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/ContentScannerModule.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7ea74225cdac822c43b0acd2ae7e42ea3998bbd9
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/ContentScannerModule.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner
+
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import io.realm.RealmConfiguration
+import org.matrix.android.sdk.api.session.contentscanner.ContentScannerService
+import org.matrix.android.sdk.internal.database.RealmKeysUtils
+import org.matrix.android.sdk.internal.di.ContentScannerDatabase
+import org.matrix.android.sdk.internal.di.SessionFilesDirectory
+import org.matrix.android.sdk.internal.di.UserMd5
+import org.matrix.android.sdk.internal.session.SessionModule
+import org.matrix.android.sdk.internal.session.SessionScope
+import org.matrix.android.sdk.internal.session.contentscanner.data.ContentScannerStore
+import org.matrix.android.sdk.internal.session.contentscanner.db.ContentScannerRealmModule
+import org.matrix.android.sdk.internal.session.contentscanner.db.RealmContentScannerStore
+import org.matrix.android.sdk.internal.session.contentscanner.tasks.DefaultDownloadEncryptedTask
+import org.matrix.android.sdk.internal.session.contentscanner.tasks.DefaultGetServerPublicKeyTask
+import org.matrix.android.sdk.internal.session.contentscanner.tasks.DefaultScanEncryptedTask
+import org.matrix.android.sdk.internal.session.contentscanner.tasks.DefaultScanMediaTask
+import org.matrix.android.sdk.internal.session.contentscanner.tasks.DownloadEncryptedTask
+import org.matrix.android.sdk.internal.session.contentscanner.tasks.GetServerPublicKeyTask
+import org.matrix.android.sdk.internal.session.contentscanner.tasks.ScanEncryptedTask
+import org.matrix.android.sdk.internal.session.contentscanner.tasks.ScanMediaTask
+import java.io.File
+
+@Module
+internal abstract class ContentScannerModule {
+    @Module
+    companion object {
+
+        @JvmStatic
+        @Provides
+        @ContentScannerDatabase
+        @SessionScope
+        fun providesContentScannerRealmConfiguration(realmKeysUtils: RealmKeysUtils,
+                                                     @SessionFilesDirectory directory: File,
+                                                     @UserMd5 userMd5: String): RealmConfiguration {
+            return RealmConfiguration.Builder()
+                    .directory(directory)
+                    .name("matrix-sdk-content-scanning.realm")
+                    .apply {
+                        realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5))
+                    }
+                    .allowWritesOnUiThread(true)
+                    .modules(ContentScannerRealmModule())
+                    .build()
+        }
+    }
+
+    @Binds
+    abstract fun bindContentScannerService(service: DisabledContentScannerService): ContentScannerService
+
+    @Binds
+    abstract fun bindContentScannerStore(store: RealmContentScannerStore): ContentScannerStore
+
+    @Binds
+    abstract fun bindDownloadEncryptedTask(task: DefaultDownloadEncryptedTask): DownloadEncryptedTask
+
+    @Binds
+    abstract fun bindGetServerPublicKeyTask(task: DefaultGetServerPublicKeyTask): GetServerPublicKeyTask
+
+    @Binds
+    abstract fun bindScanMediaTask(task: DefaultScanMediaTask): ScanMediaTask
+
+    @Binds
+    abstract fun bindScanEncryptedTask(task: DefaultScanEncryptedTask): ScanEncryptedTask
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/DefaultContentScannerService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/DefaultContentScannerService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..4ecb3376033ca7fa51e816afc04c9386690497e5
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/DefaultContentScannerService.kt
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner
+
+import androidx.lifecycle.LiveData
+import dagger.Lazy
+import kotlinx.coroutines.launch
+import okhttp3.OkHttpClient
+import org.matrix.android.sdk.api.session.contentscanner.ContentScannerService
+import org.matrix.android.sdk.api.session.contentscanner.ScanState
+import org.matrix.android.sdk.api.session.contentscanner.ScanStatusInfo
+import org.matrix.android.sdk.api.util.Optional
+import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
+import org.matrix.android.sdk.internal.di.Unauthenticated
+import org.matrix.android.sdk.internal.network.RetrofitFactory
+import org.matrix.android.sdk.internal.session.SessionScope
+import org.matrix.android.sdk.internal.session.contentscanner.data.ContentScannerStore
+import org.matrix.android.sdk.internal.session.contentscanner.tasks.GetServerPublicKeyTask
+import org.matrix.android.sdk.internal.session.contentscanner.tasks.ScanEncryptedTask
+import org.matrix.android.sdk.internal.session.contentscanner.tasks.ScanMediaTask
+import org.matrix.android.sdk.internal.task.TaskExecutor
+import timber.log.Timber
+import javax.inject.Inject
+
+@SessionScope
+internal class DefaultContentScannerService @Inject constructor(
+        private val retrofitFactory: RetrofitFactory,
+        @Unauthenticated
+        private val okHttpClient: Lazy<OkHttpClient>,
+        private val contentScannerApiProvider: ContentScannerApiProvider,
+        private val contentScannerStore: ContentScannerStore,
+        private val getServerPublicKeyTask: GetServerPublicKeyTask,
+        private val scanEncryptedTask: ScanEncryptedTask,
+        private val scanMediaTask: ScanMediaTask,
+        private val taskExecutor: TaskExecutor
+) : ContentScannerService {
+
+    // Cache public key in memory
+    override var serverPublicKey: String? = null
+        private set
+
+    override fun getContentScannerServer(): String? {
+        return contentScannerStore.getScannerUrl()
+    }
+
+    override suspend fun getServerPublicKey(forceDownload: Boolean): String? {
+        val api = contentScannerApiProvider.contentScannerApi ?: throw IllegalArgumentException("No content scanner define")
+
+        if (!forceDownload && serverPublicKey != null) {
+            return serverPublicKey
+        }
+
+        return getServerPublicKeyTask.execute(GetServerPublicKeyTask.Params(api)).also {
+            serverPublicKey = it
+        }
+    }
+
+    override suspend fun getScanResultForAttachment(mxcUrl: String, fileInfo: ElementToDecrypt?): ScanStatusInfo {
+        val result = if (fileInfo != null) {
+            scanEncryptedTask.execute(ScanEncryptedTask.Params(
+                    mxcUrl = mxcUrl,
+                    publicServerKey = getServerPublicKey(false),
+                    encryptedInfo = fileInfo
+            ))
+        } else {
+            scanMediaTask.execute(ScanMediaTask.Params(mxcUrl))
+        }
+
+        return ScanStatusInfo(
+                state = if (result.clean) ScanState.TRUSTED else ScanState.INFECTED,
+                humanReadableMessage = result.info,
+                scanDateTimestamp = System.currentTimeMillis()
+        )
+    }
+
+    override fun setScannerUrl(url: String?) = contentScannerStore.setScannerUrl(url).also {
+        if (url == null) {
+            contentScannerApiProvider.contentScannerApi = null
+            serverPublicKey = null
+        } else {
+            val api = retrofitFactory
+                    .create(okHttpClient, url)
+                    .create(ContentScannerApi::class.java)
+            contentScannerApiProvider.contentScannerApi = api
+
+            taskExecutor.executorScope.launch {
+                try {
+                    getServerPublicKey(true)
+                } catch (failure: Throwable) {
+                    Timber.e("Failed to get public server api")
+                }
+            }
+        }
+    }
+
+    override fun enableScanner(enabled: Boolean) = contentScannerStore.enableScanner(enabled)
+
+    override fun isScannerEnabled(): Boolean = contentScannerStore.isScanEnabled()
+
+    override fun getCachedScanResultForFile(mxcUrl: String): ScanStatusInfo? {
+        return contentScannerStore.getScanResult(mxcUrl)
+    }
+
+    override fun getLiveStatusForFile(mxcUrl: String, fetchIfNeeded: Boolean, fileInfo: ElementToDecrypt?): LiveData<Optional<ScanStatusInfo>> {
+        val data = contentScannerStore.getLiveScanResult(mxcUrl)
+        if (fetchIfNeeded && !contentScannerStore.isScanResultKnownOrInProgress(mxcUrl, getContentScannerServer())) {
+            taskExecutor.executorScope.launch {
+                try {
+                    getScanResultForAttachment(mxcUrl, fileInfo)
+                } catch (failure: Throwable) {
+                    Timber.e("Failed to get file status : ${failure.localizedMessage}")
+                }
+            }
+        }
+        return data
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/DisabledContentScannerService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/DisabledContentScannerService.kt
new file mode 100644
index 0000000000000000000000000000000000000000..9087c715663791aecbe866f2615c8da6ff1f9611
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/DisabledContentScannerService.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import org.matrix.android.sdk.api.session.contentscanner.ContentScannerService
+import org.matrix.android.sdk.api.session.contentscanner.ScanStatusInfo
+import org.matrix.android.sdk.api.util.Optional
+import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
+import org.matrix.android.sdk.internal.session.SessionScope
+import javax.inject.Inject
+
+/**
+ * Created to by-pass ProfileTask execution in LoginWizard.
+ */
+@SessionScope
+internal class DisabledContentScannerService @Inject constructor() : ContentScannerService {
+
+    override val serverPublicKey: String?
+        get() = null
+
+    override fun getContentScannerServer(): String? {
+        return null
+    }
+
+    override suspend fun getServerPublicKey(forceDownload: Boolean): String? {
+        return null
+    }
+
+    override suspend fun getScanResultForAttachment(mxcUrl: String, fileInfo: ElementToDecrypt?): ScanStatusInfo {
+        TODO("Not yet implemented")
+    }
+
+    override fun setScannerUrl(url: String?) {
+    }
+
+    override fun enableScanner(enabled: Boolean) {
+    }
+
+    override fun isScannerEnabled(): Boolean {
+        return false
+    }
+
+    override fun getLiveStatusForFile(mxcUrl: String, fetchIfNeeded: Boolean, fileInfo: ElementToDecrypt?): LiveData<Optional<ScanStatusInfo>> {
+        return MutableLiveData()
+    }
+
+    override fun getCachedScanResultForFile(mxcUrl: String): ScanStatusInfo? {
+        return null
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/ScanEncryptorUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/ScanEncryptorUtils.kt
new file mode 100644
index 0000000000000000000000000000000000000000..8fc84a487e58167e270b1c28c0098c05e1de75b2
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/ScanEncryptorUtils.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner
+
+import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
+import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
+import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileKey
+import org.matrix.android.sdk.internal.crypto.tools.withOlmEncryption
+import org.matrix.android.sdk.internal.session.contentscanner.model.DownloadBody
+import org.matrix.android.sdk.internal.session.contentscanner.model.EncryptedBody
+import org.matrix.android.sdk.internal.session.contentscanner.model.toCanonicalJson
+
+internal object ScanEncryptorUtils {
+
+    fun getDownloadBodyAndEncryptIfNeeded(publicServerKey: String?, mxcUrl: String, elementToDecrypt: ElementToDecrypt): DownloadBody {
+        // TODO, upstream refactoring changed the object model here...
+        // it's bad we have to recreate and use hardcoded values
+        val encryptedInfo = EncryptedFileInfo(
+                url = mxcUrl,
+                iv = elementToDecrypt.iv,
+                hashes = mapOf("sha256" to elementToDecrypt.sha256),
+                key = EncryptedFileKey(
+                        k = elementToDecrypt.k,
+                        alg = "A256CTR",
+                        keyOps = listOf("encrypt", "decrypt"),
+                        kty = "oct",
+                        ext = true
+                ),
+                v = "v2"
+        )
+        return if (publicServerKey != null) {
+            // We should encrypt
+            withOlmEncryption { olm ->
+                olm.setRecipientKey(publicServerKey)
+
+                val olmResult = olm.encrypt(DownloadBody(encryptedInfo).toCanonicalJson())
+                DownloadBody(
+                        encryptedBody = EncryptedBody(
+                                cipherText = olmResult.mCipherText,
+                                ephemeral = olmResult.mEphemeralKey,
+                                mac = olmResult.mMac
+                        )
+                )
+            }
+        } else {
+            DownloadBody(encryptedInfo)
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/data/ContentScannerStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/data/ContentScannerStore.kt
new file mode 100644
index 0000000000000000000000000000000000000000..5cfe851a5c12f5e6b5e808ff701afc2beee5437b
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/data/ContentScannerStore.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner.data
+
+import androidx.lifecycle.LiveData
+import org.matrix.android.sdk.api.session.contentscanner.ScanState
+import org.matrix.android.sdk.api.session.contentscanner.ScanStatusInfo
+import org.matrix.android.sdk.api.util.Optional
+
+internal interface ContentScannerStore {
+
+    fun getScannerUrl(): String?
+
+    fun setScannerUrl(url: String?)
+
+    fun enableScanner(enabled: Boolean)
+
+    fun isScanEnabled(): Boolean
+
+    fun getScanResult(mxcUrl: String): ScanStatusInfo?
+    fun getLiveScanResult(mxcUrl: String): LiveData<Optional<ScanStatusInfo>>
+    fun isScanResultKnownOrInProgress(mxcUrl: String, scannerUrl: String?): Boolean
+
+    fun updateStateForContent(mxcUrl: String, state: ScanState, scannerUrl: String?)
+    fun updateScanResultForContent(mxcUrl: String, scannerUrl: String?, state: ScanState, humanReadable: String)
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScanResultEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScanResultEntity.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0ffff441f82bc38577001130807ad72acc20e22d
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScanResultEntity.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner.db
+
+import io.realm.RealmObject
+import io.realm.annotations.Index
+import org.matrix.android.sdk.api.extensions.tryOrNull
+import org.matrix.android.sdk.api.session.contentscanner.ScanState
+import org.matrix.android.sdk.api.session.contentscanner.ScanStatusInfo
+
+internal open class ContentScanResultEntity(
+        @Index
+        var mediaUrl: String? = null,
+        var scanStatusString: String? = null,
+        var humanReadableMessage: String? = null,
+        var scanDateTimestamp: Long? = null,
+        var scannerUrl: String? = null
+) : RealmObject() {
+
+    var scanResult: ScanState
+        get() {
+            return scanStatusString
+                    ?.let {
+                        tryOrNull { ScanState.valueOf(it) }
+                    }
+                    ?: ScanState.UNKNOWN
+        }
+        set(result) {
+            scanStatusString = result.name
+        }
+
+    fun toModel(): ScanStatusInfo {
+        return ScanStatusInfo(
+                state = this.scanResult,
+                humanReadableMessage = humanReadableMessage,
+                scanDateTimestamp = scanDateTimestamp
+        )
+    }
+
+    companion object
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScannerEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScannerEntityQueries.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b47be235c602506f5313168d53d2628ca3e0fdac
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScannerEntityQueries.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner.db
+
+import io.realm.Realm
+import io.realm.kotlin.createObject
+import io.realm.kotlin.where
+
+internal fun ContentScanResultEntity.Companion.get(realm: Realm, attachmentUrl: String, contentScannerUrl: String?): ContentScanResultEntity? {
+    return realm.where<ContentScanResultEntity>()
+            .equalTo(ContentScanResultEntityFields.MEDIA_URL, attachmentUrl)
+            .apply {
+                contentScannerUrl?.let {
+                    equalTo(ContentScanResultEntityFields.SCANNER_URL, it)
+                }
+            }
+            .findFirst()
+}
+
+internal fun ContentScanResultEntity.Companion.getOrCreate(realm: Realm, attachmentUrl: String, contentScannerUrl: String?): ContentScanResultEntity {
+    return ContentScanResultEntity.get(realm, attachmentUrl, contentScannerUrl)
+            ?: realm.createObject<ContentScanResultEntity>().also {
+                it.mediaUrl = attachmentUrl
+                it.scanDateTimestamp = System.currentTimeMillis()
+                it.scannerUrl = contentScannerUrl
+            }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScannerInfoEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScannerInfoEntity.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d1910de36adecf94402e90f243798f41925335f3
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScannerInfoEntity.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner.db
+
+import io.realm.RealmObject
+
+internal open class ContentScannerInfoEntity(
+        var serverUrl: String? = null,
+        var enabled: Boolean? = null
+) : RealmObject() {
+
+    companion object
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScannerRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScannerRealmModule.kt
new file mode 100644
index 0000000000000000000000000000000000000000..bb53140ad989bfd2dec97b0f3ddaedd89455277c
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScannerRealmModule.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner.db
+
+import io.realm.annotations.RealmModule
+
+/**
+ * Realm module for content scanner classes
+ */
+@RealmModule(library = true,
+        classes = [
+            ContentScannerInfoEntity::class,
+            ContentScanResultEntity::class
+        ])
+internal class ContentScannerRealmModule
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/RealmContentScannerStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/RealmContentScannerStore.kt
new file mode 100644
index 0000000000000000000000000000000000000000..947a66c8b9782f609c5aecefef53985b0f750b12
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/RealmContentScannerStore.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner.db
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.Transformations
+import com.zhuinden.monarchy.Monarchy
+import io.realm.Realm
+import io.realm.RealmConfiguration
+import io.realm.kotlin.createObject
+import io.realm.kotlin.where
+import org.matrix.android.sdk.api.extensions.orFalse
+import org.matrix.android.sdk.api.session.contentscanner.ScanState
+import org.matrix.android.sdk.api.session.contentscanner.ScanStatusInfo
+import org.matrix.android.sdk.api.util.Optional
+import org.matrix.android.sdk.api.util.toOptional
+import org.matrix.android.sdk.internal.di.ContentScannerDatabase
+import org.matrix.android.sdk.internal.session.SessionScope
+import org.matrix.android.sdk.internal.session.contentscanner.data.ContentScannerStore
+import org.matrix.android.sdk.internal.util.isValidUrl
+import javax.inject.Inject
+
+@SessionScope
+internal class RealmContentScannerStore @Inject constructor(
+        @ContentScannerDatabase
+        private val realmConfiguration: RealmConfiguration
+) : ContentScannerStore {
+
+    private val monarchy = Monarchy.Builder()
+            .setRealmConfiguration(realmConfiguration)
+            .build()
+
+    override fun getScannerUrl(): String? {
+        return monarchy.fetchAllMappedSync(
+                { realm ->
+                    realm.where<ContentScannerInfoEntity>()
+                }, {
+            it.serverUrl
+        }
+        ).firstOrNull()
+    }
+
+    override fun setScannerUrl(url: String?) {
+        monarchy.runTransactionSync { realm ->
+            val info = realm.where<ContentScannerInfoEntity>().findFirst()
+                    ?: realm.createObject()
+            info.serverUrl = url
+        }
+    }
+
+    override fun enableScanner(enabled: Boolean) {
+        monarchy.runTransactionSync { realm ->
+            val info = realm.where<ContentScannerInfoEntity>().findFirst()
+                    ?: realm.createObject()
+            info.enabled = enabled
+        }
+    }
+
+    override fun isScanEnabled(): Boolean {
+        return monarchy.fetchAllMappedSync(
+                { realm ->
+                    realm.where<ContentScannerInfoEntity>()
+                }, {
+            it.enabled.orFalse() && it.serverUrl?.isValidUrl().orFalse()
+        }
+        ).firstOrNull().orFalse()
+    }
+
+    override fun updateStateForContent(mxcUrl: String, state: ScanState, scannerUrl: String?) {
+        monarchy.runTransactionSync {
+            ContentScanResultEntity.getOrCreate(it, mxcUrl, scannerUrl).scanResult = state
+        }
+    }
+
+    override fun updateScanResultForContent(mxcUrl: String, scannerUrl: String?, state: ScanState, humanReadable: String) {
+        monarchy.runTransactionSync {
+            ContentScanResultEntity.getOrCreate(it, mxcUrl, scannerUrl).apply {
+                scanResult = state
+                scanDateTimestamp = System.currentTimeMillis()
+                humanReadableMessage = humanReadable
+            }
+        }
+    }
+
+    override fun isScanResultKnownOrInProgress(mxcUrl: String, scannerUrl: String?): Boolean {
+        var isKnown = false
+        monarchy.runTransactionSync {
+            val info = ContentScanResultEntity.get(it, mxcUrl, scannerUrl)?.scanResult
+            isKnown = when (info) {
+                ScanState.IN_PROGRESS,
+                ScanState.TRUSTED,
+                ScanState.INFECTED -> true
+                else               -> false
+            }
+        }
+        return isKnown
+    }
+
+    override fun getScanResult(mxcUrl: String): ScanStatusInfo? {
+        return monarchy.fetchAllMappedSync({ realm ->
+            realm.where<ContentScanResultEntity>()
+                    .equalTo(ContentScanResultEntityFields.MEDIA_URL, mxcUrl)
+                    .apply {
+                        getScannerUrl()?.let {
+                            equalTo(ContentScanResultEntityFields.SCANNER_URL, it)
+                        }
+                    }
+        }, {
+            it.toModel()
+        })
+                .firstOrNull()
+    }
+
+    override fun getLiveScanResult(mxcUrl: String): LiveData<Optional<ScanStatusInfo>> {
+        val liveData = monarchy.findAllMappedWithChanges(
+                { realm: Realm ->
+                    realm.where<ContentScanResultEntity>()
+                            .equalTo(ContentScanResultEntityFields.MEDIA_URL, mxcUrl)
+                            .equalTo(ContentScanResultEntityFields.SCANNER_URL, getScannerUrl())
+                },
+                { entity ->
+                    entity.toModel()
+                }
+        )
+        return Transformations.map(liveData) {
+            it.firstOrNull().toOptional()
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/model/DownloadBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/model/DownloadBody.kt
new file mode 100644
index 0000000000000000000000000000000000000000..5bac96a0c0e2df645385e6f0ebd80cae31a2d3ab
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/model/DownloadBody.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
+import org.matrix.android.sdk.internal.di.MoshiProvider
+import org.matrix.android.sdk.internal.util.JsonCanonicalizer
+
+@JsonClass(generateAdapter = true)
+internal data class DownloadBody(
+        @Json(name = "file") val file: EncryptedFileInfo? = null,
+        @Json(name = "encrypted_body") val encryptedBody: EncryptedBody? = null
+)
+
+@JsonClass(generateAdapter = true)
+internal data class EncryptedBody(
+        @Json(name = "ciphertext") val cipherText: String,
+        @Json(name = "mac") val mac: String,
+        @Json(name = "ephemeral") val ephemeral: String
+)
+
+internal fun DownloadBody.toJson(): String = MoshiProvider.providesMoshi().adapter(DownloadBody::class.java).toJson(this)
+
+internal fun DownloadBody.toCanonicalJson() = JsonCanonicalizer.getCanonicalJson(DownloadBody::class.java, this)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/model/ScanResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/model/ScanResponse.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f783fe0a6c2c384a551af47803c3cfdbc141d2b2
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/model/ScanResponse.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+/**
+ * {
+ *      "clean": true,
+ *      "info": "File clean at 6/7/2018, 6:02:40 PM"
+ *  }
+ */
+@JsonClass(generateAdapter = true)
+internal data class ScanResponse(
+        @Json(name = "clean") val clean: Boolean,
+        /** Human-readable information about the result. */
+        @Json(name = "info") val info: String?
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/model/ServerPublicKeyResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/model/ServerPublicKeyResponse.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2e97a85bcaf5dec0cb7b030489a2341391e3dd9b
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/model/ServerPublicKeyResponse.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+internal data class ServerPublicKeyResponse(
+        @Json(name = "public_key")
+        val publicKey: String?
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/DownloadEncryptedTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/DownloadEncryptedTask.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f92c869cb890161901d1ad724fa5d4153573e0f9
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/DownloadEncryptedTask.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner.tasks
+
+import okhttp3.ResponseBody
+import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
+import org.matrix.android.sdk.internal.network.executeRequest
+import org.matrix.android.sdk.internal.session.contentscanner.ContentScannerApiProvider
+import org.matrix.android.sdk.internal.session.contentscanner.ScanEncryptorUtils
+import org.matrix.android.sdk.internal.task.Task
+import javax.inject.Inject
+
+internal interface DownloadEncryptedTask : Task<DownloadEncryptedTask.Params, ResponseBody> {
+    data class Params(
+            val publicServerKey: String?,
+            val encryptedInfo: ElementToDecrypt,
+            val mxcUrl: String
+    )
+}
+
+internal class DefaultDownloadEncryptedTask @Inject constructor(
+        private val contentScannerApiProvider: ContentScannerApiProvider
+) : DownloadEncryptedTask {
+
+    override suspend fun execute(params: DownloadEncryptedTask.Params): ResponseBody {
+        val dlBody = ScanEncryptorUtils.getDownloadBodyAndEncryptIfNeeded(
+                params.publicServerKey,
+                params.mxcUrl,
+                params.encryptedInfo
+        )
+
+        val api = contentScannerApiProvider.contentScannerApi ?: throw IllegalArgumentException()
+        return executeRequest(null) {
+            api.downloadEncrypted(dlBody)
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/GetServerPublicKeyTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/GetServerPublicKeyTask.kt
new file mode 100644
index 0000000000000000000000000000000000000000..41c2ec9c3869f1b17d1a149cd35736ccd95f2eab
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/GetServerPublicKeyTask.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner.tasks
+
+import org.matrix.android.sdk.internal.network.executeRequest
+import org.matrix.android.sdk.internal.session.contentscanner.ContentScannerApi
+import org.matrix.android.sdk.internal.session.contentscanner.model.ServerPublicKeyResponse
+import org.matrix.android.sdk.internal.task.Task
+import javax.inject.Inject
+
+internal interface GetServerPublicKeyTask : Task<GetServerPublicKeyTask.Params, String?> {
+    data class Params(
+            val contentScannerApi: ContentScannerApi
+    )
+}
+
+internal class DefaultGetServerPublicKeyTask @Inject constructor() : GetServerPublicKeyTask {
+
+    override suspend fun execute(params: GetServerPublicKeyTask.Params): String? {
+        return executeRequest<ServerPublicKeyResponse>(null) {
+            params.contentScannerApi.getServerPublicKey()
+        }.publicKey
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/ScanEncryptedTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/ScanEncryptedTask.kt
new file mode 100644
index 0000000000000000000000000000000000000000..dab9b5538fe47debf2bcbbfba81a190f8646512d
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/ScanEncryptedTask.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner.tasks
+
+import org.matrix.android.sdk.api.failure.toScanFailure
+import org.matrix.android.sdk.api.session.contentscanner.ScanState
+import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
+import org.matrix.android.sdk.internal.network.executeRequest
+import org.matrix.android.sdk.internal.session.contentscanner.ContentScannerApiProvider
+import org.matrix.android.sdk.internal.session.contentscanner.ScanEncryptorUtils
+import org.matrix.android.sdk.internal.session.contentscanner.data.ContentScannerStore
+import org.matrix.android.sdk.internal.session.contentscanner.model.ScanResponse
+import org.matrix.android.sdk.internal.task.Task
+import javax.inject.Inject
+
+internal interface ScanEncryptedTask : Task<ScanEncryptedTask.Params, ScanResponse> {
+    data class Params(
+            val mxcUrl: String,
+            val publicServerKey: String?,
+            val encryptedInfo: ElementToDecrypt
+    )
+}
+
+internal class DefaultScanEncryptedTask @Inject constructor(
+        private val contentScannerApiProvider: ContentScannerApiProvider,
+        private val contentScannerStore: ContentScannerStore
+) : ScanEncryptedTask {
+
+    override suspend fun execute(params: ScanEncryptedTask.Params): ScanResponse {
+        val mxcUrl = params.mxcUrl
+        val dlBody = ScanEncryptorUtils.getDownloadBodyAndEncryptIfNeeded(params.publicServerKey, params.mxcUrl, params.encryptedInfo)
+
+        val scannerUrl = contentScannerStore.getScannerUrl()
+        contentScannerStore.updateStateForContent(params.mxcUrl, ScanState.IN_PROGRESS, scannerUrl)
+
+        try {
+            val api = contentScannerApiProvider.contentScannerApi ?: throw IllegalArgumentException()
+            val executeRequest = executeRequest<ScanResponse>(null) {
+                api.scanFile(dlBody)
+            }
+            contentScannerStore.updateScanResultForContent(
+                    mxcUrl,
+                    scannerUrl,
+                    ScanState.TRUSTED.takeIf { executeRequest.clean } ?: ScanState.INFECTED,
+                    executeRequest.info ?: ""
+            )
+            return executeRequest
+        } catch (failure: Throwable) {
+            contentScannerStore.updateStateForContent(params.mxcUrl, ScanState.UNKNOWN, scannerUrl)
+            throw failure.toScanFailure() ?: failure
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/ScanMediaTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/ScanMediaTask.kt
new file mode 100644
index 0000000000000000000000000000000000000000..505eb7098c45c3cc90f187e26f0fd2a179829fbe
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/ScanMediaTask.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2021 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.contentscanner.tasks
+
+import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl
+import org.matrix.android.sdk.api.MatrixUrls.removeMxcPrefix
+import org.matrix.android.sdk.api.failure.toScanFailure
+import org.matrix.android.sdk.api.session.contentscanner.ScanState
+import org.matrix.android.sdk.internal.network.executeRequest
+import org.matrix.android.sdk.internal.session.contentscanner.ContentScannerApiProvider
+import org.matrix.android.sdk.internal.session.contentscanner.data.ContentScannerStore
+import org.matrix.android.sdk.internal.session.contentscanner.model.ScanResponse
+import org.matrix.android.sdk.internal.task.Task
+import javax.inject.Inject
+
+internal interface ScanMediaTask : Task<ScanMediaTask.Params, ScanResponse> {
+    data class Params(
+            val mxcUrl: String
+    )
+}
+
+internal class DefaultScanMediaTask @Inject constructor(
+        private val contentScannerApiProvider: ContentScannerApiProvider,
+        private val contentScannerStore: ContentScannerStore
+) : ScanMediaTask {
+
+    override suspend fun execute(params: ScanMediaTask.Params): ScanResponse {
+        // "mxc://server.org/QNDpzLopkoQYNikJfoZCQuCXJ"
+        if (!params.mxcUrl.isMxcUrl()) {
+            throw IllegalAccessException("Invalid mxc url")
+        }
+        val scannerUrl = contentScannerStore.getScannerUrl()
+        contentScannerStore.updateStateForContent(params.mxcUrl, ScanState.IN_PROGRESS, scannerUrl)
+
+        var serverAndMediaId = params.mxcUrl.removeMxcPrefix()
+        val fragmentOffset = serverAndMediaId.indexOf("#")
+        if (fragmentOffset >= 0) {
+            serverAndMediaId = serverAndMediaId.substring(0, fragmentOffset)
+        }
+
+        val split = serverAndMediaId.split("/")
+        if (split.size != 2) {
+            throw IllegalAccessException("Invalid mxc url")
+        }
+
+        try {
+            val scanResponse = executeRequest<ScanResponse>(null) {
+                val api = contentScannerApiProvider.contentScannerApi ?: throw IllegalArgumentException()
+                api.scanMedia(split[0], split[1])
+            }
+            contentScannerStore.updateScanResultForContent(
+                    params.mxcUrl,
+                    scannerUrl,
+                    ScanState.TRUSTED.takeIf { scanResponse.clean } ?: ScanState.INFECTED,
+                    scanResponse.info ?: ""
+            )
+            return scanResponse
+        } catch (failure: Throwable) {
+            contentScannerStore.updateStateForContent(params.mxcUrl, ScanState.UNKNOWN, scannerUrl)
+            throw failure.toScanFailure() ?: failure
+        }
+    }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt
index 65974151c81d054a4d0918025fa26437f20a9c4a..3e821b8956f100211b5552a0b26c8ecfde3e3958 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/DefaultPushRuleService.kt
@@ -19,6 +19,7 @@ import androidx.lifecycle.LiveData
 import androidx.lifecycle.Transformations
 import com.zhuinden.monarchy.Monarchy
 import org.matrix.android.sdk.api.pushrules.Action
+import org.matrix.android.sdk.api.pushrules.PushEvents
 import org.matrix.android.sdk.api.pushrules.PushRuleService
 import org.matrix.android.sdk.api.pushrules.RuleKind
 import org.matrix.android.sdk.api.pushrules.RuleScope
@@ -142,79 +143,6 @@ internal class DefaultPushRuleService @Inject constructor(
         return pushRuleFinder.fulfilledBingRule(event, rules)?.getActions().orEmpty()
     }
 
-//    fun processEvents(events: List<Event>) {
-//        var hasDoneSomething = false
-//        events.forEach { event ->
-//            fulfilledBingRule(event)?.let {
-//                hasDoneSomething = true
-//                dispatchBing(event, it)
-//            }
-//        }
-//        if (hasDoneSomething)
-//            dispatchFinish()
-//    }
-
-    fun dispatchBing(event: Event, rule: PushRule) {
-        synchronized(listeners) {
-            val actionsList = rule.getActions()
-            listeners.forEach {
-                try {
-                    it.onMatchRule(event, actionsList)
-                } catch (e: Throwable) {
-                    Timber.e(e, "Error while dispatching bing")
-                }
-            }
-        }
-    }
-
-    fun dispatchRoomJoined(roomId: String) {
-        synchronized(listeners) {
-            listeners.forEach {
-                try {
-                    it.onRoomJoined(roomId)
-                } catch (e: Throwable) {
-                    Timber.e(e, "Error while dispatching room joined")
-                }
-            }
-        }
-    }
-
-    fun dispatchRoomLeft(roomId: String) {
-        synchronized(listeners) {
-            listeners.forEach {
-                try {
-                    it.onRoomLeft(roomId)
-                } catch (e: Throwable) {
-                    Timber.e(e, "Error while dispatching room left")
-                }
-            }
-        }
-    }
-
-    fun dispatchRedactedEventId(redactedEventId: String) {
-        synchronized(listeners) {
-            listeners.forEach {
-                try {
-                    it.onEventRedacted(redactedEventId)
-                } catch (e: Throwable) {
-                    Timber.e(e, "Error while dispatching redacted event")
-                }
-            }
-        }
-    }
-
-    fun dispatchFinish() {
-        synchronized(listeners) {
-            listeners.forEach {
-                try {
-                    it.batchFinish()
-                } catch (e: Throwable) {
-                    Timber.e(e, "Error while dispatching finish")
-                }
-            }
-        }
-    }
-
     override fun getKeywords(): LiveData<Set<String>> {
         // Keywords are all content rules that don't start with '.'
         val liveData = monarchy.findAllMappedWithChanges(
@@ -229,4 +157,16 @@ internal class DefaultPushRuleService @Inject constructor(
             results.firstOrNull().orEmpty().toSet()
         }
     }
+
+    fun dispatchEvents(pushEvents: PushEvents) {
+        synchronized(listeners) {
+            listeners.forEach {
+                try {
+                    it.onEvents(pushEvents)
+                } catch (e: Throwable) {
+                    Timber.e(e, "Error while dispatching push events")
+                }
+            }
+        }
+    }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt
index 3c74888eda1e50f852d1f4f12a8b7dfa06bdce45..0ac21b555e840cd4268c683a0586e851928d8276 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt
@@ -16,6 +16,7 @@
 
 package org.matrix.android.sdk.internal.session.notification
 
+import org.matrix.android.sdk.api.pushrules.PushEvents
 import org.matrix.android.sdk.api.pushrules.rest.PushRule
 import org.matrix.android.sdk.api.session.events.model.EventType
 import org.matrix.android.sdk.api.session.events.model.isInvitation
@@ -39,14 +40,6 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
 ) : ProcessEventForPushTask {
 
     override suspend fun execute(params: ProcessEventForPushTask.Params) {
-        // Handle left rooms
-        params.syncResponse.leave.keys.forEach {
-            defaultPushRuleService.dispatchRoomLeft(it)
-        }
-        // Handle joined rooms
-        params.syncResponse.join.keys.forEach {
-            defaultPushRuleService.dispatchRoomJoined(it)
-        }
         val newJoinEvents = params.syncResponse.join
                 .mapNotNull { (key, value) ->
                     value.timeline?.events?.mapNotNull {
@@ -74,10 +67,10 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
         }
         Timber.v("[PushRules] Found ${allEvents.size} out of ${(newJoinEvents + inviteEvents).size}" +
                 " to check for push rules with ${params.rules.size} rules")
-        allEvents.forEach { event ->
+        val matchedEvents = allEvents.mapNotNull { event ->
             pushRuleFinder.fulfilledBingRule(event, params.rules)?.let {
                 Timber.v("[PushRules] Rule $it match for event ${event.eventId}")
-                defaultPushRuleService.dispatchBing(event, it)
+                event to it
             }
         }
 
@@ -91,10 +84,13 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
 
         Timber.v("[PushRules] Found ${allRedactedEvents.size} redacted events")
 
-        allRedactedEvents.forEach { redactedEventId ->
-            defaultPushRuleService.dispatchRedactedEventId(redactedEventId)
-        }
-
-        defaultPushRuleService.dispatchFinish()
+        defaultPushRuleService.dispatchEvents(
+                PushEvents(
+                        matchedEvents = matchedEvents,
+                        roomsJoined = params.syncResponse.join.keys,
+                        roomsLeft = params.syncResponse.leave.keys,
+                        redactedEventIds = allRedactedEvents
+                )
+        )
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/accountdata/RoomAccountDataDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/accountdata/RoomAccountDataDataSource.kt
index 0e4493846c17072ce5b90def8405c81148364df5..d96beed3f1891bfcad6e02b8fde386419697b56e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/accountdata/RoomAccountDataDataSource.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/accountdata/RoomAccountDataDataSource.kt
@@ -43,7 +43,7 @@ internal class RoomAccountDataDataSource @Inject constructor(@SessionDatabase pr
 
     fun getLiveAccountDataEvent(roomId: String, type: String): LiveData<Optional<RoomAccountDataEvent>> {
         return Transformations.map(getLiveAccountDataEvents(roomId, setOf(type))) {
-            it.firstOrNull()?.toOptional()
+            it.firstOrNull().toOptional()
         }
     }
 
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 177c98541c80a5df157972a236cc8413fdfc524f..77aadef6bd3560e7f705c57cd5c1afad076d2794 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
@@ -37,7 +37,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
-import org.matrix.android.sdk.api.session.room.model.message.OptionItem
 import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
 import org.matrix.android.sdk.api.session.room.send.SendService
 import org.matrix.android.sdk.api.session.room.send.SendState
@@ -98,7 +97,7 @@ internal class DefaultSendService @AssistedInject constructor(
                 .let { sendEvent(it) }
     }
 
-    override fun sendPoll(question: String, options: List<OptionItem>): Cancelable {
+    override fun sendPoll(question: String, options: List<String>): Cancelable {
         return localEchoEventFactory.createPollEvent(roomId, question, options)
                 .also { createLocalEcho(it) }
                 .let { sendEvent(it) }
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 8dd0c593872d394c4c748ebd539947207e361ab6..5cb96875186da936f0e6e2683cf6a74c149035ee 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
@@ -38,13 +38,14 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithF
 import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageFormat
 import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent
-import org.matrix.android.sdk.api.session.room.model.message.MessageOptionsContent
+import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
 import org.matrix.android.sdk.api.session.room.model.message.MessagePollResponseContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
 import org.matrix.android.sdk.api.session.room.model.message.MessageType
 import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
-import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_POLL
-import org.matrix.android.sdk.api.session.room.model.message.OptionItem
+import org.matrix.android.sdk.api.session.room.model.message.PollAnswer
+import org.matrix.android.sdk.api.session.room.model.message.PollCreationInfo
+import org.matrix.android.sdk.api.session.room.model.message.PollQuestion
 import org.matrix.android.sdk.api.session.room.model.message.ThumbnailInfo
 import org.matrix.android.sdk.api.session.room.model.message.VideoInfo
 import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent
@@ -138,24 +139,29 @@ internal class LocalEchoEventFactory @Inject constructor(
 
     fun createPollEvent(roomId: String,
                         question: String,
-                        options: List<OptionItem>): Event {
-        val compatLabel = buildString {
-            append("[Poll] ")
-            append(question)
-            options.forEach {
-                append("\n")
-                append(it.value)
-            }
-        }
-        return createMessageEvent(
-                roomId,
-                MessageOptionsContent(
-                        body = compatLabel,
-                        label = question,
-                        optionType = OPTION_TYPE_POLL,
-                        options = options.toList()
+                        options: List<String>): Event {
+        val content = MessagePollContent(
+                pollCreationInfo = PollCreationInfo(
+                        question = PollQuestion(
+                                question = question
+                        ),
+                        answers = options.mapIndexed { index, option ->
+                            PollAnswer(
+                                    id = index.toString(),
+                                    answer = option
+                            )
+                        }
                 )
         )
+        val localId = LocalEcho.createLocalEchoId()
+        return Event(
+                roomId = roomId,
+                originServerTs = dummyOriginServerTs(),
+                senderId = userId,
+                eventId = localId,
+                type = EventType.POLL_START,
+                content = content.toContent(),
+                unsignedData = UnsignedData(age = null, transactionId = localId))
     }
 
     fun createReplaceTextOfReply(roomId: String,
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutTask.kt
index 19f34746ab29157c967ced4ae4b69d94caad89da..7ac34e80e994a06d664bc6603daaca7f255c9670 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutTask.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/signout/SignOutTask.kt
@@ -43,6 +43,7 @@ internal class DefaultSignOutTask @Inject constructor(
     override suspend fun execute(params: SignOutTask.Params) {
         // It should be done even after a soft logout, to be sure the deviceId is deleted on the
         if (params.signOutFromHomeserver) {
+            cleanupSession.stopActiveTasks()
             Timber.d("SignOut: send request...")
             try {
                 executeRequest(globalErrorReceiver) {
@@ -67,6 +68,6 @@ internal class DefaultSignOutTask @Inject constructor(
                 .onFailure { Timber.w(it, "Unable to disconnect identity server") }
 
         Timber.d("SignOut: cleanup session...")
-        cleanupSession.handle()
+        cleanupSession.cleanup()
     }
 }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt
index 335f61962335c3d6965c1d3b2426bfc02baa189e..8fd969e373417e0923a44dfd33b16389303fd574 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt
@@ -24,11 +24,13 @@ import org.matrix.android.sdk.api.session.initsync.InitSyncStep
 import org.matrix.android.sdk.api.session.sync.model.GroupsSyncResponse
 import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse
 import org.matrix.android.sdk.api.session.sync.model.SyncResponse
+import org.matrix.android.sdk.internal.SessionManager
 import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
 import org.matrix.android.sdk.internal.di.SessionDatabase
 import org.matrix.android.sdk.internal.di.SessionId
 import org.matrix.android.sdk.internal.di.WorkManagerProvider
 import org.matrix.android.sdk.internal.session.SessionListeners
+import org.matrix.android.sdk.internal.session.dispatchTo
 import org.matrix.android.sdk.internal.session.group.GetGroupDataWorker
 import org.matrix.android.sdk.internal.session.initsync.ProgressReporter
 import org.matrix.android.sdk.internal.session.initsync.reportSubtask
@@ -51,6 +53,7 @@ private const val GET_GROUP_DATA_WORKER = "GET_GROUP_DATA_WORKER"
 internal class SyncResponseHandler @Inject constructor(
         @SessionDatabase private val monarchy: Monarchy,
         @SessionId private val sessionId: String,
+        private val sessionManager: SessionManager,
         private val sessionListeners: SessionListeners,
         private val workManagerProvider: WorkManagerProvider,
         private val roomSyncHandler: RoomSyncHandler,
@@ -158,8 +161,9 @@ internal class SyncResponseHandler @Inject constructor(
     }
 
     private fun dispatchInvitedRoom(roomsSyncResponse: RoomsSyncResponse) {
+        val session = sessionManager.getSessionComponent(sessionId)?.session()
         roomsSyncResponse.invite.keys.forEach { roomId ->
-            sessionListeners.dispatch { session, listener ->
+            session.dispatchTo(sessionListeners) { session, listener ->
                 listener.onNewInvitedRoom(session, roomId)
             }
         }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt
index e1150f2c47400ad977f39c0fda186fa72f3db96b..3faa0c9488a44d51ad861d984c81c5a6ff019b00 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt
@@ -160,6 +160,9 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
                 synchronized(lock) { lock.wait() }
                 Timber.tag(loggerTag.value).d("...retry")
             } else if (!isTokenValid) {
+                if (state == SyncState.Killing) {
+                    continue
+                }
                 Timber.tag(loggerTag.value).d("Token is invalid. Waiting...")
                 updateStateTo(SyncState.InvalidToken)
                 synchronized(lock) { lock.wait() }
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataDataSource.kt
index b36bdc80f889be918a36a413d87123a252428886..2c3d660333ec9dba8d0ed4bb9726a64580a3ade7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataDataSource.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/UserAccountDataDataSource.kt
@@ -41,7 +41,7 @@ internal class UserAccountDataDataSource @Inject constructor(@SessionDatabase pr
 
     fun getLiveAccountDataEvent(type: String): LiveData<Optional<UserAccountDataEvent>> {
         return Transformations.map(getLiveAccountDataEvents(setOf(type))) {
-            it.firstOrNull()?.toOptional()
+            it.firstOrNull().toOptional()
         }
     }
 
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 a12587ac56563515a9393a80d76d9fd722a3400a..3e977b31fb3173c7705fdfa61466fe34ba614d48 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
@@ -16,9 +16,8 @@
 
 package org.matrix.android.sdk.internal.util
 
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleObserver
-import androidx.lifecycle.OnLifecycleEvent
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
 import org.matrix.android.sdk.internal.di.MatrixScope
 import timber.log.Timber
 import javax.inject.Inject
@@ -27,13 +26,12 @@ import javax.inject.Inject
  * To be attached to ProcessLifecycleOwner lifecycle
  */
 @MatrixScope
-internal class BackgroundDetectionObserver @Inject constructor() : LifecycleObserver {
+internal class BackgroundDetectionObserver @Inject constructor() : DefaultLifecycleObserver {
 
     var isInBackground: Boolean = true
         private set
 
-    private
-    val listeners = LinkedHashSet<Listener>()
+    private val listeners = LinkedHashSet<Listener>()
 
     fun register(listener: Listener) {
         listeners.add(listener)
@@ -43,15 +41,13 @@ internal class BackgroundDetectionObserver @Inject constructor() : LifecycleObse
         listeners.remove(listener)
     }
 
-    @OnLifecycleEvent(Lifecycle.Event.ON_START)
-    fun onMoveToForeground() {
+    override fun onStart(owner: LifecycleOwner) {
         Timber.v("App returning to foreground…")
         isInBackground = false
         listeners.forEach { it.onMoveToForeground() }
     }
 
-    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
-    fun onMoveToBackground() {
+    override fun onStop(owner: LifecycleOwner) {
         Timber.v("App going to background…")
         isInBackground = true
         listeners.forEach { it.onMoveToBackground() }