Merge branch 'release/0.9.0'

This commit is contained in:
Benoit Marty 2019-12-05 09:44:06 +01:00
commit 8e9ac8198d
276 changed files with 9848 additions and 1901 deletions

View File

@ -3,7 +3,9 @@
<words>
<w>backstack</w>
<w>bytearray</w>
<w>checkables</w>
<w>ciphertext</w>
<w>coroutine</w>
<w>decryptor</w>
<w>emoji</w>
<w>emojis</w>
@ -12,8 +14,11 @@
<w>linkified</w>
<w>linkify</w>
<w>megolm</w>
<w>msisdn</w>
<w>pbkdf</w>
<w>pkcs</w>
<w>signin</w>
<w>signup</w>
</words>
</dictionary>
</component>

View File

@ -1,3 +1,24 @@
Changes in RiotX 0.9.0 (2019-12-05)
===================================================
Features ✨:
- Account creation. It's now possible to create account on any homeserver with RiotX (#34)
- Iteration of the login flow (#613)
Improvements 🙌:
- Send mention Pills from composer
- Links in message preview in the bottom sheet are now active.
- Rework the read marker to make it more usable
Other changes:
- Fix a small grammatical error when an empty room list is shown.
Bugfix 🐛:
- Do not show long click help if only invitation are displayed
- Fix emoji filtering not working
- Fix issue of closing Realm in another thread (#725)
- Attempt to properly cancel the crypto module when user signs out (#724)
Changes in RiotX 0.8.0 (2019-11-19)
===================================================

260
docs/signin.md Normal file
View File

@ -0,0 +1,260 @@
# Sign in to a homeserver
This document describes the flow of signin to a homeserver, and also the flow when user want to reset his password. Examples come from the `matrix.org` homeserver.
## Sign up flows
### Get the flow
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'
200
```json
{
"flows": [
{
"type": "m.login.password"
}
]
}
```
### Login with username
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'
```json
{
"identifier": {
"type": "m.id.user",
"user": "alice"
},
"password": "weak_password",
"type": "m.login.password",
"initial_device_display_name": "Portable"
}
```
#### Incorrect password
403
```json
{
"errcode": "M_FORBIDDEN",
"error": "Invalid password"
}
```
#### Correct password:
We get credential (200)
```json
{
"user_id": "@benoit0816:matrix.org",
"access_token": "MDAxOGxvY2F0aW9uIG1hdHREDACTEDb2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lkIG5vbmNlID0gfnYrSypfdTtkNXIuNWx1KgowMDJmc2lnbmF0dXJlIOsh1XqeAkXexh4qcofl_aR4kHJoSOWYGOhE7-ubX-DZCg",
"home_server": "matrix.org",
"device_id": "GTVREDALBF",
"well_known": {
"m.homeserver": {
"base_url": "https:\/\/matrix.org\/"
}
}
}
```
### Login with email
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'
```json
{
"identifier": {
"type": "m.id.thirdparty",
"medium": "email",
"address": "alice@yopmail.com"
},
"password": "weak_password",
"type": "m.login.password",
"initial_device_display_name": "Portable"
}
```
#### Unknown email
403
```json
{
"errcode": "M_FORBIDDEN",
"error": ""
}
```
#### Known email, wrong password
403
```json
{
"errcode": "M_FORBIDDEN",
"error": "Invalid password"
}
```
##### Known email, correct password
We get the credentials (200)
```json
{
"user_id": "@alice:matrix.org",
"access_token": "MDAxOGxvY2F0aW9uIG1hdHJpeC5vcmREDACTEDZXJfaWQgPSBAYmVub2l0MDgxNjptYXRyaXgub3JnCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lkIG5vbmNlID0gNjtDY0MwRlNPSFFoOC5wOgowMDJmc2lnbmF0dXJlIGiTRm1mYLLxQywxOh3qzQVT8HoEorSokEP2u-bAwtnYCg",
"home_server": "matrix.org",
"device_id": "WBSREDASND",
"well_known": {
"m.homeserver": {
"base_url": "https:\/\/matrix.org\/"
}
}
}
```
### Login with Msisdn
Not supported yet in RiotX
### Login with SSO
> curl -X GET 'https://homeserver.with.sso/_matrix/client/r0/login'
200
```json
{
"flows": [
{
"type": "m.login.sso"
}
]
}
```
In this case, the user can click on "Sign in with SSO" and the web screen will be displayed on the page `https://homeserver.with.sso/_matrix/static/client/login/` and the credentials will be passed back to the native code through the JS bridge
## Reset password
Ref: `https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-account-password-email-requesttoken`
When the user has forgotten his password, he can reset it by providing an email and a new password.
Here is the flow:
### Send email
User is asked to enter the email linked to his account and a new password.
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'
```json
{
"client_secret": "6c57f284-85e2-421b-8270-fb1795a120a7",
"send_attempt": 0,
"email": "user@domain.com"
}
```
#### When the email is not known
We get a 400
```json
{
"errcode": "M_THREEPID_NOT_FOUND",
"error": "Email not found"
}
```
#### When the email is known
We get a 200 with a `sid`
```json
{
"sid": "tQNbrREDACTEDldA"
}
```
Then the user is asked to click on the link in the email he just received, and to confirm when it's done.
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'
```json
{
"auth": {
"type": "m.login.email.identity",
"threepid_creds": {
"client_secret": "6c57f284-85e2-421b-8270-fb1795a120a7",
"sid": "tQNbrREDACTEDldA"
}
},
"new_password": "weak_password"
}
```
401
```json
{
"errcode": "M_UNAUTHORIZED",
"error": ""
}
```
### User clicks on the link
The link has the form:
https://matrix.org/_matrix/client/unstable/password_reset/email/submit_token?token=fzZLBlcqhTKeaFQFSRbsQnQCkzbwtGAD&client_secret=6c57f284-85e2-421b-8270-fb1795a120a7&sid=tQNbrREDACTEDldA
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'
```json
{
"auth": {
"type": "m.login.email.identity",
"threepid_creds": {
"client_secret": "6c57f284-85e2-421b-8270-fb1795a120a7",
"sid": "tQNbrREDACTEDldA"
}
},
"new_password": "weak_password"
}
```
200
```json
{}
```
The password has been changed, and all the existing token are invalidated. User can now login with the new password.

579
docs/signup.md Normal file
View File

@ -0,0 +1,579 @@
# Sign up to a homeserver
This document describes the flow of registration to a homeserver. Examples come from the `matrix.org` homeserver.
*Ref*: https://matrix.org/docs/spec/client_server/latest#account-registration-and-management
## Sign up flows
### First step
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'
```json
{
}
```
We get the flows with a 401, which also means the the registration is possible on this homeserver.
```json
{
"session": "vwehdKMtkRedactedAMwgCACZ",
"flows": [
{
"stages": [
"m.login.recaptcha",
"m.login.terms",
"m.login.dummy"
]
},
{
"stages": [
"m.login.recaptcha",
"m.login.terms",
"m.login.email.identity"
]
}
],
"params": {
"m.login.recaptcha": {
"public_key": "6LcgI54UAAAAAoREDACTEDoDdOocFpYVdjYBRe4zb"
},
"m.login.terms": {
"policies": {
"privacy_policy": {
"version": "1.0",
"en": {
"name": "Terms and Conditions",
"url": "https:\/\/matrix.org\/_matrix\/consent?v=1.0"
}
}
}
}
}
}
```
If the registration is not possible, we get a 403
```json
{
"errcode": "M_FORBIDDEN",
"error": "Registration is disabled"
}
```
### Step 1: entering user name and password
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'
```json
{
"initial_device_display_name": "Mobile device",
"username": "alice",
"password": "weak_password"
}
```
401. Note that the `session` value has changed (because we did not provide the previous value in the request body), but it's ok, we will use the new value for the next steps.
```json
{
"session": "xptUYoREDACTEDogOWAGVnbJQ",
"flows": [
{
"stages": [
"m.login.recaptcha",
"m.login.terms",
"m.login.dummy"
]
},
{
"stages": [
"m.login.recaptcha",
"m.login.terms",
"m.login.email.identity"
]
}
],
"params": {
"m.login.recaptcha": {
"public_key": "6LcgI54UAAAAAoREDACTEDoDdOocFpYVdjYBRe4zb"
},
"m.login.terms": {
"policies": {
"privacy_policy": {
"version": "1.0",
"en": {
"name": "Terms and Conditions",
"url": "https:\/\/matrix.org\/_matrix\/consent?v=1.0"
}
}
}
}
}
}
```
#### If username already exists
We get a 400:
```json
{
"errcode": "M_USER_IN_USE",
"error": "User ID already taken."
}
```
### Step 2: entering email
User is proposed to enter an email. We skip this step.
> curl -X POST --data $'{"auth":{"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.dummy"}}' 'https://matrix.org/_matrix/client/r0/register'
```json
{
"auth": {
"session": "xptUYoREDACTEDogOWAGVnbJQ",
"type": "m.login.dummy"
}
}
```
401
```json
{
"session": "xptUYoREDACTEDogOWAGVnbJQ",
"flows": [
{
"stages": [
"m.login.recaptcha",
"m.login.terms",
"m.login.dummy"
]
},
{
"stages": [
"m.login.recaptcha",
"m.login.terms",
"m.login.email.identity"
]
}
],
"params": {
"m.login.recaptcha": {
"public_key": "6LcgI54UAAAAAoREDACTEDoDdOocFpYVdjYBRe4zb"
},
"m.login.terms": {
"policies": {
"privacy_policy": {
"version": "1.0",
"en": {
"name": "Terms and Conditions",
"url": "https:\/\/matrix.org\/_matrix\/consent?v=1.0"
}
}
}
}
},
"completed": [
"m.login.dummy"
]
}
```
### Step 2 bis: we enter 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'
```json
{
"client_secret": "53e679ea-oRED-ACTED-92b8-3012c49c6cfa",
"email": "alice@yopmail.com",
"send_attempt": 0
}
```
200
```json
{
"sid": "qlBCREDACTEDEtgxD"
}
```
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'
```json
{
"auth": {
"threepid_creds": {
"client_secret": "53e679ea-oRED-ACTED-92b8-3012c49c6cfa",
"sid": "qlBCREDACTEDEtgxD"
},
"session": "xptUYoREDACTEDogOWAGVnbJQ",
"type": "m.login.email.identity"
}
}
```
We get 401 since the email is not validated yet:
```json
{
"errcode": "M_UNAUTHORIZED",
"error": ""
}
```
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'
```json
{
"auth": {
"threepid_creds": {
"client_secret": "53e679ea-oRED-ACTED-92b8-3012c49c6cfa",
"sid": "qlBCREDACTEDEtgxD"
},
"session": "xptUYoREDACTEDogOWAGVnbJQ",
"type": "m.login.email.identity"
}
}
```
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:
- A `token` vtQjQIZfwdoREDACTEDozrmKYSWlCXsJ
- The `client_secret`: 53e679ea-oRED-ACTED-92b8-3012c49c6cfa
- A `sid`: qlBCREDACTEDEtgxD
Once the link is clicked, the registration request (polling) returns a 401 with the following content:
```json
{
"session": "xptUYoREDACTEDogOWAGVnbJQ",
"flows": [
{
"stages": [
"m.login.recaptcha",
"m.login.terms",
"m.login.dummy"
]
},
{
"stages": [
"m.login.recaptcha",
"m.login.terms",
"m.login.email.identity"
]
}
],
"params": {
"m.login.recaptcha": {
"public_key": "6LcgI54UAAAAAoREDACTEDoDdOocFpYVdjYBRe4zb"
},
"m.login.terms": {
"policies": {
"privacy_policy": {
"version": "1.0",
"en": {
"name": "Terms and Conditions",
"url": "https:\/\/matrix.org\/_matrix\/consent?v=1.0"
}
}
}
}
},
"completed": [
"m.login.email.identity"
]
}
```
### Step 3: Accepting T&C
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'
```json
{
"auth": {
"session": "xptUYoREDACTEDogOWAGVnbJQ",
"type": "m.login.terms"
}
}
```
401
```json
{
"session": "xptUYoREDACTEDogOWAGVnbJQ",
"flows": [
{
"stages": [
"m.login.recaptcha",
"m.login.terms",
"m.login.dummy"
]
},
{
"stages": [
"m.login.recaptcha",
"m.login.terms",
"m.login.email.identity"
]
}
],
"params": {
"m.login.recaptcha": {
"public_key": "6LcgI54UAAAAAoREDACTEDoDdOocFpYVdjYBRe4zb"
},
"m.login.terms": {
"policies": {
"privacy_policy": {
"version": "1.0",
"en": {
"name": "Terms and Conditions",
"url": "https:\/\/matrix.org\/_matrix\/consent?v=1.0"
}
}
}
}
},
"completed": [
"m.login.dummy",
"m.login.terms"
]
}
```
### Step 4: Captcha
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'
```json
{
"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"
}
}
```
200
```json
{
"user_id": "@alice:matrix.org",
"home_server": "matrix.org",
"access_token": "MDAxOGxvY2F0aW9uIG1hdHJpeC5vcmcKMoREDACTEDo50aWZpZXIga2V5CjAwMTBjaWQgZ2VuID0gMQowMDI5Y2lkIHVzZXJfaWQgPSBAYmVub2l0eHh4eDptYXRoREDACTEDoCjAwMTZjaWQgdHlwZSA9IGFjY2VzcwowMDIxY2lkIG5vbmNlID0gNHVSVm00aVFDaWlKdoREDACTEDoJmc2lnbmF0dXJlIOmHnTLRfxiPjhrWhS-dThUX-qAzZktfRThzH1YyAsxaCg",
"device_id": "FLBAREDAJZ"
}
```
The account is created!
### Step 5: MSISDN
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
> 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
{
"client_secret": "d3e285f6-972a-496c-9a22-7915a2db57c7",
"send_attempt": 1,
"country": "FR",
"phone_number": "+33611223344"
}
```
If the msisdn is already associated to another account, you will received an error:
```json
{
"errcode": "M_THREEPID_IN_USE",
"error": "Phone number is already in use"
}
```
If it is not the case, the homeserver send the SMS and returns some data, especially a `sid` and a `submit_url`:
```json
{
"msisdn": "33611223344",
"intl_fmt": "+336 11 22 33 44",
"success": true,
"sid": "1678881798",
"submit_url": "https:\/\/matrix.org\/_matrix\/client\/unstable\/add_threepid\/msisdn\/submit_token"
}
```
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'
```json
"auth": {
"type": "m.login.msisdn",
"session": "xptUYoREDACTEDogOWAGVnbJQ",
"threepid_creds": {
"client_secret": "d3e285f6-972a-496c-9a22-7915a2db57c7",
"sid": "1678881798"
}
}
}
```
There is an issue on Synapse, which return a 401, it sends too much data along with the classical MatrixError fields:
```json
{
"session": "xptUYoREDACTEDogOWAGVnbJQ",
"flows": [
{
"stages": [
"m.login.recaptcha",
"m.login.terms",
"m.login.dummy"
]
},
{
"stages": [
"m.login.recaptcha",
"m.login.terms",
"m.login.email.identity"
]
}
],
"params": {
"m.login.recaptcha": {
"public_key": "6LcgI54UAAAAABGdGmruw6DdOocFpYVdjYBRe4zb"
},
"m.login.terms": {
"policies": {
"privacy_policy": {
"version": "1.0",
"en": {
"name": "Terms and Conditions",
"url": "https:\/\/matrix.org\/_matrix\/consent?v=1.0"
}
}
}
}
},
"completed": [],
"error": "",
"errcode": "M_UNAUTHORIZED"
}
```
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'
```json
{
"client_secret": "d3e285f6-972a-496c-9a22-7915a2db57c7",
"sid": "1678881798",
"token": "123456"
}
```
If the code is not correct, we get a 200 with:
```json
{
"success": false
}
```
And if the code is correct we get a 200 with:
```json
{
"success": true
}
```
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'
```json
{
"auth": {
"type": "m.login.msisdn",
"session": "xptUYoREDACTEDogOWAGVnbJQ",
"threepid_creds": {
"client_secret": "d3e285f6-972a-496c-9a22-7915a2db57c7",
"sid": "1678881798"
}
}
}
```
Now the homeserver consider that the `m.login.msisdn` step is completed (401):
```json
{
"session": "xptUYoREDACTEDogOWAGVnbJQ",
"flows": [
{
"stages": [
"m.login.recaptcha",
"m.login.terms",
"m.login.dummy"
]
},
{
"stages": [
"m.login.recaptcha",
"m.login.terms",
"m.login.email.identity"
]
}
],
"params": {
"m.login.recaptcha": {
"public_key": "6LcgI54UAAAAABGdGmruw6DdOocFpYVdjYBRe4zb"
},
"m.login.terms": {
"policies": {
"privacy_policy": {
"version": "1.0",
"en": {
"name": "Terms and Conditions",
"url": "https:\/\/matrix.org\/_matrix\/consent?v=1.0"
}
}
}
}
},
"completed": [
"m.login.msisdn"
]
}
```

View File

@ -21,7 +21,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.rule.GrantPermissionRule
import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.OkReplayRuleChainNoActivity
import im.vector.matrix.android.api.auth.Authenticator
import im.vector.matrix.android.api.auth.AuthenticationService
import okreplay.*
import org.junit.ClassRule
import org.junit.Rule
@ -29,9 +29,9 @@ import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
internal class AuthenticatorTest : InstrumentedTest {
internal class AuthenticationServiceTest : InstrumentedTest {
lateinit var authenticator: Authenticator
lateinit var authenticationService: AuthenticationService
lateinit var okReplayInterceptor: OkReplayInterceptor
private val okReplayConfig = OkReplayConfig.Builder()

View File

@ -22,7 +22,7 @@ import androidx.work.Configuration
import androidx.work.WorkManager
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.auth.Authenticator
import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.di.DaggerMatrixComponent
import im.vector.matrix.android.internal.network.UserAgentHolder
@ -46,7 +46,7 @@ data class MatrixConfiguration(
*/
class Matrix private constructor(context: Context, matrixConfiguration: MatrixConfiguration) {
@Inject internal lateinit var authenticator: Authenticator
@Inject internal lateinit var authenticationService: AuthenticationService
@Inject internal lateinit var userAgentHolder: UserAgentHolder
@Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver
@Inject internal lateinit var olmManager: OlmManager
@ -64,8 +64,8 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
fun getUserAgent() = userAgentHolder.userAgent
fun authenticator(): Authenticator {
return authenticator
fun authenticationService(): AuthenticationService {
return authenticationService
}
companion object {

View File

@ -19,29 +19,48 @@ package im.vector.matrix.android.api.auth
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.auth.data.LoginFlowResult
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.auth.login.LoginWizard
import im.vector.matrix.android.api.auth.registration.RegistrationWizard
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
/**
* This interface defines methods to authenticate to a matrix server.
* This interface defines methods to authenticate or to create an account to a matrix server.
*/
interface Authenticator {
interface AuthenticationService {
/**
* Request the supported login flows for this homeserver
* Request the supported login flows for this homeserver.
* This is the first method to call to be able to get a wizard to login or the create an account
*/
fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<LoginFlowResponse>): Cancelable
fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<LoginFlowResult>): Cancelable
/**
* @param homeServerConnectionConfig this param is used to configure the Homeserver
* @param login the login field
* @param password the password field
* @param callback the matrix callback on which you'll receive the result of authentication.
* @return return a [Cancelable]
* Return a LoginWizard, to login to the homeserver. The login flow has to be retrieved first.
*/
fun authenticate(homeServerConnectionConfig: HomeServerConnectionConfig, login: String, password: String, callback: MatrixCallback<Session>): Cancelable
fun getLoginWizard(): LoginWizard
/**
* Return a RegistrationWizard, to create an matrix account on the homeserver. The login flow has to be retrieved first.
*/
fun getRegistrationWizard(): RegistrationWizard
/**
* True when login and password has been sent with success to the homeserver
*/
val isRegistrationStarted: Boolean
/**
* Cancel pending login or pending registration
*/
fun cancelPendingLoginOrRegistration()
/**
* Reset all pending settings, including current HomeServerConnectionConfig
*/
fun reset()
/**
* Check if there is an authenticated [Session].
@ -67,5 +86,7 @@ interface Authenticator {
/**
* Create a session after a SSO successful login
*/
fun createSessionFromSso(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<Session>): Cancelable
fun createSessionFromSso(homeServerConnectionConfig: HomeServerConnectionConfig,
credentials: Credentials,
callback: MatrixCallback<Session>): Cancelable
}

View File

@ -30,4 +30,7 @@ data class Credentials(
@Json(name = "home_server") val homeServer: String,
@Json(name = "access_token") val accessToken: String,
@Json(name = "refresh_token") val refreshToken: String?,
@Json(name = "device_id") val deviceId: String?)
@Json(name = "device_id") val deviceId: String?,
// Optional data that may contain info to override home server and/or identity server
@Json(name = "well_known") val wellKnown: WellKnown? = null
)

View File

@ -25,7 +25,7 @@ import okhttp3.TlsVersion
/**
* This data class holds how to connect to a specific Homeserver.
* It's used with [im.vector.matrix.android.api.auth.Authenticator] class.
* It's used with [im.vector.matrix.android.api.auth.AuthenticationService] class.
* You should use the [Builder] to create one.
*/
@JsonClass(generateAdapter = true)

View File

@ -0,0 +1,29 @@
/*
* Copyright 2019 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 im.vector.matrix.android.api.auth.data
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
// Either a LoginFlowResponse, or an error if the homeserver is outdated
sealed class LoginFlowResult {
data class Success(
val loginFlowResponse: LoginFlowResponse,
val isLoginAndRegistrationSupported: Boolean
) : LoginFlowResult()
object OutdatedHomeserver : LoginFlowResult()
}

View File

@ -0,0 +1,111 @@
/*
* Copyright 2018 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 im.vector.matrix.android.api.auth.data
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Model for https://matrix.org/docs/spec/client_server/latest#get-matrix-client-versions
*
* Ex:
* <pre>
* {
* "unstable_features": {
* "m.lazy_load_members": true
* },
* "versions": [
* "r0.0.1",
* "r0.1.0",
* "r0.2.0",
* "r0.3.0"
* ]
* }
* </pre>
*/
@JsonClass(generateAdapter = true)
data class Versions(
@Json(name = "versions")
val supportedVersions: List<String>? = null,
@Json(name = "unstable_features")
val unstableFeatures: Map<String, Boolean>? = null
)
// MatrixClientServerAPIVersion
private const val r0_0_1 = "r0.0.1"
private const val r0_1_0 = "r0.1.0"
private const val r0_2_0 = "r0.2.0"
private const val r0_3_0 = "r0.3.0"
private const val r0_4_0 = "r0.4.0"
private const val r0_5_0 = "r0.5.0"
private const val r0_6_0 = "r0.6.0"
// MatrixVersionsFeature
private const val FEATURE_LAZY_LOAD_MEMBERS = "m.lazy_load_members"
private const val FEATURE_REQUIRE_IDENTITY_SERVER = "m.require_identity_server"
private const val FEATURE_ID_ACCESS_TOKEN = "m.id_access_token"
private const val FEATURE_SEPARATE_ADD_AND_BIND = "m.separate_add_and_bind"
/**
* Return true if the SDK supports this homeserver version
*/
fun Versions.isSupportedBySdk(): Boolean {
return supportLazyLoadMembers()
}
/**
* Return true if the SDK supports this homeserver version for login and registration
*/
fun Versions.isLoginAndRegistrationSupportedBySdk(): Boolean {
return !doesServerRequireIdentityServerParam()
&& doesServerAcceptIdentityAccessToken()
&& doesServerSeparatesAddAndBind()
}
/**
* Return true if the server support the lazy loading of room members
*
* @return true if the server support the lazy loading of room members
*/
private fun Versions.supportLazyLoadMembers(): Boolean {
return supportedVersions?.contains(r0_5_0) == true
|| unstableFeatures?.get(FEATURE_LAZY_LOAD_MEMBERS) == true
}
/**
* Indicate if the `id_server` parameter is required when registering with an 3pid,
* adding a 3pid or resetting password.
*/
private fun Versions.doesServerRequireIdentityServerParam(): Boolean {
if (supportedVersions?.contains(r0_6_0) == true) return false
return unstableFeatures?.get(FEATURE_REQUIRE_IDENTITY_SERVER) ?: true
}
/**
* Indicate if the `id_access_token` parameter can be safely passed to the homeserver.
* Some homeservers may trigger errors if they are not prepared for the new parameter.
*/
private fun Versions.doesServerAcceptIdentityAccessToken(): Boolean {
return supportedVersions?.contains(r0_6_0) == true
|| unstableFeatures?.get(FEATURE_ID_ACCESS_TOKEN) ?: false
}
private fun Versions.doesServerSeparatesAddAndBind(): Boolean {
return supportedVersions?.contains(r0_6_0) == true
|| unstableFeatures?.get(FEATURE_SEPARATE_ADD_AND_BIND) ?: false
}

View File

@ -0,0 +1,82 @@
/*
* Copyright 2019 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 im.vector.matrix.android.api.auth.data
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
* <pre>
* {
* "m.homeserver": {
* "base_url": "https://matrix.org"
* },
* "m.identity_server": {
* "base_url": "https://vector.im"
* }
* "m.integrations": {
* "managers": [
* {
* "api_url": "https://integrations.example.org",
* "ui_url": "https://integrations.example.org/ui"
* },
* {
* "api_url": "https://bots.example.org"
* }
* ]
* }
* }
* </pre>
*/
@JsonClass(generateAdapter = true)
data class WellKnown(
@Json(name = "m.homeserver")
var homeServer: WellKnownBaseConfig? = null,
@Json(name = "m.identity_server")
var identityServer: WellKnownBaseConfig? = null,
@Json(name = "m.integrations")
var integrations: Map<String, @JvmSuppressWildcards Any>? = null
) {
/**
* Returns the list of integration managers proposed
*/
fun getIntegrationManagers(): List<WellKnownManagerConfig> {
val managers = ArrayList<WellKnownManagerConfig>()
integrations?.get("managers")?.let {
(it as? ArrayList<*>)?.let { configs ->
configs.forEach { config ->
(config as? Map<*, *>)?.let { map ->
val apiUrl = map["api_url"] as? String
val uiUrl = map["ui_url"] as? String ?: apiUrl
if (apiUrl != null
&& apiUrl.startsWith("https://")
&& uiUrl!!.startsWith("https://")) {
managers.add(WellKnownManagerConfig(
apiUrl = apiUrl,
uiUrl = uiUrl
))
}
}
}
}
}
return managers
}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright 2019 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 im.vector.matrix.android.api.auth.data
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
* <pre>
* {
* "base_url": "https://vector.im"
* }
* </pre>
*/
@JsonClass(generateAdapter = true)
data class WellKnownBaseConfig(
@Json(name = "base_url")
val baseURL: String? = null
)

View File

@ -0,0 +1,21 @@
/*
* Copyright 2019 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 im.vector.matrix.android.api.auth.data
data class WellKnownManagerConfig(
val apiUrl : String,
val uiUrl: String
)

View File

@ -0,0 +1,48 @@
/*
* Copyright 2019 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 im.vector.matrix.android.api.auth.login
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.Cancelable
interface LoginWizard {
/**
* @param login the login field
* @param password the password field
* @param deviceName the initial device name
* @param callback the matrix callback on which you'll receive the result of authentication.
* @return return a [Cancelable]
*/
fun login(login: String,
password: String,
deviceName: String,
callback: MatrixCallback<Session>): Cancelable
/**
* Reset user password
*/
fun resetPassword(email: String,
newPassword: String,
callback: MatrixCallback<Unit>): Cancelable
/**
* Confirm the new password, once the user has checked his email
*/
fun resetPasswordMailConfirmed(callback: MatrixCallback<Unit>): Cancelable
}

View File

@ -0,0 +1,22 @@
/*
* Copyright 2019 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 im.vector.matrix.android.api.auth.registration
sealed class RegisterThreePid {
data class Email(val email: String) : RegisterThreePid()
data class Msisdn(val msisdn: String, val countryCode: String) : RegisterThreePid()
}

View File

@ -0,0 +1,30 @@
/*
* Copyright 2019 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 im.vector.matrix.android.api.auth.registration
import im.vector.matrix.android.api.session.Session
// Either a session or an object containing data about registration stages
sealed class RegistrationResult {
data class Success(val session: Session) : RegistrationResult()
data class FlowResponse(val flowResult: FlowResult) : RegistrationResult()
}
data class FlowResult(
val missingStages: List<Stage>,
val completedStages: List<Stage>
)

View File

@ -0,0 +1,46 @@
/*
* Copyright 2019 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 im.vector.matrix.android.api.auth.registration
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
interface RegistrationWizard {
fun getRegistrationFlow(callback: MatrixCallback<RegistrationResult>): Cancelable
fun createAccount(userName: String, password: String, initialDeviceDisplayName: String?, callback: MatrixCallback<RegistrationResult>): Cancelable
fun performReCaptcha(response: String, callback: MatrixCallback<RegistrationResult>): Cancelable
fun acceptTerms(callback: MatrixCallback<RegistrationResult>): Cancelable
fun dummy(callback: MatrixCallback<RegistrationResult>): Cancelable
fun addThreePid(threePid: RegisterThreePid, callback: MatrixCallback<RegistrationResult>): Cancelable
fun sendAgainThreePid(callback: MatrixCallback<RegistrationResult>): Cancelable
fun handleValidateThreePid(code: String, callback: MatrixCallback<RegistrationResult>): Cancelable
fun checkIfEmailHasBeenValidated(delayMillis: Long, callback: MatrixCallback<RegistrationResult>): Cancelable
val currentThreePid: String?
// True when login and password has been sent with success to the homeserver
val isRegistrationStarted: Boolean
}

View File

@ -0,0 +1,44 @@
/*
* Copyright 2019 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 im.vector.matrix.android.api.auth.registration
sealed class Stage(open val mandatory: Boolean) {
// m.login.recaptcha
data class ReCaptcha(override val mandatory: Boolean, val publicKey: String) : Stage(mandatory)
// m.login.oauth2
// m.login.email.identity
data class Email(override val mandatory: Boolean) : Stage(mandatory)
// m.login.msisdn
data class Msisdn(override val mandatory: Boolean) : Stage(mandatory)
// m.login.token
// m.login.dummy, can be mandatory if there is no other stages. In this case the account cannot be created by just sending a username
// and a password, the dummy stage has to be done
data class Dummy(override val mandatory: Boolean) : Stage(mandatory)
// Undocumented yet: m.login.terms
data class Terms(override val mandatory: Boolean, val policies: TermPolicies) : Stage(mandatory)
// For unknown stages
data class Other(override val mandatory: Boolean, val type: String, val params: Map<*, *>?) : Stage(mandatory)
}
typealias TermPolicies = Map<*, *>

View File

@ -34,6 +34,7 @@ sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) {
data class Cancelled(val throwable: Throwable? = null) : Failure(throwable)
data class NetworkConnection(val ioException: IOException? = null) : Failure(ioException)
data class ServerError(val error: MatrixError, val httpCode: Int) : Failure(RuntimeException(error.toString()))
object SuccessError : Failure(RuntimeException(RuntimeException("SuccessResult is false")))
// When server send an error, but it cannot be interpreted as a MatrixError
data class OtherServerError(val errorBody: String, val httpCode: Int) : Failure(RuntimeException(errorBody))

View File

@ -31,7 +31,9 @@ data class MatrixError(
@Json(name = "consent_uri") val consentUri: String? = null,
// RESOURCE_LIMIT_EXCEEDED data
@Json(name = "limit_type") val limitType: String? = null,
@Json(name = "admin_contact") val adminUri: String? = null) {
@Json(name = "admin_contact") val adminUri: String? = null,
// For LIMIT_EXCEEDED
@Json(name = "retry_after_ms") val retryAfterMillis: Long? = null) {
companion object {
const val FORBIDDEN = "M_FORBIDDEN"

View File

@ -17,6 +17,7 @@
package im.vector.matrix.android.api.session.file
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import java.io.File
@ -47,5 +48,5 @@ interface FileService {
fileName: String,
url: String?,
elementToDecrypt: ElementToDecrypt?,
callback: MatrixCallback<File>)
callback: MatrixCallback<File>): Cancelable
}

View File

@ -72,7 +72,7 @@ interface RelationService {
*/
fun editTextMessage(targetEventId: String,
msgType: String,
newBodyText: String,
newBodyText: CharSequence,
newBodyAutoMarkdown: Boolean,
compatibilityBodyText: String = "* $newBodyText"): Cancelable
@ -97,12 +97,14 @@ interface RelationService {
/**
* Reply to an event in the timeline (must be in same room)
* https://matrix.org/docs/spec/client_server/r0.4.0.html#id350
* The replyText can be a Spannable and contains special spans (UserMentionSpan) that will be translated
* by the sdk into pills.
* @param eventReplied the event referenced by the reply
* @param replyText the reply text
* @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
*/
fun replyToMessage(eventReplied: TimelineEvent,
replyText: String,
replyText: CharSequence,
autoMarkdown: Boolean = false): Cancelable?
fun getEventSummaryLive(eventId: String): LiveData<Optional<EventAnnotationsSummary>>

View File

@ -29,20 +29,23 @@ interface SendService {
/**
* Method to send a text message asynchronously.
* The text to send can be a Spannable and contains special spans (UserMentionSpan) that will be translated
* by the sdk into pills.
* @param text the text message to send
* @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE
* @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
* @return a [Cancelable]
*/
fun sendTextMessage(text: String, msgType: String = MessageType.MSGTYPE_TEXT, autoMarkdown: Boolean = false): Cancelable
fun sendTextMessage(text: CharSequence, msgType: String = MessageType.MSGTYPE_TEXT, autoMarkdown: Boolean = false): Cancelable
/**
* Method to send a text message with a formatted body.
* @param text the text message to send
* @param formattedText The formatted body using MessageType#FORMAT_MATRIX_HTML
* @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE
* @return a [Cancelable]
*/
fun sendFormattedTextMessage(text: String, formattedText: String): Cancelable
fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String = MessageType.MSGTYPE_TEXT): Cancelable
/**
* Method to send a media asynchronously.

View File

@ -0,0 +1,26 @@
/*
* Copyright 2019 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 im.vector.matrix.android.api.session.room.send
/**
* Tag class for spans that should mention a user.
* These Spans will be transformed into pills when detected in message to send
*/
interface UserMentionSpan {
val displayName: String
val userId: String
}

View File

@ -30,10 +30,16 @@ package im.vector.matrix.android.api.session.room.timeline
*/
interface Timeline {
var listener: Listener?
val timelineID: String
val isLive: Boolean
fun addListener(listener: Listener): Boolean
fun removeListener(listener: Listener): Boolean
fun removeAllListeners()
/**
* This should be called before any other method after creating the timeline. It ensures the underlying database is open
*/
@ -98,7 +104,7 @@ interface Timeline {
interface Listener {
/**
* Call when the timeline has been updated through pagination or sync.
* @param snapshot the most uptodate snapshot
* @param snapshot the most up to date snapshot
*/
fun onUpdated(snapshot: List<TimelineEvent>)
}

View File

@ -41,8 +41,7 @@ data class TimelineEvent(
val isUniqueDisplayName: Boolean,
val senderAvatar: String?,
val annotations: EventAnnotationsSummary? = null,
val readReceipts: List<ReadReceipt> = emptyList(),
val hasReadMarker: Boolean = false
val readReceipts: List<ReadReceipt> = emptyList()
) {
val metadata = HashMap<String, Any>()

View File

@ -29,3 +29,5 @@ interface Cancelable {
// no-op
}
}
object NoOpCancellable : Cancelable

View File

@ -17,20 +17,47 @@
package im.vector.matrix.android.internal.auth
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.Versions
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
import im.vector.matrix.android.internal.auth.login.ResetPasswordMailConfirmed
import im.vector.matrix.android.internal.auth.registration.*
import im.vector.matrix.android.internal.network.NetworkConstants
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.POST
import retrofit2.http.*
/**
* The login REST API.
*/
internal interface AuthAPI {
/**
* Get the version information of the homeserver
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_ + "versions")
fun versions(): Call<Versions>
/**
* Register to the homeserver
* Ref: https://matrix.org/docs/spec/client_server/latest#account-registration-and-management
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register")
fun register(@Body registrationParams: RegistrationParams): Call<Credentials>
/**
* Add 3Pid during registration
* Ref: https://gist.github.com/jryans/839a09bf0c5a70e2f36ed990d50ed928
* https://github.com/matrix-org/matrix-doc/pull/2290
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "register/{threePid}/requestToken")
fun add3Pid(@Path("threePid") threePid: String, @Body params: AddThreePidRegistrationParams): Call<AddThreePidRegistrationResponse>
/**
* Validate 3pid
*/
@POST
fun validate3Pid(@Url url: String, @Body params: ValidationCodeBody): Call<SuccessResult>
/**
* Get the supported login flow
* Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-login
@ -47,4 +74,16 @@ internal interface AuthAPI {
@Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000")
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login")
fun login(@Body loginParams: PasswordLoginParams): Call<Credentials>
/**
* Ask the homeserver to reset the password associated with the provided email.
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password/email/requestToken")
fun resetPassword(@Body params: AddThreePidRegistrationParams): Call<AddThreePidRegistrationResponse>
/**
* Ask the homeserver to reset the password with the provided new password once the email is validated.
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password")
fun resetPasswordMailConfirmed(@Body params: ResetPasswordMailConfirmed): Call<Unit>
}

View File

@ -20,8 +20,10 @@ import android.content.Context
import dagger.Binds
import dagger.Module
import dagger.Provides
import im.vector.matrix.android.api.auth.Authenticator
import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.internal.auth.db.AuthRealmMigration
import im.vector.matrix.android.internal.auth.db.AuthRealmModule
import im.vector.matrix.android.internal.auth.db.RealmPendingSessionStore
import im.vector.matrix.android.internal.auth.db.RealmSessionParamsStore
import im.vector.matrix.android.internal.database.RealmKeysUtils
import im.vector.matrix.android.internal.di.AuthDatabase
@ -50,7 +52,8 @@ internal abstract class AuthModule {
}
.name("matrix-sdk-auth.realm")
.modules(AuthRealmModule())
.deleteRealmIfMigrationNeeded()
.schemaVersion(AuthRealmMigration.SCHEMA_VERSION)
.migration(AuthRealmMigration())
.build()
}
}
@ -59,5 +62,11 @@ internal abstract class AuthModule {
abstract fun bindSessionParamsStore(sessionParamsStore: RealmSessionParamsStore): SessionParamsStore
@Binds
abstract fun bindAuthenticator(authenticator: DefaultAuthenticator): Authenticator
abstract fun bindPendingSessionStore(pendingSessionStore: RealmPendingSessionStore): PendingSessionStore
@Binds
abstract fun bindAuthenticationService(authenticationService: DefaultAuthenticationService): AuthenticationService
@Binds
abstract fun bindSessionCreator(sessionCreator: DefaultSessionCreator): SessionCreator
}

View File

@ -0,0 +1,205 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.auth
import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.api.auth.data.*
import im.vector.matrix.android.api.auth.login.LoginWizard
import im.vector.matrix.android.api.auth.registration.RegistrationWizard
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
import im.vector.matrix.android.internal.auth.db.PendingSessionData
import im.vector.matrix.android.internal.auth.login.DefaultLoginWizard
import im.vector.matrix.android.internal.auth.registration.DefaultRegistrationWizard
import im.vector.matrix.android.internal.di.Unauthenticated
import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.launchToCallback
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.toCancelable
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import javax.inject.Inject
internal class DefaultAuthenticationService @Inject constructor(@Unauthenticated
private val okHttpClient: Lazy<OkHttpClient>,
private val retrofitFactory: RetrofitFactory,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val sessionParamsStore: SessionParamsStore,
private val sessionManager: SessionManager,
private val sessionCreator: SessionCreator,
private val pendingSessionStore: PendingSessionStore
) : AuthenticationService {
private var pendingSessionData: PendingSessionData? = pendingSessionStore.getPendingSessionData()
private var currentLoginWizard: LoginWizard? = null
private var currentRegistrationWizard: RegistrationWizard? = null
override fun hasAuthenticatedSessions(): Boolean {
return sessionParamsStore.getLast() != null
}
override fun getLastAuthenticatedSession(): Session? {
val sessionParams = sessionParamsStore.getLast()
return sessionParams?.let {
sessionManager.getOrCreateSession(it)
}
}
override fun getSession(sessionParams: SessionParams): Session? {
return sessionManager.getOrCreateSession(sessionParams)
}
override fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<LoginFlowResult>): Cancelable {
pendingSessionData = null
return GlobalScope.launch(coroutineDispatchers.main) {
pendingSessionStore.delete()
val result = runCatching {
getLoginFlowInternal(homeServerConnectionConfig)
}
result.fold(
{
if (it is LoginFlowResult.Success) {
// The homeserver exists and up to date, keep the config
pendingSessionData = PendingSessionData(homeServerConnectionConfig)
.also { data -> pendingSessionStore.savePendingSessionData(data) }
}
callback.onSuccess(it)
},
{
callback.onFailure(it)
}
)
}
.toCancelable()
}
private suspend fun getLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig) = withContext(coroutineDispatchers.io) {
val authAPI = buildAuthAPI(homeServerConnectionConfig)
// First check the homeserver version
val versions = executeRequest<Versions> {
apiCall = authAPI.versions()
}
if (versions.isSupportedBySdk()) {
// Get the login flow
val loginFlowResponse = executeRequest<LoginFlowResponse> {
apiCall = authAPI.getLoginFlows()
}
LoginFlowResult.Success(loginFlowResponse, versions.isLoginAndRegistrationSupportedBySdk())
} else {
// Not supported
LoginFlowResult.OutdatedHomeserver
}
}
override fun getRegistrationWizard(): RegistrationWizard {
return currentRegistrationWizard
?: let {
pendingSessionData?.homeServerConnectionConfig?.let {
DefaultRegistrationWizard(
okHttpClient,
retrofitFactory,
coroutineDispatchers,
sessionCreator,
pendingSessionStore
).also {
currentRegistrationWizard = it
}
} ?: error("Please call getLoginFlow() with success first")
}
}
override val isRegistrationStarted: Boolean
get() = currentRegistrationWizard?.isRegistrationStarted == true
override fun getLoginWizard(): LoginWizard {
return currentLoginWizard
?: let {
pendingSessionData?.homeServerConnectionConfig?.let {
DefaultLoginWizard(
okHttpClient,
retrofitFactory,
coroutineDispatchers,
sessionCreator,
pendingSessionStore
).also {
currentLoginWizard = it
}
} ?: error("Please call getLoginFlow() with success first")
}
}
override fun cancelPendingLoginOrRegistration() {
currentLoginWizard = null
currentRegistrationWizard = null
// Keep only the home sever config
// Update the local pendingSessionData synchronously
pendingSessionData = pendingSessionData?.homeServerConnectionConfig
?.let { PendingSessionData(it) }
.also {
GlobalScope.launch(coroutineDispatchers.main) {
if (it == null) {
// Should not happen
pendingSessionStore.delete()
} else {
pendingSessionStore.savePendingSessionData(it)
}
}
}
}
override fun reset() {
currentLoginWizard = null
currentRegistrationWizard = null
pendingSessionData = null
GlobalScope.launch(coroutineDispatchers.main) {
pendingSessionStore.delete()
}
}
override fun createSessionFromSso(homeServerConnectionConfig: HomeServerConnectionConfig,
credentials: Credentials,
callback: MatrixCallback<Session>): Cancelable {
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
createSessionFromSso(credentials, homeServerConnectionConfig)
}
}
private suspend fun createSessionFromSso(credentials: Credentials,
homeServerConnectionConfig: HomeServerConnectionConfig): Session = withContext(coroutineDispatchers.computation) {
sessionCreator.createSession(credentials, homeServerConnectionConfig)
}
private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI {
val retrofit = retrofitFactory.create(okHttpClient, homeServerConnectionConfig.homeServerUri.toString())
return retrofit.create(AuthAPI::class.java)
}
}

View File

@ -1,138 +0,0 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.auth
import android.util.Patterns
import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.Authenticator
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.auth.data.LoginFlowResponse
import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
import im.vector.matrix.android.internal.auth.data.ThreePidMedium
import im.vector.matrix.android.internal.di.Unauthenticated
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.util.CancelableCoroutine
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import javax.inject.Inject
internal class DefaultAuthenticator @Inject constructor(@Unauthenticated
private val okHttpClient: Lazy<OkHttpClient>,
private val retrofitFactory: RetrofitFactory,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val sessionParamsStore: SessionParamsStore,
private val sessionManager: SessionManager
) : Authenticator {
override fun hasAuthenticatedSessions(): Boolean {
return sessionParamsStore.getLast() != null
}
override fun getLastAuthenticatedSession(): Session? {
val sessionParams = sessionParamsStore.getLast()
return sessionParams?.let {
sessionManager.getOrCreateSession(it)
}
}
override fun getSession(sessionParams: SessionParams): Session? {
return sessionManager.getOrCreateSession(sessionParams)
}
override fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<LoginFlowResponse>): Cancelable {
val job = GlobalScope.launch(coroutineDispatchers.main) {
val result = runCatching {
getLoginFlowInternal(homeServerConnectionConfig)
}
result.foldToCallback(callback)
}
return CancelableCoroutine(job)
}
override fun authenticate(homeServerConnectionConfig: HomeServerConnectionConfig,
login: String,
password: String,
callback: MatrixCallback<Session>): Cancelable {
val job = GlobalScope.launch(coroutineDispatchers.main) {
val sessionOrFailure = runCatching {
authenticate(homeServerConnectionConfig, login, password)
}
sessionOrFailure.foldToCallback(callback)
}
return CancelableCoroutine(job)
}
private suspend fun getLoginFlowInternal(homeServerConnectionConfig: HomeServerConnectionConfig) = withContext(coroutineDispatchers.io) {
val authAPI = buildAuthAPI(homeServerConnectionConfig)
executeRequest<LoginFlowResponse> {
apiCall = authAPI.getLoginFlows()
}
}
private suspend fun authenticate(homeServerConnectionConfig: HomeServerConnectionConfig,
login: String,
password: String) = withContext(coroutineDispatchers.io) {
val authAPI = buildAuthAPI(homeServerConnectionConfig)
val loginParams = if (Patterns.EMAIL_ADDRESS.matcher(login).matches()) {
PasswordLoginParams.thirdPartyIdentifier(ThreePidMedium.EMAIL, login, password, "Mobile")
} else {
PasswordLoginParams.userIdentifier(login, password, "Mobile")
}
val credentials = executeRequest<Credentials> {
apiCall = authAPI.login(loginParams)
}
val sessionParams = SessionParams(credentials, homeServerConnectionConfig)
sessionParamsStore.save(sessionParams)
sessionManager.getOrCreateSession(sessionParams)
}
override fun createSessionFromSso(credentials: Credentials,
homeServerConnectionConfig: HomeServerConnectionConfig,
callback: MatrixCallback<Session>): Cancelable {
val job = GlobalScope.launch(coroutineDispatchers.main) {
val sessionOrFailure = runCatching {
createSessionFromSso(credentials, homeServerConnectionConfig)
}
sessionOrFailure.foldToCallback(callback)
}
return CancelableCoroutine(job)
}
private suspend fun createSessionFromSso(credentials: Credentials,
homeServerConnectionConfig: HomeServerConnectionConfig): Session = withContext(coroutineDispatchers.computation) {
val sessionParams = SessionParams(credentials, homeServerConnectionConfig)
sessionParamsStore.save(sessionParams)
sessionManager.getOrCreateSession(sessionParams)
}
private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI {
val retrofit = retrofitFactory.create(okHttpClient, homeServerConnectionConfig.homeServerUri.toString())
return retrofit.create(AuthAPI::class.java)
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.auth
import im.vector.matrix.android.internal.auth.db.PendingSessionData
/**
* Store for elements when doing login or registration
*/
internal interface PendingSessionStore {
suspend fun savePendingSessionData(pendingSessionData: PendingSessionData)
fun getPendingSessionData(): PendingSessionData?
suspend fun delete()
}

View File

@ -0,0 +1,68 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.auth
import android.net.Uri
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.internal.SessionManager
import timber.log.Timber
import javax.inject.Inject
internal interface SessionCreator {
suspend fun createSession(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session
}
internal class DefaultSessionCreator @Inject constructor(
private val sessionParamsStore: SessionParamsStore,
private val sessionManager: SessionManager,
private val pendingSessionStore: PendingSessionStore
) : SessionCreator {
/**
* Credentials can affect the homeServerConnectionConfig, override home server url and/or
* identity server url if provided in the credentials
*/
override suspend fun createSession(credentials: Credentials, homeServerConnectionConfig: HomeServerConnectionConfig): Session {
// We can cleanup the pending session params
pendingSessionStore.delete()
val sessionParams = SessionParams(
credentials = credentials,
homeServerConnectionConfig = homeServerConnectionConfig.copy(
homeServerUri = credentials.wellKnown?.homeServer?.baseURL
// remove trailing "/"
?.trim { it == '/' }
?.takeIf { it.isNotBlank() }
?.also { Timber.d("Overriding homeserver url to $it") }
?.let { Uri.parse(it) }
?: homeServerConnectionConfig.homeServerUri,
identityServerUri = credentials.wellKnown?.identityServer?.baseURL
// remove trailing "/"
?.trim { it == '/' }
?.takeIf { it.isNotBlank() }
?.also { Timber.d("Overriding identity server url to $it") }
?.let { Uri.parse(it) }
?: homeServerConnectionConfig.identityServerUri
))
sessionParamsStore.save(sessionParams)
return sessionManager.getOrCreateSession(sessionParams)
}
}

View File

@ -30,12 +30,4 @@ data class InteractiveAuthenticationFlow(
@Json(name = "stages")
val stages: List<String>? = null
) {
companion object {
// Possible values for type
const val TYPE_LOGIN_SSO = "m.login.sso"
const val TYPE_LOGIN_TOKEN = "m.login.token"
const val TYPE_LOGIN_PASSWORD = "m.login.password"
}
}
)

View File

@ -25,4 +25,7 @@ object LoginFlowTypes {
const val MSISDN = "m.login.msisdn"
const val RECAPTCHA = "m.login.recaptcha"
const val DUMMY = "m.login.dummy"
const val TERMS = "m.login.terms"
const val TOKEN = "m.login.token"
const val SSO = "m.login.sso"
}

View File

@ -19,34 +19,46 @@ package im.vector.matrix.android.internal.auth.data
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Ref:
* - https://matrix.org/docs/spec/client_server/r0.5.0#password-based
* - https://matrix.org/docs/spec/client_server/r0.5.0#identifier-types
*/
@JsonClass(generateAdapter = true)
internal data class PasswordLoginParams(@Json(name = "identifier") val identifier: Map<String, String>,
@Json(name = "password") val password: String,
@Json(name = "type") override val type: String,
@Json(name = "initial_device_display_name") val deviceDisplayName: String?,
@Json(name = "device_id") val deviceId: String?) : LoginParams {
internal data class PasswordLoginParams(
@Json(name = "identifier") val identifier: Map<String, String>,
@Json(name = "password") val password: String,
@Json(name = "type") override val type: String,
@Json(name = "initial_device_display_name") val deviceDisplayName: String?,
@Json(name = "device_id") val deviceId: String?) : LoginParams {
companion object {
private const val IDENTIFIER_KEY_TYPE = "type"
val IDENTIFIER_KEY_TYPE_USER = "m.id.user"
val IDENTIFIER_KEY_TYPE_THIRD_PARTY = "m.id.thirdparty"
val IDENTIFIER_KEY_TYPE_PHONE = "m.id.phone"
private const val IDENTIFIER_KEY_TYPE_USER = "m.id.user"
private const val IDENTIFIER_KEY_USER = "user"
val IDENTIFIER_KEY_TYPE = "type"
val IDENTIFIER_KEY_MEDIUM = "medium"
val IDENTIFIER_KEY_ADDRESS = "address"
val IDENTIFIER_KEY_USER = "user"
val IDENTIFIER_KEY_COUNTRY = "country"
val IDENTIFIER_KEY_NUMBER = "number"
private const val IDENTIFIER_KEY_TYPE_THIRD_PARTY = "m.id.thirdparty"
private const val IDENTIFIER_KEY_MEDIUM = "medium"
private const val IDENTIFIER_KEY_ADDRESS = "address"
private const val IDENTIFIER_KEY_TYPE_PHONE = "m.id.phone"
private const val IDENTIFIER_KEY_COUNTRY = "country"
private const val IDENTIFIER_KEY_PHONE = "phone"
fun userIdentifier(user: String,
password: String,
deviceDisplayName: String? = null,
deviceId: String? = null): PasswordLoginParams {
val identifier = HashMap<String, String>()
identifier[IDENTIFIER_KEY_TYPE] = IDENTIFIER_KEY_TYPE_USER
identifier[IDENTIFIER_KEY_USER] = user
return PasswordLoginParams(identifier, password, LoginFlowTypes.PASSWORD, deviceDisplayName, deviceId)
return PasswordLoginParams(
mapOf(
IDENTIFIER_KEY_TYPE to IDENTIFIER_KEY_TYPE_USER,
IDENTIFIER_KEY_USER to user
),
password,
LoginFlowTypes.PASSWORD,
deviceDisplayName,
deviceId)
}
fun thirdPartyIdentifier(medium: String,
@ -54,11 +66,33 @@ internal data class PasswordLoginParams(@Json(name = "identifier") val identifie
password: String,
deviceDisplayName: String? = null,
deviceId: String? = null): PasswordLoginParams {
val identifier = HashMap<String, String>()
identifier[IDENTIFIER_KEY_TYPE] = IDENTIFIER_KEY_TYPE_THIRD_PARTY
identifier[IDENTIFIER_KEY_MEDIUM] = medium
identifier[IDENTIFIER_KEY_ADDRESS] = address
return PasswordLoginParams(identifier, password, LoginFlowTypes.PASSWORD, deviceDisplayName, deviceId)
return PasswordLoginParams(
mapOf(
IDENTIFIER_KEY_TYPE to IDENTIFIER_KEY_TYPE_THIRD_PARTY,
IDENTIFIER_KEY_MEDIUM to medium,
IDENTIFIER_KEY_ADDRESS to address
),
password,
LoginFlowTypes.PASSWORD,
deviceDisplayName,
deviceId)
}
fun phoneIdentifier(country: String,
phone: String,
password: String,
deviceDisplayName: String? = null,
deviceId: String? = null): PasswordLoginParams {
return PasswordLoginParams(
mapOf(
IDENTIFIER_KEY_TYPE to IDENTIFIER_KEY_TYPE_PHONE,
IDENTIFIER_KEY_COUNTRY to country,
IDENTIFIER_KEY_PHONE to phone
),
password,
LoginFlowTypes.PASSWORD,
deviceDisplayName,
deviceId)
}
}
}

View File

@ -0,0 +1,50 @@
/*
* Copyright 2018 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 im.vector.matrix.android.internal.auth.db
import io.realm.DynamicRealm
import io.realm.RealmMigration
import timber.log.Timber
internal class AuthRealmMigration : RealmMigration {
companion object {
// Current schema version
const val SCHEMA_VERSION = 1L
}
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.d("Migrating Auth Realm from $oldVersion to $newVersion")
if (oldVersion <= 0) {
Timber.d("Step 0 -> 1")
Timber.d("Create PendingSessionEntity")
realm.schema.create("PendingSessionEntity")
.addField(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, String::class.java)
.setRequired(PendingSessionEntityFields.HOME_SERVER_CONNECTION_CONFIG_JSON, true)
.addField(PendingSessionEntityFields.CLIENT_SECRET, String::class.java)
.setRequired(PendingSessionEntityFields.CLIENT_SECRET, true)
.addField(PendingSessionEntityFields.SEND_ATTEMPT, Integer::class.java)
.setRequired(PendingSessionEntityFields.SEND_ATTEMPT, true)
.addField(PendingSessionEntityFields.RESET_PASSWORD_DATA_JSON, String::class.java)
.addField(PendingSessionEntityFields.CURRENT_SESSION, String::class.java)
.addField(PendingSessionEntityFields.IS_REGISTRATION_STARTED, Boolean::class.java)
.addField(PendingSessionEntityFields.CURRENT_THREE_PID_DATA_JSON, String::class.java)
}
}
}

View File

@ -23,6 +23,7 @@ import io.realm.annotations.RealmModule
*/
@RealmModule(library = true,
classes = [
SessionParamsEntity::class
SessionParamsEntity::class,
PendingSessionEntity::class
])
internal class AuthRealmModule

View File

@ -0,0 +1,50 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.auth.db
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.internal.auth.login.ResetPasswordData
import im.vector.matrix.android.internal.auth.registration.ThreePidData
import java.util.*
/**
* This class holds all pending data when creating a session, either by login or by register
*/
internal data class PendingSessionData(
val homeServerConnectionConfig: HomeServerConnectionConfig,
/* ==========================================================================================
* Common
* ========================================================================================== */
val clientSecret: String = UUID.randomUUID().toString(),
val sendAttempt: Int = 0,
/* ==========================================================================================
* For login
* ========================================================================================== */
val resetPasswordData: ResetPasswordData? = null,
/* ==========================================================================================
* For register
* ========================================================================================== */
val currentSession: String? = null,
val isRegistrationStarted: Boolean = false,
val currentThreePidData: ThreePidData? = null
)

View File

@ -0,0 +1,29 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.auth.db
import io.realm.RealmObject
internal open class PendingSessionEntity(
var homeServerConnectionConfigJson: String = "",
var clientSecret: String = "",
var sendAttempt: Int = 0,
var resetPasswordDataJson: String? = null,
var currentSession: String? = null,
var isRegistrationStarted: Boolean = false,
var currentThreePidDataJson: String? = null
) : RealmObject()

View File

@ -0,0 +1,69 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.auth.db
import com.squareup.moshi.Moshi
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.internal.auth.login.ResetPasswordData
import im.vector.matrix.android.internal.auth.registration.ThreePidData
import javax.inject.Inject
internal class PendingSessionMapper @Inject constructor(moshi: Moshi) {
private val homeServerConnectionConfigAdapter = moshi.adapter(HomeServerConnectionConfig::class.java)
private val resetPasswordDataAdapter = moshi.adapter(ResetPasswordData::class.java)
private val threePidDataAdapter = moshi.adapter(ThreePidData::class.java)
fun map(entity: PendingSessionEntity?): PendingSessionData? {
if (entity == null) {
return null
}
val homeServerConnectionConfig = homeServerConnectionConfigAdapter.fromJson(entity.homeServerConnectionConfigJson)!!
val resetPasswordData = entity.resetPasswordDataJson?.let { resetPasswordDataAdapter.fromJson(it) }
val threePidData = entity.currentThreePidDataJson?.let { threePidDataAdapter.fromJson(it) }
return PendingSessionData(
homeServerConnectionConfig = homeServerConnectionConfig,
clientSecret = entity.clientSecret,
sendAttempt = entity.sendAttempt,
resetPasswordData = resetPasswordData,
currentSession = entity.currentSession,
isRegistrationStarted = entity.isRegistrationStarted,
currentThreePidData = threePidData)
}
fun map(sessionData: PendingSessionData?): PendingSessionEntity? {
if (sessionData == null) {
return null
}
val homeServerConnectionConfigJson = homeServerConnectionConfigAdapter.toJson(sessionData.homeServerConnectionConfig)
val resetPasswordDataJson = resetPasswordDataAdapter.toJson(sessionData.resetPasswordData)
val currentThreePidDataJson = threePidDataAdapter.toJson(sessionData.currentThreePidData)
return PendingSessionEntity(
homeServerConnectionConfigJson = homeServerConnectionConfigJson,
clientSecret = sessionData.clientSecret,
sendAttempt = sessionData.sendAttempt,
resetPasswordDataJson = resetPasswordDataJson,
currentSession = sessionData.currentSession,
isRegistrationStarted = sessionData.isRegistrationStarted,
currentThreePidDataJson = currentThreePidDataJson
)
}
}

View File

@ -0,0 +1,61 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.auth.db
import im.vector.matrix.android.internal.auth.PendingSessionStore
import im.vector.matrix.android.internal.database.awaitTransaction
import im.vector.matrix.android.internal.di.AuthDatabase
import io.realm.Realm
import io.realm.RealmConfiguration
import javax.inject.Inject
internal class RealmPendingSessionStore @Inject constructor(private val mapper: PendingSessionMapper,
@AuthDatabase
private val realmConfiguration: RealmConfiguration
) : PendingSessionStore {
override suspend fun savePendingSessionData(pendingSessionData: PendingSessionData) {
awaitTransaction(realmConfiguration) { realm ->
val entity = mapper.map(pendingSessionData)
if (entity != null) {
realm.where(PendingSessionEntity::class.java)
.findAll()
.deleteAllFromRealm()
realm.insert(entity)
}
}
}
override fun getPendingSessionData(): PendingSessionData? {
return Realm.getInstance(realmConfiguration).use { realm ->
realm
.where(PendingSessionEntity::class.java)
.findAll()
.map { mapper.map(it) }
.firstOrNull()
}
}
override suspend fun delete() {
awaitTransaction(realmConfiguration) {
it.where(PendingSessionEntity::class.java)
.findAll()
.deleteAllFromRealm()
}
}
}

View File

@ -22,6 +22,8 @@ import im.vector.matrix.android.internal.database.awaitTransaction
import im.vector.matrix.android.internal.di.AuthDatabase
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.exceptions.RealmPrimaryKeyConstraintException
import timber.log.Timber
import javax.inject.Inject
internal class RealmSessionParamsStore @Inject constructor(private val mapper: SessionParamsMapper,
@ -30,43 +32,45 @@ internal class RealmSessionParamsStore @Inject constructor(private val mapper: S
) : SessionParamsStore {
override fun getLast(): SessionParams? {
val realm = Realm.getInstance(realmConfiguration)
val sessionParams = realm
.where(SessionParamsEntity::class.java)
.findAll()
.map { mapper.map(it) }
.lastOrNull()
realm.close()
return sessionParams
return Realm.getInstance(realmConfiguration).use { realm ->
realm
.where(SessionParamsEntity::class.java)
.findAll()
.map { mapper.map(it) }
.lastOrNull()
}
}
override fun get(userId: String): SessionParams? {
val realm = Realm.getInstance(realmConfiguration)
val sessionParams = realm
.where(SessionParamsEntity::class.java)
.equalTo(SessionParamsEntityFields.USER_ID, userId)
.findAll()
.map { mapper.map(it) }
.firstOrNull()
realm.close()
return sessionParams
return Realm.getInstance(realmConfiguration).use { realm ->
realm
.where(SessionParamsEntity::class.java)
.equalTo(SessionParamsEntityFields.USER_ID, userId)
.findAll()
.map { mapper.map(it) }
.firstOrNull()
}
}
override fun getAll(): List<SessionParams> {
val realm = Realm.getInstance(realmConfiguration)
val sessionParams = realm
.where(SessionParamsEntity::class.java)
.findAll()
.mapNotNull { mapper.map(it) }
realm.close()
return sessionParams
return Realm.getInstance(realmConfiguration).use { realm ->
realm
.where(SessionParamsEntity::class.java)
.findAll()
.mapNotNull { mapper.map(it) }
}
}
override suspend fun save(sessionParams: SessionParams) {
awaitTransaction(realmConfiguration) {
val entity = mapper.map(sessionParams)
if (entity != null) {
it.insert(entity)
try {
it.insert(entity)
} catch (e: RealmPrimaryKeyConstraintException) {
Timber.e(e, "Something wrong happened during previous session creation. Override with new credentials")
it.insertOrUpdate(entity)
}
}
}
}

View File

@ -0,0 +1,130 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.auth.login
import android.util.Patterns
import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.login.LoginWizard
import im.vector.matrix.android.api.auth.registration.RegisterThreePid
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.NoOpCancellable
import im.vector.matrix.android.internal.auth.AuthAPI
import im.vector.matrix.android.internal.auth.PendingSessionStore
import im.vector.matrix.android.internal.auth.SessionCreator
import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
import im.vector.matrix.android.internal.auth.data.ThreePidMedium
import im.vector.matrix.android.internal.auth.db.PendingSessionData
import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationParams
import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationResponse
import im.vector.matrix.android.internal.auth.registration.RegisterAddThreePidTask
import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.launchToCallback
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
internal class DefaultLoginWizard(
okHttpClient: Lazy<OkHttpClient>,
retrofitFactory: RetrofitFactory,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val sessionCreator: SessionCreator,
private val pendingSessionStore: PendingSessionStore
) : LoginWizard {
private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here")
private val authAPI = retrofitFactory.create(okHttpClient, pendingSessionData.homeServerConnectionConfig.homeServerUri.toString())
.create(AuthAPI::class.java)
override fun login(login: String,
password: String,
deviceName: String,
callback: MatrixCallback<Session>): Cancelable {
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
loginInternal(login, password, deviceName)
}
}
private suspend fun loginInternal(login: String,
password: String,
deviceName: String) = withContext(coroutineDispatchers.computation) {
val loginParams = if (Patterns.EMAIL_ADDRESS.matcher(login).matches()) {
PasswordLoginParams.thirdPartyIdentifier(ThreePidMedium.EMAIL, login, password, deviceName)
} else {
PasswordLoginParams.userIdentifier(login, password, deviceName)
}
val credentials = executeRequest<Credentials> {
apiCall = authAPI.login(loginParams)
}
sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig)
}
override fun resetPassword(email: String, newPassword: String, callback: MatrixCallback<Unit>): Cancelable {
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
resetPasswordInternal(email, newPassword)
}
}
private suspend fun resetPasswordInternal(email: String, newPassword: String) {
val param = RegisterAddThreePidTask.Params(
RegisterThreePid.Email(email),
pendingSessionData.clientSecret,
pendingSessionData.sendAttempt
)
pendingSessionData = pendingSessionData.copy(sendAttempt = pendingSessionData.sendAttempt + 1)
.also { pendingSessionStore.savePendingSessionData(it) }
val result = executeRequest<AddThreePidRegistrationResponse> {
apiCall = authAPI.resetPassword(AddThreePidRegistrationParams.from(param))
}
pendingSessionData = pendingSessionData.copy(resetPasswordData = ResetPasswordData(newPassword, result))
.also { pendingSessionStore.savePendingSessionData(it) }
}
override fun resetPasswordMailConfirmed(callback: MatrixCallback<Unit>): Cancelable {
val safeResetPasswordData = pendingSessionData.resetPasswordData ?: run {
callback.onFailure(IllegalStateException("developer error, no reset password in progress"))
return NoOpCancellable
}
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
resetPasswordMailConfirmedInternal(safeResetPasswordData)
}
}
private suspend fun resetPasswordMailConfirmedInternal(resetPasswordData: ResetPasswordData) {
val param = ResetPasswordMailConfirmed.create(
pendingSessionData.clientSecret,
resetPasswordData.addThreePidRegistrationResponse.sid,
resetPasswordData.newPassword
)
executeRequest<Unit> {
apiCall = authAPI.resetPasswordMailConfirmed(param)
}
// Set to null?
// resetPasswordData = null
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.auth.login
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationResponse
/**
* Container to store the data when a reset password is in the email validation step
*/
@JsonClass(generateAdapter = true)
internal data class ResetPasswordData(
val newPassword: String,
val addThreePidRegistrationResponse: AddThreePidRegistrationResponse
)

View File

@ -0,0 +1,45 @@
/*
* Copyright 2014 OpenMarket Ltd
* Copyright 2017 Vector Creations Ltd
* Copyright 2018 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 im.vector.matrix.android.internal.auth.login
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.auth.registration.AuthParams
/**
* Class to pass parameters to reset the password once a email has been validated.
*/
@JsonClass(generateAdapter = true)
internal data class ResetPasswordMailConfirmed(
// authentication parameters
@Json(name = "auth")
val auth: AuthParams? = null,
// the new password
@Json(name = "new_password")
val newPassword: String? = null
) {
companion object {
fun create(clientSecret: String, sid: String, newPassword: String): ResetPasswordMailConfirmed {
return ResetPasswordMailConfirmed(
auth = AuthParams.createForResetPassword(clientSecret, sid),
newPassword = newPassword
)
}
}
}

View File

@ -0,0 +1,101 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.auth.registration
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.auth.registration.RegisterThreePid
/**
* Add a three Pid during authentication
*/
@JsonClass(generateAdapter = true)
internal data class AddThreePidRegistrationParams(
/**
* 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 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,
/**
* Optional. When the validation is completed, the identity server will redirect the user to this URL. This option is ignored when
* submitting 3PID validation information through a POST request.
*/
@Json(name = "next_link")
val nextLink: String? = null,
/**
* Required. The hostname of the identity server to communicate with. May optionally include a port.
* This parameter is ignored when the homeserver handles 3PID verification.
*/
@Json(name = "id_server")
val id_server: String? = null,
/* ==========================================================================================
* For emails
* ========================================================================================== */
/**
* Required. The email address to validate.
*/
@Json(name = "email")
val email: String? = null,
/* ==========================================================================================
* For Msisdn
* ========================================================================================== */
/**
* Required. The two-letter uppercase ISO country code that the number in phone_number should be parsed as if it were dialled from.
*/
@Json(name = "country")
val countryCode: String? = null,
/**
* Required. The phone number to validate.
*/
@Json(name = "phone_number")
val msisdn: String? = null
) {
companion object {
fun from(params: RegisterAddThreePidTask.Params): AddThreePidRegistrationParams {
return when (params.threePid) {
is RegisterThreePid.Email -> AddThreePidRegistrationParams(
email = params.threePid.email,
clientSecret = params.clientSecret,
sendAttempt = params.sendAttempt
)
is RegisterThreePid.Msisdn -> AddThreePidRegistrationParams(
msisdn = params.threePid.msisdn,
countryCode = params.threePid.countryCode,
clientSecret = params.clientSecret,
sendAttempt = params.sendAttempt
)
}
}
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.auth.registration
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class AddThreePidRegistrationResponse(
/**
* 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. 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,102 @@
/*
* Copyright 2018 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 im.vector.matrix.android.internal.auth.registration
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
/**
* Open class, parent to all possible authentication parameters
*/
@JsonClass(generateAdapter = true)
internal data class AuthParams(
@Json(name = "type")
val type: String,
/**
* Note: session can be null for reset password request
*/
@Json(name = "session")
val session: String?,
/**
* parameter for "m.login.recaptcha" type
*/
@Json(name = "response")
val captchaResponse: String? = null,
/**
* parameter for "m.login.email.identity" type
*/
@Json(name = "threepid_creds")
val threePidCredentials: ThreePidCredentials? = null
) {
companion object {
fun createForCaptcha(session: String, captchaResponse: String): AuthParams {
return AuthParams(
type = LoginFlowTypes.RECAPTCHA,
session = session,
captchaResponse = captchaResponse
)
}
fun createForEmailIdentity(session: String, threePidCredentials: ThreePidCredentials): AuthParams {
return AuthParams(
type = LoginFlowTypes.EMAIL_IDENTITY,
session = session,
threePidCredentials = threePidCredentials
)
}
/**
* Note that there is a bug in Synapse (I have to investigate where), but if we pass LoginFlowTypes.MSISDN,
* the homeserver answer with the login flow with MatrixError fields and not with a simple MatrixError 401.
*/
fun createForMsisdnIdentity(session: String, threePidCredentials: ThreePidCredentials): AuthParams {
return AuthParams(
type = LoginFlowTypes.MSISDN,
session = session,
threePidCredentials = threePidCredentials
)
}
fun createForResetPassword(clientSecret: String, sid: String): AuthParams {
return AuthParams(
type = LoginFlowTypes.EMAIL_IDENTITY,
session = null,
threePidCredentials = ThreePidCredentials(
clientSecret = clientSecret,
sid = sid
)
)
}
}
}
@JsonClass(generateAdapter = true)
data class ThreePidCredentials(
@Json(name = "client_secret")
val clientSecret: String? = null,
@Json(name = "id_server")
val idServer: String? = null,
@Json(name = "sid")
val sid: String? = null
)

View File

@ -0,0 +1,246 @@
/*
* Copyright 2018 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 im.vector.matrix.android.internal.auth.registration
import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.registration.RegisterThreePid
import im.vector.matrix.android.api.auth.registration.RegistrationResult
import im.vector.matrix.android.api.auth.registration.RegistrationWizard
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.Failure.RegistrationFlowError
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.NoOpCancellable
import im.vector.matrix.android.internal.auth.AuthAPI
import im.vector.matrix.android.internal.auth.PendingSessionStore
import im.vector.matrix.android.internal.auth.SessionCreator
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.auth.db.PendingSessionData
import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.task.launchToCallback
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import okhttp3.OkHttpClient
/**
* This class execute the registration request and is responsible to keep the session of interactive authentication
*/
internal class DefaultRegistrationWizard(
private val okHttpClient: Lazy<OkHttpClient>,
private val retrofitFactory: RetrofitFactory,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val sessionCreator: SessionCreator,
private val pendingSessionStore: PendingSessionStore
) : RegistrationWizard {
private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here")
private val authAPI = buildAuthAPI()
private val registerTask = DefaultRegisterTask(authAPI)
private val registerAddThreePidTask = DefaultRegisterAddThreePidTask(authAPI)
private val validateCodeTask = DefaultValidateCodeTask(authAPI)
override val currentThreePid: String?
get() {
return when (val threePid = pendingSessionData.currentThreePidData?.threePid) {
is RegisterThreePid.Email -> threePid.email
is RegisterThreePid.Msisdn -> {
// Take formatted msisdn if provided by the server
pendingSessionData.currentThreePidData?.addThreePidRegistrationResponse?.formattedMsisdn?.takeIf { it.isNotBlank() } ?: threePid.msisdn
}
null -> null
}
}
override val isRegistrationStarted: Boolean
get() = pendingSessionData.isRegistrationStarted
override fun getRegistrationFlow(callback: MatrixCallback<RegistrationResult>): Cancelable {
val params = RegistrationParams()
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
performRegistrationRequest(params)
}
}
override fun createAccount(userName: String,
password: String,
initialDeviceDisplayName: String?,
callback: MatrixCallback<RegistrationResult>): Cancelable {
val params = RegistrationParams(
username = userName,
password = password,
initialDeviceDisplayName = initialDeviceDisplayName
)
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
performRegistrationRequest(params)
.also {
pendingSessionData = pendingSessionData.copy(isRegistrationStarted = true)
.also { pendingSessionStore.savePendingSessionData(it) }
}
}
}
override fun performReCaptcha(response: String, callback: MatrixCallback<RegistrationResult>): Cancelable {
val safeSession = pendingSessionData.currentSession ?: run {
callback.onFailure(IllegalStateException("developer error, call createAccount() method first"))
return NoOpCancellable
}
val params = RegistrationParams(auth = AuthParams.createForCaptcha(safeSession, response))
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
performRegistrationRequest(params)
}
}
override fun acceptTerms(callback: MatrixCallback<RegistrationResult>): Cancelable {
val safeSession = pendingSessionData.currentSession ?: run {
callback.onFailure(IllegalStateException("developer error, call createAccount() method first"))
return NoOpCancellable
}
val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.TERMS, session = safeSession))
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
performRegistrationRequest(params)
}
}
override fun addThreePid(threePid: RegisterThreePid, callback: MatrixCallback<RegistrationResult>): Cancelable {
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
pendingSessionData = pendingSessionData.copy(currentThreePidData = null)
.also { pendingSessionStore.savePendingSessionData(it) }
sendThreePid(threePid)
}
}
override fun sendAgainThreePid(callback: MatrixCallback<RegistrationResult>): Cancelable {
val safeCurrentThreePid = pendingSessionData.currentThreePidData?.threePid ?: run {
callback.onFailure(IllegalStateException("developer error, call createAccount() method first"))
return NoOpCancellable
}
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
sendThreePid(safeCurrentThreePid)
}
}
private suspend fun sendThreePid(threePid: RegisterThreePid): RegistrationResult {
val safeSession = pendingSessionData.currentSession ?: throw IllegalStateException("developer error, call createAccount() method first")
val response = registerAddThreePidTask.execute(
RegisterAddThreePidTask.Params(
threePid,
pendingSessionData.clientSecret,
pendingSessionData.sendAttempt))
pendingSessionData = pendingSessionData.copy(sendAttempt = pendingSessionData.sendAttempt + 1)
.also { pendingSessionStore.savePendingSessionData(it) }
val params = RegistrationParams(
auth = if (threePid is RegisterThreePid.Email) {
AuthParams.createForEmailIdentity(safeSession,
ThreePidCredentials(
clientSecret = pendingSessionData.clientSecret,
sid = response.sid
)
)
} else {
AuthParams.createForMsisdnIdentity(safeSession,
ThreePidCredentials(
clientSecret = pendingSessionData.clientSecret,
sid = response.sid
)
)
}
)
// Store data
pendingSessionData = pendingSessionData.copy(currentThreePidData = ThreePidData.from(threePid, response, params))
.also { pendingSessionStore.savePendingSessionData(it) }
// and send the sid a first time
return performRegistrationRequest(params)
}
override fun checkIfEmailHasBeenValidated(delayMillis: Long, callback: MatrixCallback<RegistrationResult>): Cancelable {
val safeParam = pendingSessionData.currentThreePidData?.registrationParams ?: run {
callback.onFailure(IllegalStateException("developer error, no pending three pid"))
return NoOpCancellable
}
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
performRegistrationRequest(safeParam, delayMillis)
}
}
override fun handleValidateThreePid(code: String, callback: MatrixCallback<RegistrationResult>): Cancelable {
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
validateThreePid(code)
}
}
private suspend fun validateThreePid(code: String): RegistrationResult {
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 validationBody = ValidationCodeBody(
clientSecret = pendingSessionData.clientSecret,
sid = safeCurrentData.addThreePidRegistrationResponse.sid,
code = code
)
val validationResponse = validateCodeTask.execute(ValidateCodeTask.Params(url, validationBody))
if (validationResponse.success == true) {
// The entered code is correct
// Same than validate email
return performRegistrationRequest(registrationParams, 3_000)
} else {
// The code is not correct
throw Failure.SuccessError
}
}
override fun dummy(callback: MatrixCallback<RegistrationResult>): Cancelable {
val safeSession = pendingSessionData.currentSession ?: run {
callback.onFailure(IllegalStateException("developer error, call createAccount() method first"))
return NoOpCancellable
}
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.DUMMY, session = safeSession))
performRegistrationRequest(params)
}
}
private suspend fun performRegistrationRequest(registrationParams: RegistrationParams,
delayMillis: Long = 0): RegistrationResult {
delay(delayMillis)
val credentials = try {
registerTask.execute(RegisterTask.Params(registrationParams))
} catch (exception: Throwable) {
if (exception is RegistrationFlowError) {
pendingSessionData = pendingSessionData.copy(currentSession = exception.registrationFlowResponse.session)
.also { pendingSessionStore.savePendingSessionData(it) }
return RegistrationResult.FlowResponse(exception.registrationFlowResponse.toFlowResult())
} else {
throw exception
}
}
val session = sessionCreator.createSession(credentials, pendingSessionData.homeServerConnectionConfig)
return RegistrationResult.Success(session)
}
private fun buildAuthAPI(): AuthAPI {
val retrofit = retrofitFactory.create(okHttpClient, pendingSessionData.homeServerConnectionConfig.homeServerUri.toString())
return retrofit.create(AuthAPI::class.java)
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright 2018 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.androidsdk.rest.model.login
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
/**
* This class represent a localized privacy policy for registration Flow.
*/
@Parcelize
data class LocalizedFlowDataLoginTerms(
var policyName: String? = null,
var version: String? = null,
var localizedUrl: String? = null,
var localizedName: String? = null
) : Parcelable

View File

@ -0,0 +1,47 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.auth.registration
import im.vector.matrix.android.api.auth.registration.RegisterThreePid
import im.vector.matrix.android.internal.auth.AuthAPI
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
internal interface RegisterAddThreePidTask : Task<RegisterAddThreePidTask.Params, AddThreePidRegistrationResponse> {
data class Params(
val threePid: RegisterThreePid,
val clientSecret: String,
val sendAttempt: Int
)
}
internal class DefaultRegisterAddThreePidTask(private val authAPI: AuthAPI)
: RegisterAddThreePidTask {
override suspend fun execute(params: RegisterAddThreePidTask.Params): AddThreePidRegistrationResponse {
return executeRequest {
apiCall = authAPI.add3Pid(params.threePid.toPath(), AddThreePidRegistrationParams.from(params))
}
}
private fun RegisterThreePid.toPath(): String {
return when (this) {
is RegisterThreePid.Email -> "email"
is RegisterThreePid.Msisdn -> "msisdn"
}
}
}

View File

@ -0,0 +1,62 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.auth.registration
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.internal.auth.AuthAPI
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
internal interface RegisterTask : Task<RegisterTask.Params, Credentials> {
data class Params(
val registrationParams: RegistrationParams
)
}
internal class DefaultRegisterTask(private val authAPI: AuthAPI)
: RegisterTask {
override suspend fun execute(params: RegisterTask.Params): Credentials {
try {
return executeRequest {
apiCall = authAPI.register(params.registrationParams)
}
} catch (throwable: Throwable) {
if (throwable is Failure.OtherServerError && throwable.httpCode == 401) {
// Parse to get a RegistrationFlowResponse
val registrationFlowResponse = try {
MoshiProvider.providesMoshi()
.adapter(RegistrationFlowResponse::class.java)
.fromJson(throwable.errorBody)
} catch (e: Exception) {
null
}
// check if the server response can be cast
if (registrationFlowResponse != null) {
throw Failure.RegistrationFlowError(registrationFlowResponse)
} else {
throw throwable
}
} else {
// Other error
throw throwable
}
}
}
}

View File

@ -18,8 +18,12 @@ package im.vector.matrix.android.internal.auth.registration
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.auth.registration.FlowResult
import im.vector.matrix.android.api.auth.registration.Stage
import im.vector.matrix.android.api.auth.registration.TermPolicies
import im.vector.matrix.android.api.util.JsonDict
import im.vector.matrix.android.internal.auth.data.InteractiveAuthenticationFlow
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
@JsonClass(generateAdapter = true)
data class RegistrationFlowResponse(
@ -50,4 +54,46 @@ data class RegistrationFlowResponse(
*/
@Json(name = "params")
var params: JsonDict? = null
/**
* WARNING,
* The two MatrixError fields "errcode" and "error" can also be present here in case of error when validating a stage,
* But in this case Moshi will be able to parse the result as a MatrixError, see [RetrofitExtensions.toFailure]
* Ex: when polling for "m.login.msisdn" validation
*/
)
/**
* Convert to something easier to handle on client side
*/
fun RegistrationFlowResponse.toFlowResult(): FlowResult {
// Get all the returned stages
val allFlowTypes = mutableSetOf<String>()
val missingStage = mutableListOf<Stage>()
val completedStage = mutableListOf<Stage>()
this.flows?.forEach { it.stages?.mapTo(allFlowTypes) { type -> type } }
allFlowTypes.forEach { type ->
val isMandatory = flows?.all { type in it.stages ?: emptyList() } == true
val stage = when (type) {
LoginFlowTypes.RECAPTCHA -> Stage.ReCaptcha(isMandatory, ((params?.get(type) as? Map<*, *>)?.get("public_key") as? String)
?: "")
LoginFlowTypes.DUMMY -> Stage.Dummy(isMandatory)
LoginFlowTypes.TERMS -> Stage.Terms(isMandatory, params?.get(type) as? TermPolicies ?: emptyMap<String, String>())
LoginFlowTypes.EMAIL_IDENTITY -> Stage.Email(isMandatory)
LoginFlowTypes.MSISDN -> Stage.Msisdn(isMandatory)
else -> Stage.Other(isMandatory, type, (params?.get(type) as? Map<*, *>))
}
if (type in completedStages ?: emptyList()) {
completedStage.add(stage)
} else {
missingStage.add(stage)
}
}
return FlowResult(missingStage, completedStage)
}

View File

@ -0,0 +1,47 @@
/*
* Copyright 2014 OpenMarket Ltd
* Copyright 2017 Vector Creations Ltd
* Copyright 2018 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 im.vector.matrix.android.internal.auth.registration
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Class to pass parameters to the different registration types for /register.
*/
@JsonClass(generateAdapter = true)
internal data class RegistrationParams(
// authentication parameters
@Json(name = "auth")
val auth: AuthParams? = null,
// the account username
@Json(name = "username")
val username: String? = null,
// the account password
@Json(name = "password")
val password: String? = null,
// device name
@Json(name = "initial_device_display_name")
val initialDeviceDisplayName: String? = null,
// 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
)

View File

@ -0,0 +1,26 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.auth.registration
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class SuccessResult(
@Json(name = "success")
val success: Boolean?
)

View File

@ -0,0 +1,54 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.auth.registration
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.auth.registration.RegisterThreePid
/**
* Container to store the data when a three pid is in validation step
*/
@JsonClass(generateAdapter = true)
internal data class ThreePidData(
val email: String,
val msisdn: String,
val country: String,
val addThreePidRegistrationResponse: AddThreePidRegistrationResponse,
val registrationParams: RegistrationParams
) {
val threePid: RegisterThreePid
get() {
return if (email.isNotBlank()) {
RegisterThreePid.Email(email)
} else {
RegisterThreePid.Msisdn(msisdn, country)
}
}
companion object {
fun from(threePid: RegisterThreePid,
addThreePidRegistrationResponse: AddThreePidRegistrationResponse,
registrationParams: RegistrationParams): ThreePidData {
return when (threePid) {
is RegisterThreePid.Email ->
ThreePidData(threePid.email, "", "", addThreePidRegistrationResponse, registrationParams)
is RegisterThreePid.Msisdn ->
ThreePidData("", threePid.msisdn, threePid.countryCode, addThreePidRegistrationResponse, registrationParams)
}
}
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.auth.registration
import im.vector.matrix.android.internal.auth.AuthAPI
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
internal interface ValidateCodeTask : Task<ValidateCodeTask.Params, SuccessResult> {
data class Params(
val url: String,
val body: ValidationCodeBody
)
}
internal class DefaultValidateCodeTask(private val authAPI: AuthAPI)
: ValidateCodeTask {
override suspend fun execute(params: ValidateCodeTask.Params): SuccessResult {
return executeRequest {
apiCall = authAPI.validate3Pid(params.url, params.body)
}
}
}

View File

@ -0,0 +1,35 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.auth.registration
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* This object is used to send a code received by SMS to validate Msisdn ownership
*/
@JsonClass(generateAdapter = true)
data class ValidationCodeBody(
@Json(name = "client_secret")
val clientSecret: String,
@Json(name = "sid")
val sid: String,
@Json(name = "token")
val code: String
)

View File

@ -37,6 +37,8 @@ import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.cache.ClearCacheTask
import im.vector.matrix.android.internal.session.cache.RealmClearCacheTask
import io.realm.RealmConfiguration
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import retrofit2.Retrofit
import java.io.File
@ -66,6 +68,13 @@ internal abstract class CryptoModule {
.build()
}
@JvmStatic
@Provides
@SessionScope
fun providesCryptoCoroutineScope(): CoroutineScope {
return CoroutineScope(SupervisorJob())
}
@JvmStatic
@Provides
@CryptoDatabase

View File

@ -132,7 +132,8 @@ internal class DefaultCryptoService @Inject constructor(
private val loadRoomMembersTask: LoadRoomMembersTask,
private val monarchy: Monarchy,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val taskExecutor: TaskExecutor
private val taskExecutor: TaskExecutor,
private val cryptoCoroutineScope: CoroutineScope
) : CryptoService {
private val uiHandler = Handler(Looper.getMainLooper())
@ -243,7 +244,8 @@ internal class DefaultCryptoService @Inject constructor(
return
}
isStarting.set(true)
GlobalScope.launch(coroutineDispatchers.crypto) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
internalStart(isInitialSync)
}
}
@ -269,10 +271,9 @@ internal class DefaultCryptoService @Inject constructor(
isStarted.set(true)
},
{
Timber.e("Start failed: $it")
delay(1000)
isStarting.set(false)
internalStart(isInitialSync)
isStarted.set(false)
Timber.e(it, "Start failed")
}
)
}
@ -281,9 +282,12 @@ internal class DefaultCryptoService @Inject constructor(
* Close the crypto
*/
fun close() = runBlocking(coroutineDispatchers.crypto) {
cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module"))
outgoingRoomKeyRequestManager.stop()
olmDevice.release()
cryptoStore.close()
outgoingRoomKeyRequestManager.stop()
}
// Aways enabled on RiotX
@ -305,19 +309,21 @@ internal class DefaultCryptoService @Inject constructor(
* @param syncResponse the syncResponse
*/
fun onSyncCompleted(syncResponse: SyncResponse) {
GlobalScope.launch(coroutineDispatchers.crypto) {
if (syncResponse.deviceLists != null) {
deviceListManager.handleDeviceListsChanges(syncResponse.deviceLists.changed, syncResponse.deviceLists.left)
}
if (syncResponse.deviceOneTimeKeysCount != null) {
val currentCount = syncResponse.deviceOneTimeKeysCount.signedCurve25519 ?: 0
oneTimeKeysUploader.updateOneTimeKeyCount(currentCount)
}
if (isStarted()) {
// Make sure we process to-device messages before generating new one-time-keys #2782
deviceListManager.refreshOutdatedDeviceLists()
oneTimeKeysUploader.maybeUploadOneTimeKeys()
incomingRoomKeyRequestManager.processReceivedRoomKeyRequests()
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
runCatching {
if (syncResponse.deviceLists != null) {
deviceListManager.handleDeviceListsChanges(syncResponse.deviceLists.changed, syncResponse.deviceLists.left)
}
if (syncResponse.deviceOneTimeKeysCount != null) {
val currentCount = syncResponse.deviceOneTimeKeysCount.signedCurve25519 ?: 0
oneTimeKeysUploader.updateOneTimeKeyCount(currentCount)
}
if (isStarted()) {
// Make sure we process to-device messages before generating new one-time-keys #2782
deviceListManager.refreshOutdatedDeviceLists()
oneTimeKeysUploader.maybeUploadOneTimeKeys()
incomingRoomKeyRequestManager.processReceivedRoomKeyRequests()
}
}
}
}
@ -511,7 +517,7 @@ internal class DefaultCryptoService @Inject constructor(
eventType: String,
roomId: String,
callback: MatrixCallback<MXEncryptEventContentResult>) {
GlobalScope.launch(coroutineDispatchers.crypto) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
if (!isStarted()) {
Timber.v("## encryptEventContent() : wait after e2e init")
internalStart(false)
@ -571,7 +577,7 @@ internal class DefaultCryptoService @Inject constructor(
* @param callback the callback to return data or null
*/
override fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) {
GlobalScope.launch {
cryptoCoroutineScope.launch {
val result = runCatching {
withContext(coroutineDispatchers.crypto) {
internalDecryptEvent(event, timeline)
@ -621,7 +627,7 @@ internal class DefaultCryptoService @Inject constructor(
* @param event the event
*/
fun onToDeviceEvent(event: Event) {
GlobalScope.launch(coroutineDispatchers.crypto) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
when (event.getClearType()) {
EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> {
onRoomKeyEvent(event)
@ -661,7 +667,7 @@ internal class DefaultCryptoService @Inject constructor(
* @param event the encryption event.
*/
private fun onRoomEncryptionEvent(roomId: String, event: Event) {
GlobalScope.launch(coroutineDispatchers.crypto) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
val params = LoadRoomMembersTask.Params(roomId)
try {
loadRoomMembersTask.execute(params)
@ -753,7 +759,7 @@ internal class DefaultCryptoService @Inject constructor(
* @param callback the exported keys
*/
override fun exportRoomKeys(password: String, callback: MatrixCallback<ByteArray>) {
GlobalScope.launch(coroutineDispatchers.main) {
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
runCatching {
exportRoomKeys(password, MXMegolmExportEncryption.DEFAULT_ITERATION_COUNT)
}.foldToCallback(callback)
@ -791,7 +797,7 @@ internal class DefaultCryptoService @Inject constructor(
password: String,
progressListener: ProgressListener?,
callback: MatrixCallback<ImportRoomKeysResult>) {
GlobalScope.launch(coroutineDispatchers.main) {
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
runCatching {
withContext(coroutineDispatchers.crypto) {
Timber.v("## importRoomKeys starts")
@ -839,7 +845,7 @@ internal class DefaultCryptoService @Inject constructor(
*/
fun checkUnknownDevices(userIds: List<String>, callback: MatrixCallback<Unit>) {
// force the refresh to ensure that the devices list is up-to-date
GlobalScope.launch(coroutineDispatchers.crypto) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
runCatching {
val keys = deviceListManager.downloadKeys(userIds, true)
val unknownDevices = getUnknownDevices(keys)
@ -999,7 +1005,7 @@ internal class DefaultCryptoService @Inject constructor(
}
override fun downloadKeys(userIds: List<String>, forceDownload: Boolean, callback: MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>>) {
GlobalScope.launch(coroutineDispatchers.crypto) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
runCatching {
deviceListManager.downloadKeys(userIds, forceDownload)
}.foldToCallback(callback)

View File

@ -25,7 +25,6 @@ import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.session.SessionScope
import timber.log.Timber
import javax.inject.Inject
import kotlin.collections.ArrayList
@SessionScope
internal class IncomingRoomKeyRequestManager @Inject constructor(
@ -51,7 +50,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
*
* @param event the announcement event.
*/
suspend fun onRoomKeyRequestEvent(event: Event) {
fun onRoomKeyRequestEvent(event: Event) {
val roomKeyShare = event.getClearContent().toModel<RoomKeyShare>()
when (roomKeyShare?.action) {
RoomKeyShare.ACTION_SHARE_REQUEST -> receivedRoomKeyRequests.add(IncomingRoomKeyRequest(event))
@ -78,7 +77,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
Timber.v("m.room_key_request from $userId:$deviceId for $roomId / ${body.sessionId} id ${request.requestId}")
if (userId == null || credentials.userId != userId) {
// TODO: determine if we sent this device the keys already: in
Timber.e("## processReceivedRoomKeyRequests() : Ignoring room key request from other user for now")
Timber.w("## processReceivedRoomKeyRequests() : Ignoring room key request from other user for now")
return
}
// TODO: should we queue up requests we don't yet have keys for, in case they turn up later?
@ -86,11 +85,11 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
// the keys for the requested events, and can drop the requests.
val decryptor = roomDecryptorProvider.getRoomDecryptor(roomId, alg)
if (null == decryptor) {
Timber.e("## processReceivedRoomKeyRequests() : room key request for unknown $alg in room $roomId")
Timber.w("## processReceivedRoomKeyRequests() : room key request for unknown $alg in room $roomId")
continue
}
if (!decryptor.hasKeysForKeyRequest(request)) {
Timber.e("## processReceivedRoomKeyRequests() : room key request for unknown session ${body.sessionId!!}")
Timber.w("## processReceivedRoomKeyRequests() : room key request for unknown session ${body.sessionId!!}")
cryptoStore.deleteIncomingRoomKeyRequest(request)
continue
}
@ -139,7 +138,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
if (null != receivedRoomKeyRequestCancellations) {
for (request in receivedRoomKeyRequestCancellations!!) {
Timber.v("## ## processReceivedRoomKeyRequests() : m.room_key_request cancellation for " + request.userId
+ ":" + request.deviceId + " id " + request.requestId)
+ ":" + request.deviceId + " id " + request.requestId)
// we should probably only notify the app of cancellations we told it
// about, but we don't currently have a record of that, so we just pass

View File

@ -764,7 +764,7 @@ internal class MXOlmDevice @Inject constructor(
return session
}
} else {
Timber.e("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId")
Timber.w("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId")
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON)
}
}

View File

@ -42,8 +42,6 @@ internal class OneTimeKeysUploader @Inject constructor(
private var lastOneTimeKeyCheck: Long = 0
private var oneTimeKeyCount: Int? = null
private var lastPublishedOneTimeKeys: Map<String, Map<String, String>>? = null
/**
* Stores the current one_time_key count which will be handled later (in a call of
* _onSyncCompleted). The count is e.g. coming from a /sync response.
@ -59,10 +57,12 @@ internal class OneTimeKeysUploader @Inject constructor(
*/
suspend fun maybeUploadOneTimeKeys() {
if (oneTimeKeyCheckInProgress) {
Timber.v("maybeUploadOneTimeKeys: already in progress")
return
}
if (System.currentTimeMillis() - lastOneTimeKeyCheck < ONE_TIME_KEY_UPLOAD_PERIOD) {
// we've done a key upload recently.
Timber.v("maybeUploadOneTimeKeys: executed too recently")
return
}
@ -79,12 +79,8 @@ internal class OneTimeKeysUploader @Inject constructor(
// discard the oldest private keys first. This will eventually clean
// out stale private keys that won't receive a message.
val keyLimit = floor(maxOneTimeKeys / 2.0).toInt()
if (oneTimeKeyCount != null) {
uploadOTK(oneTimeKeyCount!!, keyLimit)
} else {
// ask the server how many keys we have
val uploadKeysParams = UploadKeysTask.Params(null, null, credentials.deviceId!!)
val response = uploadKeysTask.execute(uploadKeysParams)
val oneTimeKeyCountFromSync = oneTimeKeyCount
if (oneTimeKeyCountFromSync != null) {
// We need to keep a pool of one time public keys on the server so that
// other devices can start conversations with us. But we can only store
// a finite number of private keys in the olm Account object.
@ -96,14 +92,17 @@ internal class OneTimeKeysUploader @Inject constructor(
// private keys clogging up our local storage.
// So we need some kind of engineering compromise to balance all of
// these factors.
// TODO Why we do not set oneTimeKeyCount here?
// TODO This is not needed anymore, see https://github.com/matrix-org/matrix-js-sdk/pull/493 (TODO on iOS also)
val keyCount = response.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)
uploadOTK(keyCount, keyLimit)
try {
val uploadedKeys = uploadOTK(oneTimeKeyCountFromSync, keyLimit)
Timber.v("## uploadKeys() : success, $uploadedKeys key(s) sent")
} finally {
oneTimeKeyCheckInProgress = false
}
} else {
Timber.w("maybeUploadOneTimeKeys: waiting to know the number of OTK from the sync")
oneTimeKeyCheckInProgress = false
lastOneTimeKeyCheck = 0
}
Timber.v("## uploadKeys() : success")
oneTimeKeyCount = null
oneTimeKeyCheckInProgress = false
}
/**
@ -111,53 +110,51 @@ internal class OneTimeKeysUploader @Inject constructor(
*
* @param keyCount the key count
* @param keyLimit the limit
* @return the number of uploaded keys
*/
private suspend fun uploadOTK(keyCount: Int, keyLimit: Int) {
private suspend fun uploadOTK(keyCount: Int, keyLimit: Int): Int {
if (keyLimit <= keyCount) {
// If we don't need to generate any more keys then we are done.
return
return 0
}
val keysThisLoop = min(keyLimit - keyCount, ONE_TIME_KEY_GENERATION_MAX_NUMBER)
olmDevice.generateOneTimeKeys(keysThisLoop)
val response = uploadOneTimeKeys()
val response = uploadOneTimeKeys(olmDevice.getOneTimeKeys())
olmDevice.markKeysAsPublished()
if (response.hasOneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)) {
uploadOTK(response.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE), keyLimit)
// Maybe upload other keys
return keysThisLoop + uploadOTK(response.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE), keyLimit)
} else {
Timber.e("## uploadLoop() : response for uploading keys does not contain one_time_key_counts.signed_curve25519")
Timber.e("## uploadOTK() : response for uploading keys does not contain one_time_key_counts.signed_curve25519")
throw Exception("response for uploading keys does not contain one_time_key_counts.signed_curve25519")
}
}
/**
* Upload my user's one time keys.
* Upload curve25519 one time keys.
*/
private suspend fun uploadOneTimeKeys(): KeysUploadResponse {
val oneTimeKeys = olmDevice.getOneTimeKeys()
private suspend fun uploadOneTimeKeys(oneTimeKeys: Map<String, Map<String, String>>?): KeysUploadResponse {
val oneTimeJson = mutableMapOf<String, Any>()
val curve25519Map = oneTimeKeys?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY)
val curve25519Map = oneTimeKeys?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY) ?: emptyMap()
if (null != curve25519Map) {
for ((key_id, value) in curve25519Map) {
val k = mutableMapOf<String, Any>()
k["key"] = value
curve25519Map.forEach { (key_id, value) ->
val k = mutableMapOf<String, Any>()
k["key"] = value
// the key is also signed
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, k)
// the key is also signed
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, k)
k["signatures"] = objectSigner.signObject(canonicalJson)
k["signatures"] = objectSigner.signObject(canonicalJson)
oneTimeJson["signed_curve25519:$key_id"] = k
}
oneTimeJson["signed_curve25519:$key_id"] = k
}
// For now, we set the device id explicitly, as we may not be using the
// same one as used in login.
val uploadParams = UploadKeysTask.Params(null, oneTimeJson, credentials.deviceId!!)
val response = uploadKeysTask.execute(uploadParams)
lastPublishedOneTimeKeys = oneTimeKeys
olmDevice.markKeysAsPublished()
return response
return uploadKeysTask.execute(uploadParams)
}
companion object {

View File

@ -63,6 +63,7 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor(
*/
fun stop() {
isClientRunning = false
stopTimer()
}
/**
@ -171,6 +172,10 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor(
}, SEND_KEY_REQUESTS_DELAY_MS.toLong())
}
private fun stopTimer() {
BACKGROUND_HANDLER.removeCallbacksAndMessages(null)
}
// look for and send any queued requests. Runs itself recursively until
// there are no more requests, or there is an error (in which case, the
// timer will be restarted before the promise resolves).
@ -187,7 +192,7 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor(
OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND))
if (null == outgoingRoomKeyRequest) {
Timber.e("## sendOutgoingRoomKeyRequests() : No more outgoing room key requests")
Timber.v("## sendOutgoingRoomKeyRequests() : No more outgoing room key requests")
sendOutgoingRoomKeyRequestsRunning.set(false)
return
}

View File

@ -34,7 +34,7 @@ import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
@ -46,8 +46,9 @@ internal class MXMegolmDecryption(private val userId: String,
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
private val cryptoStore: IMXCryptoStore,
private val sendToDeviceTask: SendToDeviceTask,
private val coroutineDispatchers: MatrixCoroutineDispatchers)
: IMXDecrypting {
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope
) : IMXDecrypting {
var newSessionListener: NewSessionListener? = null
@ -61,7 +62,7 @@ internal class MXMegolmDecryption(private val userId: String,
return decryptEvent(event, timeline, true)
}
private suspend fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult {
private fun decryptEvent(event: Event, timeline: String, requestKeysOnFail: Boolean): MXEventDecryptionResult {
if (event.roomId.isNullOrBlank()) {
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
}
@ -292,7 +293,7 @@ internal class MXMegolmDecryption(private val userId: String,
return
}
val userId = request.userId ?: return
GlobalScope.launch(coroutineDispatchers.crypto) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
runCatching { deviceListManager.downloadKeys(listOf(userId), false) }
.mapCatching {
val deviceId = request.deviceId

View File

@ -25,17 +25,21 @@ import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope
import javax.inject.Inject
internal class MXMegolmDecryptionFactory @Inject constructor(@UserId private val userId: String,
private val olmDevice: MXOlmDevice,
private val deviceListManager: DeviceListManager,
private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager,
private val messageEncrypter: MessageEncrypter,
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
private val cryptoStore: IMXCryptoStore,
private val sendToDeviceTask: SendToDeviceTask,
private val coroutineDispatchers: MatrixCoroutineDispatchers) {
internal class MXMegolmDecryptionFactory @Inject constructor(
@UserId private val userId: String,
private val olmDevice: MXOlmDevice,
private val deviceListManager: DeviceListManager,
private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager,
private val messageEncrypter: MessageEncrypter,
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
private val cryptoStore: IMXCryptoStore,
private val sendToDeviceTask: SendToDeviceTask,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope
) {
fun create(): MXMegolmDecryption {
return MXMegolmDecryption(
@ -47,6 +51,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor(@UserId private val
ensureOlmSessionsForDevicesAction,
cryptoStore,
sendToDeviceTask,
coroutineDispatchers)
coroutineDispatchers,
cryptoCoroutineScope)
}
}

View File

@ -59,7 +59,7 @@ import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.JsonCanonicalizer
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.awaitCallback
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.olm.OlmException
@ -102,7 +102,8 @@ internal class KeysBackup @Inject constructor(
private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask,
// Task executor
private val taskExecutor: TaskExecutor,
private val coroutineDispatchers: MatrixCoroutineDispatchers
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope
) : KeysBackupService {
private val uiHandler = Handler(Looper.getMainLooper())
@ -143,7 +144,7 @@ internal class KeysBackup @Inject constructor(
override fun prepareKeysBackupVersion(password: String?,
progressListener: ProgressListener?,
callback: MatrixCallback<MegolmBackupCreationInfo>) {
GlobalScope.launch(coroutineDispatchers.main) {
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
runCatching {
withContext(coroutineDispatchers.crypto) {
val olmPkDecryption = OlmPkDecryption()
@ -233,7 +234,7 @@ internal class KeysBackup @Inject constructor(
}
override fun deleteBackup(version: String, callback: MatrixCallback<Unit>?) {
GlobalScope.launch(coroutineDispatchers.main) {
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
withContext(coroutineDispatchers.crypto) {
// If we're currently backing up to this backup... stop.
// (We start using it automatically in createKeysBackupVersion so this is symmetrical).
@ -344,9 +345,7 @@ internal class KeysBackup @Inject constructor(
}
})
}
}
keysBackupStateManager.addListener(keysBackupStateListener!!)
}.also { keysBackupStateManager.addListener(it) }
backupKeys()
}
@ -448,7 +447,7 @@ internal class KeysBackup @Inject constructor(
callback.onFailure(IllegalArgumentException("Missing element"))
} else {
GlobalScope.launch(coroutineDispatchers.main) {
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) {
// Get current signatures, or create an empty set
val myUserSignatures = authData.signatures?.get(userId)?.toMutableMap()
@ -523,7 +522,7 @@ internal class KeysBackup @Inject constructor(
callback: MatrixCallback<Unit>) {
Timber.v("trustKeysBackupVersionWithRecoveryKey: version ${keysBackupVersion.version}")
GlobalScope.launch(coroutineDispatchers.main) {
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
val isValid = withContext(coroutineDispatchers.crypto) {
isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)
}
@ -543,7 +542,7 @@ internal class KeysBackup @Inject constructor(
callback: MatrixCallback<Unit>) {
Timber.v("trustKeysBackupVersionWithPassphrase: version ${keysBackupVersion.version}")
GlobalScope.launch(coroutineDispatchers.main) {
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
val recoveryKey = withContext(coroutineDispatchers.crypto) {
recoveryKeyFromPassword(password, keysBackupVersion, null)
}
@ -614,7 +613,7 @@ internal class KeysBackup @Inject constructor(
callback: MatrixCallback<ImportRoomKeysResult>) {
Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}")
GlobalScope.launch(coroutineDispatchers.main) {
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
runCatching {
val decryption = withContext(coroutineDispatchers.crypto) {
// Check if the recovery is valid before going any further
@ -695,7 +694,7 @@ internal class KeysBackup @Inject constructor(
callback: MatrixCallback<ImportRoomKeysResult>) {
Timber.v("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}")
GlobalScope.launch(coroutineDispatchers.main) {
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
runCatching {
val progressListener = if (stepProgressListener != null) {
object : ProgressListener {
@ -729,8 +728,8 @@ internal class KeysBackup @Inject constructor(
* parameters and always returns a KeysBackupData object through the Callback
*/
private suspend fun getKeys(sessionId: String?,
roomId: String?,
version: String): KeysBackupData {
roomId: String?,
version: String): KeysBackupData {
return if (roomId != null && sessionId != null) {
// Get key for the room and for the session
val data = getRoomSessionDataTask.execute(GetRoomSessionDataTask.Params(roomId, sessionId, version))
@ -1154,7 +1153,7 @@ internal class KeysBackup @Inject constructor(
keysBackupStateManager.state = KeysBackupState.BackingUp
GlobalScope.launch(coroutineDispatchers.main) {
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
withContext(coroutineDispatchers.crypto) {
Timber.v("backupKeys: 2 - Encrypting keys")

View File

@ -53,10 +53,10 @@ internal class DefaultUploadKeysTask @Inject constructor(private val cryptoApi:
}
return executeRequest {
if (encodedDeviceId.isNullOrBlank()) {
apiCall = cryptoApi.uploadKeys(body)
apiCall = if (encodedDeviceId.isBlank()) {
cryptoApi.uploadKeys(body)
} else {
apiCall = cryptoApi.uploadKeys(encodedDeviceId, body)
cryptoApi.uploadKeys(encodedDeviceId, body)
}
}
}

View File

@ -83,7 +83,7 @@ internal class SessionRealmConfigurationFactory @Inject constructor(private val
try {
File(directory, file).deleteRecursively()
} catch (e: Exception) {
Timber.e(e, "Unable to move files")
Timber.e(e, "Unable to delete files")
}
}
}

View File

@ -23,7 +23,6 @@ import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.mapper.toEntity
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
@ -140,7 +139,7 @@ internal fun ChunkEntity.add(roomId: String,
val senderId = event.senderId ?: ""
val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst()
?: ReadReceiptsSummaryEntity(eventId, roomId)
?: ReadReceiptsSummaryEntity(eventId, roomId)
// Update RR for the sender of a new message with a dummy one
@ -168,7 +167,6 @@ internal fun ChunkEntity.add(roomId: String,
it.roomId = roomId
it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
it.readReceipts = readReceiptsSummaryEntity
it.readMarker = ReadMarkerEntity.where(realm, roomId = roomId, eventId = eventId).findFirst()
}
val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size
timelineEvents.add(position, eventEntity)
@ -176,14 +174,14 @@ internal fun ChunkEntity.add(roomId: String,
internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
return when (direction) {
PaginationDirection.FORWARDS -> forwardsDisplayIndex
PaginationDirection.BACKWARDS -> backwardsDisplayIndex
} ?: defaultValue
PaginationDirection.FORWARDS -> forwardsDisplayIndex
PaginationDirection.BACKWARDS -> backwardsDisplayIndex
} ?: defaultValue
}
internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
return when (direction) {
PaginationDirection.FORWARDS -> forwardsStateIndex
PaginationDirection.BACKWARDS -> backwardsStateIndex
} ?: defaultValue
PaginationDirection.FORWARDS -> forwardsStateIndex
PaginationDirection.BACKWARDS -> backwardsStateIndex
} ?: defaultValue
}

View File

@ -36,7 +36,7 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS
}
return TimelineEvent(
root = timelineEventEntity.root?.asDomain()
?: Event("", timelineEventEntity.eventId),
?: Event("", timelineEventEntity.eventId),
annotations = timelineEventEntity.annotations?.asDomain(),
localId = timelineEventEntity.localId,
displayIndex = timelineEventEntity.root?.displayIndex ?: 0,
@ -45,8 +45,7 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS
senderAvatar = timelineEventEntity.senderAvatar,
readReceipts = readReceipts?.sortedByDescending {
it.originServerTs
} ?: emptyList(),
hasReadMarker = timelineEventEntity.readMarker?.eventId?.isNotEmpty() == true
} ?: emptyList()
)
}
}

View File

@ -17,8 +17,6 @@
package im.vector.matrix.android.internal.database.model
import io.realm.RealmObject
import io.realm.RealmResults
import io.realm.annotations.LinkingObjects
import io.realm.annotations.PrimaryKey
internal open class ReadMarkerEntity(
@ -27,8 +25,5 @@ internal open class ReadMarkerEntity(
var eventId: String = ""
) : RealmObject() {
@LinkingObjects("readMarker")
val timelineEvent: RealmResults<TimelineEventEntity>? = null
companion object
}

View File

@ -30,8 +30,7 @@ internal open class TimelineEventEntity(var localId: Long = 0,
var isUniqueDisplayName: Boolean = false,
var senderAvatar: String? = null,
var senderMembershipEvent: EventEntity? = null,
var readReceipts: ReadReceiptsSummaryEntity? = null,
var readMarker: ReadMarkerEntity? = null
var readReceipts: ReadReceiptsSummaryEntity? = null
) : RealmObject() {
@LinkingObjects("timelineEvents")

View File

@ -16,26 +16,27 @@
package im.vector.matrix.android.internal.database.query
import im.vector.matrix.android.internal.database.awaitTransaction
import im.vector.matrix.android.internal.database.model.FilterEntity
import im.vector.matrix.android.internal.session.filter.FilterFactory
import io.realm.Realm
import io.realm.kotlin.createObject
import io.realm.kotlin.where
/**
* Get the current filter
*/
internal fun FilterEntity.Companion.get(realm: Realm): FilterEntity? {
return realm.where<FilterEntity>().findFirst()
}
/**
* Get the current filter, create one if it does not exist
*/
internal suspend fun FilterEntity.Companion.getFilter(realm: Realm): FilterEntity {
var filter = realm.where<FilterEntity>().findFirst()
if (filter == null) {
filter = FilterEntity().apply {
filterBodyJson = FilterFactory.createDefaultFilterBody().toJSONString()
roomEventFilterJson = FilterFactory.createDefaultRoomFilter().toJSONString()
filterId = ""
}
awaitTransaction(realm.configuration) {
it.insert(filter)
}
}
return filter
internal fun FilterEntity.Companion.getOrCreate(realm: Realm): FilterEntity {
return get(realm) ?: realm.createObject<FilterEntity>()
.apply {
filterBodyJson = FilterFactory.createDefaultFilterBody().toJSONString()
roomEventFilterJson = FilterFactory.createDefaultRoomFilter().toJSONString()
filterId = ""
}
}

View File

@ -22,13 +22,9 @@ import io.realm.Realm
import io.realm.RealmQuery
import io.realm.kotlin.where
internal fun ReadMarkerEntity.Companion.where(realm: Realm, roomId: String, eventId: String? = null): RealmQuery<ReadMarkerEntity> {
val query = realm.where<ReadMarkerEntity>()
internal fun ReadMarkerEntity.Companion.where(realm: Realm, roomId: String): RealmQuery<ReadMarkerEntity> {
return realm.where<ReadMarkerEntity>()
.equalTo(ReadMarkerEntityFields.ROOM_ID, roomId)
if (eventId != null) {
query.equalTo(ReadMarkerEntityFields.EVENT_ID, eventId)
}
return query
}
internal fun ReadMarkerEntity.Companion.getOrCreate(realm: Realm, roomId: String): ReadMarkerEntity {

View File

@ -18,7 +18,9 @@ package im.vector.matrix.android.internal.database.query
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.model.LocalEcho
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
import io.realm.Realm
internal fun isEventRead(monarchy: Monarchy,
userId: String?,
@ -39,8 +41,10 @@ internal fun isEventRead(monarchy: Monarchy,
isEventRead = if (eventToCheck?.sender == userId) {
true
} else {
val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst() ?: return@doWithRealm
val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex ?: Int.MIN_VALUE
val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst()
?: return@doWithRealm
val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex
?: Int.MIN_VALUE
val eventToCheckIndex = eventToCheck?.displayIndex ?: Int.MAX_VALUE
eventToCheckIndex <= readReceiptIndex
@ -49,3 +53,21 @@ internal fun isEventRead(monarchy: Monarchy,
return isEventRead
}
internal fun isReadMarkerMoreRecent(monarchy: Monarchy,
roomId: String?,
eventId: String?): Boolean {
if (roomId.isNullOrBlank() || eventId.isNullOrBlank()) {
return false
}
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) ?: return false
val eventToCheck = liveChunk.timelineEvents.find(eventId)?.root
val readMarker = ReadMarkerEntity.where(realm, roomId).findFirst() ?: return false
val readMarkerIndex = liveChunk.timelineEvents.find(readMarker.eventId)?.root?.displayIndex
?: Int.MIN_VALUE
val eventToCheckIndex = eventToCheck?.displayIndex ?: Int.MAX_VALUE
eventToCheckIndex <= readMarkerIndex
}
}

View File

@ -22,7 +22,7 @@ import com.squareup.moshi.Moshi
import dagger.BindsInstance
import dagger.Component
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.auth.Authenticator
import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.auth.AuthModule
import im.vector.matrix.android.internal.auth.SessionParamsStore
@ -44,7 +44,7 @@ internal interface MatrixComponent {
@Unauthenticated
fun okHttpClient(): OkHttpClient
fun authenticator(): Authenticator
fun authenticationService(): AuthenticationService
fun context(): Context

View File

@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.network
internal object NetworkConstants {
private const val URI_API_PREFIX_PATH = "_matrix/client"
const val URI_API_PREFIX_PATH_ = "$URI_API_PREFIX_PATH/"
const val URI_API_PREFIX_PATH_R0 = "$URI_API_PREFIX_PATH/r0/"
const val URI_API_PREFIX_PATH_UNSTABLE = "$URI_API_PREFIX_PATH/unstable/"

View File

@ -22,12 +22,14 @@ import arrow.core.Try
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.api.session.file.FileService
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
import im.vector.matrix.android.internal.di.UserMd5
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import im.vector.matrix.android.internal.util.md5
import im.vector.matrix.android.internal.util.toCancelable
import im.vector.matrix.android.internal.util.writeToFile
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@ -55,8 +57,8 @@ internal class DefaultFileService @Inject constructor(private val context: Conte
fileName: String,
url: String?,
elementToDecrypt: ElementToDecrypt?,
callback: MatrixCallback<File>) {
GlobalScope.launch(coroutineDispatchers.main) {
callback: MatrixCallback<File>): Cancelable {
return GlobalScope.launch(coroutineDispatchers.main) {
withContext(coroutineDispatchers.io) {
Try {
val folder = getFolder(downloadMode, id)
@ -96,7 +98,7 @@ internal class DefaultFileService @Inject constructor(private val context: Conte
}
}
.foldToCallback(callback)
}
}.toCancelable()
}
private fun getFolder(downloadMode: FileService.DownloadMode, id: String): File {

View File

@ -101,7 +101,7 @@ class DefaultInitialSyncProgressService @Inject constructor() : InitialSyncProgr
val parentProgress = (currentProgress * parentWeight).toInt()
it.setProgress(offset + parentProgress)
} ?: run {
Timber.e("--- ${leaf().nameRes}: $currentProgress")
Timber.v("--- ${leaf().nameRes}: $currentProgress")
status.postValue(
InitialSyncProgressService.Status(leaf().nameRes, currentProgress)
)

View File

@ -19,7 +19,8 @@ package im.vector.matrix.android.internal.session.filter
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.internal.database.model.FilterEntity
import im.vector.matrix.android.internal.database.model.FilterEntityFields
import im.vector.matrix.android.internal.database.query.getFilter
import im.vector.matrix.android.internal.database.query.get
import im.vector.matrix.android.internal.database.query.getOrCreate
import im.vector.matrix.android.internal.util.awaitTransaction
import io.realm.Realm
import io.realm.kotlin.where
@ -29,26 +30,28 @@ internal class DefaultFilterRepository @Inject constructor(private val monarchy:
override suspend fun storeFilter(filterBody: FilterBody, roomEventFilter: RoomEventFilter): Boolean {
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
val filter = FilterEntity.getFilter(realm)
val result = if (filter.filterBodyJson != filterBody.toJSONString()) {
// Filter has changed, store it and reset the filter Id
monarchy.awaitTransaction {
val filter = FilterEntity.get(realm)
// Filter has changed, or no filter Id yet
filter == null
|| filter.filterBodyJson != filterBody.toJSONString()
|| filter.filterId.isBlank()
}.also { hasChanged ->
if (hasChanged) {
// Filter is new or has changed, store it and reset the filter Id.
// This has to be done outside of the Realm.use(), because awaitTransaction change the current thread
monarchy.awaitTransaction { realm ->
// We manage only one filter for now
val filterBodyJson = filterBody.toJSONString()
val roomEventFilterJson = roomEventFilter.toJSONString()
val filterEntity = FilterEntity.getFilter(it)
val filterEntity = FilterEntity.getOrCreate(realm)
filterEntity.filterBodyJson = filterBodyJson
filterEntity.roomEventFilterJson = roomEventFilterJson
// Reset filterId
filterEntity.filterId = ""
}
true
} else {
filter.filterId.isBlank()
}
result
}
}
@ -67,7 +70,7 @@ internal class DefaultFilterRepository @Inject constructor(private val monarchy:
override suspend fun getFilter(): String {
return Realm.getInstance(monarchy.realmConfiguration).use {
val filter = FilterEntity.getFilter(it)
val filter = FilterEntity.getOrCreate(it)
if (filter.filterId.isBlank()) {
// Use the Json format
filter.filterBodyJson
@ -80,7 +83,7 @@ internal class DefaultFilterRepository @Inject constructor(private val monarchy:
override suspend fun getRoomFilter(): String {
return Realm.getInstance(monarchy.realmConfiguration).use {
FilterEntity.getFilter(it).roomEventFilterJson
FilterEntity.getOrCreate(it).roomEventFilterJson
}
}
}

View File

@ -298,7 +298,7 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
}
}
} else {
Timber.e("Unknwon relation type ${content.relatesTo?.type} for event ${event.eventId}")
Timber.e("Unknown relation type ${content.relatesTo?.type} for event ${event.eventId}")
}
}

View File

@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.session.room.read
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.model.LocalEcho
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.query.*
@ -57,22 +56,18 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
override suspend fun execute(params: SetReadMarkersTask.Params) {
val markers = HashMap<String, String>()
val fullyReadEventId: String?
val readReceiptEventId: String?
Timber.v("Execute set read marker with params: $params")
if (params.markAllAsRead) {
val (fullyReadEventId, readReceiptEventId) = if (params.markAllAsRead) {
val latestSyncedEventId = Realm.getInstance(monarchy.realmConfiguration).use { realm ->
TimelineEventEntity.latestEvent(realm, roomId = params.roomId, includesSending = false)?.eventId
}
fullyReadEventId = latestSyncedEventId
readReceiptEventId = latestSyncedEventId
Pair(latestSyncedEventId, latestSyncedEventId)
} else {
fullyReadEventId = params.fullyReadEventId
readReceiptEventId = params.readReceiptEventId
Pair(params.fullyReadEventId, params.readReceiptEventId)
}
if (fullyReadEventId != null && isReadMarkerMoreRecent(params.roomId, fullyReadEventId)) {
if (fullyReadEventId != null && !isReadMarkerMoreRecent(monarchy, params.roomId, fullyReadEventId)) {
if (LocalEcho.isLocalEchoId(fullyReadEventId)) {
Timber.w("Can't set read marker for local event $fullyReadEventId")
} else {
@ -118,16 +113,4 @@ internal class DefaultSetReadMarkersTask @Inject constructor(private val roomAPI
}
}
}
private fun isReadMarkerMoreRecent(roomId: String, newReadMarkerId: String): Boolean {
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
val currentReadMarkerId = ReadMarkerEntity.where(realm, roomId = roomId).findFirst()?.eventId
?: return true
val readMarkerEvent = TimelineEventEntity.where(realm, roomId = roomId, eventId = currentReadMarkerId).findFirst()
val newReadMarkerEvent = TimelineEventEntity.where(realm, roomId = roomId, eventId = newReadMarkerId).findFirst()
val currentReadMarkerIndex = readMarkerEvent?.root?.displayIndex ?: Int.MAX_VALUE
val newReadMarkerIndex = newReadMarkerEvent?.root?.displayIndex ?: Int.MIN_VALUE
newReadMarkerIndex > currentReadMarkerIndex
}
}
}

View File

@ -115,7 +115,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
override fun editTextMessage(targetEventId: String,
msgType: String,
newBodyText: String,
newBodyText: CharSequence,
newBodyAutoMarkdown: Boolean,
compatibilityBodyText: String): Cancelable {
val event = eventFactory
@ -164,7 +164,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
.executeBy(taskExecutor)
}
override fun replyToMessage(eventReplied: TimelineEvent, replyText: String, autoMarkdown: Boolean): Cancelable? {
override fun replyToMessage(eventReplied: TimelineEvent, replyText: CharSequence, autoMarkdown: Boolean): Cancelable? {
val event = eventFactory.createReplyTextEvent(roomId, eventReplied, replyText, autoMarkdown)
?.also { saveLocalEcho(it) }
?: return null

View File

@ -68,7 +68,7 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private
private val workerFutureListenerExecutor = Executors.newSingleThreadExecutor()
override fun sendTextMessage(text: String, msgType: String, autoMarkdown: Boolean): Cancelable {
override fun sendTextMessage(text: CharSequence, msgType: String, autoMarkdown: Boolean): Cancelable {
val event = localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown).also {
saveLocalEcho(it)
}
@ -76,8 +76,8 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private
return sendEvent(event)
}
override fun sendFormattedTextMessage(text: String, formattedText: String): Cancelable {
val event = localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText)).also {
override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String): Cancelable {
val event = localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType).also {
saveLocalEcho(it)
}

View File

@ -36,6 +36,7 @@ import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.session.content.ThumbnailExtractor
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
import im.vector.matrix.android.internal.session.room.send.pills.TextPillsUtils
import im.vector.matrix.android.internal.util.StringProvider
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
@ -50,45 +51,55 @@ import javax.inject.Inject
*
* The transactionID is used as loc
*/
internal class LocalEchoEventFactory @Inject constructor(@UserId private val userId: String,
private val stringProvider: StringProvider,
private val roomSummaryUpdater: RoomSummaryUpdater) {
internal class LocalEchoEventFactory @Inject constructor(
@UserId private val userId: String,
private val stringProvider: StringProvider,
private val roomSummaryUpdater: RoomSummaryUpdater,
private val textPillsUtils: TextPillsUtils
) {
// TODO Inject
private val parser = Parser.builder().build()
// TODO Inject
private val renderer = HtmlRenderer.builder().build()
fun createTextEvent(roomId: String, msgType: String, text: String, autoMarkdown: Boolean): Event {
if (msgType == MessageType.MSGTYPE_TEXT) {
return createFormattedTextEvent(roomId, createTextContent(text, autoMarkdown))
fun createTextEvent(roomId: String, msgType: String, text: CharSequence, autoMarkdown: Boolean): Event {
if (msgType == MessageType.MSGTYPE_TEXT || msgType == MessageType.MSGTYPE_EMOTE) {
return createFormattedTextEvent(roomId, createTextContent(text, autoMarkdown), msgType)
}
val content = MessageTextContent(type = msgType, body = text)
val content = MessageTextContent(type = msgType, body = text.toString())
return createEvent(roomId, content)
}
private fun createTextContent(text: String, autoMarkdown: Boolean): TextContent {
private fun createTextContent(text: CharSequence, autoMarkdown: Boolean): TextContent {
if (autoMarkdown) {
val document = parser.parse(text)
val source = textPillsUtils.processSpecialSpansToMarkdown(text)
?: text.toString()
val document = parser.parse(source)
val htmlText = renderer.render(document)
if (isFormattedTextPertinent(text, htmlText)) {
return TextContent(text, htmlText)
if (isFormattedTextPertinent(source, htmlText)) {
return TextContent(source, htmlText)
}
} else {
// Try to detect pills
textPillsUtils.processSpecialSpansToHtml(text)?.let {
return TextContent(text.toString(), it)
}
}
return TextContent(text)
return TextContent(text.toString())
}
private fun isFormattedTextPertinent(text: String, htmlText: String?) =
text != htmlText && htmlText != "<p>${text.trim()}</p>\n"
fun createFormattedTextEvent(roomId: String, textContent: TextContent): Event {
return createEvent(roomId, textContent.toMessageTextContent())
fun createFormattedTextEvent(roomId: String, textContent: TextContent, msgType: String): Event {
return createEvent(roomId, textContent.toMessageTextContent(msgType))
}
fun createReplaceTextEvent(roomId: String,
targetEventId: String,
newBodyText: String,
newBodyText: CharSequence,
newBodyAutoMarkdown: Boolean,
msgType: String,
compatibilityText: String): Event {
@ -279,7 +290,7 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use
return System.currentTimeMillis()
}
fun createReplyTextEvent(roomId: String, eventReplied: TimelineEvent, replyText: String, autoMarkdown: Boolean): Event? {
fun createReplyTextEvent(roomId: String, eventReplied: TimelineEvent, replyText: CharSequence, autoMarkdown: Boolean): Event? {
// Fallbacks and event representation
// TODO Add error/warning logs when any of this is null
val permalink = PermalinkFactory.createPermalink(eventReplied.root) ?: return null
@ -298,7 +309,7 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use
//
// > <@alice:example.org> This is the original body
//
val replyFallback = buildReplyFallback(body, userId, replyText)
val replyFallback = buildReplyFallback(body, userId, replyText.toString())
val eventId = eventReplied.root.eventId ?: return null
val content = MessageTextContent(

View File

@ -0,0 +1,25 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.session.room.send.pills
import im.vector.matrix.android.api.session.room.send.UserMentionSpan
internal data class MentionLinkSpec(
val span: UserMentionSpan,
val start: Int,
val end: Int
)

View File

@ -0,0 +1,32 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.session.room.send.pills
import javax.inject.Inject
internal class MentionLinkSpecComparator @Inject constructor() : Comparator<MentionLinkSpec> {
override fun compare(o1: MentionLinkSpec, o2: MentionLinkSpec): Int {
return when {
o1.start < o2.start -> -1
o1.start > o2.start -> 1
o1.end < o2.end -> 1
o1.end > o2.end -> -1
else -> 0
}
}
}

View File

@ -0,0 +1,114 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.session.room.send.pills
import android.text.SpannableString
import im.vector.matrix.android.api.session.room.send.UserMentionSpan
import java.util.*
import javax.inject.Inject
/**
* Utility class to detect special span in CharSequence and turn them into
* formatted text to send them as a Matrix messages.
*
* For now only support UserMentionSpans (TODO rooms, room aliases, etc...)
*/
internal class TextPillsUtils @Inject constructor(
private val mentionLinkSpecComparator: MentionLinkSpecComparator
) {
/**
* Detects if transformable spans are present in the text.
* @return the transformed String or null if no Span found
*/
fun processSpecialSpansToHtml(text: CharSequence): String? {
return transformPills(text, MENTION_SPAN_TO_HTML_TEMPLATE)
}
/**
* Detects if transformable spans are present in the text.
* @return the transformed String or null if no Span found
*/
fun processSpecialSpansToMarkdown(text: CharSequence): String? {
return transformPills(text, MENTION_SPAN_TO_MD_TEMPLATE)
}
private fun transformPills(text: CharSequence, template: String): String? {
val spannableString = SpannableString.valueOf(text)
val pills = spannableString
?.getSpans(0, text.length, UserMentionSpan::class.java)
?.map { MentionLinkSpec(it, spannableString.getSpanStart(it), spannableString.getSpanEnd(it)) }
?.toMutableList()
?.takeIf { it.isNotEmpty() }
?: return null
// we need to prune overlaps!
pruneOverlaps(pills)
return buildString {
var currIndex = 0
pills.forEachIndexed { _, (urlSpan, start, end) ->
// We want to replace with the pill with a html link
// append text before pill
append(text, currIndex, start)
// append the pill
append(String.format(template, urlSpan.userId, urlSpan.displayName))
currIndex = end
}
// append text after the last pill
append(text, currIndex, text.length)
}
}
private fun pruneOverlaps(links: MutableList<MentionLinkSpec>) {
Collections.sort(links, mentionLinkSpecComparator)
var len = links.size
var i = 0
while (i < len - 1) {
val a = links[i]
val b = links[i + 1]
var remove = -1
// test if there is an overlap
if (b.start in a.start until a.end) {
when {
b.end <= a.end ->
// b is inside a -> b should be removed
remove = i + 1
a.end - a.start > b.end - b.start ->
// overlap and a is bigger -> b should be removed
remove = i + 1
a.end - a.start < b.end - b.start ->
// overlap and a is smaller -> a should be removed
remove = i
}
if (remove != -1) {
links.removeAt(remove)
len--
continue
}
}
i++
}
}
companion object {
private const val MENTION_SPAN_TO_HTML_TEMPLATE = "<a href=\"https://matrix.to/#/%1\$s\">%2\$s</a>"
private const val MENTION_SPAN_TO_MD_TEMPLATE = "[%2\$s](https://matrix.to/#/%1\$s)"
}
}

View File

@ -26,7 +26,8 @@ internal interface GetContextOfEventTask : Task<GetContextOfEventTask.Params, To
data class Params(
val roomId: String,
val eventId: String
val eventId: String,
val limit: Int
)
}
@ -38,7 +39,7 @@ internal class DefaultGetContextOfEventTask @Inject constructor(private val room
override suspend fun execute(params: GetContextOfEventTask.Params): TokenChunkEventPersistor.Result {
val filter = filterRepository.getRoomFilter()
val response = executeRequest<EventContextResponse> {
apiCall = roomAPI.getContextOfEvent(params.roomId, params.eventId, 0, filter)
apiCall = roomAPI.getContextOfEvent(params.roomId, params.eventId, params.limit, filter)
}
return tokenChunkEventPersistor.insertInDb(response, params.roomId, PaginationDirection.BACKWARDS)
}

View File

@ -74,22 +74,14 @@ internal class DefaultTimeline(
private val cryptoService: CryptoService,
private val timelineEventMapper: TimelineEventMapper,
private val settings: TimelineSettings,
private val hiddenReadReceipts: TimelineHiddenReadReceipts,
private val hiddenReadMarker: TimelineHiddenReadMarker
) : Timeline, TimelineHiddenReadReceipts.Delegate, TimelineHiddenReadMarker.Delegate {
private val hiddenReadReceipts: TimelineHiddenReadReceipts
) : Timeline, TimelineHiddenReadReceipts.Delegate {
private companion object {
val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD")
}
override var listener: Timeline.Listener? = null
set(value) {
field = value
BACKGROUND_HANDLER.post {
postSnapshot()
}
}
private val listeners = ArrayList<Timeline.Listener>()
private val isStarted = AtomicBoolean(false)
private val isReady = AtomicBoolean(false)
private val mainHandler = createUIHandler()
@ -110,7 +102,7 @@ internal class DefaultTimeline(
private val backwardsState = AtomicReference(State())
private val forwardsState = AtomicReference(State())
private val timelineID = UUID.randomUUID().toString()
override val timelineID = UUID.randomUUID().toString()
override val isLive
get() = !hasMoreToLoad(Timeline.Direction.FORWARDS)
@ -197,7 +189,6 @@ internal class DefaultTimeline(
if (settings.buildReadReceipts) {
hiddenReadReceipts.start(realm, filteredEvents, nonFilteredEvents, this)
}
hiddenReadMarker.start(realm, filteredEvents, nonFilteredEvents, this)
isReady.set(true)
}
}
@ -217,7 +208,6 @@ internal class DefaultTimeline(
if (this::filteredEvents.isInitialized) {
filteredEvents.removeAllChangeListeners()
}
hiddenReadMarker.dispose()
if (settings.buildReadReceipts) {
hiddenReadReceipts.dispose()
}
@ -298,7 +288,21 @@ internal class DefaultTimeline(
return hasMoreInCache(direction) || !hasReachedEnd(direction)
}
// TimelineHiddenReadReceipts.Delegate
override fun addListener(listener: Timeline.Listener) = synchronized(listeners) {
listeners.add(listener).also {
postSnapshot()
}
}
override fun removeListener(listener: Timeline.Listener) = synchronized(listeners) {
listeners.remove(listener)
}
override fun removeAllListeners() = synchronized(listeners) {
listeners.clear()
}
// TimelineHiddenReadReceipts.Delegate
override fun rebuildEvent(eventId: String, readReceipts: List<ReadReceipt>): Boolean {
return rebuildEvent(eventId) { te ->
@ -310,19 +314,7 @@ internal class DefaultTimeline(
postSnapshot()
}
// TimelineHiddenReadMarker.Delegate
override fun rebuildEvent(eventId: String, hasReadMarker: Boolean): Boolean {
return rebuildEvent(eventId) { te ->
te.copy(hasReadMarker = hasReadMarker)
}
}
override fun onReadMarkerUpdated() {
postSnapshot()
}
// Private methods *****************************************************************************
// Private methods *****************************************************************************
private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean {
return builtEventsIdMap[eventId]?.let { builtIndex ->
@ -502,9 +494,9 @@ internal class DefaultTimeline(
return
}
val params = PaginationTask.Params(roomId = roomId,
from = token,
direction = direction.toPaginationDirection(),
limit = limit)
from = token,
direction = direction.toPaginationDirection(),
limit = limit)
Timber.v("Should fetch $limit items $direction")
cancelableBag += paginationTask
@ -579,7 +571,7 @@ internal class DefaultTimeline(
val timelineEvent = buildTimelineEvent(eventEntity)
if (timelineEvent.isEncrypted()
&& timelineEvent.root.mxDecryptionResult == null) {
&& timelineEvent.root.mxDecryptionResult == null) {
timelineEvent.root.eventId?.let { eventDecryptor.requestDecryption(it) }
}
@ -641,7 +633,7 @@ internal class DefaultTimeline(
}
private fun fetchEvent(eventId: String) {
val params = GetContextOfEventTask.Params(roomId, eventId)
val params = GetContextOfEventTask.Params(roomId, eventId, settings.initialSize)
cancelableBag += contextOfEventTask.configureWith(params).executeBy(taskExecutor)
}
@ -652,7 +644,13 @@ internal class DefaultTimeline(
}
updateLoadingStates(filteredEvents)
val snapshot = createSnapshot()
val runnable = Runnable { listener?.onUpdated(snapshot) }
val runnable = Runnable {
synchronized(listeners) {
listeners.forEach {
it.onUpdated(snapshot)
}
}
}
debouncer.debounce("post_snapshot", runnable, 50)
}
}

View File

@ -53,17 +53,16 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
override fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline {
return DefaultTimeline(roomId,
eventId,
monarchy.realmConfiguration,
taskExecutor,
contextOfEventTask,
clearUnlinkedEventsTask,
paginationTask,
cryptoService,
timelineEventMapper,
settings,
TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings),
TimelineHiddenReadMarker(roomId, settings)
eventId,
monarchy.realmConfiguration,
taskExecutor,
contextOfEventTask,
clearUnlinkedEventsTask,
paginationTask,
cryptoService,
timelineEventMapper,
settings,
TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings)
)
}

View File

@ -30,6 +30,7 @@ data class EventContextResponse(
@Json(name = "state") override val stateEvents: List<Event> = emptyList()
) : TokenChunkEvent {
override val events: List<Event>
get() = listOf(event)
override val events: List<Event> by lazy {
eventsAfter.reversed() + listOf(event) + eventsBefore
}
}

View File

@ -1,133 +0,0 @@
/*
* Copyright 2019 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 im.vector.matrix.android.internal.session.room.timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
import im.vector.matrix.android.internal.database.model.ReadMarkerEntityFields
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
import im.vector.matrix.android.internal.database.query.FilterContent
import im.vector.matrix.android.internal.database.query.where
import io.realm.OrderedRealmCollectionChangeListener
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.RealmResults
/**
* This class is responsible for handling the read marker for hidden events.
* When an hidden event has read marker, we want to transfer it on the first older displayed event.
* It has to be used in [DefaultTimeline] and we should call the [start] and [dispose] methods to properly handle realm subscription.
*/
internal class TimelineHiddenReadMarker constructor(private val roomId: String,
private val settings: TimelineSettings) {
interface Delegate {
fun rebuildEvent(eventId: String, hasReadMarker: Boolean): Boolean
fun onReadMarkerUpdated()
}
private var previousDisplayedEventId: String? = null
private var hiddenReadMarker: RealmResults<ReadMarkerEntity>? = null
private lateinit var filteredEvents: RealmResults<TimelineEventEntity>
private lateinit var nonFilteredEvents: RealmResults<TimelineEventEntity>
private lateinit var delegate: Delegate
private val readMarkerListener = OrderedRealmCollectionChangeListener<RealmResults<ReadMarkerEntity>> { readMarkers, changeSet ->
if (!readMarkers.isLoaded || !readMarkers.isValid) {
return@OrderedRealmCollectionChangeListener
}
var hasChange = false
if (changeSet.deletions.isNotEmpty()) {
previousDisplayedEventId?.also {
hasChange = delegate.rebuildEvent(it, false)
previousDisplayedEventId = null
}
}
val readMarker = readMarkers.firstOrNull() ?: return@OrderedRealmCollectionChangeListener
val hiddenEvent = readMarker.timelineEvent?.firstOrNull()
?: return@OrderedRealmCollectionChangeListener
val isLoaded = nonFilteredEvents.where()
.equalTo(TimelineEventEntityFields.EVENT_ID, hiddenEvent.eventId)
.findFirst() != null
val displayIndex = hiddenEvent.root?.displayIndex
if (isLoaded && displayIndex != null) {
// Then we are looking for the first displayable event after the hidden one
val firstDisplayedEvent = filteredEvents.where()
.lessThanOrEqualTo(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, displayIndex)
.findFirst()
// If we find one, we should rebuild this one with marker
if (firstDisplayedEvent != null) {
previousDisplayedEventId = firstDisplayedEvent.eventId
hasChange = delegate.rebuildEvent(firstDisplayedEvent.eventId, true)
}
}
if (hasChange) {
delegate.onReadMarkerUpdated()
}
}
/**
* Start the realm query subscription. Has to be called on an HandlerThread
*/
fun start(realm: Realm,
filteredEvents: RealmResults<TimelineEventEntity>,
nonFilteredEvents: RealmResults<TimelineEventEntity>,
delegate: Delegate) {
this.filteredEvents = filteredEvents
this.nonFilteredEvents = nonFilteredEvents
this.delegate = delegate
// We are looking for read receipts set on hidden events.
// We only accept those with a timelineEvent (so coming from pagination/sync).
hiddenReadMarker = ReadMarkerEntity.where(realm, roomId = roomId)
.isNotEmpty(ReadMarkerEntityFields.TIMELINE_EVENT)
.filterReceiptsWithSettings()
.findAllAsync()
.also { it.addChangeListener(readMarkerListener) }
}
/**
* Dispose the realm query subscription. Has to be called on an HandlerThread
*/
fun dispose() {
this.hiddenReadMarker?.removeAllChangeListeners()
}
/**
* We are looking for readMarker related to filtered events. So, it's the opposite of [DefaultTimeline.filterEventsWithSettings] method.
*/
private fun RealmQuery<ReadMarkerEntity>.filterReceiptsWithSettings(): RealmQuery<ReadMarkerEntity> {
beginGroup()
if (settings.filterTypes) {
not().`in`("${ReadMarkerEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.TYPE}", settings.allowedTypes.toTypedArray())
}
if (settings.filterTypes && settings.filterEdits) {
or()
}
if (settings.filterEdits) {
like("${ReadMarkerEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", FilterContent.EDIT_TYPE)
}
endGroup()
return this
}
}

View File

@ -17,6 +17,7 @@
package im.vector.matrix.android.internal.session.signout
import android.content.Context
import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.crypto.CryptoModule
@ -27,6 +28,8 @@ import im.vector.matrix.android.internal.session.SessionModule
import im.vector.matrix.android.internal.session.cache.ClearCacheTask
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.worker.WorkManagerUtil
import io.realm.Realm
import io.realm.RealmConfiguration
import timber.log.Timber
import java.io.File
import javax.inject.Inject
@ -42,6 +45,8 @@ internal class DefaultSignOutTask @Inject constructor(private val context: Conte
@CryptoDatabase private val clearCryptoDataTask: ClearCacheTask,
@UserCacheDirectory private val userFile: File,
private val realmKeysUtils: RealmKeysUtils,
@SessionDatabase private val realmSessionConfiguration: RealmConfiguration,
@CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration,
@UserMd5 private val userMd5: String) : SignOutTask {
override suspend fun execute(params: Unit) {
@ -71,5 +76,15 @@ internal class DefaultSignOutTask @Inject constructor(private val context: Conte
Timber.d("SignOut: clear the database keys")
realmKeysUtils.clear(SessionModule.DB_ALIAS_PREFIX + userMd5)
realmKeysUtils.clear(CryptoModule.DB_ALIAS_PREFIX + userMd5)
// Sanity check
if (BuildConfig.DEBUG) {
Realm.getGlobalInstanceCount(realmSessionConfiguration)
.takeIf { it > 0 }
?.let { Timber.e("All realm instance for session has not been closed ($it)") }
Realm.getGlobalInstanceCount(realmCryptoConfiguration)
.takeIf { it > 0 }
?.let { Timber.e("All realm instance for crypto has not been closed ($it)") }
}
}
}

View File

@ -16,14 +16,10 @@
package im.vector.matrix.android.internal.session.sync
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.session.room.read.FullyReadContent
import im.vector.matrix.android.internal.database.model.ReadMarkerEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
import im.vector.matrix.android.internal.database.query.getOrCreate
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.room.read.FullyReadContent
import io.realm.Realm
import timber.log.Timber
import javax.inject.Inject
@ -39,18 +35,8 @@ internal class RoomFullyReadHandler @Inject constructor() {
RoomSummaryEntity.getOrCreate(realm, roomId).apply {
readMarkerId = content.eventId
}
// Remove the old markers if any
val oldReadMarkerEvents = TimelineEventEntity
.where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.BOTH)
.isNotNull(TimelineEventEntityFields.READ_MARKER.`$`)
.findAll()
oldReadMarkerEvents.forEach { it.readMarker = null }
val readMarkerEntity = ReadMarkerEntity.getOrCreate(realm, roomId).apply {
ReadMarkerEntity.getOrCreate(realm, roomId).apply {
this.eventId = content.eventId
}
// Attach to timelineEvent if known
val timelineEventEntities = TimelineEventEntity.where(realm, roomId = roomId, eventId = content.eventId).findAll()
timelineEventEntities.forEach { it.readMarker = readMarkerEntity }
}
}

View File

@ -26,6 +26,7 @@ import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressServi
import im.vector.matrix.android.internal.session.filter.FilterRepository
import im.vector.matrix.android.internal.session.homeserver.GetHomeServerCapabilitiesTask
import im.vector.matrix.android.internal.session.sync.model.SyncResponse
import im.vector.matrix.android.internal.session.user.UserStore
import im.vector.matrix.android.internal.task.Task
import javax.inject.Inject
@ -41,7 +42,8 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI,
private val sessionParamsStore: SessionParamsStore,
private val initialSyncProgressService: DefaultInitialSyncProgressService,
private val syncTokenStore: SyncTokenStore,
private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask
private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask,
private val userStore: UserStore
) : SyncTask {
override suspend fun execute(params: SyncTask.Params) {
@ -60,6 +62,8 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI,
val isInitialSync = token == null
if (isInitialSync) {
// We might want to get the user information in parallel too
userStore.createOrUpdate(userId)
initialSyncProgressService.endAll()
initialSyncProgressService.startTask(R.string.initial_sync_start_importing_account, 100)
}

View File

@ -53,4 +53,7 @@ internal abstract class UserModule {
@Binds
abstract fun bindUpdateIgnoredUserIdsTask(task: DefaultUpdateIgnoredUserIdsTask): UpdateIgnoredUserIdsTask
@Binds
abstract fun bindUserStore(userStore: RealmUserStore): UserStore
}

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