diff --git a/CHANGES.md b/CHANGES.md index aa2ab532b4f7e7e2024c20991192df88dabcb6bb..b6fc44b4028513d1bd4bb0323dfdfd9380ddcb7c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,27 @@ Please also refer to the Changelog of Element Android: https://github.com/vector-im/element-android/blob/main/CHANGES.md +Changes in Matrix-SDK v1.5.4 (2022-10-25) +======================================= + +Imported from Element 1.5.4. (https://github.com/vector-im/element-android/releases/tag/v1.5.4) + +Target API 33. + +SDK API changes âš ï¸ +------------------ +- Stop using `original_event` field from `/relations` endpoint ([#7282](https://github.com/vector-im/element-android/issues/7282)) +- Add `formattedText` or similar optional parameters in several methods: +* RelationService: + * editTextMessage + * editReply + * replyToMessage +* SendService: + * sendQuotedTextMessage + This allows us to send any HTML formatted text message without needing to rely on automatic Markdown > HTML translation. All these new parameters have a `null` value by default, so previous calls to these API methods remain compatible. ([#7288](https://github.com/vector-im/element-android/issues/7288)) +- Add support for `m.login.token` auth during QR code based sign in ([#7358](https://github.com/vector-im/element-android/issues/7358)) +- Allow getting the formatted or plain text body of a message for the fun `TimelineEvent.getTextEditableContent()`. ([#7359](https://github.com/vector-im/element-android/issues/7359)) + + Changes in Matrix-SDK v1.5.2 (2022-10-05) ======================================= diff --git a/dependencies.gradle b/dependencies.gradle index 3bf3ab746d8753ec420b11dd0cd6cd741679f9d4..f081e0a87439a6b2b33a9ba8c020f310fc198b3a 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,35 +1,34 @@ ext.versions = [ 'minSdk' : 21, - 'compileSdk' : 32, - 'targetSdk' : 32, + 'compileSdk' : 33, + 'targetSdk' : 33, 'sourceCompat' : JavaVersion.VERSION_11, 'targetCompat' : JavaVersion.VERSION_11, ] -def gradle = "7.2.2" +def gradle = "7.3.1" // Ref: https://kotlinlang.org/releases.html def kotlin = "1.7.20" def kotlinCoroutines = "1.6.4" def dagger = "2.44" def appDistribution = "16.0.0-beta04" def retrofit = "2.9.0" -def arrow = "0.8.2" def markwon = "4.6.2" def moshi = "1.14.0" def lifecycle = "2.5.1" def flowBinding = "1.2.0" -def flipper = "0.164.0" -def epoxy = "4.6.2" -def mavericks = "2.7.0" -def glide = "4.14.1" +def flipper = "0.171.1" +def epoxy = "5.0.0" +def mavericks = "3.0.1" +def glide = "4.14.2" def bigImageViewer = "1.8.1" def jjwt = "0.11.5" // Temporary version to unblock #6929. Once 0.16.0 is released we should use it, and revert // the whole commit which set version 0.16.0-SNAPSHOT def vanniktechEmoji = "0.16.0-SNAPSHOT" -def sentry = "6.4.1" +def sentry = "6.4.3" def fragment = "1.5.3" @@ -51,12 +50,12 @@ ext.libs = [ 'coroutinesTest' : "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutines" ], androidx : [ - 'activity' : "androidx.activity:activity:1.5.1", + 'activity' : "androidx.activity:activity-ktx:1.6.0", 'appCompat' : "androidx.appcompat:appcompat:1.5.1", 'biometric' : "androidx.biometric:biometric:1.1.0", - 'core' : "androidx.core:core-ktx:1.8.0", + 'core' : "androidx.core:core-ktx:1.9.0", 'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1", - 'exifinterface' : "androidx.exifinterface:exifinterface:1.3.3", + 'exifinterface' : "androidx.exifinterface:exifinterface:1.3.4", 'fragmentKtx' : "androidx.fragment:fragment-ktx:$fragment", 'fragmentTesting' : "androidx.fragment:fragment-testing:$fragment", 'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.4", @@ -87,7 +86,7 @@ ext.libs = [ 'appdistributionApi' : "com.google.firebase:firebase-appdistribution-api-ktx:$appDistribution", 'appdistribution' : "com.google.firebase:firebase-appdistribution:$appDistribution", // Phone number https://github.com/google/libphonenumber - 'phonenumber' : "com.googlecode.libphonenumber:libphonenumber:8.12.56" + 'phonenumber' : "com.googlecode.libphonenumber:libphonenumber:8.12.57" ], dagger : [ 'dagger' : "com.google.dagger:dagger:$dagger", @@ -102,6 +101,7 @@ ext.libs = [ ], element : [ 'opusencoder' : "io.element.android:opusencoder:1.1.0", + 'wysiwyg' : "io.element.android:wysiwyg:0.2.1" ], squareup : [ 'moshi' : "com.squareup.moshi:moshi:$moshi", @@ -114,10 +114,6 @@ ext.libs = [ rx : [ 'rxKotlin' : "io.reactivex.rxjava2:rxkotlin:2.4.0" ], - arrow : [ - 'core' : "io.arrow-kt:arrow-core:$arrow", - 'instances' : "io.arrow-kt:arrow-instances-core:$arrow" - ], markwon : [ 'core' : "io.noties.markwon:core:$markwon", 'extLatex' : "io.noties.markwon:ext-latex:$markwon", diff --git a/dependencies_groups.gradle b/dependencies_groups.gradle index cdab6172d1068cf9c58980673baae54fadab9b9a..68de2c1581272ad0776c34f7da70acf83e99e804 100644 --- a/dependencies_groups.gradle +++ b/dependencies_groups.gradle @@ -84,6 +84,7 @@ ext.groups = [ 'com.google', 'com.google.android', 'com.google.api.grpc', + 'com.google.auto', 'com.google.auto.service', 'com.google.auto.value', 'com.google.code.findbugs', @@ -101,6 +102,7 @@ ext.groups = [ 'com.googlecode.json-simple', 'com.googlecode.libphonenumber', 'com.ibm.icu', + 'com.intellij', 'com.jakewharton.android.repackaged', 'com.jakewharton.timber', 'com.kgurgul.flipper', @@ -132,7 +134,6 @@ ext.groups = [ 'commons-io', 'commons-logging', 'info.picocli', - 'io.arrow-kt', 'io.element.android', 'io.github.davidburstrom.contester', 'io.github.detekt.sarif4k', @@ -146,6 +147,7 @@ ext.groups = [ 'io.netty', 'io.noties.markwon', 'io.opencensus', + 'io.perfmark', 'io.reactivex.rxjava2', 'io.realm', 'io.sentry', @@ -176,6 +178,7 @@ ext.groups = [ 'org.apache.httpcomponents', 'org.apache.sanselan', 'org.bouncycastle', + 'org.ccil.cowan.tagsoup', 'org.checkerframework', 'org.codehaus', 'org.codehaus.groovy', diff --git a/gradle.properties b/gradle.properties index f443623387d48b642dd9492286a6d64583086465..bc760114f3338ee003085f33b5006315ef45693f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,7 +26,7 @@ vector.httpLogLevel=NONE # Ref: https://github.com/vanniktech/gradle-maven-publish-plugin GROUP=org.matrix.android POM_ARTIFACT_ID=matrix-android-sdk2 -VERSION_NAME=1.5.2 +VERSION_NAME=1.5.4 POM_PACKAGING=aar diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 9836e1c8bb285132b6c9b3e298fbba3afef654a5..99a8a2c914bc87713eb625e71c76638186e21957 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -45,6 +45,8 @@ dokkaHtml { } android { + namespace "org.matrix.android.sdk" + testOptions.unitTests.includeAndroidResources = true compileSdk versions.compileSdk diff --git a/matrix-sdk-android/src/main/AndroidManifest.xml b/matrix-sdk-android/src/main/AndroidManifest.xml index de0731422c11426399db25c4e274d733ba8096e5..7f940d4e1c7c2d4e430140318f0f8d639814401f 100644 --- a/matrix-sdk-android/src/main/AndroidManifest.xml +++ b/matrix-sdk-android/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" - package="org.matrix.android.sdk"> + xmlns:tools="http://schemas.android.com/tools"> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/account/LocalNotificationSettingsContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/account/LocalNotificationSettingsContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..2a95ccce7a743114ad4b81517e6955c479c0a303 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/account/LocalNotificationSettingsContent.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.account + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class LocalNotificationSettingsContent( + @Json(name = "is_silenced") val isSilenced: Boolean = false +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt index 5ae70e1978c0e5bc9398560a12b81123b6a369ca..252c33a8c4cc8957ee23be81005896c93a76a8e8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt @@ -124,4 +124,24 @@ interface AuthenticationService { initialDeviceName: String, deviceId: String? = null ): Session + + /** + * @param homeServerConnectionConfig the information about the homeserver and other configuration + * Return true if qr code login is supported by the server, false otherwise. + */ + suspend fun isQrLoginSupported(homeServerConnectionConfig: HomeServerConnectionConfig): Boolean + + /** + * Authenticate using m.login.token method during sign in with QR code. + * @param homeServerConnectionConfig the information about the homeserver and other configuration + * @param loginToken the m.login.token + * @param initialDeviceName the initial device name + * @param deviceId the device id, optional. If not provided or null, the server will generate one. + */ + suspend fun loginUsingQrLoginToken( + homeServerConnectionConfig: HomeServerConnectionConfig, + loginToken: String, + initialDeviceName: String? = null, + deviceId: String? = null + ): Session } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/LoginType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/LoginType.kt index 627a8256799d455b80a8afa75b9bc3485b334289..991b7b654d5e2e0f443acc5e14d6bdc949c11f66 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/LoginType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/LoginType.kt @@ -22,7 +22,8 @@ enum class LoginType { UNSUPPORTED, CUSTOM, DIRECT, - UNKNOWN; + UNKNOWN, + QR; companion object { @@ -32,6 +33,7 @@ enum class LoginType { UNSUPPORTED.name -> UNSUPPORTED CUSTOM.name -> CUSTOM DIRECT.name -> DIRECT + QR.name -> QR else -> UNKNOWN } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/UserAccountDataTypes.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/UserAccountDataTypes.kt index 91167d896f0251e3d6bbafb6181dcd7a45a11fa6..0edd824c26818b585c129117a2488eab435cce5e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/UserAccountDataTypes.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/accountdata/UserAccountDataTypes.kt @@ -28,4 +28,5 @@ object UserAccountDataTypes { const val TYPE_IDENTITY_SERVER = "m.identity_server" const val TYPE_ACCEPTED_TERMS = "m.accepted_terms" const val TYPE_OVERRIDE_COLORS = "im.vector.setting.override_colors" + const val TYPE_LOCAL_NOTIFICATION_SETTINGS = "org.matrix.msc3890.local_notification_settings." } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt index f5d2c0d9a0ef6592e932fd34c5f6ba2dd321d76b..71daf4cc4f2fada741fe69d3882b1630b918f53b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -31,6 +31,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent import org.matrix.android.sdk.api.session.room.model.message.MessageType +import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent import org.matrix.android.sdk.api.session.room.model.relation.shouldRenderInThread import org.matrix.android.sdk.api.session.room.send.SendState @@ -357,6 +358,10 @@ fun Event.isAudioMessage(): Boolean { } } +fun Event.isVoiceMessage(): Boolean { + return this.asMessageAudioEvent()?.content?.voiceMessageIndicator != null +} + fun Event.isFileMessage(): Boolean { return when (getMsgType()) { MessageType.MSGTYPE_FILE -> true diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt index b5d6d891e400a3f18e132c85dfbc9683efd8c6bb..8c14ca892aae1366229f69bc75f845664e8df44e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/homeserver/HomeServerCapabilities.kt @@ -59,7 +59,12 @@ data class HomeServerCapabilities( /** * True if the home server supports controlling the logout of all devices when changing password. */ - val canControlLogoutDevices: Boolean = false + val canControlLogoutDevices: Boolean = false, + + /** + * True if the home server supports login via qr code, false otherwise. + */ + val canLoginWithQrCode: Boolean = false, ) { enum class RoomCapabilitySupport { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/HttpPusher.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/HttpPusher.kt index 1ae23e2b7015c7dd58163ab0dad7a67fc489df37..1258c5c02f77faa338c4b4203e70e7fec60fd2fa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/HttpPusher.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/HttpPusher.kt @@ -58,6 +58,16 @@ data class HttpPusher( */ val url: String, + /** + * Whether the pusher should actively create push notifications. + */ + val enabled: Boolean, + + /** + * The device ID of the session that registered the pusher. + */ + val deviceId: String, + /** * If true, the homeserver should add another pusher with the given pushkey and App ID in addition * to any others with different user IDs. Otherwise, the homeserver must remove any other pushers diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/Pusher.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/Pusher.kt index b85ab32b2107f1e3fef0b7d4c510de3b2b156391..92ac6c483b89f9168a8ea19833d4c638990b907d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/Pusher.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/Pusher.kt @@ -24,8 +24,9 @@ data class Pusher( val profileTag: String? = null, val lang: String?, val data: PusherData, - - val state: PusherState + val enabled: Boolean, + val deviceId: String?, + val state: PusherState, ) { companion object { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt index d7958ea3cdd0f6e44c1ab00577687fb3efa836ed..6a27f7af613059bc95f223b5b076e32adc3e0c74 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/pushers/PushersService.kt @@ -67,6 +67,14 @@ interface PushersService { append: Boolean = true ) + /** + * Enables or disables a registered pusher. + * + * @param pusher The pusher being toggled + * @param enable Whether the pusher should be enabled or disabled + */ + suspend fun togglePusher(pusher: Pusher, enable: Boolean) + /** * Directly ask the push gateway to send a push to this device. * If successful, the push gateway has accepted the request. In this case, the app should receive a Push with the provided eventId. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioEvent.kt new file mode 100644 index 0000000000000000000000000000000000000000..38ced8f385fd6c5a1effed89f61e84dcb21121ca --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageAudioEvent.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.room.model.message + +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel + +/** + * [Event] wrapper for [EventType.MESSAGE] event type. + * Provides additional fields and functions related to this event type. + */ +@JvmInline +value class MessageAudioEvent(val root: Event) { + + /** + * The mapped [MessageAudioContent] model of the event content. + */ + val content: MessageAudioContent + get() = root.getClearContent().toModel<MessageContent>() as MessageAudioContent + + init { + require(tryOrNull { content } != null) + } +} + +/** + * Map a [EventType.MESSAGE] event to a [MessageAudioEvent]. + */ +fun Event.asMessageAudioEvent() = if (getClearType() == EventType.MESSAGE) { + tryOrNull { MessageAudioEvent(this) } +} else null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt index b12d9ed6c825145c7d029e816be8ba4bdc8ce999..e97a5be3037d2f8978633fcc43a1befd4b181869 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageType.kt @@ -43,4 +43,7 @@ object MessageType { // Fake message types for live location events to be able to inherit them from MessageContent const val MSGTYPE_BEACON_INFO = "org.matrix.android.sdk.beacon.info" const val MSGTYPE_BEACON_LOCATION_DATA = "org.matrix.android.sdk.beacon.location.data" + + // Fake message types for voice broadcast events to be able to inherit them from MessageContent + const val MSGTYPE_VOICE_BROADCAST_INFO = "io.element.voicebroadcast.info" } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt index d34ea3c7d3658780e9b176d622957c9e7f2c1950..e7fcabf3867e7ec4e7b0c536544fa708caad5e68 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/relation/RelationService.kt @@ -91,7 +91,8 @@ interface RelationService { * Edit a text message body. Limited to "m.text" contentType. * @param targetEvent The event to edit * @param msgType the message type - * @param newBodyText The edited body + * @param newBodyText The edited body in plain text + * @param newFormattedBodyText The edited body with format * @param newBodyAutoMarkdown true to parse markdown on the new body * @param compatibilityBodyText The text that will appear on clients that don't support yet edition */ @@ -99,6 +100,7 @@ interface RelationService { targetEvent: TimelineEvent, msgType: String, newBodyText: CharSequence, + newFormattedBodyText: CharSequence? = null, newBodyAutoMarkdown: Boolean, compatibilityBodyText: String = "* $newBodyText" ): Cancelable @@ -108,13 +110,15 @@ interface RelationService { * This method will take the new body (stripped from fallbacks) and re-add them before sending. * @param replyToEdit The event to edit * @param originalTimelineEvent the message that this reply (being edited) is relating to - * @param newBodyText The edited body (stripped from in reply to content) + * @param newBodyText The plain text edited body (stripped from in reply to content) + * @param newFormattedBodyText The formatted edited body (stripped from in reply to content) * @param compatibilityBodyText The text that will appear on clients that don't support yet edition */ fun editReply( replyToEdit: TimelineEvent, originalTimelineEvent: TimelineEvent, newBodyText: String, + newFormattedBodyText: String? = null, compatibilityBodyText: String = "* $newBodyText" ): Cancelable @@ -133,6 +137,7 @@ interface RelationService { * by the sdk into pills. * @param eventReplied the event referenced by the reply * @param replyText the reply text + * @param replyFormattedText the reply text, formatted * @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present * @param showInThread If true, relation will be added to the reply in order to be visible from within threads * @param rootThreadEventId If show in thread is true then we need the rootThreadEventId to generate the relation @@ -140,6 +145,7 @@ interface RelationService { fun replyToMessage( eventReplied: TimelineEvent, replyText: CharSequence, + replyFormattedText: CharSequence? = null, autoMarkdown: Boolean = false, showInThread: Boolean = false, rootThreadEventId: 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 9cf062356f3e132159307746e4681d69d23cbf68..6a6fadc95a7d3e0f61b98e9d44a04f06113e23a5 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 @@ -21,6 +21,7 @@ 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.PollType +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.util.Cancelable @@ -44,28 +45,49 @@ interface SendService { * @param text the text message to send * @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE * @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present + * @param additionalContent additional content to put in the event content * @return a [Cancelable] */ - fun sendTextMessage(text: CharSequence, msgType: String = MessageType.MSGTYPE_TEXT, autoMarkdown: Boolean = false): Cancelable + fun sendTextMessage( + text: CharSequence, + msgType: String = MessageType.MSGTYPE_TEXT, + autoMarkdown: Boolean = false, + additionalContent: Content? = null, + ): Cancelable /** * Method to send a text message with a formatted body. * @param text the text message to send * @param formattedText The formatted body using MessageType#FORMAT_MATRIX_HTML * @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE + * @param additionalContent additional content to put in the event content * @return a [Cancelable] */ - fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String = MessageType.MSGTYPE_TEXT): Cancelable + fun sendFormattedTextMessage( + text: String, + formattedText: String, + msgType: String = MessageType.MSGTYPE_TEXT, + additionalContent: Content? = null, + ): Cancelable /** * Method to quote an events content. * @param quotedEvent The event to which we will quote it's content. - * @param text the text message to send + * @param text the plain text message to send + * @param formattedText the formatted text message to send * @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present * @param rootThreadEventId when this param is not null, the message will be sent in this specific thread + * @param additionalContent additional content to put in the event content * @return a [Cancelable] */ - fun sendQuotedTextMessage(quotedEvent: TimelineEvent, text: String, autoMarkdown: Boolean, rootThreadEventId: String? = null): Cancelable + fun sendQuotedTextMessage( + quotedEvent: TimelineEvent, + text: String, + formattedText: String? = null, + autoMarkdown: Boolean, + rootThreadEventId: String? = null, + additionalContent: Content? = null, + ): Cancelable /** * Method to send a media asynchronously. @@ -74,13 +96,17 @@ interface SendService { * @param roomIds set of roomIds to where the media will be sent. The current roomId will be add to this set if not present. * It can be useful to send media to multiple room. It's safe to include the current roomId in this set * @param rootThreadEventId when this param is not null, the Media will be sent in this specific thread + * @param relatesTo add a relation content to the media event + * @param additionalContent additional content to put in the event content * @return a [Cancelable] */ fun sendMedia( attachment: ContentAttachmentData, compressBeforeSending: Boolean, roomIds: Set<String>, - rootThreadEventId: String? = null + rootThreadEventId: String? = null, + relatesTo: RelationDefaultContent? = null, + additionalContent: Content? = null, ): Cancelable /** @@ -90,13 +116,15 @@ interface SendService { * @param roomIds set of roomIds to where the media will be sent. The current roomId will be add to this set if not present. * It can be useful to send media to multiple room. It's safe to include the current roomId in this set * @param rootThreadEventId when this param is not null, all the Media will be sent in this specific thread + * @param additionalContent additional content to put in the event content * @return a [Cancelable] */ fun sendMedias( attachments: List<ContentAttachmentData>, compressBeforeSending: Boolean, roomIds: Set<String>, - rootThreadEventId: String? = null + rootThreadEventId: String? = null, + additionalContent: Content? = null, ): Cancelable /** @@ -104,31 +132,35 @@ interface SendService { * @param pollType indicates open or closed polls * @param question the question * @param options list of options + * @param additionalContent additional content to put in the event content * @return a [Cancelable] */ - fun sendPoll(pollType: PollType, question: String, options: List<String>): Cancelable + fun sendPoll(pollType: PollType, question: String, options: List<String>, additionalContent: Content? = null): Cancelable /** * Method to send a poll response. * @param pollEventId the poll currently replied to * @param answerId The id of the answer + * @param additionalContent additional content to put in the event content * @return a [Cancelable] */ - fun voteToPoll(pollEventId: String, answerId: String): Cancelable + fun voteToPoll(pollEventId: String, answerId: String, additionalContent: Content? = null): Cancelable /** * End a poll in the room. * @param pollEventId event id of the poll + * @param additionalContent additional content to put in the event content * @return a [Cancelable] */ - fun endPoll(pollEventId: String): Cancelable + fun endPoll(pollEventId: String, additionalContent: Content? = null): Cancelable /** * Redact (delete) the given event. * @param event The event to redact * @param reason Optional reason string + * @param additionalContent additional content to put in the event content */ - fun redactEvent(event: Event, reason: String?): Cancelable + fun redactEvent(event: Event, reason: String?, additionalContent: Content? = null): Cancelable /** * Schedule this message to be resent. 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 d391abf1e61528cc3768335b0d36aab8a550489b..223acd1b9c6bff98e50ba829053d3a39ed7ffe69 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 @@ -33,6 +33,7 @@ import org.matrix.android.sdk.api.session.room.model.ReadReceipt import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageContentWithFormattedBody import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent @@ -180,8 +181,13 @@ fun TimelineEvent.isRootThread(): Boolean { /** * Get the latest message body, after a possible edition, stripping the reply prefix if necessary. */ -fun TimelineEvent.getTextEditableContent(): String { - val lastContentBody = getLastMessageContent()?.body ?: return "" +fun TimelineEvent.getTextEditableContent(formatted: Boolean): String { + val lastMessageContent = getLastMessageContent() + val lastContentBody = if (formatted && lastMessageContent is MessageContentWithFormattedBody) { + lastMessageContent.formattedBody + } else { + lastMessageContent?.body + } ?: return "" return if (isReply()) { extractUsefulTextFromReply(lastContentBody) } else { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt index 46433f387d36b2297d3571c687feadb122a9e2c0..aa9afd5c8c141e1867ba80c21b1090680dd684fe 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineService.kt @@ -55,4 +55,9 @@ interface TimelineService { * Returns a snapshot list of TimelineEvent with EventType.MESSAGE and MessageType.MSGTYPE_IMAGE or MessageType.MSGTYPE_VIDEO. */ fun getAttachmentMessages(): List<TimelineEvent> + + /** + * Returns a snapshot list of TimelineEvent with a content relation of the given type to the given eventId. + */ + fun getTimelineEventsRelatedTo(relationType: String, eventId: String): List<TimelineEvent> } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Compat.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Compat.kt new file mode 100644 index 0000000000000000000000000000000000000000..b685c4971371ec594c1b306ffa003a7da591a346 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Compat.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.util + +import android.content.pm.ApplicationInfo +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.os.Build + +fun PackageManager.getApplicationInfoCompat(packageName: String, flags: Int): ApplicationInfo { + return when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getApplicationInfo( + packageName, + PackageManager.ApplicationInfoFlags.of(flags.toLong()) + ) + else -> @Suppress("DEPRECATION") getApplicationInfo(packageName, flags) + } +} + +fun PackageManager.getPackageInfoCompat(packageName: String, flags: Int): PackageInfo { + return when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> getPackageInfo( + packageName, + PackageManager.PackageInfoFlags.of(flags.toLong()) + ) + else -> @Suppress("DEPRECATION") getPackageInfo(packageName, flags) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Optional.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Optional.kt index 42724746c016d229b450efa490cd25b86376c4eb..2da6d4cbf8659bf7ddcf0820a874ac7f97bd4f63 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Optional.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/Optional.kt @@ -15,15 +15,13 @@ */ package org.matrix.android.sdk.api.util -data class Optional<T : Any> constructor(private val value: T?) { +data class Optional<T : Any>(private val value: T?) { - fun get(): T { - return value!! - } + fun get(): T = value!! - fun getOrNull(): T? { - return value - } + fun orNull(): T? = value + + fun getOrNull(): T? = value fun <U : Any> map(fn: (T) -> U?): Optional<U> { return if (value == null) { @@ -33,23 +31,19 @@ data class Optional<T : Any> constructor(private val value: T?) { } } - fun getOrElse(fn: () -> T): T { + fun orElse(fn: () -> T): T { return value ?: fn() } - fun hasValue(): Boolean { - return value != null - } + fun hasValue(): Boolean = value != null companion object { - fun <T : Any> from(value: T?): Optional<T> { - return Optional(value) - } + fun <T : Any> from(value: T?): Optional<T> = Optional(value) - fun <T : Any> empty(): Optional<T> { - return Optional(null) - } + fun <T : Any> empty(): Optional<T> = Optional(null) } } +fun <T : Any> T?.toOption() = Optional(this) + fun <T : Any> T?.toOptional() = Optional(this) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthModule.kt index 463692e574cf767a9837150a643c9ffa9094e4fb..b1f65194f1e16250531073ed9de81eda7dbd725f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/AuthModule.kt @@ -29,7 +29,9 @@ import org.matrix.android.sdk.internal.auth.db.AuthRealmModule import org.matrix.android.sdk.internal.auth.db.RealmPendingSessionStore import org.matrix.android.sdk.internal.auth.db.RealmSessionParamsStore import org.matrix.android.sdk.internal.auth.login.DefaultDirectLoginTask +import org.matrix.android.sdk.internal.auth.login.DefaultQrLoginTokenTask import org.matrix.android.sdk.internal.auth.login.DirectLoginTask +import org.matrix.android.sdk.internal.auth.login.QrLoginTokenTask import org.matrix.android.sdk.internal.database.RealmKeysUtils import org.matrix.android.sdk.internal.di.AuthDatabase import org.matrix.android.sdk.internal.legacy.DefaultLegacySessionImporter @@ -94,4 +96,7 @@ internal abstract class AuthModule { @Binds abstract fun bindHomeServerHistoryService(service: DefaultHomeServerHistoryService): HomeServerHistoryService + + @Binds + abstract fun bindQrLoginTokenTask(task: DefaultQrLoginTokenTask): QrLoginTokenTask } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt index 446f9318479fc16d0898f84d23b6c034b68645e3..5449c0a735ff068dc796fbab1d9bbce5e860ddbe 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt @@ -30,6 +30,7 @@ import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.auth.login.LoginWizard import org.matrix.android.sdk.api.auth.registration.RegistrationWizard import org.matrix.android.sdk.api.auth.wellknown.WellknownResult +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.MatrixIdFailure import org.matrix.android.sdk.api.session.Session @@ -39,9 +40,11 @@ import org.matrix.android.sdk.internal.auth.data.WebClientConfig import org.matrix.android.sdk.internal.auth.db.PendingSessionData import org.matrix.android.sdk.internal.auth.login.DefaultLoginWizard import org.matrix.android.sdk.internal.auth.login.DirectLoginTask +import org.matrix.android.sdk.internal.auth.login.QrLoginTokenTask import org.matrix.android.sdk.internal.auth.registration.DefaultRegistrationWizard import org.matrix.android.sdk.internal.auth.version.Versions import org.matrix.android.sdk.internal.auth.version.doesServerSupportLogoutDevices +import org.matrix.android.sdk.internal.auth.version.doesServerSupportQrCodeLogin import org.matrix.android.sdk.internal.auth.version.isLoginAndRegistrationSupportedBySdk import org.matrix.android.sdk.internal.auth.version.isSupportedBySdk import org.matrix.android.sdk.internal.di.Unauthenticated @@ -62,7 +65,8 @@ internal class DefaultAuthenticationService @Inject constructor( private val sessionCreator: SessionCreator, private val pendingSessionStore: PendingSessionStore, private val getWellknownTask: GetWellknownTask, - private val directLoginTask: DirectLoginTask + private val directLoginTask: DirectLoginTask, + private val qrLoginTokenTask: QrLoginTokenTask ) : AuthenticationService { private var pendingSessionData: PendingSessionData? = pendingSessionStore.getPendingSessionData() @@ -404,6 +408,36 @@ internal class DefaultAuthenticationService @Inject constructor( ) } + override suspend fun isQrLoginSupported(homeServerConnectionConfig: HomeServerConnectionConfig): Boolean { + val authAPI = buildAuthAPI(homeServerConnectionConfig) + val versions = runCatching { + executeRequest(null) { + authAPI.versions() + } + } + return if (versions.isSuccess) { + versions.getOrNull()?.doesServerSupportQrCodeLogin().orFalse() + } else { + false + } + } + + override suspend fun loginUsingQrLoginToken( + homeServerConnectionConfig: HomeServerConnectionConfig, + loginToken: String, + initialDeviceName: String?, + deviceId: String?, + ): Session { + return qrLoginTokenTask.execute( + QrLoginTokenTask.Params( + homeServerConnectionConfig = homeServerConnectionConfig, + loginToken = loginToken, + deviceName = initialDeviceName, + deviceId = deviceId + ) + ) + } + private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI { val retrofit = retrofitFactory.create(buildClient(homeServerConnectionConfig), homeServerConnectionConfig.homeServerUriBase.toString()) return retrofit.create(AuthAPI::class.java) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginParams.kt index ea8578ed8c5850efdb85be2bbbfe1fe25662a8dd..864675208371f80d89ca6089fb6cd25ad821719b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginParams.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginParams.kt @@ -18,4 +18,6 @@ package org.matrix.android.sdk.internal.auth.data internal interface LoginParams { val type: String + val deviceDisplayName: String? + val deviceId: String? } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/PasswordLoginParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/PasswordLoginParams.kt index 5f0a2298cb670acad891450f0a6a68adbea4bca7..062b2466e5549016613e08d4f9e889b4c4f01138 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/PasswordLoginParams.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/PasswordLoginParams.kt @@ -30,8 +30,8 @@ internal data class PasswordLoginParams( @Json(name = "identifier") val identifier: Map<String, String>, @Json(name = "password") val password: String, @Json(name = "type") override val type: String, - @Json(name = "initial_device_display_name") val deviceDisplayName: String?, - @Json(name = "device_id") val deviceId: String? + @Json(name = "initial_device_display_name") override val deviceDisplayName: String?, + @Json(name = "device_id") override val deviceId: String? ) : LoginParams { companion object { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/TokenLoginParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/TokenLoginParams.kt index 0c6fb45e78863f682c71c5fd9c06d145940291a6..52045a1d7a8699703f20d9d185b70979233b6b2d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/TokenLoginParams.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/TokenLoginParams.kt @@ -23,5 +23,7 @@ import org.matrix.android.sdk.api.auth.data.LoginFlowTypes @JsonClass(generateAdapter = true) internal data class TokenLoginParams( @Json(name = "type") override val type: String = LoginFlowTypes.TOKEN, - @Json(name = "token") val token: String + @Json(name = "token") val token: String, + @Json(name = "initial_device_display_name") override val deviceDisplayName: String? = null, + @Json(name = "device_id") override val deviceId: String? = null ) : LoginParams diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/QrLoginTokenTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/QrLoginTokenTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..477719f607ae8b9b70b280f4d9d4bd96e731cacf --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/QrLoginTokenTask.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.auth.login + +import dagger.Lazy +import okhttp3.OkHttpClient +import org.matrix.android.sdk.api.auth.LoginType +import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.internal.auth.AuthAPI +import org.matrix.android.sdk.internal.auth.SessionCreator +import org.matrix.android.sdk.internal.auth.data.TokenLoginParams +import org.matrix.android.sdk.internal.di.Unauthenticated +import org.matrix.android.sdk.internal.network.RetrofitFactory +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.network.httpclient.addSocketFactory +import org.matrix.android.sdk.internal.network.ssl.UnrecognizedCertificateException +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface QrLoginTokenTask : Task<QrLoginTokenTask.Params, Session> { + data class Params( + val homeServerConnectionConfig: HomeServerConnectionConfig, + val loginToken: String, + val deviceName: String?, + val deviceId: String? + ) +} + +internal class DefaultQrLoginTokenTask @Inject constructor( + @Unauthenticated + private val okHttpClient: Lazy<OkHttpClient>, + private val retrofitFactory: RetrofitFactory, + private val sessionCreator: SessionCreator, +) : QrLoginTokenTask { + + override suspend fun execute(params: QrLoginTokenTask.Params): Session { + val client = buildClient(params.homeServerConnectionConfig) + val homeServerUrl = params.homeServerConnectionConfig.homeServerUriBase.toString() + + val authAPI = retrofitFactory.create(client, homeServerUrl) + .create(AuthAPI::class.java) + + val loginParams = TokenLoginParams( + token = params.loginToken, + deviceDisplayName = params.deviceName, + deviceId = params.deviceId + ) + + val credentials = try { + executeRequest(null) { + authAPI.login(loginParams) + } + } catch (throwable: Throwable) { + throw when (throwable) { + is UnrecognizedCertificateException -> Failure.UnrecognizedCertificateFailure( + homeServerUrl, + throwable.fingerprint + ) + else -> throwable + } + } + + return sessionCreator.createSession(credentials, params.homeServerConnectionConfig, LoginType.QR) + } + + private fun buildClient(homeServerConnectionConfig: HomeServerConnectionConfig): OkHttpClient { + return okHttpClient.get() + .newBuilder() + .addSocketFactory(homeServerConnectionConfig) + .build() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt index 915b25134b2f0f687a7a443796db800f50c6e0d2..5e133fab9ccaa0c3e627eb4107f51f3a535a6cd1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/version/Versions.kt @@ -53,6 +53,7 @@ private const val FEATURE_ID_ACCESS_TOKEN = "m.id_access_token" private const val FEATURE_SEPARATE_ADD_AND_BIND = "m.separate_add_and_bind" private const val FEATURE_THREADS_MSC3440 = "org.matrix.msc3440" private const val FEATURE_THREADS_MSC3440_STABLE = "org.matrix.msc3440.stable" +private const val FEATURE_QR_CODE_LOGIN = "org.matrix.msc3882" /** * Return true if the SDK supports this homeserver version. @@ -78,6 +79,10 @@ internal fun Versions.doesServerSupportThreads(): Boolean { return unstableFeatures?.get(FEATURE_THREADS_MSC3440_STABLE) ?: false } +internal fun Versions.doesServerSupportQrCodeLogin(): Boolean { + return unstableFeatures?.get(FEATURE_QR_CODE_LOGIN) ?: false +} + /** * Return true if the server support the lazy loading of room members. * diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt index e97cf437c68836363a5e0d5ce2637d9703e740c3..1b52b79746e35d4293e6901d05fe03a1baa75cb4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt @@ -657,14 +657,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun saveMyDevicesInfo(info: List<DeviceInfo>) { - val entities = info.map { - MyDeviceLastSeenInfoEntity( - lastSeenTs = it.lastSeenTs, - lastSeenIp = it.lastSeenIp, - displayName = it.displayName, - deviceId = it.deviceId - ) - } + val entities = info.map { myDeviceLastSeenInfoEntityMapper.map(it) } doRealmTransactionAsync(realmConfiguration) { realm -> realm.where<MyDeviceLastSeenInfoEntity>().findAll().deleteAllFromRealm() entities.forEach { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt index de2b74308dd278883fe4effe3682ba10d150d74a..9129453c8a9dead961786a827140f21a21329bfc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStoreMigration.kt @@ -36,6 +36,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo017 import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo018 import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo019 +import org.matrix.android.sdk.internal.crypto.store.db.migration.MigrateCryptoTo020 import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import org.matrix.android.sdk.internal.util.time.Clock import javax.inject.Inject @@ -50,7 +51,7 @@ internal class RealmCryptoStoreMigration @Inject constructor( private val clock: Clock, ) : MatrixRealmMigration( dbName = "Crypto", - schemaVersion = 19L, + schemaVersion = 20L, ) { /** * Forces all RealmCryptoStoreMigration instances to be equal. @@ -79,5 +80,6 @@ internal class RealmCryptoStoreMigration @Inject constructor( if (oldVersion < 17) MigrateCryptoTo017(realm).perform() if (oldVersion < 18) MigrateCryptoTo018(realm).perform() if (oldVersion < 19) MigrateCryptoTo019(realm).perform() + if (oldVersion < 20) MigrateCryptoTo020(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/MyDeviceLastSeenInfoEntityMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/MyDeviceLastSeenInfoEntityMapper.kt index 38a7569aabc30e441e4e48bc7c4b5c4e94dd7832..b81883fb3842ed09d75dbeb0b4be5f30125bb3dc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/MyDeviceLastSeenInfoEntityMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/MyDeviceLastSeenInfoEntityMapper.kt @@ -27,7 +27,18 @@ internal class MyDeviceLastSeenInfoEntityMapper @Inject constructor() { deviceId = entity.deviceId, lastSeenIp = entity.lastSeenIp, lastSeenTs = entity.lastSeenTs, - displayName = entity.displayName + displayName = entity.displayName, + unstableLastSeenUserAgent = entity.lastSeenUserAgent, + ) + } + + fun map(deviceInfo: DeviceInfo): MyDeviceLastSeenInfoEntity { + return MyDeviceLastSeenInfoEntity( + deviceId = deviceInfo.deviceId, + lastSeenIp = deviceInfo.lastSeenIp, + lastSeenTs = deviceInfo.lastSeenTs, + displayName = deviceInfo.displayName, + lastSeenUserAgent = deviceInfo.getBestLastSeenUserAgent(), ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo019.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo019.kt index 9d2eb60a600879508134c866f982897eb54e06e2..65280300ab3bc85509e27866581fbb51cf9dbde2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo019.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo019.kt @@ -30,7 +30,7 @@ import org.matrix.android.sdk.internal.util.database.RealmMigrator * mark existing keys as safe. * This migration can take long depending on the account */ -internal class MigrateCryptoTo019(realm: DynamicRealm) : RealmMigrator(realm, 18) { +internal class MigrateCryptoTo019(realm: DynamicRealm) : RealmMigrator(realm, 19) { override fun doMigrate(realm: DynamicRealm) { realm.schema.get("CrossSigningInfoEntity") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo020.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo020.kt new file mode 100644 index 0000000000000000000000000000000000000000..44d07ab538a53f4dc7b71b091a9b882bc7523acf --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/migration/MigrateCryptoTo020.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db.migration + +import io.realm.DynamicRealm +import org.matrix.android.sdk.internal.crypto.store.db.model.MyDeviceLastSeenInfoEntityFields +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +/** + * This migration adds a new field into MyDeviceLastSeenInfoEntity corresponding to the last seen user agent. + */ +internal class MigrateCryptoTo020(realm: DynamicRealm) : RealmMigrator(realm, 20) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.get("MyDeviceLastSeenInfoEntity") + ?.addField(MyDeviceLastSeenInfoEntityFields.LAST_SEEN_USER_AGENT, String::class.java) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/MyDeviceLastSeenInfoEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/MyDeviceLastSeenInfoEntity.kt index 74a81d5b01a045328d01edd8b4749cb1a7782084..3e6dc2de16dc23c01548c2c015b6f7564be92c9d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/MyDeviceLastSeenInfoEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/model/MyDeviceLastSeenInfoEntity.kt @@ -27,7 +27,9 @@ internal open class MyDeviceLastSeenInfoEntity( /** The last time this device has been seen. */ var lastSeenTs: Long? = null, /** The last ip address. */ - var lastSeenIp: String? = null + var lastSeenIp: String? = null, + /** The last user agent. */ + var lastSeenUserAgent: String? = null, ) : RealmObject() { companion object diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 2693ca474c21d8cf434b9c94f9ee876916a81a2b..9a2c32f97cdd982f677ef8b0ca6acd191f0dafbd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -54,6 +54,8 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo034 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo035 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo036 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo037 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo038 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo039 import org.matrix.android.sdk.internal.util.Normalizer import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import javax.inject.Inject @@ -62,7 +64,7 @@ internal class RealmSessionStoreMigration @Inject constructor( private val normalizer: Normalizer ) : MatrixRealmMigration( dbName = "Session", - schemaVersion = 37L, + schemaVersion = 39L, ) { /** * Forces all RealmSessionStoreMigration instances to be equal. @@ -109,5 +111,7 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion < 35) MigrateSessionTo035(realm).perform() if (oldVersion < 36) MigrateSessionTo036(realm).perform() if (oldVersion < 37) MigrateSessionTo037(realm).perform() + if (oldVersion < 38) MigrateSessionTo038(realm).perform() + if (oldVersion < 39) MigrateSessionTo039(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt index 184a0108b910191cacf7380f48b7bf96f03230db..63fa101c45ce9c3f19f465090d49d2999d5cf474 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/HomeServerCapabilitiesMapper.kt @@ -43,7 +43,8 @@ internal object HomeServerCapabilitiesMapper { defaultIdentityServerUrl = entity.defaultIdentityServerUrl, roomVersions = mapRoomVersion(entity.roomVersionsJson), canUseThreading = entity.canUseThreading, - canControlLogoutDevices = entity.canControlLogoutDevices + canControlLogoutDevices = entity.canControlLogoutDevices, + canLoginWithQrCode = entity.canLoginWithQrCode, ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PushersMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PushersMapper.kt index 2dba2c228bb16421825f63dc1ac4c7ded3b4ecc6..c3a37f5b95bbb61b002b2c499f284f28043b6667 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PushersMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PushersMapper.kt @@ -33,7 +33,9 @@ internal object PushersMapper { profileTag = pushEntity.profileTag, lang = pushEntity.lang, data = PusherData(pushEntity.data?.url, pushEntity.data?.format), - state = pushEntity.state + enabled = pushEntity.enabled, + deviceId = pushEntity.deviceId, + state = pushEntity.state, ) } @@ -46,7 +48,9 @@ internal object PushersMapper { deviceDisplayName = pusher.deviceDisplayName, profileTag = pusher.profileTag, lang = pusher.lang, - data = PusherDataEntity(pusher.data?.url, pusher.data?.format) + data = PusherDataEntity(pusher.data?.url, pusher.data?.format), + enabled = pusher.enabled, + deviceId = pusher.deviceId, ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo038.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo038.kt new file mode 100644 index 0000000000000000000000000000000000000000..b5848f04cdb982f8f34a4bf39a9206e3d63b6bf9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo038.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.migration + +import io.realm.DynamicRealm +import org.matrix.android.sdk.internal.database.model.PusherEntityFields +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +internal class MigrateSessionTo038(realm: DynamicRealm) : RealmMigrator(realm, 38) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.get("PusherEntity") + ?.addField(PusherEntityFields.ENABLED, Boolean::class.java) + ?.addField(PusherEntityFields.DEVICE_ID, String::class.java) + ?.transform { obj -> obj.set(PusherEntityFields.ENABLED, true) } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo039.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo039.kt new file mode 100644 index 0000000000000000000000000000000000000000..190a71c9be949a9bbfd62d1f78c281f082b759d2 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo039.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.migration + +import io.realm.DynamicRealm +import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields +import org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabilities +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +internal class MigrateSessionTo039(realm: DynamicRealm) : RealmMigrator(realm, 39) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.get("HomeServerCapabilitiesEntity") + ?.addField(HomeServerCapabilitiesEntityFields.CAN_LOGIN_WITH_QR_CODE, Boolean::class.java) + ?.transform { obj -> + obj.set(HomeServerCapabilitiesEntityFields.CAN_LOGIN_WITH_QR_CODE, false) + } + ?.forceRefreshOfHomeServerCapabilities() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt index 9d90973f8abded22d4ccb9b017badfc5a8351ed7..cfa02b2c743df39ed1eb67350f12ed53f5f6888a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/HomeServerCapabilitiesEntity.kt @@ -30,7 +30,8 @@ internal open class HomeServerCapabilitiesEntity( var defaultIdentityServerUrl: String? = null, var lastUpdatedTimestamp: Long = 0L, var canUseThreading: Boolean = false, - var canControlLogoutDevices: Boolean = false + var canControlLogoutDevices: Boolean = false, + var canLoginWithQrCode: Boolean = false, ) : RealmObject() { companion object diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PusherEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PusherEntity.kt index af8e4f2d370ee62c03e1234a6b666846d921c9f9..c08f6951683cdcbfc1dda24e33021bdae4e997dc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PusherEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PusherEntity.kt @@ -18,15 +18,6 @@ package org.matrix.android.sdk.internal.database.model import io.realm.RealmObject import org.matrix.android.sdk.api.session.pushers.PusherState -// TODO -// at java.lang.Thread.run(Thread.java:764) -// Caused by: java.lang.IllegalArgumentException: 'value' is not a valid managed object. -// at io.realm.ProxyState.checkValidObject(ProxyState.java:213) -// at io.realm.im_vector_matrix_android_internal_database_model_PusherEntityRealmProxy -// .realmSet$data(im_vector_matrix_android_internal_database_model_PusherEntityRealmProxy.java:413) -// at org.matrix.android.sdk.internal.database.model.PusherEntity.setData(PusherEntity.kt:16) -// at org.matrix.android.sdk.internal.session.pushers.AddHttpPusherWorker$doWork$$inlined$fold$lambda$2.execute(AddHttpPusherWorker.kt:70) -// at io.realm.Realm.executeTransaction(Realm.java:1493) internal open class PusherEntity( var pushKey: String = "", var kind: String? = null, @@ -35,7 +26,9 @@ internal open class PusherEntity( var deviceDisplayName: String? = null, var profileTag: String? = null, var lang: String? = null, - var data: PusherDataEntity? = null + var data: PusherDataEntity? = null, + var enabled: Boolean = true, + var deviceId: String? = null, ) : RealmObject() { private var stateStr: String = PusherState.UNREGISTERED.name diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ComputeUserAgentUseCase.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ComputeUserAgentUseCase.kt index 6eb4d5b1042263b359c87a41481674ea967e2b26..04e8cf8a295a9f2b99fa0078584ae3ea2ed25f55 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ComputeUserAgentUseCase.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ComputeUserAgentUseCase.kt @@ -20,6 +20,8 @@ import android.content.Context import android.os.Build import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.util.getApplicationInfoCompat +import org.matrix.android.sdk.api.util.getPackageInfoCompat import javax.inject.Inject class ComputeUserAgentUseCase @Inject constructor( @@ -36,7 +38,7 @@ class ComputeUserAgentUseCase @Inject constructor( val appPackageName = context.applicationContext.packageName val pm = context.packageManager - val appName = tryOrNull { pm.getApplicationLabel(pm.getApplicationInfo(appPackageName, 0)).toString() } + val appName = tryOrNull { pm.getApplicationLabel(pm.getApplicationInfoCompat(appPackageName, 0)).toString() } ?.takeIf { it.matches("\\A\\p{ASCII}*\\z".toRegex()) } @@ -44,7 +46,7 @@ class ComputeUserAgentUseCase @Inject constructor( // Use appPackageName instead of appName if appName is null or contains any non-ASCII character appPackageName } - val appVersion = tryOrNull { pm.getPackageInfo(context.applicationContext.packageName, 0).versionName } ?: FALLBACK_APP_VERSION + val appVersion = tryOrNull { pm.getPackageInfoCompat(context.applicationContext.packageName, 0).versionName } ?: FALLBACK_APP_VERSION val deviceManufacturer = Build.MANUFACTURER val deviceModel = Build.MODEL diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt index c023646c7fa6e57f0b0f11f97dddb6b4cc7f6af9..eee55735e0a7be73207678b8bbafe06c3ff7b7c9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt @@ -130,6 +130,7 @@ internal class FileUploader @Inject constructor( workingFile.outputStream().use { inputStream.copyTo(it) } + inputStream.close() workingFile } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt index 1e62b5d7f5b2e63622b4c4aa437cd02617326140..db1cd1b33bbf662757a19732ec29e2bfde65aa77 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt @@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.crypto.model.EncryptedFileInfo +import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent @@ -407,7 +408,10 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter newAttachmentAttributes: NewAttachmentAttributes ) { localEchoRepository.updateEcho(eventId) { _, event -> - val messageContent: MessageContent? = event.asDomain().content.toModel() + val content: Content? = event.asDomain().content + val messageContent: MessageContent? = content.toModel() + // Retrieve potential additional content from the original event + val additionalContent = content.orEmpty() - messageContent?.toContent().orEmpty().keys val updatedContent = when (messageContent) { is MessageImageContent -> messageContent.update(url, encryptedFileInfo, newAttachmentAttributes) is MessageVideoContent -> messageContent.update(url, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo, newAttachmentAttributes) @@ -415,7 +419,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter is MessageAudioContent -> messageContent.update(url, encryptedFileInfo, newAttachmentAttributes.newFileSize) else -> messageContent } - event.content = ContentMapper.map(updatedContent.toContent()) + event.content = ContentMapper.map(updatedContent.toContent().plus(additionalContent)) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt index add69dd8c7d90e387909626283f3bb6358454b8a..2c3cb440b62a984d8e993e3c6938852bb94c7222 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/homeserver/GetHomeServerCapabilitiesTask.kt @@ -20,11 +20,11 @@ import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.api.MatrixPatterns.getServerName import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.wellknown.WellknownResult -import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orTrue import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities import org.matrix.android.sdk.internal.auth.version.Versions import org.matrix.android.sdk.internal.auth.version.doesServerSupportLogoutDevices +import org.matrix.android.sdk.internal.auth.version.doesServerSupportQrCodeLogin import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreads import org.matrix.android.sdk.internal.auth.version.isLoginAndRegistrationSupportedBySdk import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity @@ -132,8 +132,6 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( homeServerCapabilitiesEntity.roomVersionsJson = capabilities?.roomVersions?.let { MoshiProvider.providesMoshi().adapter(RoomVersions::class.java).toJson(it) } - homeServerCapabilitiesEntity.canUseThreading = /* capabilities?.threads?.enabled.orFalse() || */ - getVersionResult?.doesServerSupportThreads().orFalse() } if (getMediaConfigResult != null) { @@ -144,6 +142,9 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor( if (getVersionResult != null) { homeServerCapabilitiesEntity.lastVersionIdentityServerSupported = getVersionResult.isLoginAndRegistrationSupportedBySdk() homeServerCapabilitiesEntity.canControlLogoutDevices = getVersionResult.doesServerSupportLogoutDevices() + homeServerCapabilitiesEntity.canUseThreading = /* capabilities?.threads?.enabled.orFalse() || */ + getVersionResult.doesServerSupportThreads() + homeServerCapabilitiesEntity.canLoginWithQrCode = getVersionResult.doesServerSupportQrCodeLogin() } if (getWellknownResult != null && getWellknownResult is WellknownResult.Prompt) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/GetProfileInfoTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/GetProfileInfoTask.kt index 40444edcab33143960c56678f460e7b6cffd875a..22bb3d37b04623de6ccf2f202dc9b78db30fb637 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/GetProfileInfoTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/GetProfileInfoTask.kt @@ -17,26 +17,40 @@ package org.matrix.android.sdk.internal.session.profile +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.api.session.user.model.User import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.session.user.UserEntityFactory import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction import javax.inject.Inject internal abstract class GetProfileInfoTask : Task<GetProfileInfoTask.Params, JsonDict> { data class Params( - val userId: String + val userId: String, + val storeInDatabase: Boolean = true, ) } internal class DefaultGetProfileInfoTask @Inject constructor( private val profileAPI: ProfileAPI, - private val globalErrorReceiver: GlobalErrorReceiver + private val globalErrorReceiver: GlobalErrorReceiver, + @SessionDatabase private val monarchy: Monarchy, ) : GetProfileInfoTask() { override suspend fun execute(params: Params): JsonDict { return executeRequest(globalErrorReceiver) { profileAPI.getProfile(params.userId) + }.also { user -> + if (params.storeInDatabase) { + // Insert into DB + monarchy.awaitTransaction { + it.insertOrUpdate(UserEntityFactory.create(User.fromJson(params.userId, user))) + } + } } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddPusherTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddPusherTask.kt index 7d81e19265c3eef69921db05e216bd51c72209d3..3e145dc6682232ce835fe97c80965b942ea53dac 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddPusherTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/AddPusherTask.kt @@ -38,6 +38,7 @@ internal class DefaultAddPusherTask @Inject constructor( private val requestExecutor: RequestExecutor, private val globalErrorReceiver: GlobalErrorReceiver ) : AddPusherTask { + override suspend fun execute(params: AddPusherTask.Params) { val pusher = params.pusher try { @@ -71,6 +72,8 @@ internal class DefaultAddPusherTask @Inject constructor( echo.profileTag = pusher.profileTag echo.data?.format = pusher.data?.format echo.data?.url = pusher.data?.url + echo.enabled = pusher.enabled + echo.deviceId = pusher.deviceId echo.state = PusherState.REGISTERED } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt index e912d9ccf888986f31ea4fefd934224c84e2c192..e89cfa508c6b1c65197c6e97726df6965c22a7f7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersService.kt @@ -42,6 +42,7 @@ internal class DefaultPushersService @Inject constructor( private val getPusherTask: GetPushersTask, private val pushGatewayNotifyTask: PushGatewayNotifyTask, private val addPusherTask: AddPusherTask, + private val togglePusherTask: TogglePusherTask, private val removePusherTask: RemovePusherTask, private val taskExecutor: TaskExecutor ) : PushersService { @@ -78,7 +79,9 @@ internal class DefaultPushersService @Inject constructor( appDisplayName = appDisplayName, deviceDisplayName = deviceDisplayName, data = JsonPusherData(url, EVENT_ID_ONLY.takeIf { withEventIdOnly }), - append = append + append = append, + enabled = enabled, + deviceId = deviceId, ) override suspend fun addEmailPusher( @@ -106,6 +109,24 @@ internal class DefaultPushersService @Inject constructor( ) } + override suspend fun togglePusher(pusher: Pusher, enable: Boolean) { + togglePusherTask.execute(TogglePusherTask.Params(pusher.toJsonPusher(), enable)) + } + + private fun Pusher.toJsonPusher() = JsonPusher( + pushKey = pushKey, + kind = kind, + appId = appId, + appDisplayName = appDisplayName, + deviceDisplayName = deviceDisplayName, + profileTag = profileTag, + lang = lang, + data = JsonPusherData(data.url, data.format), + append = false, + enabled = enabled, + deviceId = deviceId, + ) + private fun enqueueAddPusher(pusher: JsonPusher): UUID { val params = AddPusherWorker.Params(sessionId, pusher) val request = workManagerProvider.matrixOneTimeWorkRequestBuilder<AddPusherWorker>() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/JsonPusher.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/JsonPusher.kt index 71a1ea8c6669471d191b44c8c2e068c6c186d8ef..c1cf3eb276c6ddfc9e0666a148d218581ef454f7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/JsonPusher.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/JsonPusher.kt @@ -33,6 +33,8 @@ import java.security.InvalidParameterException * "device_display_name": "Alice's Phone", * "profile_tag": "xyz", * "lang": "en-US", + * "enabled": true, + * "device_id": "abc123", * "data": { * "url": "https://example.com/_matrix/push/v1/notify" * } @@ -112,7 +114,19 @@ internal data class JsonPusher( * The default is false. */ @Json(name = "append") - val append: Boolean? = false + val append: Boolean? = false, + + /** + * Whether the pusher should actively create push notifications. + */ + @Json(name = "org.matrix.msc3881.enabled") + val enabled: Boolean = true, + + /** + * The device_id of the session that registered the pusher. + */ + @Json(name = "org.matrix.msc3881.device_id") + val deviceId: String? = null, ) { init { // Do some parameter checks. It's ok to throw Exception, to inform developer of the problem diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushersModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushersModule.kt index 4528c95e6914d88e2b90b88edfc7d7221fb1a4ec..37c1c0c3ad7515d22f7ead81a52a7741367b132c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushersModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/PushersModule.kt @@ -68,6 +68,9 @@ internal abstract class PushersModule { @Binds abstract fun bindAddPusherTask(task: DefaultAddPusherTask): AddPusherTask + @Binds + abstract fun bindTogglePusherTask(task: DefaultTogglePusherTask): TogglePusherTask + @Binds abstract fun bindRemovePusherTask(task: DefaultRemovePusherTask): RemovePusherTask diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/TogglePusherTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/TogglePusherTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..87836e1c769e01ecf3d9470bb6d990dcb528fb4f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/TogglePusherTask.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 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.pushers + +import com.zhuinden.monarchy.Monarchy +import org.matrix.android.sdk.internal.database.model.PusherEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.network.GlobalErrorReceiver +import org.matrix.android.sdk.internal.network.RequestExecutor +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import javax.inject.Inject + +internal interface TogglePusherTask : Task<TogglePusherTask.Params, Unit> { + data class Params(val pusher: JsonPusher, val enable: Boolean) +} + +internal class DefaultTogglePusherTask @Inject constructor( + private val pushersAPI: PushersAPI, + @SessionDatabase private val monarchy: Monarchy, + private val requestExecutor: RequestExecutor, + private val globalErrorReceiver: GlobalErrorReceiver +) : TogglePusherTask { + + override suspend fun execute(params: TogglePusherTask.Params) { + val pusher = params.pusher.copy(enabled = params.enable) + + requestExecutor.executeRequest(globalErrorReceiver) { + pushersAPI.setPusher(pusher) + } + + monarchy.awaitTransaction { realm -> + val entity = PusherEntity.where(realm, params.pusher.pushKey).findFirst() + entity?.apply { enabled = params.enable }?.let { realm.insertOrUpdate(it) } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt index 9839a44427fcd5939e80ab13a8753528052ab85b..ddf3e41dff231b3f0d7e85abd2675ac210efba09 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/DefaultRelationService.kt @@ -105,19 +105,21 @@ internal class DefaultRelationService @AssistedInject constructor( targetEvent: TimelineEvent, msgType: String, newBodyText: CharSequence, + newFormattedBodyText: CharSequence?, newBodyAutoMarkdown: Boolean, compatibilityBodyText: String ): Cancelable { - return eventEditor.editTextMessage(targetEvent, msgType, newBodyText, newBodyAutoMarkdown, compatibilityBodyText) + return eventEditor.editTextMessage(targetEvent, msgType, newBodyText, newFormattedBodyText, newBodyAutoMarkdown, compatibilityBodyText) } override fun editReply( replyToEdit: TimelineEvent, originalTimelineEvent: TimelineEvent, newBodyText: String, + newFormattedBodyText: String?, compatibilityBodyText: String ): Cancelable { - return eventEditor.editReply(replyToEdit, originalTimelineEvent, newBodyText, compatibilityBodyText) + return eventEditor.editReply(replyToEdit, originalTimelineEvent, newBodyText, newFormattedBodyText, compatibilityBodyText) } override suspend fun fetchEditHistory(eventId: String): List<Event> { @@ -127,6 +129,7 @@ internal class DefaultRelationService @AssistedInject constructor( override fun replyToMessage( eventReplied: TimelineEvent, replyText: CharSequence, + replyFormattedText: CharSequence?, autoMarkdown: Boolean, showInThread: Boolean, rootThreadEventId: String? @@ -135,6 +138,7 @@ internal class DefaultRelationService @AssistedInject constructor( roomId = roomId, eventReplied = eventReplied, replyText = replyText, + replyTextFormatted = replyFormattedText, autoMarkdown = autoMarkdown, rootThreadEventId = rootThreadEventId, showInThread = showInThread @@ -178,6 +182,7 @@ internal class DefaultRelationService @AssistedInject constructor( roomId = roomId, eventReplied = eventReplied, replyText = replyInThreadText, + replyTextFormatted = formattedText, autoMarkdown = autoMarkdown, rootThreadEventId = rootThreadEventId, showInThread = false diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt index 795e9003ce6baaaed8b3c21fe6b04fc79926e613..c83539c8fdb7e1ef6f9974ec463efdca847a0bdd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt @@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.NoOpCancellable +import org.matrix.android.sdk.api.util.TextContent import org.matrix.android.sdk.internal.database.mapper.toEntity import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository @@ -42,19 +43,25 @@ internal class EventEditor @Inject constructor( targetEvent: TimelineEvent, msgType: String, newBodyText: CharSequence, + newBodyFormattedText: CharSequence?, newBodyAutoMarkdown: Boolean, compatibilityBodyText: String ): Cancelable { val roomId = targetEvent.roomId if (targetEvent.root.sendState.hasFailed()) { // We create a new in memory event for the EventSenderProcessor but we keep the eventId of the failed event. - val editedEvent = eventFactory.createTextEvent(roomId, msgType, newBodyText, newBodyAutoMarkdown).copy( + val editedEvent = if (newBodyFormattedText != null) { + val content = TextContent(newBodyText.toString(), newBodyFormattedText.toString()) + eventFactory.createFormattedTextEvent(roomId, content, msgType) + } else { + eventFactory.createTextEvent(roomId, msgType, newBodyText, newBodyAutoMarkdown) + }.copy( eventId = targetEvent.eventId ) return sendFailedEvent(targetEvent, editedEvent) } else if (targetEvent.root.sendState.isSent()) { val event = eventFactory - .createReplaceTextEvent(roomId, targetEvent.eventId, newBodyText, newBodyAutoMarkdown, msgType, compatibilityBodyText) + .createReplaceTextEvent(roomId, targetEvent.eventId, newBodyText, newBodyFormattedText, newBodyAutoMarkdown, msgType, compatibilityBodyText) return sendReplaceEvent(event) } else { // Should we throw? @@ -100,6 +107,7 @@ internal class EventEditor @Inject constructor( replyToEdit: TimelineEvent, originalTimelineEvent: TimelineEvent, newBodyText: String, + newBodyFormattedText: String?, compatibilityBodyText: String ): Cancelable { val roomId = replyToEdit.roomId @@ -109,6 +117,7 @@ internal class EventEditor @Inject constructor( roomId = roomId, eventReplied = originalTimelineEvent, replyText = newBodyText, + replyTextFormatted = newBodyFormattedText, autoMarkdown = false, showInThread = false )?.copy( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FetchEditHistoryTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FetchEditHistoryTask.kt index 5f5c000171aeb122cbb525a618dbc1e74c7dec29..93c7f143fd3c6ca6c5cb53e33dd3a91321ffb96a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FetchEditHistoryTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/FetchEditHistoryTask.kt @@ -22,6 +22,7 @@ import org.matrix.android.sdk.internal.crypto.CryptoSessionInfoProvider import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDataSource import org.matrix.android.sdk.internal.task.Task import javax.inject.Inject @@ -35,7 +36,8 @@ internal interface FetchEditHistoryTask : Task<FetchEditHistoryTask.Params, List internal class DefaultFetchEditHistoryTask @Inject constructor( private val roomAPI: RoomAPI, private val globalErrorReceiver: GlobalErrorReceiver, - private val cryptoSessionInfoProvider: CryptoSessionInfoProvider + private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, + private val eventDataSource: TimelineEventDataSource, ) : FetchEditHistoryTask { override suspend fun execute(params: FetchEditHistoryTask.Params): List<Event> { @@ -50,10 +52,14 @@ internal class DefaultFetchEditHistoryTask @Inject constructor( } // Filter out edition form other users, and redacted editions - val originalSenderId = response.originalEvent?.senderId + val originalEvent = eventDataSource.getTimelineEvent( + roomId = params.roomId, + eventId = params.eventId, + ) + val originalSenderId = originalEvent?.senderInfo?.userId val events = response.chunks .filter { it.senderId == originalSenderId } .filter { !it.isRedacted() } - return events + listOfNotNull(response.originalEvent) + return events + listOfNotNull(originalEvent?.root) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/RelationsResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/RelationsResponse.kt index a65165d457de63261d9db5eb4b3ec0ad94dd00bb..f2b0651e01297c64bb9ecd58ff94cdb401d2984f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/RelationsResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/RelationsResponse.kt @@ -22,7 +22,6 @@ import org.matrix.android.sdk.api.session.events.model.Event @JsonClass(generateAdapter = true) internal data class RelationsResponse( @Json(name = "chunk") val chunks: List<Event>, - @Json(name = "original_event") val originalEvent: Event?, @Json(name = "next_batch") val nextBatch: String?, @Json(name = "prev_batch") val prevBatch: String? ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt index edd74c2ce04a2013c0ec4b2685dc378cabdb7f79..4cf6445920cb76ae3c646e4d17c3ae6a612c0363 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.room.relation.threads import com.zhuinden.monarchy.Monarchy import io.realm.Realm +import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.model.OlmDecryptionResult import org.matrix.android.sdk.api.session.events.model.Event @@ -24,6 +25,7 @@ import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.internal.crypto.DefaultCryptoService +import org.matrix.android.sdk.internal.database.RealmSessionProvider import org.matrix.android.sdk.internal.database.helper.addTimelineEvent import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.mapper.toEntity @@ -46,6 +48,7 @@ import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.events.getFixedRoomMemberContent import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse +import org.matrix.android.sdk.internal.session.room.timeline.GetEventTask import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.util.awaitTransaction @@ -87,6 +90,8 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor( @SessionDatabase private val monarchy: Monarchy, private val cryptoService: DefaultCryptoService, private val clock: Clock, + private val realmSessionProvider: RealmSessionProvider, + private val getEventTask: GetEventTask, ) : FetchThreadTimelineTask { enum class Result { @@ -114,11 +119,26 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor( params: FetchThreadTimelineTask.Params ): Result { val threadList = response.chunks - val threadRootEvent = response.originalEvent val hasReachEnd = response.nextBatch == null - monarchy.awaitTransaction { realm -> + val isRootThreadTimelineEventEntityKnown: Boolean + var threadRootEvent: Event? = null + if (hasReachEnd) { + isRootThreadTimelineEventEntityKnown = realmSessionProvider.withRealm { realm -> + TimelineEventEntity + .where(realm, roomId = params.roomId, eventId = params.rootThreadEventId) + .findFirst() + } != null + if (!isRootThreadTimelineEventEntityKnown) { + // Fetch the root event from the server + threadRootEvent = tryOrNull { + getEventTask.execute(GetEventTask.Params(roomId = params.roomId, eventId = params.rootThreadEventId)) + } + } + } + + monarchy.awaitTransaction { realm -> val threadChunk = ChunkEntity.findLastForwardChunkOfThread(realm, params.roomId, params.rootThreadEventId) ?: run { return@awaitTransaction @@ -173,7 +193,7 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor( // Case when thread event is not in the device Timber.i("###THREADS FetchThreadTimelineTask root thread event: ${params.rootThreadEventId} NOT FOUND! Lets create a temp one") val eventEntity = createEventEntity(params.roomId, threadRootEvent, realm) - roomMemberContentsByUser.addSenderState(realm, params.roomId, threadRootEvent.senderId) + roomMemberContentsByUser.addSenderState(realm, params.roomId, threadRootEvent.senderId!!) threadChunk.addTimelineEvent( roomId = params.roomId, eventEntity = eventEntity, 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 418000abed9037e598550f14533a94234b111fa0..9cdbc7ff463cbba0449d8e2df10cacd5ac6cb5cb 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 @@ -27,6 +27,7 @@ import dagger.assisted.AssistedInject import kotlinx.coroutines.launch import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl 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.events.model.isAttachmentMessage import org.matrix.android.sdk.api.session.events.model.isTextMessage @@ -39,6 +40,7 @@ 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.PollType import org.matrix.android.sdk.api.session.room.model.message.getFileUrl +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent import org.matrix.android.sdk.api.session.room.send.SendService import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent @@ -87,51 +89,60 @@ internal class DefaultSendService @AssistedInject constructor( .let { sendEvent(it) } } - override fun sendTextMessage(text: CharSequence, msgType: String, autoMarkdown: Boolean): Cancelable { - return localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown) + override fun sendTextMessage(text: CharSequence, msgType: String, autoMarkdown: Boolean, additionalContent: Content?): Cancelable { + return localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown, additionalContent) .also { createLocalEcho(it) } .let { sendEvent(it) } } - override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String): Cancelable { - return localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType) + override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String, additionalContent: Content?): Cancelable { + return localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType, additionalContent) .also { createLocalEcho(it) } .let { sendEvent(it) } } - override fun sendQuotedTextMessage(quotedEvent: TimelineEvent, text: String, autoMarkdown: Boolean, rootThreadEventId: String?): Cancelable { + override fun sendQuotedTextMessage( + quotedEvent: TimelineEvent, + text: String, + formattedText: String?, + autoMarkdown: Boolean, + rootThreadEventId: String?, + additionalContent: Content?, + ): Cancelable { return localEchoEventFactory.createQuotedTextEvent( roomId = roomId, quotedEvent = quotedEvent, text = text, + formattedText = formattedText, autoMarkdown = autoMarkdown, - rootThreadEventId = rootThreadEventId + rootThreadEventId = rootThreadEventId, + additionalContent = additionalContent, ) .also { createLocalEcho(it) } .let { sendEvent(it) } } - override fun sendPoll(pollType: PollType, question: String, options: List<String>): Cancelable { - return localEchoEventFactory.createPollEvent(roomId, pollType, question, options) + override fun sendPoll(pollType: PollType, question: String, options: List<String>, additionalContent: Content?): Cancelable { + return localEchoEventFactory.createPollEvent(roomId, pollType, question, options, additionalContent) .also { createLocalEcho(it) } .let { sendEvent(it) } } - override fun voteToPoll(pollEventId: String, answerId: String): Cancelable { - return localEchoEventFactory.createPollReplyEvent(roomId, pollEventId, answerId) + override fun voteToPoll(pollEventId: String, answerId: String, additionalContent: Content?): Cancelable { + return localEchoEventFactory.createPollReplyEvent(roomId, pollEventId, answerId, additionalContent) .also { createLocalEcho(it) } .let { sendEvent(it) } } - override fun endPoll(pollEventId: String): Cancelable { - return localEchoEventFactory.createEndPollEvent(roomId, pollEventId) + override fun endPoll(pollEventId: String, additionalContent: Content?): Cancelable { + return localEchoEventFactory.createEndPollEvent(roomId, pollEventId, additionalContent) .also { createLocalEcho(it) } .let { sendEvent(it) } } - override fun redactEvent(event: Event, reason: String?): Cancelable { + override fun redactEvent(event: Event, reason: String?, additionalContent: Content?): Cancelable { // TODO manage media/attachements? - val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason) + val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason, additionalContent) .also { createLocalEcho(it) } return eventSenderProcessor.postRedaction(redactionEcho, reason) } @@ -257,7 +268,8 @@ internal class DefaultSendService @AssistedInject constructor( attachments: List<ContentAttachmentData>, compressBeforeSending: Boolean, roomIds: Set<String>, - rootThreadEventId: String? + rootThreadEventId: String?, + additionalContent: Content?, ): Cancelable { return attachments.mapTo(CancelableBag()) { sendMedia( @@ -273,7 +285,9 @@ internal class DefaultSendService @AssistedInject constructor( attachment: ContentAttachmentData, compressBeforeSending: Boolean, roomIds: Set<String>, - rootThreadEventId: String? + rootThreadEventId: String?, + relatesTo: RelationDefaultContent?, + additionalContent: Content?, ): Cancelable { // Ensure that the event will not be send in a thread if we are a different flow. // Like sending files to multiple rooms @@ -288,7 +302,9 @@ internal class DefaultSendService @AssistedInject constructor( localEchoEventFactory.createMediaEvent( roomId = it, attachment = attachment, - rootThreadEventId = rootThreadId + rootThreadEventId = rootThreadId, + relatesTo, + additionalContent, ).also { event -> createLocalEcho(event) } 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 4fbc91e9ec2d788036ce1ac3c5ca92d7af261458..7d8605c2bd399abd1291dcdc4eaacaabefb4abaa 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 @@ -95,12 +95,12 @@ internal class LocalEchoEventFactory @Inject constructor( private val permalinkFactory: PermalinkFactory, private val clock: Clock, ) { - fun createTextEvent(roomId: String, msgType: String, text: CharSequence, autoMarkdown: Boolean): Event { + fun createTextEvent(roomId: String, msgType: String, text: CharSequence, autoMarkdown: Boolean, additionalContent: Content? = null): Event { if (msgType == MessageType.MSGTYPE_TEXT || msgType == MessageType.MSGTYPE_EMOTE) { - return createFormattedTextEvent(roomId, createTextContent(text, autoMarkdown), msgType) + return createFormattedTextEvent(roomId, createTextContent(text, autoMarkdown), msgType, additionalContent) } val content = MessageTextContent(msgType = msgType, body = text.toString()) - return createMessageEvent(roomId, content) + return createMessageEvent(roomId, content, additionalContent) } private fun createTextContent(text: CharSequence, autoMarkdown: Boolean): TextContent { @@ -116,35 +116,41 @@ internal class LocalEchoEventFactory @Inject constructor( return TextContent(text.toString()) } - fun createFormattedTextEvent(roomId: String, textContent: TextContent, msgType: String): Event { - return createMessageEvent(roomId, textContent.toMessageTextContent(msgType)) + fun createFormattedTextEvent(roomId: String, textContent: TextContent, msgType: String, additionalContent: Content? = null): Event { + return createMessageEvent(roomId, textContent.toMessageTextContent(msgType), additionalContent) } fun createReplaceTextEvent( roomId: String, targetEventId: String, newBodyText: CharSequence, + newBodyFormattedText: CharSequence?, newBodyAutoMarkdown: Boolean, msgType: String, - compatibilityText: String + compatibilityText: String, + additionalContent: Content? = null, ): Event { + val content = if (newBodyFormattedText != null) { + TextContent(newBodyText.toString(), newBodyFormattedText.toString()).toMessageTextContent(msgType) + } else { + createTextContent(newBodyText, newBodyAutoMarkdown).toMessageTextContent(msgType) + }.toContent() return createMessageEvent( roomId, MessageTextContent( msgType = msgType, body = compatibilityText, relatesTo = RelationDefaultContent(RelationType.REPLACE, targetEventId), - newContent = createTextContent(newBodyText, newBodyAutoMarkdown) - .toMessageTextContent(msgType) - .toContent() - ) + newContent = content, + ), + additionalContent, ) } private fun createPollContent( question: String, options: List<String>, - pollType: PollType + pollType: PollType, ): MessagePollContent { return MessagePollContent( unstablePollCreationInfo = PollCreationInfo( @@ -162,7 +168,8 @@ internal class LocalEchoEventFactory @Inject constructor( pollType: PollType, targetEventId: String, question: String, - options: List<String> + options: List<String>, + additionalContent: Content? = null, ): Event { val newContent = MessagePollContent( relatesTo = RelationDefaultContent(RelationType.REPLACE, targetEventId), @@ -175,14 +182,15 @@ internal class LocalEchoEventFactory @Inject constructor( senderId = userId, eventId = localId, type = EventType.POLL_START.first(), - content = newContent.toContent() + content = newContent.toContent().plus(additionalContent.orEmpty()) ) } fun createPollReplyEvent( roomId: String, pollEventId: String, - answerId: String + answerId: String, + additionalContent: Content? = null, ): Event { val content = MessagePollResponseContent( body = answerId, @@ -199,7 +207,7 @@ internal class LocalEchoEventFactory @Inject constructor( senderId = userId, eventId = localId, type = EventType.POLL_RESPONSE.first(), - content = content.toContent(), + content = content.toContent().plus(additionalContent.orEmpty()), unsignedData = UnsignedData(age = null, transactionId = localId) ) } @@ -208,7 +216,8 @@ internal class LocalEchoEventFactory @Inject constructor( roomId: String, pollType: PollType, question: String, - options: List<String> + options: List<String>, + additionalContent: Content? = null, ): Event { val content = createPollContent(question, options, pollType) val localId = LocalEcho.createLocalEchoId() @@ -218,14 +227,15 @@ internal class LocalEchoEventFactory @Inject constructor( senderId = userId, eventId = localId, type = EventType.POLL_START.first(), - content = content.toContent(), + content = content.toContent().plus(additionalContent.orEmpty()), unsignedData = UnsignedData(age = null, transactionId = localId) ) } fun createEndPollEvent( roomId: String, - eventId: String + eventId: String, + additionalContent: Content? = null, ): Event { val content = MessageEndPollContent( relatesTo = RelationDefaultContent( @@ -240,7 +250,7 @@ internal class LocalEchoEventFactory @Inject constructor( senderId = userId, eventId = localId, type = EventType.POLL_END.first(), - content = content.toContent(), + content = content.toContent().plus(additionalContent.orEmpty()), unsignedData = UnsignedData(age = null, transactionId = localId) ) } @@ -250,7 +260,8 @@ internal class LocalEchoEventFactory @Inject constructor( latitude: Double, longitude: Double, uncertainty: Double?, - isUserLocation: Boolean + isUserLocation: Boolean, + additionalContent: Content? = null, ): Event { val geoUri = buildGeoUri(latitude, longitude, uncertainty) val assetType = if (isUserLocation) LocationAssetType.SELF else LocationAssetType.PIN @@ -262,7 +273,7 @@ internal class LocalEchoEventFactory @Inject constructor( unstableTimestampMillis = clock.epochMillis(), unstableText = geoUri ) - return createMessageEvent(roomId, content) + return createMessageEvent(roomId, content, additionalContent) } fun createLiveLocationEvent( @@ -270,7 +281,8 @@ internal class LocalEchoEventFactory @Inject constructor( roomId: String, latitude: Double, longitude: Double, - uncertainty: Double? + uncertainty: Double?, + additionalContent: Content? = null, ): Event { val geoUri = buildGeoUri(latitude, longitude, uncertainty) val content = MessageBeaconLocationDataContent( @@ -289,7 +301,7 @@ internal class LocalEchoEventFactory @Inject constructor( senderId = userId, eventId = localId, type = EventType.BEACON_LOCATION_DATA.first(), - content = content.toContent(), + content = content.toContent().plus(additionalContent.orEmpty()), unsignedData = UnsignedData(age = null, transactionId = localId) ) } @@ -301,7 +313,8 @@ internal class LocalEchoEventFactory @Inject constructor( newBodyText: String, autoMarkdown: Boolean, msgType: String, - compatibilityText: String + compatibilityText: String, + additionalContent: Content? = null, ): Event { val permalink = permalinkFactory.createPermalink(roomId, originalEvent.root.eventId ?: "", false) val userLink = originalEvent.root.senderId?.let { permalinkFactory.createPermalink(it, false) } ?: "" @@ -336,25 +349,42 @@ internal class LocalEchoEventFactory @Inject constructor( formattedBody = replyFormatted ) .toContent() - ) + ), + additionalContent, ) } fun createMediaEvent( roomId: String, attachment: ContentAttachmentData, - rootThreadEventId: String? + rootThreadEventId: String?, + relatesTo: RelationDefaultContent?, + additionalContent: Content? = null, ): Event { return when (attachment.type) { - ContentAttachmentData.Type.IMAGE -> createImageEvent(roomId, attachment, rootThreadEventId) - ContentAttachmentData.Type.VIDEO -> createVideoEvent(roomId, attachment, rootThreadEventId) - ContentAttachmentData.Type.AUDIO -> createAudioEvent(roomId, attachment, isVoiceMessage = false, rootThreadEventId = rootThreadEventId) - ContentAttachmentData.Type.VOICE_MESSAGE -> createAudioEvent(roomId, attachment, isVoiceMessage = true, rootThreadEventId = rootThreadEventId) - ContentAttachmentData.Type.FILE -> createFileEvent(roomId, attachment, rootThreadEventId) + ContentAttachmentData.Type.IMAGE -> createImageEvent(roomId, attachment, rootThreadEventId, relatesTo, additionalContent) + ContentAttachmentData.Type.VIDEO -> createVideoEvent(roomId, attachment, rootThreadEventId, relatesTo, additionalContent) + ContentAttachmentData.Type.AUDIO -> createAudioEvent( + roomId, + attachment, + isVoiceMessage = false, + rootThreadEventId = rootThreadEventId, + relatesTo, + additionalContent + ) + ContentAttachmentData.Type.VOICE_MESSAGE -> createAudioEvent( + roomId, + attachment, + isVoiceMessage = true, + rootThreadEventId = rootThreadEventId, + relatesTo, + additionalContent, + ) + ContentAttachmentData.Type.FILE -> createFileEvent(roomId, attachment, rootThreadEventId, relatesTo, additionalContent) } } - fun createReactionEvent(roomId: String, targetEventId: String, reaction: String): Event { + fun createReactionEvent(roomId: String, targetEventId: String, reaction: String, additionalContent: Content? = null): Event { val content = ReactionContent( ReactionInfo( RelationType.ANNOTATION, @@ -369,12 +399,18 @@ internal class LocalEchoEventFactory @Inject constructor( senderId = userId, eventId = localId, type = EventType.REACTION, - content = content.toContent(), + content = content.toContent().plus(additionalContent.orEmpty()), unsignedData = UnsignedData(age = null, transactionId = localId) ) } - private fun createImageEvent(roomId: String, attachment: ContentAttachmentData, rootThreadEventId: String?): Event { + private fun createImageEvent( + roomId: String, + attachment: ContentAttachmentData, + rootThreadEventId: String?, + relatesTo: RelationDefaultContent?, + additionalContent: Content?, + ): Event { var width = attachment.width var height = attachment.height @@ -399,19 +435,18 @@ internal class LocalEchoEventFactory @Inject constructor( size = attachment.size ), url = attachment.queryUri.toString(), - relatesTo = rootThreadEventId?.let { - RelationDefaultContent( - type = RelationType.THREAD, - eventId = it, - isFallingBack = true, - inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it)) - ) - } + relatesTo = relatesTo ?: rootThreadEventId?.let { generateThreadRelationContent(it) } ) - return createMessageEvent(roomId, content) + return createMessageEvent(roomId, content, additionalContent) } - private fun createVideoEvent(roomId: String, attachment: ContentAttachmentData, rootThreadEventId: String?): Event { + private fun createVideoEvent( + roomId: String, + attachment: ContentAttachmentData, + rootThreadEventId: String?, + relatesTo: RelationDefaultContent?, + additionalContent: Content?, + ): Event { val mediaDataRetriever = MediaMetadataRetriever() mediaDataRetriever.setDataSource(context, attachment.queryUri) @@ -443,23 +478,18 @@ internal class LocalEchoEventFactory @Inject constructor( thumbnailInfo = thumbnailInfo ), url = attachment.queryUri.toString(), - relatesTo = rootThreadEventId?.let { - RelationDefaultContent( - type = RelationType.THREAD, - eventId = it, - isFallingBack = true, - inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it)) - ) - } + relatesTo = relatesTo ?: rootThreadEventId?.let { generateThreadRelationContent(it) } ) - return createMessageEvent(roomId, content) + return createMessageEvent(roomId, content, additionalContent) } private fun createAudioEvent( roomId: String, attachment: ContentAttachmentData, isVoiceMessage: Boolean, - rootThreadEventId: String? + rootThreadEventId: String?, + relatesTo: RelationDefaultContent?, + additionalContent: Content? ): Event { val content = MessageAudioContent( msgType = MessageType.MSGTYPE_AUDIO, @@ -475,19 +505,18 @@ internal class LocalEchoEventFactory @Inject constructor( waveform = waveformSanitizer.sanitize(attachment.waveform) ), voiceMessageIndicator = if (!isVoiceMessage) null else emptyMap(), - relatesTo = rootThreadEventId?.let { - RelationDefaultContent( - type = RelationType.THREAD, - eventId = it, - isFallingBack = true, - inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it)) - ) - } + relatesTo = relatesTo ?: rootThreadEventId?.let { generateThreadRelationContent(it) } ) - return createMessageEvent(roomId, content) + return createMessageEvent(roomId, content, additionalContent) } - private fun createFileEvent(roomId: String, attachment: ContentAttachmentData, rootThreadEventId: String?): Event { + private fun createFileEvent( + roomId: String, + attachment: ContentAttachmentData, + rootThreadEventId: String?, + relatesTo: RelationDefaultContent?, + additionalContent: Content? + ): Event { val content = MessageFileContent( msgType = MessageType.MSGTYPE_FILE, body = attachment.name ?: "file", @@ -496,24 +525,18 @@ internal class LocalEchoEventFactory @Inject constructor( size = attachment.size ), url = attachment.queryUri.toString(), - relatesTo = rootThreadEventId?.let { - RelationDefaultContent( - type = RelationType.THREAD, - eventId = it, - isFallingBack = true, - inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(it)) - ) - } + relatesTo = relatesTo ?: rootThreadEventId?.let { generateThreadRelationContent(it) } ) - return createMessageEvent(roomId, content) + return createMessageEvent(roomId, content, additionalContent) } - private fun createMessageEvent(roomId: String, content: MessageContent? = null): Event { - return createEvent(roomId, EventType.MESSAGE, content.toContent()) + private fun createMessageEvent(roomId: String, content: MessageContent, additionalContent: Content?): Event { + return createEvent(roomId, EventType.MESSAGE, content.toContent(), additionalContent) } - fun createEvent(roomId: String, type: String, content: Content?): Event { + fun createEvent(roomId: String, type: String, content: Content?, additionalContent: Content? = null): Event { val newContent = enhanceStickerIfNeeded(type, content) ?: content + val updatedNewContent = newContent?.plus(additionalContent.orEmpty()) ?: additionalContent val localId = LocalEcho.createLocalEchoId() return Event( roomId = roomId, @@ -521,7 +544,7 @@ internal class LocalEchoEventFactory @Inject constructor( senderId = userId, eventId = localId, type = type, - content = newContent, + content = updatedNewContent, unsignedData = UnsignedData(age = null, transactionId = localId) ) } @@ -555,7 +578,8 @@ internal class LocalEchoEventFactory @Inject constructor( text: CharSequence, msgType: String, autoMarkdown: Boolean, - formattedText: String? + formattedText: String?, + additionalContent: Content? = null, ): Event { val content = formattedText?.let { TextContent(text.toString(), it) } ?: createTextContent(text, autoMarkdown) return createEvent( @@ -565,8 +589,7 @@ internal class LocalEchoEventFactory @Inject constructor( rootThreadEventId = rootThreadEventId, latestThreadEventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId), msgType = msgType - ) - .toContent() + ).toContent().plus(additionalContent.orEmpty()) ) } @@ -581,9 +604,11 @@ internal class LocalEchoEventFactory @Inject constructor( roomId: String, eventReplied: TimelineEvent, replyText: CharSequence, + replyTextFormatted: CharSequence?, autoMarkdown: Boolean, rootThreadEventId: String? = null, - showInThread: Boolean + showInThread: Boolean, + additionalContent: Content? = null ): Event? { // Fallbacks and event representation // TODO Add error/warning logs when any of this is null @@ -594,7 +619,7 @@ internal class LocalEchoEventFactory @Inject constructor( val body = bodyForReply(eventReplied.getLastMessageContent(), eventReplied.isReply()) // As we always supply formatted body for replies we should force the MarkdownParser to produce html. - val replyTextFormatted = markdownParser.parse(replyText, force = true, advanced = autoMarkdown).takeFormatted() + val finalReplyTextFormatted = replyTextFormatted?.toString() ?: markdownParser.parse(replyText, force = true, advanced = autoMarkdown).takeFormatted() // Body of the original message may not have formatted version, so may also have to convert to html. val bodyFormatted = body.formattedText ?: markdownParser.parse(body.text, force = true, advanced = autoMarkdown).takeFormatted() val replyFormatted = buildFormattedReply( @@ -602,7 +627,7 @@ internal class LocalEchoEventFactory @Inject constructor( userLink, userId, bodyFormatted, - replyTextFormatted + finalReplyTextFormatted ) // // > <@alice:example.org> This is the original body @@ -621,9 +646,17 @@ internal class LocalEchoEventFactory @Inject constructor( showInThread = showInThread ) ) - return createMessageEvent(roomId, content) + return createMessageEvent(roomId, content, additionalContent) } + private fun generateThreadRelationContent(rootThreadEventId: String) = + RelationDefaultContent( + type = RelationType.THREAD, + eventId = rootThreadEventId, + isFallingBack = true, + inReplyTo = ReplyToContent(eventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId)), + ) + /** * Generates the appropriate relatesTo object for a reply event. * It can either be a regular reply or a reply within a thread @@ -742,7 +775,7 @@ internal class LocalEchoEventFactory @Inject constructor( } } */ - fun createRedactEvent(roomId: String, eventId: String, reason: String?): Event { + fun createRedactEvent(roomId: String, eventId: String, reason: String?, additionalContent: Content? = null): Event { val localId = LocalEcho.createLocalEchoId() return Event( roomId = roomId, @@ -751,7 +784,7 @@ internal class LocalEchoEventFactory @Inject constructor( eventId = localId, type = EventType.REDACTION, redacts = eventId, - content = reason?.let { mapOf("reason" to it).toContent() }, + content = reason?.let { mapOf("reason" to it).toContent().plus(additionalContent.orEmpty()) } ?: additionalContent, unsignedData = UnsignedData(age = null, transactionId = localId) ) } @@ -765,29 +798,38 @@ internal class LocalEchoEventFactory @Inject constructor( roomId: String, quotedEvent: TimelineEvent, text: String, + formattedText: String?, autoMarkdown: Boolean, - rootThreadEventId: String? + rootThreadEventId: String?, + additionalContent: Content? = null, ): Event { val messageContent = quotedEvent.getLastMessageContent() - val textMsg = messageContent?.body + val textMsg = if (messageContent is MessageContentWithFormattedBody) { + messageContent.formattedBody + } else { + messageContent?.body + } val quoteText = legacyRiotQuoteText(textMsg, text) + val quoteFormattedText = "<blockquote>$textMsg</blockquote>$formattedText" return if (rootThreadEventId != null) { createMessageEvent( roomId, markdownParser - .parse(quoteText, force = true, advanced = autoMarkdown) + .parse(quoteText, force = true, advanced = autoMarkdown).copy(formattedText = quoteFormattedText) .toThreadTextContent( rootThreadEventId = rootThreadEventId, latestThreadEventId = localEchoRepository.getLatestThreadEvent(rootThreadEventId), msgType = MessageType.MSGTYPE_TEXT - ) + ), + additionalContent, ) } else { createFormattedTextEvent( roomId, - markdownParser.parse(quoteText, force = true, advanced = autoMarkdown), - MessageType.MSGTYPE_TEXT + markdownParser.parse(quoteText, force = true, advanced = autoMarkdown).copy(formattedText = quoteFormattedText), + MessageType.MSGTYPE_TEXT, + additionalContent, ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt index 53c025387668a2cd3d5fee93e76959dd2969f0b1..b1a3d51b36a2e9070065f834691fbaf18ac1d54d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt @@ -96,4 +96,8 @@ internal class DefaultTimelineService @AssistedInject constructor( override fun getAttachmentMessages(): List<TimelineEvent> { return timelineEventDataSource.getAttachmentMessages(roomId) } + + override fun getTimelineEventsRelatedTo(relationType: String, eventId: String): List<TimelineEvent> { + return timelineEventDataSource.getTimelineEventsRelatedTo(roomId, relationType, eventId) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDataSource.kt index b1b9e4bb2280f5bca2451c9ed756eb286d99ea8d..20094e4be8a354b483740bc37176381739bca62e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineEventDataSource.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.room.timeline import androidx.lifecycle.LiveData import com.zhuinden.monarchy.Monarchy import io.realm.Sort +import org.matrix.android.sdk.api.session.events.model.getRelationContent import org.matrix.android.sdk.api.session.events.model.isImageMessage import org.matrix.android.sdk.api.session.events.model.isVideoMessage import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent @@ -63,4 +64,18 @@ internal class TimelineEventDataSource @Inject constructor( .orEmpty() } } + + fun getTimelineEventsRelatedTo(roomId: String, eventType: String, eventId: String): List<TimelineEvent> { + // TODO Remove this trick and call relations API + // see https://spec.matrix.org/latest/client-server-api/#get_matrixclientv1roomsroomidrelationseventidreltypeeventtype + return realmSessionProvider.withRealm { realm -> + TimelineEventEntity.whereRoomId(realm, roomId) + .sort(TimelineEventEntityFields.ROOT.ORIGIN_SERVER_TS, Sort.ASCENDING) + .distinct(TimelineEventEntityFields.EVENT_ID) + .findAll() + .mapNotNull { + timelineEventMapper.map(it).takeIf { it.root.getRelationContent()?.takeIf { it.type == eventType && it.eventId == eventId } != null } + } + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UpdateUserWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UpdateUserWorker.kt index 1f840a82d565fa4f76e8b0501783e4f1907f8d8e..1ee2fc4802c16193d6da2c0545bb848da5f4cbf2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UpdateUserWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/UpdateUserWorker.kt @@ -71,10 +71,16 @@ internal class UpdateUserWorker(context: Context, params: WorkerParameters, sess ?.saveLocally() } - private suspend fun fetchUsers(userIdsToFetch: Collection<String>) = userIdsToFetch.mapNotNull { - tryOrNull { - val profileJson = getProfileInfoTask.execute(GetProfileInfoTask.Params(it)) - User.fromJson(it, profileJson) + private suspend fun fetchUsers(userIdsToFetch: Collection<String>): List<User> { + return userIdsToFetch.mapNotNull { userId -> + tryOrNull { + val profileJson = getProfileInfoTask.execute(GetProfileInfoTask.Params( + userId = userId, + // Bulk insert later, so tell the task not to store the User. + storeInDatabase = false, + )) + User.fromJson(userId, profileJson) + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserDataSource.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserDataSource.kt index f9feb04e97ae868fe4600cfd3e59311fcd786b28..98108008fe78cd0cc3a5d8a561617db83bcf06cd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserDataSource.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/UserDataSource.kt @@ -66,6 +66,8 @@ internal class UserDataSource @Inject constructor( } } + fun getUserOrDefault(userId: String): User = getUser(userId) ?: User(userId) + fun getUserLive(userId: String): LiveData<Optional<User>> { val liveData = monarchy.findAllMappedWithChanges( { UserEntity.where(it, userId) }, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt index df9dcfb903ec533efddbd1b77f93e73e043c7d4b..c73446cf25838472b936d71dcc7523b4d4780db6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/DefaultSessionAccountDataService.kt @@ -66,7 +66,7 @@ internal class DefaultSessionAccountDataService @Inject constructor( override suspend fun updateUserAccountData(type: String, content: Content) { val params = UpdateUserAccountDataTask.AnyParams(type = type, any = content) - awaitCallback<Unit> { callback -> + awaitCallback { callback -> updateUserAccountDataTask.configureWith(params) { this.retryCount = 5 // TODO Need to refactor retrying out into a helper method. this.callback = callback diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/WidgetFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/WidgetFactory.kt index 8bd61a7bdf5ffdb962bd7acb56e353e6861ef4e3..a43c59a83b9b96126c419f4ab1a33534cf95d6f7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/WidgetFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/WidgetFactory.kt @@ -20,7 +20,6 @@ import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.sender.SenderInfo -import org.matrix.android.sdk.api.session.user.model.User import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.api.session.widgets.model.WidgetContent import org.matrix.android.sdk.api.session.widgets.model.WidgetType @@ -74,7 +73,7 @@ internal class WidgetFactory @Inject constructor( // Ref: https://github.com/matrix-org/matrix-widget-api/blob/master/src/templating/url-template.ts#L29-L33 fun computeURL(widget: Widget, isLightTheme: Boolean): String? { var computedUrl = widget.widgetContent.url ?: return null - val myUser = userDataSource.getUser(userId) ?: User(userId) + val myUser = userDataSource.getUserOrDefault(userId) val keyValue = widget.widgetContent.data.mapKeys { "\$${it.key}" }.toMutableMap() diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/MyDeviceLastSeenInfoEntityMapperTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/MyDeviceLastSeenInfoEntityMapperTest.kt index a27f430edc55a7980c89b05467c5446ddf5d8747..8515427e8e3a74c6cae1246911e4d7bc6ce24f5d 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/MyDeviceLastSeenInfoEntityMapperTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/store/db/mapper/MyDeviceLastSeenInfoEntityMapperTest.kt @@ -25,6 +25,7 @@ private const val A_DEVICE_ID = "device-id" private const val AN_IP_ADDRESS = "ip-address" private const val A_TIMESTAMP = 123L private const val A_DISPLAY_NAME = "display-name" +private const val A_USER_AGENT = "user-agent" class MyDeviceLastSeenInfoEntityMapperTest { @@ -32,21 +33,55 @@ class MyDeviceLastSeenInfoEntityMapperTest { @Test fun `given an entity when mapping to model then all fields are correctly mapped`() { + // Given val entity = MyDeviceLastSeenInfoEntity( deviceId = A_DEVICE_ID, lastSeenIp = AN_IP_ADDRESS, lastSeenTs = A_TIMESTAMP, - displayName = A_DISPLAY_NAME + displayName = A_DISPLAY_NAME, + lastSeenUserAgent = A_USER_AGENT, ) val expectedDeviceInfo = DeviceInfo( deviceId = A_DEVICE_ID, lastSeenIp = AN_IP_ADDRESS, lastSeenTs = A_TIMESTAMP, - displayName = A_DISPLAY_NAME + displayName = A_DISPLAY_NAME, + unstableLastSeenUserAgent = A_USER_AGENT, ) + // When val deviceInfo = myDeviceLastSeenInfoEntityMapper.map(entity) + // Then deviceInfo shouldBeEqualTo expectedDeviceInfo } + + @Test + fun `given a device info when mapping to entity then all fields are correctly mapped`() { + // Given + val deviceInfo = DeviceInfo( + deviceId = A_DEVICE_ID, + lastSeenIp = AN_IP_ADDRESS, + lastSeenTs = A_TIMESTAMP, + displayName = A_DISPLAY_NAME, + unstableLastSeenUserAgent = A_USER_AGENT, + ) + val expectedEntity = MyDeviceLastSeenInfoEntity( + deviceId = A_DEVICE_ID, + lastSeenIp = AN_IP_ADDRESS, + lastSeenTs = A_TIMESTAMP, + displayName = A_DISPLAY_NAME, + lastSeenUserAgent = A_USER_AGENT + ) + + // When + val entity = myDeviceLastSeenInfoEntityMapper.map(deviceInfo) + + // Then + entity.deviceId shouldBeEqualTo expectedEntity.deviceId + entity.lastSeenIp shouldBeEqualTo expectedEntity.lastSeenIp + entity.lastSeenTs shouldBeEqualTo expectedEntity.lastSeenTs + entity.displayName shouldBeEqualTo expectedEntity.displayName + entity.lastSeenUserAgent shouldBeEqualTo expectedEntity.lastSeenUserAgent + } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/PushersMapperTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/PushersMapperTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..08ed20a7660fff3223cbbf98947154dc7824236f --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/database/mapper/PushersMapperTest.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.database.mapper + +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test +import org.matrix.android.sdk.test.fixtures.JsonPusherFixture.aJsonPusher +import org.matrix.android.sdk.test.fixtures.PusherEntityFixture.aPusherEntity + +class PushersMapperTest { + + @Test + fun `when mapping PusherEntity, then it is mapped into Pusher successfully`() { + val pusherEntity = aPusherEntity() + + val mappedPusher = PushersMapper.map(pusherEntity) + + mappedPusher.pushKey shouldBeEqualTo pusherEntity.pushKey + mappedPusher.kind shouldBeEqualTo pusherEntity.kind.orEmpty() + mappedPusher.appId shouldBeEqualTo pusherEntity.appId + mappedPusher.appDisplayName shouldBeEqualTo pusherEntity.appDisplayName + mappedPusher.deviceDisplayName shouldBeEqualTo pusherEntity.deviceDisplayName + mappedPusher.profileTag shouldBeEqualTo pusherEntity.profileTag + mappedPusher.lang shouldBeEqualTo pusherEntity.lang + mappedPusher.data.url shouldBeEqualTo pusherEntity.data?.url + mappedPusher.data.format shouldBeEqualTo pusherEntity.data?.format + mappedPusher.enabled shouldBeEqualTo pusherEntity.enabled + mappedPusher.deviceId shouldBeEqualTo pusherEntity.deviceId + mappedPusher.state shouldBeEqualTo pusherEntity.state + } + + @Test + fun `when mapping JsonPusher, then it is mapped into Pusher successfully`() { + val jsonPusher = aJsonPusher() + + val mappedPusherEntity = PushersMapper.map(jsonPusher) + + mappedPusherEntity.pushKey shouldBeEqualTo jsonPusher.pushKey + mappedPusherEntity.kind shouldBeEqualTo jsonPusher.kind + mappedPusherEntity.appId shouldBeEqualTo jsonPusher.appId + mappedPusherEntity.appDisplayName shouldBeEqualTo jsonPusher.appDisplayName + mappedPusherEntity.deviceDisplayName shouldBeEqualTo jsonPusher.deviceDisplayName + mappedPusherEntity.profileTag shouldBeEqualTo jsonPusher.profileTag + mappedPusherEntity.lang shouldBeEqualTo jsonPusher.lang + mappedPusherEntity.data?.url shouldBeEqualTo jsonPusher.data?.url + mappedPusherEntity.data?.format shouldBeEqualTo jsonPusher.data?.format + mappedPusherEntity.enabled shouldBeEqualTo jsonPusher.enabled + mappedPusherEntity.deviceId shouldBeEqualTo jsonPusher.deviceId + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/network/ComputeUserAgentUseCaseTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/network/ComputeUserAgentUseCaseTest.kt index 9ed6f28d7e22cd5bae3436db318fff951b0ccacc..2170371cdecd20cb961957cee3a640bd48eee1df 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/network/ComputeUserAgentUseCaseTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/network/ComputeUserAgentUseCaseTest.kt @@ -27,6 +27,8 @@ import org.amshove.kluent.shouldBeEqualTo import org.junit.Before import org.junit.Test import org.matrix.android.sdk.BuildConfig +import org.matrix.android.sdk.api.util.getApplicationInfoCompat +import org.matrix.android.sdk.api.util.getPackageInfoCompat import java.lang.Exception private const val A_PACKAGE_NAME = "org.matrix.sdk" @@ -49,8 +51,8 @@ class ComputeUserAgentUseCaseTest { every { context.applicationContext } returns context every { context.packageName } returns A_PACKAGE_NAME every { context.packageManager } returns packageManager - every { packageManager.getApplicationInfo(any(), any()) } returns applicationInfo - every { packageManager.getPackageInfo(any<String>(), any()) } returns packageInfo + every { packageManager.getApplicationInfoCompat(any(), any()) } returns applicationInfo + every { packageManager.getPackageInfoCompat(any(), any()) } returns packageInfo } @Test diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt index dac33069f348732ec667531b993025c5e1ebbad6..a971973f5664617205adba432fcbb286bf50b950 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultAddPusherTaskTest.kt @@ -71,7 +71,7 @@ class DefaultAddPusherTaskTest { } @Test - fun `given a persisted pusher when adding Pusher then updates api and mutates persisted result with Registered state`() { + fun `given a persisted pusher, when adding Pusher, then updates api and mutates persisted result with Registered state`() { val realmResult = PusherEntity(appDisplayName = null) monarchy.givenWhereReturns(result = realmResult) .givenEqualTo(PusherEntityFields.PUSH_KEY, A_JSON_PUSHER.pushKey) @@ -85,7 +85,7 @@ class DefaultAddPusherTaskTest { } @Test - fun `given a persisted push entity and SetPush API fails when adding Pusher then mutates persisted result with Failed registration state and rethrows`() { + fun `given a persisted push entity and SetPush API fails, when adding Pusher, then mutates persisted result with Failed registration state and rethrows`() { val realmResult = PusherEntity() monarchy.givenWhereReturns(result = realmResult) .givenEqualTo(PusherEntityFields.PUSH_KEY, A_JSON_PUSHER.pushKey) @@ -99,7 +99,7 @@ class DefaultAddPusherTaskTest { } @Test - fun `given no persisted push entity and SetPush API fails when adding Pusher then rethrows error`() { + fun `given no persisted push entity and SetPush API fails, when adding Pusher, then rethrows error`() { monarchy.givenWhereReturns<PusherEntity>(result = null) .givenEqualTo(PusherEntityFields.PUSH_KEY, A_JSON_PUSHER.pushKey) pushersAPI.givenSetPusherErrors(SocketException()) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersServiceTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersServiceTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..a00ac3a17d5fc7b6d4336c1666afba886671e9fe --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultPushersServiceTest.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.pushers + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.matrix.android.sdk.test.fakes.FakeAddPusherTask +import org.matrix.android.sdk.test.fakes.FakeGetPushersTask +import org.matrix.android.sdk.test.fakes.FakeMonarchy +import org.matrix.android.sdk.test.fakes.FakeRemovePusherTask +import org.matrix.android.sdk.test.fakes.FakeTaskExecutor +import org.matrix.android.sdk.test.fakes.FakeTogglePusherTask +import org.matrix.android.sdk.test.fakes.FakeWorkManagerProvider +import org.matrix.android.sdk.test.fakes.internal.FakePushGatewayNotifyTask +import org.matrix.android.sdk.test.fixtures.PusherFixture + +@OptIn(ExperimentalCoroutinesApi::class) +class DefaultPushersServiceTest { + + private val workManagerProvider = FakeWorkManagerProvider() + private val monarchy = FakeMonarchy() + private val sessionId = "" + private val getPushersTask = FakeGetPushersTask() + private val pushGatewayNotifyTask = FakePushGatewayNotifyTask() + private val addPusherTask = FakeAddPusherTask() + private val togglePusherTask = FakeTogglePusherTask() + private val removePusherTask = FakeRemovePusherTask() + private val taskExecutor = FakeTaskExecutor() + + private val pushersService = DefaultPushersService( + workManagerProvider.instance, + monarchy.instance, + sessionId, + getPushersTask, + pushGatewayNotifyTask, + addPusherTask, + togglePusherTask, + removePusherTask, + taskExecutor.instance, + ) + + @Test + fun `when togglePusher, then execute task`() = runTest { + val pusher = PusherFixture.aPusher() + val enable = true + + pushersService.togglePusher(pusher, enable) + + togglePusherTask.verifyExecution(pusher, enable) + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultTogglePusherTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultTogglePusherTaskTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..3c54f6f1e12e00e3fffaf1e24c11e3fb64e9c265 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/pushers/DefaultTogglePusherTaskTest.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.pushers + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test +import org.matrix.android.sdk.internal.database.model.PusherEntity +import org.matrix.android.sdk.internal.database.model.PusherEntityFields +import org.matrix.android.sdk.test.fakes.FakeGlobalErrorReceiver +import org.matrix.android.sdk.test.fakes.FakeMonarchy +import org.matrix.android.sdk.test.fakes.FakePushersAPI +import org.matrix.android.sdk.test.fakes.FakeRequestExecutor +import org.matrix.android.sdk.test.fakes.givenEqualTo +import org.matrix.android.sdk.test.fakes.givenFindFirst +import org.matrix.android.sdk.test.fixtures.JsonPusherFixture.aJsonPusher +import org.matrix.android.sdk.test.fixtures.PusherEntityFixture.aPusherEntity + +@OptIn(ExperimentalCoroutinesApi::class) +class DefaultTogglePusherTaskTest { + + private val pushersAPI = FakePushersAPI() + private val monarchy = FakeMonarchy() + private val requestExecutor = FakeRequestExecutor() + private val globalErrorReceiver = FakeGlobalErrorReceiver() + + private val togglePusherTask = DefaultTogglePusherTask(pushersAPI, monarchy.instance, requestExecutor, globalErrorReceiver) + + @Test + fun `execution toggles enable on both local and remote`() = runTest { + val jsonPusher = aJsonPusher(enabled = false) + val params = TogglePusherTask.Params(aJsonPusher(), true) + + val pusherEntity = aPusherEntity(enabled = false) + monarchy.givenWhere<PusherEntity>() + .givenEqualTo(PusherEntityFields.PUSH_KEY, jsonPusher.pushKey) + .givenFindFirst(pusherEntity) + + togglePusherTask.execute(params) + + val expectedPayload = jsonPusher.copy(enabled = true) + pushersAPI.verifySetPusher(expectedPayload) + monarchy.verifyInsertOrUpdate<PusherEntity> { + withArg { actual -> + actual.enabled shouldBeEqualTo true + } + } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeAddPusherTask.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeAddPusherTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..16cdd7a62645f6a04448ce2b883189723105d262 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeAddPusherTask.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.test.fakes + +import io.mockk.mockk +import org.matrix.android.sdk.internal.session.pushers.AddPusherTask + +class FakeAddPusherTask : AddPusherTask by mockk() diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeGetPushersTask.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeGetPushersTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..d5a41bb0e002bd49df922b1f7baf5a0828d3ca82 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeGetPushersTask.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.test.fakes + +import io.mockk.mockk +import org.matrix.android.sdk.internal.session.pushers.GetPushersTask + +class FakeGetPushersTask : GetPushersTask by mockk() diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRemovePusherTask.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRemovePusherTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..55a7607a032706abe213494e72392e5f8cd7043f --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRemovePusherTask.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.test.fakes + +import io.mockk.mockk +import org.matrix.android.sdk.internal.session.pushers.RemovePusherTask + +class FakeRemovePusherTask : RemovePusherTask by mockk() diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeTaskExecutor.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeTaskExecutor.kt new file mode 100644 index 0000000000000000000000000000000000000000..543dda8a4f93decc3233cf676451e95d97b76c67 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeTaskExecutor.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.test.fakes + +import io.mockk.mockk +import org.matrix.android.sdk.internal.task.TaskExecutor + +internal class FakeTaskExecutor { + + val instance: TaskExecutor = mockk() +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeTogglePusherTask.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeTogglePusherTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..b1e059a40ed20ff9605b9c60bb350a3af438b508 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeTogglePusherTask.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.test.fakes + +import io.mockk.coVerify +import io.mockk.mockk +import io.mockk.slot +import org.amshove.kluent.shouldBeEqualTo +import org.matrix.android.sdk.api.session.pushers.Pusher +import org.matrix.android.sdk.internal.session.pushers.TogglePusherTask + +class FakeTogglePusherTask : TogglePusherTask by mockk(relaxed = true) { + + fun verifyExecution(pusher: Pusher, enable: Boolean) { + val slot = slot<TogglePusherTask.Params>() + coVerify { execute(capture(slot)) } + val params = slot.captured + params.pusher.pushKey shouldBeEqualTo pusher.pushKey + params.enable shouldBeEqualTo enable + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakePushGatewayNotifyTask.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakePushGatewayNotifyTask.kt new file mode 100644 index 0000000000000000000000000000000000000000..46a106dcb26d4a732b2810c393c832cbd38eabcb --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakePushGatewayNotifyTask.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.test.fakes.internal + +import io.mockk.mockk +import org.matrix.android.sdk.internal.session.pushers.gateway.PushGatewayNotifyTask + +class FakePushGatewayNotifyTask : PushGatewayNotifyTask by mockk() diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/JsonPusherFixture.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/JsonPusherFixture.kt new file mode 100644 index 0000000000000000000000000000000000000000..8e679ff91c0294dd19e69e34637aa0e5596dedb6 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/JsonPusherFixture.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.test.fixtures + +import org.matrix.android.sdk.internal.session.pushers.JsonPusher +import org.matrix.android.sdk.internal.session.pushers.JsonPusherData + +internal object JsonPusherFixture { + + fun aJsonPusher( + pushKey: String = "", + kind: String? = null, + appId: String = "", + appDisplayName: String? = null, + deviceDisplayName: String? = null, + profileTag: String? = null, + lang: String? = null, + data: JsonPusherData? = null, + append: Boolean? = false, + enabled: Boolean = true, + deviceId: String? = null, + ) = JsonPusher( + pushKey, + kind, + appId, + appDisplayName, + deviceDisplayName, + profileTag, + lang, + data, + append, + enabled, + deviceId, + ) +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/PusherEntityFixture.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/PusherEntityFixture.kt new file mode 100644 index 0000000000000000000000000000000000000000..8d048d4c9ab432e0c04bc0a5527fff89f01c3a5b --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/PusherEntityFixture.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.test.fixtures + +import org.matrix.android.sdk.internal.database.model.PusherDataEntity +import org.matrix.android.sdk.internal.database.model.PusherEntity + +internal object PusherEntityFixture { + + fun aPusherEntity( + pushKey: String = "", + kind: String? = null, + appId: String = "", + appDisplayName: String? = null, + deviceDisplayName: String? = null, + profileTag: String? = null, + lang: String? = null, + data: PusherDataEntity? = null, + enabled: Boolean = true, + deviceId: String? = null, + ) = PusherEntity( + pushKey, + kind, + appId, + appDisplayName, + deviceDisplayName, + profileTag, + lang, + data, + enabled, + deviceId, + ) +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/PusherFixture.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/PusherFixture.kt new file mode 100644 index 0000000000000000000000000000000000000000..0ac7885062bce6134bf503da0409680dc25325f8 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fixtures/PusherFixture.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.test.fixtures + +import org.matrix.android.sdk.api.session.pushers.Pusher +import org.matrix.android.sdk.api.session.pushers.PusherData +import org.matrix.android.sdk.api.session.pushers.PusherState + +object PusherFixture { + + fun aPusher( + pushKey: String = "", + kind: String = "", + appId: String = "", + appDisplayName: String? = "", + deviceDisplayName: String? = "", + profileTag: String? = null, + lang: String? = "", + data: PusherData = PusherData("f.o/_matrix/push/v1/notify", ""), + enabled: Boolean = true, + deviceId: String? = "", + state: PusherState = PusherState.REGISTERED, + ) = Pusher( + pushKey, + kind, + appId, + appDisplayName, + deviceDisplayName, + profileTag, + lang, + data, + enabled, + deviceId, + state, + ) +}