Merge branch 'release/1.0.6'

This commit is contained in:
Benoit Marty 2020-09-08 17:47:17 +02:00
commit 0da106ba36
337 changed files with 12078 additions and 4166 deletions

View file

@ -1,6 +1,6 @@
### Pull Request Checklist
<!-- Please read [CONTRIBUTING.md](https://github.com/vector-im/riotX-android/blob/develop/CONTRIBUTING.md) before submitting your pull request -->
<!-- Please read [CONTRIBUTING.md](https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md) before submitting your pull request -->
- [ ] Changes has been tested on an Android device or Android emulator with API 21
- [ ] UI change has been tested on both light and dark themes

View file

@ -1,4 +1,4 @@
A full developer contributors list can be found [here](https://github.com/vector-im/riotX-android/graphs/contributors).
A full developer contributors list can be found [here](https://github.com/vector-im/element-android/graphs/contributors).
# Core team:

View file

@ -1,3 +1,52 @@
Changes in Element 1.0.6 (2020-09-08)
===================================================
Features ✨:
- List phone numbers and emails added to the Matrix account, and add emails and phone numbers to account (#44, #45)
Improvements 🙌:
- You can now join room through permalink and within room directory search
- Add long click gesture to copy userId, user display name, room name, room topic and room alias (#1774)
- Fix several issues when uploading big files (#1889)
- Do not propose to verify session if there is only one session and 4S is not configured (#1901)
- Call screen does not use proximity sensor (#1735)
Bugfix 🐛:
- Display name not shown under Settings/General (#1926)
- Editing message forgets line breaks and markdown (#1939)
- Words containing my name should not trigger notifications (#1781)
- Fix changing language issue
- Fix FontSize issue (#1483, #1787)
- Fix bad color for settings icon on Android < 24 (#1786)
- Change user or room avatar: when selecting Gallery, I'm not proposed to crop the selected image (#1590)
- Loudspeaker is always used (#1685)
- Fix uploads still don't work with room v6 (#1879)
- Can't handle ongoing call events in background (#1992)
- Handle room, user and group links by the Element app (#1795)
- Update associated site domain (#1833)
- Crash / Attachment viewer: Cannot draw a recycled Bitmap #2034
- Login with Matrix-Id | Autodiscovery fails if identity server is invalid and Homeserver ok (#2027)
- Support for image compression on Android 10
- Verification popup won't show
- Android 6: App crash when read Contact permission is granted (#2064)
- JSON for verification events leaks in to the room list (#1246)
- Replies to poll appears in timeline as unsupported events during sending (#1004)
Translations 🗣:
- The SDK is now using SAS string translations from [Weblate Matrix-doc project](https://translate.riot.im/projects/matrix-doc/) (#1909)
- New translation to kabyle
Build 🧱:
- Some dependencies have been upgraded (coroutine, recyclerView, appCompat, core-ktx, firebase-messaging)
- Buildkite:
New pipeline location: https://github.com/matrix-org/pipelines/blob/master/element-android/pipeline.yml
New build location: https://buildkite.com/matrix-dot-org/element-android
Other changes:
- Use File extension functions to make code more concise (#1996)
- Create a script to import SAS strings (#1909)
- Support `data-mx-[bg-]color` attributes on `<font>` tags.
Changes in Element 1.0.5 (2020-08-21)
===================================================

View file

@ -1,9 +1,9 @@
[![Buildkite](https://badge.buildkite.com/657d3db27364448d69d54f66c690f7788bc6aa80a7628e37f3.svg?branch=develop)](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop)
[![Buildkite](https://badge.buildkite.com/ad0065c1b70f557cd3b1d3d68f9c2154010f83c4d6f71706a9.svg?branch=develop)](https://buildkite.com/matrix-dot-org/element-android/builds?branch=develop)
[![Weblate](https://translate.riot.im/widgets/element-android/-/svg-badge.svg)](https://translate.riot.im/engage/element-android/?utm_source=widget)
[![Element Android Matrix room #element-android:matrix.org](https://img.shields.io/matrix/element-android:matrix.org.svg?label=%23element-android:matrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#element-android:matrix.org)
[![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=vector.android.riotx&metric=alert_status)](https://sonarcloud.io/dashboard?id=vector.android.riotx)
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=vector.android.riotx&metric=vulnerabilities)](https://sonarcloud.io/dashboard?id=vector.android.riotx)
[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=vector.android.riotx&metric=bugs)](https://sonarcloud.io/dashboard?id=vector.android.riotx)
[![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=im.vector.app.android&metric=alert_status)](https://sonarcloud.io/dashboard?id=im.vector.app.android)
[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=im.vector.app.android&metric=vulnerabilities)](https://sonarcloud.io/dashboard?id=im.vector.app.android)
[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=im.vector.app.android&metric=bugs)](https://sonarcloud.io/dashboard?id=im.vector.app.android)
# Element Android
@ -14,11 +14,13 @@ It is a total rewrite of [Riot-Android](https://github.com/vector-im/riot-androi
[<img src="resources/img/google-play-badge.png" alt="Get it on Google Play" height="60">](https://play.google.com/store/apps/details?id=im.vector.app)
[<img src="resources/img/f-droid-badge.png" alt="Get it on F-Droid" height="60">](https://f-droid.org/app/im.vector.app)
Nightly build: [![Buildkite](https://badge.buildkite.com/657d3db27364448d69d54f66c690f7788bc6aa80a7628e37f3.svg?branch=develop)](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop)
Nightly build: [![Buildkite](https://badge.buildkite.com/ad0065c1b70f557cd3b1d3d68f9c2154010f83c4d6f71706a9.svg?branch=develop)](https://buildkite.com/matrix-dot-org/element-android/builds?branch=develop)
# New Android SDK
Element is based on a new Android SDK fully written in Kotlin (like Element). In order to make the early development as fast as possible, Element and the new SDK currently share the same git repository. We will make separate repos once the SDK is stable enough.
Element is based on a new Android SDK fully written in Kotlin (like Element). In order to make the early development as fast as possible, Element and the new SDK currently share the same git repository.
At each Element release, the SDK module is copied to a dedicated repository: https://github.com/matrix-org/matrix-android-sdk2. That way, third party apps can add a regular gradle dependency to use it. So more details on how to do that here: https://github.com/matrix-org/matrix-android-sdk2.
# Roadmap
@ -27,6 +29,6 @@ The team will work to add them on a regular basis.
## Contributing
Please refer to [CONTRIBUTING.md](https://github.com/vector-im/riotX-android/blob/develop/CONTRIBUTING.md) if you want to contribute on Matrix Android projects!
Please refer to [CONTRIBUTING.md](https://github.com/vector-im/element-android/blob/develop/CONTRIBUTING.md) if you want to contribute on Matrix Android projects!
Come chat with the community in the dedicated Matrix [room](https://matrix.to/#/#element-android:matrix.org).

View file

@ -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)
}
}

View file

@ -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?) {

View file

@ -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)
}
}

View file

@ -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")
}
if (!wasPaused) {
videoView.start()
if (progress > 0) {

View file

@ -39,4 +39,9 @@ class ZoomableImageViewHolder constructor(itemView: View) :
}
internal val target = DefaultImageLoaderTarget.ZoomableImageTarget(this, touchImageView)
override fun onRecycled() {
super.onRecycled()
touchImageView.setImageDrawable(null)
}
}

View file

@ -81,15 +81,15 @@ apply plugin: 'org.sonarqube'
sonarqube {
properties {
property "sonar.projectName", "RiotX-Android"
property "sonar.projectKey", "vector.android.riotx"
property "sonar.projectName", "Element-Android"
property "sonar.projectKey", "im.vector.app.android"
property "sonar.host.url", "https://sonarcloud.io"
property "sonar.projectVersion", project(":vector").android.defaultConfig.versionName
property "sonar.sourceEncoding", "UTF-8"
property "sonar.links.homepage", "https://github.com/vector-im/riotX-android/"
property "sonar.links.ci", "https://buildkite.com/matrix-dot-org/riotx-android"
property "sonar.links.scm", "https://github.com/vector-im/riotX-android/"
property "sonar.links.issue", "https://github.com/vector-im/riotX-android/issues"
property "sonar.links.homepage", "https://github.com/vector-im/element-android/"
property "sonar.links.ci", "https://buildkite.com/matrix-dot-org/element-android"
property "sonar.links.scm", "https://github.com/vector-im/element-android/"
property "sonar.links.issue", "https://github.com/vector-im/element-android/issues"
property "sonar.organization", "new_vector_ltd_organization"
property "sonar.login", project.hasProperty("SONAR_LOGIN") ? SONAR_LOGIN : "invalid"
}
@ -100,11 +100,18 @@ project(":vector") {
properties {
property "sonar.sources", project(":vector").android.sourceSets.main.java.srcDirs
// exclude source code from analyses separated by a colon (:)
// property "sonar.exclusions", "**/*.*"
// Exclude Java source
property "sonar.exclusions", "**/BugReporterMultipartBody.java"
}
}
}
project(":diff-match-patch") {
sonarqube {
skipProject = true
}
}
//project(":matrix-sdk-android") {
// sonarqube {
// properties {

285
docs/add_threePids.md Normal file
View file

@ -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.

View file

@ -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
{

View file

@ -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
{

View file

@ -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<List<ThreePid>> {
return session.getPendingThreePidsLive().asObservable()
.startWithCallable { session.getPendingThreePids() }
}
fun createRoom(roomParams: CreateRoomParams): Single<String> = singleBuilder {
session.createRoom(roomParams, it)
}

View file

@ -112,18 +112,18 @@ dependencies {
def moshi_version = '1.8.0'
def lifecycle_version = '2.2.0'
def arch_version = '2.1.0'
def coroutines_version = "1.3.2"
def coroutines_version = "1.3.8"
def markwon_version = '3.1.0'
def daggerVersion = '2.25.4'
def work_version = '2.3.3'
def work_version = '2.4.0'
def retrofit_version = '2.6.2'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
implementation "androidx.appcompat:appcompat:1.1.0"
implementation "androidx.core:core-ktx:1.3.0"
implementation "androidx.appcompat:appcompat:1.2.0"
implementation "androidx.core:core-ktx:1.3.1"
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
@ -131,8 +131,6 @@ dependencies {
// Network
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version"
implementation "com.squareup.retrofit2:converter-scalars:$retrofit_version"
implementation(platform("com.squareup.okhttp3:okhttp-bom:4.8.1"))
implementation 'com.squareup.okhttp3:okhttp'
@ -146,7 +144,6 @@ dependencies {
// Image
implementation 'androidx.exifinterface:exifinterface:1.3.0-alpha01'
implementation 'id.zelory:compressor:3.0.0'
// Database
implementation 'com.github.Zhuinden:realm-monarchy:0.5.1'

View file

@ -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)
}
}
}

View file

@ -19,17 +19,17 @@ package org.matrix.android.sdk.internal.crypto
import android.os.MemoryFile
import android.util.Base64
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileKey
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNotNull
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import java.io.ByteArrayInputStream
import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileKey
import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt
import java.io.ByteArrayOutputStream
import java.io.InputStream
/**
@ -41,29 +41,26 @@ import java.io.InputStream
class AttachmentEncryptionTest {
private fun checkDecryption(input: String, encryptedFileInfo: EncryptedFileInfo): String {
val `in` = Base64.decode(input, Base64.DEFAULT)
val inputAsByteArray = Base64.decode(input, Base64.DEFAULT)
val inputStream: InputStream
inputStream = if (`in`.isEmpty()) {
ByteArrayInputStream(`in`)
inputStream = if (inputAsByteArray.isEmpty()) {
inputAsByteArray.inputStream()
} else {
val memoryFile = MemoryFile("file" + System.currentTimeMillis(), `in`.size)
memoryFile.outputStream.write(`in`)
val memoryFile = MemoryFile("file" + System.currentTimeMillis(), inputAsByteArray.size)
memoryFile.outputStream.write(inputAsByteArray)
memoryFile.inputStream
}
val decryptedStream = MXEncryptedAttachments.decryptAttachment(inputStream, encryptedFileInfo)
val decryptedStream = ByteArrayOutputStream()
val result = MXEncryptedAttachments.decryptAttachment(inputStream, encryptedFileInfo.toElementToDecrypt()!!, decryptedStream)
assertNotNull(decryptedStream)
assert(result)
val buffer = ByteArray(100)
val toByteArray = decryptedStream.toByteArray()
val len = decryptedStream!!.read(buffer)
decryptedStream.close()
return Base64.encodeToString(buffer, 0, len, Base64.DEFAULT).replace("\n".toRegex(), "").replace("=".toRegex(), "")
return Base64.encodeToString(toByteArray, 0, toByteArray.size, Base64.DEFAULT).replace("\n".toRegex(), "").replace("=".toRegex(), "")
}
@Test
@ -74,7 +71,7 @@ class AttachmentEncryptionTest {
key = EncryptedFileKey(
alg = "A256CTR",
k = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
key_ops = listOf("encrypt", "decrypt"),
keyOps = listOf("encrypt", "decrypt"),
kty = "oct",
ext = true
),
@ -93,7 +90,7 @@ class AttachmentEncryptionTest {
key = EncryptedFileKey(
alg = "A256CTR",
k = "__________________________________________8",
key_ops = listOf("encrypt", "decrypt"),
keyOps = listOf("encrypt", "decrypt"),
kty = "oct",
ext = true
),
@ -112,7 +109,7 @@ class AttachmentEncryptionTest {
key = EncryptedFileKey(
alg = "A256CTR",
k = "__________________________________________8",
key_ops = listOf("encrypt", "decrypt"),
keyOps = listOf("encrypt", "decrypt"),
kty = "oct",
ext = true
),
@ -133,7 +130,7 @@ class AttachmentEncryptionTest {
key = EncryptedFileKey(
alg = "A256CTR",
k = "__________________________________________8",
key_ops = listOf("encrypt", "decrypt"),
keyOps = listOf("encrypt", "decrypt"),
kty = "oct",
ext = true
),

View file

@ -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", "<em>italic</em>")
// Element Web format
"_italic_".let { markdownParser.parse(it).expect(it, "<em>italic</em>") }
}
/**
* 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, "<ul><li>item1</li></ul>") }
"- item1\n- item2".let { markdownParser.parse(it).expect(it, "<ul><li>item1</li><li>item2</li></ul>") }
"- item1".let { markdownParser.parse(it).expect(it, "<ul>\n<li>item1</li>\n</ul>") }
"- item1\n- item2".let { markdownParser.parse(it).expect(it, "<ul>\n<li>item1</li>\n<li>item2</li>\n</ul>") }
}
@Test
fun parseOrderedList() {
"1. item1".let { markdownParser.parse(it).expect(it, "<ol><li>item1</li></ol>") }
"1. item1\n2. item2".let { markdownParser.parse(it).expect(it, "<ol><li>item1</li><li>item2</li></ol>") }
"1. item1".let { markdownParser.parse(it).expect(it, "<ol>\n<li>item1</li>\n</ol>") }
"1. item1\n2. item2".let { markdownParser.parse(it).expect(it, "<ol>\n<li>item1</li>\n<li>item2</li>\n</ol>") }
}
@Test
fun parseHorizontalLine() {
"---".let { markdownParser.parse(it) }.expect("***", "<hr />")
"---".let { markdownParser.parse(it).expect(it, "<hr />") }
}
@Test
fun parseH2AndContent() {
"a\n---\nb".let { markdownParser.parse(it) }.expect("a\nb", "<h2>a</h2><p>b</p>")
"a\n---\nb".let { markdownParser.parse(it).expect(it, "<h2>a</h2>\n<p>b</p>") }
}
@Test
fun parseQuote() {
"> quoted".let { markdownParser.parse(it) }.expect("«quoted»", "<blockquote><p>quoted</p></blockquote>")
"> quoted".let { markdownParser.parse(it).expect(it, "<blockquote>\n<p>quoted</p>\n</blockquote>") }
}
@Test
fun parseQuote_not_passing() {
"> quoted\nline2".let { markdownParser.parse(it) }.expect("«quoted\nline2»", "<blockquote><p>quoted<br/>line2</p></blockquote>")
"> quoted\nline2".let { markdownParser.parse(it).expect(it, "<blockquote><p>quoted<br />line2</p></blockquote>") }
}
@Test
fun parseBoldItalic() {
"*italic* **bold**".let { markdownParser.parse(it) }.expect("italic bold", "<em>italic</em> <strong>bold</strong>")
"**bold** *italic*".let { markdownParser.parse(it) }.expect("bold italic", "<strong>bold</strong> <em>italic</em>")
"*italic* **bold**".let { markdownParser.parse(it).expect(it, "<em>italic</em> <strong>bold</strong>") }
"**bold** *italic*".let { markdownParser.parse(it).expect(it, "<strong>bold</strong> <em>italic</em>") }
}
@Test
fun parseHead() {
"# head1".let { markdownParser.parse(it) }.expect("head1", "<h1>head1</h1>")
"## head2".let { markdownParser.parse(it) }.expect("head2", "<h2>head2</h2>")
"### head3".let { markdownParser.parse(it) }.expect("head3", "<h3>head3</h3>")
"#### head4".let { markdownParser.parse(it) }.expect("head4", "<h4>head4</h4>")
"##### head5".let { markdownParser.parse(it) }.expect("head5", "<h5>head5</h5>")
"###### head6".let { markdownParser.parse(it) }.expect("head6", "<h6>head6</h6>")
"# head1".let { markdownParser.parse(it).expect(it, "<h1>head1</h1>") }
"## head2".let { markdownParser.parse(it).expect(it, "<h2>head2</h2>") }
"### head3".let { markdownParser.parse(it).expect(it, "<h3>head3</h3>") }
"#### head4".let { markdownParser.parse(it).expect(it, "<h4>head4</h4>") }
"##### head5".let { markdownParser.parse(it).expect(it, "<h5>head5</h5>") }
"###### head6".let { markdownParser.parse(it).expect(it, "<h6>head6</h6>") }
}
@Test
fun parseHeads() {
"# head1\n# head2".let { markdownParser.parse(it) }.expect("head1\nhead2", "<h1>head1</h1><h1>head2</h1>")
"# head1\n# head2".let { markdownParser.parse(it).expect(it, "<h1>head1</h1>\n<h1>head2</h1>") }
}
@Test
fun parseBoldNewLines_not_passing() {
"**bold**\nline2".let { markdownParser.parse(it) }.expect("bold\nline2", "<strong>bold</strong><br />line2")
"**bold**\nline2".let { markdownParser.parse(it).expect(it, "<strong>bold</strong><br />line2") }
}
@Test
fun parseLinks() {
"[link](target)".let { markdownParser.parse(it) }.expect(""""link" (target)""", """<a href="target">link</a>""")
"[link](target)".let { markdownParser.parse(it).expect(it, """<a href="target">link</a>""") }
}
@Test
fun parseParagraph() {
"# head\ncontent".let { markdownParser.parse(it) }.expect("head\ncontent", "<h1>head</h1><p>content</p>")
"# head\ncontent".let { markdownParser.parse(it).expect(it, "<h1>head</h1>\n<p>content</p>") }
}
private fun testIdentity(text: String) {
@ -217,60 +263,94 @@ 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",
.let {
markdownParser.parse(it)
.expect(expectedText = it,
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag>")
}
// 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",
.let {
markdownParser.parse(it)
.expect(expectedText = it,
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag> and <$htmlExpectedTag>$name bis</$htmlExpectedTag>")
}
val textBefore = "a"
val textAfter = "b"
// With sticked text before
"$textBefore$markdownPattern$name$markdownPattern"
.let { markdownParser.parse(it) }
.expect(expectedText = "$textBefore$plainTextPrefix$name$plainTextSuffix",
.let {
markdownParser.parse(it)
.expect(expectedText = it,
expectedFormattedText = "$textBefore<$htmlExpectedTag>$name</$htmlExpectedTag>")
}
// With text before and space
"$textBefore $markdownPattern$name$markdownPattern"
.let { markdownParser.parse(it) }
.expect(expectedText = "$textBefore $plainTextPrefix$name$plainTextSuffix",
.let {
markdownParser.parse(it)
.expect(expectedText = it,
expectedFormattedText = "$textBefore <$htmlExpectedTag>$name</$htmlExpectedTag>")
}
// With sticked text after
"$markdownPattern$name$markdownPattern$textAfter"
.let { markdownParser.parse(it) }
.expect(expectedText = "$plainTextPrefix$name$plainTextSuffix$textAfter",
.let {
markdownParser.parse(it)
.expect(expectedText = it,
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag>$textAfter")
}
// With space and text after
"$markdownPattern$name$markdownPattern $textAfter"
.let { markdownParser.parse(it) }
.expect(expectedText = "$plainTextPrefix$name$plainTextSuffix $textAfter",
.let {
markdownParser.parse(it)
.expect(expectedText = it,
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag> $textAfter")
}
// With sticked text before and text after
"$textBefore$markdownPattern$name$markdownPattern$textAfter"
.let { markdownParser.parse(it) }
.expect(expectedText = "$textBefore$plainTextPrefix$name$plainTextSuffix$textAfter",
.let {
markdownParser.parse(it)
.expect(expectedText = it,
expectedFormattedText = "a<$htmlExpectedTag>$name</$htmlExpectedTag>$textAfter")
}
// With text before and after, with spaces
"$textBefore $markdownPattern$name$markdownPattern $textAfter"
.let { markdownParser.parse(it) }
.expect(expectedText = "$textBefore $plainTextPrefix$name$plainTextSuffix $textAfter",
.let {
markdownParser.parse(it)
.expect(expectedText = it,
expectedFormattedText = "$textBefore <$htmlExpectedTag>$name</$htmlExpectedTag> $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<br />$name</$htmlExpectedTag>")
}
// With new line between two blocks
"$markdownPattern$name$markdownPattern\n$markdownPattern$name$markdownPattern"
.let {
markdownParser.parse(it)
.expect(expectedText = it,
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag><$htmlExpectedTag>$name</$htmlExpectedTag>")
}
}
private fun TextContent.expect(expectedText: String, expectedFormattedText: String?) {
assertEquals("TextContent are not identical", TextContent(expectedText, expectedFormattedText), this)

View file

@ -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)
}
}
}

View file

@ -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.

View file

@ -24,20 +24,23 @@ sealed class Action {
object DoNotNotify : Action()
data class Sound(val sound: String = ACTION_OBJECT_VALUE_VALUE_DEFAULT) : Action()
data class Highlight(val highlight: Boolean) : Action()
}
private const val ACTION_NOTIFY = "notify"
private const val ACTION_DONT_NOTIFY = "dont_notify"
private const val ACTION_COALESCE = "coalesce"
companion object {
const val ACTION_NOTIFY = "notify"
const val ACTION_DONT_NOTIFY = "dont_notify"
const val ACTION_COALESCE = "coalesce"
// Ref: https://matrix.org/docs/spec/client_server/latest#tweaks
private const val ACTION_OBJECT_SET_TWEAK_KEY = "set_tweak"
const val ACTION_OBJECT_SET_TWEAK_KEY = "set_tweak"
private const val ACTION_OBJECT_SET_TWEAK_VALUE_SOUND = "sound"
private const val ACTION_OBJECT_SET_TWEAK_VALUE_HIGHLIGHT = "highlight"
const val ACTION_OBJECT_SET_TWEAK_VALUE_SOUND = "sound"
const val ACTION_OBJECT_SET_TWEAK_VALUE_HIGHLIGHT = "highlight"
private const val ACTION_OBJECT_VALUE_KEY = "value"
private const val ACTION_OBJECT_VALUE_VALUE_DEFAULT = "default"
const val ACTION_OBJECT_VALUE_KEY = "value"
const val ACTION_OBJECT_VALUE_VALUE_DEFAULT = "default"
const val ACTION_OBJECT_VALUE_VALUE_RING = "ring"
}
}
/**
* Ref: https://matrix.org/docs/spec/client_server/latest#actions
@ -69,18 +72,18 @@ private const val ACTION_OBJECT_VALUE_VALUE_DEFAULT = "default"
fun List<Action>.toJson(): List<Any> {
return map { action ->
when (action) {
is Action.Notify -> ACTION_NOTIFY
is Action.DoNotNotify -> ACTION_DONT_NOTIFY
is Action.Notify -> Action.ACTION_NOTIFY
is Action.DoNotNotify -> Action.ACTION_DONT_NOTIFY
is Action.Sound -> {
mapOf(
ACTION_OBJECT_SET_TWEAK_KEY to ACTION_OBJECT_SET_TWEAK_VALUE_SOUND,
ACTION_OBJECT_VALUE_KEY to action.sound
Action.ACTION_OBJECT_SET_TWEAK_KEY to Action.ACTION_OBJECT_SET_TWEAK_VALUE_SOUND,
Action.ACTION_OBJECT_VALUE_KEY to action.sound
)
}
is Action.Highlight -> {
mapOf(
ACTION_OBJECT_SET_TWEAK_KEY to ACTION_OBJECT_SET_TWEAK_VALUE_HIGHLIGHT,
ACTION_OBJECT_VALUE_KEY to action.highlight
Action.ACTION_OBJECT_SET_TWEAK_KEY to Action.ACTION_OBJECT_SET_TWEAK_VALUE_HIGHLIGHT,
Action.ACTION_OBJECT_VALUE_KEY to action.highlight
)
}
}
@ -92,26 +95,26 @@ fun PushRule.getActions(): List<Action> {
actions.forEach { actionStrOrObj ->
when (actionStrOrObj) {
ACTION_NOTIFY -> Action.Notify
ACTION_DONT_NOTIFY -> Action.DoNotNotify
Action.ACTION_NOTIFY -> Action.Notify
Action.ACTION_DONT_NOTIFY -> Action.DoNotNotify
is Map<*, *> -> {
when (actionStrOrObj[ACTION_OBJECT_SET_TWEAK_KEY]) {
ACTION_OBJECT_SET_TWEAK_VALUE_SOUND -> {
(actionStrOrObj[ACTION_OBJECT_VALUE_KEY] as? String)?.let { stringValue ->
when (actionStrOrObj[Action.ACTION_OBJECT_SET_TWEAK_KEY]) {
Action.ACTION_OBJECT_SET_TWEAK_VALUE_SOUND -> {
(actionStrOrObj[Action.ACTION_OBJECT_VALUE_KEY] as? String)?.let { stringValue ->
Action.Sound(stringValue)
}
// When the value is not there, default sound (not specified by the spec)
?: Action.Sound(ACTION_OBJECT_VALUE_VALUE_DEFAULT)
?: Action.Sound(Action.ACTION_OBJECT_VALUE_VALUE_DEFAULT)
}
ACTION_OBJECT_SET_TWEAK_VALUE_HIGHLIGHT -> {
(actionStrOrObj[ACTION_OBJECT_VALUE_KEY] as? Boolean)?.let { boolValue ->
Action.ACTION_OBJECT_SET_TWEAK_VALUE_HIGHLIGHT -> {
(actionStrOrObj[Action.ACTION_OBJECT_VALUE_KEY] as? Boolean)?.let { boolValue ->
Action.Highlight(boolValue)
}
// When the value is not there, default is true, says the spec
?: Action.Highlight(true)
}
else -> {
Timber.w("Unsupported set_tweak value ${actionStrOrObj[ACTION_OBJECT_SET_TWEAK_KEY]}")
Timber.w("Unsupported set_tweak value ${actionStrOrObj[Action.ACTION_OBJECT_SET_TWEAK_KEY]}")
null
}
}

View file

@ -18,32 +18,8 @@ package org.matrix.android.sdk.api.pushrules
import org.matrix.android.sdk.api.session.events.model.Event
abstract class Condition(val kind: Kind) {
interface Condition {
fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean
enum class Kind(val value: String) {
EventMatch("event_match"),
ContainsDisplayName("contains_display_name"),
RoomMemberCount("room_member_count"),
SenderNotificationPermission("sender_notification_permission"),
Unrecognised("");
companion object {
fun fromString(value: String): Kind {
return when (value) {
"event_match" -> EventMatch
"contains_display_name" -> ContainsDisplayName
"room_member_count" -> RoomMemberCount
"sender_notification_permission" -> SenderNotificationPermission
else -> Unrecognised
}
}
}
}
abstract fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean
open fun technicalDescription(): String {
return "Kind: $kind"
}
fun technicalDescription(): String
}

View file

@ -20,17 +20,15 @@ import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import timber.log.Timber
import org.matrix.android.sdk.internal.util.caseInsensitiveFind
class ContainsDisplayNameCondition : Condition(Kind.ContainsDisplayName) {
class ContainsDisplayNameCondition : Condition {
override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean {
return conditionResolver.resolveContainsDisplayNameCondition(event, this)
}
override fun technicalDescription(): String {
return "User is mentioned"
}
override fun technicalDescription() = "User is mentioned"
fun isSatisfied(event: Event, displayName: String): Boolean {
val message = when (event.type) {
@ -45,31 +43,6 @@ class ContainsDisplayNameCondition : Condition(Kind.ContainsDisplayName) {
else -> null
} ?: return false
return caseInsensitiveFind(displayName, message.body)
}
companion object {
/**
* Returns whether a string contains an occurrence of another, as a standalone word, regardless of case.
*
* @param subString the string to search for
* @param longString the string to search in
* @return whether a match was found
*/
fun caseInsensitiveFind(subString: String, longString: String): Boolean {
// add sanity checks
if (subString.isEmpty() || longString.isEmpty()) {
return false
}
try {
val regex = Regex("(\\W|^)" + Regex.escape(subString) + "(\\W|$)", RegexOption.IGNORE_CASE)
return regex.containsMatchIn(longString)
} catch (e: Exception) {
Timber.e(e, "## caseInsensitiveFind() : failed")
}
return false
}
return message.body.caseInsensitiveFind(displayName)
}
}

View file

@ -18,6 +18,9 @@ package org.matrix.android.sdk.api.pushrules
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.internal.di.MoshiProvider
import org.matrix.android.sdk.internal.util.caseInsensitiveFind
import org.matrix.android.sdk.internal.util.hasSpecialGlobChar
import org.matrix.android.sdk.internal.util.simpleGlobToRegExp
import timber.log.Timber
class EventMatchCondition(
@ -29,16 +32,18 @@ class EventMatchCondition(
* The glob-style pattern to match against. Patterns with no special glob characters should
* be treated as having asterisks prepended and appended when testing the condition.
*/
val pattern: String
) : Condition(Kind.EventMatch) {
val pattern: String,
/**
* true to match only words. In this case pattern will not be considered as a glob
*/
val wordsOnly: Boolean
) : Condition {
override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean {
return conditionResolver.resolveEventMatchCondition(event, this)
}
override fun technicalDescription(): String {
return "'$key' Matches '$pattern'"
}
override fun technicalDescription() = "'$key' matches '$pattern', words only '$wordsOnly'"
fun isSatisfied(event: Event): Boolean {
// TODO encrypted events?
@ -48,14 +53,18 @@ class EventMatchCondition(
// Patterns with no special glob characters should be treated as having asterisks prepended
// and appended when testing the condition.
try {
val modPattern = if (hasSpecialGlobChar(pattern)) simpleGlobToRegExp(pattern) else simpleGlobToRegExp("*$pattern*")
return try {
if (wordsOnly) {
value.caseInsensitiveFind(pattern)
} else {
val modPattern = if (pattern.hasSpecialGlobChar()) pattern.simpleGlobToRegExp() else "*$pattern*".simpleGlobToRegExp()
val regex = Regex(modPattern, RegexOption.DOT_MATCHES_ALL)
return regex.containsMatchIn(value)
regex.containsMatchIn(value)
}
} catch (e: Throwable) {
// e.g PatternSyntaxException
Timber.e(e, "Failed to evaluate push condition")
return false
false
}
}
@ -78,27 +87,4 @@ class EventMatchCondition(
}
return null
}
companion object {
private fun hasSpecialGlobChar(glob: String): Boolean {
return glob.contains("*") || glob.contains("?")
}
// Very simple glob to regexp converter
private fun simpleGlobToRegExp(glob: String): String {
var out = "" // "^"
for (element in glob) {
when (element) {
'*' -> out += ".*"
'?' -> out += '.'.toString()
'.' -> out += "\\."
'\\' -> out += "\\\\"
else -> out += element
}
}
out += "" // '$'.toString()
return out
}
}
}

View file

@ -0,0 +1,39 @@
/*
* Copyright (c) 2020 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.pushrules
enum class Kind(val value: String) {
EventMatch("event_match"),
ContainsDisplayName("contains_display_name"),
RoomMemberCount("room_member_count"),
SenderNotificationPermission("sender_notification_permission"),
Unrecognised("");
companion object {
fun fromString(value: String): Kind {
return when (value) {
"event_match" -> EventMatch
"contains_display_name" -> ContainsDisplayName
"room_member_count" -> RoomMemberCount
"sender_notification_permission" -> SenderNotificationPermission
else -> Unrecognised
}
}
}
}

View file

@ -29,15 +29,13 @@ class RoomMemberCountCondition(
* If no prefix is present, this parameter defaults to ==.
*/
val iz: String
) : Condition(Kind.RoomMemberCount) {
) : Condition {
override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean {
return conditionResolver.resolveRoomMemberCountCondition(event, this)
}
override fun technicalDescription(): String {
return "Room member count is $iz"
}
override fun technicalDescription() = "Room member count is $iz"
internal fun isSatisfied(event: Event, roomGetter: RoomGetter): Boolean {
// sanity checks

View file

@ -45,4 +45,6 @@ object RuleIds {
// Not documented
const val RULE_ID_FALLBACK = ".m.rule.fallback"
const val RULE_ID_REACTION = ".m.rule.reaction"
}

View file

@ -28,15 +28,13 @@ class SenderNotificationPermissionCondition(
* type from the notifications object in the power level event content.
*/
val key: String
) : Condition(Kind.SenderNotificationPermission) {
) : Condition {
override fun isSatisfied(event: Event, conditionResolver: ConditionResolver): Boolean {
return conditionResolver.resolveSenderNotificationPermissionCondition(event, this)
}
override fun technicalDescription(): String {
return "User power level <$key>"
}
override fun technicalDescription() = "User power level <$key>"
fun isSatisfied(event: Event, powerLevels: PowerLevelsContent): Boolean {
val powerLevelsHelper = PowerLevelsHelper(powerLevels)

View file

@ -21,7 +21,9 @@ import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.pushrules.Condition
import org.matrix.android.sdk.api.pushrules.ContainsDisplayNameCondition
import org.matrix.android.sdk.api.pushrules.EventMatchCondition
import org.matrix.android.sdk.api.pushrules.Kind
import org.matrix.android.sdk.api.pushrules.RoomMemberCountCondition
import org.matrix.android.sdk.api.pushrules.RuleIds
import org.matrix.android.sdk.api.pushrules.SenderNotificationPermissionCondition
import timber.log.Timber
@ -58,20 +60,20 @@ data class PushCondition(
val iz: String? = null
) {
fun asExecutableCondition(): Condition? {
return when (Condition.Kind.fromString(kind)) {
Condition.Kind.EventMatch -> {
fun asExecutableCondition(rule: PushRule): Condition? {
return when (Kind.fromString(kind)) {
Kind.EventMatch -> {
if (key != null && pattern != null) {
EventMatchCondition(key, pattern)
EventMatchCondition(key, pattern, rule.ruleId == RuleIds.RULE_ID_CONTAIN_USER_NAME)
} else {
Timber.e("Malformed Event match condition")
null
}
}
Condition.Kind.ContainsDisplayName -> {
Kind.ContainsDisplayName -> {
ContainsDisplayNameCondition()
}
Condition.Kind.RoomMemberCount -> {
Kind.RoomMemberCount -> {
if (iz.isNullOrEmpty()) {
Timber.e("Malformed ROOM_MEMBER_COUNT condition")
null
@ -79,7 +81,7 @@ data class PushCondition(
RoomMemberCountCondition(iz)
}
}
Condition.Kind.SenderNotificationPermission -> {
Kind.SenderNotificationPermission -> {
if (key == null) {
Timber.e("Malformed Sender Notification Permission condition")
null
@ -87,7 +89,7 @@ data class PushCondition(
SenderNotificationPermissionCondition(key)
}
}
Condition.Kind.Unrecognised -> {
Kind.Unrecognised -> {
Timber.e("Unknown kind $kind")
null
}

View file

@ -63,7 +63,7 @@ data class PushRule(
* Add the default notification sound.
*/
fun setNotificationSound(): PushRule {
return setNotificationSound(ACTION_VALUE_DEFAULT)
return setNotificationSound(Action.ACTION_OBJECT_VALUE_VALUE_DEFAULT)
}
fun getNotificationSound(): String? {
@ -109,13 +109,13 @@ data class PushRule(
fun setNotify(notify: Boolean): PushRule {
val mutableActions = actions.toMutableList()
mutableActions.remove(ACTION_DONT_NOTIFY)
mutableActions.remove(ACTION_NOTIFY)
mutableActions.remove(Action.ACTION_DONT_NOTIFY)
mutableActions.remove(Action.ACTION_NOTIFY)
if (notify) {
mutableActions.add(ACTION_NOTIFY)
mutableActions.add(Action.ACTION_NOTIFY)
} else {
mutableActions.add(ACTION_DONT_NOTIFY)
mutableActions.add(Action.ACTION_DONT_NOTIFY)
}
return copy(actions = mutableActions)
@ -126,51 +126,12 @@ data class PushRule(
*
* @return true if the rule should play sound
*/
fun shouldNotify() = actions.contains(ACTION_NOTIFY)
fun shouldNotify() = actions.contains(Action.ACTION_NOTIFY)
/**
* Return true if the rule should not highlight the event.
*
* @return true if the rule should not play sound
*/
fun shouldNotNotify() = actions.contains(ACTION_DONT_NOTIFY)
companion object {
/* ==========================================================================================
* Rule id
* ========================================================================================== */
const val RULE_ID_DISABLE_ALL = ".m.rule.master"
const val RULE_ID_CONTAIN_USER_NAME = ".m.rule.contains_user_name"
const val RULE_ID_CONTAIN_DISPLAY_NAME = ".m.rule.contains_display_name"
const val RULE_ID_ONE_TO_ONE_ROOM = ".m.rule.room_one_to_one"
const val RULE_ID_INVITE_ME = ".m.rule.invite_for_me"
const val RULE_ID_PEOPLE_JOIN_LEAVE = ".m.rule.member_event"
const val RULE_ID_CALL = ".m.rule.call"
const val RULE_ID_SUPPRESS_BOTS_NOTIFICATIONS = ".m.rule.suppress_notices"
const val RULE_ID_ALL_OTHER_MESSAGES_ROOMS = ".m.rule.message"
const val RULE_ID_AT_ROOMS = ".m.rule.roomnotif"
const val RULE_ID_TOMBSTONE = ".m.rule.tombstone"
const val RULE_ID_E2E_ONE_TO_ONE_ROOM = ".m.rule.encrypted_room_one_to_one"
const val RULE_ID_E2E_GROUP = ".m.rule.encrypted"
const val RULE_ID_REACTION = ".m.rule.reaction"
const val RULE_ID_FALLBACK = ".m.rule.fallback"
/* ==========================================================================================
* Actions
* ========================================================================================== */
const val ACTION_NOTIFY = "notify"
const val ACTION_DONT_NOTIFY = "dont_notify"
const val ACTION_COALESCE = "coalesce"
const val ACTION_SET_TWEAK_SOUND_VALUE = "sound"
const val ACTION_SET_TWEAK_HIGHLIGHT_VALUE = "highlight"
const val ACTION_PARAMETER_SET_TWEAK = "set_tweak"
const val ACTION_PARAMETER_VALUE = "value"
const val ACTION_VALUE_DEFAULT = "default"
const val ACTION_VALUE_RING = "ring"
}
fun shouldNotNotify() = actions.contains(Action.ACTION_DONT_NOTIFY)
}

View file

@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.pushrules.rest
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.pushrules.RuleIds
import org.matrix.android.sdk.api.pushrules.RuleSetKey
/**
@ -51,7 +52,7 @@ data class RuleSet(
var result: PushRuleAndKind? = null
// sanity check
if (null != ruleId) {
if (PushRule.RULE_ID_CONTAIN_USER_NAME == ruleId) {
if (RuleIds.RULE_ID_CONTAIN_USER_NAME == ruleId) {
result = findRule(content, ruleId)?.let { PushRuleAndKind(it, RuleSetKey.CONTENT) }
} else {
// assume that the ruleId is unique.

View file

@ -34,4 +34,6 @@ interface CallSignalingService {
fun removeCallListener(listener: CallsListener)
fun getCallWithId(callId: String) : MxCall?
fun isThereAnyActiveCall(): Boolean
}

View file

@ -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()

View file

@ -239,6 +239,14 @@ fun Event.isVideoMessage(): Boolean {
}
}
fun Event.isAudioMessage(): Boolean {
return getClearType() == EventType.MESSAGE
&& when (getClearContent()?.toModel<MessageContent>()?.msgType) {
MessageType.MSGTYPE_AUDIO -> true
else -> false
}
}
fun Event.isFileMessage(): Boolean {
return getClearType() == EventType.MESSAGE
&& when (getClearContent()?.toModel<MessageContent>()?.msgType) {
@ -246,6 +254,16 @@ fun Event.isFileMessage(): Boolean {
else -> false
}
}
fun Event.isAttachmentMessage(): Boolean {
return getClearType() == EventType.MESSAGE
&& when (getClearContent()?.toModel<MessageContent>()?.msgType) {
MessageType.MSGTYPE_IMAGE,
MessageType.MSGTYPE_AUDIO,
MessageType.MSGTYPE_VIDEO,
MessageType.MSGTYPE_FILE -> true
else -> false
}
}
fun Event.getRelationContent(): RelationDefaultContent? {
return if (isEncrypted()) {

View file

@ -25,7 +25,12 @@ import android.net.Uri
*/
sealed class PermalinkData {
data class RoomLink(val roomIdOrAlias: String, val isRoomAlias: Boolean, val eventId: String?) : PermalinkData()
data class RoomLink(
val roomIdOrAlias: String,
val isRoomAlias: Boolean,
val eventId: String?,
val viaParameters: List<String>
) : PermalinkData()
data class UserLink(val userId: String) : PermalinkData()

View file

@ -18,6 +18,7 @@
package org.matrix.android.sdk.api.session.permalinks
import android.net.Uri
import android.net.UrlQuerySanitizer
import org.matrix.android.sdk.api.MatrixPatterns
/**
@ -40,14 +41,13 @@ object PermalinkParser {
if (!uri.toString().startsWith(PermalinkService.MATRIX_TO_URL_BASE)) {
return PermalinkData.FallbackLink(uri)
}
val fragment = uri.fragment
if (fragment.isNullOrEmpty()) {
return PermalinkData.FallbackLink(uri)
}
val indexOfQuery = fragment.indexOf("?")
val safeFragment = if (indexOfQuery != -1) fragment.substring(0, indexOfQuery) else fragment
val viaQueryParameters = fragment.getViaParameters()
// we are limiting to 2 params
val params = safeFragment
@ -65,17 +65,29 @@ object PermalinkParser {
PermalinkData.RoomLink(
roomIdOrAlias = identifier,
isRoomAlias = false,
eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) }
eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) },
viaParameters = viaQueryParameters
)
}
MatrixPatterns.isRoomAlias(identifier) -> {
PermalinkData.RoomLink(
roomIdOrAlias = identifier,
isRoomAlias = true,
eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) }
eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) },
viaParameters = viaQueryParameters
)
}
else -> PermalinkData.FallbackLink(uri)
}
}
private fun String.getViaParameters(): List<String> {
return UrlQuerySanitizer(this)
.parameterList
.filter {
it.mParameter == "via"
}.map {
it.mValue
}
}
}

View file

@ -83,4 +83,43 @@ interface ProfileService {
* @param refreshData set to true to fetch data from the homeserver
*/
fun getThreePidsLive(refreshData: Boolean): LiveData<List<ThreePid>>
/**
* Get the pending 3Pids, i.e. ThreePids that have requested a token, but not yet validated by the user.
*/
fun getPendingThreePids(): List<ThreePid>
/**
* Get the pending 3Pids Live
*/
fun getPendingThreePidsLive(): LiveData<List<ThreePid>>
/**
* 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<Unit>): Cancelable
/**
* Validate a code received by text message
*/
fun submitSmsCode(threePid: ThreePid.Msisdn, code: String, matrixCallback: MatrixCallback<Unit>): 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<Unit>): Cancelable
/**
* Cancel adding a threepid. It will remove locally stored data about this ThreePid
*/
fun cancelAddingThreePid(threePid: ThreePid,
matrixCallback: MatrixCallback<Unit>): Cancelable
/**
* Remove a 3Pid from the Matrix account.
*/
fun deleteThreePid(threePid: ThreePid, matrixCallback: MatrixCallback<Unit>): Cancelable
}

View file

@ -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)
*/

View file

@ -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)
}

View file

@ -54,7 +54,7 @@ internal data class AddThreePidRegistrationParams(
* This parameter is ignored when the homeserver handles 3PID verification.
*/
@Json(name = "id_server")
val id_server: String? = null,
val idServer: String? = null,
/* ==========================================================================================
* For emails

View file

@ -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,

View file

@ -44,5 +44,6 @@ internal data class RegistrationParams(
// Temporary flag to notify the server that we support msisdn flow. Used to prevent old app
// versions to end up in fallback because the HS returns the msisdn flow which they don't support
val x_show_msisdn: Boolean? = null
@Json(name = "x_show_msisdn")
val xShowMsisdn: Boolean? = null
)

View file

@ -145,7 +145,7 @@ object MXMegolmExportEncryption {
*/
@Throws(Exception::class)
@JvmOverloads
fun encryptMegolmKeyFile(data: String, password: String, kdf_rounds: Int = DEFAULT_ITERATION_COUNT): ByteArray {
fun encryptMegolmKeyFile(data: String, password: String, kdfRounds: Int = DEFAULT_ITERATION_COUNT): ByteArray {
if (password.isEmpty()) {
throw Exception("Empty password is not supported")
}
@ -163,7 +163,7 @@ object MXMegolmExportEncryption {
// of a single bit of salt is a price we have to pay.
iv[9] = iv[9] and 0x7f
val deriveKey = deriveKeys(salt, kdf_rounds, password)
val deriveKey = deriveKeys(salt, kdfRounds, password)
val decryptCipher = Cipher.getInstance("AES/CTR/NoPadding")
@ -188,10 +188,10 @@ object MXMegolmExportEncryption {
System.arraycopy(iv, 0, resultBuffer, idx, iv.size)
idx += iv.size
resultBuffer[idx++] = (kdf_rounds shr 24 and 0xff).toByte()
resultBuffer[idx++] = (kdf_rounds shr 16 and 0xff).toByte()
resultBuffer[idx++] = (kdf_rounds shr 8 and 0xff).toByte()
resultBuffer[idx++] = (kdf_rounds and 0xff).toByte()
resultBuffer[idx++] = (kdfRounds shr 24 and 0xff).toByte()
resultBuffer[idx++] = (kdfRounds shr 16 and 0xff).toByte()
resultBuffer[idx++] = (kdfRounds shr 8 and 0xff).toByte()
resultBuffer[idx++] = (kdfRounds and 0xff).toByte()
System.arraycopy(cipherArray, 0, resultBuffer, idx, cipherArray.size)
idx += cipherArray.size
@ -320,26 +320,26 @@ object MXMegolmExportEncryption {
// 512 bits key length
val key = ByteArray(64)
val Uc = ByteArray(64)
val uc = ByteArray(64)
// U1 = PRF(Password, Salt || INT_32_BE(i))
prf.update(salt)
val int32BE = ByteArray(4) { 0.toByte() }
int32BE[3] = 1.toByte()
prf.update(int32BE)
prf.doFinal(Uc, 0)
prf.doFinal(uc, 0)
// copy to the key
System.arraycopy(Uc, 0, key, 0, Uc.size)
System.arraycopy(uc, 0, key, 0, uc.size)
for (index in 2..iterations) {
// Uc = PRF(Password, Uc-1)
prf.update(Uc)
prf.doFinal(Uc, 0)
prf.update(uc)
prf.doFinal(uc, 0)
// F(Password, Salt, c, i) = U1 ^ U2 ^ ... ^ Uc
for (byteIndex in Uc.indices) {
key[byteIndex] = key[byteIndex] xor Uc[byteIndex]
for (byteIndex in uc.indices) {
key[byteIndex] = key[byteIndex] xor uc[byteIndex]
}
}

View file

@ -102,7 +102,7 @@ internal class MXOlmDecryption(
String.format(MXCryptoError.BAD_RECIPIENT_REASON, olmPayloadContent.recipient))
}
val recipientKeys = olmPayloadContent.recipient_keys ?: run {
val recipientKeys = olmPayloadContent.recipientKeys ?: run {
Timber.e("## decryptEvent() : Olm event (id=${event.eventId}) contains no 'recipient_keys'" +
" property; cannot prevent unknown-key attack")
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_PROPERTY,
@ -129,10 +129,10 @@ internal class MXOlmDecryption(
String.format(MXCryptoError.FORWARDED_MESSAGE_REASON, olmPayloadContent.sender))
}
if (olmPayloadContent.room_id != event.roomId) {
Timber.e("## decryptEvent() : Event ${event.eventId}: original room ${olmPayloadContent.room_id} does not match reported room ${event.roomId}")
if (olmPayloadContent.roomId != event.roomId) {
Timber.e("## decryptEvent() : Event ${event.eventId}: original room ${olmPayloadContent.roomId} does not match reported room ${event.roomId}")
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ROOM,
String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.room_id))
String.format(MXCryptoError.BAD_ROOM_REASON, olmPayloadContent.roomId))
}
val keys = olmPayloadContent.keys ?: run {

View file

@ -20,10 +20,14 @@ package org.matrix.android.sdk.internal.crypto.attachments
import android.util.Base64
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileKey
import org.matrix.android.sdk.internal.util.base64ToBase64Url
import org.matrix.android.sdk.internal.util.base64ToUnpaddedBase64
import org.matrix.android.sdk.internal.util.base64UrlToBase64
import timber.log.Timber
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import java.security.MessageDigest
import java.security.SecureRandom
import javax.crypto.Cipher
@ -36,8 +40,121 @@ internal object MXEncryptedAttachments {
private const val SECRET_KEY_SPEC_ALGORITHM = "AES"
private const val MESSAGE_DIGEST_ALGORITHM = "SHA-256"
fun encrypt(clearStream: InputStream, mimetype: String?, outputFile: File, progress: ((current: Int, total: Int) -> Unit)): EncryptedFileInfo {
val t0 = System.currentTimeMillis()
val secureRandom = SecureRandom()
val initVectorBytes = ByteArray(16) { 0.toByte() }
val ivRandomPart = ByteArray(8)
secureRandom.nextBytes(ivRandomPart)
System.arraycopy(ivRandomPart, 0, initVectorBytes, 0, ivRandomPart.size)
val key = ByteArray(32)
secureRandom.nextBytes(key)
val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM)
outputFile.outputStream().use { outputStream ->
val encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM)
val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
val ivParameterSpec = IvParameterSpec(initVectorBytes)
encryptCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec)
val data = ByteArray(CRYPTO_BUFFER_SIZE)
var read: Int
var encodedBytes: ByteArray
clearStream.use { inputStream ->
val estimatedSize = inputStream.available()
progress.invoke(0, estimatedSize)
read = inputStream.read(data)
var totalRead = read
while (read != -1) {
progress.invoke(totalRead, estimatedSize)
encodedBytes = encryptCipher.update(data, 0, read)
messageDigest.update(encodedBytes, 0, encodedBytes.size)
outputStream.write(encodedBytes)
read = inputStream.read(data)
totalRead += read
}
}
// encrypt the latest chunk
encodedBytes = encryptCipher.doFinal()
messageDigest.update(encodedBytes, 0, encodedBytes.size)
outputStream.write(encodedBytes)
}
return EncryptedFileInfo(
url = null,
mimetype = mimetype,
key = EncryptedFileKey(
alg = "A256CTR",
ext = true,
keyOps = listOf("encrypt", "decrypt"),
kty = "oct",
k = base64ToBase64Url(Base64.encodeToString(key, Base64.DEFAULT))
),
iv = Base64.encodeToString(initVectorBytes, Base64.DEFAULT).replace("\n", "").replace("=", ""),
hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))),
v = "v2"
)
.also { Timber.v("Encrypt in ${System.currentTimeMillis() - t0}ms") }
}
// fun cipherInputStream(attachmentStream: InputStream, mimetype: String?): Pair<DigestInputStream, EncryptedFileInfo> {
// val secureRandom = SecureRandom()
//
// // generate a random iv key
// // Half of the IV is random, the lower order bits are zeroed
// // such that the counter never wraps.
// // See https://github.com/matrix-org/matrix-ios-kit/blob/3dc0d8e46b4deb6669ed44f72ad79be56471354c/MatrixKit/Models/Room/MXEncryptedAttachments.m#L75
// val initVectorBytes = ByteArray(16) { 0.toByte() }
//
// val ivRandomPart = ByteArray(8)
// secureRandom.nextBytes(ivRandomPart)
//
// System.arraycopy(ivRandomPart, 0, initVectorBytes, 0, ivRandomPart.size)
//
// val key = ByteArray(32)
// secureRandom.nextBytes(key)
//
// val encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM)
// val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
// val ivParameterSpec = IvParameterSpec(initVectorBytes)
// encryptCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec)
//
// val cipherInputStream = CipherInputStream(attachmentStream, encryptCipher)
//
// // Could it be possible to get the digest on the fly instead of
// val info = EncryptedFileInfo(
// url = null,
// mimetype = mimetype,
// key = EncryptedFileKey(
// alg = "A256CTR",
// ext = true,
// key_ops = listOf("encrypt", "decrypt"),
// kty = "oct",
// k = base64ToBase64Url(Base64.encodeToString(key, Base64.DEFAULT))
// ),
// iv = Base64.encodeToString(initVectorBytes, Base64.DEFAULT).replace("\n", "").replace("=", ""),
// //hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))),
// v = "v2"
// )
//
// val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM)
// return DigestInputStream(cipherInputStream, messageDigest) to info
// }
//
// fun updateInfoWithDigest(digestInputStream: DigestInputStream, info: EncryptedFileInfo): EncryptedFileInfo {
// return info.copy(
// hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(digestInputStream.messageDigest.digest(), Base64.DEFAULT)))
// )
// }
/***
* Encrypt an attachment stream.
* DO NOT USE for big files, it will load all in memory
* @param attachmentStream the attachment stream. Will be closed after this method call.
* @param mimetype the mime type
* @return the encryption file info
@ -60,14 +177,14 @@ internal object MXEncryptedAttachments {
val key = ByteArray(32)
secureRandom.nextBytes(key)
ByteArrayOutputStream().use { outputStream ->
val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM)
val byteArrayOutputStream = ByteArrayOutputStream()
byteArrayOutputStream.use { outputStream ->
val encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM)
val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
val ivParameterSpec = IvParameterSpec(initVectorBytes)
encryptCipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec)
val messageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM)
val data = ByteArray(CRYPTO_BUFFER_SIZE)
var read: Int
var encodedBytes: ByteArray
@ -86,6 +203,7 @@ internal object MXEncryptedAttachments {
encodedBytes = encryptCipher.doFinal()
messageDigest.update(encodedBytes, 0, encodedBytes.size)
outputStream.write(encodedBytes)
}
return EncryptionResult(
encryptedFileInfo = EncryptedFileInfo(
@ -94,7 +212,7 @@ internal object MXEncryptedAttachments {
key = EncryptedFileKey(
alg = "A256CTR",
ext = true,
key_ops = listOf("encrypt", "decrypt"),
keyOps = listOf("encrypt", "decrypt"),
kty = "oct",
k = base64ToBase64Url(Base64.encodeToString(key, Base64.DEFAULT))
),
@ -102,47 +220,28 @@ internal object MXEncryptedAttachments {
hashes = mapOf("sha256" to base64ToUnpaddedBase64(Base64.encodeToString(messageDigest.digest(), Base64.DEFAULT))),
v = "v2"
),
encryptedByteArray = outputStream.toByteArray()
encryptedByteArray = byteArrayOutputStream.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)
}
/**
* Decrypt an attachment
*
* @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)
@ -176,39 +275,16 @@ internal object MXEncryptedAttachments {
if (elementToDecrypt.sha256 != currentDigestValue) {
Timber.e("## decryptAttachment() : Digest value mismatch")
return null
return false
}
return ByteArrayInputStream(outputStream.toByteArray())
.also { Timber.v("Decrypt in ${System.currentTimeMillis() - t0}ms") }
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
}
}

View file

@ -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")
}
}
}

View file

@ -16,6 +16,7 @@
*/
package org.matrix.android.sdk.internal.crypto.model.event
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.internal.di.MoshiProvider
@ -27,27 +28,32 @@ data class OlmPayloadContent(
/**
* The room id
*/
var room_id: String? = null,
@Json(name = "room_id")
val roomId: String? = null,
/**
* The sender
*/
var sender: String? = null,
@Json(name = "sender")
val sender: String? = null,
/**
* The recipient
*/
var recipient: String? = null,
@Json(name = "recipient")
val recipient: String? = null,
/**
* the recipient keys
*/
var recipient_keys: Map<String, String>? = null,
@Json(name = "recipient_keys")
val recipientKeys: Map<String, String>? = null,
/**
* The keys
*/
var keys: Map<String, String>? = null
@Json(name = "keys")
val keys: Map<String, String>? = null
) {
fun toJsonString(): String {
return MoshiProvider.providesMoshi().adapter(OlmPayloadContent::class.java).toJson(this)

View file

@ -31,7 +31,7 @@ data class DeviceInfo(
* The owner user id (not documented and useless but the homeserver sent it. You should not need it)
*/
@Json(name = "user_id")
val user_id: String? = null,
val userId: String? = null,
/**
* The device id

View file

@ -37,7 +37,7 @@ data class EncryptedFileKey(
* Required. Key operations. Must at least contain "encrypt" and "decrypt".
*/
@Json(name = "key_ops")
val key_ops: List<String>? = null,
val keyOps: List<String>? = null,
/**
* Required. Key type. Must be "oct".
@ -63,7 +63,7 @@ data class EncryptedFileKey(
return false
}
if (key_ops?.contains("encrypt") != true || !key_ops.contains("decrypt")) {
if (keyOps?.contains("encrypt") != true || !keyOps.contains("decrypt")) {
return false
}

View file

@ -21,7 +21,6 @@ import android.util.Base64
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.RealmObject
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.ObjectOutputStream
import java.util.zip.GZIPInputStream
@ -96,7 +95,7 @@ fun <T> deserializeFromRealm(string: String?): T? {
}
val decodedB64 = Base64.decode(string.toByteArray(), Base64.DEFAULT)
val bais = ByteArrayInputStream(decodedB64)
val bais = decodedB64.inputStream()
val gzis = GZIPInputStream(bais)
val ois = SafeObjectInputStream(gzis)
return ois.use {

View file

@ -28,7 +28,7 @@ import java.io.ObjectStreamClass
*
* Ref: https://stackoverflow.com/questions/3884492/how-can-i-change-package-for-a-bunch-of-java-serializable-classes
*/
internal class SafeObjectInputStream(`in`: InputStream) : ObjectInputStream(`in`) {
internal class SafeObjectInputStream(inputStream: InputStream) : ObjectInputStream(inputStream) {
init {
enableResolveObject(true)

View file

@ -175,8 +175,8 @@ internal abstract class SASDefaultVerificationTransaction(
?.unpaddedBase64PublicKey
?.let { masterPublicKey ->
val crossSigningKeyId = "ed25519:$masterPublicKey"
macUsingAgreedMethod(masterPublicKey, baseInfo + crossSigningKeyId)?.let { MSKMacString ->
keyMap[crossSigningKeyId] = MSKMacString
macUsingAgreedMethod(masterPublicKey, baseInfo + crossSigningKeyId)?.let { mskMacString ->
keyMap[crossSigningKeyId] = mskMacString
}
}

View file

@ -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)
}

View file

@ -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)
}
}

View file

@ -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()

View file

@ -17,12 +17,12 @@
package org.matrix.android.sdk.internal.database.mapper
import com.squareup.moshi.Types
import org.matrix.android.sdk.api.pushrules.Condition
import io.realm.RealmList
import org.matrix.android.sdk.api.pushrules.Kind
import org.matrix.android.sdk.api.pushrules.rest.PushCondition
import org.matrix.android.sdk.api.pushrules.rest.PushRule
import org.matrix.android.sdk.internal.database.model.PushRuleEntity
import org.matrix.android.sdk.internal.di.MoshiProvider
import io.realm.RealmList
import timber.log.Timber
internal object PushRulesMapper {
@ -39,7 +39,7 @@ internal object PushRulesMapper {
enabled = pushrule.enabled,
ruleId = pushrule.ruleId,
conditions = listOf(
PushCondition(Condition.Kind.EventMatch.value, "content.body", pushrule.pattern)
PushCondition(Kind.EventMatch.value, "content.body", pushrule.pattern)
)
)
}
@ -60,7 +60,7 @@ internal object PushRulesMapper {
enabled = pushrule.enabled,
ruleId = pushrule.ruleId,
conditions = listOf(
PushCondition(Condition.Kind.EventMatch.value, "room_id", pushrule.ruleId)
PushCondition(Kind.EventMatch.value, "room_id", pushrule.ruleId)
)
)
}
@ -72,7 +72,7 @@ internal object PushRulesMapper {
enabled = pushrule.enabled,
ruleId = pushrule.ruleId,
conditions = listOf(
PushCondition(Condition.Kind.EventMatch.value, "user_id", pushrule.ruleId)
PushCondition(Kind.EventMatch.value, "user_id", pushrule.ruleId)
)
)
}

View file

@ -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()

View file

@ -36,6 +36,7 @@ import io.realm.annotations.RealmModule
RoomSummaryEntity::class,
RoomTagEntity::class,
SyncEntity::class,
PendingThreePidEntity::class,
UserEntity::class,
IgnoredUserEntity::class,
BreadcrumbsEntity::class,

View file

@ -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) {

View file

@ -19,13 +19,12 @@ package org.matrix.android.sdk.internal.network
import com.squareup.moshi.Moshi
import dagger.Lazy
import org.matrix.android.sdk.internal.util.ensureTrailingSlash
import okhttp3.Call
import okhttp3.OkHttpClient
import okhttp3.Request
import org.matrix.android.sdk.internal.util.ensureTrailingSlash
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.converter.scalars.ScalarsConverterFactory
import javax.inject.Inject
internal class RetrofitFactory @Inject constructor(private val moshi: Moshi) {
@ -50,7 +49,6 @@ internal class RetrofitFactory @Inject constructor(private val moshi: Moshi) {
return okHttpClient.get().newCall(request)
}
})
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(UnitConverterFactory)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()

View file

@ -0,0 +1,66 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.network.parsing
import androidx.annotation.Nullable
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.Moshi
import java.io.IOException
import java.lang.reflect.Type
import java.math.BigDecimal
/**
* This is used to check if NUMBER in json is integer or double, so we can preserve typing when serializing/deserializing in a row.
*/
interface CheckNumberType {
companion object {
val JSON_ADAPTER_FACTORY = object : JsonAdapter.Factory {
@Nullable
override fun create(type: Type, annotations: Set<Annotation?>?, moshi: Moshi): JsonAdapter<*>? {
if (type !== Any::class.java) {
return null
}
val delegate: JsonAdapter<Any> = moshi.nextAdapter(this, Any::class.java, emptySet())
return object : JsonAdapter<Any?>() {
@Nullable
@Throws(IOException::class)
override fun fromJson(reader: JsonReader): Any? {
return if (reader.peek() !== JsonReader.Token.NUMBER) {
delegate.fromJson(reader)
} else {
val numberAsString = reader.nextString()
val decimal = BigDecimal(numberAsString)
if (decimal.scale() <= 0) {
decimal.intValueExact()
} else {
decimal.toDouble()
}
}
}
override fun toJson(writer: JsonWriter, value: Any?) {
delegate.toJson(writer, value)
}
}
}
}
}
}

View file

@ -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))

View file

@ -0,0 +1,45 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.call
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import org.matrix.android.sdk.api.session.call.MxCall
import org.matrix.android.sdk.internal.session.SessionScope
import javax.inject.Inject
@SessionScope
internal class ActiveCallHandler @Inject constructor() {
private val activeCallListLiveData: MutableLiveData<MutableList<MxCall>> by lazy {
MutableLiveData<MutableList<MxCall>>(mutableListOf())
}
fun addCall(call: MxCall) {
activeCallListLiveData.postValue(activeCallListLiveData.value?.apply { add(call) })
}
fun removeCall(callId: String) {
activeCallListLiveData.postValue(activeCallListLiveData.value?.apply { removeAll { it.callId == callId } })
}
fun getCallWithId(callId: String): MxCall? {
return activeCallListLiveData.value?.find { it.callId == callId }
}
fun getActiveCallsLiveData(): LiveData<MutableList<MxCall>> = activeCallListLiveData
}

View file

@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.session.call
import android.os.SystemClock
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.extensions.tryThis
import org.matrix.android.sdk.api.session.call.CallSignalingService
@ -48,6 +49,7 @@ import javax.inject.Inject
internal class DefaultCallSignalingService @Inject constructor(
@UserId
private val userId: String,
private val activeCallHandler: ActiveCallHandler,
private val localEchoEventFactory: LocalEchoEventFactory,
private val roomEventSender: RoomEventSender,
private val taskExecutor: TaskExecutor,
@ -56,13 +58,11 @@ internal class DefaultCallSignalingService @Inject constructor(
private val callListeners = mutableSetOf<CallsListener>()
private val activeCalls = mutableListOf<MxCall>()
private val cachedTurnServerResponse = object {
// Keep one minute safe to avoid considering the data is valid and then actually it is not when effectively using it.
private val MIN_TTL = 60
private val now = { System.currentTimeMillis() / 1000 }
private val now = { SystemClock.elapsedRealtime() / 1000 }
private var expiresAt: Long = 0
@ -96,7 +96,7 @@ internal class DefaultCallSignalingService @Inject constructor(
}
override fun createOutgoingCall(roomId: String, otherUserId: String, isVideoCall: Boolean): MxCall {
return MxCallImpl(
val call = MxCallImpl(
callId = UUID.randomUUID().toString(),
isOutgoing = true,
roomId = roomId,
@ -105,8 +105,9 @@ internal class DefaultCallSignalingService @Inject constructor(
isVideoCall = isVideoCall,
localEchoEventFactory = localEchoEventFactory,
roomEventSender = roomEventSender
).also {
activeCalls.add(it)
)
activeCallHandler.addCall(call).also {
return call
}
}
@ -119,8 +120,12 @@ internal class DefaultCallSignalingService @Inject constructor(
}
override fun getCallWithId(callId: String): MxCall? {
Timber.v("## VOIP getCallWithId $callId all calls ${activeCalls.map { it.callId }}")
return activeCalls.find { it.callId == callId }
Timber.v("## VOIP getCallWithId $callId all calls ${activeCallHandler.getActiveCallsLiveData().value?.map { it.callId }}")
return activeCallHandler.getCallWithId(callId)
}
override fun isThereAnyActiveCall(): Boolean {
return activeCallHandler.getActiveCallsLiveData().value?.isNotEmpty() == true
}
internal fun onCallEvent(event: Event) {
@ -151,6 +156,7 @@ internal class DefaultCallSignalingService @Inject constructor(
// Always ignore local echos of invite
return
}
event.getClearContent().toModel<CallInviteContent>()?.let { content ->
val incomingCall = MxCallImpl(
callId = content.callId ?: return@let,
@ -162,7 +168,7 @@ internal class DefaultCallSignalingService @Inject constructor(
localEchoEventFactory = localEchoEventFactory,
roomEventSender = roomEventSender
)
activeCalls.add(incomingCall)
activeCallHandler.addCall(incomingCall)
onCallInvite(incomingCall, content)
}
}
@ -184,8 +190,8 @@ internal class DefaultCallSignalingService @Inject constructor(
return
}
activeCallHandler.removeCall(content.callId)
onCallHangup(content)
activeCalls.removeAll { it.callId == content.callId }
}
}
EventType.CALL_CANDIDATES -> {
@ -194,7 +200,7 @@ internal class DefaultCallSignalingService @Inject constructor(
return
}
event.getClearContent().toModel<CallCandidatesContent>()?.let { content ->
activeCalls.firstOrNull { it.callId == content.callId }?.let {
activeCallHandler.getCallWithId(content.callId)?.let {
onCallIceCandidate(it, content)
}
}

View file

@ -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)
}

View file

@ -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()
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() }
}
}
inputStream.use {
uploadByteArray(it.readBytes(), filename, mimeType, progressListener)
}
}
}
private suspend fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): ContentUploadResponse {
val urlBuilder = uploadUrl.toHttpUrlOrNull()?.newBuilder() ?: throw RuntimeException()

View file

@ -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)
}
}

View file

@ -22,8 +22,7 @@ import android.graphics.BitmapFactory
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass
import id.zelory.compressor.Compressor
import id.zelory.compressor.constraint.default
import org.matrix.android.sdk.api.extensions.tryThis
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.toContent
@ -37,15 +36,13 @@ import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
import org.matrix.android.sdk.internal.network.ProgressRequestBody
import org.matrix.android.sdk.internal.session.DefaultFileService
import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker
import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import org.matrix.android.sdk.internal.worker.getSessionComponent
import timber.log.Timber
import java.io.ByteArrayInputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.util.UUID
import javax.inject.Inject
@ -74,6 +71,8 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
@Inject lateinit var fileUploader: FileUploader
@Inject lateinit var contentUploadStateTracker: DefaultContentUploadStateTracker
@Inject lateinit var fileService: DefaultFileService
@Inject lateinit var cancelSendTracker: CancelSendTracker
@Inject lateinit var imageCompressor: ImageCompressor
override suspend fun doWork(): Result {
val params = WorkerParamsFactory.fromData<Params>(inputData)
@ -101,9 +100,15 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
sessionComponent.inject(this)
val attachment = params.attachment
val allCancelled = params.events.all { cancelSendTracker.isCancelRequestedFor(it.eventId, it.roomId) }
if (allCancelled) {
// there is no point in uploading the image!
return Result.success(inputData)
.also { Timber.e("## Send: Work cancelled by user") }
}
var newImageAttributes: NewImageAttributes? = null
val attachment = params.attachment
val filesToDelete = mutableListOf<File>()
try {
val inputStream = context.contentResolver.openInputStream(attachment.queryUri)
@ -115,39 +120,16 @@ 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) }
// 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)
}
}
try {
val contentUploadResponse = if (params.isEncrypted) {
Timber.v("Encrypt thumbnail")
notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) }
val encryptionResult = MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType)
uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo
fileUploader.uploadByteArray(encryptionResult.encryptedByteArray,
"thumb_${attachment.name}",
"application/octet-stream",
thumbnailProgressListener)
} else {
fileUploader.uploadByteArray(thumbnailData.bytes,
"thumb_${attachment.name}",
thumbnailData.mimeType,
thumbnailProgressListener)
}
uploadedThumbnailUrl = contentUploadResponse.contentUri
} catch (t: Throwable) {
Timber.e(t, "Thumbnail update failed")
}
}
val uploadThumbnailResult = dealWithThumbnail(params)
val progressListener = object : ProgressRequestBody.Listener {
override fun onProgress(current: Long, total: Long) {
@ -164,33 +146,17 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null
return try {
// Compressor library works with File instead of Uri for now. Since Scoped Storage doesn't allow us to access files directly, we should
// copy it to a cache folder by using InputStream and OutputStream.
// https://github.com/zetbaitsu/Compressor/pull/150
// As soon as the above PR is merged, we can use attachment.queryUri instead of creating a cacheFile.
var cacheFile = File.createTempFile(attachment.name ?: UUID.randomUUID().toString(), ".jpg", context.cacheDir)
cacheFile.parentFile?.mkdirs()
if (cacheFile.exists()) {
cacheFile.delete()
}
cacheFile.createNewFile()
cacheFile.deleteOnExit()
val outputStream = FileOutputStream(cacheFile)
outputStream.use {
inputStream.copyTo(outputStream)
}
val fileToUpload: File
var newImageAttributes: NewImageAttributes? = null
if (attachment.type == ContentAttachmentData.Type.IMAGE && params.compressBeforeSending) {
cacheFile = Compressor.compress(context, cacheFile) {
default(
width = MAX_IMAGE_SIZE,
height = MAX_IMAGE_SIZE
)
}.also { compressedFile ->
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 }
BitmapFactory.decodeFile(compressedFile.absolutePath, options)
val fileSize = compressedFile.length().toInt()
val bitmap = BitmapFactory.decodeStream(it, null, options)
val fileSize = bitmap?.byteCount ?: 0
newImageAttributes = NewImageAttributes(
options.outWidth,
options.outHeight,
@ -198,41 +164,56 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
)
}
}
val contentUploadResponse = if (params.isEncrypted) {
Timber.v("Encrypt file")
notifyTracker(params) { contentUploadStateTracker.setEncrypting(it) }
val encryptionResult = MXEncryptedAttachments.encryptAttachment(FileInputStream(cacheFile), attachment.getSafeMimeType())
uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo
fileUploader
.uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener)
.also { filesToDelete.add(it) }
} else {
fileUploader
.uploadFile(cacheFile, attachment.name, attachment.getSafeMimeType(), progressListener)
fileToUpload = workingFile
}
// If it's a file update the file service so that it does not redownload?
if (params.attachment.type == ContentAttachmentData.Type.FILE) {
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,
uploadedThumbnailUrl,
uploadedThumbnailEncryptedFileInfo,
uploadThumbnailResult?.uploadedThumbnailUrl,
uploadThumbnailResult?.uploadedThumbnailEncryptedFileInfo,
newImageAttributes)
} catch (t: Throwable) {
Timber.e(t)
Timber.e(t, "## FileService: ERROR ${t.localizedMessage}")
handleFailure(params, t)
}
}
} catch (e: Exception) {
Timber.e(e)
Timber.e(e, "## FileService: ERROR")
notifyTracker(params) { contentUploadStateTracker.setFailure(it, e) }
return Result.success(
WorkerParamsFactory.toData(
@ -241,6 +222,58 @@ 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
}
}
}
@ -262,7 +295,6 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
thumbnailUrl: String?,
thumbnailEncryptedFileInfo: EncryptedFileInfo?,
newImageAttributes: NewImageAttributes?): Result {
Timber.v("handleSuccess $attachmentUrl, work is stopped $isStopped")
notifyTracker(params) { contentUploadStateTracker.setSuccess(it) }
val updatedEvents = params.events
@ -271,7 +303,9 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
}
val sendParams = MultipleEventSendingDispatcherWorker.Params(params.sessionId, updatedEvents, params.isEncrypted)
return Result.success(WorkerParamsFactory.toData(sendParams))
return Result.success(WorkerParamsFactory.toData(sendParams)).also {
Timber.v("## handleSuccess $attachmentUrl, work is stopped $isStopped")
}
}
private fun updateEvent(event: Event,

View file

@ -61,6 +61,7 @@ internal class DefaultContentDownloadStateTracker @Inject constructor() : Progre
// private fun URL.toKey() = toString()
override fun update(url: String, bytesRead: Long, contentLength: Long, done: Boolean) {
mainHandler.post {
Timber.v("## DL Progress url:$url read:$bytesRead total:$contentLength done:$done")
if (done) {
updateState(url, ContentDownloadStateTracker.State.Success)
@ -68,14 +69,17 @@ internal class DefaultContentDownloadStateTracker @Inject constructor() : Progre
updateState(url, ContentDownloadStateTracker.State.Downloading(bytesRead, contentLength, contentLength == -1L))
}
}
}
override fun error(url: String, errorCode: Int) {
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)) }
}
}
}
private fun updateState(url: String, state: ContentDownloadStateTracker.State) {
states[url] = state

View file

@ -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

View file

@ -100,7 +100,7 @@ internal class DefaultProcessEventForPushTask @Inject constructor(
return rules.firstOrNull { rule ->
// All conditions must hold true for an event in order to apply the action for the event.
rule.enabled && rule.conditions?.all {
it.asExecutableCondition()?.isSatisfied(event, conditionResolver) ?: false
it.asExecutableCondition(rule)?.isSatisfied(event, conditionResolver) ?: false
} ?: false
}
}

View file

@ -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
)

View file

@ -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
)

View file

@ -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
)

View file

@ -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
)

View file

@ -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<AddThreePidTask.Params, Unit> {
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<AddEmailResponse>(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<AddMsisdnResponse>(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) }
}
}
}

View file

@ -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<Optional<String>>): Cancelable {
@ -116,9 +122,7 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
override fun getThreePidsLive(refreshData: Boolean): LiveData<List<ThreePid>> {
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<ThreePid> {
return monarchy.fetchAllMappedSync(
{ it.where<PendingThreePidEntity>() },
{ pendingThreePidMapper.map(it).threePid }
)
}
override fun getPendingThreePidsLive(): LiveData<List<ThreePid>> {
return monarchy.findAllMappedWithChanges(
{ it.where<PendingThreePidEntity>() },
{ pendingThreePidMapper.map(it).threePid }
)
}
override fun addThreePid(threePid: ThreePid, matrixCallback: MatrixCallback<Unit>): Cancelable {
return addThreePidTask
.configureWith(AddThreePidTask.Params(threePid)) {
callback = matrixCallback
}
.executeBy(taskExecutor)
}
override fun submitSmsCode(threePid: ThreePid.Msisdn, code: String, matrixCallback: MatrixCallback<Unit>): Cancelable {
return validateSmsCodeTask
.configureWith(ValidateSmsCodeTask.Params(threePid, code)) {
callback = matrixCallback
}
.executeBy(taskExecutor)
}
override fun finalizeAddingThreePid(threePid: ThreePid,
uiaSession: String?,
accountPassword: String?,
matrixCallback: MatrixCallback<Unit>): 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<Unit>): 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<Unit>): MatrixCallback<Unit> {
return object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
override fun onSuccess(data: Unit) {
refreshThreePids()
callback.onSuccess(data)
}
}
}
override fun deleteThreePid(threePid: ThreePid, matrixCallback: MatrixCallback<Unit>): Cancelable {
return deleteThreePidTask
.configureWith(DeleteThreePidTask.Params(threePid)) {
callback = alsoRefresh(matrixCallback)
}
.executeBy(taskExecutor)
}
}
private fun UserThreePidEntity.asDomain(): ThreePid {

View file

@ -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
)

View file

@ -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
)

View file

@ -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<DeleteThreePidTask.Params, Unit> {
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<DeleteThreePidResponse>(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
}
}

View file

@ -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?
)

View file

@ -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<FinalizeAddingThreePidTask.Params, Unit> {
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<Unit>(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()
}
}
}

View file

@ -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?
)

View file

@ -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
)
}
}

View file

@ -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<UnbindThreePidResponse>
/**
* 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<AddEmailResponse>
/**
* 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<AddMsisdnResponse>
/**
* Validate Msisdn code (same model than for Identity server API)
*/
@POST
fun validateMsisdn(@Url url: String,
@Body params: ValidationCodeBody): Call<SuccessResult>
/**
* 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<Unit>
/**
* 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<DeleteThreePidResponse>
}

View file

@ -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
}

View file

@ -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<ValidateSmsCodeTask.Params, Unit> {
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<SuccessResult>(eventBus) {
apiCall = profileAPI.validateMsisdn(url, body)
}
if (!result.isSuccess()) {
throw Failure.SuccessError
}
}
}

View file

@ -347,7 +347,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr
if (userId == senderId) {
sumModel.myVote = optionIndex
}
Timber.v("## POLL adding vote $optionIndex for user $senderId in poll :$relatedEventId ")
Timber.v("## POLL adding vote $optionIndex for user $senderId in poll :$targetEventId ")
} else {
Timber.v("## POLL Ignoring vote (older than known one) eventId:$eventId ")
}
@ -356,7 +356,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor(@UserId pr
if (userId == senderId) {
sumModel.myVote = optionIndex
}
Timber.v("## POLL adding vote $optionIndex for user $senderId in poll :$relatedEventId ")
Timber.v("## POLL adding vote $optionIndex for user $senderId in poll :$targetEventId ")
}
sumModel.votes = votes
if (isLocalEcho) {

View file

@ -130,21 +130,6 @@ internal interface RoomAPI {
@Body content: Content?
): Call<SendResponse>
/**
* Send an event to a room.
*
* @param txId the transaction Id
* @param roomId the room id
* @param eventType the event type
* @param content the event content as string
*/
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/send/{eventType}/{txId}")
fun send(@Path("txId") txId: String,
@Path("roomId") roomId: String,
@Path("eventType") eventType: String,
@Body content: String?
): Call<SendResponse>
/**
* Get the context surrounding an event.
*
@ -235,9 +220,9 @@ internal interface RoomAPI {
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/send_relation/{parent_id}/{relation_type}/{event_type}")
fun sendRelation(@Path("roomId") roomId: String,
@Path("parentId") parent_id: String,
@Path("parent_id") parentId: String,
@Path("relation_type") relationType: String,
@Path("eventType") eventType: String,
@Path("event_type") eventType: String,
@Body content: Content?
): Call<SendResponse>
@ -320,7 +305,7 @@ internal interface RoomAPI {
fun redactEvent(
@Path("txnId") txId: String,
@Path("roomId") roomId: String,
@Path("eventId") parent_id: String,
@Path("eventId") eventId: String,
@Body reason: Map<String, String>
): Call<SendResponse>

View file

@ -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

View file

@ -57,8 +57,8 @@ internal class CreateRoomBodyBuilder @Inject constructor(
invites.map {
ThreePidInviteBody(
id_server = identityServerUrlWithoutProtocol,
id_access_token = identityServerAccessToken,
idServer = identityServerUrlWithoutProtocol,
idAccessToken = identityServerAccessToken,
medium = it.toMedium(),
address = it.value
)

View file

@ -17,7 +17,7 @@
package org.matrix.android.sdk.internal.session.room.membership
import android.content.Context
import io.realm.Realm
import org.matrix.android.sdk.R
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
@ -34,13 +34,14 @@ import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.query.getOrNull
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.UserId
import io.realm.Realm
import org.matrix.android.sdk.internal.util.StringProvider
import javax.inject.Inject
/**
* This class computes room display name
*/
internal class RoomDisplayNameResolver @Inject constructor(private val context: Context,
internal class RoomDisplayNameResolver @Inject constructor(
private val stringProvider: StringProvider,
@UserId private val userId: String
) {
@ -89,7 +90,7 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context:
.findFirst()
?.displayName
} else {
context.getString(R.string.room_displayname_room_invite)
stringProvider.getString(R.string.room_displayname_room_invite)
}
} else if (roomEntity?.membership == Membership.JOIN) {
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
@ -108,13 +109,13 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context:
}
val otherMembersCount = otherMembersSubset.count()
name = when (otherMembersCount) {
0 -> context.getString(R.string.room_displayname_empty_room)
0 -> stringProvider.getString(R.string.room_displayname_empty_room)
1 -> resolveRoomMemberName(otherMembersSubset[0], roomMembers)
2 -> context.getString(R.string.room_displayname_two_members,
2 -> stringProvider.getString(R.string.room_displayname_two_members,
resolveRoomMemberName(otherMembersSubset[0], roomMembers),
resolveRoomMemberName(otherMembersSubset[1], roomMembers)
)
else -> context.resources.getQuantityString(R.plurals.room_displayname_three_and_more_members,
else -> stringProvider.getQuantityString(R.plurals.room_displayname_three_and_more_members,
roomMembers.getNumberOfJoinedMembers() - 1,
resolveRoomMemberName(otherMembersSubset[0], roomMembers),
roomMembers.getNumberOfJoinedMembers() - 1)

View file

@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.session.room.membership.threepid
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.session.identity.IdentityServiceError
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.identity.toMedium
@ -28,7 +29,6 @@ import org.matrix.android.sdk.internal.session.identity.data.IdentityStore
import org.matrix.android.sdk.internal.session.identity.data.getIdentityServerUrlWithoutProtocol
import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.task.Task
import org.greenrobot.eventbus.EventBus
import javax.inject.Inject
internal interface InviteThreePidTask : Task<InviteThreePidTask.Params, Unit> {
@ -55,8 +55,8 @@ internal class DefaultInviteThreePidTask @Inject constructor(
return executeRequest(eventBus) {
val body = ThreePidInviteBody(
id_server = identityServerUrlWithoutProtocol,
id_access_token = identityServerAccessToken,
idServer = identityServerUrlWithoutProtocol,
idAccessToken = identityServerAccessToken,
medium = params.threePid.toMedium(),
address = params.threePid.value
)

View file

@ -25,18 +25,22 @@ internal data class ThreePidInviteBody(
/**
* Required. The hostname+port of the identity server which should be used for third party identifier lookups.
*/
@Json(name = "id_server") val id_server: String,
@Json(name = "id_server")
val idServer: String,
/**
* Required. An access token previously registered with the identity server. Servers can treat this as optional
* to distinguish between r0.5-compatible clients and this specification version.
*/
@Json(name = "id_access_token") val id_access_token: String,
@Json(name = "id_access_token")
val idAccessToken: String,
/**
* Required. The kind of address being passed in the address field, for example email.
*/
@Json(name = "medium") val medium: String,
@Json(name = "medium")
val medium: String,
/**
* Required. The invitee's third party identifier.
*/
@Json(name = "address") val address: String
@Json(name = "address")
val address: String
)

View file

@ -18,7 +18,7 @@
package org.matrix.android.sdk.internal.session.room.notification
import org.matrix.android.sdk.api.pushrules.Action
import org.matrix.android.sdk.api.pushrules.Condition
import org.matrix.android.sdk.api.pushrules.Kind
import org.matrix.android.sdk.api.pushrules.RuleSetKey
import org.matrix.android.sdk.api.pushrules.getActions
import org.matrix.android.sdk.api.pushrules.rest.PushCondition
@ -59,7 +59,7 @@ internal fun RoomNotificationState.toRoomPushRule(roomId: String): RoomPushRule?
}
else -> {
val condition = PushCondition(
kind = Condition.Kind.EventMatch.value,
kind = Kind.EventMatch.value,
key = "room_id",
pattern = roomId
)

View file

@ -208,7 +208,7 @@ internal class DefaultRelationService @AssistedInject constructor(
}
private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
val sendContentWorkerParams = SendEventWorker.Params(sessionId, event)
val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, event = event)
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
return timeLineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)
}

View file

@ -20,6 +20,7 @@ import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.toModel
@ -31,7 +32,6 @@ import org.matrix.android.sdk.internal.session.room.send.SendResponse
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import org.matrix.android.sdk.internal.worker.getSessionComponent
import org.greenrobot.eventbus.EventBus
import timber.log.Timber
import javax.inject.Inject
@ -92,7 +92,7 @@ internal class SendRelationWorker(context: Context, params: WorkerParameters) :
executeRequest<SendResponse>(eventBus) {
apiCall = roomAPI.sendRelation(
roomId = roomId,
parent_id = relatedEventId,
parentId = relatedEventId,
relationType = relationType,
eventType = localEvent.type,
content = localEvent.content

View file

@ -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<Request>()
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)
}
}
}
}

View file

@ -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<MessageContent>() ?: return null
// when (messageContent.type) {
// MessageType.MSGTYPE_IMAGE -> {
// val imageContent = clearContent.toModel<MessageImageContent>() ?: 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<MessageContent>() 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)
}
return null
// 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 NoOpCancellable
}
override fun deleteFailedEcho(localEcho: TimelineEvent) {
@ -196,15 +242,33 @@ 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)
}
localEchoRepository.updateSendState(roomId, eventsToResend.mapNotNull { it.eventId }, SendState.UNSENT)
if (it.root.isTextMessage()) {
resendTextMessage(it)
} else if (it.root.isAttachmentMessage()) {
resendMediaMessage(it)
}
}
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,

View file

@ -54,6 +54,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
@Inject lateinit var crypto: CryptoService
@Inject lateinit var localEchoRepository: LocalEchoRepository
@Inject lateinit var cancelSendTracker: CancelSendTracker
override suspend fun doWork(): Result {
Timber.v("Start Encrypt work")
@ -61,7 +62,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
?: return Result.success()
.also { Timber.e("Unable to parse work parameters") }
Timber.v("Start Encrypt work for event ${params.event.eventId}")
Timber.v("## SendEvent: Start Encrypt work for event ${params.event.eventId}")
if (params.lastFailureMessage != null) {
// Transmit the error
return Result.success(inputData)
@ -75,6 +76,12 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
if (localEvent.eventId == null) {
return Result.success()
}
if (cancelSendTracker.isCancelRequestedFor(localEvent.eventId, localEvent.roomId)) {
return Result.success()
.also { Timber.e("## SendEvent: Event sending has been cancelled ${localEvent.eventId}") }
}
localEchoRepository.updateSendState(localEvent.eventId, SendState.ENCRYPTING)
val localMutableContent = localEvent.content?.toMutableMap() ?: mutableMapOf()
@ -120,7 +127,7 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
localEchoRepository.updateEncryptedEcho(localEvent.eventId, safeResult.eventContent, decryptionLocalEcho)
}
val nextWorkerParams = SendEventWorker.Params(params.sessionId, encryptedEvent)
val nextWorkerParams = SendEventWorker.Params(sessionId = params.sessionId, event = encryptedEvent)
return Result.success(WorkerParamsFactory.toData(nextWorkerParams))
} else {
val sendState = when (error) {
@ -129,8 +136,11 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
}
localEchoRepository.updateSendState(localEvent.eventId, sendState)
// always return success, or the chain will be stuck for ever!
val nextWorkerParams = SendEventWorker.Params(params.sessionId, localEvent, error?.localizedMessage
?: "Error")
val nextWorkerParams = SendEventWorker.Params(
sessionId = params.sessionId,
event = localEvent,
lastFailureMessage = error?.localizedMessage ?: "Error"
)
return Result.success(WorkerParamsFactory.toData(nextWorkerParams))
}
}

Some files were not shown because too many files have changed in this diff Show more