diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 77dffb43d2..34d7b40a88 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,6 @@ ### Pull Request Checklist - + - [ ] Changes has been tested on an Android device or Android emulator with API 21 - [ ] UI change has been tested on both light and dark themes diff --git a/AUTHORS.md b/AUTHORS.md index 3d9dffbef5..a85beb2d6f 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -1,4 +1,4 @@ -A full developer contributors list can be found [here](https://github.com/vector-im/riotX-android/graphs/contributors). +A full developer contributors list can be found [here](https://github.com/vector-im/element-android/graphs/contributors). # Core team: diff --git a/CHANGES.md b/CHANGES.md index fb404b971e..4d1be71a25 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,52 @@ +Changes in Element 1.0.6 (2020-09-08) +=================================================== + +Features ✨: + - List phone numbers and emails added to the Matrix account, and add emails and phone numbers to account (#44, #45) + +Improvements 🙌: + - You can now join room through permalink and within room directory search + - Add long click gesture to copy userId, user display name, room name, room topic and room alias (#1774) + - Fix several issues when uploading big files (#1889) + - Do not propose to verify session if there is only one session and 4S is not configured (#1901) + - Call screen does not use proximity sensor (#1735) + +Bugfix 🐛: + - Display name not shown under Settings/General (#1926) + - Editing message forgets line breaks and markdown (#1939) + - Words containing my name should not trigger notifications (#1781) + - Fix changing language issue + - Fix FontSize issue (#1483, #1787) + - Fix bad color for settings icon on Android < 24 (#1786) + - Change user or room avatar: when selecting Gallery, I'm not proposed to crop the selected image (#1590) + - Loudspeaker is always used (#1685) + - Fix uploads still don't work with room v6 (#1879) + - Can't handle ongoing call events in background (#1992) + - Handle room, user and group links by the Element app (#1795) + - Update associated site domain (#1833) + - Crash / Attachment viewer: Cannot draw a recycled Bitmap #2034 + - Login with Matrix-Id | Autodiscovery fails if identity server is invalid and Homeserver ok (#2027) + - Support for image compression on Android 10 + - Verification popup won't show + - Android 6: App crash when read Contact permission is granted (#2064) + - JSON for verification events leaks in to the room list (#1246) + - Replies to poll appears in timeline as unsupported events during sending (#1004) + +Translations 🗣: + - The SDK is now using SAS string translations from [Weblate Matrix-doc project](https://translate.riot.im/projects/matrix-doc/) (#1909) + - New translation to kabyle + +Build 🧱: + - Some dependencies have been upgraded (coroutine, recyclerView, appCompat, core-ktx, firebase-messaging) + - Buildkite: + New pipeline location: https://github.com/matrix-org/pipelines/blob/master/element-android/pipeline.yml + New build location: https://buildkite.com/matrix-dot-org/element-android + +Other changes: + - Use File extension functions to make code more concise (#1996) + - Create a script to import SAS strings (#1909) + - Support `data-mx-[bg-]color` attributes on `` tags. + Changes in Element 1.0.5 (2020-08-21) =================================================== diff --git a/README.md b/README.md index 1ec425793f..64c6c9d04d 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -[![Buildkite](https://badge.buildkite.com/657d3db27364448d69d54f66c690f7788bc6aa80a7628e37f3.svg?branch=develop)](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop) +[![Buildkite](https://badge.buildkite.com/ad0065c1b70f557cd3b1d3d68f9c2154010f83c4d6f71706a9.svg?branch=develop)](https://buildkite.com/matrix-dot-org/element-android/builds?branch=develop) [![Weblate](https://translate.riot.im/widgets/element-android/-/svg-badge.svg)](https://translate.riot.im/engage/element-android/?utm_source=widget) [![Element Android Matrix room #element-android:matrix.org](https://img.shields.io/matrix/element-android:matrix.org.svg?label=%23element-android:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#element-android:matrix.org) -[![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=vector.android.riotx&metric=alert_status)](https://sonarcloud.io/dashboard?id=vector.android.riotx) -[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=vector.android.riotx&metric=vulnerabilities)](https://sonarcloud.io/dashboard?id=vector.android.riotx) -[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=vector.android.riotx&metric=bugs)](https://sonarcloud.io/dashboard?id=vector.android.riotx) +[![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=im.vector.app.android&metric=alert_status)](https://sonarcloud.io/dashboard?id=im.vector.app.android) +[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=im.vector.app.android&metric=vulnerabilities)](https://sonarcloud.io/dashboard?id=im.vector.app.android) +[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=im.vector.app.android&metric=bugs)](https://sonarcloud.io/dashboard?id=im.vector.app.android) # Element Android @@ -14,11 +14,13 @@ It is a total rewrite of [Riot-Android](https://github.com/vector-im/riot-androi [Get it on Google Play](https://play.google.com/store/apps/details?id=im.vector.app) [Get it on F-Droid](https://f-droid.org/app/im.vector.app) -Nightly build: [![Buildkite](https://badge.buildkite.com/657d3db27364448d69d54f66c690f7788bc6aa80a7628e37f3.svg?branch=develop)](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop) +Nightly build: [![Buildkite](https://badge.buildkite.com/ad0065c1b70f557cd3b1d3d68f9c2154010f83c4d6f71706a9.svg?branch=develop)](https://buildkite.com/matrix-dot-org/element-android/builds?branch=develop) # New Android SDK -Element is based on a new Android SDK fully written in Kotlin (like Element). In order to make the early development as fast as possible, Element and the new SDK currently share the same git repository. We will make separate repos once the SDK is stable enough. +Element is based on a new Android SDK fully written in Kotlin (like Element). In order to make the early development as fast as possible, Element and the new SDK currently share the same git repository. + +At each Element release, the SDK module is copied to a dedicated repository: https://github.com/matrix-org/matrix-android-sdk2. That way, third party apps can add a regular gradle dependency to use it. So more details on how to do that here: https://github.com/matrix-org/matrix-android-sdk2. # Roadmap @@ -27,6 +29,6 @@ The team will work to add them on a regular basis. ## Contributing -Please refer to [CONTRIBUTING.md](https://github.com/vector-im/riotX-android/blob/develop/CONTRIBUTING.md) if you want to contribute on Matrix Android projects! +Please refer to [CONTRIBUTING.md](https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md) if you want to contribute on Matrix Android projects! Come chat with the community in the dedicated Matrix [room](https://matrix.to/#/#element-android:matrix.org). diff --git a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AnimatedImageViewHolder.kt b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AnimatedImageViewHolder.kt index 6b7818c612..96e6c92467 100644 --- a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AnimatedImageViewHolder.kt +++ b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AnimatedImageViewHolder.kt @@ -27,4 +27,9 @@ class AnimatedImageViewHolder constructor(itemView: View) : val imageLoaderProgress: ProgressBar = itemView.findViewById(R.id.imageLoaderProgress) internal val target = DefaultImageLoaderTarget(this, this.touchImageView) + + override fun onRecycled() { + super.onRecycled() + touchImageView.setImageDrawable(null) + } } diff --git a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/ImageLoaderTarget.kt b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/ImageLoaderTarget.kt index 1e5e88d91f..9166c2ce4f 100644 --- a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/ImageLoaderTarget.kt +++ b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/ImageLoaderTarget.kt @@ -50,6 +50,7 @@ internal class DefaultImageLoaderTarget(val holder: AnimatedImageViewHolder, pri override fun onLoadFailed(uid: String, errorDrawable: Drawable?) { if (holder.boundResourceUid != uid) return holder.imageLoaderProgress.isVisible = false + holder.touchImageView.setImageDrawable(errorDrawable) } override fun onResourceCleared(uid: String, placeholder: Drawable?) { @@ -77,11 +78,13 @@ internal class DefaultImageLoaderTarget(val holder: AnimatedImageViewHolder, pri override fun onResourceLoading(uid: String, placeholder: Drawable?) { if (holder.boundResourceUid != uid) return holder.imageLoaderProgress.isVisible = true + holder.touchImageView.setImageDrawable(placeholder) } override fun onLoadFailed(uid: String, errorDrawable: Drawable?) { if (holder.boundResourceUid != uid) return holder.imageLoaderProgress.isVisible = false + holder.touchImageView.setImageDrawable(errorDrawable) } override fun onResourceCleared(uid: String, placeholder: Drawable?) { diff --git a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoLoaderTarget.kt b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoLoaderTarget.kt index 78f46a320f..d88faba110 100644 --- a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoLoaderTarget.kt +++ b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoLoaderTarget.kt @@ -35,6 +35,7 @@ interface VideoLoaderTarget { fun onVideoFileLoading(uid: String) fun onVideoFileLoadFailed(uid: String) fun onVideoFileReady(uid: String, file: File) + fun onVideoURLReady(uid: String, path: String) } internal class DefaultVideoLoaderTarget(val holder: VideoViewHolder, private val contextView: ImageView) : VideoLoaderTarget { @@ -47,6 +48,8 @@ internal class DefaultVideoLoaderTarget(val holder: VideoViewHolder, private val } override fun onThumbnailResourceCleared(uid: String, placeholder: Drawable?) { + if (holder.boundResourceUid != uid) return + holder.thumbnailImage.setImageDrawable(placeholder) } override fun onThumbnailResourceReady(uid: String, resource: Drawable) { @@ -68,9 +71,19 @@ internal class DefaultVideoLoaderTarget(val holder: VideoViewHolder, private val override fun onVideoFileReady(uid: String, file: File) { if (holder.boundResourceUid != uid) return + arrangeForVideoReady() + holder.videoReady(file) + } + + override fun onVideoURLReady(uid: String, path: String) { + if (holder.boundResourceUid != uid) return + arrangeForVideoReady() + holder.videoReady(path) + } + + private fun arrangeForVideoReady() { holder.thumbnailImage.isVisible = false holder.loaderProgressBar.isVisible = false holder.videoView.isVisible = true - holder.videoReady(file) } } diff --git a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt index 32f449d6fe..4d8be6468b 100644 --- a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt +++ b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/VideoViewHolder.kt @@ -16,6 +16,7 @@ package im.vector.lib.attachmentviewer +import android.util.Log import android.view.View import android.widget.ImageView import android.widget.ProgressBar @@ -65,6 +66,13 @@ class VideoViewHolder constructor(itemView: View) : } } + fun videoReady(path: String) { + mVideoPath = path + if (isSelected) { + startPlaying() + } + } + fun videoFileLoadError() { } @@ -118,8 +126,13 @@ class VideoViewHolder constructor(itemView: View) : eventListener?.get()?.onEvent(AttachmentEvents.VideoEvent(isPlaying, progress, duration)) } } + try { + videoView.setVideoPath(mVideoPath) + } catch (failure: Throwable) { + // Couldn't open + Log.v(VideoViewHolder::class.java.name, "Failed to start video") + } - videoView.setVideoPath(mVideoPath) if (!wasPaused) { videoView.start() if (progress > 0) { diff --git a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/ZoomableImageViewHolder.kt b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/ZoomableImageViewHolder.kt index 019cd3202d..49378631e8 100644 --- a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/ZoomableImageViewHolder.kt +++ b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/ZoomableImageViewHolder.kt @@ -39,4 +39,9 @@ class ZoomableImageViewHolder constructor(itemView: View) : } internal val target = DefaultImageLoaderTarget.ZoomableImageTarget(this, touchImageView) + + override fun onRecycled() { + super.onRecycled() + touchImageView.setImageDrawable(null) + } } diff --git a/build.gradle b/build.gradle index fdaef6a2f7..f06d1859b5 100644 --- a/build.gradle +++ b/build.gradle @@ -81,15 +81,15 @@ apply plugin: 'org.sonarqube' sonarqube { properties { - property "sonar.projectName", "RiotX-Android" - property "sonar.projectKey", "vector.android.riotx" + property "sonar.projectName", "Element-Android" + property "sonar.projectKey", "im.vector.app.android" property "sonar.host.url", "https://sonarcloud.io" property "sonar.projectVersion", project(":vector").android.defaultConfig.versionName property "sonar.sourceEncoding", "UTF-8" - property "sonar.links.homepage", "https://github.com/vector-im/riotX-android/" - property "sonar.links.ci", "https://buildkite.com/matrix-dot-org/riotx-android" - property "sonar.links.scm", "https://github.com/vector-im/riotX-android/" - property "sonar.links.issue", "https://github.com/vector-im/riotX-android/issues" + property "sonar.links.homepage", "https://github.com/vector-im/element-android/" + property "sonar.links.ci", "https://buildkite.com/matrix-dot-org/element-android" + property "sonar.links.scm", "https://github.com/vector-im/element-android/" + property "sonar.links.issue", "https://github.com/vector-im/element-android/issues" property "sonar.organization", "new_vector_ltd_organization" property "sonar.login", project.hasProperty("SONAR_LOGIN") ? SONAR_LOGIN : "invalid" } @@ -100,11 +100,18 @@ project(":vector") { properties { property "sonar.sources", project(":vector").android.sourceSets.main.java.srcDirs // exclude source code from analyses separated by a colon (:) - // property "sonar.exclusions", "**/*.*" + // Exclude Java source + property "sonar.exclusions", "**/BugReporterMultipartBody.java" } } } +project(":diff-match-patch") { + sonarqube { + skipProject = true + } +} + //project(":matrix-sdk-android") { // sonarqube { // properties { diff --git a/docs/add_threePids.md b/docs/add_threePids.md new file mode 100644 index 0000000000..89fc92f329 --- /dev/null +++ b/docs/add_threePids.md @@ -0,0 +1,285 @@ +# Adding and removing ThreePids to an account + +## Add email + +### User enter the email + +> POST https://homeserver.org/_matrix/client/r0/account/3pid/email/requestToken + +```json +{ + "email": "alice@email-provider.org", + "client_secret": "TixzvOnw7nLEUdiQEmkHzkXKrY4HhiGh", + "send_attempt": 1 +} +``` + +#### The email is already added to an account + +400 + +```json +{ + "errcode": "M_THREEPID_IN_USE", + "error": "Email is already in use" +} +``` + +#### The email is free + +Wording: "We've sent you an email to verify your address. Please follow the instructions there and then click the button below." + +200 + +```json +{ + "sid": "bxyDHuJKsdkjMlTJ" +} +``` + +## User receive an e-mail + +> [homeserver.org] Validate your email +> +> A request to add an email address to your Matrix account has been received. If this was you, please click the link below to confirm adding this email: + https://homeserver.org/_matrix/client/unstable/add_threepid/email/submit_token?token=WUnEhQAmJrXupdEbXgdWvnVIKaGYZFsU&client_secret=TixzvOnw7nLEUdiQEmkHzkXKrY4HhiGh&sid=bxyDHuJKsdkjMlTJ +> +> If this was not you, you can safely ignore this email. Thank you. + +### User clicks on the link + +The browser displays the following message: + +> Your email has now been validated, please return to your client. You may now close this window. + +### User returns on Element + +User clicks on CONTINUE + +> POST https://homeserver.org/_matrix/client/r0/account/3pid/add + +```json +{ + "sid": "bxyDHuJKsdkjMlTJ", + "client_secret": "TixzvOnw7nLEUdiQEmkHzkXKrY4HhiGh" +} +``` + +401 User Interactive Authentication + +```json +{ + "session": "ppvvnozXCQZFaggUBlHJYPjA", + "flows": [ + { + "stages": [ + "m.login.password" + ] + } + ], + "params": { + } +} +``` + +### User enters his password + +POST https://homeserver.org/_matrix/client/r0/account/3pid/add + +```json +{ + "sid": "bxyDHuJKsdkjMlTJ", + "client_secret": "TixzvOnw7nLEUdiQEmkHzkXKrY4HhiGh", + "auth": { + "session": "ppvvnozXCQZFaggUBlHJYPjA", + "type": "m.login.password", + "user": "@benoitx:matrix.org", + "password": "weak_password" + } +} +``` + +#### The link has not been clicked + +400 + +```json +{ + "errcode": "M_THREEPID_AUTH_FAILED", + "error": "No validated 3pid session found" +} +``` + +#### Wrong password + +401 + +```json +{ + "session": "fXHOvoQsPMhEebVqTnIrzZJN", + "flows": [ + { + "stages": [ + "m.login.password" + ] + } + ], + "params": { + }, + "completed":[ + ], + "error": "Invalid password", + "errcode": "M_FORBIDDEN" +} +``` + +#### The link has been clicked and the account password is correct + +200 + +```json +{} +``` + +## Remove email + +### User want to remove an email from his account + +> POST https://homeserver.org/_matrix/client/r0/account/3pid/delete + +```json +{ + "medium": "email", + "address": "alice@email-provider.org" +} +``` + +#### Email was not bound to an identity server + +200 + +```json +{ + "id_server_unbind_result": "no-support" +} +``` + +#### Email was bound to an identity server + +200 + +```json +{ + "id_server_unbind_result": "success" +} +``` + +## Add phone number + +> POST https://homeserver.org/_matrix/client/r0/account/3pid/msisdn/requestToken + +```json +{ + "country": "FR", + "phone_number": "611223344", + "client_secret": "f1K29wFZBEr4RZYatu7xj8nEbXiVpr7J", + "send_attempt": 1 +} +``` + +Note that the phone number is sent without `+` and without the country code + +#### The phone number is already added to an account + +400 + +```json +{ + "errcode": "M_THREEPID_IN_USE", + "error": "MSISDN is already in use" +} +``` + +#### The phone number is free + +Wording: "A text message has been sent to +33611223344. Please enter the verification code it contains." + +200 + +```json +{ + "msisdn": "33651547677", + "intl_fmt": "+33 6 51 54 76 77", + "success": true, + "sid": "253299954", + "submit_url": "https://homeserver.org/_matrix/client/unstable/add_threepid/msisdn/submit_token" +} +``` + +## User receive a text message + +> Riot + +> Your Riot validation code is 892541, please enter this into the app + +### User enter the code to the app + +#### Wrong code + +> POST https://homeserver.org/_matrix/client/unstable/add_threepid/msisdn/submit_token + +```json +{ + "sid": "253299954", + "client_secret": "f1K29wFZBEr4RZYatu7xj8nEbXiVpr7J", + "token": "111111" +} +``` + +400 + +```json +{ + "errcode": "M_UNKNOWN", + "error": "Error contacting the identity server" +} +``` + +This is not an ideal, but the client will display a hint to check the entered code to the user. + +#### Correct code + +> POST https://homeserver.org/_matrix/client/unstable/add_threepid/msisdn/submit_token + +```json +{ + "sid": "253299954", + "client_secret": "f1K29wFZBEr4RZYatu7xj8nEbXiVpr7J", + "token": "892541" +} +``` + +200 + +```json +{ + "success": true +} +``` + +Then the app call `https://homeserver.org/_matrix/client/r0/account/3pid/add` as per adding an email and follow the same UIS flow + +## Remove phone number + +### User wants to remove a phone number from his account + +This is the same request and response than to remove email, but with this body: + +```json +{ + "medium": "msisdn", + "address": "33611223344" +} +``` + +Note that the phone number is provided without `+`, but with the country code. diff --git a/docs/signin.md b/docs/signin.md index f5ec03e708..06f715c46b 100644 --- a/docs/signin.md +++ b/docs/signin.md @@ -8,7 +8,9 @@ This document describes the flow of signin to a homeserver, and also the flow wh Client request the sign-in flows, once the homeserver is chosen by the user and its url is known (in the example it's `https://matrix.org`) -> curl -X GET 'https://matrix.org/_matrix/client/r0/login' +```shell script +curl -X GET 'https://matrix.org/_matrix/client/r0/login' +``` 200 @@ -26,7 +28,9 @@ Client request the sign-in flows, once the homeserver is chosen by the user and The user is able to connect using `m.login.password` -> curl -X POST --data $'{"identifier":{"type":"m.id.user","user":"alice"},"password":"weak_password","type":"m.login.password","initial_device_display_name":"Portable"}' 'https://matrix.org/_matrix/client/r0/login' +```shell script +curl -X POST --data $'{"identifier":{"type":"m.id.user","user":"alice"},"password":"weak_password","type":"m.login.password","initial_device_display_name":"Portable"}' 'https://matrix.org/_matrix/client/r0/login' +``` ```json { @@ -73,14 +77,16 @@ We get credential (200) If the user has associated an email with its account, he can signin using the email. -> curl -X POST --data $'{"identifier":{"type":"m.id.thirdparty","medium":"email","address":"alice@yopmail.com"},"password":"weak_password","type":"m.login.password","initial_device_display_name":"Portable"}' 'https://matrix.org/_matrix/client/r0/login' +```shell script +curl -X POST --data $'{"identifier":{"type":"m.id.thirdparty","medium":"email","address":"alice@email-provider.org"},"password":"weak_password","type":"m.login.password","initial_device_display_name":"Portable"}' 'https://matrix.org/_matrix/client/r0/login' +``` ```json { "identifier": { "type": "m.id.thirdparty", "medium": "email", - "address": "alice@yopmail.com" + "address": "alice@email-provider.org" }, "password": "weak_password", "type": "m.login.password", @@ -136,7 +142,9 @@ Not supported yet in Element ### Login with SSO -> curl -X GET 'https://homeserver.with.sso/_matrix/client/r0/login' +```shell script +curl -X GET 'https://homeserver.with.sso/_matrix/client/r0/login' +``` 200 @@ -171,7 +179,9 @@ Once the process is finished, the web page will call the `redirectUrl` with an e This navigation is intercepted by Element by the `LoginActivity`, which will then ask the homeserver to convert this `loginToken` to an access token -> curl -X POST --data $'{"type":"m.login.token","token":"MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy"}' 'https://homeserver.with.sso/_matrix/client/r0/login' +```shell script +curl -X POST --data $'{"type":"m.login.token","token":"MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy"}' 'https://homeserver.with.sso/_matrix/client/r0/login' +``` ```json { @@ -214,7 +224,9 @@ We display a warning regarding e2e. At the first step, we do not send the password, only the email and a client secret, generated by the application -> curl -X POST --data $'{"client_secret":"6c57f284-85e2-421b-8270-fb1795a120a7","send_attempt":0,"email":"user@domain.com"}' 'https://matrix.org/_matrix/client/r0/account/password/email/requestToken' +```shell script +curl -X POST --data $'{"client_secret":"6c57f284-85e2-421b-8270-fb1795a120a7","send_attempt":0,"email":"user@domain.com"}' 'https://matrix.org/_matrix/client/r0/account/password/email/requestToken' +``` ```json { @@ -251,7 +263,9 @@ During this step, the new password is sent to the homeserver. If the user confirms before the link is clicked, we get an error: -> curl -X POST --data $'{"auth":{"type":"m.login.email.identity","threepid_creds":{"client_secret":"6c57f284-85e2-421b-8270-fb1795a120a7","sid":"tQNbrREDACTEDldA"}},"new_password":"weak_password"}' 'https://matrix.org/_matrix/client/r0/account/password' +```shell script +curl -X POST --data $'{"auth":{"type":"m.login.email.identity","threepid_creds":{"client_secret":"6c57f284-85e2-421b-8270-fb1795a120a7","sid":"tQNbrREDACTEDldA"}},"new_password":"weak_password"}' 'https://matrix.org/_matrix/client/r0/account/password' +``` ```json { @@ -285,7 +299,9 @@ It contains the client secret, a token and the sid When the user click the link, if validate his ownership and the new password can now be ent by the application (on user demand): -> curl -X POST --data $'{"auth":{"type":"m.login.email.identity","threepid_creds":{"client_secret":"6c57f284-85e2-421b-8270-fb1795a120a7","sid":"tQNbrREDACTEDldA"}},"new_password":"weak_password"}' 'https://matrix.org/_matrix/client/r0/account/password' +```shell script +curl -X POST --data $'{"auth":{"type":"m.login.email.identity","threepid_creds":{"client_secret":"6c57f284-85e2-421b-8270-fb1795a120a7","sid":"tQNbrREDACTEDldA"}},"new_password":"weak_password"}' 'https://matrix.org/_matrix/client/r0/account/password' +``` ```json { diff --git a/docs/signup.md b/docs/signup.md index 995f5d50a6..97cd20a423 100644 --- a/docs/signup.md +++ b/docs/signup.md @@ -10,7 +10,9 @@ This document describes the flow of registration to a homeserver. Examples come Client request the sign-up flows, once the homeserver is chosen by the user and its url is known (in the example it's `https://matrix.org`) -> curl -X POST --data $'{}' 'https://matrix.org/_matrix/client/r0/register' +```shell script +curl -X POST --data $'{}' 'https://matrix.org/_matrix/client/r0/register' +``` ```json { @@ -70,7 +72,9 @@ If the registration is not possible, we get a 403 The app is displaying a form to enter username and password. -> curl -X POST --data $'{"initial_device_display_name":"Mobile device","username":"alice","password": "weak_password"}' 'https://matrix.org/_matrix/client/r0/register' +```shell script +curl -X POST --data $'{"initial_device_display_name":"Mobile device","username":"alice","password": "weak_password"}' 'https://matrix.org/_matrix/client/r0/register' +``` ```json { @@ -133,9 +137,11 @@ We get a 400: ### Step 2: entering email -User is proposed to enter an email. We skip this step. +User is proposed to enter an email. User skips this step. -> curl -X POST --data $'{"auth":{"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.dummy"}}' 'https://matrix.org/_matrix/client/r0/register' +```shell script +curl -X POST --data $'{"auth":{"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.dummy"}}' 'https://matrix.org/_matrix/client/r0/register' +``` ```json { @@ -189,16 +195,18 @@ User is proposed to enter an email. We skip this step. } ``` -### Step 2 bis: we enter an email +### Step 2 bis: user enters an email We request a token to the homeserver. The `client_secret` is generated by the application -> curl -X POST --data $'{"client_secret":"53e679ea-oRED-ACTED-92b8-3012c49c6cfa","email":"alice@yopmail.com","send_attempt":0}' 'https://matrix.org/_matrix/client/r0/register/email/requestToken' +```shell script +curl -X POST --data $'{"client_secret":"53e679ea-oRED-ACTED-92b8-3012c49c6cfa","email":"alice@email-provider.org","send_attempt":0}' 'https://matrix.org/_matrix/client/r0/register/email/requestToken' +``` ```json { "client_secret": "53e679ea-oRED-ACTED-92b8-3012c49c6cfa", - "email": "alice@yopmail.com", + "email": "alice@email-provider.org", "send_attempt": 0 } ``` @@ -213,7 +221,9 @@ We request a token to the homeserver. The `client_secret` is generated by the ap And -> curl -X POST --data $'{"auth":{"threepid_creds":{"client_secret":"53e679ea-oRED-ACTED-92b8-3012c49c6cfa","sid":"qlBCREDACTEDEtgxD"},"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.email.identity"}}' 'https://matrix.org/_matrix/client/r0/register' +```shell script +curl -X POST --data $'{"auth":{"threepid_creds":{"client_secret":"53e679ea-oRED-ACTED-92b8-3012c49c6cfa","sid":"qlBCREDACTEDEtgxD"},"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.email.identity"}}' 'https://matrix.org/_matrix/client/r0/register' +``` ```json { @@ -239,7 +249,9 @@ We get 401 since the email is not validated yet: The app is now polling on -> curl -X POST --data $'{"auth":{"threepid_creds":{"client_secret":"53e679ea-oRED-ACTED-92b8-3012c49c6cfa","sid":"qlBCREDACTEDEtgxD"},"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.email.identity"}}' 'https://matrix.org/_matrix/client/r0/register' +```shell script +curl -X POST --data $'{"auth":{"threepid_creds":{"client_secret":"53e679ea-oRED-ACTED-92b8-3012c49c6cfa","sid":"qlBCREDACTEDEtgxD"},"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.email.identity"}}' 'https://matrix.org/_matrix/client/r0/register' +``` ```json { @@ -254,7 +266,7 @@ The app is now polling on } ``` -We click on the link received by email `https://matrix.org/_matrix/client/unstable/registration/email/submit_token?token=vtQjQIZfwdoREDACTEDozrmKYSWlCXsJ&client_secret=53e679ea-oRED-ACTED-92b8-3012c49c6cfa&sid=qlBCREDACTEDEtgxD` which contains: +User clicks on the link received by email `https://matrix.org/_matrix/client/unstable/registration/email/submit_token?token=vtQjQIZfwdoREDACTEDozrmKYSWlCXsJ&client_secret=53e679ea-oRED-ACTED-92b8-3012c49c6cfa&sid=qlBCREDACTEDEtgxD` which contains: - A `token` vtQjQIZfwdoREDACTEDozrmKYSWlCXsJ - The `client_secret`: 53e679ea-oRED-ACTED-92b8-3012c49c6cfa - A `sid`: qlBCREDACTEDEtgxD @@ -306,7 +318,9 @@ Once the link is clicked, the registration request (polling) returns a 401 with User is proposed to accept T&C and he accepts them -> curl -X POST --data $'{"auth":{"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.terms"}}' 'https://matrix.org/_matrix/client/r0/register' +```shell script +curl -X POST --data $'{"auth":{"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.terms"}}' 'https://matrix.org/_matrix/client/r0/register' +``` ```json { @@ -365,7 +379,9 @@ User is proposed to accept T&C and he accepts them User is proposed to prove he is not a robot and he does it: -> curl -X POST --data $'{"auth":{"response":"03AOLTBLSiGS9GhFDpAMblJ2nlXOmHXqAYJ5OvHCPUjiVLBef3k9snOYI_BDC32-t4D2jv-tpvkaiEI_uloobFd9RUTPpJ7con2hMddbKjSCYqXqcUQFhzhbcX6kw8uBnh2sbwBe80_ihrHGXEoACXQkL0ki1Q0uEtOeW20YBRjbNABsZPpLNZhGIWC0QVXnQ4FouAtZrl3gOAiyM-oG3cgP6M9pcANIAC_7T2P2amAHbtsTlSR9CsazNyS-rtDR9b5MywdtnWN9Aw8fTJb8cXQk_j7nvugMxzofPjSOrPKcr8h5OqPlpUCyxxnFtag6cuaPSUwh43D2L0E-ZX7djzaY2Yh_U2n6HegFNPOQ22CJmfrKwDlodmAfMPvAXyq77n3HpoREDACTEDo3830RHF4BfkGXUaZjctgg-A1mvC17hmQmQpkG7IhDqyw0onU-0vF_-ehCjq_CcQEDpS_O3uiHJaG5xGf-0rhLm57v_wA3deugbsZuO4uTuxZZycN_mKxZ97jlDVBetl9hc_5REPbhcT1w3uzTCSx7Q","session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.recaptcha"}}' 'https://matrix.org/_matrix/client/r0/register' +```shell script +curl -X POST --data $'{"auth":{"response":"03AOLTBLSiGS9GhFDpAMblJ2nlXOmHXqAYJ5OvHCPUjiVLBef3k9snOYI_BDC32-t4D2jv-tpvkaiEI_uloobFd9RUTPpJ7con2hMddbKjSCYqXqcUQFhzhbcX6kw8uBnh2sbwBe80_ihrHGXEoACXQkL0ki1Q0uEtOeW20YBRjbNABsZPpLNZhGIWC0QVXnQ4FouAtZrl3gOAiyM-oG3cgP6M9pcANIAC_7T2P2amAHbtsTlSR9CsazNyS-rtDR9b5MywdtnWN9Aw8fTJb8cXQk_j7nvugMxzofPjSOrPKcr8h5OqPlpUCyxxnFtag6cuaPSUwh43D2L0E-ZX7djzaY2Yh_U2n6HegFNPOQ22CJmfrKwDlodmAfMPvAXyq77n3HpoREDACTEDo3830RHF4BfkGXUaZjctgg-A1mvC17hmQmQpkG7IhDqyw0onU-0vF_-ehCjq_CcQEDpS_O3uiHJaG5xGf-0rhLm57v_wA3deugbsZuO4uTuxZZycN_mKxZ97jlDVBetl9hc_5REPbhcT1w3uzTCSx7Q","session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.recaptcha"}}' 'https://matrix.org/_matrix/client/r0/register' +``` ```json { @@ -396,9 +412,11 @@ Some homeservers may require the user to enter MSISDN. On matrix.org, it's not required, and not even optional, but it's still possible for the app to add a MSISDN during the registration. -The user enter a phone number and select a country, the `client_secret` is generated by the application +The user enters a phone number and selects a country, the `client_secret` is generated by the application -> curl -X POST --data $'{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","send_attempt":1,"country":"FR","phone_number":"+33611223344"}' 'https://matrix.org/_matrix/client/r0/register/msisdn/requestToken' +```shell script +curl -X POST --data $'{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","send_attempt":1,"country":"FR","phone_number":"+33611223344"}' 'https://matrix.org/_matrix/client/r0/register/msisdn/requestToken' +``` ```json { @@ -430,10 +448,11 @@ If it is not the case, the homeserver send the SMS and returns some data, especi } ``` -When you execute the register request, with the received `sid`, you get an error since the MSISDN is not validated yet: - -> curl -X POST --data $'{"auth":{"type":"m.login.msisdn","session":"xptUYoREDACTEDogOWAGVnbJQ","threepid_creds":{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","sid":"1678881798"}}}' 'https://matrix.org/_matrix/client/r0/register' +When we execute the register request, with the received `sid`, we get an error since the MSISDN is not validated yet: +```shell script +curl -X POST --data $'{"auth":{"type":"m.login.msisdn","session":"xptUYoREDACTEDogOWAGVnbJQ","threepid_creds":{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","sid":"1678881798"}}}' 'https://matrix.org/_matrix/client/r0/register' +``` ```json "auth": { @@ -492,7 +511,9 @@ There is an issue on Synapse, which return a 401, it sends too much data along w The user receive the SMS, he can enter the SMS code in the app, which is sent using the "submit_url" received ie the response of the `requestToken` request: -> curl -X POST --data $'{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","sid":"1678881798","token":"123456"}' 'https://matrix.org/_matrix/client/unstable/add_threepid/msisdn/submit_token' +```shell script +curl -X POST --data $'{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","sid":"1678881798","token":"123456"}' 'https://matrix.org/_matrix/client/unstable/add_threepid/msisdn/submit_token' +``` ```json { @@ -520,7 +541,9 @@ And if the code is correct we get a 200 with: We can now execute the registration request, to the homeserver -> curl -X POST --data $'{"auth":{"type":"m.login.msisdn","session":"xptUYoREDACTEDogOWAGVnbJQ","threepid_creds":{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","sid":"1678881798"}}}' 'https://matrix.org/_matrix/client/r0/register' +```shell script +curl -X POST --data $'{"auth":{"type":"m.login.msisdn","session":"xptUYoREDACTEDogOWAGVnbJQ","threepid_creds":{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","sid":"1678881798"}}}' 'https://matrix.org/_matrix/client/r0/register' +``` ```json { @@ -535,7 +558,7 @@ We can now execute the registration request, to the homeserver } ``` -Now the homeserver consider that the `m.login.msisdn` step is completed (401): +Now the homeserver considers that the `m.login.msisdn` step is completed (401): ```json { diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt index 45efd125ee..55ede52c0c 100644 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt +++ b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxSession.kt @@ -18,9 +18,13 @@ package org.matrix.android.sdk.rx import androidx.paging.PagedList +import io.reactivex.Observable +import io.reactivex.Single +import io.reactivex.functions.Function3 import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo @@ -43,10 +47,6 @@ import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo -import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent -import io.reactivex.Observable -import io.reactivex.Single -import io.reactivex.functions.Function3 class RxSession(private val session: Session) { @@ -110,6 +110,11 @@ class RxSession(private val session: Session) { .startWithCallable { session.getThreePids() } } + fun livePendingThreePIds(): Observable> { + return session.getPendingThreePidsLive().asObservable() + .startWithCallable { session.getPendingThreePids() } + } + fun createRoom(roomParams: CreateRoomParams): Single = singleBuilder { session.createRoom(roomParams, it) } diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index d98533e4bc..2c20137647 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -112,18 +112,18 @@ dependencies { def moshi_version = '1.8.0' def lifecycle_version = '2.2.0' def arch_version = '2.1.0' - def coroutines_version = "1.3.2" + def coroutines_version = "1.3.8" def markwon_version = '3.1.0' def daggerVersion = '2.25.4' - def work_version = '2.3.3' + def work_version = '2.4.0' def retrofit_version = '2.6.2' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" - implementation "androidx.appcompat:appcompat:1.1.0" - implementation "androidx.core:core-ktx:1.3.0" + implementation "androidx.appcompat:appcompat:1.2.0" + implementation "androidx.core:core-ktx:1.3.1" implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" @@ -131,8 +131,6 @@ dependencies { // Network implementation "com.squareup.retrofit2:retrofit:$retrofit_version" implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version" - implementation "com.squareup.retrofit2:converter-scalars:$retrofit_version" - implementation(platform("com.squareup.okhttp3:okhttp-bom:4.8.1")) implementation 'com.squareup.okhttp3:okhttp' @@ -146,7 +144,6 @@ dependencies { // Image implementation 'androidx.exifinterface:exifinterface:1.3.0-alpha01' - implementation 'id.zelory:compressor:3.0.0' // Database implementation 'com.github.Zhuinden:realm-monarchy:0.5.1' diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt index df359f2adc..df26bb1227 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/api/Matrix.kt @@ -23,15 +23,12 @@ import androidx.work.WorkManager import com.zhuinden.monarchy.Monarchy import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.api.auth.AuthenticationService -import org.matrix.android.sdk.common.DaggerTestMatrixComponent import org.matrix.android.sdk.api.legacy.LegacySessionImporter +import org.matrix.android.sdk.common.DaggerTestMatrixComponent import org.matrix.android.sdk.internal.SessionManager -import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt -import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments import org.matrix.android.sdk.internal.network.UserAgentHolder import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver import org.matrix.olm.OlmManager -import java.io.InputStream import java.util.concurrent.Executors import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject @@ -96,9 +93,5 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo fun getSdkVersion(): String { return BuildConfig.VERSION_NAME + " (" + BuildConfig.GIT_SDK_REVISION + ")" } - - fun decryptStream(inputStream: InputStream?, elementToDecrypt: ElementToDecrypt): InputStream? { - return MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt) - } } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt index 05dbc40e1e..1e109f11ae 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/AttachmentEncryptionTest.kt @@ -19,17 +19,17 @@ package org.matrix.android.sdk.internal.crypto import android.os.MemoryFile import android.util.Base64 import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments -import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo -import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileKey import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals -import org.junit.Assert.assertNotNull import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters -import java.io.ByteArrayInputStream +import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo +import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileKey +import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt +import java.io.ByteArrayOutputStream import java.io.InputStream /** @@ -41,29 +41,26 @@ import java.io.InputStream class AttachmentEncryptionTest { private fun checkDecryption(input: String, encryptedFileInfo: EncryptedFileInfo): String { - val `in` = Base64.decode(input, Base64.DEFAULT) + val inputAsByteArray = Base64.decode(input, Base64.DEFAULT) val inputStream: InputStream - inputStream = if (`in`.isEmpty()) { - ByteArrayInputStream(`in`) + inputStream = if (inputAsByteArray.isEmpty()) { + inputAsByteArray.inputStream() } else { - val memoryFile = MemoryFile("file" + System.currentTimeMillis(), `in`.size) - memoryFile.outputStream.write(`in`) + val memoryFile = MemoryFile("file" + System.currentTimeMillis(), inputAsByteArray.size) + memoryFile.outputStream.write(inputAsByteArray) memoryFile.inputStream } - val decryptedStream = MXEncryptedAttachments.decryptAttachment(inputStream, encryptedFileInfo) + val decryptedStream = ByteArrayOutputStream() + val result = MXEncryptedAttachments.decryptAttachment(inputStream, encryptedFileInfo.toElementToDecrypt()!!, decryptedStream) - assertNotNull(decryptedStream) + assert(result) - val buffer = ByteArray(100) + val toByteArray = decryptedStream.toByteArray() - val len = decryptedStream!!.read(buffer) - - decryptedStream.close() - - return Base64.encodeToString(buffer, 0, len, Base64.DEFAULT).replace("\n".toRegex(), "").replace("=".toRegex(), "") + return Base64.encodeToString(toByteArray, 0, toByteArray.size, Base64.DEFAULT).replace("\n".toRegex(), "").replace("=".toRegex(), "") } @Test @@ -74,7 +71,7 @@ class AttachmentEncryptionTest { key = EncryptedFileKey( alg = "A256CTR", k = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", - key_ops = listOf("encrypt", "decrypt"), + keyOps = listOf("encrypt", "decrypt"), kty = "oct", ext = true ), @@ -93,7 +90,7 @@ class AttachmentEncryptionTest { key = EncryptedFileKey( alg = "A256CTR", k = "__________________________________________8", - key_ops = listOf("encrypt", "decrypt"), + keyOps = listOf("encrypt", "decrypt"), kty = "oct", ext = true ), @@ -112,7 +109,7 @@ class AttachmentEncryptionTest { key = EncryptedFileKey( alg = "A256CTR", k = "__________________________________________8", - key_ops = listOf("encrypt", "decrypt"), + keyOps = listOf("encrypt", "decrypt"), kty = "oct", ext = true ), @@ -133,7 +130,7 @@ class AttachmentEncryptionTest { key = EncryptedFileKey( alg = "A256CTR", k = "__________________________________________8", - key_ops = listOf("encrypt", "decrypt"), + keyOps = listOf("encrypt", "decrypt"), kty = "oct", ext = true ), diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt index 9b85310d50..eebaa93415 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt @@ -17,23 +17,22 @@ package org.matrix.android.sdk.internal.session.room.send import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.matrix.android.sdk.InstrumentedTest import org.commonmark.parser.Parser import org.commonmark.renderer.html.HtmlRenderer -import org.commonmark.renderer.text.TextContentRenderer import org.junit.Assert.assertEquals import org.junit.FixMethodOrder import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.MethodSorters +import org.matrix.android.sdk.InstrumentedTest /** * It will not be possible to test all combinations. For the moment I add a few tests, then, depending on the problem discovered in the wild, * we can add more tests to cover the edge cases. * Some tests are suffixed with `_not_passing`, maybe one day we will fix them... - * Riot-Web should be used as a reference for expected results, but not always. Especially Riot-Web add lots of `\n` in the - * formatted body, which is quite useless. - * Also Riot-Web does not provide plain text body when formatted text is provided. The body contains what the user has entered. + * Element Web should be used as a reference for expected results, but not always. + * Also Element Web does not provide plain text body when formatted text is provided. The body contains what the user has entered. We are doing + * the same to be able to edit messages (See #1939) * See https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes */ @Suppress("SpellCheckingInspection") @@ -46,8 +45,7 @@ class MarkdownParserTest : InstrumentedTest { */ private val markdownParser = MarkdownParser( Parser.builder().build(), - HtmlRenderer.builder().build(), - TextContentRenderer.builder().build() + HtmlRenderer.builder().build() ) @Test @@ -83,6 +81,15 @@ class MarkdownParserTest : InstrumentedTest { ) } + @Test + fun parseBoldNewLines() { + testTypeNewLines( + name = "bold", + markdownPattern = "**", + htmlExpectedTag = "strong" + ) + } + @Test fun parseItalic() { testType( @@ -92,14 +99,23 @@ class MarkdownParserTest : InstrumentedTest { ) } + @Test + fun parseItalicNewLines() { + testTypeNewLines( + name = "italic", + markdownPattern = "*", + htmlExpectedTag = "em" + ) + } + @Test fun parseItalic2() { - // Riot-Web format - "_italic_".let { markdownParser.parse(it) }.expect("italic", "italic") + // Element Web format + "_italic_".let { markdownParser.parse(it).expect(it, "italic") } } /** - * Note: the test is not passing, it does not work on Riot-Web neither + * Note: the test is not passing, it does not work on Element Web neither */ @Test fun parseStrike_not_passing() { @@ -110,14 +126,30 @@ class MarkdownParserTest : InstrumentedTest { ) } + @Test + fun parseStrikeNewLines() { + testTypeNewLines( + name = "strike", + markdownPattern = "~~", + htmlExpectedTag = "del" + ) + } + @Test fun parseCode() { testType( name = "code", markdownPattern = "`", - htmlExpectedTag = "code", - plainTextPrefix = "\"", - plainTextSuffix = "\"" + htmlExpectedTag = "code" + ) + } + + @Test + fun parseCodeNewLines() { + testTypeNewLines( + name = "code", + markdownPattern = "`", + htmlExpectedTag = "code" ) } @@ -126,9 +158,16 @@ class MarkdownParserTest : InstrumentedTest { testType( name = "code", markdownPattern = "``", - htmlExpectedTag = "code", - plainTextPrefix = "\"", - plainTextSuffix = "\"" + htmlExpectedTag = "code" + ) + } + + @Test + fun parseCode2NewLines() { + testTypeNewLines( + name = "code", + markdownPattern = "``", + htmlExpectedTag = "code" ) } @@ -137,78 +176,85 @@ class MarkdownParserTest : InstrumentedTest { testType( name = "code", markdownPattern = "```", - htmlExpectedTag = "code", - plainTextPrefix = "\"", - plainTextSuffix = "\"" + htmlExpectedTag = "code" + ) + } + + @Test + fun parseCode3NewLines() { + testTypeNewLines( + name = "code", + markdownPattern = "```", + htmlExpectedTag = "code" ) } @Test fun parseUnorderedList() { - "- item1".let { markdownParser.parse(it).expect(it, "
  • item1
