From 52f6c3eda849d13a01c536d820d6d767831faa2c Mon Sep 17 00:00:00 2001 From: Benoit Marty <benoit@matrix.org> Date: Wed, 17 Nov 2021 16:18:48 +0100 Subject: [PATCH] Import v1.3.8 from Element Android --- dependencies.gradle | 36 ++--- matrix-sdk-android/build.gradle | 12 +- .../org/matrix/android/sdk/api/MatrixUrls.kt | 11 ++ .../android/sdk/api/failure/Extensions.kt | 18 +++ .../android/sdk/api/pushrules/PushEvents.kt | 26 ++++ .../sdk/api/pushrules/PushRuleService.kt | 6 +- .../matrix/android/sdk/api/session/Session.kt | 6 + .../api/session/content/ContentUrlResolver.kt | 16 ++ .../contentscanner/ContentScannerError.kt | 49 ++++++ .../contentscanner/ContentScannerService.kt | 40 +++++ .../api/session/contentscanner/ScanState.kt | 30 ++++ .../sdk/api/session/events/model/EventType.kt | 3 + .../room/model/message/MessagePollContent.kt | 25 +++ .../session/room/model/message/PollAnswer.kt | 26 ++++ .../room/model/message/PollCreationInfo.kt | 28 ++++ .../room/model/message/PollQuestion.kt | 25 +++ .../sdk/api/session/room/send/SendService.kt | 5 +- .../session/room/timeline/TimelineEvent.kt | 38 ++--- .../android/sdk/api/util/ContentUtils.kt | 13 ++ .../android/sdk/internal/SessionManager.kt | 5 + .../internal/auth/login/DefaultLoginWizard.kt | 3 +- .../sdk/internal/crypto/model/MXKey.kt | 16 +- .../android/sdk/internal/di/DbQualifiers.kt | 4 + .../sdk/internal/network/NetworkConstants.kt | 3 + .../internal/session/DefaultFileService.kt | 24 ++- .../sdk/internal/session/DefaultSession.kt | 20 ++- .../sdk/internal/session/SessionComponent.kt | 2 + .../sdk/internal/session/SessionListeners.kt | 19 +-- .../session/account/DeactivateAccountTask.kt | 4 +- .../session/cleanup/CleanupSession.kt | 20 ++- .../content/DefaultContentUrlResolver.kt | 57 ++++++- .../contentscanner/ContentScannerApi.kt | 45 ++++++ .../ContentScannerApiProvider.kt | 25 +++ .../contentscanner/ContentScannerModule.kt | 84 ++++++++++ .../DefaultContentScannerService.kt | 131 ++++++++++++++++ .../DisabledContentScannerService.kt | 66 ++++++++ .../contentscanner/ScanEncryptorUtils.kt | 63 ++++++++ .../data/ContentScannerStore.kt | 40 +++++ .../db/ContentScanResultEntity.kt | 55 +++++++ .../db/ContentScannerEntityQueries.kt | 41 +++++ .../db/ContentScannerInfoEntity.kt | 27 ++++ .../db/ContentScannerRealmModule.kt | 29 ++++ .../db/RealmContentScannerStore.kt | 143 ++++++++++++++++++ .../contentscanner/model/DownloadBody.kt | 40 +++++ .../contentscanner/model/ScanResponse.kt | 33 ++++ .../model/ServerPublicKeyResponse.kt | 26 ++++ .../tasks/DownloadEncryptedTask.kt | 51 +++++++ .../tasks/GetServerPublicKeyTask.kt | 38 +++++ .../contentscanner/tasks/ScanEncryptedTask.kt | 67 ++++++++ .../contentscanner/tasks/ScanMediaTask.kt | 77 ++++++++++ .../notification/DefaultPushRuleService.kt | 86 ++--------- .../notification/ProcessEventForPushTask.kt | 26 ++-- .../accountdata/RoomAccountDataDataSource.kt | 2 +- .../session/room/send/DefaultSendService.kt | 3 +- .../room/send/LocalEchoEventFactory.kt | 44 +++--- .../internal/session/signout/SignOutTask.kt | 3 +- .../session/sync/SyncResponseHandler.kt | 6 +- .../internal/session/sync/job/SyncThread.kt | 3 + .../accountdata/UserAccountDataDataSource.kt | 2 +- .../util/BackgroundDetectionObserver.kt | 16 +- 60 files changed, 1645 insertions(+), 217 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/PushEvents.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/contentscanner/ContentScannerError.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/contentscanner/ContentScannerService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/contentscanner/ScanState.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessagePollContent.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollAnswer.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollQuestion.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/ContentScannerApi.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/ContentScannerApiProvider.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/ContentScannerModule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/DefaultContentScannerService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/DisabledContentScannerService.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/ScanEncryptorUtils.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/data/ContentScannerStore.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScanResultEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScannerEntityQueries.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScannerInfoEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/ContentScannerRealmModule.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/db/RealmContentScannerStore.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/model/DownloadBody.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/model/ScanResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/model/ServerPublicKeyResponse.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/DownloadEncryptedTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/GetServerPublicKeyTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/ScanEncryptedTask.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/contentscanner/tasks/ScanMediaTask.kt diff --git a/dependencies.gradle b/dependencies.gradle index 47090d47..8cc7b3b2 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 98724c5b..a96ca6c8 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 dc4e0f15..4a41eaec 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 b2035bb2..13a26c89 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 00000000..466e345c --- /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 1d0acf38..88268f0f 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 dfe43aed..3f817ec4 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 36c471bb..3dd096e1 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 00000000..cef5d41f --- /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 00000000..1dd7bab0 --- /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 00000000..da209080 --- /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 d0ce5507..a39ca5b4 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 00000000..ef2fd186 --- /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 00000000..8f5ff53c --- /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 00000000..e652514b --- /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 00000000..76025f74 --- /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 6ae42de9..a2b38b66 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 4a646247..86cb10bf 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 1a00b85f..e453cb2d 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 c746ad86..934d61de 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 b72cff3c..05839511 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 f71c5079..9f425eee 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 49e155ad..8b78cd0d 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 361a306d..1ab10421 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 46c59678..14dfc097 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 c5246261..c07ff48c 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 bc8a7075..76e5d84e 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 d5c661b1..756b9cef 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 1a8e80ab..752856b9 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 e8d3eb1a..c42141a0 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 5c8cf99d..660ab872 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 00000000..46f17058 --- /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 00000000..d8548bb2 --- /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 00000000..7ea74225 --- /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 00000000..4ecb3376 --- /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 00000000..9087c715 --- /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 00000000..8fc84a48 --- /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 00000000..5cfe851a --- /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 00000000..0ffff441 --- /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 00000000..b47be235 --- /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 00000000..d1910de3 --- /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 00000000..bb53140a --- /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 00000000..947a66c8 --- /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 00000000..5bac96a0 --- /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 00000000..f783fe0a --- /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 00000000..2e97a85b --- /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 00000000..f92c869c --- /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 00000000..41c2ec9c --- /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 00000000..dab9b553 --- /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 00000000..505eb709 --- /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 65974151..3e821b89 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 3c74888e..0ac21b55 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 0e449384..d96beed3 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 177c9854..77aadef6 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 8dd0c593..5cb96875 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 19f34746..7ac34e80 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 335f6196..8fd969e3 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 e1150f2c..3faa0c94 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 b36bdc80..2c3d6603 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 a12587ac..3e977b31 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() } -- GitLab