mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-01-09 17:57:41 +03:00
Merge branch 'release/0.9.0'
This commit is contained in:
commit
8e9ac8198d
276 changed files with 9848 additions and 1901 deletions
|
@ -3,7 +3,9 @@
|
||||||
<words>
|
<words>
|
||||||
<w>backstack</w>
|
<w>backstack</w>
|
||||||
<w>bytearray</w>
|
<w>bytearray</w>
|
||||||
|
<w>checkables</w>
|
||||||
<w>ciphertext</w>
|
<w>ciphertext</w>
|
||||||
|
<w>coroutine</w>
|
||||||
<w>decryptor</w>
|
<w>decryptor</w>
|
||||||
<w>emoji</w>
|
<w>emoji</w>
|
||||||
<w>emojis</w>
|
<w>emojis</w>
|
||||||
|
@ -12,8 +14,11 @@
|
||||||
<w>linkified</w>
|
<w>linkified</w>
|
||||||
<w>linkify</w>
|
<w>linkify</w>
|
||||||
<w>megolm</w>
|
<w>megolm</w>
|
||||||
|
<w>msisdn</w>
|
||||||
<w>pbkdf</w>
|
<w>pbkdf</w>
|
||||||
<w>pkcs</w>
|
<w>pkcs</w>
|
||||||
|
<w>signin</w>
|
||||||
|
<w>signup</w>
|
||||||
</words>
|
</words>
|
||||||
</dictionary>
|
</dictionary>
|
||||||
</component>
|
</component>
|
21
CHANGES.md
21
CHANGES.md
|
@ -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)
|
Changes in RiotX 0.8.0 (2019-11-19)
|
||||||
===================================================
|
===================================================
|
||||||
|
|
||||||
|
|
260
docs/signin.md
Normal file
260
docs/signin.md
Normal 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
579
docs/signup.md
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
|
@ -21,7 +21,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.rule.GrantPermissionRule
|
import androidx.test.rule.GrantPermissionRule
|
||||||
import im.vector.matrix.android.InstrumentedTest
|
import im.vector.matrix.android.InstrumentedTest
|
||||||
import im.vector.matrix.android.OkReplayRuleChainNoActivity
|
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 okreplay.*
|
||||||
import org.junit.ClassRule
|
import org.junit.ClassRule
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
@ -29,9 +29,9 @@ import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
internal class AuthenticatorTest : InstrumentedTest {
|
internal class AuthenticationServiceTest : InstrumentedTest {
|
||||||
|
|
||||||
lateinit var authenticator: Authenticator
|
lateinit var authenticationService: AuthenticationService
|
||||||
lateinit var okReplayInterceptor: OkReplayInterceptor
|
lateinit var okReplayInterceptor: OkReplayInterceptor
|
||||||
|
|
||||||
private val okReplayConfig = OkReplayConfig.Builder()
|
private val okReplayConfig = OkReplayConfig.Builder()
|
|
@ -22,7 +22,7 @@ import androidx.work.Configuration
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.BuildConfig
|
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.SessionManager
|
||||||
import im.vector.matrix.android.internal.di.DaggerMatrixComponent
|
import im.vector.matrix.android.internal.di.DaggerMatrixComponent
|
||||||
import im.vector.matrix.android.internal.network.UserAgentHolder
|
import im.vector.matrix.android.internal.network.UserAgentHolder
|
||||||
|
@ -46,7 +46,7 @@ data class MatrixConfiguration(
|
||||||
*/
|
*/
|
||||||
class Matrix private constructor(context: Context, matrixConfiguration: 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 userAgentHolder: UserAgentHolder
|
||||||
@Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver
|
@Inject internal lateinit var backgroundDetectionObserver: BackgroundDetectionObserver
|
||||||
@Inject internal lateinit var olmManager: OlmManager
|
@Inject internal lateinit var olmManager: OlmManager
|
||||||
|
@ -64,8 +64,8 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
|
||||||
|
|
||||||
fun getUserAgent() = userAgentHolder.userAgent
|
fun getUserAgent() = userAgentHolder.userAgent
|
||||||
|
|
||||||
fun authenticator(): Authenticator {
|
fun authenticationService(): AuthenticationService {
|
||||||
return authenticator
|
return authenticationService
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -19,29 +19,48 @@ package im.vector.matrix.android.api.auth
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.auth.data.Credentials
|
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.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.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.session.Session
|
||||||
import im.vector.matrix.android.api.util.Cancelable
|
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
|
* Return a LoginWizard, to login to the homeserver. The login flow has to be retrieved first.
|
||||||
* @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]
|
|
||||||
*/
|
*/
|
||||||
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].
|
* Check if there is an authenticated [Session].
|
||||||
|
@ -67,5 +86,7 @@ interface Authenticator {
|
||||||
/**
|
/**
|
||||||
* Create a session after a SSO successful login
|
* 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
|
||||||
}
|
}
|
|
@ -30,4 +30,7 @@ data class Credentials(
|
||||||
@Json(name = "home_server") val homeServer: String,
|
@Json(name = "home_server") val homeServer: String,
|
||||||
@Json(name = "access_token") val accessToken: String,
|
@Json(name = "access_token") val accessToken: String,
|
||||||
@Json(name = "refresh_token") val refreshToken: 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
|
||||||
|
)
|
||||||
|
|
|
@ -25,7 +25,7 @@ import okhttp3.TlsVersion
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This data class holds how to connect to a specific Homeserver.
|
* 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.
|
* You should use the [Builder] to create one.
|
||||||
*/
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
|
@ -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
|
||||||
|
)
|
|
@ -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
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
|
@ -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>
|
||||||
|
)
|
|
@ -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
|
||||||
|
}
|
|
@ -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<*, *>
|
|
@ -34,6 +34,7 @@ sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) {
|
||||||
data class Cancelled(val throwable: Throwable? = null) : Failure(throwable)
|
data class Cancelled(val throwable: Throwable? = null) : Failure(throwable)
|
||||||
data class NetworkConnection(val ioException: IOException? = null) : Failure(ioException)
|
data class NetworkConnection(val ioException: IOException? = null) : Failure(ioException)
|
||||||
data class ServerError(val error: MatrixError, val httpCode: Int) : Failure(RuntimeException(error.toString()))
|
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
|
// 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))
|
data class OtherServerError(val errorBody: String, val httpCode: Int) : Failure(RuntimeException(errorBody))
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,9 @@ data class MatrixError(
|
||||||
@Json(name = "consent_uri") val consentUri: String? = null,
|
@Json(name = "consent_uri") val consentUri: String? = null,
|
||||||
// RESOURCE_LIMIT_EXCEEDED data
|
// RESOURCE_LIMIT_EXCEEDED data
|
||||||
@Json(name = "limit_type") val limitType: String? = null,
|
@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 {
|
companion object {
|
||||||
const val FORBIDDEN = "M_FORBIDDEN"
|
const val FORBIDDEN = "M_FORBIDDEN"
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
package im.vector.matrix.android.api.session.file
|
package im.vector.matrix.android.api.session.file
|
||||||
|
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
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 im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
@ -47,5 +48,5 @@ interface FileService {
|
||||||
fileName: String,
|
fileName: String,
|
||||||
url: String?,
|
url: String?,
|
||||||
elementToDecrypt: ElementToDecrypt?,
|
elementToDecrypt: ElementToDecrypt?,
|
||||||
callback: MatrixCallback<File>)
|
callback: MatrixCallback<File>): Cancelable
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,7 +72,7 @@ interface RelationService {
|
||||||
*/
|
*/
|
||||||
fun editTextMessage(targetEventId: String,
|
fun editTextMessage(targetEventId: String,
|
||||||
msgType: String,
|
msgType: String,
|
||||||
newBodyText: String,
|
newBodyText: CharSequence,
|
||||||
newBodyAutoMarkdown: Boolean,
|
newBodyAutoMarkdown: Boolean,
|
||||||
compatibilityBodyText: String = "* $newBodyText"): Cancelable
|
compatibilityBodyText: String = "* $newBodyText"): Cancelable
|
||||||
|
|
||||||
|
@ -97,12 +97,14 @@ interface RelationService {
|
||||||
/**
|
/**
|
||||||
* Reply to an event in the timeline (must be in same room)
|
* Reply to an event in the timeline (must be in same room)
|
||||||
* https://matrix.org/docs/spec/client_server/r0.4.0.html#id350
|
* 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 eventReplied the event referenced by the reply
|
||||||
* @param replyText the reply text
|
* @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
|
* @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,
|
fun replyToMessage(eventReplied: TimelineEvent,
|
||||||
replyText: String,
|
replyText: CharSequence,
|
||||||
autoMarkdown: Boolean = false): Cancelable?
|
autoMarkdown: Boolean = false): Cancelable?
|
||||||
|
|
||||||
fun getEventSummaryLive(eventId: String): LiveData<Optional<EventAnnotationsSummary>>
|
fun getEventSummaryLive(eventId: String): LiveData<Optional<EventAnnotationsSummary>>
|
||||||
|
|
|
@ -29,20 +29,23 @@ interface SendService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to send a text message asynchronously.
|
* 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 text the text message to send
|
||||||
* @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE
|
* @param msgType the message type: MessageType.MSGTYPE_TEXT (default) or MessageType.MSGTYPE_EMOTE
|
||||||
* @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
|
* @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
|
||||||
* @return a [Cancelable]
|
* @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.
|
* Method to send a text message with a formatted body.
|
||||||
* @param text the text message to send
|
* @param text the text message to send
|
||||||
* @param formattedText The formatted body using MessageType#FORMAT_MATRIX_HTML
|
* @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]
|
* @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.
|
* Method to send a media asynchronously.
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -30,10 +30,16 @@ package im.vector.matrix.android.api.session.room.timeline
|
||||||
*/
|
*/
|
||||||
interface Timeline {
|
interface Timeline {
|
||||||
|
|
||||||
var listener: Listener?
|
val timelineID: String
|
||||||
|
|
||||||
val isLive: Boolean
|
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
|
* 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 {
|
interface Listener {
|
||||||
/**
|
/**
|
||||||
* Call when the timeline has been updated through pagination or sync.
|
* 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>)
|
fun onUpdated(snapshot: List<TimelineEvent>)
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,8 +41,7 @@ data class TimelineEvent(
|
||||||
val isUniqueDisplayName: Boolean,
|
val isUniqueDisplayName: Boolean,
|
||||||
val senderAvatar: String?,
|
val senderAvatar: String?,
|
||||||
val annotations: EventAnnotationsSummary? = null,
|
val annotations: EventAnnotationsSummary? = null,
|
||||||
val readReceipts: List<ReadReceipt> = emptyList(),
|
val readReceipts: List<ReadReceipt> = emptyList()
|
||||||
val hasReadMarker: Boolean = false
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val metadata = HashMap<String, Any>()
|
val metadata = HashMap<String, Any>()
|
||||||
|
|
|
@ -29,3 +29,5 @@ interface Cancelable {
|
||||||
// no-op
|
// no-op
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object NoOpCancellable : Cancelable
|
||||||
|
|
|
@ -17,20 +17,47 @@
|
||||||
package im.vector.matrix.android.internal.auth
|
package im.vector.matrix.android.internal.auth
|
||||||
|
|
||||||
import im.vector.matrix.android.api.auth.data.Credentials
|
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.LoginFlowResponse
|
||||||
import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
|
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 im.vector.matrix.android.internal.network.NetworkConstants
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.http.Body
|
import retrofit2.http.*
|
||||||
import retrofit2.http.GET
|
|
||||||
import retrofit2.http.Headers
|
|
||||||
import retrofit2.http.POST
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The login REST API.
|
* The login REST API.
|
||||||
*/
|
*/
|
||||||
internal interface AuthAPI {
|
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
|
* Get the supported login flow
|
||||||
* Ref: https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-login
|
* 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")
|
@Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000")
|
||||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login")
|
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login")
|
||||||
fun login(@Body loginParams: PasswordLoginParams): Call<Credentials>
|
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>
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,8 +20,10 @@ import android.content.Context
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
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.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.auth.db.RealmSessionParamsStore
|
||||||
import im.vector.matrix.android.internal.database.RealmKeysUtils
|
import im.vector.matrix.android.internal.database.RealmKeysUtils
|
||||||
import im.vector.matrix.android.internal.di.AuthDatabase
|
import im.vector.matrix.android.internal.di.AuthDatabase
|
||||||
|
@ -50,7 +52,8 @@ internal abstract class AuthModule {
|
||||||
}
|
}
|
||||||
.name("matrix-sdk-auth.realm")
|
.name("matrix-sdk-auth.realm")
|
||||||
.modules(AuthRealmModule())
|
.modules(AuthRealmModule())
|
||||||
.deleteRealmIfMigrationNeeded()
|
.schemaVersion(AuthRealmMigration.SCHEMA_VERSION)
|
||||||
|
.migration(AuthRealmMigration())
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,5 +62,11 @@ internal abstract class AuthModule {
|
||||||
abstract fun bindSessionParamsStore(sessionParamsStore: RealmSessionParamsStore): SessionParamsStore
|
abstract fun bindSessionParamsStore(sessionParamsStore: RealmSessionParamsStore): SessionParamsStore
|
||||||
|
|
||||||
@Binds
|
@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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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()
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -30,12 +30,4 @@ data class InteractiveAuthenticationFlow(
|
||||||
|
|
||||||
@Json(name = "stages")
|
@Json(name = "stages")
|
||||||
val stages: List<String>? = null
|
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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -25,4 +25,7 @@ object LoginFlowTypes {
|
||||||
const val MSISDN = "m.login.msisdn"
|
const val MSISDN = "m.login.msisdn"
|
||||||
const val RECAPTCHA = "m.login.recaptcha"
|
const val RECAPTCHA = "m.login.recaptcha"
|
||||||
const val DUMMY = "m.login.dummy"
|
const val DUMMY = "m.login.dummy"
|
||||||
|
const val TERMS = "m.login.terms"
|
||||||
|
const val TOKEN = "m.login.token"
|
||||||
|
const val SSO = "m.login.sso"
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,34 +19,46 @@ package im.vector.matrix.android.internal.auth.data
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
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)
|
@JsonClass(generateAdapter = true)
|
||||||
internal data class PasswordLoginParams(@Json(name = "identifier") val identifier: Map<String, String>,
|
internal data class PasswordLoginParams(
|
||||||
@Json(name = "password") val password: String,
|
@Json(name = "identifier") val identifier: Map<String, String>,
|
||||||
@Json(name = "type") override val type: String,
|
@Json(name = "password") val password: String,
|
||||||
@Json(name = "initial_device_display_name") val deviceDisplayName: String?,
|
@Json(name = "type") override val type: String,
|
||||||
@Json(name = "device_id") val deviceId: String?) : LoginParams {
|
@Json(name = "initial_device_display_name") val deviceDisplayName: String?,
|
||||||
|
@Json(name = "device_id") val deviceId: String?) : LoginParams {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val IDENTIFIER_KEY_TYPE = "type"
|
||||||
|
|
||||||
val IDENTIFIER_KEY_TYPE_USER = "m.id.user"
|
private const val IDENTIFIER_KEY_TYPE_USER = "m.id.user"
|
||||||
val IDENTIFIER_KEY_TYPE_THIRD_PARTY = "m.id.thirdparty"
|
private const val IDENTIFIER_KEY_USER = "user"
|
||||||
val IDENTIFIER_KEY_TYPE_PHONE = "m.id.phone"
|
|
||||||
|
|
||||||
val IDENTIFIER_KEY_TYPE = "type"
|
private const val IDENTIFIER_KEY_TYPE_THIRD_PARTY = "m.id.thirdparty"
|
||||||
val IDENTIFIER_KEY_MEDIUM = "medium"
|
private const val IDENTIFIER_KEY_MEDIUM = "medium"
|
||||||
val IDENTIFIER_KEY_ADDRESS = "address"
|
private const val IDENTIFIER_KEY_ADDRESS = "address"
|
||||||
val IDENTIFIER_KEY_USER = "user"
|
|
||||||
val IDENTIFIER_KEY_COUNTRY = "country"
|
private const val IDENTIFIER_KEY_TYPE_PHONE = "m.id.phone"
|
||||||
val IDENTIFIER_KEY_NUMBER = "number"
|
private const val IDENTIFIER_KEY_COUNTRY = "country"
|
||||||
|
private const val IDENTIFIER_KEY_PHONE = "phone"
|
||||||
|
|
||||||
fun userIdentifier(user: String,
|
fun userIdentifier(user: String,
|
||||||
password: String,
|
password: String,
|
||||||
deviceDisplayName: String? = null,
|
deviceDisplayName: String? = null,
|
||||||
deviceId: String? = null): PasswordLoginParams {
|
deviceId: String? = null): PasswordLoginParams {
|
||||||
val identifier = HashMap<String, String>()
|
return PasswordLoginParams(
|
||||||
identifier[IDENTIFIER_KEY_TYPE] = IDENTIFIER_KEY_TYPE_USER
|
mapOf(
|
||||||
identifier[IDENTIFIER_KEY_USER] = user
|
IDENTIFIER_KEY_TYPE to IDENTIFIER_KEY_TYPE_USER,
|
||||||
return PasswordLoginParams(identifier, password, LoginFlowTypes.PASSWORD, deviceDisplayName, deviceId)
|
IDENTIFIER_KEY_USER to user
|
||||||
|
),
|
||||||
|
password,
|
||||||
|
LoginFlowTypes.PASSWORD,
|
||||||
|
deviceDisplayName,
|
||||||
|
deviceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun thirdPartyIdentifier(medium: String,
|
fun thirdPartyIdentifier(medium: String,
|
||||||
|
@ -54,11 +66,33 @@ internal data class PasswordLoginParams(@Json(name = "identifier") val identifie
|
||||||
password: String,
|
password: String,
|
||||||
deviceDisplayName: String? = null,
|
deviceDisplayName: String? = null,
|
||||||
deviceId: String? = null): PasswordLoginParams {
|
deviceId: String? = null): PasswordLoginParams {
|
||||||
val identifier = HashMap<String, String>()
|
return PasswordLoginParams(
|
||||||
identifier[IDENTIFIER_KEY_TYPE] = IDENTIFIER_KEY_TYPE_THIRD_PARTY
|
mapOf(
|
||||||
identifier[IDENTIFIER_KEY_MEDIUM] = medium
|
IDENTIFIER_KEY_TYPE to IDENTIFIER_KEY_TYPE_THIRD_PARTY,
|
||||||
identifier[IDENTIFIER_KEY_ADDRESS] = address
|
IDENTIFIER_KEY_MEDIUM to medium,
|
||||||
return PasswordLoginParams(identifier, password, LoginFlowTypes.PASSWORD, deviceDisplayName, deviceId)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,6 +23,7 @@ import io.realm.annotations.RealmModule
|
||||||
*/
|
*/
|
||||||
@RealmModule(library = true,
|
@RealmModule(library = true,
|
||||||
classes = [
|
classes = [
|
||||||
SessionParamsEntity::class
|
SessionParamsEntity::class,
|
||||||
|
PendingSessionEntity::class
|
||||||
])
|
])
|
||||||
internal class AuthRealmModule
|
internal class AuthRealmModule
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
|
@ -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()
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,6 +22,8 @@ import im.vector.matrix.android.internal.database.awaitTransaction
|
||||||
import im.vector.matrix.android.internal.di.AuthDatabase
|
import im.vector.matrix.android.internal.di.AuthDatabase
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import io.realm.RealmConfiguration
|
import io.realm.RealmConfiguration
|
||||||
|
import io.realm.exceptions.RealmPrimaryKeyConstraintException
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class RealmSessionParamsStore @Inject constructor(private val mapper: SessionParamsMapper,
|
internal class RealmSessionParamsStore @Inject constructor(private val mapper: SessionParamsMapper,
|
||||||
|
@ -30,43 +32,45 @@ internal class RealmSessionParamsStore @Inject constructor(private val mapper: S
|
||||||
) : SessionParamsStore {
|
) : SessionParamsStore {
|
||||||
|
|
||||||
override fun getLast(): SessionParams? {
|
override fun getLast(): SessionParams? {
|
||||||
val realm = Realm.getInstance(realmConfiguration)
|
return Realm.getInstance(realmConfiguration).use { realm ->
|
||||||
val sessionParams = realm
|
realm
|
||||||
.where(SessionParamsEntity::class.java)
|
.where(SessionParamsEntity::class.java)
|
||||||
.findAll()
|
.findAll()
|
||||||
.map { mapper.map(it) }
|
.map { mapper.map(it) }
|
||||||
.lastOrNull()
|
.lastOrNull()
|
||||||
realm.close()
|
}
|
||||||
return sessionParams
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun get(userId: String): SessionParams? {
|
override fun get(userId: String): SessionParams? {
|
||||||
val realm = Realm.getInstance(realmConfiguration)
|
return Realm.getInstance(realmConfiguration).use { realm ->
|
||||||
val sessionParams = realm
|
realm
|
||||||
.where(SessionParamsEntity::class.java)
|
.where(SessionParamsEntity::class.java)
|
||||||
.equalTo(SessionParamsEntityFields.USER_ID, userId)
|
.equalTo(SessionParamsEntityFields.USER_ID, userId)
|
||||||
.findAll()
|
.findAll()
|
||||||
.map { mapper.map(it) }
|
.map { mapper.map(it) }
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
realm.close()
|
}
|
||||||
return sessionParams
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAll(): List<SessionParams> {
|
override fun getAll(): List<SessionParams> {
|
||||||
val realm = Realm.getInstance(realmConfiguration)
|
return Realm.getInstance(realmConfiguration).use { realm ->
|
||||||
val sessionParams = realm
|
realm
|
||||||
.where(SessionParamsEntity::class.java)
|
.where(SessionParamsEntity::class.java)
|
||||||
.findAll()
|
.findAll()
|
||||||
.mapNotNull { mapper.map(it) }
|
.mapNotNull { mapper.map(it) }
|
||||||
realm.close()
|
}
|
||||||
return sessionParams
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun save(sessionParams: SessionParams) {
|
override suspend fun save(sessionParams: SessionParams) {
|
||||||
awaitTransaction(realmConfiguration) {
|
awaitTransaction(realmConfiguration) {
|
||||||
val entity = mapper.map(sessionParams)
|
val entity = mapper.map(sessionParams)
|
||||||
if (entity != null) {
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
|
@ -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
|
||||||
|
)
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,8 +18,12 @@ package im.vector.matrix.android.internal.auth.registration
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
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.api.util.JsonDict
|
||||||
import im.vector.matrix.android.internal.auth.data.InteractiveAuthenticationFlow
|
import im.vector.matrix.android.internal.auth.data.InteractiveAuthenticationFlow
|
||||||
|
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class RegistrationFlowResponse(
|
data class RegistrationFlowResponse(
|
||||||
|
@ -50,4 +54,46 @@ data class RegistrationFlowResponse(
|
||||||
*/
|
*/
|
||||||
@Json(name = "params")
|
@Json(name = "params")
|
||||||
var params: JsonDict? = null
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
|
@ -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?
|
||||||
|
)
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
|
@ -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.ClearCacheTask
|
||||||
import im.vector.matrix.android.internal.session.cache.RealmClearCacheTask
|
import im.vector.matrix.android.internal.session.cache.RealmClearCacheTask
|
||||||
import io.realm.RealmConfiguration
|
import io.realm.RealmConfiguration
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
@ -66,6 +68,13 @@ internal abstract class CryptoModule {
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
@Provides
|
||||||
|
@SessionScope
|
||||||
|
fun providesCryptoCoroutineScope(): CoroutineScope {
|
||||||
|
return CoroutineScope(SupervisorJob())
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
@Provides
|
@Provides
|
||||||
@CryptoDatabase
|
@CryptoDatabase
|
||||||
|
|
|
@ -132,7 +132,8 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
private val loadRoomMembersTask: LoadRoomMembersTask,
|
private val loadRoomMembersTask: LoadRoomMembersTask,
|
||||||
private val monarchy: Monarchy,
|
private val monarchy: Monarchy,
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||||
private val taskExecutor: TaskExecutor
|
private val taskExecutor: TaskExecutor,
|
||||||
|
private val cryptoCoroutineScope: CoroutineScope
|
||||||
) : CryptoService {
|
) : CryptoService {
|
||||||
|
|
||||||
private val uiHandler = Handler(Looper.getMainLooper())
|
private val uiHandler = Handler(Looper.getMainLooper())
|
||||||
|
@ -243,7 +244,8 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isStarting.set(true)
|
isStarting.set(true)
|
||||||
GlobalScope.launch(coroutineDispatchers.crypto) {
|
|
||||||
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
internalStart(isInitialSync)
|
internalStart(isInitialSync)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -269,10 +271,9 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
isStarted.set(true)
|
isStarted.set(true)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Timber.e("Start failed: $it")
|
|
||||||
delay(1000)
|
|
||||||
isStarting.set(false)
|
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
|
* Close the crypto
|
||||||
*/
|
*/
|
||||||
fun close() = runBlocking(coroutineDispatchers.crypto) {
|
fun close() = runBlocking(coroutineDispatchers.crypto) {
|
||||||
|
cryptoCoroutineScope.coroutineContext.cancelChildren(CancellationException("Closing crypto module"))
|
||||||
|
|
||||||
|
outgoingRoomKeyRequestManager.stop()
|
||||||
|
|
||||||
olmDevice.release()
|
olmDevice.release()
|
||||||
cryptoStore.close()
|
cryptoStore.close()
|
||||||
outgoingRoomKeyRequestManager.stop()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Aways enabled on RiotX
|
// Aways enabled on RiotX
|
||||||
|
@ -305,19 +309,21 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
* @param syncResponse the syncResponse
|
* @param syncResponse the syncResponse
|
||||||
*/
|
*/
|
||||||
fun onSyncCompleted(syncResponse: SyncResponse) {
|
fun onSyncCompleted(syncResponse: SyncResponse) {
|
||||||
GlobalScope.launch(coroutineDispatchers.crypto) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
if (syncResponse.deviceLists != null) {
|
runCatching {
|
||||||
deviceListManager.handleDeviceListsChanges(syncResponse.deviceLists.changed, syncResponse.deviceLists.left)
|
if (syncResponse.deviceLists != null) {
|
||||||
}
|
deviceListManager.handleDeviceListsChanges(syncResponse.deviceLists.changed, syncResponse.deviceLists.left)
|
||||||
if (syncResponse.deviceOneTimeKeysCount != null) {
|
}
|
||||||
val currentCount = syncResponse.deviceOneTimeKeysCount.signedCurve25519 ?: 0
|
if (syncResponse.deviceOneTimeKeysCount != null) {
|
||||||
oneTimeKeysUploader.updateOneTimeKeyCount(currentCount)
|
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
|
if (isStarted()) {
|
||||||
deviceListManager.refreshOutdatedDeviceLists()
|
// Make sure we process to-device messages before generating new one-time-keys #2782
|
||||||
oneTimeKeysUploader.maybeUploadOneTimeKeys()
|
deviceListManager.refreshOutdatedDeviceLists()
|
||||||
incomingRoomKeyRequestManager.processReceivedRoomKeyRequests()
|
oneTimeKeysUploader.maybeUploadOneTimeKeys()
|
||||||
|
incomingRoomKeyRequestManager.processReceivedRoomKeyRequests()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -511,7 +517,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
eventType: String,
|
eventType: String,
|
||||||
roomId: String,
|
roomId: String,
|
||||||
callback: MatrixCallback<MXEncryptEventContentResult>) {
|
callback: MatrixCallback<MXEncryptEventContentResult>) {
|
||||||
GlobalScope.launch(coroutineDispatchers.crypto) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
if (!isStarted()) {
|
if (!isStarted()) {
|
||||||
Timber.v("## encryptEventContent() : wait after e2e init")
|
Timber.v("## encryptEventContent() : wait after e2e init")
|
||||||
internalStart(false)
|
internalStart(false)
|
||||||
|
@ -571,7 +577,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
* @param callback the callback to return data or null
|
* @param callback the callback to return data or null
|
||||||
*/
|
*/
|
||||||
override fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) {
|
override fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) {
|
||||||
GlobalScope.launch {
|
cryptoCoroutineScope.launch {
|
||||||
val result = runCatching {
|
val result = runCatching {
|
||||||
withContext(coroutineDispatchers.crypto) {
|
withContext(coroutineDispatchers.crypto) {
|
||||||
internalDecryptEvent(event, timeline)
|
internalDecryptEvent(event, timeline)
|
||||||
|
@ -621,7 +627,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
* @param event the event
|
* @param event the event
|
||||||
*/
|
*/
|
||||||
fun onToDeviceEvent(event: Event) {
|
fun onToDeviceEvent(event: Event) {
|
||||||
GlobalScope.launch(coroutineDispatchers.crypto) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
when (event.getClearType()) {
|
when (event.getClearType()) {
|
||||||
EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> {
|
EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> {
|
||||||
onRoomKeyEvent(event)
|
onRoomKeyEvent(event)
|
||||||
|
@ -661,7 +667,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
* @param event the encryption event.
|
* @param event the encryption event.
|
||||||
*/
|
*/
|
||||||
private fun onRoomEncryptionEvent(roomId: String, event: Event) {
|
private fun onRoomEncryptionEvent(roomId: String, event: Event) {
|
||||||
GlobalScope.launch(coroutineDispatchers.crypto) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
val params = LoadRoomMembersTask.Params(roomId)
|
val params = LoadRoomMembersTask.Params(roomId)
|
||||||
try {
|
try {
|
||||||
loadRoomMembersTask.execute(params)
|
loadRoomMembersTask.execute(params)
|
||||||
|
@ -753,7 +759,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
* @param callback the exported keys
|
* @param callback the exported keys
|
||||||
*/
|
*/
|
||||||
override fun exportRoomKeys(password: String, callback: MatrixCallback<ByteArray>) {
|
override fun exportRoomKeys(password: String, callback: MatrixCallback<ByteArray>) {
|
||||||
GlobalScope.launch(coroutineDispatchers.main) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
||||||
runCatching {
|
runCatching {
|
||||||
exportRoomKeys(password, MXMegolmExportEncryption.DEFAULT_ITERATION_COUNT)
|
exportRoomKeys(password, MXMegolmExportEncryption.DEFAULT_ITERATION_COUNT)
|
||||||
}.foldToCallback(callback)
|
}.foldToCallback(callback)
|
||||||
|
@ -791,7 +797,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
password: String,
|
password: String,
|
||||||
progressListener: ProgressListener?,
|
progressListener: ProgressListener?,
|
||||||
callback: MatrixCallback<ImportRoomKeysResult>) {
|
callback: MatrixCallback<ImportRoomKeysResult>) {
|
||||||
GlobalScope.launch(coroutineDispatchers.main) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
||||||
runCatching {
|
runCatching {
|
||||||
withContext(coroutineDispatchers.crypto) {
|
withContext(coroutineDispatchers.crypto) {
|
||||||
Timber.v("## importRoomKeys starts")
|
Timber.v("## importRoomKeys starts")
|
||||||
|
@ -839,7 +845,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
*/
|
*/
|
||||||
fun checkUnknownDevices(userIds: List<String>, callback: MatrixCallback<Unit>) {
|
fun checkUnknownDevices(userIds: List<String>, callback: MatrixCallback<Unit>) {
|
||||||
// force the refresh to ensure that the devices list is up-to-date
|
// force the refresh to ensure that the devices list is up-to-date
|
||||||
GlobalScope.launch(coroutineDispatchers.crypto) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
runCatching {
|
runCatching {
|
||||||
val keys = deviceListManager.downloadKeys(userIds, true)
|
val keys = deviceListManager.downloadKeys(userIds, true)
|
||||||
val unknownDevices = getUnknownDevices(keys)
|
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>>) {
|
override fun downloadKeys(userIds: List<String>, forceDownload: Boolean, callback: MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>>) {
|
||||||
GlobalScope.launch(coroutineDispatchers.crypto) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
runCatching {
|
runCatching {
|
||||||
deviceListManager.downloadKeys(userIds, forceDownload)
|
deviceListManager.downloadKeys(userIds, forceDownload)
|
||||||
}.foldToCallback(callback)
|
}.foldToCallback(callback)
|
||||||
|
|
|
@ -25,7 +25,6 @@ import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||||
import im.vector.matrix.android.internal.session.SessionScope
|
import im.vector.matrix.android.internal.session.SessionScope
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
|
|
||||||
@SessionScope
|
@SessionScope
|
||||||
internal class IncomingRoomKeyRequestManager @Inject constructor(
|
internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||||
|
@ -51,7 +50,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||||
*
|
*
|
||||||
* @param event the announcement event.
|
* @param event the announcement event.
|
||||||
*/
|
*/
|
||||||
suspend fun onRoomKeyRequestEvent(event: Event) {
|
fun onRoomKeyRequestEvent(event: Event) {
|
||||||
val roomKeyShare = event.getClearContent().toModel<RoomKeyShare>()
|
val roomKeyShare = event.getClearContent().toModel<RoomKeyShare>()
|
||||||
when (roomKeyShare?.action) {
|
when (roomKeyShare?.action) {
|
||||||
RoomKeyShare.ACTION_SHARE_REQUEST -> receivedRoomKeyRequests.add(IncomingRoomKeyRequest(event))
|
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}")
|
Timber.v("m.room_key_request from $userId:$deviceId for $roomId / ${body.sessionId} id ${request.requestId}")
|
||||||
if (userId == null || credentials.userId != userId) {
|
if (userId == null || credentials.userId != userId) {
|
||||||
// TODO: determine if we sent this device the keys already: in
|
// 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
|
return
|
||||||
}
|
}
|
||||||
// TODO: should we queue up requests we don't yet have keys for, in case they turn up later?
|
// 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.
|
// the keys for the requested events, and can drop the requests.
|
||||||
val decryptor = roomDecryptorProvider.getRoomDecryptor(roomId, alg)
|
val decryptor = roomDecryptorProvider.getRoomDecryptor(roomId, alg)
|
||||||
if (null == decryptor) {
|
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
|
continue
|
||||||
}
|
}
|
||||||
if (!decryptor.hasKeysForKeyRequest(request)) {
|
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)
|
cryptoStore.deleteIncomingRoomKeyRequest(request)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -139,7 +138,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||||
if (null != receivedRoomKeyRequestCancellations) {
|
if (null != receivedRoomKeyRequestCancellations) {
|
||||||
for (request in receivedRoomKeyRequestCancellations!!) {
|
for (request in receivedRoomKeyRequestCancellations!!) {
|
||||||
Timber.v("## ## processReceivedRoomKeyRequests() : m.room_key_request cancellation for " + request.userId
|
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
|
// 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
|
// about, but we don't currently have a record of that, so we just pass
|
||||||
|
|
|
@ -764,7 +764,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
} else {
|
} 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)
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,8 +42,6 @@ internal class OneTimeKeysUploader @Inject constructor(
|
||||||
private var lastOneTimeKeyCheck: Long = 0
|
private var lastOneTimeKeyCheck: Long = 0
|
||||||
private var oneTimeKeyCount: Int? = null
|
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
|
* 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.
|
* _onSyncCompleted). The count is e.g. coming from a /sync response.
|
||||||
|
@ -59,10 +57,12 @@ internal class OneTimeKeysUploader @Inject constructor(
|
||||||
*/
|
*/
|
||||||
suspend fun maybeUploadOneTimeKeys() {
|
suspend fun maybeUploadOneTimeKeys() {
|
||||||
if (oneTimeKeyCheckInProgress) {
|
if (oneTimeKeyCheckInProgress) {
|
||||||
|
Timber.v("maybeUploadOneTimeKeys: already in progress")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (System.currentTimeMillis() - lastOneTimeKeyCheck < ONE_TIME_KEY_UPLOAD_PERIOD) {
|
if (System.currentTimeMillis() - lastOneTimeKeyCheck < ONE_TIME_KEY_UPLOAD_PERIOD) {
|
||||||
// we've done a key upload recently.
|
// we've done a key upload recently.
|
||||||
|
Timber.v("maybeUploadOneTimeKeys: executed too recently")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,12 +79,8 @@ internal class OneTimeKeysUploader @Inject constructor(
|
||||||
// discard the oldest private keys first. This will eventually clean
|
// discard the oldest private keys first. This will eventually clean
|
||||||
// out stale private keys that won't receive a message.
|
// out stale private keys that won't receive a message.
|
||||||
val keyLimit = floor(maxOneTimeKeys / 2.0).toInt()
|
val keyLimit = floor(maxOneTimeKeys / 2.0).toInt()
|
||||||
if (oneTimeKeyCount != null) {
|
val oneTimeKeyCountFromSync = oneTimeKeyCount
|
||||||
uploadOTK(oneTimeKeyCount!!, keyLimit)
|
if (oneTimeKeyCountFromSync != null) {
|
||||||
} else {
|
|
||||||
// ask the server how many keys we have
|
|
||||||
val uploadKeysParams = UploadKeysTask.Params(null, null, credentials.deviceId!!)
|
|
||||||
val response = uploadKeysTask.execute(uploadKeysParams)
|
|
||||||
// We need to keep a pool of one time public keys on the server so that
|
// 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
|
// other devices can start conversations with us. But we can only store
|
||||||
// a finite number of private keys in the olm Account object.
|
// 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.
|
// private keys clogging up our local storage.
|
||||||
// So we need some kind of engineering compromise to balance all of
|
// So we need some kind of engineering compromise to balance all of
|
||||||
// these factors.
|
// these factors.
|
||||||
// TODO Why we do not set oneTimeKeyCount here?
|
try {
|
||||||
// TODO This is not needed anymore, see https://github.com/matrix-org/matrix-js-sdk/pull/493 (TODO on iOS also)
|
val uploadedKeys = uploadOTK(oneTimeKeyCountFromSync, keyLimit)
|
||||||
val keyCount = response.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)
|
Timber.v("## uploadKeys() : success, $uploadedKeys key(s) sent")
|
||||||
uploadOTK(keyCount, keyLimit)
|
} 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 keyCount the key count
|
||||||
* @param keyLimit the limit
|
* @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 (keyLimit <= keyCount) {
|
||||||
// If we don't need to generate any more keys then we are done.
|
// 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)
|
val keysThisLoop = min(keyLimit - keyCount, ONE_TIME_KEY_GENERATION_MAX_NUMBER)
|
||||||
olmDevice.generateOneTimeKeys(keysThisLoop)
|
olmDevice.generateOneTimeKeys(keysThisLoop)
|
||||||
val response = uploadOneTimeKeys()
|
val response = uploadOneTimeKeys(olmDevice.getOneTimeKeys())
|
||||||
|
olmDevice.markKeysAsPublished()
|
||||||
|
|
||||||
if (response.hasOneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)) {
|
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 {
|
} 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")
|
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 {
|
private suspend fun uploadOneTimeKeys(oneTimeKeys: Map<String, Map<String, String>>?): KeysUploadResponse {
|
||||||
val oneTimeKeys = olmDevice.getOneTimeKeys()
|
|
||||||
val oneTimeJson = mutableMapOf<String, Any>()
|
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) {
|
curve25519Map.forEach { (key_id, value) ->
|
||||||
for ((key_id, value) in curve25519Map) {
|
val k = mutableMapOf<String, Any>()
|
||||||
val k = mutableMapOf<String, Any>()
|
k["key"] = value
|
||||||
k["key"] = value
|
|
||||||
|
|
||||||
// the key is also signed
|
// the key is also signed
|
||||||
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, k)
|
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
|
// For now, we set the device id explicitly, as we may not be using the
|
||||||
// same one as used in login.
|
// same one as used in login.
|
||||||
val uploadParams = UploadKeysTask.Params(null, oneTimeJson, credentials.deviceId!!)
|
val uploadParams = UploadKeysTask.Params(null, oneTimeJson, credentials.deviceId!!)
|
||||||
val response = uploadKeysTask.execute(uploadParams)
|
return uploadKeysTask.execute(uploadParams)
|
||||||
lastPublishedOneTimeKeys = oneTimeKeys
|
|
||||||
olmDevice.markKeysAsPublished()
|
|
||||||
return response
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -63,6 +63,7 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor(
|
||||||
*/
|
*/
|
||||||
fun stop() {
|
fun stop() {
|
||||||
isClientRunning = false
|
isClientRunning = false
|
||||||
|
stopTimer()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -171,6 +172,10 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor(
|
||||||
}, SEND_KEY_REQUESTS_DELAY_MS.toLong())
|
}, SEND_KEY_REQUESTS_DELAY_MS.toLong())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun stopTimer() {
|
||||||
|
BACKGROUND_HANDLER.removeCallbacksAndMessages(null)
|
||||||
|
}
|
||||||
|
|
||||||
// look for and send any queued requests. Runs itself recursively until
|
// 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
|
// there are no more requests, or there is an error (in which case, the
|
||||||
// timer will be restarted before the promise resolves).
|
// timer will be restarted before the promise resolves).
|
||||||
|
@ -187,7 +192,7 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor(
|
||||||
OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND))
|
OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND))
|
||||||
|
|
||||||
if (null == outgoingRoomKeyRequest) {
|
if (null == outgoingRoomKeyRequest) {
|
||||||
Timber.e("## sendOutgoingRoomKeyRequests() : No more outgoing room key requests")
|
Timber.v("## sendOutgoingRoomKeyRequests() : No more outgoing room key requests")
|
||||||
sendOutgoingRoomKeyRequestsRunning.set(false)
|
sendOutgoingRoomKeyRequestsRunning.set(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.store.IMXCryptoStore
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
@ -46,8 +46,9 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
||||||
private val cryptoStore: IMXCryptoStore,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
private val sendToDeviceTask: SendToDeviceTask,
|
private val sendToDeviceTask: SendToDeviceTask,
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers)
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||||
: IMXDecrypting {
|
private val cryptoCoroutineScope: CoroutineScope
|
||||||
|
) : IMXDecrypting {
|
||||||
|
|
||||||
var newSessionListener: NewSessionListener? = null
|
var newSessionListener: NewSessionListener? = null
|
||||||
|
|
||||||
|
@ -61,7 +62,7 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||||
return decryptEvent(event, timeline, true)
|
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()) {
|
if (event.roomId.isNullOrBlank()) {
|
||||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
|
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)
|
||||||
}
|
}
|
||||||
|
@ -292,7 +293,7 @@ internal class MXMegolmDecryption(private val userId: String,
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val userId = request.userId ?: return
|
val userId = request.userId ?: return
|
||||||
GlobalScope.launch(coroutineDispatchers.crypto) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||||
runCatching { deviceListManager.downloadKeys(listOf(userId), false) }
|
runCatching { deviceListManager.downloadKeys(listOf(userId), false) }
|
||||||
.mapCatching {
|
.mapCatching {
|
||||||
val deviceId = request.deviceId
|
val deviceId = request.deviceId
|
||||||
|
|
|
@ -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.crypto.tasks.SendToDeviceTask
|
||||||
import im.vector.matrix.android.internal.di.UserId
|
import im.vector.matrix.android.internal.di.UserId
|
||||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class MXMegolmDecryptionFactory @Inject constructor(@UserId private val userId: String,
|
internal class MXMegolmDecryptionFactory @Inject constructor(
|
||||||
private val olmDevice: MXOlmDevice,
|
@UserId private val userId: String,
|
||||||
private val deviceListManager: DeviceListManager,
|
private val olmDevice: MXOlmDevice,
|
||||||
private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager,
|
private val deviceListManager: DeviceListManager,
|
||||||
private val messageEncrypter: MessageEncrypter,
|
private val outgoingRoomKeyRequestManager: OutgoingRoomKeyRequestManager,
|
||||||
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
private val messageEncrypter: MessageEncrypter,
|
||||||
private val cryptoStore: IMXCryptoStore,
|
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
|
||||||
private val sendToDeviceTask: SendToDeviceTask,
|
private val cryptoStore: IMXCryptoStore,
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers) {
|
private val sendToDeviceTask: SendToDeviceTask,
|
||||||
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||||
|
private val cryptoCoroutineScope: CoroutineScope
|
||||||
|
) {
|
||||||
|
|
||||||
fun create(): MXMegolmDecryption {
|
fun create(): MXMegolmDecryption {
|
||||||
return MXMegolmDecryption(
|
return MXMegolmDecryption(
|
||||||
|
@ -47,6 +51,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor(@UserId private val
|
||||||
ensureOlmSessionsForDevicesAction,
|
ensureOlmSessionsForDevicesAction,
|
||||||
cryptoStore,
|
cryptoStore,
|
||||||
sendToDeviceTask,
|
sendToDeviceTask,
|
||||||
coroutineDispatchers)
|
coroutineDispatchers,
|
||||||
|
cryptoCoroutineScope)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.JsonCanonicalizer
|
||||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||||
import im.vector.matrix.android.internal.util.awaitCallback
|
import im.vector.matrix.android.internal.util.awaitCallback
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.matrix.olm.OlmException
|
import org.matrix.olm.OlmException
|
||||||
|
@ -102,7 +102,8 @@ internal class KeysBackup @Inject constructor(
|
||||||
private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask,
|
private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask,
|
||||||
// Task executor
|
// Task executor
|
||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
private val coroutineDispatchers: MatrixCoroutineDispatchers
|
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||||
|
private val cryptoCoroutineScope: CoroutineScope
|
||||||
) : KeysBackupService {
|
) : KeysBackupService {
|
||||||
|
|
||||||
private val uiHandler = Handler(Looper.getMainLooper())
|
private val uiHandler = Handler(Looper.getMainLooper())
|
||||||
|
@ -143,7 +144,7 @@ internal class KeysBackup @Inject constructor(
|
||||||
override fun prepareKeysBackupVersion(password: String?,
|
override fun prepareKeysBackupVersion(password: String?,
|
||||||
progressListener: ProgressListener?,
|
progressListener: ProgressListener?,
|
||||||
callback: MatrixCallback<MegolmBackupCreationInfo>) {
|
callback: MatrixCallback<MegolmBackupCreationInfo>) {
|
||||||
GlobalScope.launch(coroutineDispatchers.main) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
||||||
runCatching {
|
runCatching {
|
||||||
withContext(coroutineDispatchers.crypto) {
|
withContext(coroutineDispatchers.crypto) {
|
||||||
val olmPkDecryption = OlmPkDecryption()
|
val olmPkDecryption = OlmPkDecryption()
|
||||||
|
@ -233,7 +234,7 @@ internal class KeysBackup @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun deleteBackup(version: String, callback: MatrixCallback<Unit>?) {
|
override fun deleteBackup(version: String, callback: MatrixCallback<Unit>?) {
|
||||||
GlobalScope.launch(coroutineDispatchers.main) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
||||||
withContext(coroutineDispatchers.crypto) {
|
withContext(coroutineDispatchers.crypto) {
|
||||||
// If we're currently backing up to this backup... stop.
|
// If we're currently backing up to this backup... stop.
|
||||||
// (We start using it automatically in createKeysBackupVersion so this is symmetrical).
|
// (We start using it automatically in createKeysBackupVersion so this is symmetrical).
|
||||||
|
@ -344,9 +345,7 @@ internal class KeysBackup @Inject constructor(
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}.also { keysBackupStateManager.addListener(it) }
|
||||||
|
|
||||||
keysBackupStateManager.addListener(keysBackupStateListener!!)
|
|
||||||
|
|
||||||
backupKeys()
|
backupKeys()
|
||||||
}
|
}
|
||||||
|
@ -448,7 +447,7 @@ internal class KeysBackup @Inject constructor(
|
||||||
|
|
||||||
callback.onFailure(IllegalArgumentException("Missing element"))
|
callback.onFailure(IllegalArgumentException("Missing element"))
|
||||||
} else {
|
} else {
|
||||||
GlobalScope.launch(coroutineDispatchers.main) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
||||||
val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) {
|
val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) {
|
||||||
// Get current signatures, or create an empty set
|
// Get current signatures, or create an empty set
|
||||||
val myUserSignatures = authData.signatures?.get(userId)?.toMutableMap()
|
val myUserSignatures = authData.signatures?.get(userId)?.toMutableMap()
|
||||||
|
@ -523,7 +522,7 @@ internal class KeysBackup @Inject constructor(
|
||||||
callback: MatrixCallback<Unit>) {
|
callback: MatrixCallback<Unit>) {
|
||||||
Timber.v("trustKeysBackupVersionWithRecoveryKey: version ${keysBackupVersion.version}")
|
Timber.v("trustKeysBackupVersionWithRecoveryKey: version ${keysBackupVersion.version}")
|
||||||
|
|
||||||
GlobalScope.launch(coroutineDispatchers.main) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
||||||
val isValid = withContext(coroutineDispatchers.crypto) {
|
val isValid = withContext(coroutineDispatchers.crypto) {
|
||||||
isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)
|
isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)
|
||||||
}
|
}
|
||||||
|
@ -543,7 +542,7 @@ internal class KeysBackup @Inject constructor(
|
||||||
callback: MatrixCallback<Unit>) {
|
callback: MatrixCallback<Unit>) {
|
||||||
Timber.v("trustKeysBackupVersionWithPassphrase: version ${keysBackupVersion.version}")
|
Timber.v("trustKeysBackupVersionWithPassphrase: version ${keysBackupVersion.version}")
|
||||||
|
|
||||||
GlobalScope.launch(coroutineDispatchers.main) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
||||||
val recoveryKey = withContext(coroutineDispatchers.crypto) {
|
val recoveryKey = withContext(coroutineDispatchers.crypto) {
|
||||||
recoveryKeyFromPassword(password, keysBackupVersion, null)
|
recoveryKeyFromPassword(password, keysBackupVersion, null)
|
||||||
}
|
}
|
||||||
|
@ -614,7 +613,7 @@ internal class KeysBackup @Inject constructor(
|
||||||
callback: MatrixCallback<ImportRoomKeysResult>) {
|
callback: MatrixCallback<ImportRoomKeysResult>) {
|
||||||
Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}")
|
Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}")
|
||||||
|
|
||||||
GlobalScope.launch(coroutineDispatchers.main) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
||||||
runCatching {
|
runCatching {
|
||||||
val decryption = withContext(coroutineDispatchers.crypto) {
|
val decryption = withContext(coroutineDispatchers.crypto) {
|
||||||
// Check if the recovery is valid before going any further
|
// Check if the recovery is valid before going any further
|
||||||
|
@ -695,7 +694,7 @@ internal class KeysBackup @Inject constructor(
|
||||||
callback: MatrixCallback<ImportRoomKeysResult>) {
|
callback: MatrixCallback<ImportRoomKeysResult>) {
|
||||||
Timber.v("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}")
|
Timber.v("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}")
|
||||||
|
|
||||||
GlobalScope.launch(coroutineDispatchers.main) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
||||||
runCatching {
|
runCatching {
|
||||||
val progressListener = if (stepProgressListener != null) {
|
val progressListener = if (stepProgressListener != null) {
|
||||||
object : ProgressListener {
|
object : ProgressListener {
|
||||||
|
@ -729,8 +728,8 @@ internal class KeysBackup @Inject constructor(
|
||||||
* parameters and always returns a KeysBackupData object through the Callback
|
* parameters and always returns a KeysBackupData object through the Callback
|
||||||
*/
|
*/
|
||||||
private suspend fun getKeys(sessionId: String?,
|
private suspend fun getKeys(sessionId: String?,
|
||||||
roomId: String?,
|
roomId: String?,
|
||||||
version: String): KeysBackupData {
|
version: String): KeysBackupData {
|
||||||
return if (roomId != null && sessionId != null) {
|
return if (roomId != null && sessionId != null) {
|
||||||
// Get key for the room and for the session
|
// Get key for the room and for the session
|
||||||
val data = getRoomSessionDataTask.execute(GetRoomSessionDataTask.Params(roomId, sessionId, version))
|
val data = getRoomSessionDataTask.execute(GetRoomSessionDataTask.Params(roomId, sessionId, version))
|
||||||
|
@ -1154,7 +1153,7 @@ internal class KeysBackup @Inject constructor(
|
||||||
|
|
||||||
keysBackupStateManager.state = KeysBackupState.BackingUp
|
keysBackupStateManager.state = KeysBackupState.BackingUp
|
||||||
|
|
||||||
GlobalScope.launch(coroutineDispatchers.main) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
||||||
withContext(coroutineDispatchers.crypto) {
|
withContext(coroutineDispatchers.crypto) {
|
||||||
Timber.v("backupKeys: 2 - Encrypting keys")
|
Timber.v("backupKeys: 2 - Encrypting keys")
|
||||||
|
|
||||||
|
|
|
@ -53,10 +53,10 @@ internal class DefaultUploadKeysTask @Inject constructor(private val cryptoApi:
|
||||||
}
|
}
|
||||||
|
|
||||||
return executeRequest {
|
return executeRequest {
|
||||||
if (encodedDeviceId.isNullOrBlank()) {
|
apiCall = if (encodedDeviceId.isBlank()) {
|
||||||
apiCall = cryptoApi.uploadKeys(body)
|
cryptoApi.uploadKeys(body)
|
||||||
} else {
|
} else {
|
||||||
apiCall = cryptoApi.uploadKeys(encodedDeviceId, body)
|
cryptoApi.uploadKeys(encodedDeviceId, body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,7 +83,7 @@ internal class SessionRealmConfigurationFactory @Inject constructor(private val
|
||||||
try {
|
try {
|
||||||
File(directory, file).deleteRecursively()
|
File(directory, file).deleteRecursively()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "Unable to move files")
|
Timber.e(e, "Unable to delete files")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.mapper.toEntity
|
||||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
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.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.ReadReceiptEntity
|
||||||
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity
|
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity
|
||||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||||
|
@ -140,7 +139,7 @@ internal fun ChunkEntity.add(roomId: String,
|
||||||
val senderId = event.senderId ?: ""
|
val senderId = event.senderId ?: ""
|
||||||
|
|
||||||
val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst()
|
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
|
// 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.roomId = roomId
|
||||||
it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
|
it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
|
||||||
it.readReceipts = readReceiptsSummaryEntity
|
it.readReceipts = readReceiptsSummaryEntity
|
||||||
it.readMarker = ReadMarkerEntity.where(realm, roomId = roomId, eventId = eventId).findFirst()
|
|
||||||
}
|
}
|
||||||
val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size
|
val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size
|
||||||
timelineEvents.add(position, eventEntity)
|
timelineEvents.add(position, eventEntity)
|
||||||
|
@ -176,14 +174,14 @@ internal fun ChunkEntity.add(roomId: String,
|
||||||
|
|
||||||
internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
|
internal fun ChunkEntity.lastDisplayIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
|
||||||
return when (direction) {
|
return when (direction) {
|
||||||
PaginationDirection.FORWARDS -> forwardsDisplayIndex
|
PaginationDirection.FORWARDS -> forwardsDisplayIndex
|
||||||
PaginationDirection.BACKWARDS -> backwardsDisplayIndex
|
PaginationDirection.BACKWARDS -> backwardsDisplayIndex
|
||||||
} ?: defaultValue
|
} ?: defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
|
internal fun ChunkEntity.lastStateIndex(direction: PaginationDirection, defaultValue: Int = 0): Int {
|
||||||
return when (direction) {
|
return when (direction) {
|
||||||
PaginationDirection.FORWARDS -> forwardsStateIndex
|
PaginationDirection.FORWARDS -> forwardsStateIndex
|
||||||
PaginationDirection.BACKWARDS -> backwardsStateIndex
|
PaginationDirection.BACKWARDS -> backwardsStateIndex
|
||||||
} ?: defaultValue
|
} ?: defaultValue
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS
|
||||||
}
|
}
|
||||||
return TimelineEvent(
|
return TimelineEvent(
|
||||||
root = timelineEventEntity.root?.asDomain()
|
root = timelineEventEntity.root?.asDomain()
|
||||||
?: Event("", timelineEventEntity.eventId),
|
?: Event("", timelineEventEntity.eventId),
|
||||||
annotations = timelineEventEntity.annotations?.asDomain(),
|
annotations = timelineEventEntity.annotations?.asDomain(),
|
||||||
localId = timelineEventEntity.localId,
|
localId = timelineEventEntity.localId,
|
||||||
displayIndex = timelineEventEntity.root?.displayIndex ?: 0,
|
displayIndex = timelineEventEntity.root?.displayIndex ?: 0,
|
||||||
|
@ -45,8 +45,7 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS
|
||||||
senderAvatar = timelineEventEntity.senderAvatar,
|
senderAvatar = timelineEventEntity.senderAvatar,
|
||||||
readReceipts = readReceipts?.sortedByDescending {
|
readReceipts = readReceipts?.sortedByDescending {
|
||||||
it.originServerTs
|
it.originServerTs
|
||||||
} ?: emptyList(),
|
} ?: emptyList()
|
||||||
hasReadMarker = timelineEventEntity.readMarker?.eventId?.isNotEmpty() == true
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,8 +17,6 @@
|
||||||
package im.vector.matrix.android.internal.database.model
|
package im.vector.matrix.android.internal.database.model
|
||||||
|
|
||||||
import io.realm.RealmObject
|
import io.realm.RealmObject
|
||||||
import io.realm.RealmResults
|
|
||||||
import io.realm.annotations.LinkingObjects
|
|
||||||
import io.realm.annotations.PrimaryKey
|
import io.realm.annotations.PrimaryKey
|
||||||
|
|
||||||
internal open class ReadMarkerEntity(
|
internal open class ReadMarkerEntity(
|
||||||
|
@ -27,8 +25,5 @@ internal open class ReadMarkerEntity(
|
||||||
var eventId: String = ""
|
var eventId: String = ""
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
|
||||||
@LinkingObjects("readMarker")
|
|
||||||
val timelineEvent: RealmResults<TimelineEventEntity>? = null
|
|
||||||
|
|
||||||
companion object
|
companion object
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,8 +30,7 @@ internal open class TimelineEventEntity(var localId: Long = 0,
|
||||||
var isUniqueDisplayName: Boolean = false,
|
var isUniqueDisplayName: Boolean = false,
|
||||||
var senderAvatar: String? = null,
|
var senderAvatar: String? = null,
|
||||||
var senderMembershipEvent: EventEntity? = null,
|
var senderMembershipEvent: EventEntity? = null,
|
||||||
var readReceipts: ReadReceiptsSummaryEntity? = null,
|
var readReceipts: ReadReceiptsSummaryEntity? = null
|
||||||
var readMarker: ReadMarkerEntity? = null
|
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
|
||||||
@LinkingObjects("timelineEvents")
|
@LinkingObjects("timelineEvents")
|
||||||
|
|
|
@ -16,26 +16,27 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.database.query
|
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.database.model.FilterEntity
|
||||||
import im.vector.matrix.android.internal.session.filter.FilterFactory
|
import im.vector.matrix.android.internal.session.filter.FilterFactory
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
|
import io.realm.kotlin.createObject
|
||||||
import io.realm.kotlin.where
|
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
|
* Get the current filter, create one if it does not exist
|
||||||
*/
|
*/
|
||||||
internal suspend fun FilterEntity.Companion.getFilter(realm: Realm): FilterEntity {
|
internal fun FilterEntity.Companion.getOrCreate(realm: Realm): FilterEntity {
|
||||||
var filter = realm.where<FilterEntity>().findFirst()
|
return get(realm) ?: realm.createObject<FilterEntity>()
|
||||||
if (filter == null) {
|
.apply {
|
||||||
filter = FilterEntity().apply {
|
filterBodyJson = FilterFactory.createDefaultFilterBody().toJSONString()
|
||||||
filterBodyJson = FilterFactory.createDefaultFilterBody().toJSONString()
|
roomEventFilterJson = FilterFactory.createDefaultRoomFilter().toJSONString()
|
||||||
roomEventFilterJson = FilterFactory.createDefaultRoomFilter().toJSONString()
|
filterId = ""
|
||||||
filterId = ""
|
}
|
||||||
}
|
|
||||||
awaitTransaction(realm.configuration) {
|
|
||||||
it.insert(filter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filter
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,13 +22,9 @@ import io.realm.Realm
|
||||||
import io.realm.RealmQuery
|
import io.realm.RealmQuery
|
||||||
import io.realm.kotlin.where
|
import io.realm.kotlin.where
|
||||||
|
|
||||||
internal fun ReadMarkerEntity.Companion.where(realm: Realm, roomId: String, eventId: String? = null): RealmQuery<ReadMarkerEntity> {
|
internal fun ReadMarkerEntity.Companion.where(realm: Realm, roomId: String): RealmQuery<ReadMarkerEntity> {
|
||||||
val query = realm.where<ReadMarkerEntity>()
|
return realm.where<ReadMarkerEntity>()
|
||||||
.equalTo(ReadMarkerEntityFields.ROOM_ID, roomId)
|
.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 {
|
internal fun ReadMarkerEntity.Companion.getOrCreate(realm: Realm, roomId: String): ReadMarkerEntity {
|
||||||
|
|
|
@ -18,7 +18,9 @@ package im.vector.matrix.android.internal.database.query
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
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.ChunkEntity
|
||||||
|
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.ReadReceiptEntity
|
||||||
|
import io.realm.Realm
|
||||||
|
|
||||||
internal fun isEventRead(monarchy: Monarchy,
|
internal fun isEventRead(monarchy: Monarchy,
|
||||||
userId: String?,
|
userId: String?,
|
||||||
|
@ -39,8 +41,10 @@ internal fun isEventRead(monarchy: Monarchy,
|
||||||
isEventRead = if (eventToCheck?.sender == userId) {
|
isEventRead = if (eventToCheck?.sender == userId) {
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst() ?: return@doWithRealm
|
val readReceipt = ReadReceiptEntity.where(realm, roomId, userId).findFirst()
|
||||||
val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex ?: Int.MIN_VALUE
|
?: return@doWithRealm
|
||||||
|
val readReceiptIndex = liveChunk.timelineEvents.find(readReceipt.eventId)?.root?.displayIndex
|
||||||
|
?: Int.MIN_VALUE
|
||||||
val eventToCheckIndex = eventToCheck?.displayIndex ?: Int.MAX_VALUE
|
val eventToCheckIndex = eventToCheck?.displayIndex ?: Int.MAX_VALUE
|
||||||
|
|
||||||
eventToCheckIndex <= readReceiptIndex
|
eventToCheckIndex <= readReceiptIndex
|
||||||
|
@ -49,3 +53,21 @@ internal fun isEventRead(monarchy: Monarchy,
|
||||||
|
|
||||||
return isEventRead
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ import com.squareup.moshi.Moshi
|
||||||
import dagger.BindsInstance
|
import dagger.BindsInstance
|
||||||
import dagger.Component
|
import dagger.Component
|
||||||
import im.vector.matrix.android.api.Matrix
|
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.SessionManager
|
||||||
import im.vector.matrix.android.internal.auth.AuthModule
|
import im.vector.matrix.android.internal.auth.AuthModule
|
||||||
import im.vector.matrix.android.internal.auth.SessionParamsStore
|
import im.vector.matrix.android.internal.auth.SessionParamsStore
|
||||||
|
@ -44,7 +44,7 @@ internal interface MatrixComponent {
|
||||||
@Unauthenticated
|
@Unauthenticated
|
||||||
fun okHttpClient(): OkHttpClient
|
fun okHttpClient(): OkHttpClient
|
||||||
|
|
||||||
fun authenticator(): Authenticator
|
fun authenticationService(): AuthenticationService
|
||||||
|
|
||||||
fun context(): Context
|
fun context(): Context
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.network
|
||||||
internal object NetworkConstants {
|
internal object NetworkConstants {
|
||||||
|
|
||||||
private const val URI_API_PREFIX_PATH = "_matrix/client"
|
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_R0 = "$URI_API_PREFIX_PATH/r0/"
|
||||||
const val URI_API_PREFIX_PATH_UNSTABLE = "$URI_API_PREFIX_PATH/unstable/"
|
const val URI_API_PREFIX_PATH_UNSTABLE = "$URI_API_PREFIX_PATH/unstable/"
|
||||||
|
|
||||||
|
|
|
@ -22,12 +22,14 @@ import arrow.core.Try
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||||
import im.vector.matrix.android.api.session.file.FileService
|
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.ElementToDecrypt
|
||||||
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
|
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
|
||||||
import im.vector.matrix.android.internal.di.UserMd5
|
import im.vector.matrix.android.internal.di.UserMd5
|
||||||
import im.vector.matrix.android.internal.extensions.foldToCallback
|
import im.vector.matrix.android.internal.extensions.foldToCallback
|
||||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||||
import im.vector.matrix.android.internal.util.md5
|
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 im.vector.matrix.android.internal.util.writeToFile
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -55,8 +57,8 @@ internal class DefaultFileService @Inject constructor(private val context: Conte
|
||||||
fileName: String,
|
fileName: String,
|
||||||
url: String?,
|
url: String?,
|
||||||
elementToDecrypt: ElementToDecrypt?,
|
elementToDecrypt: ElementToDecrypt?,
|
||||||
callback: MatrixCallback<File>) {
|
callback: MatrixCallback<File>): Cancelable {
|
||||||
GlobalScope.launch(coroutineDispatchers.main) {
|
return GlobalScope.launch(coroutineDispatchers.main) {
|
||||||
withContext(coroutineDispatchers.io) {
|
withContext(coroutineDispatchers.io) {
|
||||||
Try {
|
Try {
|
||||||
val folder = getFolder(downloadMode, id)
|
val folder = getFolder(downloadMode, id)
|
||||||
|
@ -96,7 +98,7 @@ internal class DefaultFileService @Inject constructor(private val context: Conte
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.foldToCallback(callback)
|
.foldToCallback(callback)
|
||||||
}
|
}.toCancelable()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFolder(downloadMode: FileService.DownloadMode, id: String): File {
|
private fun getFolder(downloadMode: FileService.DownloadMode, id: String): File {
|
||||||
|
|
|
@ -101,7 +101,7 @@ class DefaultInitialSyncProgressService @Inject constructor() : InitialSyncProgr
|
||||||
val parentProgress = (currentProgress * parentWeight).toInt()
|
val parentProgress = (currentProgress * parentWeight).toInt()
|
||||||
it.setProgress(offset + parentProgress)
|
it.setProgress(offset + parentProgress)
|
||||||
} ?: run {
|
} ?: run {
|
||||||
Timber.e("--- ${leaf().nameRes}: $currentProgress")
|
Timber.v("--- ${leaf().nameRes}: $currentProgress")
|
||||||
status.postValue(
|
status.postValue(
|
||||||
InitialSyncProgressService.Status(leaf().nameRes, currentProgress)
|
InitialSyncProgressService.Status(leaf().nameRes, currentProgress)
|
||||||
)
|
)
|
||||||
|
|
|
@ -19,7 +19,8 @@ package im.vector.matrix.android.internal.session.filter
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.internal.database.model.FilterEntity
|
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.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 im.vector.matrix.android.internal.util.awaitTransaction
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import io.realm.kotlin.where
|
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 {
|
override suspend fun storeFilter(filterBody: FilterBody, roomEventFilter: RoomEventFilter): Boolean {
|
||||||
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
|
return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
|
||||||
val filter = FilterEntity.getFilter(realm)
|
val filter = FilterEntity.get(realm)
|
||||||
val result = if (filter.filterBodyJson != filterBody.toJSONString()) {
|
// Filter has changed, or no filter Id yet
|
||||||
// Filter has changed, store it and reset the filter Id
|
filter == null
|
||||||
monarchy.awaitTransaction {
|
|| 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
|
// We manage only one filter for now
|
||||||
val filterBodyJson = filterBody.toJSONString()
|
val filterBodyJson = filterBody.toJSONString()
|
||||||
val roomEventFilterJson = roomEventFilter.toJSONString()
|
val roomEventFilterJson = roomEventFilter.toJSONString()
|
||||||
|
|
||||||
val filterEntity = FilterEntity.getFilter(it)
|
val filterEntity = FilterEntity.getOrCreate(realm)
|
||||||
|
|
||||||
filterEntity.filterBodyJson = filterBodyJson
|
filterEntity.filterBodyJson = filterBodyJson
|
||||||
filterEntity.roomEventFilterJson = roomEventFilterJson
|
filterEntity.roomEventFilterJson = roomEventFilterJson
|
||||||
// Reset filterId
|
// Reset filterId
|
||||||
filterEntity.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 {
|
override suspend fun getFilter(): String {
|
||||||
return Realm.getInstance(monarchy.realmConfiguration).use {
|
return Realm.getInstance(monarchy.realmConfiguration).use {
|
||||||
val filter = FilterEntity.getFilter(it)
|
val filter = FilterEntity.getOrCreate(it)
|
||||||
if (filter.filterId.isBlank()) {
|
if (filter.filterId.isBlank()) {
|
||||||
// Use the Json format
|
// Use the Json format
|
||||||
filter.filterBodyJson
|
filter.filterBodyJson
|
||||||
|
@ -80,7 +83,7 @@ internal class DefaultFilterRepository @Inject constructor(private val monarchy:
|
||||||
|
|
||||||
override suspend fun getRoomFilter(): String {
|
override suspend fun getRoomFilter(): String {
|
||||||
return Realm.getInstance(monarchy.realmConfiguration).use {
|
return Realm.getInstance(monarchy.realmConfiguration).use {
|
||||||
FilterEntity.getFilter(it).roomEventFilterJson
|
FilterEntity.getOrCreate(it).roomEventFilterJson
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -298,7 +298,7 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} 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}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.session.room.read
|
||||||
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
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.RoomSummaryEntity
|
||||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||||
import im.vector.matrix.android.internal.database.query.*
|
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) {
|
override suspend fun execute(params: SetReadMarkersTask.Params) {
|
||||||
val markers = HashMap<String, String>()
|
val markers = HashMap<String, String>()
|
||||||
val fullyReadEventId: String?
|
|
||||||
val readReceiptEventId: String?
|
|
||||||
|
|
||||||
Timber.v("Execute set read marker with params: $params")
|
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 ->
|
val latestSyncedEventId = Realm.getInstance(monarchy.realmConfiguration).use { realm ->
|
||||||
TimelineEventEntity.latestEvent(realm, roomId = params.roomId, includesSending = false)?.eventId
|
TimelineEventEntity.latestEvent(realm, roomId = params.roomId, includesSending = false)?.eventId
|
||||||
}
|
}
|
||||||
fullyReadEventId = latestSyncedEventId
|
Pair(latestSyncedEventId, latestSyncedEventId)
|
||||||
readReceiptEventId = latestSyncedEventId
|
|
||||||
} else {
|
} else {
|
||||||
fullyReadEventId = params.fullyReadEventId
|
Pair(params.fullyReadEventId, params.readReceiptEventId)
|
||||||
readReceiptEventId = params.readReceiptEventId
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fullyReadEventId != null && isReadMarkerMoreRecent(params.roomId, fullyReadEventId)) {
|
if (fullyReadEventId != null && !isReadMarkerMoreRecent(monarchy, params.roomId, fullyReadEventId)) {
|
||||||
if (LocalEcho.isLocalEchoId(fullyReadEventId)) {
|
if (LocalEcho.isLocalEchoId(fullyReadEventId)) {
|
||||||
Timber.w("Can't set read marker for local event $fullyReadEventId")
|
Timber.w("Can't set read marker for local event $fullyReadEventId")
|
||||||
} else {
|
} 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,7 +115,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
|
||||||
|
|
||||||
override fun editTextMessage(targetEventId: String,
|
override fun editTextMessage(targetEventId: String,
|
||||||
msgType: String,
|
msgType: String,
|
||||||
newBodyText: String,
|
newBodyText: CharSequence,
|
||||||
newBodyAutoMarkdown: Boolean,
|
newBodyAutoMarkdown: Boolean,
|
||||||
compatibilityBodyText: String): Cancelable {
|
compatibilityBodyText: String): Cancelable {
|
||||||
val event = eventFactory
|
val event = eventFactory
|
||||||
|
@ -164,7 +164,7 @@ internal class DefaultRelationService @AssistedInject constructor(@Assisted priv
|
||||||
.executeBy(taskExecutor)
|
.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)
|
val event = eventFactory.createReplyTextEvent(roomId, eventReplied, replyText, autoMarkdown)
|
||||||
?.also { saveLocalEcho(it) }
|
?.also { saveLocalEcho(it) }
|
||||||
?: return null
|
?: return null
|
||||||
|
|
|
@ -68,7 +68,7 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private
|
||||||
|
|
||||||
private val workerFutureListenerExecutor = Executors.newSingleThreadExecutor()
|
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 {
|
val event = localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown).also {
|
||||||
saveLocalEcho(it)
|
saveLocalEcho(it)
|
||||||
}
|
}
|
||||||
|
@ -76,8 +76,8 @@ internal class DefaultSendService @AssistedInject constructor(@Assisted private
|
||||||
return sendEvent(event)
|
return sendEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun sendFormattedTextMessage(text: String, formattedText: String): Cancelable {
|
override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String): Cancelable {
|
||||||
val event = localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText)).also {
|
val event = localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType).also {
|
||||||
saveLocalEcho(it)
|
saveLocalEcho(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.di.UserId
|
||||||
import im.vector.matrix.android.internal.session.content.ThumbnailExtractor
|
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.RoomSummaryUpdater
|
||||||
|
import im.vector.matrix.android.internal.session.room.send.pills.TextPillsUtils
|
||||||
import im.vector.matrix.android.internal.util.StringProvider
|
import im.vector.matrix.android.internal.util.StringProvider
|
||||||
import org.commonmark.parser.Parser
|
import org.commonmark.parser.Parser
|
||||||
import org.commonmark.renderer.html.HtmlRenderer
|
import org.commonmark.renderer.html.HtmlRenderer
|
||||||
|
@ -50,45 +51,55 @@ import javax.inject.Inject
|
||||||
*
|
*
|
||||||
* The transactionID is used as loc
|
* The transactionID is used as loc
|
||||||
*/
|
*/
|
||||||
internal class LocalEchoEventFactory @Inject constructor(@UserId private val userId: String,
|
internal class LocalEchoEventFactory @Inject constructor(
|
||||||
private val stringProvider: StringProvider,
|
@UserId private val userId: String,
|
||||||
private val roomSummaryUpdater: RoomSummaryUpdater) {
|
private val stringProvider: StringProvider,
|
||||||
|
private val roomSummaryUpdater: RoomSummaryUpdater,
|
||||||
|
private val textPillsUtils: TextPillsUtils
|
||||||
|
) {
|
||||||
// TODO Inject
|
// TODO Inject
|
||||||
private val parser = Parser.builder().build()
|
private val parser = Parser.builder().build()
|
||||||
// TODO Inject
|
// TODO Inject
|
||||||
private val renderer = HtmlRenderer.builder().build()
|
private val renderer = HtmlRenderer.builder().build()
|
||||||
|
|
||||||
fun createTextEvent(roomId: String, msgType: String, text: String, autoMarkdown: Boolean): Event {
|
fun createTextEvent(roomId: String, msgType: String, text: CharSequence, autoMarkdown: Boolean): Event {
|
||||||
if (msgType == MessageType.MSGTYPE_TEXT) {
|
if (msgType == MessageType.MSGTYPE_TEXT || msgType == MessageType.MSGTYPE_EMOTE) {
|
||||||
return createFormattedTextEvent(roomId, createTextContent(text, autoMarkdown))
|
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)
|
return createEvent(roomId, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createTextContent(text: String, autoMarkdown: Boolean): TextContent {
|
private fun createTextContent(text: CharSequence, autoMarkdown: Boolean): TextContent {
|
||||||
if (autoMarkdown) {
|
if (autoMarkdown) {
|
||||||
val document = parser.parse(text)
|
val source = textPillsUtils.processSpecialSpansToMarkdown(text)
|
||||||
|
?: text.toString()
|
||||||
|
val document = parser.parse(source)
|
||||||
val htmlText = renderer.render(document)
|
val htmlText = renderer.render(document)
|
||||||
|
|
||||||
if (isFormattedTextPertinent(text, htmlText)) {
|
if (isFormattedTextPertinent(source, htmlText)) {
|
||||||
return TextContent(text, 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?) =
|
private fun isFormattedTextPertinent(text: String, htmlText: String?) =
|
||||||
text != htmlText && htmlText != "<p>${text.trim()}</p>\n"
|
text != htmlText && htmlText != "<p>${text.trim()}</p>\n"
|
||||||
|
|
||||||
fun createFormattedTextEvent(roomId: String, textContent: TextContent): Event {
|
fun createFormattedTextEvent(roomId: String, textContent: TextContent, msgType: String): Event {
|
||||||
return createEvent(roomId, textContent.toMessageTextContent())
|
return createEvent(roomId, textContent.toMessageTextContent(msgType))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createReplaceTextEvent(roomId: String,
|
fun createReplaceTextEvent(roomId: String,
|
||||||
targetEventId: String,
|
targetEventId: String,
|
||||||
newBodyText: String,
|
newBodyText: CharSequence,
|
||||||
newBodyAutoMarkdown: Boolean,
|
newBodyAutoMarkdown: Boolean,
|
||||||
msgType: String,
|
msgType: String,
|
||||||
compatibilityText: String): Event {
|
compatibilityText: String): Event {
|
||||||
|
@ -279,7 +290,7 @@ internal class LocalEchoEventFactory @Inject constructor(@UserId private val use
|
||||||
return System.currentTimeMillis()
|
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
|
// Fallbacks and event representation
|
||||||
// TODO Add error/warning logs when any of this is null
|
// TODO Add error/warning logs when any of this is null
|
||||||
val permalink = PermalinkFactory.createPermalink(eventReplied.root) ?: return 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
|
// > <@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 eventId = eventReplied.root.eventId ?: return null
|
||||||
val content = MessageTextContent(
|
val content = MessageTextContent(
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)"
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,7 +26,8 @@ internal interface GetContextOfEventTask : Task<GetContextOfEventTask.Params, To
|
||||||
|
|
||||||
data class Params(
|
data class Params(
|
||||||
val roomId: String,
|
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 {
|
override suspend fun execute(params: GetContextOfEventTask.Params): TokenChunkEventPersistor.Result {
|
||||||
val filter = filterRepository.getRoomFilter()
|
val filter = filterRepository.getRoomFilter()
|
||||||
val response = executeRequest<EventContextResponse> {
|
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)
|
return tokenChunkEventPersistor.insertInDb(response, params.roomId, PaginationDirection.BACKWARDS)
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,22 +74,14 @@ internal class DefaultTimeline(
|
||||||
private val cryptoService: CryptoService,
|
private val cryptoService: CryptoService,
|
||||||
private val timelineEventMapper: TimelineEventMapper,
|
private val timelineEventMapper: TimelineEventMapper,
|
||||||
private val settings: TimelineSettings,
|
private val settings: TimelineSettings,
|
||||||
private val hiddenReadReceipts: TimelineHiddenReadReceipts,
|
private val hiddenReadReceipts: TimelineHiddenReadReceipts
|
||||||
private val hiddenReadMarker: TimelineHiddenReadMarker
|
) : Timeline, TimelineHiddenReadReceipts.Delegate {
|
||||||
) : Timeline, TimelineHiddenReadReceipts.Delegate, TimelineHiddenReadMarker.Delegate {
|
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD")
|
val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD")
|
||||||
}
|
}
|
||||||
|
|
||||||
override var listener: Timeline.Listener? = null
|
private val listeners = ArrayList<Timeline.Listener>()
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
BACKGROUND_HANDLER.post {
|
|
||||||
postSnapshot()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val isStarted = AtomicBoolean(false)
|
private val isStarted = AtomicBoolean(false)
|
||||||
private val isReady = AtomicBoolean(false)
|
private val isReady = AtomicBoolean(false)
|
||||||
private val mainHandler = createUIHandler()
|
private val mainHandler = createUIHandler()
|
||||||
|
@ -110,7 +102,7 @@ internal class DefaultTimeline(
|
||||||
private val backwardsState = AtomicReference(State())
|
private val backwardsState = AtomicReference(State())
|
||||||
private val forwardsState = AtomicReference(State())
|
private val forwardsState = AtomicReference(State())
|
||||||
|
|
||||||
private val timelineID = UUID.randomUUID().toString()
|
override val timelineID = UUID.randomUUID().toString()
|
||||||
|
|
||||||
override val isLive
|
override val isLive
|
||||||
get() = !hasMoreToLoad(Timeline.Direction.FORWARDS)
|
get() = !hasMoreToLoad(Timeline.Direction.FORWARDS)
|
||||||
|
@ -197,7 +189,6 @@ internal class DefaultTimeline(
|
||||||
if (settings.buildReadReceipts) {
|
if (settings.buildReadReceipts) {
|
||||||
hiddenReadReceipts.start(realm, filteredEvents, nonFilteredEvents, this)
|
hiddenReadReceipts.start(realm, filteredEvents, nonFilteredEvents, this)
|
||||||
}
|
}
|
||||||
hiddenReadMarker.start(realm, filteredEvents, nonFilteredEvents, this)
|
|
||||||
isReady.set(true)
|
isReady.set(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -217,7 +208,6 @@ internal class DefaultTimeline(
|
||||||
if (this::filteredEvents.isInitialized) {
|
if (this::filteredEvents.isInitialized) {
|
||||||
filteredEvents.removeAllChangeListeners()
|
filteredEvents.removeAllChangeListeners()
|
||||||
}
|
}
|
||||||
hiddenReadMarker.dispose()
|
|
||||||
if (settings.buildReadReceipts) {
|
if (settings.buildReadReceipts) {
|
||||||
hiddenReadReceipts.dispose()
|
hiddenReadReceipts.dispose()
|
||||||
}
|
}
|
||||||
|
@ -298,7 +288,21 @@ internal class DefaultTimeline(
|
||||||
return hasMoreInCache(direction) || !hasReachedEnd(direction)
|
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 {
|
override fun rebuildEvent(eventId: String, readReceipts: List<ReadReceipt>): Boolean {
|
||||||
return rebuildEvent(eventId) { te ->
|
return rebuildEvent(eventId) { te ->
|
||||||
|
@ -310,19 +314,7 @@ internal class DefaultTimeline(
|
||||||
postSnapshot()
|
postSnapshot()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TimelineHiddenReadMarker.Delegate
|
// Private methods *****************************************************************************
|
||||||
|
|
||||||
override fun rebuildEvent(eventId: String, hasReadMarker: Boolean): Boolean {
|
|
||||||
return rebuildEvent(eventId) { te ->
|
|
||||||
te.copy(hasReadMarker = hasReadMarker)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onReadMarkerUpdated() {
|
|
||||||
postSnapshot()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Private methods *****************************************************************************
|
|
||||||
|
|
||||||
private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean {
|
private fun rebuildEvent(eventId: String, builder: (TimelineEvent) -> TimelineEvent): Boolean {
|
||||||
return builtEventsIdMap[eventId]?.let { builtIndex ->
|
return builtEventsIdMap[eventId]?.let { builtIndex ->
|
||||||
|
@ -502,9 +494,9 @@ internal class DefaultTimeline(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val params = PaginationTask.Params(roomId = roomId,
|
val params = PaginationTask.Params(roomId = roomId,
|
||||||
from = token,
|
from = token,
|
||||||
direction = direction.toPaginationDirection(),
|
direction = direction.toPaginationDirection(),
|
||||||
limit = limit)
|
limit = limit)
|
||||||
|
|
||||||
Timber.v("Should fetch $limit items $direction")
|
Timber.v("Should fetch $limit items $direction")
|
||||||
cancelableBag += paginationTask
|
cancelableBag += paginationTask
|
||||||
|
@ -579,7 +571,7 @@ internal class DefaultTimeline(
|
||||||
val timelineEvent = buildTimelineEvent(eventEntity)
|
val timelineEvent = buildTimelineEvent(eventEntity)
|
||||||
|
|
||||||
if (timelineEvent.isEncrypted()
|
if (timelineEvent.isEncrypted()
|
||||||
&& timelineEvent.root.mxDecryptionResult == null) {
|
&& timelineEvent.root.mxDecryptionResult == null) {
|
||||||
timelineEvent.root.eventId?.let { eventDecryptor.requestDecryption(it) }
|
timelineEvent.root.eventId?.let { eventDecryptor.requestDecryption(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -641,7 +633,7 @@ internal class DefaultTimeline(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchEvent(eventId: String) {
|
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)
|
cancelableBag += contextOfEventTask.configureWith(params).executeBy(taskExecutor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -652,7 +644,13 @@ internal class DefaultTimeline(
|
||||||
}
|
}
|
||||||
updateLoadingStates(filteredEvents)
|
updateLoadingStates(filteredEvents)
|
||||||
val snapshot = createSnapshot()
|
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)
|
debouncer.debounce("post_snapshot", runnable, 50)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,17 +53,16 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
|
||||||
|
|
||||||
override fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline {
|
override fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline {
|
||||||
return DefaultTimeline(roomId,
|
return DefaultTimeline(roomId,
|
||||||
eventId,
|
eventId,
|
||||||
monarchy.realmConfiguration,
|
monarchy.realmConfiguration,
|
||||||
taskExecutor,
|
taskExecutor,
|
||||||
contextOfEventTask,
|
contextOfEventTask,
|
||||||
clearUnlinkedEventsTask,
|
clearUnlinkedEventsTask,
|
||||||
paginationTask,
|
paginationTask,
|
||||||
cryptoService,
|
cryptoService,
|
||||||
timelineEventMapper,
|
timelineEventMapper,
|
||||||
settings,
|
settings,
|
||||||
TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings),
|
TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings)
|
||||||
TimelineHiddenReadMarker(roomId, settings)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ data class EventContextResponse(
|
||||||
@Json(name = "state") override val stateEvents: List<Event> = emptyList()
|
@Json(name = "state") override val stateEvents: List<Event> = emptyList()
|
||||||
) : TokenChunkEvent {
|
) : TokenChunkEvent {
|
||||||
|
|
||||||
override val events: List<Event>
|
override val events: List<Event> by lazy {
|
||||||
get() = listOf(event)
|
eventsAfter.reversed() + listOf(event) + eventsBefore
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -17,6 +17,7 @@
|
||||||
package im.vector.matrix.android.internal.session.signout
|
package im.vector.matrix.android.internal.session.signout
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import im.vector.matrix.android.BuildConfig
|
||||||
import im.vector.matrix.android.internal.SessionManager
|
import im.vector.matrix.android.internal.SessionManager
|
||||||
import im.vector.matrix.android.internal.auth.SessionParamsStore
|
import im.vector.matrix.android.internal.auth.SessionParamsStore
|
||||||
import im.vector.matrix.android.internal.crypto.CryptoModule
|
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.session.cache.ClearCacheTask
|
||||||
import im.vector.matrix.android.internal.task.Task
|
import im.vector.matrix.android.internal.task.Task
|
||||||
import im.vector.matrix.android.internal.worker.WorkManagerUtil
|
import im.vector.matrix.android.internal.worker.WorkManagerUtil
|
||||||
|
import io.realm.Realm
|
||||||
|
import io.realm.RealmConfiguration
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -42,6 +45,8 @@ internal class DefaultSignOutTask @Inject constructor(private val context: Conte
|
||||||
@CryptoDatabase private val clearCryptoDataTask: ClearCacheTask,
|
@CryptoDatabase private val clearCryptoDataTask: ClearCacheTask,
|
||||||
@UserCacheDirectory private val userFile: File,
|
@UserCacheDirectory private val userFile: File,
|
||||||
private val realmKeysUtils: RealmKeysUtils,
|
private val realmKeysUtils: RealmKeysUtils,
|
||||||
|
@SessionDatabase private val realmSessionConfiguration: RealmConfiguration,
|
||||||
|
@CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration,
|
||||||
@UserMd5 private val userMd5: String) : SignOutTask {
|
@UserMd5 private val userMd5: String) : SignOutTask {
|
||||||
|
|
||||||
override suspend fun execute(params: Unit) {
|
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")
|
Timber.d("SignOut: clear the database keys")
|
||||||
realmKeysUtils.clear(SessionModule.DB_ALIAS_PREFIX + userMd5)
|
realmKeysUtils.clear(SessionModule.DB_ALIAS_PREFIX + userMd5)
|
||||||
realmKeysUtils.clear(CryptoModule.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)") }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,14 +16,10 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.internal.session.sync
|
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.ReadMarkerEntity
|
||||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
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.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 io.realm.Realm
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -39,18 +35,8 @@ internal class RoomFullyReadHandler @Inject constructor() {
|
||||||
RoomSummaryEntity.getOrCreate(realm, roomId).apply {
|
RoomSummaryEntity.getOrCreate(realm, roomId).apply {
|
||||||
readMarkerId = content.eventId
|
readMarkerId = content.eventId
|
||||||
}
|
}
|
||||||
// Remove the old markers if any
|
ReadMarkerEntity.getOrCreate(realm, roomId).apply {
|
||||||
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 {
|
|
||||||
this.eventId = content.eventId
|
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 }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.filter.FilterRepository
|
||||||
import im.vector.matrix.android.internal.session.homeserver.GetHomeServerCapabilitiesTask
|
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.sync.model.SyncResponse
|
||||||
|
import im.vector.matrix.android.internal.session.user.UserStore
|
||||||
import im.vector.matrix.android.internal.task.Task
|
import im.vector.matrix.android.internal.task.Task
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -41,7 +42,8 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI,
|
||||||
private val sessionParamsStore: SessionParamsStore,
|
private val sessionParamsStore: SessionParamsStore,
|
||||||
private val initialSyncProgressService: DefaultInitialSyncProgressService,
|
private val initialSyncProgressService: DefaultInitialSyncProgressService,
|
||||||
private val syncTokenStore: SyncTokenStore,
|
private val syncTokenStore: SyncTokenStore,
|
||||||
private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask
|
private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask,
|
||||||
|
private val userStore: UserStore
|
||||||
) : SyncTask {
|
) : SyncTask {
|
||||||
|
|
||||||
override suspend fun execute(params: SyncTask.Params) {
|
override suspend fun execute(params: SyncTask.Params) {
|
||||||
|
@ -60,6 +62,8 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI,
|
||||||
|
|
||||||
val isInitialSync = token == null
|
val isInitialSync = token == null
|
||||||
if (isInitialSync) {
|
if (isInitialSync) {
|
||||||
|
// We might want to get the user information in parallel too
|
||||||
|
userStore.createOrUpdate(userId)
|
||||||
initialSyncProgressService.endAll()
|
initialSyncProgressService.endAll()
|
||||||
initialSyncProgressService.startTask(R.string.initial_sync_start_importing_account, 100)
|
initialSyncProgressService.startTask(R.string.initial_sync_start_importing_account, 100)
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,4 +53,7 @@ internal abstract class UserModule {
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindUpdateIgnoredUserIdsTask(task: DefaultUpdateIgnoredUserIdsTask): UpdateIgnoredUserIdsTask
|
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
Loading…
Reference in a new issue