") } - "- item1\n- item2".let { markdownParser.parse(it).expect(it, "
  • item1
  • item2
") } + "- item1".let { markdownParser.parse(it).expect(it, "
    \n
  • item1
  • \n
") } + "- item1\n- item2".let { markdownParser.parse(it).expect(it, "
    \n
  • item1
  • \n
  • item2
  • \n
") } } @Test fun parseOrderedList() { - "1. item1".let { markdownParser.parse(it).expect(it, "
  1. item1
") } - "1. item1\n2. item2".let { markdownParser.parse(it).expect(it, "
  1. item1
  2. item2
") } + "1. item1".let { markdownParser.parse(it).expect(it, "
    \n
  1. item1
  2. \n
") } + "1. item1\n2. item2".let { markdownParser.parse(it).expect(it, "
    \n
  1. item1
  2. \n
  3. item2
  4. \n
") } } @Test fun parseHorizontalLine() { - "---".let { markdownParser.parse(it) }.expect("***", "
") + "---".let { markdownParser.parse(it).expect(it, "
") } } @Test fun parseH2AndContent() { - "a\n---\nb".let { markdownParser.parse(it) }.expect("a\nb", "

a

b

") + "a\n---\nb".let { markdownParser.parse(it).expect(it, "

a

\n

b

") } } @Test fun parseQuote() { - "> quoted".let { markdownParser.parse(it) }.expect("«quoted»", "

quoted

") + "> quoted".let { markdownParser.parse(it).expect(it, "
\n

quoted

\n
") } } @Test fun parseQuote_not_passing() { - "> quoted\nline2".let { markdownParser.parse(it) }.expect("«quoted\nline2»", "

quoted
line2

") + "> quoted\nline2".let { markdownParser.parse(it).expect(it, "

quoted
line2

") } } @Test fun parseBoldItalic() { - "*italic* **bold**".let { markdownParser.parse(it) }.expect("italic bold", "italic bold") - "**bold** *italic*".let { markdownParser.parse(it) }.expect("bold italic", "bold italic") + "*italic* **bold**".let { markdownParser.parse(it).expect(it, "italic bold") } + "**bold** *italic*".let { markdownParser.parse(it).expect(it, "bold italic") } } @Test fun parseHead() { - "# head1".let { markdownParser.parse(it) }.expect("head1", "

head1

") - "## head2".let { markdownParser.parse(it) }.expect("head2", "

head2

") - "### head3".let { markdownParser.parse(it) }.expect("head3", "

head3

") - "#### head4".let { markdownParser.parse(it) }.expect("head4", "

head4

") - "##### head5".let { markdownParser.parse(it) }.expect("head5", "
head5
") - "###### head6".let { markdownParser.parse(it) }.expect("head6", "
head6
") + "# head1".let { markdownParser.parse(it).expect(it, "

head1

") } + "## head2".let { markdownParser.parse(it).expect(it, "

head2

") } + "### head3".let { markdownParser.parse(it).expect(it, "

head3

") } + "#### head4".let { markdownParser.parse(it).expect(it, "

head4

") } + "##### head5".let { markdownParser.parse(it).expect(it, "
head5
") } + "###### head6".let { markdownParser.parse(it).expect(it, "
head6
") } } @Test fun parseHeads() { - "# head1\n# head2".let { markdownParser.parse(it) }.expect("head1\nhead2", "

head1

head2

") + "# head1\n# head2".let { markdownParser.parse(it).expect(it, "

head1

\n

head2

