diff --git a/CHANGES.md b/CHANGES.md index ae9e428795..acf41b40e6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,31 +2,44 @@ Changes in Element 1.0.6 (2020-XX-XX) =================================================== 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 bug 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) + - 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 Translations 🗣: - - + - The SDK is now using SAS string translations from [Weblate Matrix-doc project](https://translate.riot.im/projects/matrix-doc/) (#1909) SDK API changes ⚠️: - 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) diff --git a/README.md b/README.md index 9f31e83ef6..64c6c9d04d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![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=im.vector.app.android&metric=alert_status)](https://sonarcloud.io/dashboard?id=im.vector.app.android) @@ -14,7 +14,7 @@ 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 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 061dd73a40..f06d1859b5 100644 --- a/build.gradle +++ b/build.gradle @@ -87,7 +87,7 @@ sonarqube { property "sonar.projectVersion", project(":vector").android.defaultConfig.versionName property "sonar.sourceEncoding", "UTF-8" property "sonar.links.homepage", "https://github.com/vector-im/element-android/" - property "sonar.links.ci", "https://buildkite.com/matrix-dot-org/riotx-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" 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 90bdf02243..2c20137647 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -115,7 +115,7 @@ dependencies { 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" @@ -144,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 80e7b6dbbb..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 @@ -21,7 +21,6 @@ import android.util.Base64 import androidx.test.ext.junit.runners.AndroidJUnit4 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 @@ -29,6 +28,8 @@ import org.junit.runners.MethodSorters 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 /** @@ -52,17 +53,14 @@ class AttachmentEncryptionTest { 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 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\n- item2".let { markdownParser.parse(it).expect(it, "") } + "- item1".let { markdownParser.parse(it).expect(it, "") } + "- item1\n- item2".let { markdownParser.parse(it).expect(it, "") } } @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/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/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/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/crypto/attachments/MXEncryptedAttachments.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/attachments/MXEncryptedAttachments.kt index 9e1ef19b3a..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,9 +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.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 @@ -35,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 @@ -59,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 @@ -85,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, - 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 = 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") } } /** @@ -130,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 outputStream.toByteArray().inputStream() - .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/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/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/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/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 720269404f..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,6 +36,7 @@ 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 @@ -71,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) @@ -98,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) @@ -112,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(thumbnailData.bytes.inputStream(), 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 = cacheFile.outputStream() - 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(cacheFile.inputStream(), 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( @@ -238,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) } @@ -259,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 @@ -268,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/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/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/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 f878df52b2..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() 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 ead2dc9377..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) } 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 e46adeb9c1..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) } 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 5da14f0a41..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 @@ -34,7 +34,7 @@ import org.matrix.android.sdk.internal.worker.getSessionComponent 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 @@ -56,12 +56,12 @@ internal class SendEventWorker(context: Context, @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) @@ -75,22 +75,32 @@ internal class SendEventWorker(context: Context, .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(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(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()) { + 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() } } 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..a569b775a4 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 @@ -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) } 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/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/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/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/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 @@