") } } @Test fun parseBoldNewLines_not_passing() { - "**bold**\nline2".let { markdownParser.parse(it) }.expect("bold\nline2", "bold
line2") + "**bold**\nline2".let { markdownParser.parse(it).expect(it, "bold
line2") } } @Test fun parseLinks() { - "[link](target)".let { markdownParser.parse(it) }.expect(""""link" (target)""", """link""") + "[link](target)".let { markdownParser.parse(it).expect(it, """link""") } } @Test fun parseParagraph() { - "# head\ncontent".let { markdownParser.parse(it) }.expect("head\ncontent", "

head

content

") + "# head\ncontent".let { markdownParser.parse(it).expect(it, "

head

\n

content

") } } private fun testIdentity(text: String) { @@ -217,59 +263,93 @@ class MarkdownParserTest : InstrumentedTest { private fun testType(name: String, markdownPattern: String, - htmlExpectedTag: String, - plainTextPrefix: String = "", - plainTextSuffix: String = "") { + htmlExpectedTag: String) { // Test simple case "$markdownPattern$name$markdownPattern" - .let { markdownParser.parse(it) } - .expect(expectedText = "$plainTextPrefix$name$plainTextSuffix", - expectedFormattedText = "<$htmlExpectedTag>$name") + .let { + markdownParser.parse(it) + .expect(expectedText = it, + expectedFormattedText = "<$htmlExpectedTag>$name") + } // Test twice the same tag "$markdownPattern$name$markdownPattern and $markdownPattern$name bis$markdownPattern" - .let { markdownParser.parse(it) } - .expect(expectedText = "$plainTextPrefix$name$plainTextSuffix and $plainTextPrefix$name bis$plainTextSuffix", - expectedFormattedText = "<$htmlExpectedTag>$name and <$htmlExpectedTag>$name bis") + .let { + markdownParser.parse(it) + .expect(expectedText = it, + expectedFormattedText = "<$htmlExpectedTag>$name and <$htmlExpectedTag>$name bis") + } val textBefore = "a" val textAfter = "b" // With sticked text before "$textBefore$markdownPattern$name$markdownPattern" - .let { markdownParser.parse(it) } - .expect(expectedText = "$textBefore$plainTextPrefix$name$plainTextSuffix", - expectedFormattedText = "$textBefore<$htmlExpectedTag>$name") + .let { + markdownParser.parse(it) + .expect(expectedText = it, + expectedFormattedText = "$textBefore<$htmlExpectedTag>$name") + } // With text before and space "$textBefore $markdownPattern$name$markdownPattern" - .let { markdownParser.parse(it) } - .expect(expectedText = "$textBefore $plainTextPrefix$name$plainTextSuffix", - expectedFormattedText = "$textBefore <$htmlExpectedTag>$name") + .let { + markdownParser.parse(it) + .expect(expectedText = it, + expectedFormattedText = "$textBefore <$htmlExpectedTag>$name") + } // With sticked text after "$markdownPattern$name$markdownPattern$textAfter" - .let { markdownParser.parse(it) } - .expect(expectedText = "$plainTextPrefix$name$plainTextSuffix$textAfter", - expectedFormattedText = "<$htmlExpectedTag>$name$textAfter") + .let { + markdownParser.parse(it) + .expect(expectedText = it, + expectedFormattedText = "<$htmlExpectedTag>$name$textAfter") + } // With space and text after "$markdownPattern$name$markdownPattern $textAfter" - .let { markdownParser.parse(it) } - .expect(expectedText = "$plainTextPrefix$name$plainTextSuffix $textAfter", - expectedFormattedText = "<$htmlExpectedTag>$name $textAfter") + .let { + markdownParser.parse(it) + .expect(expectedText = it, + expectedFormattedText = "<$htmlExpectedTag>$name $textAfter") + } // With sticked text before and text after "$textBefore$markdownPattern$name$markdownPattern$textAfter" - .let { markdownParser.parse(it) } - .expect(expectedText = "$textBefore$plainTextPrefix$name$plainTextSuffix$textAfter", - expectedFormattedText = "a<$htmlExpectedTag>$name$textAfter") + .let { + markdownParser.parse(it) + .expect(expectedText = it, + expectedFormattedText = "a<$htmlExpectedTag>$name$textAfter") + } // With text before and after, with spaces "$textBefore $markdownPattern$name$markdownPattern $textAfter" - .let { markdownParser.parse(it) } - .expect(expectedText = "$textBefore $plainTextPrefix$name$plainTextSuffix $textAfter", - expectedFormattedText = "$textBefore <$htmlExpectedTag>$name $textAfter") + .let { + markdownParser.parse(it) + .expect(expectedText = it, + expectedFormattedText = "$textBefore <$htmlExpectedTag>$name $textAfter") + } + } + + private fun testTypeNewLines(name: String, + markdownPattern: String, + htmlExpectedTag: String) { + // With new line inside the block + "$markdownPattern$name\n$name$markdownPattern" + .let { + markdownParser.parse(it) + .expect(expectedText = it, + expectedFormattedText = "<$htmlExpectedTag>$name
$name") + } + + // With new line between two blocks + "$markdownPattern$name$markdownPattern\n$markdownPattern$name$markdownPattern" + .let { + markdownParser.parse(it) + .expect(expectedText = it, + expectedFormattedText = "<$htmlExpectedTag>$name<$htmlExpectedTag>$name") + } } private fun TextContent.expect(expectedText: String, expectedFormattedText: String?) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt index 6cd003ddae..aafefa2048 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/Matrix.kt @@ -26,13 +26,10 @@ import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.legacy.LegacySessionImporter import org.matrix.android.sdk.internal.SessionManager -import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt -import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments import org.matrix.android.sdk.internal.di.DaggerMatrixComponent import org.matrix.android.sdk.internal.network.UserAgentHolder import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver import org.matrix.olm.OlmManager -import java.io.InputStream import java.util.concurrent.Executors import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject @@ -97,9 +94,5 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo fun getSdkVersion(): String { return BuildConfig.VERSION_NAME + " (" + BuildConfig.GIT_SDK_REVISION + ")" } - - fun decryptStream(inputStream: InputStream?, elementToDecrypt: ElementToDecrypt): InputStream? { - return MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt) - } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/wellknown/WellknownResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/wellknown/WellknownResult.kt index a736a4f1be..ec2dfd214c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/wellknown/WellknownResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/wellknown/WellknownResult.kt @@ -45,7 +45,7 @@ sealed class WellknownResult { /** * Inform the user that auto-discovery failed due to invalid/empty data and PROMPT for the parameter. */ - object FailPrompt : WellknownResult() + data class FailPrompt(val homeServerUrl: String?, val wellKnown: WellKnown?) : WellknownResult() /** * Inform the user that auto-discovery did not return any usable URLs. Do not continue further with the current login process. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/Action.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/Action.kt index 7a21920e58..ff58650781 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/Action.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/Action.kt @@ -24,21 +24,24 @@ sealed class Action { object DoNotNotify : Action() data class Sound(val sound: String = ACTION_OBJECT_VALUE_VALUE_DEFAULT) : Action() data class Highlight(val highlight: Boolean) : Action() + + companion object { + const val ACTION_NOTIFY = "notify" + const val ACTION_DONT_NOTIFY = "dont_notify" + const val ACTION_COALESCE = "coalesce" + + // Ref: https://matrix.org/docs/spec/client_server/latest#tweaks + const val ACTION_OBJECT_SET_TWEAK_KEY = "set_tweak" + + const val ACTION_OBJECT_SET_TWEAK_VALUE_SOUND = "sound" + const val ACTION_OBJECT_SET_TWEAK_VALUE_HIGHLIGHT = "highlight" + + const val ACTION_OBJECT_VALUE_KEY = "value" + const val ACTION_OBJECT_VALUE_VALUE_DEFAULT = "default" + const val ACTION_OBJECT_VALUE_VALUE_RING = "ring" + } } -private const val ACTION_NOTIFY = "notify" -private const val ACTION_DONT_NOTIFY = "dont_notify" -private const val ACTION_COALESCE = "coalesce" - -// Ref: https://matrix.org/docs/spec/client_server/latest#tweaks -private const val ACTION_OBJECT_SET_TWEAK_KEY = "set_tweak" - -private const val ACTION_OBJECT_SET_TWEAK_VALUE_SOUND = "sound" -private const val ACTION_OBJECT_SET_TWEAK_VALUE_HIGHLIGHT = "highlight" - -private const val ACTION_OBJECT_VALUE_KEY = "value" -private const val ACTION_OBJECT_VALUE_VALUE_DEFAULT = "default" - /** * Ref: https://matrix.org/docs/spec/client_server/latest#actions * @@ -69,18 +72,18 @@ private const val ACTION_OBJECT_VALUE_VALUE_DEFAULT = "default" fun List.toJson(): List { return map { action -> when (action) { - is Action.Notify -> ACTION_NOTIFY - is Action.DoNotNotify -> ACTION_DONT_NOTIFY - is Action.Sound -> { + is Action.Notify -> Action.ACTION_NOTIFY + is Action.DoNotNotify -> Action.ACTION_DONT_NOTIFY + is Action.Sound -> { mapOf( - ACTION_OBJECT_SET_TWEAK_KEY to ACTION_OBJECT_SET_TWEAK_VALUE_SOUND, - ACTION_OBJECT_VALUE_KEY to action.sound + Action.ACTION_OBJECT_SET_TWEAK_KEY to Action.ACTION_OBJECT_SET_TWEAK_VALUE_SOUND, + Action.ACTION_OBJECT_VALUE_KEY to action.sound ) } - is Action.Highlight -> { + is Action.Highlight -> { mapOf( - ACTION_OBJECT_SET_TWEAK_KEY to ACTION_OBJECT_SET_TWEAK_VALUE_HIGHLIGHT, - ACTION_OBJECT_VALUE_KEY to action.highlight + Action.ACTION_OBJECT_SET_TWEAK_KEY to Action.ACTION_OBJECT_SET_TWEAK_VALUE_HIGHLIGHT, + Action.ACTION_OBJECT_VALUE_KEY to action.highlight ) } } @@ -92,26 +95,26 @@ fun PushRule.getActions(): List { actions.forEach { actionStrOrObj -> when (actionStrOrObj) { - ACTION_NOTIFY -> Action.Notify - ACTION_DONT_NOTIFY -> Action.DoNotNotify - is Map<*, *> -> { - when (actionStrOrObj[ACTION_OBJECT_SET_TWEAK_KEY]) { - ACTION_OBJECT_SET_TWEAK_VALUE_SOUND -> { - (actionStrOrObj[ACTION_OBJECT_VALUE_KEY] as? String)?.let { stringValue -> + Action.ACTION_NOTIFY -> Action.Notify + Action.ACTION_DONT_NOTIFY -> Action.DoNotNotify + is Map<*, *> -> { + when (actionStrOrObj[Action.ACTION_OBJECT_SET_TWEAK_KEY]) { + Action.ACTION_OBJECT_SET_TWEAK_VALUE_SOUND -> { + (actionStrOrObj[Action.ACTION_OBJECT_VALUE_KEY] as? String)?.let { stringValue -> Action.Sound(stringValue) } // When the value is not there, default sound (not specified by the spec) - ?: Action.Sound(ACTION_OBJECT_VALUE_VALUE_DEFAULT) + ?: Action.Sound(Action.ACTION_OBJECT_VALUE_VALUE_DEFAULT) } - ACTION_OBJECT_SET_TWEAK_VALUE_HIGHLIGHT -> { - (actionStrOrObj[ACTION_OBJECT_VALUE_KEY] as? Boolean)?.let { boolValue -> + Action.ACTION_OBJECT_SET_TWEAK_VALUE_HIGHLIGHT -> { + (actionStrOrObj[Action.ACTION_OBJECT_VALUE_KEY] as? Boolean)?.let { boolValue -> Action.Highlight(boolValue) } // When the value is not there, default is true, says the spec ?: Action.Highlight(true) } else -> { - Timber.w("Unsupported set_tweak value ${actionStrOrObj[ACTION_OBJECT_SET_TWEAK_KEY]}") + Timber.w("Unsupported set_tweak value ${actionStrOrObj[Action.ACTION_OBJECT_SET_TWEAK_KEY]}") null } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/Condition.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/Condition.kt index 50c2f8505b..3b1082ea0a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/Condition.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/Condition.kt @@ -18,32 +18,8 @@ package org.matrix.android.sdk.api.pushrules import org.matrix.android.sdk.api.session.events.model.Event -abstract class Condition(val kind: Kind) { +interface Condition { + fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean - enum class Kind(val value: String) { - EventMatch("event_match"), - ContainsDisplayName("contains_display_name"), - RoomMemberCount("room_member_count"), - SenderNotificationPermission("sender_notification_permission"), - Unrecognised(""); - - companion object { - - fun fromString(value: String): Kind { - return when (value) { - "event_match" -> EventMatch - "contains_display_name" -> ContainsDisplayName - "room_member_count" -> RoomMemberCount - "sender_notification_permission" -> SenderNotificationPermission - else -> Unrecognised - } - } - } - } - - abstract fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean - - open fun technicalDescription(): String { - return "Kind: $kind" - } + fun technicalDescription(): String } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/ContainsDisplayNameCondition.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/ContainsDisplayNameCondition.kt index a836c24c4e..72eda20679 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/ContainsDisplayNameCondition.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/ContainsDisplayNameCondition.kt @@ -20,17 +20,15 @@ import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.message.MessageContent -import timber.log.Timber +import org.matrix.android.sdk.internal.util.caseInsensitiveFind -class ContainsDisplayNameCondition : Condition(Kind.ContainsDisplayName) { +class ContainsDisplayNameCondition : Condition { override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean { return conditionResolver.resolveContainsDisplayNameCondition(event, this) } - override fun technicalDescription(): String { - return "User is mentioned" - } + override fun technicalDescription() = "User is mentioned" fun isSatisfied(event: Event, displayName: String): Boolean { val message = when (event.type) { @@ -45,31 +43,6 @@ class ContainsDisplayNameCondition : Condition(Kind.ContainsDisplayName) { else -> null } ?: return false - return caseInsensitiveFind(displayName, message.body) - } - - companion object { - /** - * Returns whether a string contains an occurrence of another, as a standalone word, regardless of case. - * - * @param subString the string to search for - * @param longString the string to search in - * @return whether a match was found - */ - fun caseInsensitiveFind(subString: String, longString: String): Boolean { - // add sanity checks - if (subString.isEmpty() || longString.isEmpty()) { - return false - } - - try { - val regex = Regex("(\\W|^)" + Regex.escape(subString) + "(\\W|$)", RegexOption.IGNORE_CASE) - return regex.containsMatchIn(longString) - } catch (e: Exception) { - Timber.e(e, "## caseInsensitiveFind() : failed") - } - - return false - } + return message.body.caseInsensitiveFind(displayName) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/EventMatchCondition.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/EventMatchCondition.kt index 5eed785899..c9aa0d001a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/EventMatchCondition.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/EventMatchCondition.kt @@ -18,6 +18,9 @@ package org.matrix.android.sdk.api.pushrules import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.util.caseInsensitiveFind +import org.matrix.android.sdk.internal.util.hasSpecialGlobChar +import org.matrix.android.sdk.internal.util.simpleGlobToRegExp import timber.log.Timber class EventMatchCondition( @@ -29,16 +32,18 @@ class EventMatchCondition( * The glob-style pattern to match against. Patterns with no special glob characters should * be treated as having asterisks prepended and appended when testing the condition. */ - val pattern: String -) : Condition(Kind.EventMatch) { + val pattern: String, + /** + * true to match only words. In this case pattern will not be considered as a glob + */ + val wordsOnly: Boolean +) : Condition { override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean { return conditionResolver.resolveEventMatchCondition(event, this) } - override fun technicalDescription(): String { - return "'$key' Matches '$pattern'" - } + override fun technicalDescription() = "'$key' matches '$pattern', words only '$wordsOnly'" fun isSatisfied(event: Event): Boolean { // TODO encrypted events? @@ -48,14 +53,18 @@ class EventMatchCondition( // Patterns with no special glob characters should be treated as having asterisks prepended // and appended when testing the condition. - try { - val modPattern = if (hasSpecialGlobChar(pattern)) simpleGlobToRegExp(pattern) else simpleGlobToRegExp("*$pattern*") - val regex = Regex(modPattern, RegexOption.DOT_MATCHES_ALL) - return regex.containsMatchIn(value) + return try { + if (wordsOnly) { + value.caseInsensitiveFind(pattern) + } else { + val modPattern = if (pattern.hasSpecialGlobChar()) pattern.simpleGlobToRegExp() else "*$pattern*".simpleGlobToRegExp() + val regex = Regex(modPattern, RegexOption.DOT_MATCHES_ALL) + regex.containsMatchIn(value) + } } catch (e: Throwable) { // e.g PatternSyntaxException Timber.e(e, "Failed to evaluate push condition") - return false + false } } @@ -78,27 +87,4 @@ class EventMatchCondition( } return null } - - companion object { - - private fun hasSpecialGlobChar(glob: String): Boolean { - return glob.contains("*") || glob.contains("?") - } - - // Very simple glob to regexp converter - private fun simpleGlobToRegExp(glob: String): String { - var out = "" // "^" - for (element in glob) { - when (element) { - '*' -> out += ".*" - '?' -> out += '.'.toString() - '.' -> out += "\\." - '\\' -> out += "\\\\" - else -> out += element - } - } - out += "" // '$'.toString() - return out - } - } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/Kind.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/Kind.kt new file mode 100644 index 0000000000..78f3a8a156 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/Kind.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.pushrules + +enum class Kind(val value: String) { + EventMatch("event_match"), + ContainsDisplayName("contains_display_name"), + RoomMemberCount("room_member_count"), + SenderNotificationPermission("sender_notification_permission"), + Unrecognised(""); + + companion object { + + fun fromString(value: String): Kind { + return when (value) { + "event_match" -> EventMatch + "contains_display_name" -> ContainsDisplayName + "room_member_count" -> RoomMemberCount + "sender_notification_permission" -> SenderNotificationPermission + else -> Unrecognised + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RoomMemberCountCondition.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RoomMemberCountCondition.kt index f97636a7bd..ba36c54fb4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RoomMemberCountCondition.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RoomMemberCountCondition.kt @@ -29,15 +29,13 @@ class RoomMemberCountCondition( * If no prefix is present, this parameter defaults to ==. */ val iz: String -) : Condition(Kind.RoomMemberCount) { +) : Condition { override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean { return conditionResolver.resolveRoomMemberCountCondition(event, this) } - override fun technicalDescription(): String { - return "Room member count is $iz" - } + override fun technicalDescription() = "Room member count is $iz" internal fun isSatisfied(event: Event, roomGetter: RoomGetter): Boolean { // sanity checks diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RuleIds.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RuleIds.kt index eeb2577d4c..2353d06252 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RuleIds.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/RuleIds.kt @@ -45,4 +45,6 @@ object RuleIds { // Not documented const val RULE_ID_FALLBACK = ".m.rule.fallback" + + const val RULE_ID_REACTION = ".m.rule.reaction" } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/SenderNotificationPermissionCondition.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/SenderNotificationPermissionCondition.kt index a8d08e5458..aeb2f01c80 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/SenderNotificationPermissionCondition.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/SenderNotificationPermissionCondition.kt @@ -28,15 +28,13 @@ class SenderNotificationPermissionCondition( * type from the notifications object in the power level event content. */ val key: String -) : Condition(Kind.SenderNotificationPermission) { +) : Condition { override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean { return conditionResolver.resolveSenderNotificationPermissionCondition(event, this) } - override fun technicalDescription(): String { - return "User power level <$key>" - } + override fun technicalDescription() = "User power level <$key>" fun isSatisfied(event: Event, powerLevels: PowerLevelsContent): Boolean { val powerLevelsHelper = PowerLevelsHelper(powerLevels) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/PushCondition.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/PushCondition.kt index 9469da3ea5..29466ca33b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/PushCondition.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/PushCondition.kt @@ -21,7 +21,9 @@ import com.squareup.moshi.JsonClass import org.matrix.android.sdk.api.pushrules.Condition import org.matrix.android.sdk.api.pushrules.ContainsDisplayNameCondition import org.matrix.android.sdk.api.pushrules.EventMatchCondition +import org.matrix.android.sdk.api.pushrules.Kind import org.matrix.android.sdk.api.pushrules.RoomMemberCountCondition +import org.matrix.android.sdk.api.pushrules.RuleIds import org.matrix.android.sdk.api.pushrules.SenderNotificationPermissionCondition import timber.log.Timber @@ -58,20 +60,20 @@ data class PushCondition( val iz: String? = null ) { - fun asExecutableCondition(): Condition? { - return when (Condition.Kind.fromString(kind)) { - Condition.Kind.EventMatch -> { + fun asExecutableCondition(rule: PushRule): Condition? { + return when (Kind.fromString(kind)) { + Kind.EventMatch -> { if (key != null && pattern != null) { - EventMatchCondition(key, pattern) + EventMatchCondition(key, pattern, rule.ruleId == RuleIds.RULE_ID_CONTAIN_USER_NAME) } else { Timber.e("Malformed Event match condition") null } } - Condition.Kind.ContainsDisplayName -> { + Kind.ContainsDisplayName -> { ContainsDisplayNameCondition() } - Condition.Kind.RoomMemberCount -> { + Kind.RoomMemberCount -> { if (iz.isNullOrEmpty()) { Timber.e("Malformed ROOM_MEMBER_COUNT condition") null @@ -79,7 +81,7 @@ data class PushCondition( RoomMemberCountCondition(iz) } } - Condition.Kind.SenderNotificationPermission -> { + Kind.SenderNotificationPermission -> { if (key == null) { Timber.e("Malformed Sender Notification Permission condition") null @@ -87,7 +89,7 @@ data class PushCondition( SenderNotificationPermissionCondition(key) } } - Condition.Kind.Unrecognised -> { + Kind.Unrecognised -> { Timber.e("Unknown kind $kind") null } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/PushRule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/PushRule.kt index 46d73a8aa2..bdb4f2cf29 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/PushRule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/PushRule.kt @@ -63,7 +63,7 @@ data class PushRule( * Add the default notification sound. */ fun setNotificationSound(): PushRule { - return setNotificationSound(ACTION_VALUE_DEFAULT) + return setNotificationSound(Action.ACTION_OBJECT_VALUE_VALUE_DEFAULT) } fun getNotificationSound(): String? { @@ -109,13 +109,13 @@ data class PushRule( fun setNotify(notify: Boolean): PushRule { val mutableActions = actions.toMutableList() - mutableActions.remove(ACTION_DONT_NOTIFY) - mutableActions.remove(ACTION_NOTIFY) + mutableActions.remove(Action.ACTION_DONT_NOTIFY) + mutableActions.remove(Action.ACTION_NOTIFY) if (notify) { - mutableActions.add(ACTION_NOTIFY) + mutableActions.add(Action.ACTION_NOTIFY) } else { - mutableActions.add(ACTION_DONT_NOTIFY) + mutableActions.add(Action.ACTION_DONT_NOTIFY) } return copy(actions = mutableActions) @@ -126,51 +126,12 @@ data class PushRule( * * @return true if the rule should play sound */ - fun shouldNotify() = actions.contains(ACTION_NOTIFY) + fun shouldNotify() = actions.contains(Action.ACTION_NOTIFY) /** * Return true if the rule should not highlight the event. * * @return true if the rule should not play sound */ - fun shouldNotNotify() = actions.contains(ACTION_DONT_NOTIFY) - - companion object { - /* ========================================================================================== - * Rule id - * ========================================================================================== */ - - const val RULE_ID_DISABLE_ALL = ".m.rule.master" - const val RULE_ID_CONTAIN_USER_NAME = ".m.rule.contains_user_name" - const val RULE_ID_CONTAIN_DISPLAY_NAME = ".m.rule.contains_display_name" - const val RULE_ID_ONE_TO_ONE_ROOM = ".m.rule.room_one_to_one" - const val RULE_ID_INVITE_ME = ".m.rule.invite_for_me" - const val RULE_ID_PEOPLE_JOIN_LEAVE = ".m.rule.member_event" - const val RULE_ID_CALL = ".m.rule.call" - const val RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS = ".m.rule.suppress_notices" - const val RULE_ID_ALL_OTHER_MESSAGES_ROOMS = ".m.rule.message" - const val RULE_ID_AT_ROOMS = ".m.rule.roomnotif" - const val RULE_ID_TOMBSTONE = ".m.rule.tombstone" - const val RULE_ID_E2E_ONE_TO_ONE_ROOM = ".m.rule.encrypted_room_one_to_one" - const val RULE_ID_E2E_GROUP = ".m.rule.encrypted" - const val RULE_ID_REACTION = ".m.rule.reaction" - const val RULE_ID_FALLBACK = ".m.rule.fallback" - - /* ========================================================================================== - * Actions - * ========================================================================================== */ - - const val ACTION_NOTIFY = "notify" - const val ACTION_DONT_NOTIFY = "dont_notify" - const val ACTION_COALESCE = "coalesce" - - const val ACTION_SET_TWEAK_SOUND_VALUE = "sound" - const val ACTION_SET_TWEAK_HIGHLIGHT_VALUE = "highlight" - - const val ACTION_PARAMETER_SET_TWEAK = "set_tweak" - const val ACTION_PARAMETER_VALUE = "value" - - const val ACTION_VALUE_DEFAULT = "default" - const val ACTION_VALUE_RING = "ring" - } + fun shouldNotNotify() = actions.contains(Action.ACTION_DONT_NOTIFY) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/RuleSet.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/RuleSet.kt index eb813dba45..a8708819f5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/RuleSet.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/pushrules/rest/RuleSet.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.pushrules.rest import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.pushrules.RuleIds import org.matrix.android.sdk.api.pushrules.RuleSetKey /** @@ -51,7 +52,7 @@ data class RuleSet( var result: PushRuleAndKind? = null // sanity check if (null != ruleId) { - if (PushRule.RULE_ID_CONTAIN_USER_NAME == ruleId) { + if (RuleIds.RULE_ID_CONTAIN_USER_NAME == ruleId) { result = findRule(content, ruleId)?.let { PushRuleAndKind(it, RuleSetKey.CONTENT) } } else { // assume that the ruleId is unique. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallSignalingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallSignalingService.kt index 2962f9fac3..382ab54248 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallSignalingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallSignalingService.kt @@ -34,4 +34,6 @@ interface CallSignalingService { fun removeCallListener(listener: CallsListener) fun getCallWithId(callId: String) : MxCall? + + fun isThereAnyActiveCall(): Boolean } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUploadStateTracker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUploadStateTracker.kt index a29e7110e2..a216770939 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUploadStateTracker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUploadStateTracker.kt @@ -33,7 +33,7 @@ interface ContentUploadStateTracker { object Idle : State() object EncryptingThumbnail : State() data class UploadingThumbnail(val current: Long, val total: Long) : State() - object Encrypting : State() + data class Encrypting(val current: Long, val total: Long) : State() data class Uploading(val current: Long, val total: Long) : State() object Success : State() data class Failure(val throwable: Throwable) : State() 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 fdd3e66703..1068b92019 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 @@ -239,6 +239,14 @@ fun Event.isVideoMessage(): Boolean { } } +fun Event.isAudioMessage(): Boolean { + return getClearType() == EventType.MESSAGE + && when (getClearContent()?.toModel()?.msgType) { + MessageType.MSGTYPE_AUDIO -> true + else -> false + } +} + fun Event.isFileMessage(): Boolean { return getClearType() == EventType.MESSAGE && when (getClearContent()?.toModel()?.msgType) { @@ -246,6 +254,16 @@ fun Event.isFileMessage(): Boolean { else -> false } } +fun Event.isAttachmentMessage(): Boolean { + return getClearType() == EventType.MESSAGE + && when (getClearContent()?.toModel()?.msgType) { + MessageType.MSGTYPE_IMAGE, + MessageType.MSGTYPE_AUDIO, + MessageType.MSGTYPE_VIDEO, + MessageType.MSGTYPE_FILE -> true + else -> false + } +} fun Event.getRelationContent(): RelationDefaultContent? { return if (isEncrypted()) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkData.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkData.kt index 85632d6e83..1da65b3002 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkData.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkData.kt @@ -25,7 +25,12 @@ import android.net.Uri */ sealed class PermalinkData { - data class RoomLink(val roomIdOrAlias: String, val isRoomAlias: Boolean, val eventId: String?) : PermalinkData() + data class RoomLink( + val roomIdOrAlias: String, + val isRoomAlias: Boolean, + val eventId: String?, + val viaParameters: List + ) : PermalinkData() data class UserLink(val userId: String) : PermalinkData() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkParser.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkParser.kt index dd6847f1e3..59e289ffd7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkParser.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkParser.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.permalinks import android.net.Uri +import android.net.UrlQuerySanitizer import org.matrix.android.sdk.api.MatrixPatterns /** @@ -40,14 +41,13 @@ object PermalinkParser { if (!uri.toString().startsWith(PermalinkService.MATRIX_TO_URL_BASE)) { return PermalinkData.FallbackLink(uri) } - val fragment = uri.fragment if (fragment.isNullOrEmpty()) { return PermalinkData.FallbackLink(uri) } - val indexOfQuery = fragment.indexOf("?") val safeFragment = if (indexOfQuery != -1) fragment.substring(0, indexOfQuery) else fragment + val viaQueryParameters = fragment.getViaParameters() // we are limiting to 2 params val params = safeFragment @@ -65,17 +65,29 @@ object PermalinkParser { PermalinkData.RoomLink( roomIdOrAlias = identifier, isRoomAlias = false, - eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) } + eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) }, + viaParameters = viaQueryParameters ) } MatrixPatterns.isRoomAlias(identifier) -> { PermalinkData.RoomLink( roomIdOrAlias = identifier, isRoomAlias = true, - eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) } + eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) }, + viaParameters = viaQueryParameters ) } else -> PermalinkData.FallbackLink(uri) } } + + private fun String.getViaParameters(): List { + return UrlQuerySanitizer(this) + .parameterList + .filter { + it.mParameter == "via" + }.map { + it.mValue + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt index 449c670983..15066cc4a6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt @@ -83,4 +83,43 @@ interface ProfileService { * @param refreshData set to true to fetch data from the homeserver */ fun getThreePidsLive(refreshData: Boolean): LiveData> + + /** + * Get the pending 3Pids, i.e. ThreePids that have requested a token, but not yet validated by the user. + */ + fun getPendingThreePids(): List + + /** + * Get the pending 3Pids Live + */ + fun getPendingThreePidsLive(): LiveData> + + /** + * Add a 3Pids. This is the first step to add a ThreePid to an account. Then the threePid will be added to the pending threePid list. + */ + fun addThreePid(threePid: ThreePid, matrixCallback: MatrixCallback): Cancelable + + /** + * Validate a code received by text message + */ + fun submitSmsCode(threePid: ThreePid.Msisdn, code: String, matrixCallback: MatrixCallback): Cancelable + + /** + * Finalize adding a 3Pids. Call this method once the user has validated that he owns the ThreePid + */ + fun finalizeAddingThreePid(threePid: ThreePid, + uiaSession: String?, + accountPassword: String?, + matrixCallback: MatrixCallback): Cancelable + + /** + * Cancel adding a threepid. It will remove locally stored data about this ThreePid + */ + fun cancelAddingThreePid(threePid: ThreePid, + matrixCallback: MatrixCallback): Cancelable + + /** + * Remove a 3Pid from the Matrix account. + */ + fun deleteThreePid(threePid: ThreePid, matrixCallback: MatrixCallback): Cancelable } 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 e84b75d0af..b8e536cb33 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 @@ -110,13 +110,13 @@ interface SendService { * Schedule this message to be resent * @param localEcho the unsent local echo */ - fun resendTextMessage(localEcho: TimelineEvent): Cancelable? + fun resendTextMessage(localEcho: TimelineEvent): Cancelable /** * Schedule this message to be resent * @param localEcho the unsent local echo */ - fun resendMediaMessage(localEcho: TimelineEvent): Cancelable? + fun resendMediaMessage(localEcho: TimelineEvent): Cancelable /** * Remove this failed message from the timeline @@ -124,8 +124,16 @@ interface SendService { */ fun deleteFailedEcho(localEcho: TimelineEvent) + /** + * Delete all the events in one of the sending states + */ fun clearSendingQueue() + /** + * Cancel sending a specific event. It has to be in one of the sending states + */ + fun cancelSend(eventId: String) + /** * Resend all failed messages one by one (and keep order) */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendState.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendState.kt index f0dd2f3025..be8849b20e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendState.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendState.kt @@ -37,7 +37,8 @@ enum class SendState { internal companion object { val HAS_FAILED_STATES = listOf(UNDELIVERED, FAILED_UNKNOWN_DEVICES) val IS_SENT_STATES = listOf(SENT, SYNCED) - val IS_SENDING_STATES = listOf(UNSENT, ENCRYPTING, SENDING) + val IS_PROGRESSING_STATES = listOf(ENCRYPTING, SENDING) + val IS_SENDING_STATES = IS_PROGRESSING_STATES + UNSENT val PENDING_STATES = IS_SENDING_STATES + HAS_FAILED_STATES } @@ -45,5 +46,7 @@ enum class SendState { fun hasFailed() = HAS_FAILED_STATES.contains(this) + fun isInProgress() = IS_PROGRESSING_STATES.contains(this) + fun isSending() = IS_SENDING_STATES.contains(this) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/AddThreePidRegistrationParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/AddThreePidRegistrationParams.kt index 7fbdaacb81..3e6e3054b5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/AddThreePidRegistrationParams.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/AddThreePidRegistrationParams.kt @@ -54,7 +54,7 @@ internal data class AddThreePidRegistrationParams( * This parameter is ignored when the homeserver handles 3PID verification. */ @Json(name = "id_server") - val id_server: String? = null, + val idServer: String? = null, /* ========================================================================================== * For emails diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt index 79b71b208e..676f40a918 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt @@ -17,6 +17,9 @@ package org.matrix.android.sdk.internal.auth.registration +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import okhttp3.OkHttpClient import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.auth.registration.RegisterThreePid @@ -33,9 +36,6 @@ import org.matrix.android.sdk.internal.auth.db.PendingSessionData import org.matrix.android.sdk.internal.network.RetrofitFactory import org.matrix.android.sdk.internal.task.launchToCallback import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay -import okhttp3.OkHttpClient /** * This class execute the registration request and is responsible to keep the session of interactive authentication @@ -193,7 +193,7 @@ internal class DefaultRegistrationWizard( val registrationParams = pendingSessionData.currentThreePidData?.registrationParams ?: throw IllegalStateException("developer error, no pending three pid") val safeCurrentData = pendingSessionData.currentThreePidData ?: throw IllegalStateException("developer error, call createAccount() method first") - val url = safeCurrentData.addThreePidRegistrationResponse.submitUrl ?: throw IllegalStateException("Missing url the send the code") + val url = safeCurrentData.addThreePidRegistrationResponse.submitUrl ?: throw IllegalStateException("Missing url to send the code") val validationBody = ValidationCodeBody( clientSecret = pendingSessionData.clientSecret, sid = safeCurrentData.addThreePidRegistrationResponse.sid, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationParams.kt index 4089e280d7..ca475566f1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationParams.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationParams.kt @@ -44,5 +44,6 @@ internal data class RegistrationParams( // Temporary flag to notify the server that we support msisdn flow. Used to prevent old app // versions to end up in fallback because the HS returns the msisdn flow which they don't support - val x_show_msisdn: Boolean? = null + @Json(name = "x_show_msisdn") + val xShowMsisdn: Boolean? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXMegolmExportEncryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXMegolmExportEncryption.kt index 4526ba8a51..85ecc540cd 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXMegolmExportEncryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/MXMegolmExportEncryption.kt @@ -145,7 +145,7 @@ object MXMegolmExportEncryption { */ @Throws(Exception::class) @JvmOverloads - fun encryptMegolmKeyFile(data: String, password: String, kdf_rounds: Int = DEFAULT_ITERATION_COUNT): ByteArray { + fun encryptMegolmKeyFile(data: String, password: String, kdfRounds: Int = DEFAULT_ITERATION_COUNT): ByteArray { if (password.isEmpty()) { throw Exception("Empty password is not supported") } @@ -163,7 +163,7 @@ object MXMegolmExportEncryption { // of a single bit of salt is a price we have to pay. iv[9] = iv[9] and 0x7f - val deriveKey = deriveKeys(salt, kdf_rounds, password) + val deriveKey = deriveKeys(salt, kdfRounds, password) val decryptCipher = Cipher.getInstance("AES/CTR/NoPadding") @@ -188,10 +188,10 @@ object MXMegolmExportEncryption { System.arraycopy(iv, 0, resultBuffer, idx, iv.size) idx += iv.size - resultBuffer[idx++] = (kdf_rounds shr 24 and 0xff).toByte() - resultBuffer[idx++] = (kdf_rounds shr 16 and 0xff).toByte() - resultBuffer[idx++] = (kdf_rounds shr 8 and 0xff).toByte() - resultBuffer[idx++] = (kdf_rounds and 0xff).toByte() + resultBuffer[idx++] = (kdfRounds shr 24 and 0xff).toByte() + resultBuffer[idx++] = (kdfRounds shr 16 and 0xff).toByte() + resultBuffer[idx++] = (kdfRounds shr 8 and 0xff).toByte() + resultBuffer[idx++] = (kdfRounds and 0xff).toByte() System.arraycopy(cipherArray, 0, resultBuffer, idx, cipherArray.size) idx += cipherArray.size @@ -320,26 +320,26 @@ object MXMegolmExportEncryption { // 512 bits key length val key = ByteArray(64) - val Uc = ByteArray(64) + val uc = ByteArray(64) // U1 = PRF(Password, Salt || INT_32_BE(i)) prf.update(salt) val int32BE = ByteArray(4) { 0.toByte() } int32BE[3] = 1.toByte() prf.update(int32BE) - prf.doFinal(Uc, 0) + prf.doFinal(uc, 0) // copy to the key - System.arraycopy(Uc, 0, key, 0, Uc.size) + System.arraycopy(uc, 0, key, 0, uc.size) for (index in 2..iterations) { // Uc = PRF(Password, Uc-1) - prf.update(Uc) - prf.doFinal(Uc, 0) + prf.update(uc) + prf.doFinal(uc, 0) // F(Password, Salt, c, i) = U1 ^ U2 ^ ... ^ Uc - for (byteIndex in Uc.indices) { - key[byteIndex] = key[byteIndex] xor Uc[byteIndex] + for (byteIndex in uc.indices) { + key[byteIndex] = key[byteIndex] xor uc[byteIndex] } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt index d4295e2cec..a4e3ee950d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/algorithms/olm/MXOlmDecryption.kt @@ -102,7 +102,7 @@ internal class MXOlmDecryption( String.format(MXCryptoError.BAD_RECIPIENT_REASON, olmPayloadContent.recipient)) } - val recipientKeys = olmPayloadContent.recipient_keys ?: run { + val recipientKeys = olmPayloadContent.recipientKeys ?: run { Timber.e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'recipient_keys'" + " property; cannot prevent unknown-key attack") throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY, @@ -129,10 +129,10 @@ internal class MXOlmDecryption( String.format(MXCryptoError.FORWARDED_MESSAGE_REASON, olmPayloadContent.sender)) } - if (olmPayloadContent.room_id != event.roomId) { - Timber.e("## decryptEvent() : Event ${event.eventId}: original room ${olmPayloadContent.room_id} does not match reported room ${event.roomId}") + if (olmPayloadContent.roomId != event.roomId) { + Timber.e("## decryptEvent() : Event ${event.eventId}: original room ${olmPayloadContent.roomId} does not match reported room ${event.roomId}") throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ROOM, - String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.room_id)) + String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.roomId)) } val keys = olmPayloadContent.keys ?: run { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt index cec1480d7b..11d5b4796a 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt @@ -20,10 +20,14 @@ package org.matrix.android.sdk.internal.crypto.attachments import android.util.Base64 import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileKey +import org.matrix.android.sdk.internal.util.base64ToBase64Url +import org.matrix.android.sdk.internal.util.base64ToUnpaddedBase64 +import org.matrix.android.sdk.internal.util.base64UrlToBase64 import timber.log.Timber -import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream +import java.io.File import java.io.InputStream +import java.io.OutputStream import java.security.MessageDigest import java.security.SecureRandom import javax.crypto.Cipher @@ -36,8 +40,121 @@ internal object MXEncryptedAttachments { private const val SECRET_KEY_SPEC_ALGORITHM = "AES" private const val MESSAGE_DIGEST_ALGORITHM = "SHA-256" + fun encrypt(clearStream: InputStream, mimetype: String?, outputFile: File, progress: ((current: Int, total: Int) -> Unit)): EncryptedFileInfo { + val t0 = System.currentTimeMillis() + val secureRandom = SecureRandom() + val initVectorBytes = ByteArray(16) { 0.toByte() } + + val ivRandomPart = ByteArray(8) + secureRandom.nextBytes(ivRandomPart) + + System.arraycopy(ivRandomPart, 0, initVectorBytes, 0, ivRandomPart.size) + + val key = ByteArray(32) + secureRandom.nextBytes(key) + + val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM) + + outputFile.outputStream().use { outputStream -> + val encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM) + val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM) + val ivParameterSpec = IvParameterSpec(initVectorBytes) + encryptCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec) + + val data = ByteArray(CRYPTO_BUFFER_SIZE) + var read: Int + var encodedBytes: ByteArray + clearStream.use { inputStream -> + val estimatedSize = inputStream.available() + progress.invoke(0, estimatedSize) + read = inputStream.read(data) + var totalRead = read + while (read != -1) { + progress.invoke(totalRead, estimatedSize) + encodedBytes = encryptCipher.update(data, 0, read) + messageDigest.update(encodedBytes, 0, encodedBytes.size) + outputStream.write(encodedBytes) + read = inputStream.read(data) + totalRead += read + } + } + + // encrypt the latest chunk + encodedBytes = encryptCipher.doFinal() + messageDigest.update(encodedBytes, 0, encodedBytes.size) + outputStream.write(encodedBytes) + } + + return EncryptedFileInfo( + url = null, + mimetype = mimetype, + key = EncryptedFileKey( + alg = "A256CTR", + ext = true, + keyOps = listOf("encrypt", "decrypt"), + kty = "oct", + k = base64ToBase64Url(Base64.encodeToString(key, Base64.DEFAULT)) + ), + iv = Base64.encodeToString(initVectorBytes, Base64.DEFAULT).replace("\n", "").replace("=", ""), + hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))), + v = "v2" + ) + .also { Timber.v("Encrypt in ${System.currentTimeMillis() - t0}ms") } + } + +// fun cipherInputStream(attachmentStream: InputStream, mimetype: String?): Pair { +// val secureRandom = SecureRandom() +// +// // generate a random iv key +// // Half of the IV is random, the lower order bits are zeroed +// // such that the counter never wraps. +// // See https://github.com/matrix-org/matrix-ios-kit/blob/3dc0d8e46b4deb6669ed44f72ad79be56471354c/MatrixKit/Models/Room/MXEncryptedAttachments.m#L75 +// val initVectorBytes = ByteArray(16) { 0.toByte() } +// +// val ivRandomPart = ByteArray(8) +// secureRandom.nextBytes(ivRandomPart) +// +// System.arraycopy(ivRandomPart, 0, initVectorBytes, 0, ivRandomPart.size) +// +// val key = ByteArray(32) +// secureRandom.nextBytes(key) +// +// val encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM) +// val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM) +// val ivParameterSpec = IvParameterSpec(initVectorBytes) +// encryptCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec) +// +// val cipherInputStream = CipherInputStream(attachmentStream, encryptCipher) +// +// // Could it be possible to get the digest on the fly instead of +// val info = EncryptedFileInfo( +// url = null, +// mimetype = mimetype, +// key = EncryptedFileKey( +// alg = "A256CTR", +// ext = true, +// key_ops = listOf("encrypt", "decrypt"), +// kty = "oct", +// k = base64ToBase64Url(Base64.encodeToString(key, Base64.DEFAULT)) +// ), +// iv = Base64.encodeToString(initVectorBytes, Base64.DEFAULT).replace("\n", "").replace("=", ""), +// //hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))), +// v = "v2" +// ) +// +// val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM) +// return DigestInputStream(cipherInputStream, messageDigest) to info +// } +// +// fun updateInfoWithDigest(digestInputStream: DigestInputStream, info: EncryptedFileInfo): EncryptedFileInfo { +// return info.copy( +// hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(digestInputStream.messageDigest.digest(), Base64.DEFAULT))) +// ) +// } + /*** * Encrypt an attachment stream. + * DO NOT USE for big files, it will load all in memory * @param attachmentStream the attachment stream. Will be closed after this method call. * @param mimetype the mime type * @return the encryption file info @@ -60,14 +177,14 @@ internal object MXEncryptedAttachments { val key = ByteArray(32) secureRandom.nextBytes(key) - ByteArrayOutputStream().use { outputStream -> + val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM) + val byteArrayOutputStream = ByteArrayOutputStream() + byteArrayOutputStream.use { outputStream -> val encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM) val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM) val ivParameterSpec = IvParameterSpec(initVectorBytes) encryptCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec) - val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM) - val data = ByteArray(CRYPTO_BUFFER_SIZE) var read: Int var encodedBytes: ByteArray @@ -86,44 +203,26 @@ internal object MXEncryptedAttachments { encodedBytes = encryptCipher.doFinal() messageDigest.update(encodedBytes, 0, encodedBytes.size) outputStream.write(encodedBytes) - - return EncryptionResult( - encryptedFileInfo = EncryptedFileInfo( - url = null, - mimetype = mimetype, - key = EncryptedFileKey( - alg = "A256CTR", - ext = true, - key_ops = listOf("encrypt", "decrypt"), - kty = "oct", - k = base64ToBase64Url(Base64.encodeToString(key, Base64.DEFAULT)) - ), - iv = Base64.encodeToString(initVectorBytes, Base64.DEFAULT).replace("\n", "").replace("=", ""), - hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))), - v = "v2" - ), - encryptedByteArray = outputStream.toByteArray() - ) - .also { Timber.v("Encrypt in ${System.currentTimeMillis() - t0}ms") } - } - } - - /** - * Decrypt an attachment - * - * @param attachmentStream the attachment stream. Will be closed after this method call. - * @param encryptedFileInfo the encryption file info - * @return the decrypted attachment stream - */ - fun decryptAttachment(attachmentStream: InputStream?, encryptedFileInfo: EncryptedFileInfo?): InputStream? { - if (encryptedFileInfo?.isValid() != true) { - Timber.e("## decryptAttachment() : some fields are not defined, or invalid key fields") - return null } - val elementToDecrypt = encryptedFileInfo.toElementToDecrypt() - - return decryptAttachment(attachmentStream, elementToDecrypt) + return EncryptionResult( + encryptedFileInfo = EncryptedFileInfo( + url = null, + mimetype = mimetype, + key = EncryptedFileKey( + alg = "A256CTR", + ext = true, + keyOps = listOf("encrypt", "decrypt"), + kty = "oct", + k = base64ToBase64Url(Base64.encodeToString(key, Base64.DEFAULT)) + ), + iv = Base64.encodeToString(initVectorBytes, Base64.DEFAULT).replace("\n", "").replace("=", ""), + hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))), + v = "v2" + ), + encryptedByteArray = byteArrayOutputStream.toByteArray() + ) + .also { Timber.v("Encrypt in ${System.currentTimeMillis() - t0}ms") } } /** @@ -131,84 +230,61 @@ internal object MXEncryptedAttachments { * * @param attachmentStream the attachment stream. Will be closed after this method call. * @param elementToDecrypt the elementToDecrypt info - * @return the decrypted attachment stream + * @param outputStream the outputStream where the decrypted attachment will be write. + * @return true in case of success, false in case of error */ - fun decryptAttachment(attachmentStream: InputStream?, elementToDecrypt: ElementToDecrypt?): InputStream? { + fun decryptAttachment(attachmentStream: InputStream?, elementToDecrypt: ElementToDecrypt?, outputStream: OutputStream): Boolean { // sanity checks if (null == attachmentStream || elementToDecrypt == null) { Timber.e("## decryptAttachment() : null stream") - return null + return false } val t0 = System.currentTimeMillis() - ByteArrayOutputStream().use { outputStream -> - try { - val key = Base64.decode(base64UrlToBase64(elementToDecrypt.k), Base64.DEFAULT) - val initVectorBytes = Base64.decode(elementToDecrypt.iv, Base64.DEFAULT) + try { + val key = Base64.decode(base64UrlToBase64(elementToDecrypt.k), Base64.DEFAULT) + val initVectorBytes = Base64.decode(elementToDecrypt.iv, Base64.DEFAULT) - val decryptCipher = Cipher.getInstance(CIPHER_ALGORITHM) - val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM) - val ivParameterSpec = IvParameterSpec(initVectorBytes) - decryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec) + val decryptCipher = Cipher.getInstance(CIPHER_ALGORITHM) + val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM) + val ivParameterSpec = IvParameterSpec(initVectorBytes) + decryptCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec) - val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM) + val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM) - var read: Int - val data = ByteArray(CRYPTO_BUFFER_SIZE) - var decodedBytes: ByteArray + var read: Int + val data = ByteArray(CRYPTO_BUFFER_SIZE) + var decodedBytes: ByteArray - attachmentStream.use { inputStream -> + attachmentStream.use { inputStream -> + read = inputStream.read(data) + while (read != -1) { + messageDigest.update(data, 0, read) + decodedBytes = decryptCipher.update(data, 0, read) + outputStream.write(decodedBytes) read = inputStream.read(data) - while (read != -1) { - messageDigest.update(data, 0, read) - decodedBytes = decryptCipher.update(data, 0, read) - outputStream.write(decodedBytes) - read = inputStream.read(data) - } } - - // decrypt the last chunk - decodedBytes = decryptCipher.doFinal() - outputStream.write(decodedBytes) - - val currentDigestValue = base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT)) - - if (elementToDecrypt.sha256 != currentDigestValue) { - Timber.e("## decryptAttachment() : Digest value mismatch") - return null - } - - return ByteArrayInputStream(outputStream.toByteArray()) - .also { Timber.v("Decrypt in ${System.currentTimeMillis() - t0}ms") } - } catch (oom: OutOfMemoryError) { - Timber.e(oom, "## decryptAttachment() failed: OOM") - } catch (e: Exception) { - Timber.e(e, "## decryptAttachment() failed") } + + // decrypt the last chunk + decodedBytes = decryptCipher.doFinal() + outputStream.write(decodedBytes) + + val currentDigestValue = base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT)) + + if (elementToDecrypt.sha256 != currentDigestValue) { + Timber.e("## decryptAttachment() : Digest value mismatch") + return false + } + + return true.also { Timber.v("Decrypt in ${System.currentTimeMillis() - t0}ms") } + } catch (oom: OutOfMemoryError) { + Timber.e(oom, "## decryptAttachment() failed: OOM") + } catch (e: Exception) { + Timber.e(e, "## decryptAttachment() failed") } - return null - } - - /** - * Base64 URL conversion methods - */ - - private fun base64UrlToBase64(base64Url: String): String { - return base64Url.replace('-', '+') - .replace('_', '/') - } - - internal fun base64ToBase64Url(base64: String): String { - return base64.replace("\n".toRegex(), "") - .replace("\\+".toRegex(), "-") - .replace('/', '_') - .replace("=", "") - } - - private fun base64ToUnpaddedBase64(base64: String): String { - return base64.replace("\n".toRegex(), "") - .replace("=", "") + return false } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MatrixDigestCheckInputStream.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MatrixDigestCheckInputStream.kt new file mode 100644 index 0000000000..7ca5158f64 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MatrixDigestCheckInputStream.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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.attachments + +import android.util.Base64 +import org.matrix.android.sdk.internal.util.base64ToUnpaddedBase64 +import java.io.FilterInputStream +import java.io.IOException +import java.io.InputStream +import java.security.MessageDigest + +class MatrixDigestCheckInputStream( + inputStream: InputStream?, + private val expectedDigest: String +) : FilterInputStream(inputStream) { + + private val digest = MessageDigest.getInstance("SHA-256") + + @Throws(IOException::class) + override fun read(): Int { + val b = `in`.read() + if (b >= 0) { + digest.update(b.toByte()) + } + + if (b == -1) { + ensureDigest() + } + return b + } + + @Throws(IOException::class) + override fun read( + b: ByteArray, + off: Int, + len: Int): Int { + val n = `in`.read(b, off, len) + if (n > 0) { + digest.update(b, off, n) + } + + if (n == -1) { + ensureDigest() + } + return n + } + + @Throws(IOException::class) + private fun ensureDigest() { + val currentDigestValue = base64ToUnpaddedBase64(Base64.encodeToString(digest.digest(), Base64.DEFAULT)) + if (currentDigestValue != expectedDigest) { + throw IOException("Bad digest") + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/OlmPayloadContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/OlmPayloadContent.kt index a3a9ee2e51..bf18cad0f4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/OlmPayloadContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/event/OlmPayloadContent.kt @@ -16,6 +16,7 @@ */ package org.matrix.android.sdk.internal.crypto.model.event +import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import org.matrix.android.sdk.internal.di.MoshiProvider @@ -27,27 +28,32 @@ data class OlmPayloadContent( /** * The room id */ - var room_id: String? = null, + @Json(name = "room_id") + val roomId: String? = null, /** * The sender */ - var sender: String? = null, + @Json(name = "sender") + val sender: String? = null, /** * The recipient */ - var recipient: String? = null, + @Json(name = "recipient") + val recipient: String? = null, /** * the recipient keys */ - var recipient_keys: Map? = null, + @Json(name = "recipient_keys") + val recipientKeys: Map? = null, /** * The keys */ - var keys: Map? = null + @Json(name = "keys") + val keys: Map? = null ) { fun toJsonString(): String { return MoshiProvider.providesMoshi().adapter(OlmPayloadContent::class.java).toJson(this) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceInfo.kt index 97c7c59b50..0c6d03613a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeviceInfo.kt @@ -31,7 +31,7 @@ data class DeviceInfo( * The owner user id (not documented and useless but the homeserver sent it. You should not need it) */ @Json(name = "user_id") - val user_id: String? = null, + val userId: String? = null, /** * The device id diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedFileKey.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedFileKey.kt index f0a680cfd3..077fd4451f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedFileKey.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/EncryptedFileKey.kt @@ -37,7 +37,7 @@ data class EncryptedFileKey( * Required. Key operations. Must at least contain "encrypt" and "decrypt". */ @Json(name = "key_ops") - val key_ops: List? = null, + val keyOps: List? = null, /** * Required. Key type. Must be "oct". @@ -63,7 +63,7 @@ data class EncryptedFileKey( return false } - if (key_ops?.contains("encrypt") != true || !key_ops.contains("decrypt")) { + if (keyOps?.contains("encrypt") != true || !keyOps.contains("decrypt")) { return false } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/Helper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/Helper.kt index 978c82303e..67e06b5455 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/Helper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/Helper.kt @@ -21,7 +21,6 @@ import android.util.Base64 import io.realm.Realm import io.realm.RealmConfiguration import io.realm.RealmObject -import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.ObjectOutputStream import java.util.zip.GZIPInputStream @@ -96,7 +95,7 @@ fun deserializeFromRealm(string: String?): T? { } val decodedB64 = Base64.decode(string.toByteArray(), Base64.DEFAULT) - val bais = ByteArrayInputStream(decodedB64) + val bais = decodedB64.inputStream() val gzis = GZIPInputStream(bais) val ois = SafeObjectInputStream(gzis) return ois.use { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/SafeObjectInputStream.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/SafeObjectInputStream.kt index 17538c7cbe..3f811ed7d2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/SafeObjectInputStream.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/SafeObjectInputStream.kt @@ -28,7 +28,7 @@ import java.io.ObjectStreamClass * * Ref: https://stackoverflow.com/questions/3884492/how-can-i-change-package-for-a-bunch-of-java-serializable-classes */ -internal class SafeObjectInputStream(`in`: InputStream) : ObjectInputStream(`in`) { +internal class SafeObjectInputStream(inputStream: InputStream) : ObjectInputStream(inputStream) { init { enableResolveObject(true) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt index 7ebd3b51b0..17eb33c886 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/SASDefaultVerificationTransaction.kt @@ -175,8 +175,8 @@ internal abstract class SASDefaultVerificationTransaction( ?.unpaddedBase64PublicKey ?.let { masterPublicKey -> val crossSigningKeyId = "ed25519:$masterPublicKey" - macUsingAgreedMethod(masterPublicKey, baseInfo + crossSigningKeyId)?.let { MSKMacString -> - keyMap[crossSigningKeyId] = MSKMacString + macUsingAgreedMethod(masterPublicKey, baseInfo + crossSigningKeyId)?.let { mskMacString -> + keyMap[crossSigningKeyId] = mskMacString } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationEmoji.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationEmoji.kt index 5a55ec2a9c..ae5852452a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationEmoji.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationEmoji.kt @@ -55,14 +55,14 @@ internal fun getEmojiForCode(code: Int): EmojiRepresentation { 31 -> EmojiRepresentation("🤖", R.string.verification_emoji_robot, R.drawable.ic_verification_robot) 32 -> EmojiRepresentation("🎩", R.string.verification_emoji_hat, R.drawable.ic_verification_hat) 33 -> EmojiRepresentation("👓", R.string.verification_emoji_glasses, R.drawable.ic_verification_glasses) - 34 -> EmojiRepresentation("🔧", R.string.verification_emoji_wrench, R.drawable.ic_verification_wrench) + 34 -> EmojiRepresentation("🔧", R.string.verification_emoji_spanner, R.drawable.ic_verification_spanner) 35 -> EmojiRepresentation("🎅", R.string.verification_emoji_santa, R.drawable.ic_verification_santa) - 36 -> EmojiRepresentation("👍", R.string.verification_emoji_thumbsup, R.drawable.ic_verification_thumbs_up) + 36 -> EmojiRepresentation("👍", R.string.verification_emoji_thumbs_up, R.drawable.ic_verification_thumbs_up) 37 -> EmojiRepresentation("☂️", R.string.verification_emoji_umbrella, R.drawable.ic_verification_umbrella) 38 -> EmojiRepresentation("⌛", R.string.verification_emoji_hourglass, R.drawable.ic_verification_hourglass) 39 -> EmojiRepresentation("⏰", R.string.verification_emoji_clock, R.drawable.ic_verification_clock) 40 -> EmojiRepresentation("🎁", R.string.verification_emoji_gift, R.drawable.ic_verification_gift) - 41 -> EmojiRepresentation("💡", R.string.verification_emoji_lightbulb, R.drawable.ic_verification_light_bulb) + 41 -> EmojiRepresentation("💡", R.string.verification_emoji_light_bulb, R.drawable.ic_verification_light_bulb) 42 -> EmojiRepresentation("📕", R.string.verification_emoji_book, R.drawable.ic_verification_book) 43 -> EmojiRepresentation("✏️", R.string.verification_emoji_pencil, R.drawable.ic_verification_pencil) 44 -> EmojiRepresentation("📎", R.string.verification_emoji_paperclip, R.drawable.ic_verification_paperclip) @@ -74,7 +74,7 @@ internal fun getEmojiForCode(code: Int): EmojiRepresentation { 50 -> EmojiRepresentation("🏁", R.string.verification_emoji_flag, R.drawable.ic_verification_flag) 51 -> EmojiRepresentation("🚂", R.string.verification_emoji_train, R.drawable.ic_verification_train) 52 -> EmojiRepresentation("🚲", R.string.verification_emoji_bicycle, R.drawable.ic_verification_bicycle) - 53 -> EmojiRepresentation("✈️", R.string.verification_emoji_airplane, R.drawable.ic_verification_airplane) + 53 -> EmojiRepresentation("✈️", R.string.verification_emoji_aeroplane, R.drawable.ic_verification_aeroplane) 54 -> EmojiRepresentation("🚀", R.string.verification_emoji_rocket, R.drawable.ic_verification_rocket) 55 -> EmojiRepresentation("🏆", R.string.verification_emoji_trophy, R.drawable.ic_verification_trophy) 56 -> EmojiRepresentation("⚽", R.string.verification_emoji_ball, R.drawable.ic_verification_ball) @@ -82,7 +82,7 @@ internal fun getEmojiForCode(code: Int): EmojiRepresentation { 58 -> EmojiRepresentation("🎺", R.string.verification_emoji_trumpet, R.drawable.ic_verification_trumpet) 59 -> EmojiRepresentation("🔔", R.string.verification_emoji_bell, R.drawable.ic_verification_bell) 60 -> EmojiRepresentation("⚓", R.string.verification_emoji_anchor, R.drawable.ic_verification_anchor) - 61 -> EmojiRepresentation("🎧", R.string.verification_emoji_headphone, R.drawable.ic_verification_headphone) + 61 -> EmojiRepresentation("🎧", R.string.verification_emoji_headphones, R.drawable.ic_verification_headphones) 62 -> EmojiRepresentation("📁", R.string.verification_emoji_folder, R.drawable.ic_verification_folder) /* 63 */ else -> EmojiRepresentation("📌", R.string.verification_emoji_pin, R.drawable.ic_verification_pin) } 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 7d2a4ea581..ad05406aa0 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 @@ -20,18 +20,24 @@ package org.matrix.android.sdk.internal.database import io.realm.DynamicRealm import io.realm.RealmMigration import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields +import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields import timber.log.Timber import javax.inject.Inject class RealmSessionStoreMigration @Inject constructor() : RealmMigration { + companion object { + const val SESSION_STORE_SCHEMA_VERSION = 4L + } + override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { Timber.v("Migrating Realm Session from $oldVersion to $newVersion") if (oldVersion <= 0) migrateTo1(realm) if (oldVersion <= 1) migrateTo2(realm) if (oldVersion <= 2) migrateTo3(realm) + if (oldVersion <= 3) migrateTo4(realm) } private fun migrateTo1(realm: DynamicRealm) { @@ -63,4 +69,17 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { obj.setLong(HomeServerCapabilitiesEntityFields.LAST_UPDATED_TIMESTAMP, 0) } } + + private fun migrateTo4(realm: DynamicRealm) { + Timber.d("Step 3 -> 4") + realm.schema.create("PendingThreePidEntity") + .addField(PendingThreePidEntityFields.CLIENT_SECRET, String::class.java) + .setRequired(PendingThreePidEntityFields.CLIENT_SECRET, true) + .addField(PendingThreePidEntityFields.EMAIL, String::class.java) + .addField(PendingThreePidEntityFields.MSISDN, String::class.java) + .addField(PendingThreePidEntityFields.SEND_ATTEMPT, Int::class.java) + .addField(PendingThreePidEntityFields.SID, String::class.java) + .setRequired(PendingThreePidEntityFields.SID, true) + .addField(PendingThreePidEntityFields.SUBMIT_URL, String::class.java) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/SessionRealmConfigurationFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/SessionRealmConfigurationFactory.kt index 456eecc54a..d5c259050f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/SessionRealmConfigurationFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/SessionRealmConfigurationFactory.kt @@ -19,13 +19,14 @@ package org.matrix.android.sdk.internal.database import android.content.Context import androidx.core.content.edit +import io.realm.Realm +import io.realm.RealmConfiguration +import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.internal.database.model.SessionRealmModule import org.matrix.android.sdk.internal.di.SessionFilesDirectory import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.di.UserMd5 import org.matrix.android.sdk.internal.session.SessionModule -import io.realm.Realm -import io.realm.RealmConfiguration import timber.log.Timber import java.io.File import javax.inject.Inject @@ -46,20 +47,16 @@ internal class SessionRealmConfigurationFactory @Inject constructor( val migration: RealmSessionStoreMigration, context: Context) { - companion object { - const val SESSION_STORE_SCHEMA_VERSION = 3L - } - // Keep legacy preferences name for compatibility reason private val sharedPreferences = context.getSharedPreferences("im.vector.matrix.android.realm", Context.MODE_PRIVATE) fun create(): RealmConfiguration { val shouldClearRealm = sharedPreferences.getBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", false) if (shouldClearRealm) { - Timber.v("************************************************************") - Timber.v("The realm file session was corrupted and couldn't be loaded.") - Timber.v("The file has been deleted to recover.") - Timber.v("************************************************************") + Timber.e("************************************************************") + Timber.e("The realm file session was corrupted and couldn't be loaded.") + Timber.e("The file has been deleted to recover.") + Timber.e("************************************************************") deleteRealmFiles() } sharedPreferences.edit { @@ -74,7 +71,7 @@ internal class SessionRealmConfigurationFactory @Inject constructor( realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5)) } .modules(SessionRealmModule()) - .schemaVersion(SESSION_STORE_SCHEMA_VERSION) + .schemaVersion(RealmSessionStoreMigration.SESSION_STORE_SCHEMA_VERSION) .migration(migration) .build() @@ -90,6 +87,11 @@ internal class SessionRealmConfigurationFactory @Inject constructor( // Delete all the realm files of the session private fun deleteRealmFiles() { + if (BuildConfig.DEBUG) { + Timber.e("No op because it is a debug build") + return + } + listOf(REALM_NAME, "$REALM_NAME.lock", "$REALM_NAME.note", "$REALM_NAME.management").forEach { file -> try { File(directory, file).deleteRecursively() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PushRulesMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PushRulesMapper.kt index 90fc62f8f3..3bcc3d2ea7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PushRulesMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PushRulesMapper.kt @@ -17,12 +17,12 @@ package org.matrix.android.sdk.internal.database.mapper import com.squareup.moshi.Types -import org.matrix.android.sdk.api.pushrules.Condition +import io.realm.RealmList +import org.matrix.android.sdk.api.pushrules.Kind import org.matrix.android.sdk.api.pushrules.rest.PushCondition import org.matrix.android.sdk.api.pushrules.rest.PushRule import org.matrix.android.sdk.internal.database.model.PushRuleEntity import org.matrix.android.sdk.internal.di.MoshiProvider -import io.realm.RealmList import timber.log.Timber internal object PushRulesMapper { @@ -39,7 +39,7 @@ internal object PushRulesMapper { enabled = pushrule.enabled, ruleId = pushrule.ruleId, conditions = listOf( - PushCondition(Condition.Kind.EventMatch.value, "content.body", pushrule.pattern) + PushCondition(Kind.EventMatch.value, "content.body", pushrule.pattern) ) ) } @@ -60,7 +60,7 @@ internal object PushRulesMapper { enabled = pushrule.enabled, ruleId = pushrule.ruleId, conditions = listOf( - PushCondition(Condition.Kind.EventMatch.value, "room_id", pushrule.ruleId) + PushCondition(Kind.EventMatch.value, "room_id", pushrule.ruleId) ) ) } @@ -72,7 +72,7 @@ internal object PushRulesMapper { enabled = pushrule.enabled, ruleId = pushrule.ruleId, conditions = listOf( - PushCondition(Condition.Kind.EventMatch.value, "user_id", pushrule.ruleId) + PushCondition(Kind.EventMatch.value, "user_id", pushrule.ruleId) ) ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PendingThreePidEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PendingThreePidEntity.kt new file mode 100644 index 0000000000..2f5643d7bc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PendingThreePidEntity.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 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.model + +import io.realm.RealmObject + +/** + * This class is used to store pending threePid data, when user wants to add a threePid to his account + */ +internal open class PendingThreePidEntity( + var email: String? = null, + var msisdn: String? = null, + var clientSecret: String = "", + var sendAttempt: Int = 0, + var sid: String = "", + var submitUrl: String? = null +) : RealmObject() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt index ea466db352..2c45cfcdbf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt @@ -36,6 +36,7 @@ import io.realm.annotations.RealmModule RoomSummaryEntity::class, RoomTagEntity::class, SyncEntity::class, + PendingThreePidEntity::class, UserEntity::class, IgnoredUserEntity::class, BreadcrumbsEntity::class, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ProgressRequestBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ProgressRequestBody.kt index 7ce260e54e..98dec301ee 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ProgressRequestBody.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/ProgressRequestBody.kt @@ -24,6 +24,7 @@ import okio.BufferedSink import okio.ForwardingSink import okio.Sink import okio.buffer +import org.matrix.android.sdk.api.extensions.tryThis import java.io.IOException internal class ProgressRequestBody(private val delegate: RequestBody, @@ -35,15 +36,13 @@ internal class ProgressRequestBody(private val delegate: RequestBody, return delegate.contentType() } - override fun contentLength(): Long { - try { - return delegate.contentLength() - } catch (e: IOException) { - e.printStackTrace() - } + override fun isOneShot() = delegate.isOneShot() - return -1 - } + override fun isDuplex() = delegate.isDuplex() + + val length = tryThis { delegate.contentLength() } ?: -1 + + override fun contentLength() = length @Throws(IOException::class) override fun writeTo(sink: BufferedSink) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitFactory.kt index 368611dd7d..89a0ce597a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitFactory.kt @@ -19,13 +19,12 @@ package org.matrix.android.sdk.internal.network import com.squareup.moshi.Moshi import dagger.Lazy -import org.matrix.android.sdk.internal.util.ensureTrailingSlash import okhttp3.Call import okhttp3.OkHttpClient import okhttp3.Request +import org.matrix.android.sdk.internal.util.ensureTrailingSlash import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory -import retrofit2.converter.scalars.ScalarsConverterFactory import javax.inject.Inject internal class RetrofitFactory @Inject constructor(private val moshi: Moshi) { @@ -50,7 +49,6 @@ internal class RetrofitFactory @Inject constructor(private val moshi: Moshi) { return okHttpClient.get().newCall(request) } }) - .addConverterFactory(ScalarsConverterFactory.create()) .addConverterFactory(UnitConverterFactory) .addConverterFactory(MoshiConverterFactory.create(moshi)) .build() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt new file mode 100644 index 0000000000..d4ceca2006 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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.network.parsing + +import androidx.annotation.Nullable +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.JsonReader +import com.squareup.moshi.JsonWriter + +import com.squareup.moshi.Moshi +import java.io.IOException +import java.lang.reflect.Type +import java.math.BigDecimal + +/** + * This is used to check if NUMBER in json is integer or double, so we can preserve typing when serializing/deserializing in a row. + */ +interface CheckNumberType { + + companion object { + val JSON_ADAPTER_FACTORY = object : JsonAdapter.Factory { + @Nullable + override fun create(type: Type, annotations: Set?, moshi: Moshi): JsonAdapter<*>? { + if (type !== Any::class.java) { + return null + } + val delegate: JsonAdapter = moshi.nextAdapter(this, Any::class.java, emptySet()) + return object : JsonAdapter() { + @Nullable + @Throws(IOException::class) + override fun fromJson(reader: JsonReader): Any? { + return if (reader.peek() !== JsonReader.Token.NUMBER) { + delegate.fromJson(reader) + } else { + val numberAsString = reader.nextString() + val decimal = BigDecimal(numberAsString) + if (decimal.scale() <= 0) { + decimal.intValueExact() + } else { + decimal.toDouble() + } + } + } + + override fun toJson(writer: JsonWriter, value: Any?) { + delegate.toJson(writer, value) + } + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt index 97ebe943ec..aa4114c8c2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultFileService.kt @@ -143,20 +143,22 @@ internal class DefaultFileService @Inject constructor( Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${!source.exhausted()}") if (elementToDecrypt != null) { - Timber.v("## decrypt file") - val decryptedStream = MXEncryptedAttachments.decryptAttachment(source.inputStream(), elementToDecrypt) + Timber.v("## FileService: decrypt file") + val decryptSuccess = MXEncryptedAttachments.decryptAttachment( + source.inputStream(), + elementToDecrypt, + destFile.outputStream().buffered() + ) response.close() - if (decryptedStream == null) { + if (!decryptSuccess) { return@flatMap Try.Failure(IllegalStateException("Decryption error")) - } else { - decryptedStream.use { - writeToFile(decryptedStream, destFile) - } } } else { writeToFile(source.inputStream(), destFile) response.close() } + } else { + Timber.v("## FileService: cache hit for $url") } Try.just(copyFile(destFile, downloadMode)) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/ActiveCallHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/ActiveCallHandler.kt new file mode 100644 index 0000000000..40f5a56c33 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/ActiveCallHandler.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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.call + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import org.matrix.android.sdk.api.session.call.MxCall +import org.matrix.android.sdk.internal.session.SessionScope +import javax.inject.Inject + +@SessionScope +internal class ActiveCallHandler @Inject constructor() { + + private val activeCallListLiveData: MutableLiveData> by lazy { + MutableLiveData>(mutableListOf()) + } + + fun addCall(call: MxCall) { + activeCallListLiveData.postValue(activeCallListLiveData.value?.apply { add(call) }) + } + + fun removeCall(callId: String) { + activeCallListLiveData.postValue(activeCallListLiveData.value?.apply { removeAll { it.callId == callId } }) + } + + fun getCallWithId(callId: String): MxCall? { + return activeCallListLiveData.value?.find { it.callId == callId } + } + + fun getActiveCallsLiveData(): LiveData> = activeCallListLiveData +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt index 6a17f0e925..d9bc66eddf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/DefaultCallSignalingService.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.call +import android.os.SystemClock import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.extensions.tryThis import org.matrix.android.sdk.api.session.call.CallSignalingService @@ -48,6 +49,7 @@ import javax.inject.Inject internal class DefaultCallSignalingService @Inject constructor( @UserId private val userId: String, + private val activeCallHandler: ActiveCallHandler, private val localEchoEventFactory: LocalEchoEventFactory, private val roomEventSender: RoomEventSender, private val taskExecutor: TaskExecutor, @@ -56,13 +58,11 @@ internal class DefaultCallSignalingService @Inject constructor( private val callListeners = mutableSetOf() - private val activeCalls = mutableListOf() - private val cachedTurnServerResponse = object { - + // Keep one minute safe to avoid considering the data is valid and then actually it is not when effectively using it. private val MIN_TTL = 60 - private val now = { System.currentTimeMillis() / 1000 } + private val now = { SystemClock.elapsedRealtime() / 1000 } private var expiresAt: Long = 0 @@ -96,7 +96,7 @@ internal class DefaultCallSignalingService @Inject constructor( } override fun createOutgoingCall(roomId: String, otherUserId: String, isVideoCall: Boolean): MxCall { - return MxCallImpl( + val call = MxCallImpl( callId = UUID.randomUUID().toString(), isOutgoing = true, roomId = roomId, @@ -105,8 +105,9 @@ internal class DefaultCallSignalingService @Inject constructor( isVideoCall = isVideoCall, localEchoEventFactory = localEchoEventFactory, roomEventSender = roomEventSender - ).also { - activeCalls.add(it) + ) + activeCallHandler.addCall(call).also { + return call } } @@ -119,8 +120,12 @@ internal class DefaultCallSignalingService @Inject constructor( } override fun getCallWithId(callId: String): MxCall? { - Timber.v("## VOIP getCallWithId $callId all calls ${activeCalls.map { it.callId }}") - return activeCalls.find { it.callId == callId } + Timber.v("## VOIP getCallWithId $callId all calls ${activeCallHandler.getActiveCallsLiveData().value?.map { it.callId }}") + return activeCallHandler.getCallWithId(callId) + } + + override fun isThereAnyActiveCall(): Boolean { + return activeCallHandler.getActiveCallsLiveData().value?.isNotEmpty() == true } internal fun onCallEvent(event: Event) { @@ -151,6 +156,7 @@ internal class DefaultCallSignalingService @Inject constructor( // Always ignore local echos of invite return } + event.getClearContent().toModel()?.let { content -> val incomingCall = MxCallImpl( callId = content.callId ?: return@let, @@ -162,7 +168,7 @@ internal class DefaultCallSignalingService @Inject constructor( localEchoEventFactory = localEchoEventFactory, roomEventSender = roomEventSender ) - activeCalls.add(incomingCall) + activeCallHandler.addCall(incomingCall) onCallInvite(incomingCall, content) } } @@ -184,8 +190,8 @@ internal class DefaultCallSignalingService @Inject constructor( return } + activeCallHandler.removeCall(content.callId) onCallHangup(content) - activeCalls.removeAll { it.callId == content.callId } } } EventType.CALL_CANDIDATES -> { @@ -194,7 +200,7 @@ internal class DefaultCallSignalingService @Inject constructor( return } event.getClearContent().toModel()?.let { content -> - activeCalls.firstOrNull { it.callId == content.callId }?.let { + activeCallHandler.getCallWithId(content.callId)?.let { onCallIceCandidate(it, content) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUploadStateTracker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUploadStateTracker.kt index aa8b98ae62..951c24ccb7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUploadStateTracker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUploadStateTracker.kt @@ -74,8 +74,8 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU updateState(key, progressData) } - internal fun setEncrypting(key: String) { - val progressData = ContentUploadStateTracker.State.Encrypting + internal fun setEncrypting(key: String, current: Long, total: Long) { + val progressData = ContentUploadStateTracker.State.Encrypting(current, total) updateState(key, progressData) } 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 5e5380fce1..4ddf394b00 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 @@ -23,13 +23,16 @@ import com.squareup.moshi.Moshi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.MediaType import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody -import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody +import okio.BufferedSink +import okio.source import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.api.extensions.tryThis import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.internal.di.Authenticated import org.matrix.android.sdk.internal.network.ProgressRequestBody @@ -38,6 +41,7 @@ import org.matrix.android.sdk.internal.network.toFailure import java.io.File import java.io.FileNotFoundException import java.io.IOException +import java.util.UUID import javax.inject.Inject internal class FileUploader @Inject constructor(@Authenticated @@ -54,7 +58,21 @@ internal class FileUploader @Inject constructor(@Authenticated filename: String?, mimeType: String?, progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse { - val uploadBody = file.asRequestBody(mimeType?.toMediaTypeOrNull()) + val uploadBody = object : RequestBody() { + override fun contentLength() = file.length() + + // Disable okhttp auto resend for 'large files' + override fun isOneShot() = contentLength() == 0L || contentLength() >= 1_000_000 + + override fun contentType(): MediaType? { + return mimeType?.toMediaTypeOrNull() + } + + override fun writeTo(sink: BufferedSink) { + file.source().use { sink.writeAll(it) } + } + } + return upload(uploadBody, filename, progressListener) } @@ -70,14 +88,18 @@ internal class FileUploader @Inject constructor(@Authenticated filename: String?, mimeType: String?, progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse { - return withContext(Dispatchers.IO) { - val inputStream = context.contentResolver.openInputStream(uri) ?: throw FileNotFoundException() - - inputStream.use { - uploadByteArray(it.readBytes(), filename, mimeType, progressListener) - } + val inputStream = withContext(Dispatchers.IO) { + context.contentResolver.openInputStream(uri) + } ?: throw FileNotFoundException() + val workingFile = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir) + workingFile.outputStream().use { + inputStream.copyTo(it) + } + return uploadFile(workingFile, filename, mimeType, progressListener).also { + tryThis { workingFile.delete() } } } + private suspend fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): ContentUploadResponse { val urlBuilder = uploadUrl.toHttpUrlOrNull()?.newBuilder() ?: throw RuntimeException() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt new file mode 100644 index 0000000000..a125c0aea4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 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.content + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Matrix +import androidx.exifinterface.media.ExifInterface +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import timber.log.Timber +import java.io.File +import java.util.UUID +import javax.inject.Inject + +internal class ImageCompressor @Inject constructor() { + suspend fun compress( + context: Context, + imageFile: File, + desiredWidth: Int, + desiredHeight: Int, + desiredQuality: Int = 80): File { + return withContext(Dispatchers.IO) { + val compressedBitmap = BitmapFactory.Options().run { + inJustDecodeBounds = true + decodeBitmap(imageFile, this) + inSampleSize = calculateInSampleSize(outWidth, outHeight, desiredWidth, desiredHeight) + inJustDecodeBounds = false + decodeBitmap(imageFile, this)?.let { + rotateBitmap(imageFile, it) + } + } ?: return@withContext imageFile + + val destinationFile = createDestinationFile(context) + + runCatching { + destinationFile.outputStream().use { + compressedBitmap.compress(Bitmap.CompressFormat.JPEG, desiredQuality, it) + } + } + + return@withContext destinationFile + } + } + + private fun rotateBitmap(file: File, bitmap: Bitmap): Bitmap { + file.inputStream().use { inputStream -> + try { + ExifInterface(inputStream).let { exifInfo -> + val orientation = exifInfo.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) + val matrix = Matrix() + when (orientation) { + ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270f) + ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180f) + ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90f) + ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.preScale(-1f, 1f) + ExifInterface.ORIENTATION_FLIP_VERTICAL -> matrix.preScale(1f, -1f) + ExifInterface.ORIENTATION_TRANSPOSE -> { + matrix.preRotate(-90f) + matrix.preScale(-1f, 1f) + } + ExifInterface.ORIENTATION_TRANSVERSE -> { + matrix.preRotate(90f) + matrix.preScale(-1f, 1f) + } + else -> return bitmap + } + return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) + } + } catch (e: Exception) { + Timber.e(e, "Cannot read orientation") + } + } + return bitmap + } + + // https://developer.android.com/topic/performance/graphics/load-bitmap + private fun calculateInSampleSize(width: Int, height: Int, desiredWidth: Int, desiredHeight: Int): Int { + var inSampleSize = 1 + + if (width > desiredWidth || height > desiredHeight) { + val halfHeight: Int = height / 2 + val halfWidth: Int = width / 2 + + // Calculate the largest inSampleSize value that is a power of 2 and keeps both + // height and width larger than the requested height and width. + while (halfHeight / inSampleSize >= desiredHeight && halfWidth / inSampleSize >= desiredWidth) { + inSampleSize *= 2 + } + } + + return inSampleSize + } + + private fun decodeBitmap(file: File, options: BitmapFactory.Options = BitmapFactory.Options()): Bitmap? { + return try { + file.inputStream().use { inputStream -> + BitmapFactory.decodeStream(inputStream, null, options) + } + } catch (e: Exception) { + Timber.e(e, "Cannot decode Bitmap") + null + } + } + + private fun createDestinationFile(context: Context): File { + return File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir) + } +} 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 6d354cdcbe..6e70906d13 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 @@ -22,8 +22,7 @@ import android.graphics.BitmapFactory import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass -import id.zelory.compressor.Compressor -import id.zelory.compressor.constraint.default +import org.matrix.android.sdk.api.extensions.tryThis import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.toContent @@ -37,15 +36,13 @@ import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo import org.matrix.android.sdk.internal.network.ProgressRequestBody import org.matrix.android.sdk.internal.session.DefaultFileService +import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker import org.matrix.android.sdk.internal.worker.SessionWorkerParams import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.worker.getSessionComponent import timber.log.Timber -import java.io.ByteArrayInputStream import java.io.File -import java.io.FileInputStream -import java.io.FileOutputStream import java.util.UUID import javax.inject.Inject @@ -74,6 +71,8 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter @Inject lateinit var fileUploader: FileUploader @Inject lateinit var contentUploadStateTracker: DefaultContentUploadStateTracker @Inject lateinit var fileService: DefaultFileService + @Inject lateinit var cancelSendTracker: CancelSendTracker + @Inject lateinit var imageCompressor: ImageCompressor override suspend fun doWork(): Result { val params = WorkerParamsFactory.fromData(inputData) @@ -101,9 +100,15 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() sessionComponent.inject(this) - val attachment = params.attachment + val allCancelled = params.events.all { cancelSendTracker.isCancelRequestedFor(it.eventId, it.roomId) } + if (allCancelled) { + // there is no point in uploading the image! + return Result.success(inputData) + .also { Timber.e("## Send: Work cancelled by user") } + } - var newImageAttributes: NewImageAttributes? = null + val attachment = params.attachment + val filesToDelete = mutableListOf() try { val inputStream = context.contentResolver.openInputStream(attachment.queryUri) @@ -115,124 +120,100 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter ) ) - inputStream.use { - var uploadedThumbnailUrl: String? = null - var uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? = null - - ThumbnailExtractor.extractThumbnail(context, params.attachment)?.let { thumbnailData -> - val thumbnailProgressListener = object : ProgressRequestBody.Listener { - override fun onProgress(current: Long, total: Long) { - notifyTracker(params) { contentUploadStateTracker.setProgressThumbnail(it, current, total) } - } - } - - try { - val contentUploadResponse = if (params.isEncrypted) { - Timber.v("Encrypt thumbnail") - notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) } - val encryptionResult = MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType) - uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo - fileUploader.uploadByteArray(encryptionResult.encryptedByteArray, - "thumb_${attachment.name}", - "application/octet-stream", - thumbnailProgressListener) - } else { - fileUploader.uploadByteArray(thumbnailData.bytes, - "thumb_${attachment.name}", - thumbnailData.mimeType, - thumbnailProgressListener) - } - - uploadedThumbnailUrl = contentUploadResponse.contentUri - } catch (t: Throwable) { - Timber.e(t, "Thumbnail update failed") - } - } - - val progressListener = object : ProgressRequestBody.Listener { - override fun onProgress(current: Long, total: Long) { - notifyTracker(params) { - if (isStopped) { - contentUploadStateTracker.setFailure(it, Throwable("Cancelled")) - } else { - contentUploadStateTracker.setProgress(it, current, total) - } - } - } - } - - var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null - - return try { - // Compressor library works with File instead of Uri for now. Since Scoped Storage doesn't allow us to access files directly, we should - // copy it to a cache folder by using InputStream and OutputStream. - // https://github.com/zetbaitsu/Compressor/pull/150 - // As soon as the above PR is merged, we can use attachment.queryUri instead of creating a cacheFile. - var cacheFile = File.createTempFile(attachment.name ?: UUID.randomUUID().toString(), ".jpg", context.cacheDir) - cacheFile.parentFile?.mkdirs() - if (cacheFile.exists()) { - cacheFile.delete() - } - cacheFile.createNewFile() - cacheFile.deleteOnExit() - - val outputStream = FileOutputStream(cacheFile) - outputStream.use { - inputStream.copyTo(outputStream) - } - - if (attachment.type == ContentAttachmentData.Type.IMAGE && params.compressBeforeSending) { - cacheFile = Compressor.compress(context, cacheFile) { - default( - width = MAX_IMAGE_SIZE, - height = MAX_IMAGE_SIZE - ) - }.also { compressedFile -> - val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } - BitmapFactory.decodeFile(compressedFile.absolutePath, options) - val fileSize = compressedFile.length().toInt() - newImageAttributes = NewImageAttributes( - options.outWidth, - options.outHeight, - fileSize - ) - } - } - - val contentUploadResponse = if (params.isEncrypted) { - Timber.v("Encrypt file") - notifyTracker(params) { contentUploadStateTracker.setEncrypting(it) } - - val encryptionResult = MXEncryptedAttachments.encryptAttachment(FileInputStream(cacheFile), attachment.getSafeMimeType()) - uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo - - fileUploader - .uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener) - } else { - fileUploader - .uploadFile(cacheFile, attachment.name, attachment.getSafeMimeType(), progressListener) - } - - // If it's a file update the file service so that it does not redownload? - if (params.attachment.type == ContentAttachmentData.Type.FILE) { - context.contentResolver.openInputStream(attachment.queryUri)?.let { - fileService.storeDataFor(contentUploadResponse.contentUri, params.attachment.getSafeMimeType(), it) - } - } - - handleSuccess(params, - contentUploadResponse.contentUri, - uploadedFileEncryptedFileInfo, - uploadedThumbnailUrl, - uploadedThumbnailEncryptedFileInfo, - newImageAttributes) - } catch (t: Throwable) { - Timber.e(t) - handleFailure(params, t) + // always use a temporary file, it guaranties that we could report progress on upload and simplifies the flows + val workingFile = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir) + .also { filesToDelete.add(it) } + workingFile.outputStream().use { outputStream -> + inputStream.use { inputStream -> + inputStream.copyTo(outputStream) } } + + val uploadThumbnailResult = dealWithThumbnail(params) + + val progressListener = object : ProgressRequestBody.Listener { + override fun onProgress(current: Long, total: Long) { + notifyTracker(params) { + if (isStopped) { + contentUploadStateTracker.setFailure(it, Throwable("Cancelled")) + } else { + contentUploadStateTracker.setProgress(it, current, total) + } + } + } + } + + var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null + + return try { + val fileToUpload: File + var newImageAttributes: NewImageAttributes? = null + + if (attachment.type == ContentAttachmentData.Type.IMAGE && params.compressBeforeSending) { + fileToUpload = imageCompressor.compress(context, workingFile, MAX_IMAGE_SIZE, MAX_IMAGE_SIZE) + .also { compressedFile -> + // Get new Bitmap size + compressedFile.inputStream().use { + val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } + val bitmap = BitmapFactory.decodeStream(it, null, options) + val fileSize = bitmap?.byteCount ?: 0 + newImageAttributes = NewImageAttributes( + options.outWidth, + options.outHeight, + fileSize + ) + } + } + .also { filesToDelete.add(it) } + } else { + fileToUpload = workingFile + } + + val contentUploadResponse = if (params.isEncrypted) { + Timber.v("## FileService: Encrypt file") + + val tmpEncrypted = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir) + .also { filesToDelete.add(it) } + + uploadedFileEncryptedFileInfo = + MXEncryptedAttachments.encrypt(fileToUpload.inputStream(), attachment.getSafeMimeType(), tmpEncrypted) { read, total -> + notifyTracker(params) { + contentUploadStateTracker.setEncrypting(it, read.toLong(), total.toLong()) + } + } + + Timber.v("## FileService: Uploading file") + + fileUploader + .uploadFile(tmpEncrypted, attachment.name, "application/octet-stream", progressListener) + } else { + Timber.v("## FileService: Clear file") + fileUploader + .uploadFile(fileToUpload, attachment.name, attachment.getSafeMimeType(), progressListener) + } + + Timber.v("## FileService: Update cache storage for ${contentUploadResponse.contentUri}") + try { + context.contentResolver.openInputStream(attachment.queryUri)?.let { + fileService.storeDataFor(contentUploadResponse.contentUri, params.attachment.getSafeMimeType(), it) + } + Timber.v("## FileService: cache storage updated") + } catch (failure: Throwable) { + Timber.e(failure, "## FileService: Failed to update file cache") + } + + handleSuccess(params, + contentUploadResponse.contentUri, + uploadedFileEncryptedFileInfo, + uploadThumbnailResult?.uploadedThumbnailUrl, + uploadThumbnailResult?.uploadedThumbnailEncryptedFileInfo, + newImageAttributes) + } catch (t: Throwable) { + Timber.e(t, "## FileService: ERROR ${t.localizedMessage}") + handleFailure(params, t) + } } catch (e: Exception) { - Timber.e(e) + Timber.e(e, "## FileService: ERROR") notifyTracker(params) { contentUploadStateTracker.setFailure(it, e) } return Result.success( WorkerParamsFactory.toData( @@ -241,9 +222,61 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter ) ) ) + } finally { + // Delete all temporary files + filesToDelete.forEach { + tryThis { it.delete() } + } } } + private data class UploadThumbnailResult( + val uploadedThumbnailUrl: String, + val uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? + ) + + /** + * If appropriate, it will create and upload a thumbnail + */ + private suspend fun dealWithThumbnail(params: Params): UploadThumbnailResult? { + return ThumbnailExtractor.extractThumbnail(context, params.attachment) + ?.let { thumbnailData -> + val thumbnailProgressListener = object : ProgressRequestBody.Listener { + override fun onProgress(current: Long, total: Long) { + notifyTracker(params) { contentUploadStateTracker.setProgressThumbnail(it, current, total) } + } + } + + try { + if (params.isEncrypted) { + Timber.v("Encrypt thumbnail") + notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) } + val encryptionResult = MXEncryptedAttachments.encryptAttachment(thumbnailData.bytes.inputStream(), thumbnailData.mimeType) + val contentUploadResponse = fileUploader.uploadByteArray(encryptionResult.encryptedByteArray, + "thumb_${params.attachment.name}", + "application/octet-stream", + thumbnailProgressListener) + UploadThumbnailResult( + contentUploadResponse.contentUri, + encryptionResult.encryptedFileInfo + ) + } else { + val contentUploadResponse = fileUploader.uploadByteArray(thumbnailData.bytes, + "thumb_${params.attachment.name}", + thumbnailData.mimeType, + thumbnailProgressListener) + UploadThumbnailResult( + contentUploadResponse.contentUri, + null + ) + } + } catch (t: Throwable) { + Timber.e(t, "Thumbnail upload failed") + null + } + } + } + private fun handleFailure(params: Params, failure: Throwable): Result { notifyTracker(params) { contentUploadStateTracker.setFailure(it, failure) } @@ -262,7 +295,6 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter thumbnailUrl: String?, thumbnailEncryptedFileInfo: EncryptedFileInfo?, newImageAttributes: NewImageAttributes?): Result { - Timber.v("handleSuccess $attachmentUrl, work is stopped $isStopped") notifyTracker(params) { contentUploadStateTracker.setSuccess(it) } val updatedEvents = params.events @@ -271,7 +303,9 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter } val sendParams = MultipleEventSendingDispatcherWorker.Params(params.sessionId, updatedEvents, params.isEncrypted) - return Result.success(WorkerParamsFactory.toData(sendParams)) + return Result.success(WorkerParamsFactory.toData(sendParams)).also { + Timber.v("## handleSuccess $attachmentUrl, work is stopped $isStopped") + } } private fun updateEvent(event: Event, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/DefaultContentDownloadStateTracker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/DefaultContentDownloadStateTracker.kt index 295a829b08..c4ba95af84 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/DefaultContentDownloadStateTracker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/download/DefaultContentDownloadStateTracker.kt @@ -61,19 +61,23 @@ internal class DefaultContentDownloadStateTracker @Inject constructor() : Progre // private fun URL.toKey() = toString() override fun update(url: String, bytesRead: Long, contentLength: Long, done: Boolean) { - Timber.v("## DL Progress url:$url read:$bytesRead total:$contentLength done:$done") - if (done) { - updateState(url, ContentDownloadStateTracker.State.Success) - } else { - updateState(url, ContentDownloadStateTracker.State.Downloading(bytesRead, contentLength, contentLength == -1L)) + mainHandler.post { + Timber.v("## DL Progress url:$url read:$bytesRead total:$contentLength done:$done") + if (done) { + updateState(url, ContentDownloadStateTracker.State.Success) + } else { + updateState(url, ContentDownloadStateTracker.State.Downloading(bytesRead, contentLength, contentLength == -1L)) + } } } override fun error(url: String, errorCode: Int) { - Timber.v("## DL Progress Error code:$errorCode") - updateState(url, ContentDownloadStateTracker.State.Failure(errorCode)) - listeners[url]?.forEach { - tryThis { it.onDownloadStateUpdate(ContentDownloadStateTracker.State.Failure(errorCode)) } + mainHandler.post { + Timber.v("## DL Progress Error code:$errorCode") + updateState(url, ContentDownloadStateTracker.State.Failure(errorCode)) + listeners[url]?.forEach { + tryThis { it.onDownloadStateUpdate(ContentDownloadStateTracker.State.Failure(errorCode)) } + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt index ac33c2666f..45d7d48a18 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/identity/IdentityBulkLookupTask.kt @@ -23,7 +23,6 @@ import org.matrix.android.sdk.api.session.identity.FoundThreePid import org.matrix.android.sdk.api.session.identity.IdentityServiceError import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.identity.toMedium -import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments.base64ToBase64Url import org.matrix.android.sdk.internal.crypto.tools.withOlmUtility import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.network.executeRequest @@ -32,6 +31,7 @@ import org.matrix.android.sdk.internal.session.identity.model.IdentityHashDetail import org.matrix.android.sdk.internal.session.identity.model.IdentityLookUpParams import org.matrix.android.sdk.internal.session.identity.model.IdentityLookUpResponse import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.base64ToBase64Url import java.util.Locale import javax.inject.Inject diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt index 49a92acc54..a0667cc4a3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/notification/ProcessEventForPushTask.kt @@ -100,7 +100,7 @@ internal class DefaultProcessEventForPushTask @Inject constructor( return rules.firstOrNull { rule -> // All conditions must hold true for an event in order to apply the action for the event. rule.enabled && rule.conditions?.all { - it.asExecutableCondition()?.isSatisfied(event, conditionResolver) ?: false + it.asExecutableCondition(rule)?.isSatisfied(event, conditionResolver) ?: false } ?: false } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddEmailBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddEmailBody.kt new file mode 100644 index 0000000000..ff81ad6a5c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddEmailBody.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 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.profile + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class AddEmailBody( + /** + * Required. A unique string generated by the client, and used to identify the validation attempt. + * It must be a string consisting of the characters [0-9a-zA-Z.=_-]. Its length must not exceed + * 255 characters and it must not be empty. + */ + @Json(name = "client_secret") + val clientSecret: String, + + /** + * Required. The email address to validate. + */ + @Json(name = "email") + val email: String, + + /** + * Required. The server will only send an email if the send_attempt is a number greater than the most + * recent one which it has seen, scoped to that email + client_secret pair. This is to avoid repeatedly + * sending the same email in the case of request retries between the POSTing user and the identity server. + * The client should increment this value if they desire a new email (e.g. a reminder) to be sent. + * If they do not, the server should respond with success but not resend the email. + */ + @Json(name = "send_attempt") + val sendAttempt: Int +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddEmailResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddEmailResponse.kt new file mode 100644 index 0000000000..8654d7c5ba --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddEmailResponse.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 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.profile + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class AddEmailResponse( + /** + * Required. The session ID. Session IDs are opaque strings that must consist entirely + * of the characters [0-9a-zA-Z.=_-]. Their length must not exceed 255 characters and they must not be empty. + */ + @Json(name = "sid") + val sid: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddMsisdnBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddMsisdnBody.kt new file mode 100644 index 0000000000..64c53f6729 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddMsisdnBody.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 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.profile + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class AddMsisdnBody( + /** + * Required. A unique string generated by the client, and used to identify the validation attempt. + * It must be a string consisting of the characters [0-9a-zA-Z.=_-]. Its length must not exceed + * 255 characters and it must not be empty. + */ + @Json(name = "client_secret") + val clientSecret: String, + + /** + * Required. The two-letter uppercase ISO-3166-1 alpha-2 country code that the number in + * phone_number should be parsed as if it were dialled from. + */ + @Json(name = "country") + val country: String, + + /** + * Required. The phone number to validate. + */ + @Json(name = "phone_number") + val phoneNumber: String, + + /** + * Required. The server will only send an SMS if the send_attempt is a number greater than the most + * recent one which it has seen, scoped to that country + phone_number + client_secret triple. This + * is to avoid repeatedly sending the same SMS in the case of request retries between the POSTing user + * and the identity server. The client should increment this value if they desire a new SMS (e.g. a + * reminder) to be sent. + */ + @Json(name = "send_attempt") + val sendAttempt: Int +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddMsisdnResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddMsisdnResponse.kt new file mode 100644 index 0000000000..b4c137b3a1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddMsisdnResponse.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 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.profile + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class AddMsisdnResponse( + /** + * Required. The session ID. Session IDs are opaque strings that must consist entirely of the characters [0-9a-zA-Z.=_-]. + * Their length must not exceed 255 characters and they must not be empty. + */ + @Json(name = "sid") + val sid: String, + + /** + * An optional field containing a URL where the client must submit the validation token to, with identical parameters to the Identity + * Service API's POST /validate/email/submitToken endpoint (without the requirement for an access token). + * The homeserver must send this token to the user (if applicable), who should then be prompted to provide it to the client. + * + * If this field is not present, the client can assume that verification will happen without the client's involvement provided + * the homeserver advertises this specification version in the /versions response (ie: r0.5.0). + */ + @Json(name = "submit_url") + val submitUrl: String? = null, + + /* ========================================================================================== + * It seems that the homeserver is sending more data, we may need it + * ========================================================================================== */ + + @Json(name = "msisdn") + val msisdn: String? = null, + + @Json(name = "intl_fmt") + val formattedMsisdn: String? = null, + + @Json(name = "success") + val success: Boolean? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddThreePidTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddThreePidTask.kt new file mode 100644 index 0000000000..c844c8ca6f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/AddThreePidTask.kt @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 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.profile + +import com.google.i18n.phonenumbers.PhoneNumberUtil +import com.zhuinden.monarchy.Monarchy +import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import java.util.UUID +import javax.inject.Inject + +internal abstract class AddThreePidTask : Task { + data class Params( + val threePid: ThreePid + ) +} + +internal class DefaultAddThreePidTask @Inject constructor( + private val profileAPI: ProfileAPI, + @SessionDatabase private val monarchy: Monarchy, + private val pendingThreePidMapper: PendingThreePidMapper, + private val eventBus: EventBus) : AddThreePidTask() { + + override suspend fun execute(params: Params) { + when (params.threePid) { + is ThreePid.Email -> addEmail(params.threePid) + is ThreePid.Msisdn -> addMsisdn(params.threePid) + } + } + + private suspend fun addEmail(threePid: ThreePid.Email) { + val clientSecret = UUID.randomUUID().toString() + val sendAttempt = 1 + + val result = executeRequest(eventBus) { + val body = AddEmailBody( + clientSecret = clientSecret, + email = threePid.email, + sendAttempt = sendAttempt + ) + apiCall = profileAPI.addEmail(body) + } + + // Store as a pending three pid + monarchy.awaitTransaction { realm -> + PendingThreePid( + threePid = threePid, + clientSecret = clientSecret, + sendAttempt = sendAttempt, + sid = result.sid, + submitUrl = null + ) + .let { pendingThreePidMapper.map(it) } + .let { realm.copyToRealm(it) } + } + } + + private suspend fun addMsisdn(threePid: ThreePid.Msisdn) { + val clientSecret = UUID.randomUUID().toString() + val sendAttempt = 1 + + // Get country code and national number from the phone number + val phoneNumber = threePid.msisdn + val phoneNumberUtil = PhoneNumberUtil.getInstance() + val parsedNumber = phoneNumberUtil.parse(phoneNumber, null) + val countryCode = parsedNumber.countryCode + val country = phoneNumberUtil.getRegionCodeForCountryCode(countryCode) + + val result = executeRequest(eventBus) { + val body = AddMsisdnBody( + clientSecret = clientSecret, + country = country, + phoneNumber = parsedNumber.nationalNumber.toString(), + sendAttempt = sendAttempt + ) + apiCall = profileAPI.addMsisdn(body) + } + + // Store as a pending three pid + monarchy.awaitTransaction { realm -> + PendingThreePid( + threePid = threePid, + clientSecret = clientSecret, + sendAttempt = sendAttempt, + sid = result.sid, + submitUrl = result.submitUrl + ) + .let { pendingThreePidMapper.map(it) } + .let { realm.copyToRealm(it) } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt index 633b047994..97212a8687 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt @@ -28,6 +28,7 @@ import org.matrix.android.sdk.api.session.profile.ProfileService import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity import org.matrix.android.sdk.internal.database.model.UserThreePidEntity import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.session.content.FileUploader @@ -44,6 +45,11 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto private val getProfileInfoTask: GetProfileInfoTask, private val setDisplayNameTask: SetDisplayNameTask, private val setAvatarUrlTask: SetAvatarUrlTask, + private val addThreePidTask: AddThreePidTask, + private val validateSmsCodeTask: ValidateSmsCodeTask, + private val finalizeAddingThreePidTask: FinalizeAddingThreePidTask, + private val deleteThreePidTask: DeleteThreePidTask, + private val pendingThreePidMapper: PendingThreePidMapper, private val fileUploader: FileUploader) : ProfileService { override fun getDisplayName(userId: String, matrixCallback: MatrixCallback>): Cancelable { @@ -116,9 +122,7 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto override fun getThreePidsLive(refreshData: Boolean): LiveData> { if (refreshData) { // Force a refresh of the values - refreshUserThreePidsTask - .configureWith() - .executeBy(taskExecutor) + refreshThreePids() } return monarchy.findAllMappedWithChanges( @@ -126,6 +130,95 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto { it.asDomain() } ) } + + private fun refreshThreePids() { + refreshUserThreePidsTask + .configureWith() + .executeBy(taskExecutor) + } + + override fun getPendingThreePids(): List { + return monarchy.fetchAllMappedSync( + { it.where() }, + { pendingThreePidMapper.map(it).threePid } + ) + } + + override fun getPendingThreePidsLive(): LiveData> { + return monarchy.findAllMappedWithChanges( + { it.where() }, + { pendingThreePidMapper.map(it).threePid } + ) + } + + override fun addThreePid(threePid: ThreePid, matrixCallback: MatrixCallback): Cancelable { + return addThreePidTask + .configureWith(AddThreePidTask.Params(threePid)) { + callback = matrixCallback + } + .executeBy(taskExecutor) + } + + override fun submitSmsCode(threePid: ThreePid.Msisdn, code: String, matrixCallback: MatrixCallback): Cancelable { + return validateSmsCodeTask + .configureWith(ValidateSmsCodeTask.Params(threePid, code)) { + callback = matrixCallback + } + .executeBy(taskExecutor) + } + + override fun finalizeAddingThreePid(threePid: ThreePid, + uiaSession: String?, + accountPassword: String?, + matrixCallback: MatrixCallback): Cancelable { + return finalizeAddingThreePidTask + .configureWith(FinalizeAddingThreePidTask.Params( + threePid = threePid, + session = uiaSession, + accountPassword = accountPassword, + userWantsToCancel = false + )) { + callback = alsoRefresh(matrixCallback) + } + .executeBy(taskExecutor) + } + + override fun cancelAddingThreePid(threePid: ThreePid, matrixCallback: MatrixCallback): Cancelable { + return finalizeAddingThreePidTask + .configureWith(FinalizeAddingThreePidTask.Params( + threePid = threePid, + session = null, + accountPassword = null, + userWantsToCancel = true + )) { + callback = alsoRefresh(matrixCallback) + } + .executeBy(taskExecutor) + } + + /** + * Wrap the callback to fetch 3Pids from the server in case of success + */ + private fun alsoRefresh(callback: MatrixCallback): MatrixCallback { + return object : MatrixCallback { + override fun onFailure(failure: Throwable) { + callback.onFailure(failure) + } + + override fun onSuccess(data: Unit) { + refreshThreePids() + callback.onSuccess(data) + } + } + } + + override fun deleteThreePid(threePid: ThreePid, matrixCallback: MatrixCallback): Cancelable { + return deleteThreePidTask + .configureWith(DeleteThreePidTask.Params(threePid)) { + callback = alsoRefresh(matrixCallback) + } + .executeBy(taskExecutor) + } } private fun UserThreePidEntity.asDomain(): ThreePid { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DeleteThreePidBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DeleteThreePidBody.kt new file mode 100644 index 0000000000..e7d4568f8b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DeleteThreePidBody.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 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.profile + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class DeleteThreePidBody( + /** + * Required. The medium of the third party identifier being removed. One of: ["email", "msisdn"] + */ + @Json(name = "medium") val medium: String, + /** + * Required. The third party address being removed. + */ + @Json(name = "address") val address: String +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DeleteThreePidResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DeleteThreePidResponse.kt new file mode 100644 index 0000000000..3817277a9d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DeleteThreePidResponse.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 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.profile + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class DeleteThreePidResponse( + /** + * Required. An indicator as to whether or not the homeserver was able to unbind the 3PID from + * the identity server. success indicates that the identity server has unbound the identifier + * whereas no-support indicates that the identity server refuses to support the request or the + * homeserver was not able to determine an identity server to unbind from. One of: ["no-support", "success"] + */ + @Json(name = "id_server_unbind_result") + val idServerUnbindResult: String? = null +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DeleteThreePidTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DeleteThreePidTask.kt new file mode 100644 index 0000000000..69ff7d82da --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DeleteThreePidTask.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 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.profile + +import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.api.session.identity.toMedium +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal abstract class DeleteThreePidTask : Task { + data class Params( + val threePid: ThreePid + ) +} + +internal class DefaultDeleteThreePidTask @Inject constructor( + private val profileAPI: ProfileAPI, + private val eventBus: EventBus) : DeleteThreePidTask() { + + override suspend fun execute(params: Params) { + executeRequest(eventBus) { + val body = DeleteThreePidBody( + medium = params.threePid.toMedium(), + address = params.threePid.value + ) + apiCall = profileAPI.deleteThreePid(body) + } + + // We do not really care about the result for the moment + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddThreePidBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddThreePidBody.kt new file mode 100644 index 0000000000..73e9b39cea --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddThreePidBody.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 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.profile + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth + +@JsonClass(generateAdapter = true) +internal data class FinalizeAddThreePidBody( + /** + * Required. The client secret used in the session with the homeserver. + */ + @Json(name = "client_secret") + val clientSecret: String, + + /** + * Required. The session identifier given by the homeserver. + */ + @Json(name = "sid") + val sid: String, + + /** + * Additional authentication information for the user-interactive authentication API. + */ + @Json(name = "auth") + val auth: UserPasswordAuth? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddingThreePidTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddingThreePidTask.kt new file mode 100644 index 0000000000..3886b926ba --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddingThreePidTask.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 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.profile + +import com.zhuinden.monarchy.Monarchy +import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth +import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity +import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.awaitTransaction +import javax.inject.Inject + +internal abstract class FinalizeAddingThreePidTask : Task { + data class Params( + val threePid: ThreePid, + val session: String?, + val accountPassword: String?, + val userWantsToCancel: Boolean + ) +} + +internal class DefaultFinalizeAddingThreePidTask @Inject constructor( + private val profileAPI: ProfileAPI, + @SessionDatabase private val monarchy: Monarchy, + private val pendingThreePidMapper: PendingThreePidMapper, + @UserId private val userId: String, + private val eventBus: EventBus) : FinalizeAddingThreePidTask() { + + override suspend fun execute(params: Params) { + if (params.userWantsToCancel.not()) { + // Get the required pending data + val pendingThreePids = monarchy.fetchAllMappedSync( + { it.where(PendingThreePidEntity::class.java) }, + { pendingThreePidMapper.map(it) } + ) + .firstOrNull { it.threePid == params.threePid } + ?: throw IllegalArgumentException("unknown threepid") + + try { + executeRequest(eventBus) { + val body = FinalizeAddThreePidBody( + clientSecret = pendingThreePids.clientSecret, + sid = pendingThreePids.sid, + auth = if (params.session != null && params.accountPassword != null) { + UserPasswordAuth( + session = params.session, + user = userId, + password = params.accountPassword + ) + } else null + ) + apiCall = profileAPI.finalizeAddThreePid(body) + } + } catch (throwable: Throwable) { + throw throwable.toRegistrationFlowResponse() + ?.let { Failure.RegistrationFlowError(it) } + ?: throwable + } + } + + cleanupDatabase(params) + } + + private suspend fun cleanupDatabase(params: Params) { + // Delete the pending three pid + monarchy.awaitTransaction { realm -> + realm.where(PendingThreePidEntity::class.java) + .equalTo(PendingThreePidEntityFields.EMAIL, params.threePid.value) + .or() + .equalTo(PendingThreePidEntityFields.MSISDN, params.threePid.value) + .findAll() + .deleteAllFromRealm() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/PendingThreePid.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/PendingThreePid.kt new file mode 100644 index 0000000000..af7e217d47 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/PendingThreePid.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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.profile + +import org.matrix.android.sdk.api.session.identity.ThreePid + +internal data class PendingThreePid( + val threePid: ThreePid, + val clientSecret: String, + val sendAttempt: Int, + // For Msisdn and Email + val sid: String, + // For Msisdn only + val submitUrl: String? +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/PendingThreePidMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/PendingThreePidMapper.kt new file mode 100644 index 0000000000..b1877027ed --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/PendingThreePidMapper.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * 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.profile + +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity +import javax.inject.Inject + +internal class PendingThreePidMapper @Inject constructor() { + + fun map(entity: PendingThreePidEntity): PendingThreePid { + return PendingThreePid( + threePid = entity.email?.let { ThreePid.Email(it) } + ?: entity.msisdn?.let { ThreePid.Msisdn(it) } + ?: error("Invalid data"), + clientSecret = entity.clientSecret, + sendAttempt = entity.sendAttempt, + sid = entity.sid, + submitUrl = entity.submitUrl + ) + } + + fun map(domain: PendingThreePid): PendingThreePidEntity { + return PendingThreePidEntity( + email = domain.threePid.takeIf { it is ThreePid.Email }?.value, + msisdn = domain.threePid.takeIf { it is ThreePid.Msisdn }?.value, + clientSecret = domain.clientSecret, + sendAttempt = domain.sendAttempt, + sid = domain.sid, + submitUrl = domain.submitUrl + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileAPI.kt index 31e1f09bbd..4e2f518c5a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileAPI.kt @@ -19,6 +19,8 @@ package org.matrix.android.sdk.internal.session.profile import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.internal.auth.registration.SuccessResult +import org.matrix.android.sdk.internal.auth.registration.ValidationCodeBody import org.matrix.android.sdk.internal.network.NetworkConstants import retrofit2.Call import retrofit2.http.Body @@ -26,9 +28,9 @@ import retrofit2.http.GET import retrofit2.http.POST import retrofit2.http.PUT import retrofit2.http.Path +import retrofit2.http.Url internal interface ProfileAPI { - /** * Get the combined profile information for this user. * This API may be used to fetch the user's own profile information or other users; either locally or on remote homeservers. @@ -71,4 +73,35 @@ internal interface ProfileAPI { */ @POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "account/3pid/unbind") fun unbindThreePid(@Body body: UnbindThreePidBody): Call + + /** + * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-account-3pid-email-requesttoken + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid/email/requestToken") + fun addEmail(@Body body: AddEmailBody): Call + + /** + * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-account-3pid-msisdn-requesttoken + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid/msisdn/requestToken") + fun addMsisdn(@Body body: AddMsisdnBody): Call + + /** + * Validate Msisdn code (same model than for Identity server API) + */ + @POST + fun validateMsisdn(@Url url: String, + @Body params: ValidationCodeBody): Call + + /** + * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-account-3pid-add + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid/add") + fun finalizeAddThreePid(@Body body: FinalizeAddThreePidBody): Call + + /** + * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-account-3pid-delete + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid/delete") + fun deleteThreePid(@Body body: DeleteThreePidBody): Call } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileModule.kt index 57a86d03e0..ae7ae7a6f3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ProfileModule.kt @@ -58,4 +58,16 @@ internal abstract class ProfileModule { @Binds abstract fun bindSetAvatarUrlTask(task: DefaultSetAvatarUrlTask): SetAvatarUrlTask + + @Binds + abstract fun bindAddThreePidTask(task: DefaultAddThreePidTask): AddThreePidTask + + @Binds + abstract fun bindValidateSmsCodeTask(task: DefaultValidateSmsCodeTask): ValidateSmsCodeTask + + @Binds + abstract fun bindFinalizeAddingThreePidTask(task: DefaultFinalizeAddingThreePidTask): FinalizeAddingThreePidTask + + @Binds + abstract fun bindDeleteThreePidTask(task: DefaultDeleteThreePidTask): DeleteThreePidTask } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ValidateSmsCodeTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ValidateSmsCodeTask.kt new file mode 100644 index 0000000000..b11955b96a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/ValidateSmsCodeTask.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2019 New Vector Ltd + * Copyright 2020 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.profile + +import com.zhuinden.monarchy.Monarchy +import org.greenrobot.eventbus.EventBus +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.session.identity.ThreePid +import org.matrix.android.sdk.internal.auth.registration.SuccessResult +import org.matrix.android.sdk.internal.auth.registration.ValidationCodeBody +import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface ValidateSmsCodeTask : Task { + data class Params( + val threePid: ThreePid.Msisdn, + val code: String + ) +} + +internal class DefaultValidateSmsCodeTask @Inject constructor( + private val profileAPI: ProfileAPI, + @SessionDatabase + private val monarchy: Monarchy, + private val pendingThreePidMapper: PendingThreePidMapper, + private val eventBus: EventBus +) : ValidateSmsCodeTask { + + override suspend fun execute(params: ValidateSmsCodeTask.Params) { + // Search the pending ThreePid + val pendingThreePids = monarchy.fetchAllMappedSync( + { it.where(PendingThreePidEntity::class.java) }, + { pendingThreePidMapper.map(it) } + ) + .firstOrNull { it.threePid == params.threePid } + ?: throw IllegalArgumentException("unknown threepid") + + val url = pendingThreePids.submitUrl ?: throw IllegalArgumentException("invalid threepid") + val body = ValidationCodeBody( + clientSecret = pendingThreePids.clientSecret, + sid = pendingThreePids.sid, + code = params.code + ) + val result = executeRequest(eventBus) { + apiCall = profileAPI.validateMsisdn(url, body) + } + + if (!result.isSuccess()) { + throw Failure.SuccessError + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index 4893947fc3..4a196193ba 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -347,7 +347,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr if (userId == senderId) { sumModel.myVote = optionIndex } - Timber.v("## POLL adding vote $optionIndex for user $senderId in poll :$relatedEventId ") + Timber.v("## POLL adding vote $optionIndex for user $senderId in poll :$targetEventId ") } else { Timber.v("## POLL Ignoring vote (older than known one) eventId:$eventId ") } @@ -356,7 +356,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr if (userId == senderId) { sumModel.myVote = optionIndex } - Timber.v("## POLL adding vote $optionIndex for user $senderId in poll :$relatedEventId ") + Timber.v("## POLL adding vote $optionIndex for user $senderId in poll :$targetEventId ") } sumModel.votes = votes if (isLocalEcho) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt index 25dcc69fa8..35c20cf5cb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt @@ -130,21 +130,6 @@ internal interface RoomAPI { @Body content: Content? ): Call - /** - * Send an event to a room. - * - * @param txId the transaction Id - * @param roomId the room id - * @param eventType the event type - * @param content the event content as string - */ - @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/send/{eventType}/{txId}") - fun send(@Path("txId") txId: String, - @Path("roomId") roomId: String, - @Path("eventType") eventType: String, - @Body content: String? - ): Call - /** * Get the context surrounding an event. * @@ -235,9 +220,9 @@ internal interface RoomAPI { */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/send_relation/{parent_id}/{relation_type}/{event_type}") fun sendRelation(@Path("roomId") roomId: String, - @Path("parentId") parent_id: String, + @Path("parent_id") parentId: String, @Path("relation_type") relationType: String, - @Path("eventType") eventType: String, + @Path("event_type") eventType: String, @Body content: Content? ): Call @@ -311,16 +296,16 @@ internal interface RoomAPI { * This cannot be undone. * Users may redact their own events, and any user with a power level greater than or equal to the redact power level of the room may redact events there. * - * @param txId the transaction Id - * @param roomId the room id - * @param eventId the event to delete + * @param txId the transaction Id + * @param roomId the room id + * @param eventId the event to delete * @param reason json containing reason key {"reason": "Indecent material"} */ @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/redact/{eventId}/{txnId}") fun redactEvent( @Path("txnId") txId: String, @Path("roomId") roomId: String, - @Path("eventId") parent_id: String, + @Path("eventId") eventId: String, @Body reason: Map ): Call diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt index 7f21ee84f6..700507735b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -20,6 +20,8 @@ package org.matrix.android.sdk.internal.session.room import dagger.Binds import dagger.Module import dagger.Provides +import org.commonmark.parser.Parser +import org.commonmark.renderer.html.HtmlRenderer import org.matrix.android.sdk.api.session.file.FileService import org.matrix.android.sdk.api.session.room.RoomDirectoryService import org.matrix.android.sdk.api.session.room.RoomService @@ -75,9 +77,6 @@ import org.matrix.android.sdk.internal.session.room.typing.DefaultSendTypingTask import org.matrix.android.sdk.internal.session.room.typing.SendTypingTask import org.matrix.android.sdk.internal.session.room.uploads.DefaultGetUploadsTask import org.matrix.android.sdk.internal.session.room.uploads.GetUploadsTask -import org.commonmark.parser.Parser -import org.commonmark.renderer.html.HtmlRenderer -import org.commonmark.renderer.text.TextContentRenderer import retrofit2.Retrofit @Module @@ -105,14 +104,6 @@ internal abstract class RoomModule { .builder() .build() } - - @Provides - @JvmStatic - fun providesTextContentRenderer(): TextContentRenderer { - return TextContentRenderer - .builder() - .build() - } } @Binds diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt index 6e450e5428..0a814a4c93 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt @@ -57,8 +57,8 @@ internal class CreateRoomBodyBuilder @Inject constructor( invites.map { ThreePidInviteBody( - id_server = identityServerUrlWithoutProtocol, - id_access_token = identityServerAccessToken, + idServer = identityServerUrlWithoutProtocol, + idAccessToken = identityServerAccessToken, medium = it.toMedium(), address = it.value ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt index d11226bdb1..942da9995e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/RoomDisplayNameResolver.kt @@ -17,7 +17,7 @@ package org.matrix.android.sdk.internal.session.room.membership -import android.content.Context +import io.realm.Realm import org.matrix.android.sdk.R import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel @@ -34,14 +34,15 @@ import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.query.getOrNull import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.UserId -import io.realm.Realm +import org.matrix.android.sdk.internal.util.StringProvider import javax.inject.Inject /** * This class computes room display name */ -internal class RoomDisplayNameResolver @Inject constructor(private val context: Context, - @UserId private val userId: String +internal class RoomDisplayNameResolver @Inject constructor( + private val stringProvider: StringProvider, + @UserId private val userId: String ) { /** @@ -89,7 +90,7 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context: .findFirst() ?.displayName } else { - context.getString(R.string.room_displayname_room_invite) + stringProvider.getString(R.string.room_displayname_room_invite) } } else if (roomEntity?.membership == Membership.JOIN) { val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst() @@ -108,13 +109,13 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context: } val otherMembersCount = otherMembersSubset.count() name = when (otherMembersCount) { - 0 -> context.getString(R.string.room_displayname_empty_room) - 1 -> resolveRoomMemberName(otherMembersSubset[0], roomMembers) - 2 -> context.getString(R.string.room_displayname_two_members, + 0 -> stringProvider.getString(R.string.room_displayname_empty_room) + 1 -> resolveRoomMemberName(otherMembersSubset[0], roomMembers) + 2 -> stringProvider.getString(R.string.room_displayname_two_members, resolveRoomMemberName(otherMembersSubset[0], roomMembers), resolveRoomMemberName(otherMembersSubset[1], roomMembers) ) - else -> context.resources.getQuantityString(R.plurals.room_displayname_three_and_more_members, + else -> stringProvider.getQuantityString(R.plurals.room_displayname_three_and_more_members, roomMembers.getNumberOfJoinedMembers() - 1, resolveRoomMemberName(otherMembersSubset[0], roomMembers), roomMembers.getNumberOfJoinedMembers() - 1) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/threepid/InviteThreePidTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/threepid/InviteThreePidTask.kt index b18e44360d..88809fec13 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/threepid/InviteThreePidTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/threepid/InviteThreePidTask.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.room.membership.threepid +import org.greenrobot.eventbus.EventBus import org.matrix.android.sdk.api.session.identity.IdentityServiceError import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.identity.toMedium @@ -28,7 +29,6 @@ import org.matrix.android.sdk.internal.session.identity.data.IdentityStore import org.matrix.android.sdk.internal.session.identity.data.getIdentityServerUrlWithoutProtocol import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.task.Task -import org.greenrobot.eventbus.EventBus import javax.inject.Inject internal interface InviteThreePidTask : Task { @@ -55,8 +55,8 @@ internal class DefaultInviteThreePidTask @Inject constructor( return executeRequest(eventBus) { val body = ThreePidInviteBody( - id_server = identityServerUrlWithoutProtocol, - id_access_token = identityServerAccessToken, + idServer = identityServerUrlWithoutProtocol, + idAccessToken = identityServerAccessToken, medium = params.threePid.toMedium(), address = params.threePid.value ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/threepid/ThreePidInviteBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/threepid/ThreePidInviteBody.kt index 93b5c577fc..5b0098dc9b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/threepid/ThreePidInviteBody.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/threepid/ThreePidInviteBody.kt @@ -25,18 +25,22 @@ internal data class ThreePidInviteBody( /** * Required. The hostname+port of the identity server which should be used for third party identifier lookups. */ - @Json(name = "id_server") val id_server: String, + @Json(name = "id_server") + val idServer: String, /** * Required. An access token previously registered with the identity server. Servers can treat this as optional * to distinguish between r0.5-compatible clients and this specification version. */ - @Json(name = "id_access_token") val id_access_token: String, + @Json(name = "id_access_token") + val idAccessToken: String, /** * Required. The kind of address being passed in the address field, for example email. */ - @Json(name = "medium") val medium: String, + @Json(name = "medium") + val medium: String, /** * Required. The invitee's third party identifier. */ - @Json(name = "address") val address: String + @Json(name = "address") + val address: String ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/RoomPushRuleMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/RoomPushRuleMapper.kt index 1a19a40602..86a9500339 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/RoomPushRuleMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/notification/RoomPushRuleMapper.kt @@ -18,7 +18,7 @@ package org.matrix.android.sdk.internal.session.room.notification import org.matrix.android.sdk.api.pushrules.Action -import org.matrix.android.sdk.api.pushrules.Condition +import org.matrix.android.sdk.api.pushrules.Kind import org.matrix.android.sdk.api.pushrules.RuleSetKey import org.matrix.android.sdk.api.pushrules.getActions import org.matrix.android.sdk.api.pushrules.rest.PushCondition @@ -59,7 +59,7 @@ internal fun RoomNotificationState.toRoomPushRule(roomId: String): RoomPushRule? } else -> { val condition = PushCondition( - kind = Condition.Kind.EventMatch.value, + kind = Kind.EventMatch.value, key = "room_id", pattern = roomId ) 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 2199193de0..111551d66d 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 @@ -208,7 +208,7 @@ internal class DefaultRelationService @AssistedInject constructor( } private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { - val sendContentWorkerParams = SendEventWorker.Params(sessionId, event) + val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, event = event) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) return timeLineSendEventWorkCommon.createWork(sendWorkData, startChain) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/SendRelationWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/SendRelationWorker.kt index dc72c3b96b..fc7f712629 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/SendRelationWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/SendRelationWorker.kt @@ -20,6 +20,7 @@ import android.content.Context import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass +import org.greenrobot.eventbus.EventBus import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.toModel @@ -31,7 +32,6 @@ import org.matrix.android.sdk.internal.session.room.send.SendResponse import org.matrix.android.sdk.internal.worker.SessionWorkerParams import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.worker.getSessionComponent -import org.greenrobot.eventbus.EventBus import timber.log.Timber import javax.inject.Inject @@ -92,7 +92,7 @@ internal class SendRelationWorker(context: Context, params: WorkerParameters) : executeRequest(eventBus) { apiCall = roomAPI.sendRelation( roomId = roomId, - parent_id = relatedEventId, + parentId = relatedEventId, relationType = relationType, eventType = localEvent.type, content = localEvent.content diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/CancelSendTracker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/CancelSendTracker.kt new file mode 100644 index 0000000000..0b79b93cf6 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/CancelSendTracker.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.send + +import org.matrix.android.sdk.internal.session.SessionScope +import javax.inject.Inject + +/** + * We cannot use work manager cancellation mechanism because cancelling a work will just ignore + * any follow up send that was already queued. + * We use this class to track cancel requests, the workers will look for this to check for cancellation request + * and just ignore the work request and continue by returning success. + * + * Known limitation, for now requests are not persisted + */ +@SessionScope +internal class CancelSendTracker @Inject constructor() { + + data class Request( + val localId: String, + val roomId: String + ) + + private val cancellingRequests = ArrayList() + + fun markLocalEchoForCancel(eventId: String, roomId: String) { + synchronized(cancellingRequests) { + cancellingRequests.add(Request(eventId, roomId)) + } + } + + fun isCancelRequestedFor(eventId: String?, roomId: String?): Boolean { + val index = synchronized(cancellingRequests) { + cancellingRequests.indexOfFirst { it.localId == eventId && it.roomId == roomId } + } + return index != -1 + } + + fun markCancelled(eventId: String, roomId: String) { + synchronized(cancellingRequests) { + val index = cancellingRequests.indexOfFirst { it.localId == eventId && it.roomId == roomId } + if (index != -1) { + cancellingRequests.removeAt(index) + } + } + } +} 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 d6fa6775ee..95cd1c699c 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 @@ -17,24 +17,35 @@ package org.matrix.android.sdk.internal.session.room.send +import android.net.Uri import androidx.work.BackoffPolicy import androidx.work.ExistingWorkPolicy import androidx.work.OneTimeWorkRequest import androidx.work.Operation import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.isImageMessage +import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage import org.matrix.android.sdk.api.session.events.model.isTextMessage +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent +import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent +import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent import org.matrix.android.sdk.api.session.room.model.message.OptionItem +import org.matrix.android.sdk.api.session.room.model.message.getFileUrl import org.matrix.android.sdk.api.session.room.send.SendService import org.matrix.android.sdk.api.session.room.send.SendState 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.CancelableBag import org.matrix.android.sdk.api.util.JsonDict +import org.matrix.android.sdk.api.util.NoOpCancellable import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.session.content.UploadContentWorker @@ -44,7 +55,6 @@ import org.matrix.android.sdk.internal.util.CancelableWork import org.matrix.android.sdk.internal.worker.AlwaysSuccessfulWorker import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.worker.startChain -import kotlinx.coroutines.launch import timber.log.Timber import java.util.concurrent.Executors import java.util.concurrent.TimeUnit @@ -60,7 +70,8 @@ internal class DefaultSendService @AssistedInject constructor( private val cryptoService: CryptoService, private val taskExecutor: TaskExecutor, private val localEchoRepository: LocalEchoRepository, - private val roomEventSender: RoomEventSender + private val roomEventSender: RoomEventSender, + private val cancelSendTracker: CancelSendTracker ) : SendService { @AssistedInject.Factory @@ -127,48 +138,83 @@ internal class DefaultSendService @AssistedInject constructor( .let { timelineSendEventWorkCommon.postWork(roomId, it) } } - override fun resendTextMessage(localEcho: TimelineEvent): Cancelable? { + override fun resendTextMessage(localEcho: TimelineEvent): Cancelable { if (localEcho.root.isTextMessage() && localEcho.root.sendState.hasFailed()) { localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT) return sendEvent(localEcho.root) } - return null + return NoOpCancellable } - override fun resendMediaMessage(localEcho: TimelineEvent): Cancelable? { - if (localEcho.root.isImageMessage() && localEcho.root.sendState.hasFailed()) { - // TODO this need a refactoring of attachement sending -// val clearContent = localEcho.root.getClearContent() -// val messageContent = clearContent?.toModel() ?: return null -// when (messageContent.type) { -// MessageType.MSGTYPE_IMAGE -> { -// val imageContent = clearContent.toModel() ?: return null -// val url = imageContent.url ?: return null -// if (url.startsWith("mxc://")) { -// //TODO -// } else { -// //The image has not yet been sent -// val attachmentData = ContentAttachmentData( -// size = imageContent.info!!.size.toLong(), -// mimeType = imageContent.info.mimeType!!, -// width = imageContent.info.width.toLong(), -// height = imageContent.info.height.toLong(), -// name = imageContent.body, -// path = imageContent.url, -// type = ContentAttachmentData.Type.IMAGE -// ) -// monarchy.runTransactionSync { -// EventEntity.where(it,eventId = localEcho.root.eventId ?: "").findFirst()?.let { -// it.sendState = SendState.UNSENT -// } -// } -// return internalSendMedia(localEcho.root,attachmentData) -// } -// } -// } - return null + override fun resendMediaMessage(localEcho: TimelineEvent): Cancelable { + if (localEcho.root.sendState.hasFailed()) { + val clearContent = localEcho.root.getClearContent() + val messageContent = clearContent?.toModel() as? MessageWithAttachmentContent ?: return NoOpCancellable + + val url = messageContent.getFileUrl() ?: return NoOpCancellable + if (url.startsWith("mxc://")) { + // We need to resend only the message as the attachment is ok + localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT) + return sendEvent(localEcho.root) + } + + // we need to resend the media + return when (messageContent) { + is MessageImageContent -> { + // The image has not yet been sent + val attachmentData = ContentAttachmentData( + size = messageContent.info!!.size.toLong(), + mimeType = messageContent.info.mimeType!!, + width = messageContent.info.width.toLong(), + height = messageContent.info.height.toLong(), + name = messageContent.body, + queryUri = Uri.parse(messageContent.url), + type = ContentAttachmentData.Type.IMAGE + ) + localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT) + internalSendMedia(listOf(localEcho.root), attachmentData, true) + } + is MessageVideoContent -> { + val attachmentData = ContentAttachmentData( + size = messageContent.videoInfo?.size ?: 0L, + mimeType = messageContent.mimeType, + width = messageContent.videoInfo?.width?.toLong(), + height = messageContent.videoInfo?.height?.toLong(), + duration = messageContent.videoInfo?.duration?.toLong(), + name = messageContent.body, + queryUri = Uri.parse(messageContent.url), + type = ContentAttachmentData.Type.VIDEO + ) + localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT) + internalSendMedia(listOf(localEcho.root), attachmentData, true) + } + is MessageFileContent -> { + val attachmentData = ContentAttachmentData( + size = messageContent.info!!.size, + mimeType = messageContent.info.mimeType!!, + name = messageContent.body, + queryUri = Uri.parse(messageContent.url), + type = ContentAttachmentData.Type.FILE + ) + localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT) + internalSendMedia(listOf(localEcho.root), attachmentData, true) + } + is MessageAudioContent -> { + val attachmentData = ContentAttachmentData( + size = messageContent.audioInfo?.size ?: 0, + duration = messageContent.audioInfo?.duration?.toLong() ?: 0L, + mimeType = messageContent.audioInfo?.mimeType, + name = messageContent.body, + queryUri = Uri.parse(messageContent.url), + type = ContentAttachmentData.Type.AUDIO + ) + localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT) + internalSendMedia(listOf(localEcho.root), attachmentData, true) + } + else -> NoOpCancellable + } } - return null + return NoOpCancellable } override fun deleteFailedEcho(localEcho: TimelineEvent) { @@ -196,16 +242,34 @@ internal class DefaultSendService @AssistedInject constructor( } } + override fun cancelSend(eventId: String) { + cancelSendTracker.markLocalEchoForCancel(eventId, roomId) + taskExecutor.executorScope.launch { + localEchoRepository.deleteFailedEcho(roomId, eventId) + } + } + override fun resendAllFailedMessages() { taskExecutor.executorScope.launch { val eventsToResend = localEchoRepository.getAllFailedEventsToResend(roomId) eventsToResend.forEach { - sendEvent(it) + if (it.root.isTextMessage()) { + resendTextMessage(it) + } else if (it.root.isAttachmentMessage()) { + resendMediaMessage(it) + } } - localEchoRepository.updateSendState(roomId, eventsToResend.mapNotNull { it.eventId }, SendState.UNSENT) + localEchoRepository.updateSendState(roomId, eventsToResend.map { it.eventId }, SendState.UNSENT) } } +// override fun failAllPendingMessages() { +// taskExecutor.executorScope.launch { +// val eventsToResend = localEchoRepository.getAllEventsWithStates(roomId, SendState.PENDING_STATES) +// localEchoRepository.updateSendState(roomId, eventsToResend.map { it.eventId }, SendState.UNDELIVERED) +// } +// } + override fun sendMedia(attachment: ContentAttachmentData, compressBeforeSending: Boolean, roomIds: Set): Cancelable { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/EncryptEventWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/EncryptEventWorker.kt index d23835e838..6b2a2ab115 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/EncryptEventWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/EncryptEventWorker.kt @@ -54,6 +54,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) @Inject lateinit var crypto: CryptoService @Inject lateinit var localEchoRepository: LocalEchoRepository + @Inject lateinit var cancelSendTracker: CancelSendTracker override suspend fun doWork(): Result { Timber.v("Start Encrypt work") @@ -61,7 +62,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) ?: return Result.success() .also { Timber.e("Unable to parse work parameters") } - Timber.v("Start Encrypt work for event ${params.event.eventId}") + Timber.v("## SendEvent: Start Encrypt work for event ${params.event.eventId}") if (params.lastFailureMessage != null) { // Transmit the error return Result.success(inputData) @@ -75,6 +76,12 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) if (localEvent.eventId == null) { return Result.success() } + + if (cancelSendTracker.isCancelRequestedFor(localEvent.eventId, localEvent.roomId)) { + return Result.success() + .also { Timber.e("## SendEvent: Event sending has been cancelled ${localEvent.eventId}") } + } + localEchoRepository.updateSendState(localEvent.eventId, SendState.ENCRYPTING) val localMutableContent = localEvent.content?.toMutableMap() ?: mutableMapOf() @@ -120,7 +127,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) localEchoRepository.updateEncryptedEcho(localEvent.eventId, safeResult.eventContent, decryptionLocalEcho) } - val nextWorkerParams = SendEventWorker.Params(params.sessionId, encryptedEvent) + val nextWorkerParams = SendEventWorker.Params(sessionId = params.sessionId, event = encryptedEvent) return Result.success(WorkerParamsFactory.toData(nextWorkerParams)) } else { val sendState = when (error) { @@ -129,8 +136,11 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) } localEchoRepository.updateSendState(localEvent.eventId, sendState) // always return success, or the chain will be stuck for ever! - val nextWorkerParams = SendEventWorker.Params(params.sessionId, localEvent, error?.localizedMessage - ?: "Error") + val nextWorkerParams = SendEventWorker.Params( + sessionId = params.sessionId, + event = localEvent, + lastFailureMessage = error?.localizedMessage ?: "Error" + ) return Result.success(WorkerParamsFactory.toData(nextWorkerParams)) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt index a9859136ad..b3188883c0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt @@ -30,7 +30,6 @@ import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult import org.matrix.android.sdk.internal.database.helper.nextId import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper -import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.mapper.toEntity import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventInsertEntity @@ -88,7 +87,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private } fun updateSendState(eventId: String, sendState: SendState) { - Timber.v("Update local state of $eventId to ${sendState.name}") + Timber.v("## SendEvent: [${System.currentTimeMillis()}] Update local state of $eventId to ${sendState.name}") monarchy.writeAsync { realm -> val sendingEventEntity = EventEntity.where(realm, eventId).findFirst() if (sendingEventEntity != null) { @@ -114,9 +113,13 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private } suspend fun deleteFailedEcho(roomId: String, localEcho: TimelineEvent) { + deleteFailedEcho(roomId, localEcho.eventId) + } + + suspend fun deleteFailedEcho(roomId: String, eventId: String?) { monarchy.awaitTransaction { realm -> - TimelineEventEntity.where(realm, roomId = roomId, eventId = localEcho.root.eventId ?: "").findFirst()?.deleteFromRealm() - EventEntity.where(realm, eventId = localEcho.root.eventId ?: "").findFirst()?.deleteFromRealm() + TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId ?: "").findFirst()?.deleteFromRealm() + EventEntity.where(realm, eventId = eventId ?: "").findFirst()?.deleteFromRealm() roomSummaryUpdater.updateSendingInformation(realm, roomId) } } @@ -142,45 +145,47 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private } } - fun getAllFailedEventsToResend(roomId: String): List { + fun getAllFailedEventsToResend(roomId: String): List { + return getAllEventsWithStates(roomId, SendState.HAS_FAILED_STATES) + } + + fun getAllEventsWithStates(roomId: String, states : List): List { return Realm.getInstance(monarchy.realmConfiguration).use { realm -> TimelineEventEntity - .findAllInRoomWithSendStates(realm, roomId, SendState.HAS_FAILED_STATES) + .findAllInRoomWithSendStates(realm, roomId, states) .sortedByDescending { it.displayIndex } - .mapNotNull { it.root?.asDomain() } + .mapNotNull { it?.let { timelineEventMapper.map(it) } } .filter { event -> - when (event.getClearType()) { + when (event.root.getClearType()) { EventType.MESSAGE, EventType.REDACTION, EventType.REACTION -> { - val content = event.getClearContent().toModel() + val content = event.root.getClearContent().toModel() if (content != null) { when (content.msgType) { MessageType.MSGTYPE_EMOTE, MessageType.MSGTYPE_NOTICE, MessageType.MSGTYPE_LOCATION, - MessageType.MSGTYPE_TEXT -> { - true - } + MessageType.MSGTYPE_TEXT, MessageType.MSGTYPE_FILE, MessageType.MSGTYPE_VIDEO, MessageType.MSGTYPE_IMAGE, MessageType.MSGTYPE_AUDIO -> { // need to resend the attachment - false + true } else -> { - Timber.e("Cannot resend message ${event.type} / ${content.msgType}") + Timber.e("Cannot resend message ${event.root.getClearType()} / ${content.msgType}") false } } } else { - Timber.e("Unsupported message to resend ${event.type}") + Timber.e("Unsupported message to resend ${event.root.getClearType()}") false } } else -> { - Timber.e("Unsupported message to resend ${event.type}") + Timber.e("Unsupported message to resend ${event.root.getClearType()}") false } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParser.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParser.kt index 3390d9dc79..f80285574e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParser.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParser.kt @@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.session.room.send import org.commonmark.parser.Parser import org.commonmark.renderer.html.HtmlRenderer -import org.commonmark.renderer.text.TextContentRenderer import javax.inject.Inject /** @@ -29,11 +28,10 @@ import javax.inject.Inject */ internal class MarkdownParser @Inject constructor( private val parser: Parser, - private val htmlRenderer: HtmlRenderer, - private val textContentRenderer: TextContentRenderer + private val htmlRenderer: HtmlRenderer ) { - private val mdSpecialChars = "[`_\\-\\*>\\.\\[\\]#~]".toRegex() + private val mdSpecialChars = "[`_\\-*>.\\[\\]#~]".toRegex() fun parse(text: String): TextContent { // If no special char are detected, just return plain text @@ -54,8 +52,8 @@ internal class MarkdownParser @Inject constructor( return if (isFormattedTextPertinent(text, cleanHtmlText)) { // According to https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes: // The plain text version of the HTML should be provided in the body. - val plainText = textContentRenderer.render(document) - TextContent(plainText, cleanHtmlText.postTreatment()) + // But it caused too many problems so it has been removed in #2002 + TextContent(text, cleanHtmlText.postTreatment()) } else { TextContent(text) } @@ -72,6 +70,7 @@ internal class MarkdownParser @Inject constructor( // Remove extra space before and after the content .trim() // There is no need to include new line in an html-like source - .replace("\n", "") + // But new line can be in embedded code block, so do not remove them + // .replace("\n", "") } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt index 73791e8412..8e8d24c227 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt @@ -58,7 +58,7 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo @Inject lateinit var localEchoRepository: LocalEchoRepository override suspend fun doWork(): Result { - Timber.v("Start dispatch sending multiple event work") + Timber.v("## SendEvent: Start dispatch sending multiple event work") val params = WorkerParamsFactory.fromData(inputData) ?: return Result.success() .also { Timber.e("Unable to parse work parameters") } @@ -72,18 +72,21 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo } // Transmit the error if needed? return Result.success(inputData) - .also { Timber.e("Work cancelled due to input error from parent") } + .also { Timber.e("## SendEvent: Work cancelled due to input error from parent ${params.lastFailureMessage}") } } // Create a work for every event params.events.forEach { event -> if (params.isEncrypted) { - Timber.v("Send event in encrypted room") + localEchoRepository.updateSendState(event.eventId ?: "", SendState.ENCRYPTING) + Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule encrypt and send event ${event.eventId}") val encryptWork = createEncryptEventWork(params.sessionId, event, true) // Note that event will be replaced by the result of the previous work val sendWork = createSendEventWork(params.sessionId, event, false) timelineSendEventWorkCommon.postSequentialWorks(event.roomId!!, encryptWork, sendWork) } else { + localEchoRepository.updateSendState(event.eventId ?: "", SendState.SENDING) + Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule send event ${event.eventId}") val sendWork = createSendEventWork(params.sessionId, event, true) timelineSendEventWorkCommon.postWork(event.roomId!!, sendWork) } @@ -105,7 +108,7 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo } private fun createSendEventWork(sessionId: String, event: Event, startChain: Boolean): OneTimeWorkRequest { - val sendContentWorkerParams = SendEventWorker.Params(sessionId, event) + val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, event = event) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) return timelineSendEventWorkCommon.createWork(sendWorkData, startChain) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RoomEventSender.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RoomEventSender.kt index 65c692f42e..6085459a08 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RoomEventSender.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/RoomEventSender.kt @@ -39,13 +39,16 @@ internal class RoomEventSender @Inject constructor( ) { fun sendEvent(event: Event): Cancelable { // Encrypted room handling - return if (cryptoService.isRoomEncrypted(event.roomId ?: "")) { - Timber.v("Send event in encrypted room") + return if (cryptoService.isRoomEncrypted(event.roomId ?: "") + && !event.isEncrypted() // In case of resend where it's already encrypted so skip to send + ) { + Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule encrypt and send event ${event.eventId}") val encryptWork = createEncryptEventWork(event, true) // Note that event will be replaced by the result of the previous work val sendWork = createSendEventWork(event, false) timelineSendEventWorkCommon.postSequentialWorks(event.roomId ?: "", encryptWork, sendWork) } else { + Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule send event ${event.eventId}") val sendWork = createSendEventWork(event, true) timelineSendEventWorkCommon.postWork(event.roomId ?: "", sendWork) } @@ -65,7 +68,7 @@ internal class RoomEventSender @Inject constructor( } private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { - val sendContentWorkerParams = SendEventWorker.Params(sessionId, event) + val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, event = event) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) return timelineSendEventWorkCommon.createWork(sendWorkData, startChain) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt index 2868ce29c1..16acde7d16 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt @@ -21,20 +21,20 @@ import android.content.Context import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass +import org.greenrobot.eventbus.EventBus import org.matrix.android.sdk.api.failure.shouldBeRetried +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.send.SendState -import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.worker.SessionWorkerParams import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.worker.getSessionComponent -import org.greenrobot.eventbus.EventBus import timber.log.Timber import javax.inject.Inject -private const val MAX_NUMBER_OF_RETRY_BEFORE_FAILING = 3 +// private const val MAX_NUMBER_OF_RETRY_BEFORE_FAILING = 3 /** * Possible previous worker: [EncryptEventWorker] or first worker @@ -47,68 +47,69 @@ internal class SendEventWorker(context: Context, @JsonClass(generateAdapter = true) internal data class Params( override val sessionId: String, - // TODO remove after some time, it's used for compat + override val lastFailureMessage: String? = null, val event: Event? = null, - val eventId: String? = null, - val roomId: String? = null, - val type: String? = null, - val contentStr: String? = null, - override val lastFailureMessage: String? = null - ) : SessionWorkerParams { - - constructor(sessionId: String, event: Event, lastFailureMessage: String? = null) : this( - sessionId = sessionId, - eventId = event.eventId, - roomId = event.roomId, - type = event.type, - contentStr = ContentMapper.map(event.content), - lastFailureMessage = lastFailureMessage - ) - } + // Keep for compat at the moment, will be removed later + val eventId: String? = null + ) : SessionWorkerParams @Inject lateinit var localEchoRepository: LocalEchoRepository @Inject lateinit var roomAPI: RoomAPI @Inject lateinit var eventBus: EventBus + @Inject lateinit var cancelSendTracker: CancelSendTracker override suspend fun doWork(): Result { val params = WorkerParamsFactory.fromData(inputData) ?: return Result.success() - .also { Timber.e("Unable to parse work parameters") } - + .also { Timber.e("## SendEvent: Unable to parse work parameters") } val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() sessionComponent.inject(this) - if (params.eventId == null || params.roomId == null || params.type == null) { - // compat with old params, make it fail if any - if (params.event?.eventId != null) { - localEchoRepository.updateSendState(params.event.eventId, SendState.UNDELIVERED) + + val event = params.event + if (event?.eventId == null || event.roomId == null) { + // Old way of sending + if (params.eventId != null) { + localEchoRepository.updateSendState(params.eventId, SendState.UNDELIVERED) } return Result.success() + .also { Timber.e("Work cancelled due to bad input data") } } + + if (cancelSendTracker.isCancelRequestedFor(params.eventId, event.roomId)) { + return Result.success() + .also { + cancelSendTracker.markCancelled(event.eventId, event.roomId) + Timber.e("## SendEvent: Event sending has been cancelled ${params.eventId}") + } + } + if (params.lastFailureMessage != null) { - localEchoRepository.updateSendState(params.eventId, SendState.UNDELIVERED) + localEchoRepository.updateSendState(event.eventId, SendState.UNDELIVERED) // Transmit the error return Result.success(inputData) .also { Timber.e("Work cancelled due to input error from parent") } } + + Timber.v("## SendEvent: [${System.currentTimeMillis()}] Send event ${params.eventId}") return try { - sendEvent(params.eventId, params.roomId, params.type, params.contentStr) + sendEvent(event.eventId, event.roomId, event.type, event.content) Result.success() } catch (exception: Throwable) { - // It does start from 0, we want it to stop if it fails the third time - val currentAttemptCount = runAttemptCount + 1 - if (currentAttemptCount >= MAX_NUMBER_OF_RETRY_BEFORE_FAILING || !exception.shouldBeRetried()) { - localEchoRepository.updateSendState(params.eventId, SendState.UNDELIVERED) + if (/*currentAttemptCount >= MAX_NUMBER_OF_RETRY_BEFORE_FAILING ||**/ !exception.shouldBeRetried()) { + Timber.e("## SendEvent: [${System.currentTimeMillis()}] Send event Failed cannot retry ${params.eventId} > ${exception.localizedMessage}") + localEchoRepository.updateSendState(event.eventId, SendState.UNDELIVERED) return Result.success() } else { + Timber.e("## SendEvent: [${System.currentTimeMillis()}] Send event Failed schedule retry ${params.eventId} > ${exception.localizedMessage}") Result.retry() } } } - private suspend fun sendEvent(eventId: String, roomId: String, type: String, contentStr: String?) { + private suspend fun sendEvent(eventId: String, roomId: String, type: String, content: Content?) { localEchoRepository.updateSendState(eventId, SendState.SENDING) executeRequest(eventBus) { - apiCall = roomAPI.send(eventId, roomId, type, contentStr) + apiCall = roomAPI.send(eventId, roomId, type, content) } localEchoRepository.updateSendState(eventId, SendState.SENT) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index b4c32c045e..421cd1b063 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -17,6 +17,16 @@ package org.matrix.android.sdk.internal.session.room.timeline +import io.realm.OrderedCollectionChangeSet +import io.realm.OrderedRealmCollectionChangeListener +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.RealmQuery +import io.realm.RealmResults +import io.realm.Sort +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.events.model.EventType @@ -44,16 +54,6 @@ import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.util.Debouncer import org.matrix.android.sdk.internal.util.createBackgroundHandler import org.matrix.android.sdk.internal.util.createUIHandler -import io.realm.OrderedCollectionChangeSet -import io.realm.OrderedRealmCollectionChangeListener -import io.realm.Realm -import io.realm.RealmConfiguration -import io.realm.RealmQuery -import io.realm.RealmResults -import io.realm.Sort -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode import timber.log.Timber import java.util.Collections import java.util.UUID @@ -115,6 +115,7 @@ internal class DefaultTimeline( if (!results.isLoaded || !results.isValid) { return@OrderedRealmCollectionChangeListener } + Timber.v("## SendEvent: [${System.currentTimeMillis()}] DB update for room $roomId") handleUpdates(results, changeSet) } @@ -316,12 +317,15 @@ internal class DefaultTimeline( @Subscribe(threadMode = ThreadMode.MAIN) fun onLocalEchoCreated(onLocalEchoCreated: OnLocalEchoCreated) { if (isLive && onLocalEchoCreated.roomId == roomId) { - listeners.forEach { - it.onNewTimelineEvents(listOf(onLocalEchoCreated.timelineEvent.eventId)) + // do not add events that would have been filtered + if (listOf(onLocalEchoCreated.timelineEvent).filterEventsWithSettings().isNotEmpty()) { + listeners.forEach { + it.onNewTimelineEvents(listOf(onLocalEchoCreated.timelineEvent.eventId)) + } + Timber.v("On local echo created: ${onLocalEchoCreated.timelineEvent.eventId}") + inMemorySendingEvents.add(0, onLocalEchoCreated.timelineEvent) + postSnapshot() } - Timber.v("On local echo created: $onLocalEchoCreated") - inMemorySendingEvents.add(0, onLocalEchoCreated.timelineEvent) - postSnapshot() } } @@ -777,7 +781,7 @@ internal class DefaultTimeline( val filterEdits = if (settings.filterEdits && it.root.type == EventType.MESSAGE) { val messageContent = it.root.content.toModel() - messageContent?.relatesTo?.type != RelationType.REPLACE + messageContent?.relatesTo?.type != RelationType.REPLACE && messageContent?.relatesTo?.type != RelationType.RESPONSE } else { true } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineSendEventWorkCommon.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineSendEventWorkCommon.kt index d3124b68ca..3bc6a85cfb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineSendEventWorkCommon.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineSendEventWorkCommon.kt @@ -57,7 +57,7 @@ internal class TimelineSendEventWorkCommon @Inject constructor( } } - fun postWork(roomId: String, workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND): Cancelable { + fun postWork(roomId: String, workRequest: OneTimeWorkRequest, policy: ExistingWorkPolicy = ExistingWorkPolicy.APPEND_OR_REPLACE): Cancelable { workManagerProvider.workManager .beginUniqueWork(buildWorkName(roomId), policy, workRequest) .enqueue() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt index 2ae115f325..8eab44366c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/securestorage/SecretStoringUtils.kt @@ -219,7 +219,7 @@ internal class SecretStoringUtils @Inject constructor(private val context: Conte @RequiresApi(Build.VERSION_CODES.M) private fun decryptStringM(encryptedChunk: ByteArray, keyAlias: String): String { - val (iv, encryptedText) = formatMExtract(ByteArrayInputStream(encryptedChunk)) + val (iv, encryptedText) = formatMExtract(encryptedChunk.inputStream()) val secretKey = getOrGenerateSymmetricKeyForAliasM(keyAlias) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt index 1a2d6b1fd3..9fd9c313db 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/job/SyncThread.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.internal.session.sync.job import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer import com.squareup.moshi.JsonEncodingException import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.isTokenError @@ -30,11 +31,14 @@ import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver import org.matrix.android.sdk.internal.util.Debouncer import org.matrix.android.sdk.internal.util.createUIHandler import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import org.matrix.android.sdk.api.session.call.MxCall +import org.matrix.android.sdk.internal.session.call.ActiveCallHandler import timber.log.Timber import java.net.SocketTimeoutException import java.util.Timer @@ -48,8 +52,9 @@ private const val DEFAULT_LONG_POOL_TIMEOUT = 30_000L internal class SyncThread @Inject constructor(private val syncTask: SyncTask, private val typingUsersTracker: DefaultTypingUsersTracker, private val networkConnectivityChecker: NetworkConnectivityChecker, - private val backgroundDetectionObserver: BackgroundDetectionObserver) - : Thread(), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener { + private val backgroundDetectionObserver: BackgroundDetectionObserver, + private val activeCallHandler: ActiveCallHandler +) : Thread(), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener { private var state: SyncState = SyncState.Idle private var liveState = MutableLiveData(state) @@ -62,6 +67,12 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, private var isTokenValid = true private var retryNoNetworkTask: TimerTask? = null + private val activeCallListObserver = Observer> { activeCalls -> + if (activeCalls.isEmpty() && backgroundDetectionObserver.isInBackground) { + pause() + } + } + init { updateStateTo(SyncState.Idle) } @@ -115,9 +126,11 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, override fun run() { Timber.v("Start syncing...") + isStarted = true networkConnectivityChecker.register(this) backgroundDetectionObserver.register(this) + registerActiveCallsObserver() while (state != SyncState.Killing) { Timber.v("Entering loop, state: $state") if (!isStarted) { @@ -163,6 +176,19 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, updateStateTo(SyncState.Killed) backgroundDetectionObserver.unregister(this) networkConnectivityChecker.unregister(this) + unregisterActiveCallsObserver() + } + + private fun registerActiveCallsObserver() { + syncScope.launch(Dispatchers.Main) { + activeCallHandler.getActiveCallsLiveData().observeForever(activeCallListObserver) + } + } + + private fun unregisterActiveCallsObserver() { + syncScope.launch(Dispatchers.Main) { + activeCallHandler.getActiveCallsLiveData().removeObserver(activeCallListObserver) + } } private suspend fun doSync(params: SyncTask.Params) { @@ -215,6 +241,8 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask, } override fun onMoveToBackground() { - pause() + if (activeCallHandler.getActiveCallsLiveData().value.isNullOrEmpty()) { + pause() + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceInfo.kt index 226486596e..0e7fbf492f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/model/DeviceInfo.kt @@ -16,6 +16,7 @@ */ package org.matrix.android.sdk.internal.session.sync.model +import com.squareup.moshi.Json import com.squareup.moshi.JsonClass /** @@ -26,26 +27,30 @@ internal data class DeviceInfo( /** * The owner user id */ - val user_id: String? = null, + @Json(name = "user_id") + val userId: String? = null, /** * The device id */ - val device_id: String? = null, + @Json(name = "device_id") + val deviceId: String? = null, /** * The device display name */ - val display_name: String? = null, + @Json(name = "display_name") + val displayName: String? = null, /** * The last time this device has been seen. */ - val last_seen_ts: Long = 0, + @Json(name = "last_seen_ts") + val lastSeenTs: Long = 0, /** * The last ip address */ - val last_seen_ip: String? = null - + @Json(name = "last_seen_ip") + val lastSeenIp: String? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Base64.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Base64.kt new file mode 100644 index 0000000000..76e24c4e31 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Base64.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 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.util + +/** + * Base64 URL conversion methods + */ + +internal fun base64UrlToBase64(base64Url: String): String { + return base64Url.replace('-', '+') + .replace('_', '/') +} + +internal fun base64ToBase64Url(base64: String): String { + return base64.replace("\n".toRegex(), "") + .replace("\\+".toRegex(), "-") + .replace('/', '_') + .replace("=", "") +} + +internal fun base64ToUnpaddedBase64(base64: String): String { + return base64.replace("\n".toRegex(), "") + .replace("=", "") +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/CompatUtil.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/CompatUtil.kt index 6583dc89ea..6a5cfec095 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/CompatUtil.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/CompatUtil.kt @@ -257,12 +257,11 @@ object CompatUtil { /** * Create a CipherInputStream instance. - * Before Kitkat, this method will return `in` because local storage encryption is not implemented for devices before KitKat. - * Warning, if `in` is not an encrypted stream, it's up to the caller to close and reopen `in`, because the stream has been read. + * Warning, if inputStream is not an encrypted stream, it's up to the caller to close and reopen inputStream, because the stream has been read. * - * @param in the input stream - * @param context the context holding the application shared preferences - * @return in, or the created InputStream, or null if the InputStream `in` does not contain encrypted data + * @param inputStream the input stream + * @param context the context holding the application shared preferences + * @return inputStream, or the created InputStream, or null if the InputStream inputStream does not contain encrypted data */ @Throws(NoSuchPaddingException::class, NoSuchAlgorithmException::class, @@ -274,15 +273,15 @@ object CompatUtil { NoSuchProviderException::class, InvalidAlgorithmParameterException::class, IOException::class) - fun createCipherInputStream(`in`: InputStream, context: Context): InputStream? { - val iv_len = `in`.read() - if (iv_len != AES_GCM_IV_LENGTH) { - Timber.e(TAG, "Invalid IV length $iv_len") + fun createCipherInputStream(inputStream: InputStream, context: Context): InputStream? { + val ivLen = inputStream.read() + if (ivLen != AES_GCM_IV_LENGTH) { + Timber.e(TAG, "Invalid IV length $ivLen") return null } val iv = ByteArray(AES_GCM_IV_LENGTH) - `in`.read(iv) + inputStream.read(iv) val cipher = Cipher.getInstance(AES_GCM_CIPHER_TYPE) @@ -296,6 +295,6 @@ object CompatUtil { cipher.init(Cipher.DECRYPT_MODE, keyAndVersion.secretKey, spec) - return CipherInputStream(`in`, cipher) + return CipherInputStream(inputStream, cipher) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FileSaver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FileSaver.kt index 27625d90bc..da524cc1b2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FileSaver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FileSaver.kt @@ -19,7 +19,6 @@ package org.matrix.android.sdk.internal.util import androidx.annotation.WorkerThread import java.io.File -import java.io.FileOutputStream import java.io.InputStream /** @@ -27,7 +26,7 @@ import java.io.InputStream */ @WorkerThread fun writeToFile(inputStream: InputStream, outputFile: File) { - FileOutputStream(outputFile).use { + outputFile.outputStream().use { inputStream.copyTo(it) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Glob.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Glob.kt new file mode 100644 index 0000000000..129bf0dec4 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/Glob.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 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.util + +internal fun String.hasSpecialGlobChar(): Boolean { + return contains("*") || contains("?") +} + +// Very simple glob to regexp converter +internal fun String.simpleGlobToRegExp(): String { + val string = this + return buildString { + // append("^") + string.forEach { char -> + when (char) { + '*' -> append(".*") + '?' -> append(".") + '.' -> append("\\.") + '\\' -> append("\\\\") + else -> append(char) + } + } + // append("$") + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringProvider.kt index 902d7d3316..9233b2b807 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringProvider.kt @@ -18,8 +18,8 @@ package org.matrix.android.sdk.internal.util import android.content.res.Resources -import androidx.annotation.ArrayRes import androidx.annotation.NonNull +import androidx.annotation.PluralsRes import androidx.annotation.StringRes import dagger.Reusable import javax.inject.Inject @@ -56,8 +56,8 @@ internal class StringProvider @Inject constructor(private val resources: Resourc return resources.getString(resId, *formatArgs) } - @Throws(Resources.NotFoundException::class) - fun getStringArray(@ArrayRes id: Int): Array { - return resources.getStringArray(id) + @NonNull + fun getQuantityString(@PluralsRes resId: Int, quantity: Int, vararg formatArgs: Any?): String { + return resources.getQuantityString(resId, quantity, *formatArgs) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt index a236771cd6..681e7e5fed 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/StringUtils.kt @@ -52,3 +52,25 @@ fun convertFromUTF8(s: String): String { } fun String.withoutPrefix(prefix: String) = if (startsWith(prefix)) substringAfter(prefix) else this + +/** + * Returns whether a string contains an occurrence of another, as a standalone word, regardless of case. + * + * @param subString the string to search for + * @return whether a match was found + */ +fun String.caseInsensitiveFind(subString: String): Boolean { + // add sanity checks + if (subString.isEmpty() || isEmpty()) { + return false + } + + try { + val regex = Regex("(\\W|^)" + Regex.escape(subString) + "(\\W|$)", RegexOption.IGNORE_CASE) + return regex.containsMatchIn(this) + } catch (e: Exception) { + Timber.e(e, "## caseInsensitiveFind() : failed") + } + + return false +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/GetWellknownTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/GetWellknownTask.kt index e20fe9a304..80ede5e884 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/GetWellknownTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/wellknown/GetWellknownTask.kt @@ -97,7 +97,7 @@ internal class DefaultGetWellknownTask @Inject constructor( // Success val homeServerBaseUrl = wellKnown.homeServer?.baseURL if (homeServerBaseUrl.isNullOrBlank()) { - WellknownResult.FailPrompt + WellknownResult.FailPrompt(null, null) } else { if (homeServerBaseUrl.isValidUrl()) { // Check that HS is a real one @@ -120,11 +120,11 @@ internal class DefaultGetWellknownTask @Inject constructor( is Failure.OtherServerError -> { when (throwable.httpCode) { HttpsURLConnection.HTTP_NOT_FOUND -> WellknownResult.Ignore - else -> WellknownResult.FailPrompt + else -> WellknownResult.FailPrompt(null, null) } } is MalformedJsonException, is EOFException -> { - WellknownResult.FailPrompt + WellknownResult.FailPrompt(null, null) } else -> { throw throwable @@ -162,7 +162,7 @@ internal class DefaultGetWellknownTask @Inject constructor( // All is ok WellknownResult.Prompt(homeServerBaseUrl, identityServerBaseUrl, wellKnown) } else { - WellknownResult.FailError + WellknownResult.FailPrompt(homeServerBaseUrl, wellKnown) } } else { WellknownResult.FailError diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/WorkerParamsFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/WorkerParamsFactory.kt index 2b7cba0b0c..b162566403 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/WorkerParamsFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/worker/WorkerParamsFactory.kt @@ -19,13 +19,23 @@ package org.matrix.android.sdk.internal.worker import androidx.work.Data import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.network.parsing.CheckNumberType -object WorkerParamsFactory { +internal object WorkerParamsFactory { + + val moshi by lazy { + // We are adding the CheckNumberType as we are serializing/deserializing multiple time in a row + // and we lost typing information doing so. + // We don't want this check to be done on all adapters, so we just add it here. + MoshiProvider.providesMoshi() + .newBuilder() + .add(CheckNumberType.JSON_ADAPTER_FACTORY) + .build() + } const val KEY = "WORKER_PARAMS_JSON" inline fun toData(params: T): Data { - val moshi = MoshiProvider.providesMoshi() val adapter = moshi.adapter(T::class.java) val json = adapter.toJson(params) return Data.Builder().putString(KEY, json).build() @@ -36,7 +46,6 @@ object WorkerParamsFactory { return if (json == null) { null } else { - val moshi = MoshiProvider.providesMoshi() val adapter = moshi.adapter(T::class.java) adapter.fromJson(json) } diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_airplane.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_aeroplane.xml similarity index 100% rename from matrix-sdk-android/src/main/res/drawable/ic_verification_airplane.xml rename to matrix-sdk-android/src/main/res/drawable/ic_verification_aeroplane.xml diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_headphone.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_headphones.xml similarity index 100% rename from matrix-sdk-android/src/main/res/drawable/ic_verification_headphone.xml rename to matrix-sdk-android/src/main/res/drawable/ic_verification_headphones.xml diff --git a/matrix-sdk-android/src/main/res/drawable/ic_verification_wrench.xml b/matrix-sdk-android/src/main/res/drawable/ic_verification_spanner.xml similarity index 100% rename from matrix-sdk-android/src/main/res/drawable/ic_verification_wrench.xml rename to matrix-sdk-android/src/main/res/drawable/ic_verification_spanner.xml diff --git a/matrix-sdk-android/src/main/res/values-az/strings.xml b/matrix-sdk-android/src/main/res/values-az/strings.xml index 9c60dfafa7..1f366c647f 100644 --- a/matrix-sdk-android/src/main/res/values-az/strings.xml +++ b/matrix-sdk-android/src/main/res/values-az/strings.xml @@ -79,72 +79,6 @@ Boş otaq - - It - Pişik - Aslan - At - Kərgədan - Donuz - Fil - Dovşan - Panda - Xoruz - Pinqvin - Tısbağa - Balıq - Ahtapot - Kəpənək - Çiçək - Ağac - Kaktus - Göbələk - Qlobus - Ay - Bulud - Atəş - Banan - Alma - Çiyələk - Qarğıdalı - Pizza - Tort - Ürək - Təbəssüm - Robot - Papaq - Eynəklər - Açar - Santa - Baş barmaqlar yuxarı - Çətir - Qum saatı - Saat - Hədiyyə - Lampa - Kitab - Qələm - Kağız sancağı - Qayçı - Qıfıl - Açar - Çəkic - Telefon - Bayraq - Qatar - Velosiped - Təyyarə - Raket - Kubok - Top - Gitara - Saz - Zəng - Anker - Qulaqlıqlar - Qovluq - Sancaq - İlkin sinxronizasiya: \nHesab idxal olunur… İlkin sinxronizasiya: diff --git a/matrix-sdk-android/src/main/res/values-bg/strings.xml b/matrix-sdk-android/src/main/res/values-bg/strings.xml index 07d59852f3..9654fd00b5 100644 --- a/matrix-sdk-android/src/main/res/values-bg/strings.xml +++ b/matrix-sdk-android/src/main/res/values-bg/strings.xml @@ -78,70 +78,6 @@ Съобщение премахнато от %1$s Премахнато съобщение [причина: %1$s] Съобщение премахнато от %1$s [причина: %2$s] - Куче - Котка - Лъв - Кон - Еднорог - Прасе - Слон - Заек - Панда - Петел - Пингвин - Костенурка - Риба - Октопод - Пеперуда - Цвете - Дърво - Кактус - Гъба - Глобус - Луна - Облак - Огън - Банан - Ябълка - Ягода - Царевица - Пица - Торта - Сърце - Усмивка - Робот - Шапка - Очила - Гаечен ключ - Дядо Коледа - Палец нагоре - Чадър - Пясъчен часовник - Часовник - Подарък - Лампа - Книга - Молив - Кламер - Ножици - Катинар - Ключ - Чук - Телефон - Знаме - Влак - Колело - Самолет - Ракета - Трофей - Топка - Китара - Тромпет - Звънец - Котва - Слушалки - Папка - Карфица Начална синхронизация: \nИмпортиране на профил… @@ -204,4 +140,16 @@ %s изпрати запитване за потвърждение на ключа ви, но клиентът ви не поддържа верифициране посредством чат. Ще трябва да използвате стария метод за верифициране на ключове. %1$s създаде стаята + Изпратихте снимка. + Изпратихте стикер. + + Ваша покана + Създадохте стаята + Поканихте %1$s + Присъединихте се в стаята + Напуснахте стаята + Отхвърлихте поканата + Изгонихте %1$s + Отблокирахте %1$s + Блокирахте %1$s diff --git a/matrix-sdk-android/src/main/res/values-bn-rIN/strings.xml b/matrix-sdk-android/src/main/res/values-bn-rIN/strings.xml index c8e70a9b20..5d38f0b3ee 100644 --- a/matrix-sdk-android/src/main/res/values-bn-rIN/strings.xml +++ b/matrix-sdk-android/src/main/res/values-bn-rIN/strings.xml @@ -136,72 +136,6 @@ খালি কক্ষ - - কুকুর - বেড়াল - সিংহ - ঘোড়া - ইউনিকর্ন - শূকর - হাতি - খরগোশ - পান্ডা - গৃহপালিত মোরগ - পেংগুইন - কচ্ছপ - মাছ - অক্টোপাস - প্রজাপতি - ফুল - গাছ - ফণীমনসা - মাশরুম - পৃথিবী - চন্দ্র - মেঘ - আগুন - কলা - আপেল - স্ট্রবেরি - ভূট্টা - পিজা - কেক - হৃদয় - স্মাইলি - রোবট - টুপি - চশমা - রেঞ্চ - সান্তা - থাম্বস আপ - ছাতা - বালিঘড়ি - ঘড়ি - উপহার - আলো বালব - বই - পেন্সিল - পেপার ক্লিপ - কাঁচি - লক - চাবি - হাতুড়ি - টেলিফোন - পতাকা - রেলগাড়ি - সাইকেল - বিমান - রকেট - ট্রফি - বল - গিটার - ট্রাম্পেট - ঘণ্টা - নোঙ্গর - হেডফোন - ফোল্ডার - পিন - প্রাথমিক সিঙ্ক: \nঅ্যাকাউন্ট আমদানি করা হচ্ছে… প্রাথমিক সিঙ্ক: @@ -288,8 +222,4 @@ %s আপনার কীটি যাচাই করার জন্য অনুরোধ করছে, তবে আপনার ক্লায়েন্ট ইন-চ্যাট কী যাচাইকরণ সমর্থন করে না। কীগুলি যাচাই করতে আপনাকে লিগ্যাসি কী যাচাইকরণ ব্যবহার করতে হবে। - গ্রহণ - পতন - বন্ধ করুন - diff --git a/matrix-sdk-android/src/main/res/values-cs/strings.xml b/matrix-sdk-android/src/main/res/values-cs/strings.xml index 44908c38f7..9e208f812a 100644 --- a/matrix-sdk-android/src/main/res/values-cs/strings.xml +++ b/matrix-sdk-android/src/main/res/values-cs/strings.xml @@ -79,68 +79,7 @@ Zpráva byla smazána [důvod: %1$s] Zpráva smazána uživatelem %1$s [důvod: %2$s] Uživatel %1$s obnovil pozvánku do místnosti pro uživatele %2$s - Kočka - Lev - Kůň - Jednorožec - Prase - Slon - Králík - Panda - Kohout - Tučňák - Želva - Ryba - Chobotnice - Motýl - Květina - Strom - Kaktus - Houba - Zeměkoule - Měsíc - Mrak - Oheň - Banán - Jablko - Jahoda - Kukuřice - Pizza - Dort - Srdce - Smajlík - Robot - Klobouk - Brýle - Santa Klaus - Zvednutý palec - Deštník - Přesípací hodiny - Hodiny - Dárek - Žárovka - Kniha - Tužka - Sponka - Nůžky - Zámek - Klíč - Kladivo - Telefon - Vlajka - Vlak - Jízdní kolo - Letadlo - Raketa - Trofej - Míč - Kytara - Trumpeta - Zvon - Kotva - Sluchátka - Desky - Úvodní synchronizace: + Úvodní synchronizace: \nImport účtu… Úvodní synchronizace: \nImport klíčů @@ -156,8 +95,6 @@ \nImport dat účtu Odesílání zprávy… - Maticový klíč - Připínáček Úvodní synchronizace: \nImport pozvánek diff --git a/matrix-sdk-android/src/main/res/values-de/strings.xml b/matrix-sdk-android/src/main/res/values-de/strings.xml index 0c857e78ee..ae80edb47f 100644 --- a/matrix-sdk-android/src/main/res/values-de/strings.xml +++ b/matrix-sdk-android/src/main/res/values-de/strings.xml @@ -89,73 +89,8 @@ Nachricht entfernt von %1$s Nachricht entfernt [Grund: %1$s] Nachricht entfernt von %1$s [Grund: %2$s] - Pizza - Hund - Katze - Löwe - Pferd - Einhorn - Schwein - Elefant - Kaninchen %s hat diesen Raum aufgewertet. - Panda - Hahn - Pinguin - Schildkröte - Fisch - Oktopus - Schmetterling - Blume - Baum - Kaktus - Pilz - Globus - Mond - Wolke - Feuer - Banane - Apfel - Erdbeere - Mais - Kuchen - Herz - Smiley - Roboter - Hut - Brille - Schraubenschlüssel - Weihnachtsmann - Daumen hoch - Regenschirm - Sanduhr - Uhr - Geschenk - Glühbirne - Buch - Bleistift - Büroklammer - Schere - Schloss - Schlüssel - Hammer - Telefon - Flagge - Zug - Fahrrad - Flugzeug - Rakete - Pokal - Ball - Gitarre - Trompete - Glocke - Anker - Kopfhörer - Ordner - Stecknadel - Sende eine Nachricht… Sendewarteschlange leeren @@ -297,10 +232,6 @@ Du hast Ende-zu-Ende-Verschlüsselung aktiviert. Du hast Ende-zu-Ende-Verschlüsselung aktiviert (unbekannter Algorithmus %1$s). - Akzeptiere - Ablehnen - Anruf beenden - %s hat Daten gesendet, um einen Anruf zu starten. Du hast Daten geschickt, um eine Anruf zu starten. diff --git a/matrix-sdk-android/src/main/res/values-de/strings_sas.xml b/matrix-sdk-android/src/main/res/values-de/strings_sas.xml new file mode 100644 index 0000000000..108dedd1a5 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-de/strings_sas.xml @@ -0,0 +1,68 @@ + + + + Hund + Katze + Löwe + Pferd + Einhorn + Schwein + Elefant + Hase + Panda + Hahn + Pinguin + Schildkröte + Fisch + Oktopus + Schmetterling + Blume + Baum + Kaktus + Pilz + Globus + Mond + Wolke + Feuer + Banane + Apfel + Erdbeere + Korn + Pizza + Kuchen + Herz + Smiley + Roboter + Hut + Brille + Schraubenschlüssel + Nikolaus + Daumen Hoch + Regenschirm + Sanduhr + Wecker + Geschenk + Glühbirne + Buch + Bleistift + Büroklammer + Schere + Schloss + Schlüssel + Hammer + Telefon + Flagge + Zug + Fahrrad + Flugzeug + Rakete + Trophäe + Ball + Gitarre + Trompete + Glocke + Anker + Kopfhörer + Ordner + Stecknadel + diff --git a/matrix-sdk-android/src/main/res/values-en-rGB/strings.xml b/matrix-sdk-android/src/main/res/values-en-rGB/strings.xml deleted file mode 100644 index f457e30ed0..0000000000 --- a/matrix-sdk-android/src/main/res/values-en-rGB/strings.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - Spanner - Aeroplane - diff --git a/matrix-sdk-android/src/main/res/values-eo/strings.xml b/matrix-sdk-android/src/main/res/values-eo/strings.xml index 4a1e2c4c65..69b009ca7e 100644 --- a/matrix-sdk-android/src/main/res/values-eo/strings.xml +++ b/matrix-sdk-android/src/main/res/values-eo/strings.xml @@ -72,72 +72,6 @@ Malplena ĉambro - - Hundo - Kato - Leono - Ĉevalo - Unukorno - Porko - Elefanto - Kuniklo - Pando - Koko - Pingveno - Testudo - Fiŝo - Polpo - Papilio - Floro - Arbo - Kakto - Fungo - Globo - Luno - Nubo - Fajro - Banano - Pomo - Frago - Maizo - Pico - Kuko - Koro - Mieneto - Roboto - Ĉapelo - Okulvitroj - Boltilo - Kristnaska viro - Dikfingro supren - Ombrelo - Sablohorloĝo - Horloĝo - Donaco - Lampo - Libro - Grifelo - Paperkuntenilo - Tondilo - Seruro - Ŝlosilo - Martelo - Telefono - Flago - Vagonaro - Biciklo - Aviadilo - Raketo - Trofeo - Pilko - Gitaro - Trumpeto - Sonorilo - Ankro - Kapaŭdilo - Dosierujo - Pinglo - Komenca spegulado: \nEnportante konton… Komenca spegulado: diff --git a/matrix-sdk-android/src/main/res/values-eo/strings_sas.xml b/matrix-sdk-android/src/main/res/values-eo/strings_sas.xml new file mode 100644 index 0000000000..16f762e362 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-eo/strings_sas.xml @@ -0,0 +1,68 @@ + + + + Hundo + Kato + Leono + Ĉevalo + Unukorno + Porko + Elefanto + Kuniklo + Pando + Virkoko + Pingveno + Testudo + Fiŝo + Polpo + Papilio + Floro + Arbo + Kakto + Fungo + Globo + Luno + Nubo + Fajro + Banano + Pomo + Frago + Maizo + Pico + Torto + Koro + Rideto + Roboto + Ĉapelo + Okulvitroj + Ŝraŭbŝlosilo + Kristnaska viro + Dikfingro supren + Ombrelo + Sablohorloĝo + Horloĝo + Donaco + Lampo + Libro + Krajono + Paperkuntenilo + Tondilo + Seruro + Ŝlosilo + Martelo + Telefono + Flago + Vagonaro + Biciklo + Aviadilo + Raketo + Trofeo + Pilko + Gitaro + Trumpeto + Sonorilo + Ankro + Kapaŭdilo + Dosierujo + Pinglo + diff --git a/matrix-sdk-android/src/main/res/values-es-rMX/strings.xml b/matrix-sdk-android/src/main/res/values-es-rMX/strings.xml index 35b7bfc829..a8e8477005 100644 --- a/matrix-sdk-android/src/main/res/values-es-rMX/strings.xml +++ b/matrix-sdk-android/src/main/res/values-es-rMX/strings.xml @@ -89,8 +89,4 @@ Mensaje eliminado por %1$s Mensaje eliminado [motivo: %1$s] Mensaje eliminado por %1$s [motivo: %2$s] - Perro - Gato - León - Caballo diff --git a/matrix-sdk-android/src/main/res/values-es/strings.xml b/matrix-sdk-android/src/main/res/values-es/strings.xml index 3c019b3b80..ae1f5633bf 100644 --- a/matrix-sdk-android/src/main/res/values-es/strings.xml +++ b/matrix-sdk-android/src/main/res/values-es/strings.xml @@ -90,65 +90,6 @@ Mensaje eliminado [motivo: %1$s] Mensaje eliminado por %1$s [motivo: %2$s] %1$s ha revocado la invitación a unirse a la sala para %2$s - Perro - Gato - León - Caballo - Unicornio - Cerdo - Elefante - Conejo - Panda - Gallo - Pingüino - Tortuga - Pez - Pulpo - Mariposa - Flor - Árbol - Cactus - Seta - Luna - Nube - Fuego - Plátano - Manzana - Fresa - Maíz - Pizza - Pastel - Corazón - Sombrero - Gafas - Llave inglesa - Pulgares arriba - Paraguas - Reloj de arena - Reloj - Regalo - Bombilla - Libro - Lápiz - Clip - Tijeras - Candado - Llave - Martillo - Teléfono - Bandera - Tren - Bicicleta - Avión - Cohete - Trofeo - Pelota - Guitarra - Trompeta - Campana - Ancla - Auriculares - Carpeta Sincronización Inicial \nImportando cuenta… Sincronización Inicial: @@ -173,12 +114,6 @@ %s ha actualizado la sala. - Globo Terráqueo - Cara sonriente - Robot - Papá Noel - Pin - Sincronización Inicial: \nImportando criptografía Sincronización Inicial: diff --git a/matrix-sdk-android/src/main/res/values-es/strings_sas.xml b/matrix-sdk-android/src/main/res/values-es/strings_sas.xml new file mode 100644 index 0000000000..fd396c1778 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-es/strings_sas.xml @@ -0,0 +1,53 @@ + + + + Perro + Gato + León + Caballo + Unicornio + Cerdo + Elefante + Conejo + Panda + Gallo + Pingüino + Tortuga + Pez + Pulpo + Mariposa + Flor + Árbol + Cactus + Seta + Globo + Luna + Nube + Fuego + Plátano + Manzana + Fresa + Maíz + Pizza + Tarta + Corazón + Emoticono + Robot + Sombrero + Gafas + Llave inglesa + Reloj + Regalo + Libro + Lápiz + Llave + Martillo + Telefono + Tren + Bicicleta + Bola + Guitarra + Trompeta + Campana + Alfiler + diff --git a/matrix-sdk-android/src/main/res/values-et/strings.xml b/matrix-sdk-android/src/main/res/values-et/strings.xml index b7cd202063..2fbe263464 100644 --- a/matrix-sdk-android/src/main/res/values-et/strings.xml +++ b/matrix-sdk-android/src/main/res/values-et/strings.xml @@ -77,72 +77,6 @@ Tühi jututuba - - Koer - Kass - Lõvi - Hobune - Ükssarvik - Siga - Elevant - Jänes - Panda - Kukk - Pingviin - Kilpkonn - Kala - Kaheksajalg - Liblikas - Lill - Puu - Kaktus - Seen - Maakera - Kuu - Pilv - Tuli - Banaan - Õun - Maasikas - Mais - Pitsa - Kook - Süda - Smaili - Robot - Kübar - Prillid - Mutrivõti - Jõuluvana - Pöidlad püsti - Vihmavari - Liivakell - Kell - Kingitus - Lambipirn - Raamat - Pliiats - Kirjaklamber - Käärid - Lukk - Võti - Haamer - Telefon - Lipp - Rong - Jalgratas - Lennuk - Rakett - Auhind - Pall - Kitarr - Trompet - Kelluke - Ankur - Kõrvaklapid - Kaust - Nööpnõel - Alglaadimine: \nImpordin kontot… Alglaadimine: @@ -295,8 +229,4 @@ Sa lülitasid sisse läbiva krüptimise. Sa lülitasid sisse läbiva krüptimise (kasutusel on tundmatu algoritm %1$s). - Võta vastu - Keeldu - Lõpeta kõne - diff --git a/matrix-sdk-android/src/main/res/values-et/strings_sas.xml b/matrix-sdk-android/src/main/res/values-et/strings_sas.xml new file mode 100644 index 0000000000..60df725eaa --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-et/strings_sas.xml @@ -0,0 +1,68 @@ + + + + Koer + Kass + Lõvi + Hobune + Ükssarvik + Siga + Elevant + Jänes + Panda + Kukk + Pingviin + Kilpkonn + Kala + Kaheksajalg + Liblikas + Lill + Puu + Kaktus + Seen + Maakera + Kuu + Pilv + Tuli + Banaan + Õun + Maasikas + Mais + Pitsa + Kook + Süda + Smaili + Robot + Kübar + Prillid + Mutrivõti + Jõuluvana + Pöidlad püsti + Vihmavari + Liivakell + Kell + Kingitus + Lambipirn + Raamat + Pliiats + Kirjaklamber + Käärid + Lukk + Võti + Haamer + Telefon + Lipp + Rong + Jalgratas + Lennuk + Rakett + Auhind + Pall + Kitarr + Trompet + Kelluke + Ankur + Kõrvaklapid + Kaust + Nööpnõel + diff --git a/matrix-sdk-android/src/main/res/values-eu/strings.xml b/matrix-sdk-android/src/main/res/values-eu/strings.xml index 1a5c81fe5e..bc61035c24 100644 --- a/matrix-sdk-android/src/main/res/values-eu/strings.xml +++ b/matrix-sdk-android/src/main/res/values-eu/strings.xml @@ -78,70 +78,6 @@ %1$s erabiltzaileak mezua kendu du Mezua kendu da [arrazoia: %1$s] %1$s erabiltzaileak mezua kendu du [arrazoia: %2$s] - Txakurra - Katua - Lehoia - Zaldia - Unikornioa - Zerria - Elefantea - Untxia - Panda - Oilarra - Pinguinoa - Dortoka - Arraina - Olagarroa - Tximeleta - Lorea - Zuhaitza - Kaktusa - Perretxikoa - Lurra - Ilargia - Hodeia - Sua - Banana - Sagarra - Marrubia - Artoa - Pizza - Pastela - Bihotza - Irrifartxoa - Robota - Txanoa - Betaurrekoak - Giltza - Santa - Ederto - Aterkia - Harea-erlojua - Erlojua - Oparia - Bonbilla - Liburua - Arkatza - Klipa - Artaziak - Giltzarrapoa - Giltza - Mailua - Telefonoa - Bandera - Trena - Bizikleta - Hegazkina - Kohetea - Saria - Baloia - Gitarra - Tronpeta - Kanpaia - Aingura - Aurikularrak - Karpeta - Txintxeta Hasierako sinkronizazioa: \nKontua inportatzen… diff --git a/matrix-sdk-android/src/main/res/values-fa/strings.xml b/matrix-sdk-android/src/main/res/values-fa/strings.xml index 18d8578e54..b88a98459d 100644 --- a/matrix-sdk-android/src/main/res/values-fa/strings.xml +++ b/matrix-sdk-android/src/main/res/values-fa/strings.xml @@ -77,72 +77,6 @@ اتاق خالی - - سگ - گربه - شیر - اسب - تک‌شاخ - خوک - فیل - خرگوش - پاندا - خروس - پنگوئن - لاک‌پشت - ماهی - هشت‌پا - پروانه - گل - درخت - کاکتوس - قارچ - جهان - ماه - ابر - آتش - موز - سیب - توت‌فرنگی - بلال - پیتزا - کیک - قلب - لبخند - آدم‌آهنی - کلاه - عینک - آچار - بابانوئل - شست - چتر - ساعت شنی - ساعت - هدیه - لامپ - کتاب - مداد - گیره کاغذ - قیچی - قفل - کلید - چکّش - تلفن - پرچم - قطار - دوچرخه - هواپیما - موشک - جام - توپ - گیتار - ترومپت - زنگ - لنگر - هدفون - پوشه - پونز - همگام‌سازی نخستین: \nدر حال درون‌ریزی حساب… همگام‌سازی نخستین: diff --git a/matrix-sdk-android/src/main/res/values-fi/strings.xml b/matrix-sdk-android/src/main/res/values-fi/strings.xml index 078769942c..fccd22d3b6 100644 --- a/matrix-sdk-android/src/main/res/values-fi/strings.xml +++ b/matrix-sdk-android/src/main/res/values-fi/strings.xml @@ -79,70 +79,6 @@ %1$s poisti viestin Viesti poistettu [syy: %1$s] %1$s poisti viestin [syy: %2$s] - Koira - Kissa - Leijona - Hevonen - Yksisarvinen - Sika - Norsu - Kani - Panda - Kukko - Pingviini - Kilpikonna - Kala - Tursas - Perhonen - Kukka - Puu - Kaktus - Sieni - Maapallo - Kuu - Pilvi - Tuli - Banaani - Omena - Mansikka - Maissi - Pizza - Kakku - Sydän - Hymiö - Robotti - Hattu - Silmälasit - Jakoavain - Joulupukki - Peukut ylös - Sateenvarjo - Tiimalasi - Kello - Lahja - Hehkulamppu - Kirja - Lyijykynä - Klemmari - Sakset - Lukko - Avain - Vasara - Puhelin - Lippu - Juna - Polkupyörä - Lentokone - Raketti - Palkinto - Pallo - Kitara - Trumpetti - Soittokello - Ankkuri - Kuulokkeet - Kansio - Nuppineula Alkusynkronointi: \nTuodaan tiliä… diff --git a/matrix-sdk-android/src/main/res/values-fi/strings_sas.xml b/matrix-sdk-android/src/main/res/values-fi/strings_sas.xml new file mode 100644 index 0000000000..b690fee4ed --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-fi/strings_sas.xml @@ -0,0 +1,68 @@ + + + + Koira + Kissa + Leijona + Hevonen + Yksisarvinen + Sika + Norsu + Kani + Panda + Kukko + Pingviini + Kilpikonna + Kala + Tursas + Perhonen + Kukka + Puu + Kaktus + Sieni + Maapallo + Kuu + Pilvi + Tuli + Banaani + Omena + Mansikka + Maissi + Pizza + Kakku + Sydän + Hymynaama + Robotti + Hattu + Silmälasit + Mutteriavain + Joulupukki + Peukalo ylös + Sateenvarjo + Tiimalasi + Pöytäkello + Lahja + Hehkulamppu + Kirja + Lyijykynä + Paperiliitin + Sakset + Lukko + Avain + Vasara + Puhelin + Lippu + Juna + Polkupyörä + Lentokone + Raketti + Palkinto + Pallo + Kitara + Trumpetti + Soittokello + Ankkuri + Kuulokkeet + Kansio + Nuppineula + diff --git a/matrix-sdk-android/src/main/res/values-fr/strings.xml b/matrix-sdk-android/src/main/res/values-fr/strings.xml index aad3bd1afb..71b956a7e7 100644 --- a/matrix-sdk-android/src/main/res/values-fr/strings.xml +++ b/matrix-sdk-android/src/main/res/values-fr/strings.xml @@ -78,70 +78,6 @@ Message supprimé par %1$s Message supprimé [motif : %1$s] Message supprimé par %1$s [motif : %2$s] - Chien - Chat - Lion - Cheval - Licorne - Cochon - Éléphant - Lapin - Panda - Coq - Manchot - Tortue - Poisson - Pieuvre - Papillon - Fleur - Arbre - Cactus - Champignon - Terre - Lune - Nuage - Feu - Banane - Pomme - Fraise - Maïs - Pizza - Gâteau - Cœur - Smiley - Robot - Chapeau - Lunettes - Clé plate - Père Noël - Pouce levé - Parapluie - Sablier - Horloge - Cadeau - Ampoule - Livre - Crayon - Trombone - Ciseaux - Cadenas - Clé - Marteau - Téléphone - Drapeau - Train - Vélo - Avion - Fusée - Trophée - Balle - Guitare - Trompette - Cloche - Ancre - Écouteurs - Dossier - Épingle Synchronisation initiale : \nImportation du compte… diff --git a/matrix-sdk-android/src/main/res/values-fr/strings_sas.xml b/matrix-sdk-android/src/main/res/values-fr/strings_sas.xml new file mode 100644 index 0000000000..af9d797542 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-fr/strings_sas.xml @@ -0,0 +1,68 @@ + + + + Chien + Chat + Lion + Cheval + Licorne + Cochon + Éléphant + Lapin + Panda + Coq + Manchot + Tortue + Poisson + Poulpe + Papillon + Fleur + Arbre + Cactus + Champignon + Globe + Lune + Nuage + Feu + Banane + Pomme + Fraise + Maïs + Pizza + Gâteau + Cœur + Sourire + Robot + Châpeau + Lunettes + Clé à molette + Père Noël + Pouce en l\'air + Parapluie + Sablier + Réveil + Cadeau + Ampoule + Livre + Crayon + Trombone + Ciseaux + Cadenas + Clé + Marteau + Téléphone + Drapeau + Train + Vélo + Avion + Fusée + Trophée + Ballon + Guitare + Trompette + Cloche + Ancre + Casque audio + Dossier + Punaise + diff --git a/matrix-sdk-android/src/main/res/values-hu/strings.xml b/matrix-sdk-android/src/main/res/values-hu/strings.xml index 35f35eaecd..896a97b023 100644 --- a/matrix-sdk-android/src/main/res/values-hu/strings.xml +++ b/matrix-sdk-android/src/main/res/values-hu/strings.xml @@ -77,70 +77,6 @@ Üzenetet eltávolította: %1$s Üzenet eltávolítva [ok: %1$s] Üzenetet eltávolította: %1$s [ok: %2$s] - Kutya - Macska - Oroszlán - - Egyszarvú - Malac - Elefánt - Nyúl - Panda - Kakas - Pingvin - Teknős - Hal - Polip - Pillangó - Virág - Fa - Kaktusz - Gomba - Föld - Hold - Felhő - Tűz - Banán - Alma - Eper - Kukorica - Pizza - Süti - Szív - Smiley - Robot - Kalap - Szemüveg - Csavarkulcs - Télapó - Hüvelykujj fel - Esernyő - Homokóra - Óra - Ajándék - Égő - Könyv - Ceruza - Gémkapocs - Olló - Zár - Kulcs - Kalapács - Telefon - Zászló - Vonat - Kerékpár - Repülő - Rakéta - Trófea - Labda - Gitár - Trombita - Harang - Vasmacska - Fejhallgató - Mappa - Induló szinkronizáció: \nFiók betöltése… diff --git a/matrix-sdk-android/src/main/res/values-it/strings.xml b/matrix-sdk-android/src/main/res/values-it/strings.xml index 2b2a097f13..cf081752a2 100644 --- a/matrix-sdk-android/src/main/res/values-it/strings.xml +++ b/matrix-sdk-android/src/main/res/values-it/strings.xml @@ -78,70 +78,6 @@ Messaggio rimosso da %1$s Messaggio rimosso [motivo: %1$s] Messaggio rimosso da %1$s [motivo: %2$s] - Cane - Gatto - Leone - Cavallo - Unicorno - Maiale - Elefante - Coniglio - Panda - Gallo - Pinguino - Tartaruga - Pesce - Piovra - Farfalla - Fiore - Albero - Cactus - Fungo - Globo - Luna - Nuvola - Fuoco - Banana - Mela - Fragola - Mais - Pizza - Torta - Cuore - Sorriso - Robot - Cappello - Occhiali - Chiave inglese - Babbo Natale - Pollice in su - Ombrello - Clessidra - Orologio - Regalo - Lampadina - Libro - Matita - Graffetta - Forbici - Lucchetto - Chiave - Martello - Telefono - Bandiera - Treno - Bicicletta - Aeroplano - Razzo - Trofeo - Palla - Chitarra - Tromba - Campana - Ancora - Cuffie - Cartella - Spillo Sync iniziale: \nImportazione account… @@ -296,8 +232,4 @@ Hai attivato la crittografia end-to-end. Hai attivato la crittografia end-to-end (algoritmo %1$s sconosciuto). - Accetta - Rifiuta - Riaggancia - diff --git a/matrix-sdk-android/src/main/res/values-ja/strings_sas.xml b/matrix-sdk-android/src/main/res/values-ja/strings_sas.xml new file mode 100644 index 0000000000..618302eb4f --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-ja/strings_sas.xml @@ -0,0 +1,20 @@ + + + + + + + たこ + + + きのこ + + リンゴ + ケーキ + ロボと + めがね + + 電話機 + 電車 + 自転車 + diff --git a/matrix-sdk-android/src/main/res/values-kab/strings.xml b/matrix-sdk-android/src/main/res/values-kab/strings.xml new file mode 100644 index 0000000000..0d1cad6550 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-kab/strings.xml @@ -0,0 +1,225 @@ + + + %1$s: %2$s + %1$s t.yuzen tugna. + Tuzneḍ tugna. + Tinubga n %s + Tinubga-k•m + %1$s yesnulfa-d taxxamt + Tesnulfaḍ-d taxxamt-a + %1$s inced-d %2$s + Tnecdeḍ-d %1$s + %1$s inced-ik-id + %1$s yedda ɣer texxamt + Teddiḍ ɣer texxamt + %1$s yeǧǧa taxxamt + Teǧǧiḍ taxxamt + %1$s yugi/tugi tinubga + Tufiḍ tinubga + %1$s yessufeɣ %2$s + Tessufɣeḍ %1$s + Tbeddleḍ avatar-inek·inem + Anedbal + Aseɣyad + Amezwer + Sagen + + %1$s seg %2$s ɣer %3$s + + Tegguma ad d-tali tugna + + Tansa n yimayl + + %1$s azen astiker. + Tuzneḍ amenṭaḍ. + + %1$s yekkes agdal i %2$s + Tekkseḍ agdal i %1$s + %1$s igdel %2$s + Tgedleḍ %1$s + %1$s issefsex tinubga n %2$s + Tesfesxeḍ tinubga n %1$s + %1$s ibeddel avatar-is + %1$s isbadu isem-is i d-ittuseknen ɣer %2$s + Tesbaduḍ isem-ik•im i d-ittuseknen ɣer %1$s + %1$s ibeddel isem-is i d-ittuseknen seg %2$s ɣer %3$s + Tbeddleḍ isem-ik•im i d-ittuseknen seg %1$s ɣer %2$s + %1$s yekkes isem-is i d-ittuseknen (yella %2$s) + Tekkseḍ isem-ik·im yettwaskanen (d %1$s) + %1$S isnifel asentel s: %2$S + Tesnifleḍ asentel s: %2$S + %1$s ibeddel avaṭar n texxamt + Tbeddleḍ avaṭar n texxamt + %1$s ibeddel isem n texxamt s: %2$s + Tbeddleḍ isem n texxamt s: %2$s + %s isɛedda siwel s tvidyut. + Tesɛeddaḍ siwel s tvidyut. + %s isɛedda asiwel s taɣect. + Tesɛeddaḍ siwel s taɣect. + %s yuzen isefka i usbadu n usiwel. + Tuzneḍ isefka i usbadu n usiwel. + %s yerra ɣef usiwel. + Terriḍ ɣef usiwel. + %s iḥbes asiwel. + Tḥebseḍ asiwel. + meṛṛa iɛeggalen n texxamt, segmi ara d-ttwanecden. + meṛṛa iɛeggalen n texamt, segmi ara d-rnun. + meṛṛa iɛeggalen n texxamt. + yal yiwen. + arussin (%s). + %1$s isermed awgelhen seg yixef ɣer yixef (%2$s) + Tesremdeḍ awgelhen seg yixef ɣer yixef (%2$s) + %s ileqqem taxxamt-a. + Tleqqmeḍ taxxamt-a. + + %1$s isuter-d asarag VoIP + Tsutreḍ-d asarag VoIP + Asarag VoIP yebda + Asarag VoIP yekfa + + (avatar daɣen ibeddel) + %1$s yekkes isem n texxamt + Tekkseḍ isem n texxamt + %1$s yekkes asentel n texxamt + Tekkseḍ asentel n texxamt + %1$s yekkes avatar n texxamt + Tekkseḍ avatar n texxamt + Izen ittwakkes + Izen ittwakkes sɣur %1$s + Izen ittwakkes [tamentilt: %1$s] + Izen ittwakkes sɣur %1$s [tamentilt: %2$s] + %1$s ileqqem amaɣnu-ines %2$s + Tleqqmeḍ amaɣnu-inek•inem %1$s + %1$s yuzen tinubga i %2$s akken ad yeddu ɣer texxamt + Tuzneḍ tinubga i %1$s akken ad yeddu ɣer texxamt + %1$s iqbel tinubga i %2$s + Tqebleḍ tinubga i %1$s + + %1$s yerna awiǧit %2$s + Terniḍ awiǧit %1$s + %1$s yekkes awiǧit %2$s + Tekkseḍ awiǧit %1$s + %1$s ibeddel awiǧit %2$s + Tbeddleḍ awiǧit %1$s + + Sagen (%1$) + Tbeddleḍ aswir n tezmert n %1$s. + %1$s ibeddel aswir n tezmert n %2$s. + ** Awgelhen d awezɣi: %s ** + Ibenk n umazan ur aɣ-d-yuzin ara tisura i yizen-a. + + Tuzna n yizen d tawezɣit + + Tuccḍa deg uẓeṭṭa + Tuccḍa deg Matrix + + %1$s iga amazray n texxamyt i d-iteddun yettban i %2$s + Tgiḍ amazray n texxamyt i d-iteddun yettban i %1$s + %1$s issefsax tinubga i %2$s i wakken ad d-yekcem ɣer texxamt + Tesfesxeḍ tinubga i %1$s i wakken ad d-yernu ɣer texxamt + D awezɣi tura ad nales ad nuɣal ɣer texxamt tilemt. + + Izen yettwawgelhen + + Uṭṭun n tiliɣri + + Tinubga sɣur %s + Tinubga ɣer texxamt + + %1$s d %2$s + + + %1$s d 1 wayeḍ + %1$s d %2$d wiyaḍ + + + Tremdeḍ awgelhen seg yixef ɣer yixef (alguritm %1$s ur yettwassen ara). + + %s isuter-d ad isenqed tasarut-ik·im, maca amsaɣ-ik·im ur issefrak ara asenqed n tsura deg yidiwenniyen. Ilaq-ak·am useqdec asenqed iqdim n tsura i usenqed n tsura. + + Taxxamt tilemt + + Amtawi n tazwara: +\nAktar n umiḍan… + Amtawi n tazwara: +\nAktar n uwgelhen + Amtawi n tazwara: +\nAktar n texxamin + Amtawi n tazwara: +\nAktar n texxamin iɣer terniḍ + Amtawi n tazwara: +\nAktar n texxamin iɣer tettwanecdeḍ + Amtawi n tazwara: +\nAktar n texxamin i teǧǧiḍ + Amtawi n tazwara: +\nAktar n tmezdagnutin + Amtawi n tazwara: +\nAktar n yisefka n umiḍan + + Tuzzna n yizen… + Tinubga n %1$s. Tamentilt: %2$s + Tinubga-k•m. Tamentilt: %1$s + %1$s inced %2$s. Tamentilt: %3$s + Tnecdeḍ %1$s. Tamentilt: %2$s + %1$s inced-ik•ikem. Tamentilt: %2$s + %1$s yedda ɣer texxamt. Tamentilt: %2$s + Teddiḍ ɣer texxamt. Tamentilt: %1$s + %1$s yeǧǧa taxxamt. Tamentilt: %2$s + Teǧǧiḍ taxxamt. Tamentilt: %1$s + %1$s yugi tinubga. Tamentilt: %2$s + Tugiḍ tinubga. Tamentilt: %1$s + %1$s yessufeɣ %2$s. Tamentilt: %3$s + Tessufɣeḍ %1$s. Tamentilt: %2$s + %1$s yekkes agdal i %2$s. Tamentilt: %3$s + Tekkseḍ agdal i %1$s. Tamentilt: %2$s + %1$s igdel %2$s. Tamentilt: %3$s + Tgedleḍ %1$s. Tamentilt: %2$s + %1$s yuzen tinubga i %2$s akken ad yeddu ɣer texxamt. Tamentilt: %3$s + Tuzneḍ tinubga i %1$s iwakken ad yeddu ɣer texxamt. Tamentilt: %2$s + %1$s iqbel tinubga i %2$s. Tamentilt: %3$s + Tqebleḍ tinubga i %1$s. Tamentilt: %2$s + %1$s issefsex tinubga n %2$s. Tamentilt: %3$s + Tesfesxeḍ tinubga n %1$s. Tamentilt: %2$s + + + %1$s yerna %2$s d tansa i texxamt-a. + %1$s yerna %2$s d tansiwin i texxamt-a. + + + + Terniḍ %1$s d tansa i texxamt-a. + Terniḍ %1$s d tansiwin i texxamt-a. + + + + %1$s yekkes %2$s am tansa i texxamt-a. + %1$s yekkes %3$s am tansiwin i texxamt-a. + + + + Tekkseḍ %1$s am tansa i texxamt-a. + Tekkseḍ %2$s am tansiwin i texxamt-a. + + + %1$s yerna %2$s terniḍ tekkseḍ %3s am tansiwin i texxamt-a. + Terniḍ %1$s terniḍ tekkseḍ %2$s am tansiwin i texxamt-a. + + %1$s isbadu %2$s am tansa tagejdant i texxamt-a. + Tesbaduḍ %1$s am tansa tagejdant i texxamt-a. + %1$s yekkes tansa tagejdant i texxamt-a. + Tekkseḍ tansa tagejdant i texxamt-a. + + %1$s isireg inebgawen ad ddun ɣer texxamt. + Tsirgeḍ inebgawen ad ddun ɣer texxamt. + %1$s issewḥel inebgawen iwakken ur tteddun ara ɣer texxamt. + Tesweḥleḍ inebgawen iwakken ur tteddun ara ɣer texxamt. + + %1$s yermed awgelhen seg yixef ɣer yixef. + Tremdeḍ awgelhen seg yixef ɣer yixef. + %1$s yermed awgelhen seg yixef ɣer yixef (alguritm %2$s ur yettwassen ara). + Sfeḍ tabdart n uraǧu n tuzzna + + %1$s issefsex tinubga n %2$s i tmerniwt ɣer texxamt. Tamentilt: %2$s + Tesfesxeḍ tinubga n %1$s i tmerna ɣer texxamt. Tamentilt: %2$s + Yegguma ad yaru + diff --git a/matrix-sdk-android/src/main/res/values-ko/strings.xml b/matrix-sdk-android/src/main/res/values-ko/strings.xml index 88c5e7d618..eee67628eb 100644 --- a/matrix-sdk-android/src/main/res/values-ko/strings.xml +++ b/matrix-sdk-android/src/main/res/values-ko/strings.xml @@ -2,7 +2,6 @@ %1$s: %2$s %s님의 초대 - 헤드폰 %1$s님이 사진을 보냈습니다. %1$s님이 스티커를 보냈습니다. @@ -78,71 +77,6 @@ 빈 방 - - - 고양이 - 사자 - - 유니콘 - 돼지 - 코끼리 - 토끼 - 판다 - 수탉 - 펭귄 - 거북 - 물고기 - 문어 - 나비 - - 나무 - 선인장 - 버섯 - 지구본 - - 구름 - - 바나나 - 사과 - 딸기 - 옥수수 - 피자 - 케이크 - 하트 - 웃음 - 로봇 - 모자 - 안경 - 스패너 - 산타클로스 - 좋아요 - 우산 - 모래시계 - 시계 - 선물 - 전구 - - 연필 - 클립 - 가위 - 자물쇠 - 열쇠 - 망치 - 전화기 - 깃발 - 기차 - 자전거 - 비행기 - 로켓 - 트로피 - - 기타 - 트럼펫 - - - 폴더 - - 초기 동기화: \n계정 가져오는 중… 초기 동기화: diff --git a/matrix-sdk-android/src/main/res/values-nb-rNO/strings_sas.xml b/matrix-sdk-android/src/main/res/values-nb-rNO/strings_sas.xml new file mode 100644 index 0000000000..c348b5bfbb --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-nb-rNO/strings_sas.xml @@ -0,0 +1,68 @@ + + + + Hund + Katt + Løve + Hest + Enhjørning + Gris + Elefant + Kanin + Panda + Hane + Pingvin + Skilpadde + Fisk + Blekksprut + Sommerfugl + Blomst + Tre + Kaktus + Sopp + Globus + Måne + Sky + Flamme + Banan + Eple + Jordbær + Mais + Pizza + Kake + Hjerte + Smilefjes + Robot + Hatt + Briller + Fastnøkkel + Julenisse + Tommel Opp + Paraply + Timeglass + Klokke + Gave + Lyspære + Bok + Blyant + BInders + Saks + Lås + Nøkkel + Hammer + Telefon + Flagg + Tog + Sykkel + Fly + Rakett + Pokal + Ball + Gitar + Trompet + Bjelle + Anker + Hodetelefoner + Mappe + Tegnestift + diff --git a/matrix-sdk-android/src/main/res/values-nl/strings.xml b/matrix-sdk-android/src/main/res/values-nl/strings.xml index 22eb61f109..1b05052ba6 100644 --- a/matrix-sdk-android/src/main/res/values-nl/strings.xml +++ b/matrix-sdk-android/src/main/res/values-nl/strings.xml @@ -87,70 +87,6 @@ Bericht verwijderd door %1$s Bericht verwijderd [reden: %1$s] Bericht verwijderd door %1$s [reden: %2$s] - Hond - Kat - Leeuw - Paard - Eenhoorn - Varken - Olifant - Konijn - Panda - Haan - Pinguïn - Schildpad - Vis - Octopus - Vlinder - Bloem - Boom - Cactus - Paddenstoel - Aardbol - Maan - Wolk - Vuur - Banaan - Appel - Aardbei - Maïs - Pizza - Taart - Hart - Smiley - Robot - Hoed - Bril - Moersleutel - Kerstman - Duim omhoog - Paraplu - Zandloper - Klok - Cadeau - Gloeilamp - Boek - Potlood - Paperclip - Schaar - Slot - Sleutel - Hamer - Telefoon - Vlag - Trein - Fiets - Vliegtuig - Raket - Trofee - Bal - Gitaar - Trompet - Bel - Anker - Koptelefoon - Map - Speld Initiële synchronisatie: \nAccount wordt geïmporteerd… diff --git a/matrix-sdk-android/src/main/res/values-nl/strings_sas.xml b/matrix-sdk-android/src/main/res/values-nl/strings_sas.xml new file mode 100644 index 0000000000..077244232a --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-nl/strings_sas.xml @@ -0,0 +1,68 @@ + + + + Hond + Kat + Leeuw + Paard + Eenhoorn + Varken + Olifant + Konijn + Panda + Haan + Pinguïn + Schildpad + Vis + Octopus + Vlinder + Bloem + Boom + Cactus + Paddenstoel + Wereldbol + Maan + Wolk + Vuur + Banaan + Appel + Aardbei + Maïs + Pizza + Taart + Hart + Smiley + Robot + Hoed + Bril + Moersleutel + Kerstman + Duim omhoog + Paraplu + Zandloper + Wekker + Geschenk + Gloeilamp + Boek + Potlood + Papierklemmetje + Schaar + Slot + Sleutel + Hamer + Telefoon + Vlag + Trein + Fiets + Vliegtuig + Raket + Trofee + Bal + Gitaar + Trompet + Bel + Anker + Koptelefoon + Map + Duimspijker + diff --git a/matrix-sdk-android/src/main/res/values-nn/strings.xml b/matrix-sdk-android/src/main/res/values-nn/strings.xml index 601cf4c9df..d986e697ad 100644 --- a/matrix-sdk-android/src/main/res/values-nn/strings.xml +++ b/matrix-sdk-android/src/main/res/values-nn/strings.xml @@ -77,70 +77,6 @@ %1$s strauk meldingi Meldingi vart stroki [av di: %1$s] %1$s strauk meldingi [av di: %2$s] - Hund - Katt - Løva - Hest - Einhyrning - Gris - Elefant - Hare - Panda - Hane - Pingvin - Skjoldpadda - Fisk - Blekksprut - Fivrelde - Blome - Tre - Kaktus - Sopp - Klote - Måne - Sky - Eld - Banan - Eple - Jordbær - Mais - Pizza - Kaka - Hjarta - Smilandlit - Robot - Hatt - Brillor - Skiftenykel - Nissen - Tumalen Upp - Regnskjold - Timeglas - Ur - Gåva - Ljospera - Bok - Blyant - Binders - Saks - Lås - Nykel - Hamar - Telefon - Flagg - Tog - Sykkel - Flyg - Rakett - Pokal - Ball - Gitar - Trompet - Klokka - Ankar - Hodetelefon - Mappa - Nål %s oppgraderte rommet. diff --git a/matrix-sdk-android/src/main/res/values-pl/strings.xml b/matrix-sdk-android/src/main/res/values-pl/strings.xml index dc380516b7..0d79edc658 100644 --- a/matrix-sdk-android/src/main/res/values-pl/strings.xml +++ b/matrix-sdk-android/src/main/res/values-pl/strings.xml @@ -79,73 +79,8 @@ Wiadomość usunięta przez %1$s Wiadomość usunięta [powód: %1$s] Wiadomość usunięta przez %1$s [powód: %2$s] - Pies - Kot - Lew - Koń - Jednorożec - Świnia - Słoń - Królik - Panda - Kogut - Pingwin - Żółw - Ryba - Ośmiornica - Motyl - Kwiat - Drzewo - Kaktus - Grzyb - Księżyc - Chmura - Ogień - Banan - Jabłko - Truskawka - Kukurydza - Pizza - Ciasto - Serce - Robot - Kapelusz - Okulary - Parasol - Klepsydra - Zegar - Żarówka - Książka - Ołówek - Spinacz - Nożyczki - Klucz - Telefon - Flaga - Pociąg - Rower - Samolot - Rakieta - Trofeum - Gitara - Trąbka - Dzwonek - Kotwica - Słuchawki - Folder - Pinezka - - Ziemia - Uśmiech - Klucz francuski - Mikołaj - Prezent - Młotek %s zakutalizował(a) ten pokój. - Kciuk w górę - Zamek - Piłka Synchronizacja początkowa: \nImportowanie konta… Synchronizacja początkowa: diff --git a/matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml b/matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml index e1b99506c8..2ba369b93a 100644 --- a/matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml +++ b/matrix-sdk-android/src/main/res/values-pt-rBR/strings.xml @@ -25,9 +25,9 @@ %s aceitou a chamada. %s encerrou a chamada. %1$s deixou o histórico futuro da sala visível para %2$s - todos os membros da sala, a partir do momento em que foram convidados. - todos os membros da sala, a partir do momento em que entraram nela. - todos os membros da sala. + todos os participantes da sala, a partir do momento em que foram convidados. + todos os participantes da sala, a partir do momento em que entraram nela. + todos os participantes da sala. qualquer pessoa. desconhecido (%s). %1$s ativou a criptografia de ponta a ponta (%2$s) @@ -124,10 +124,10 @@ Você removeu a descrição da sala %1$s removeu a foto da sala Você removeu a foto da sala - Mensagem removida - Mensagem removida por %1$s - Mensagem removida [motivo: %1$s] - Mensagem removida por %1$s [motivo: %2$s] + Mensagem apagada + Mensagem apagada por %1$s + Mensagem apagada [motivo: %1$s] + Mensagem apagada por %1$s [motivo: %2$s] Você atualizou o seu perfil %1$s Você enviou um convite para %1$s entrar na sala %1$s cancelou o convite a %2$s para entrar na sala @@ -151,71 +151,6 @@ %1$s alterou o nível de permissão de %2$s. %1$s de %2$s para %3$s - Cachorro - Gato - Leão - Cavalo - Unicórnio - Porco - Elefante - Coelho - Panda - Galo - Pinguim - Tartaruga - Peixe - Polvo - Borboleta - Flor - Árvore - Cacto - Cogumelo - Globo - Lua - Nuvem - Fogo - Banana - Maçã - Morango - Milho - Pizza - Bolo - Coração - Sorriso - Robô - Chapéu - Óculos - Chave inglesa - Papai-noel - Joinha - Guarda-chuva - Ampulheta - Relógio - Presente - Lâmpada - Livro - Lápis - Clipe de papel - Tesoura - Cadeado - Chave - Martelo - Telefone - Bandeira - Trem - Bicicleta - Avião - Foguete - Troféu - Bola - Guitarra - Trombeta - Sino - Âncora - Fones de ouvido - Pasta - Alfinete - Primeira sincronização:↵ \nImportando a conta… Primeira sincronização:↵ @@ -302,8 +237,4 @@ %s deseja confirmar a sua chave, mas o seu aplicativo não suporta a confirmação da chave da conversa. Você precisará usar a confirmação tradicional de chaves para confirmar chaves. - Aceitar - Recusar - Encerrar - diff --git a/matrix-sdk-android/src/main/res/values-ru/strings.xml b/matrix-sdk-android/src/main/res/values-ru/strings.xml index 1657d80f1c..a4d752782e 100644 --- a/matrix-sdk-android/src/main/res/values-ru/strings.xml +++ b/matrix-sdk-android/src/main/res/values-ru/strings.xml @@ -91,70 +91,6 @@ %1$s удалил(а) сообщение Сообщение удалено [причина: %1$s] %1$s удалил(а) сообщение [причина: %2$s] - Собака - Кошка - Лев - Лошадь - Единорог - Поросёнок - Слон - Кролик - Панда - Петух - Пингвин - Черепаха - Рыба - Осьминог - Бабочка - Цветок - Дерево - Кактус - Гриб - Земля - Луна - Облако - Огонь - Банан - Яблоко - Клубника - Кукуруза - Пицца - Пирожное - Сердце - Смайлик - Робот - Шляпа - Очки - Гаечный ключ - Санта - Большой палец вверх - Зонтик - Песочные часы - Часы - Подарок - Лампочка - Книга - Карандаш - Скрепка для бумаг - Ножницы - Замок - Ключ - Молоток - Телефон - Флаг - Поезд - Велосипед - Самолёт - Ракета - Трофей - Мяч - Гитара - Труба - Колокол - Якорь - Наушники - Папка - Булавка Начальная синхронизация: \nИмпорт учетной записи… @@ -313,8 +249,5 @@ Вы отправили данные для начала звонка. %1$s удалил(а) аватар комнаты Вы удалили аватар комнаты - Принять - Отклонить - Завершить звонок diff --git a/matrix-sdk-android/src/main/res/values-ru/strings_sas.xml b/matrix-sdk-android/src/main/res/values-ru/strings_sas.xml new file mode 100644 index 0000000000..f60d7026b6 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-ru/strings_sas.xml @@ -0,0 +1,68 @@ + + + + Собака + Кошка + Лев + Лошадь + Единорог + Свинья + Слон + Кролик + Панда + Петух + Пингвин + Черепаха + Рыба + Осьминог + Бабочка + Цветок + Дерево + Кактус + Гриб + Глобус + Луна + Облако + Огонь + Банан + Яблоко + Клубника + Кукуруза + Пицца + Торт + Сердце + Улыбка + Робот + Шляпа + Очки + Ключ + Санта + Большой палец вверх + Зонт + Песочные часы + Часы + Подарок + Лампочка + Книга + Карандаш + Скрепка + Ножницы + Замок + Ключ + Молоток + Телефон + Флаг + Поезд + Велосипед + Самолет + Ракета + Кубок + Мяч + Гитара + Труба + Колокол + Якорь + Наушники + Папка + Булавка + diff --git a/matrix-sdk-android/src/main/res/values-sk/strings.xml b/matrix-sdk-android/src/main/res/values-sk/strings.xml index 70e3a6ebd0..da869eacc2 100644 --- a/matrix-sdk-android/src/main/res/values-sk/strings.xml +++ b/matrix-sdk-android/src/main/res/values-sk/strings.xml @@ -81,70 +81,6 @@ Odstránená správa používateľom %1$s Odstránená správa [dôvod: %1$s] Odstránená správa používateľom %1$s [dôvod: %2$s] - Hlava psa - Hlava mačky - Hlava leva - Kôň - Hlava jednorožca - Hlava prasaťa - Slon - Hlava zajaca - Hlava pandy - Kohút - Tučniak - Korytnačka - Ryba - Chobotnica - Motýľ - Tulipán - Listnatý strom - Kaktus - Huba - Zemeguľa - Polmesiac - Oblak - Oheň - Banán - Červené jablko - Jahoda - Kukuričný klas - Pizza - Narodeninová torta - Červené srdce - Škeriaca sa tvár - Robot - Cylinder - Okuliare - Francúzsky kľúč - Santa Claus - Palec nahor - Dáždnik - Presýpacie hodiny - Budík - Zabalený darček - Žiarovka - Zatvorená kniha - Ceruzka - Sponka na papier - Nožnice - Zatvorená zámka - Kľúč - Kladivo - Telefón - Kockovaná zástava - Rušeň - Bicykel - Lietadlo - Raketa - Trofej - Futbal - Gitara - Trúbka - Zvon - Kotva - Slúchadlá - Fascikel - Špendlík Úvodná synchronizácia: \nPrebieha import účtu… @@ -299,8 +235,4 @@ %s požaduje overenie vašich šifrovacích kľúčov, ale váš klient nepodporuje overenie kľúčov v konverzácii. Budete musieť použiť zastaralú metódu overenia. - Prijať - Odmietnuť - Zavesiť - diff --git a/matrix-sdk-android/src/main/res/values-sk/strings_sas.xml b/matrix-sdk-android/src/main/res/values-sk/strings_sas.xml new file mode 100644 index 0000000000..72fd9cc2a3 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-sk/strings_sas.xml @@ -0,0 +1,68 @@ + + + + Hlava psa + Hlava mačky + Hlava leva + Kôň + Hlava jednorožca + Hlava prasaťa + Slon + Hlava zajaca + Hlava pandy + Kohút + Tučniak + Korytnačka + Ryba + Chobotnica + Motýľ + Tulipán + Listnatý strom + Kaktus + Huba + Zemeguľa + Polmesiac + Oblak + Oheň + Banán + Červené jablko + Jahoda + Kukuričný klas + Pizza + Narodeninová torta + červené srdce + Škeriaca sa tvár + Robot + Cilinder + Okuliare + Francúzsky kľúč + Santa Claus + Palec nahor + Dáždnik + Presýpacie hodiny + Budík + Zabalený darček + Žiarovka + Zatvorená kniha + Ceruzka + Sponka na papier + Nožnice + Zatvorená zámka + Kľúč + Kladivo + Telefón + Kockovaná zástava + Rušeň + Bicykel + Lietadlo + Raketa + Trofej + Futbal + Gitara + Trúbka + Zvon + Kotva + Slúchadlá + Fascikel + Špendlík + diff --git a/matrix-sdk-android/src/main/res/values-sq/strings.xml b/matrix-sdk-android/src/main/res/values-sq/strings.xml index e63e28288f..14a7c61bbc 100644 --- a/matrix-sdk-android/src/main/res/values-sq/strings.xml +++ b/matrix-sdk-android/src/main/res/values-sq/strings.xml @@ -77,68 +77,6 @@ Mesazhi u hoq nga %1$s Mesazh i hequr [arsye: %1$s] Mesazh i hequr nga %1$s [arsye: %2$s] - Qen - Mace - Luan - Kalë - Njëbrirësh - Derr - Elefant - Lepur - Panda - Këndes - Pinguin - Breshkë - Peshk - Oktapod - Flutur - Lule - Pemë - Kaktus - Kërpudhë - Rruzull - Hëna - Re - Zjarr - Banane - Mollë - Luleshtrydhe - Misër - Picë - Tortë - Zemër - Emotikon - Robot - Kapë - Syze - Çelës - Babagjyshi i Vitit të Ri - Ombrellë - Klepsidër - Sahat - Dhuratë - Llambë - Libër - Laps - Kapëse - Gërshërë - Dry - Kyç - Çekiç - Telefon - Flamur - Tren - Biçikletë - Aeroplan - Raketë - Trofe - Top - Kitarë - Trombë - Kambanë - Spirancë - Kufje - Dosje %s e përmirësoi këtë dhomë. Njëkohësimi Fillestar: @@ -200,6 +138,4 @@ %s po kërkon të verifikojë kyçin tuaj, por klienti juaj nuk mbulon verifikim kyçesh brenda fjalosjeje. Që të verifikoni kyça, do t’ju duhet të përdorni verifikim të dikurshëm kyçesh. %1$s krijo dhomën - Fiksoje - diff --git a/matrix-sdk-android/src/main/res/values-sv/strings.xml b/matrix-sdk-android/src/main/res/values-sv/strings.xml new file mode 100644 index 0000000000..491eb0bc49 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-sv/strings.xml @@ -0,0 +1,224 @@ + + + %1$s: %2$s + %1$s skickade en bild. + Du skickade en bild. + %1$s skickade en dekal. + Du skickade en dekal. + + Inbjudan från %s + Inbjudan från dig + %1$s skapade rummet + Du skapade rummet + %1$s bjöd in %2$s + Du bjöd in %1$s + %1$s bjöd in dig + %1$s gick med i rummet + Du gick med i rummet + %1$s lämnade rummet + Du lämnade rummet + %1$s avböjde inbjudan + Du avböjde inbjudan + %1$s kickade %2$s + Du kickade %1$s + %1$s avbannade %2$s + Du avbannade %1$s + %1$s avbannade %2$s + Du bannade %1$s + %1$s drog tillbaka inbjudan för %2$s + Du drog tillbaka inbjudan för %1$s + %1$s ändrade sin avatar + Du ändrade din avatar + %1$s satte sitt visningsnamn till %2$s + Du satte ditt visningsnamn till %1$s + %1$s bytte sitt visningsnamn från %2$s till %3$s + Du bytte ditt visningsnamn från %1$s till %2$s + %1$s tog bort sitt visningsnamn (det var %2$s) + Du tog bort ditt visningsnamn (det var %1$s) + %1$s bytte ämnet till: %2$s + Du bytte ämnet till: %1$s + %1$s bytte rummets avatar + Du bytte rummets avatar + %1$s bytte rummets namn till: %2$s + Du bytte rummets namnet till: %1$s + %s startade ett videosamtal. + Du startade ett videosamtal. + %s startade ett röstsamtal. + Du startade ett röstsamtal. + %s skickade data för att sätta upp samtalet. + Du skickade data för att sätta upp samtalet. + %s svarade på samtalet. + Du svarade på samtalet. + %s avslutade samtalet. + Du avslutade samtalet. + %1$s gjorde framtida rumshistorik synlig för %2$s + Du gjorde framtida rumshistorik synlig för %1$s + alla rumsmedlemmar, från tiden de bjöds in. + alla rumsmedlemmar, från tiden de gick med. + alla rumsmedlemmar. + vem som helst. + okänt (%s). + %1$s aktiverade totalsträckskryptering (%2$s) + Du aktiverade totalsträckskryptering (%1$s) + %s uppgraderade det här rummet. + Du uppgraderade det här rummet. + + %1$s begärde ett VoIP-gruppsamtal + Du begärde ett VoIP-gruppsamtal + VoIP-gruppsamtal startat + VoIP-gruppsamtal avslutat + + (avataren blev även bytt) + %1$s tog bort rummets namn + Du tog bort rummets namn + %1$s tog bort rummets ämne + Du tog bort rummets ämne + %1$s tog bort rummets avatar + Du tog bort rummets avatar + Meddelande borttaget + Meddelande borttaget av %1$s + Meddelande borttaget [anledning: %1$s] + Meddelande borttaget av %1$s [anledning: %2$s] + %1$s uppdaterade sim profil %2$s + Du uppdaterade din profil %1$s + %1$s bjöd in %2$s att gå med i rummet + Du bjöd in %1$s att gå med i rummet + %1$s drog tillbaka inbjudan för %2$s att gå med i rummet + Du drog tillbaka inbjudan för %1$s att gå med i rummet + %1$s accepterade inbjudan för %2$s + Du accepterade inbjudan för %1$s + + %1$s lade till %2$s-widget + Du lade till %1$s-widget + %1$s tog bort %2$s-widget + Du tog bort %1$s-widget + %1$s modifierade %2$s-widget + Du modifierade %1$s-widget + + Admin + Moderator + Standard + Anpassad (%1$d) + Anpassad + + Du ändrade behörighetsnivå för %1$s. + %1$s ändrade behörighetsnivå för %2$s. + %1$s från %2$s till %3$s + + ** Kan inte avkryptera: %s ** + Avsändarens enhet har inte gett oss nycklarna för det här meddelandet. + + Kunde inte dölja + Kunde inte skicka meddelandet + + Misslyckades att ladda upp bilden + + Nätverksfel + Matrixfel + + Det går för närvarande inte att gå med i ett tomt rum igen. + + Krypterat meddelande + + E-postadress + Telefonnummer + + Inbjudan från %s + Rumsinbjudan + + %1$s och %2$s + + + %1$s och en till + %1$s och %2$d till + + + Tomt rum + + Inledande synk: +\nImporterar konto… + Inledande synk: +\nImporterar krypto + Inledande synk: +\nImporterar rum + Inledande synk: +\nImporterar anslutna rum + Inledande synk: +\nImporterar inbjudna rum + Inledande synk: +\nImporterar lämnade rum + Inledande synk: +\nImporterar gemenskaper + Inledande synk: +\nImporterar kontodata + + Skickar meddelande… + Rensa sändningskö + + Inbjudan från %1$s. Anledning: %2$s + Inbjudan från dig. Anledning: %1$s + %1$s bjöd in %2$s. Anledning: %3$s + Du bjöd in %1$s. Anledning: %2$s + %1$s bjöd in dig. Anledning: %2$s + %1$s gick med i rummet. Anledning: %2$s + Du gick med i rummet. Anledning: %1$s + %1$s lämnade rummet. Anledning: %2$s + Du lämnade rummet. Anledning: %1$s + %1$s avböjde inbjudan. Anledning: %2$s + Du avböjde inbjudan. Anledning: %1$s + %1$s kickade %2$s. Anledning: %3$s + Du kickade %1$s. Anledning: %2$s + %1$s avbannade %2$s. Anledning: %3$s + Du avbannade %1$s. Anledning: %2$s + %1$s bannade %2$s. Anledning: %3$s + Du bannade %1$s. Anledning: %2$s + %1$s bjöd in %2$s att gå med i rummet. Anledning: %3$s + Du bjöd in %1$s att gå med i rummet. Anledning: %2$s + %1$s drog tillbaka inbjudan för %2$s att gå med i rummet. Anledning: %3$s + Du drog tillbaka inbjudan för %1$s att gå med i rummet. Anledning: %2$s + %1$s accepterade inbjudan för %2$s. Anledning: %3$s + Du accepterade inbjudan för %1$s. Anledning: %2$s + %1$s drog tillbaka inbjudan för %2$s. Anledning: %3$s + Du drog tillbaka inbjudan för %1$s. Anledning: %2$s + + + %1$s lade till %2$s som en adress för det här rummet. + %1$s lade till %2$s som adresser för det här rummet. + + + + Du lade till %1$s som en adress för det här rummet. + Du lade till %1$s som adresser för det här rummet. + + + + %1$s tog bort %2$s som en adress för det här rummet. + %1$s tog bort %2$s som adresser för det här rummet. + + + + Du tog bort %1$s som en adress för det här rummet. + Du tog bort %2$s som adresser för det här rummet. + + + %1$s lade till %2$s och tog bort %3$s som adresser för det här rummet. + Du lade till %1$s och tog bort %2$s som adresser för det här rummet. + + %1$s satta huvudadressen för det här rummet till %2$s. + Du satta huvudadressen för det här rummet till %1$s. + %1$s tog bort huvudadressen för det här rummet. + Du tog bort huvudadressen för det här rummet. + + %1$s tillät gäster att gå med i rummet. + Du tillät gäster att gå med i rummet. + %1$s hindrade gäster från att gå med i rummet. + Du hindrade gäster från att gå med i rummet. + + %1$s aktiverade totalsträckskryptering. + Du aktiverade totalsträckskryptering. + %1$s aktiverade totalsträckskryptering (okänd algoritm %2$s). + Du aktiverade totalsträckskryptering (okänd algoritm %1$s). + + %s begär att verifiera din nyckel, men din klient stöder inte nyckelverifiering i chatten. Du behöver använda legacynyckelverifiering för att verifiera nycklar. + + diff --git a/matrix-sdk-android/src/main/res/values-sv/strings_sas.xml b/matrix-sdk-android/src/main/res/values-sv/strings_sas.xml new file mode 100644 index 0000000000..1e06452ba5 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-sv/strings_sas.xml @@ -0,0 +1,68 @@ + + + + Hund + Katt + Lejon + Häst + Enhörning + Gris + Elefant + Kanin + Panda + Tupp + Pingvin + Sköldpadda + Fisk + Bläckfisk + Fjäril + Blomma + Träd + Kaktus + Svamp + Jordklot + Måne + Moln + Eld + Banan + Äpple + Jordgubbe + Majskolv + Pizza + Tårta + Hjärta + Smiley + Robot + Hatt + Glasögon + Skruvnyckel + Tomte + Tummen upp + Paraply + Timglas + Klocka + Paket + Lampa + Bok + Penna + Gem + Sax + Lås + Nyckel + Hammare + Telefon + Flagga + Ånglok + Cykel + Flygplan + Raket + Trofé + Boll + Gitarr + Trumpet + Bjällra + Ankare + Hörlurar + Mapp + Häftstift + diff --git a/matrix-sdk-android/src/main/res/values-uk/strings_sas.xml b/matrix-sdk-android/src/main/res/values-uk/strings_sas.xml new file mode 100644 index 0000000000..f979f77250 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-uk/strings_sas.xml @@ -0,0 +1,68 @@ + + + + Пес + Кіт + Лев + Кінь + Єдиноріг + Свиня + Слон + Кріль + Панда + Когут + Пінгвін + Черепаха + Риба + Восьминіг + Метелик + Квітка + Дерево + Кактус + Гриб + Глобус + Місяць + Хмара + Вогонь + Банан + Яблуко + Полуниця + Кукурудза + Піца + Пиріг + Серце + Посмішка + Робот + Капелюх + Окуляри + Гайковий ключ + Санта Клаус + Великий палець вгору + Парасолька + Пісковий годинник + Годинник + Подарунок + Лампочка + Книга + Олівець + Спиначка + Ножиці + Замок + Ключ + Молоток + Телефон + Прапор + Потяг + Велосипед + Літак + Ракета + Приз + М\'яч + Гітара + Труба + Дзвін + Якір + Навушники + Тека + Кнопка + diff --git a/matrix-sdk-android/src/main/res/values-vls/strings.xml b/matrix-sdk-android/src/main/res/values-vls/strings.xml index 5c9132ed35..f0f2287a8d 100644 --- a/matrix-sdk-android/src/main/res/values-vls/strings.xml +++ b/matrix-sdk-android/src/main/res/values-vls/strings.xml @@ -77,73 +77,7 @@ Leeg gesprek - - Hound - Katte - Leeuw - Peird - Eenhoorn - Zwyn - Olifant - Keun - Panda - Hoane - Pinguin - Schildpadde - Vis - Octopus - Beutervlieg - Bloem - Boom - Cactus - Paddestoel - Eirdbol - Moane - Wolk - Vier - Banoan - Appel - Freize - Mais - Pizza - Toarte - Erte - Smiley - Robot - Hoed - Bril - Moersleutel - Kestman - Duum omhooge - Paraplu - Zandloper - Klok - Cadeau - Gloeilampe - Boek - Potlood - Paperclip - Schoar - Hangslot - Sleutel - Oamer - Telefong - Vlagge - Tring - Veloo - Vlieger - Rakette - Trofee - Bolle - Gitoar - Trompette - Belle - Anker - Koptelefong - Mappe - Pinne - - Initiële synchronisoasje: + Initiële synchronisoasje: \nAccount wor geïmporteerd… Initiële synchronisoasje: \nCrypto wor geïmporteerd diff --git a/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml b/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml index ef080e8357..60322821d4 100644 --- a/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml +++ b/matrix-sdk-android/src/main/res/values-zh-rCN/strings.xml @@ -75,69 +75,6 @@ 消息已被 %1$s 移除 消息已被移除 [原因: %1$s] 消息已被 %1$s 移除 [原因: %2$s] - - - 狮子 - - 独角兽 - - 大象 - 兔子 - 熊猫 - 公鸡 - 企鹅 - 乌龟 - - 章鱼 - 蝴蝶 - - - 仙人掌 - 蘑菇 - 地球 - 月亮 - - - 香蕉 - 苹果 - 草莓 - 玉米 - 披萨 - 蛋糕 - - 微笑 - 机器人 - 帽子 - 眼镜 - 扳手 - 圣诞老人 - 点赞 - 雨伞 - 沙漏 - - 礼物 - 灯泡 - - 铅笔 - 回形针 - 剪刀 - - 钥匙 - 锤子 - 电话 - 旗子 - 火车 - 自行车 - 飞机 - 火箭 - 奖杯 - - 吉他 - 喇叭 - 铃铛 - - 耳机 - 文件夹 初始化同步: \n正在导入账号… 初始化同步: @@ -161,7 +98,6 @@ 清除正在发送队列 %1$s 撤回了对 %2$s 加入聊天室的邀请 - 置顶 %1$s 的邀请。理由:%2$s %1$s 邀请了 %2$s。理由:%3$s @@ -289,8 +225,4 @@ 您已开启端到端加密。 您已开启端到端加密(无法识别的算法 %1$s)。 - 接受 - 拒绝 - 挂断 - diff --git a/matrix-sdk-android/src/main/res/values-zh-rCN/strings_sas.xml b/matrix-sdk-android/src/main/res/values-zh-rCN/strings_sas.xml new file mode 100644 index 0000000000..439615735a --- /dev/null +++ b/matrix-sdk-android/src/main/res/values-zh-rCN/strings_sas.xml @@ -0,0 +1,10 @@ + + + + + + 狮子 + + 独角兽 + + diff --git a/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml b/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml index ee2662f143..355890923c 100644 --- a/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml +++ b/matrix-sdk-android/src/main/res/values-zh-rTW/strings.xml @@ -71,75 +71,10 @@ %1$s 和 和其他 %2$d 個人 - 訊息已移除 訊息已被 %1$s 移除 訊息已移除 [理由:%1$s] 訊息已被 %1$s 移除 [理由:%2$s] - - - - - 獨角獸 - - - - 貓熊 - 公雞 - 企鵝 - - - 章魚 - - - - 仙人掌 - 蘑菇 - 地球 - 月亮 - - - 香蕉 - 蘋果 - 草莓 - 玉米 - 披薩 - 蛋糕 - - 微笑 - 機器人 - 帽子 - 眼鏡 - 扳手 - 聖誕老人 - - 雨傘 - 沙漏 - 時鐘 - 禮物 - 燈泡 - - 鉛筆 - 迴紋針 - 剪刀 - - 鑰匙 - 鎚子 - 電話 - 旗子 - 火車 - 腳踏車 - 飛機 - 火箭 - 獎盃 - - 吉他 - 喇叭 - - - 耳機 - 資料夾 - 別針 初始化同步: \n正在匯入帳號…… @@ -290,8 +225,4 @@ 您開啟了端到端加密。 您開啟了端到端加密(無法識別的演算法 %1$s)。 - 接受 - 拒絕 - 掛斷 - diff --git a/matrix-sdk-android/src/main/res/values/strings.xml b/matrix-sdk-android/src/main/res/values/strings.xml index 0dc64c1b4b..f64ec9926e 100644 --- a/matrix-sdk-android/src/main/res/values/strings.xml +++ b/matrix-sdk-android/src/main/res/values/strings.xml @@ -152,137 +152,6 @@ Empty room - - - Dog - - Cat - - Lion - - Horse - - Unicorn - - Pig - - Elephant - - Rabbit - - Panda - - Rooster - - Penguin - - Turtle - - Fish - - Octopus - - Butterfly - - Flower - - Tree - - Cactus - - Mushroom - - Globe - - Moon - - Cloud - - Fire - - Banana - - Apple - - Strawberry - - Corn - - Pizza - - Cake - - Heart - - Smiley - - Robot - - Hat - - Glasses - - Wrench - - Santa - - Thumbs Up - - Umbrella - - Hourglass - - Clock - - Gift - - Light Bulb - - Book - - Pencil - - Paperclip - - Scissors - - Lock - - Key - - Hammer - - Telephone - - Flag - - Train - - Bicycle - - Airplane - - Rocket - - Trophy - - Ball - - Guitar - - Trumpet - - Bell - - Anchor - - Headphones - - Folder - - Pin - - Initial Sync:\nImporting account… Initial Sync:\nImporting crypto Initial Sync:\nImporting Rooms @@ -361,8 +230,4 @@ %s is requesting to verify your key, but your client does not support in-chat key verification. You will need to use legacy key verification to verify keys. - Accept - Decline - Hang Up - diff --git a/matrix-sdk-android/src/main/res/values/strings_sas.xml b/matrix-sdk-android/src/main/res/values/strings_sas.xml new file mode 100644 index 0000000000..8a366a22e5 --- /dev/null +++ b/matrix-sdk-android/src/main/res/values/strings_sas.xml @@ -0,0 +1,68 @@ + + + + Dog + Cat + Lion + Horse + Unicorn + Pig + Elephant + Rabbit + Panda + Rooster + Penguin + Turtle + Fish + Octopus + Butterfly + Flower + Tree + Cactus + Mushroom + Globe + Moon + Cloud + Fire + Banana + Apple + Strawberry + Corn + Pizza + Cake + Heart + Smiley + Robot + Hat + Glasses + Spanner + Santa + Thumbs Up + Umbrella + Hourglass + Clock + Gift + Light Bulb + Book + Pencil + Paperclip + Scissors + Lock + Key + Hammer + Telephone + Flag + Train + Bicycle + Aeroplane + Rocket + Trophy + Ball + Guitar + Trumpet + Bell + Anchor + Headphones + Folder + Pin + diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/pushrules/PushrulesConditionTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/pushrules/PushRulesConditionTest.kt similarity index 82% rename from matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/pushrules/PushrulesConditionTest.kt rename to matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/pushrules/PushRulesConditionTest.kt index be5aeaaf0f..b2c8d3bda8 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/pushrules/PushrulesConditionTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/api/pushrules/PushRulesConditionTest.kt @@ -16,6 +16,12 @@ package org.matrix.android.sdk.api.pushrules +import io.mockk.every +import io.mockk.mockk +import org.amshove.kluent.shouldBe +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test import org.matrix.android.sdk.MatrixTest import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.toContent @@ -24,28 +30,26 @@ import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent import org.matrix.android.sdk.internal.session.room.RoomGetter -import io.mockk.every -import io.mockk.mockk -import org.amshove.kluent.shouldBe -import org.junit.Assert.assertFalse -import org.junit.Assert.assertTrue -import org.junit.Test -class PushrulesConditionTest: MatrixTest { +class PushRulesConditionTest : MatrixTest { /* ========================================================================================== * Test EventMatchCondition * ========================================================================================== */ - @Test - fun test_eventmatch_type_condition() { - val condition = EventMatchCondition("type", "m.room.message") - - val simpleTextEvent = Event( + private fun createSimpleTextEvent(text: String): Event { + return Event( type = "m.room.message", eventId = "mx0", - content = MessageTextContent("m.text", "Yo wtf?").toContent(), + content = MessageTextContent("m.text", text).toContent(), originServerTs = 0) + } + + @Test + fun test_eventmatch_type_condition() { + val condition = EventMatchCondition("type", "m.room.message", false) + + val simpleTextEvent = createSimpleTextEvent("Yo wtf?") val rm = RoomMemberContent( Membership.INVITE, @@ -65,13 +69,9 @@ class PushrulesConditionTest: MatrixTest { @Test fun test_eventmatch_path_condition() { - val condition = EventMatchCondition("content.msgtype", "m.text") + val condition = EventMatchCondition("content.msgtype", "m.text", false) - val simpleTextEvent = Event( - type = "m.room.message", - eventId = "mx0", - content = MessageTextContent("m.text", "Yo wtf?").toContent(), - originServerTs = 0) + val simpleTextEvent = createSimpleTextEvent("Yo wtf?") assert(condition.isSatisfied(simpleTextEvent)) @@ -86,49 +86,44 @@ class PushrulesConditionTest: MatrixTest { ).toContent(), originServerTs = 0 ).apply { - assert(EventMatchCondition("content.membership", "invite").isSatisfied(this)) + assert(EventMatchCondition("content.membership", "invite", false).isSatisfied(this)) } } @Test fun test_eventmatch_cake_condition() { - val condition = EventMatchCondition("content.body", "cake") + val condition = EventMatchCondition("content.body", "cake", false) - Event( - type = "m.room.message", - eventId = "mx0", - content = MessageTextContent("m.text", "How was the cake?").toContent(), - originServerTs = 0 - ).apply { - assert(condition.isSatisfied(this)) - } - - Event( - type = "m.room.message", - eventId = "mx0", - content = MessageTextContent("m.text", "Howwasthecake?").toContent(), - originServerTs = 0 - ).apply { - assert(condition.isSatisfied(this)) - } + assert(condition.isSatisfied(createSimpleTextEvent("How was the cake?"))) + assert(condition.isSatisfied(createSimpleTextEvent("Howwasthecake?"))) } @Test fun test_eventmatch_cakelie_condition() { - val condition = EventMatchCondition("content.body", "cake*lie") + val condition = EventMatchCondition("content.body", "cake*lie", false) - val simpleTextEvent = Event( - type = "m.room.message", - eventId = "mx0", - content = MessageTextContent("m.text", "How was the cakeisalie?").toContent(), - originServerTs = 0) + assert(condition.isSatisfied(createSimpleTextEvent("How was the cakeisalie?"))) + } - assert(condition.isSatisfied(simpleTextEvent)) + @Test + fun test_eventmatch_words_only_condition() { + val condition = EventMatchCondition("content.body", "ben", true) + + assertFalse(condition.isSatisfied(createSimpleTextEvent("benoit"))) + assertFalse(condition.isSatisfied(createSimpleTextEvent("Hello benoit"))) + assertFalse(condition.isSatisfied(createSimpleTextEvent("superben"))) + + assert(condition.isSatisfied(createSimpleTextEvent("ben"))) + assert(condition.isSatisfied(createSimpleTextEvent("hello ben"))) + assert(condition.isSatisfied(createSimpleTextEvent("ben is there"))) + assert(condition.isSatisfied(createSimpleTextEvent("hello ben!"))) + assert(condition.isSatisfied(createSimpleTextEvent("hello Ben!"))) + assert(condition.isSatisfied(createSimpleTextEvent("BEN"))) } @Test fun test_notice_condition() { - val conditionEqual = EventMatchCondition("content.msgtype", "m.notice") + val conditionEqual = EventMatchCondition("content.msgtype", "m.notice", false) Event( type = "m.room.message", diff --git a/tools/import_sas_strings.py b/tools/import_sas_strings.py new file mode 100755 index 0000000000..faf4ed7080 --- /dev/null +++ b/tools/import_sas_strings.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2020 New Vector Ltd +# +# 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. + +import argparse +import json +import os +import os.path +# Run `pip3 install requests` if not installed yet +import requests + +### Arguments + +parser = argparse.ArgumentParser(description='Download sas string from matrix-doc.') +parser.add_argument('-v', + '--verbose', + help="increase output verbosity.", + action="store_true") + +args = parser.parse_args() + +if args.verbose: + print("Argument:") + print(args) + +base_url = "https://raw.githubusercontent.com/matrix-org/matrix-doc/master/data-definitions/sas-emoji.json" + +print("Downloading " + base_url + "…") + +r0 = requests.get(base_url) +data0 = json.loads(r0.content.decode()) + +if args.verbose: + print("Json data:") + print(data0) + +print() + +# emoji -> translation +default = dict() +# Language -> emoji -> translation +cumul = dict() + +for emoji in data0: + description = emoji["description"] + if args.verbose: + print("Description: " + description) + default[description] = description + + for lang in emoji["translated_descriptions"]: + if args.verbose: + print("Lang: " + lang) + if not (lang in cumul): + cumul[lang] = dict() + cumul[lang][description] = emoji["translated_descriptions"][lang] + +if args.verbose: + print(default) + print(cumul) + +def write_file(file, dict): + print("Writing file " + file) + if args.verbose: + print("With") + print(dict) + os.makedirs(os.path.dirname(file), exist_ok=True) + with open(file, mode="w", encoding="utf8") as o: + o.write("\n") + o.write("\n") + o.write(" \n") + for key in dict: + if dict[key] is None: + continue + o.write(" " + dict[key].replace("'", "\\'") + "\n") + o.write("\n") + +scripts_dir = os.path.dirname(os.path.abspath(__file__)) +data_defs_dir = os.path.join(scripts_dir, "../matrix-sdk-android/src/main/res") + +# Write default file +write_file(os.path.join(data_defs_dir, "values/strings_sas.xml"), default) + +# Write each language file +for lang in cumul: + androidLang = lang\ + .replace("_", "-r")\ + .replace("zh-rHans", "zh-rCN") + write_file(os.path.join(data_defs_dir, "values-" + androidLang + "/strings_sas.xml"), cumul[lang]) + +print() +print("Success!") diff --git a/tools/release/download_buildkite_artifacts.py b/tools/release/download_buildkite_artifacts.py index ef4251a14f..4439c2fb8c 100755 --- a/tools/release/download_buildkite_artifacts.py +++ b/tools/release/download_buildkite_artifacts.py @@ -25,9 +25,9 @@ import requests # This script downloads artifacts from buildkite. # Ref: https://buildkite.com/docs/apis/rest-api/artifacts#download-an-artifact -# Those two variable are specific to the RiotX project +# Those two variables are specific to the Element Android project ORG_SLUG = "matrix-dot-org" -PIPELINE_SLUG = "riotx-android" +PIPELINE_SLUG = "element-android" ### Arguments diff --git a/tools/templates/RiotXFeature/globals.xml.ftl b/tools/templates/ElementFeature/globals.xml.ftl similarity index 100% rename from tools/templates/RiotXFeature/globals.xml.ftl rename to tools/templates/ElementFeature/globals.xml.ftl diff --git a/tools/templates/RiotXFeature/recipe.xml.ftl b/tools/templates/ElementFeature/recipe.xml.ftl similarity index 100% rename from tools/templates/RiotXFeature/recipe.xml.ftl rename to tools/templates/ElementFeature/recipe.xml.ftl diff --git a/tools/templates/RiotXFeature/root/res/layout/fragment.xml.ftl b/tools/templates/ElementFeature/root/res/layout/fragment.xml.ftl similarity index 100% rename from tools/templates/RiotXFeature/root/res/layout/fragment.xml.ftl rename to tools/templates/ElementFeature/root/res/layout/fragment.xml.ftl diff --git a/tools/templates/RiotXFeature/root/src/app_package/Action.kt.ftl b/tools/templates/ElementFeature/root/src/app_package/Action.kt.ftl similarity index 100% rename from tools/templates/RiotXFeature/root/src/app_package/Action.kt.ftl rename to tools/templates/ElementFeature/root/src/app_package/Action.kt.ftl diff --git a/tools/templates/RiotXFeature/root/src/app_package/Activity.kt.ftl b/tools/templates/ElementFeature/root/src/app_package/Activity.kt.ftl similarity index 100% rename from tools/templates/RiotXFeature/root/src/app_package/Activity.kt.ftl rename to tools/templates/ElementFeature/root/src/app_package/Activity.kt.ftl diff --git a/tools/templates/RiotXFeature/root/src/app_package/Fragment.kt.ftl b/tools/templates/ElementFeature/root/src/app_package/Fragment.kt.ftl similarity index 100% rename from tools/templates/RiotXFeature/root/src/app_package/Fragment.kt.ftl rename to tools/templates/ElementFeature/root/src/app_package/Fragment.kt.ftl diff --git a/tools/templates/RiotXFeature/root/src/app_package/ViewEvents.kt.ftl b/tools/templates/ElementFeature/root/src/app_package/ViewEvents.kt.ftl similarity index 100% rename from tools/templates/RiotXFeature/root/src/app_package/ViewEvents.kt.ftl rename to tools/templates/ElementFeature/root/src/app_package/ViewEvents.kt.ftl diff --git a/tools/templates/RiotXFeature/root/src/app_package/ViewModel.kt.ftl b/tools/templates/ElementFeature/root/src/app_package/ViewModel.kt.ftl similarity index 100% rename from tools/templates/RiotXFeature/root/src/app_package/ViewModel.kt.ftl rename to tools/templates/ElementFeature/root/src/app_package/ViewModel.kt.ftl diff --git a/tools/templates/RiotXFeature/root/src/app_package/ViewState.kt.ftl b/tools/templates/ElementFeature/root/src/app_package/ViewState.kt.ftl similarity index 100% rename from tools/templates/RiotXFeature/root/src/app_package/ViewState.kt.ftl rename to tools/templates/ElementFeature/root/src/app_package/ViewState.kt.ftl diff --git a/tools/templates/RiotXFeature/template.xml b/tools/templates/ElementFeature/template.xml similarity index 99% rename from tools/templates/RiotXFeature/template.xml rename to tools/templates/ElementFeature/template.xml index 33d2edfc70..14c718c993 100644 --- a/tools/templates/RiotXFeature/template.xml +++ b/tools/templates/ElementFeature/template.xml @@ -2,7 +2,7 @@