Merge branch 'develop' into patch-1

This commit is contained in:
Benoit Marty 2020-09-29 16:28:58 +02:00 committed by GitHub
commit 51f225056c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
399 changed files with 15827 additions and 4718 deletions

10
.github/ISSUE_TEMPLATE/matrix-sdk.md vendored Normal file
View File

@ -0,0 +1,10 @@
---
name: Matrix SDK
about: Report issue or ask for a feature regarding the Android Matrix SDK
title: "[SDK] "
labels: matrix-sdk
assignees: ''
---
<!-- This issue template should be used by third party application maintainers, to report a bug or to request a feature on the SDK module of the application Element Android-->

View File

@ -1,4 +1,81 @@
Changes in Element 1.0.6 (2020-XX-XX) Changes in Element 1.0.9 (2020-XX-XX)
===================================================
Features ✨:
-
Improvements 🙌:
- PIN code: request PIN code if phone has been locked
- Small optimisation of scrolling experience in timeline (#2114)
Bugfix 🐛:
- Improve support for image selection with intent changes (#1376)
- Fix Splash layout on small screens
Translations 🗣:
-
SDK API changes ⚠️:
-
Build 🧱:
-
Other changes:
- Added registration/verification automated UI tests
- Create a script to help getting public information form any homeserver
Changes in Element 1.0.8 (2020-09-25)
===================================================
Improvements 🙌:
- Add "show password" in import Megolm keys dialog
- Visually disable call buttons in menu and prohibit calling when permissions are insufficient (#2112)
- Better management of requested permissions (#2048)
- Add a setting to show timestamp for all messages (#2123)
- Use cache for user color
- Allow using an outdated homeserver, at user's risk (#1972)
- Restore small logo on login screens and fix scrolling issue on those screens
- PIN Code Improvements: Add more settings: biometrics, grace period, notification content (#1985)
Bugfix 🐛:
- Long message cannot be sent/takes infinite time & blocks other messages (#1397)
- Fix crash when wellknown are malformed, or redirect to some HTML content (reported by rageshakes)
- User Verification in DM not working
- Manual import of Megolm keys does back up the imported keys
- Auto scrolling to the latest message when sending (#2094)
- Fix incorrect permission check when creating widgets (#2137)
- Pin code: user has to enter pin code twice (#2005)
SDK API changes ⚠️:
- Rename `tryThis` to `tryOrNull`
Other changes:
- Add an advanced action to reset an account data entry
Changes in Element 1.0.7 (2020-09-17)
===================================================
Improvements 🙌:
- Handle date formatting properly (show time am/pm if needed, display year when needed)
- Improve F-Droid Notification (#2055)
Bugfix 🐛:
- Clear the notification when the event is read elsewhere (#1822)
- Speakerphone is not used for ringback tone (#1644, #1645)
- Back camera preview is not mirrored anymore (#1776)
- Various report of people that cannot play video (#2107)
- Rooms incorrectly marked as unread (#588)
- Allow users to show/hide room member state events (#1231)
- Fix stuck on loader when launching home
SDK API changes ⚠️:
- Create a new RawService to get plain data from the server.
Other changes:
- Performance: share Realm instance used on UI thread and improve SharedPreferences reading time.
Changes in Element 1.0.6 (2020-09-08)
=================================================== ===================================================
Features ✨: Features ✨:
@ -7,8 +84,9 @@ Features ✨:
Improvements 🙌: Improvements 🙌:
- You can now join room through permalink and within room directory search - You can now join room through permalink and within room directory search
- Add long click gesture to copy userId, user display name, room name, room topic and room alias (#1774) - Add long click gesture to copy userId, user display name, room name, room topic and room alias (#1774)
- Fix several issues when uploading bug files (#1889) - Fix several issues when uploading big files (#1889)
- Do not propose to verify session if there is only one session and 4S is not configured (#1901) - Do not propose to verify session if there is only one session and 4S is not configured (#1901)
- Call screen does not use proximity sensor (#1735)
Bugfix 🐛: Bugfix 🐛:
- Display name not shown under Settings/General (#1926) - Display name not shown under Settings/General (#1926)
@ -21,15 +99,19 @@ Bugfix 🐛:
- Loudspeaker is always used (#1685) - Loudspeaker is always used (#1685)
- Fix uploads still don't work with room v6 (#1879) - Fix uploads still don't work with room v6 (#1879)
- Can't handle ongoing call events in background (#1992) - Can't handle ongoing call events in background (#1992)
- Handle room, user and group links by the Element app (#1795)
- Update associated site domain (#1833)
- Crash / Attachment viewer: Cannot draw a recycled Bitmap #2034 - Crash / Attachment viewer: Cannot draw a recycled Bitmap #2034
- Login with Matrix-Id | Autodiscovery fails if identity server is invalid and Homeserver ok (#2027) - Login with Matrix-Id | Autodiscovery fails if identity server is invalid and Homeserver ok (#2027)
- Improve support for image selection with intent changes (#1376) - Support for image compression on Android 10
- Verification popup won't show
- Android 6: App crash when read Contact permission is granted (#2064)
- JSON for verification events leaks in to the room list (#1246)
- Replies to poll appears in timeline as unsupported events during sending (#1004)
Translations 🗣: Translations 🗣:
- - The SDK is now using SAS string translations from [Weblate Matrix-doc project](https://translate.riot.im/projects/matrix-doc/) (#1909)
- New translation to kabyle
SDK API changes ⚠️:
-
Build 🧱: Build 🧱:
- Some dependencies have been upgraded (coroutine, recyclerView, appCompat, core-ktx, firebase-messaging) - Some dependencies have been upgraded (coroutine, recyclerView, appCompat, core-ktx, firebase-messaging)
@ -37,10 +119,10 @@ Build 🧱:
New pipeline location: https://github.com/matrix-org/pipelines/blob/master/element-android/pipeline.yml New pipeline location: https://github.com/matrix-org/pipelines/blob/master/element-android/pipeline.yml
New build location: https://buildkite.com/matrix-dot-org/element-android New build location: https://buildkite.com/matrix-dot-org/element-android
Other changes: Other changes:
- Use File extension functions to make code more concise (#1996) - Use File extension functions to make code more concise (#1996)
- Create a script to import SAS strings (#1909) - Create a script to import SAS strings (#1909)
- Support `data-mx-[bg-]color` attributes on `<font>` tags.
Changes in Element 1.0.5 (2020-08-21) Changes in Element 1.0.5 (2020-08-21)
=================================================== ===================================================

View File

@ -261,11 +261,11 @@ This is not an ideal, but the client will display a hint to check the entered co
200 200
````json ```json
{ {
"success": true "success": true
} }
```` ```
Then the app call `https://homeserver.org/_matrix/client/r0/account/3pid/add` as per adding an email and follow the same UIS flow Then the app call `https://homeserver.org/_matrix/client/r0/account/3pid/add` as per adding an email and follow the same UIS flow

View File

@ -8,7 +8,9 @@ This document describes the flow of signin to a homeserver, and also the flow wh
Client request the sign-in flows, once the homeserver is chosen by the user and its url is known (in the example it's `https://matrix.org`) Client request the sign-in flows, once the homeserver is chosen by the user and its url is known (in the example it's `https://matrix.org`)
> curl -X GET 'https://matrix.org/_matrix/client/r0/login' ```shell script
curl -X GET 'https://matrix.org/_matrix/client/r0/login'
```
200 200
@ -26,7 +28,9 @@ Client request the sign-in flows, once the homeserver is chosen by the user and
The user is able to connect using `m.login.password` The user is able to connect using `m.login.password`
> curl -X POST --data $'{"identifier":{"type":"m.id.user","user":"alice"},"password":"weak_password","type":"m.login.password","initial_device_display_name":"Portable"}' 'https://matrix.org/_matrix/client/r0/login' ```shell script
curl -X POST --data $'{"identifier":{"type":"m.id.user","user":"alice"},"password":"weak_password","type":"m.login.password","initial_device_display_name":"Portable"}' 'https://matrix.org/_matrix/client/r0/login'
```
```json ```json
{ {
@ -73,14 +77,16 @@ We get credential (200)
If the user has associated an email with its account, he can signin using the email. If the user has associated an email with its account, he can signin using the email.
> curl -X POST --data $'{"identifier":{"type":"m.id.thirdparty","medium":"email","address":"alice@yopmail.com"},"password":"weak_password","type":"m.login.password","initial_device_display_name":"Portable"}' 'https://matrix.org/_matrix/client/r0/login' ```shell script
curl -X POST --data $'{"identifier":{"type":"m.id.thirdparty","medium":"email","address":"alice@email-provider.org"},"password":"weak_password","type":"m.login.password","initial_device_display_name":"Portable"}' 'https://matrix.org/_matrix/client/r0/login'
```
```json ```json
{ {
"identifier": { "identifier": {
"type": "m.id.thirdparty", "type": "m.id.thirdparty",
"medium": "email", "medium": "email",
"address": "alice@yopmail.com" "address": "alice@email-provider.org"
}, },
"password": "weak_password", "password": "weak_password",
"type": "m.login.password", "type": "m.login.password",
@ -136,7 +142,9 @@ Not supported yet in Element
### Login with SSO ### Login with SSO
> curl -X GET 'https://homeserver.with.sso/_matrix/client/r0/login' ```shell script
curl -X GET 'https://homeserver.with.sso/_matrix/client/r0/login'
```
200 200
@ -171,7 +179,9 @@ Once the process is finished, the web page will call the `redirectUrl` with an e
This navigation is intercepted by Element by the `LoginActivity`, which will then ask the homeserver to convert this `loginToken` to an access token This navigation is intercepted by Element by the `LoginActivity`, which will then ask the homeserver to convert this `loginToken` to an access token
> curl -X POST --data $'{"type":"m.login.token","token":"MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy"}' 'https://homeserver.with.sso/_matrix/client/r0/login' ```shell script
curl -X POST --data $'{"type":"m.login.token","token":"MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy"}' 'https://homeserver.with.sso/_matrix/client/r0/login'
```
```json ```json
{ {
@ -214,7 +224,9 @@ We display a warning regarding e2e.
At the first step, we do not send the password, only the email and a client secret, generated by the application At the first step, we do not send the password, only the email and a client secret, generated by the application
> curl -X POST --data $'{"client_secret":"6c57f284-85e2-421b-8270-fb1795a120a7","send_attempt":0,"email":"user@domain.com"}' 'https://matrix.org/_matrix/client/r0/account/password/email/requestToken' ```shell script
curl -X POST --data $'{"client_secret":"6c57f284-85e2-421b-8270-fb1795a120a7","send_attempt":0,"email":"user@domain.com"}' 'https://matrix.org/_matrix/client/r0/account/password/email/requestToken'
```
```json ```json
{ {
@ -251,7 +263,9 @@ During this step, the new password is sent to the homeserver.
If the user confirms before the link is clicked, we get an error: If the user confirms before the link is clicked, we get an error:
> curl -X POST --data $'{"auth":{"type":"m.login.email.identity","threepid_creds":{"client_secret":"6c57f284-85e2-421b-8270-fb1795a120a7","sid":"tQNbrREDACTEDldA"}},"new_password":"weak_password"}' 'https://matrix.org/_matrix/client/r0/account/password' ```shell script
curl -X POST --data $'{"auth":{"type":"m.login.email.identity","threepid_creds":{"client_secret":"6c57f284-85e2-421b-8270-fb1795a120a7","sid":"tQNbrREDACTEDldA"}},"new_password":"weak_password"}' 'https://matrix.org/_matrix/client/r0/account/password'
```
```json ```json
{ {
@ -285,7 +299,9 @@ It contains the client secret, a token and the sid
When the user click the link, if validate his ownership and the new password can now be ent by the application (on user demand): When the user click the link, if validate his ownership and the new password can now be ent by the application (on user demand):
> curl -X POST --data $'{"auth":{"type":"m.login.email.identity","threepid_creds":{"client_secret":"6c57f284-85e2-421b-8270-fb1795a120a7","sid":"tQNbrREDACTEDldA"}},"new_password":"weak_password"}' 'https://matrix.org/_matrix/client/r0/account/password' ```shell script
curl -X POST --data $'{"auth":{"type":"m.login.email.identity","threepid_creds":{"client_secret":"6c57f284-85e2-421b-8270-fb1795a120a7","sid":"tQNbrREDACTEDldA"}},"new_password":"weak_password"}' 'https://matrix.org/_matrix/client/r0/account/password'
```
```json ```json
{ {

View File

@ -10,7 +10,9 @@ This document describes the flow of registration to a homeserver. Examples come
Client request the sign-up flows, once the homeserver is chosen by the user and its url is known (in the example it's `https://matrix.org`) Client request the sign-up flows, once the homeserver is chosen by the user and its url is known (in the example it's `https://matrix.org`)
> curl -X POST --data $'{}' 'https://matrix.org/_matrix/client/r0/register' ```shell script
curl -X POST --data $'{}' 'https://matrix.org/_matrix/client/r0/register'
```
```json ```json
{ {
@ -70,7 +72,9 @@ If the registration is not possible, we get a 403
The app is displaying a form to enter username and password. The app is displaying a form to enter username and password.
> curl -X POST --data $'{"initial_device_display_name":"Mobile device","username":"alice","password": "weak_password"}' 'https://matrix.org/_matrix/client/r0/register' ```shell script
curl -X POST --data $'{"initial_device_display_name":"Mobile device","username":"alice","password": "weak_password"}' 'https://matrix.org/_matrix/client/r0/register'
```
```json ```json
{ {
@ -133,9 +137,11 @@ We get a 400:
### Step 2: entering email ### Step 2: entering email
User is proposed to enter an email. We skip this step. User is proposed to enter an email. User skips this step.
> curl -X POST --data $'{"auth":{"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.dummy"}}' 'https://matrix.org/_matrix/client/r0/register' ```shell script
curl -X POST --data $'{"auth":{"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.dummy"}}' 'https://matrix.org/_matrix/client/r0/register'
```
```json ```json
{ {
@ -189,16 +195,18 @@ User is proposed to enter an email. We skip this step.
} }
``` ```
### Step 2 bis: we enter an email ### Step 2 bis: user enters an email
We request a token to the homeserver. The `client_secret` is generated by the application We request a token to the homeserver. The `client_secret` is generated by the application
> curl -X POST --data $'{"client_secret":"53e679ea-oRED-ACTED-92b8-3012c49c6cfa","email":"alice@yopmail.com","send_attempt":0}' 'https://matrix.org/_matrix/client/r0/register/email/requestToken' ```shell script
curl -X POST --data $'{"client_secret":"53e679ea-oRED-ACTED-92b8-3012c49c6cfa","email":"alice@email-provider.org","send_attempt":0}' 'https://matrix.org/_matrix/client/r0/register/email/requestToken'
```
```json ```json
{ {
"client_secret": "53e679ea-oRED-ACTED-92b8-3012c49c6cfa", "client_secret": "53e679ea-oRED-ACTED-92b8-3012c49c6cfa",
"email": "alice@yopmail.com", "email": "alice@email-provider.org",
"send_attempt": 0 "send_attempt": 0
} }
``` ```
@ -213,7 +221,9 @@ We request a token to the homeserver. The `client_secret` is generated by the ap
And And
> curl -X POST --data $'{"auth":{"threepid_creds":{"client_secret":"53e679ea-oRED-ACTED-92b8-3012c49c6cfa","sid":"qlBCREDACTEDEtgxD"},"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.email.identity"}}' 'https://matrix.org/_matrix/client/r0/register' ```shell script
curl -X POST --data $'{"auth":{"threepid_creds":{"client_secret":"53e679ea-oRED-ACTED-92b8-3012c49c6cfa","sid":"qlBCREDACTEDEtgxD"},"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.email.identity"}}' 'https://matrix.org/_matrix/client/r0/register'
```
```json ```json
{ {
@ -239,7 +249,9 @@ We get 401 since the email is not validated yet:
The app is now polling on The app is now polling on
> curl -X POST --data $'{"auth":{"threepid_creds":{"client_secret":"53e679ea-oRED-ACTED-92b8-3012c49c6cfa","sid":"qlBCREDACTEDEtgxD"},"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.email.identity"}}' 'https://matrix.org/_matrix/client/r0/register' ```shell script
curl -X POST --data $'{"auth":{"threepid_creds":{"client_secret":"53e679ea-oRED-ACTED-92b8-3012c49c6cfa","sid":"qlBCREDACTEDEtgxD"},"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.email.identity"}}' 'https://matrix.org/_matrix/client/r0/register'
```
```json ```json
{ {
@ -254,7 +266,7 @@ The app is now polling on
} }
``` ```
We click on the link received by email `https://matrix.org/_matrix/client/unstable/registration/email/submit_token?token=vtQjQIZfwdoREDACTEDozrmKYSWlCXsJ&client_secret=53e679ea-oRED-ACTED-92b8-3012c49c6cfa&sid=qlBCREDACTEDEtgxD` which contains: User clicks on the link received by email `https://matrix.org/_matrix/client/unstable/registration/email/submit_token?token=vtQjQIZfwdoREDACTEDozrmKYSWlCXsJ&client_secret=53e679ea-oRED-ACTED-92b8-3012c49c6cfa&sid=qlBCREDACTEDEtgxD` which contains:
- A `token` vtQjQIZfwdoREDACTEDozrmKYSWlCXsJ - A `token` vtQjQIZfwdoREDACTEDozrmKYSWlCXsJ
- The `client_secret`: 53e679ea-oRED-ACTED-92b8-3012c49c6cfa - The `client_secret`: 53e679ea-oRED-ACTED-92b8-3012c49c6cfa
- A `sid`: qlBCREDACTEDEtgxD - A `sid`: qlBCREDACTEDEtgxD
@ -306,7 +318,9 @@ Once the link is clicked, the registration request (polling) returns a 401 with
User is proposed to accept T&C and he accepts them User is proposed to accept T&C and he accepts them
> curl -X POST --data $'{"auth":{"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.terms"}}' 'https://matrix.org/_matrix/client/r0/register' ```shell script
curl -X POST --data $'{"auth":{"session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.terms"}}' 'https://matrix.org/_matrix/client/r0/register'
```
```json ```json
{ {
@ -365,7 +379,9 @@ User is proposed to accept T&C and he accepts them
User is proposed to prove he is not a robot and he does it: User is proposed to prove he is not a robot and he does it:
> curl -X POST --data $'{"auth":{"response":"03AOLTBLSiGS9GhFDpAMblJ2nlXOmHXqAYJ5OvHCPUjiVLBef3k9snOYI_BDC32-t4D2jv-tpvkaiEI_uloobFd9RUTPpJ7con2hMddbKjSCYqXqcUQFhzhbcX6kw8uBnh2sbwBe80_ihrHGXEoACXQkL0ki1Q0uEtOeW20YBRjbNABsZPpLNZhGIWC0QVXnQ4FouAtZrl3gOAiyM-oG3cgP6M9pcANIAC_7T2P2amAHbtsTlSR9CsazNyS-rtDR9b5MywdtnWN9Aw8fTJb8cXQk_j7nvugMxzofPjSOrPKcr8h5OqPlpUCyxxnFtag6cuaPSUwh43D2L0E-ZX7djzaY2Yh_U2n6HegFNPOQ22CJmfrKwDlodmAfMPvAXyq77n3HpoREDACTEDo3830RHF4BfkGXUaZjctgg-A1mvC17hmQmQpkG7IhDqyw0onU-0vF_-ehCjq_CcQEDpS_O3uiHJaG5xGf-0rhLm57v_wA3deugbsZuO4uTuxZZycN_mKxZ97jlDVBetl9hc_5REPbhcT1w3uzTCSx7Q","session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.recaptcha"}}' 'https://matrix.org/_matrix/client/r0/register' ```shell script
curl -X POST --data $'{"auth":{"response":"03AOLTBLSiGS9GhFDpAMblJ2nlXOmHXqAYJ5OvHCPUjiVLBef3k9snOYI_BDC32-t4D2jv-tpvkaiEI_uloobFd9RUTPpJ7con2hMddbKjSCYqXqcUQFhzhbcX6kw8uBnh2sbwBe80_ihrHGXEoACXQkL0ki1Q0uEtOeW20YBRjbNABsZPpLNZhGIWC0QVXnQ4FouAtZrl3gOAiyM-oG3cgP6M9pcANIAC_7T2P2amAHbtsTlSR9CsazNyS-rtDR9b5MywdtnWN9Aw8fTJb8cXQk_j7nvugMxzofPjSOrPKcr8h5OqPlpUCyxxnFtag6cuaPSUwh43D2L0E-ZX7djzaY2Yh_U2n6HegFNPOQ22CJmfrKwDlodmAfMPvAXyq77n3HpoREDACTEDo3830RHF4BfkGXUaZjctgg-A1mvC17hmQmQpkG7IhDqyw0onU-0vF_-ehCjq_CcQEDpS_O3uiHJaG5xGf-0rhLm57v_wA3deugbsZuO4uTuxZZycN_mKxZ97jlDVBetl9hc_5REPbhcT1w3uzTCSx7Q","session":"xptUYoREDACTEDogOWAGVnbJQ","type":"m.login.recaptcha"}}' 'https://matrix.org/_matrix/client/r0/register'
```
```json ```json
{ {
@ -396,9 +412,11 @@ Some homeservers may require the user to enter MSISDN.
On matrix.org, it's not required, and not even optional, but it's still possible for the app to add a MSISDN during the registration. On matrix.org, it's not required, and not even optional, but it's still possible for the app to add a MSISDN during the registration.
The user enter a phone number and select a country, the `client_secret` is generated by the application The user enters a phone number and selects a country, the `client_secret` is generated by the application
> curl -X POST --data $'{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","send_attempt":1,"country":"FR","phone_number":"+33611223344"}' 'https://matrix.org/_matrix/client/r0/register/msisdn/requestToken' ```shell script
curl -X POST --data $'{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","send_attempt":1,"country":"FR","phone_number":"+33611223344"}' 'https://matrix.org/_matrix/client/r0/register/msisdn/requestToken'
```
```json ```json
{ {
@ -430,10 +448,11 @@ If it is not the case, the homeserver send the SMS and returns some data, especi
} }
``` ```
When you execute the register request, with the received `sid`, you get an error since the MSISDN is not validated yet: When we execute the register request, with the received `sid`, we 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'
```shell script
curl -X POST --data $'{"auth":{"type":"m.login.msisdn","session":"xptUYoREDACTEDogOWAGVnbJQ","threepid_creds":{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","sid":"1678881798"}}}' 'https://matrix.org/_matrix/client/r0/register'
```
```json ```json
"auth": { "auth": {
@ -492,7 +511,9 @@ There is an issue on Synapse, which return a 401, it sends too much data along w
The user receive the SMS, he can enter the SMS code in the app, which is sent using the "submit_url" received ie the response of the `requestToken` request: The user receive the SMS, he can enter the SMS code in the app, which is sent using the "submit_url" received ie the response of the `requestToken` request:
> curl -X POST --data $'{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","sid":"1678881798","token":"123456"}' 'https://matrix.org/_matrix/client/unstable/add_threepid/msisdn/submit_token' ```shell script
curl -X POST --data $'{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","sid":"1678881798","token":"123456"}' 'https://matrix.org/_matrix/client/unstable/add_threepid/msisdn/submit_token'
```
```json ```json
{ {
@ -520,7 +541,9 @@ And if the code is correct we get a 200 with:
We can now execute the registration request, to the homeserver We can now execute the registration request, to the homeserver
> curl -X POST --data $'{"auth":{"type":"m.login.msisdn","session":"xptUYoREDACTEDogOWAGVnbJQ","threepid_creds":{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","sid":"1678881798"}}}' 'https://matrix.org/_matrix/client/r0/register' ```shell script
curl -X POST --data $'{"auth":{"type":"m.login.msisdn","session":"xptUYoREDACTEDogOWAGVnbJQ","threepid_creds":{"client_secret":"d3e285f6-972a-496c-9a22-7915a2db57c7","sid":"1678881798"}}}' 'https://matrix.org/_matrix/client/r0/register'
```
```json ```json
{ {
@ -535,7 +558,7 @@ We can now execute the registration request, to the homeserver
} }
``` ```
Now the homeserver consider that the `m.login.msisdn` step is completed (401): Now the homeserver considers that the `m.login.msisdn` step is completed (401):
```json ```json
{ {

107
docs/ui-tests.md Normal file
View File

@ -0,0 +1,107 @@
# Automate user interface tests
Element Android ensures that some fundamental flows are properly working by running automated user interface tests.
Ui tests are using the android [Espresso](https://developer.android.com/training/testing/espresso) library.
Tests can be run on a real device, or on a virtual device (such as the emulator in Android Studio).
Currently the test are covering a small set of application flows:
- Registration
- Self verification via emoji
- Self verification via passphrase
## Prerequisites:
Out of the box, the tests use one of the homeservers (located at http://localhost:8080) of the "Demo Federation of Homeservers" (https://github.com/matrix-org/synapse#running-a-demo-federation-of-synapses).
You first need to follow instructions to set up Synapse in development mode at https://github.com/matrix-org/synapse#synapse-development. If you have already installed all dependencies, the steps are:
```shell script
$ git clone https://github.com/matrix-org/synapse.git
$ cd synapse
$ virtualenv -p python3 env
$ source env/bin/activate
(env) $ python -m pip install --no-use-pep517 -e .
```
Every time you want to launch these test homeservers, type:
```shell script
$ virtualenv -p python3 env
$ source env/bin/activate
(env) $ demo/start.sh --no-rate-limit
```
**Emulator/Device set up**
When running the test via android studio on a device, you have to disable system animations in order for the test to work properly.
First, ensure developer mode is enabled:
- To enable developer options, tap the **Build Number** option 7 times. You can find this option in one of the following locations, depending on your Android version:
- Android 9 (API level 28) and higher: **Settings > About Phone > Build Number**
- Android 8.0.0 (API level 26) and Android 8.1.0 (API level 26): **Settings > System > About Phone > Build Number**
- Android 7.1 (API level 25) and lower: **Settings > About Phone > Build Number**
On your device, under **Settings > Developer options**, disable the following 3 settings:
- Window animation scale
- Transition animation scale
- Animator duration scale
## Run the tests
Once Synapse is running, and an emulator is running, you can run the UI tests.
### From the source code
Click on the green arrow in front of each test. Clicking on the arrow in front of the test class, or from the package directory does not always work (Tests not found issue).
### From command line
````shell script
./gradlew vector:connectedGplayDebugAndroidTest
````
To run all the tests from the `vector` module.
In case of trouble, you can try to uninstall the previous installed test APK first with this command:
```shell script
adb uninstall im.vector.app.debug.test
```
## Recipes
We added some specific Espresso IdlingResources, and other utilities for matrix related tests
### Wait for initial sync
```kotlin
// Wait for initial sync and check room list is there
withIdlingResource(initialSyncIdlingResource(uiSession)) {
onView(withId(R.id.roomListContainer))
.check(matches(isDisplayed()))
}
```
### Accessing current activity
```kotlin
val activity = EspressoHelper.getCurrentActivity()!!
val uiSession = (activity as HomeActivity).activeSessionHolder.getActiveSession()
```
### Interact with other session
It's possible to create a session via the SDK, and then use this session to interact with the one that the emulator is using (to check verifications for example)
```kotlin
@Before
fun initAccount() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val matrix = Matrix.getInstance(context)
val userName = "foobar_${System.currentTimeMillis()}"
existingSession = createAccountAndSync(matrix, userName, password, true)
}
```

View File

@ -144,7 +144,6 @@ dependencies {
// Image // Image
implementation 'androidx.exifinterface:exifinterface:1.3.0-alpha01' implementation 'androidx.exifinterface:exifinterface:1.3.0-alpha01'
implementation 'id.zelory:compressor:3.0.0'
// Database // Database
implementation 'com.github.Zhuinden:realm-monarchy:0.5.1' implementation 'com.github.Zhuinden:realm-monarchy:0.5.1'

View File

@ -24,6 +24,7 @@ import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.BuildConfig
import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.legacy.LegacySessionImporter import org.matrix.android.sdk.api.legacy.LegacySessionImporter
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.common.DaggerTestMatrixComponent import org.matrix.android.sdk.common.DaggerTestMatrixComponent
import org.matrix.android.sdk.internal.SessionManager import org.matrix.android.sdk.internal.SessionManager
import org.matrix.android.sdk.internal.network.UserAgentHolder import org.matrix.android.sdk.internal.network.UserAgentHolder
@ -41,6 +42,7 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
@Inject internal lateinit var legacySessionImporter: LegacySessionImporter @Inject internal lateinit var legacySessionImporter: LegacySessionImporter
@Inject internal lateinit var authenticationService: AuthenticationService @Inject internal lateinit var authenticationService: AuthenticationService
@Inject internal lateinit var rawService: RawService
@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
@ -61,6 +63,8 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
return authenticationService return authenticationService
} }
fun rawService() = rawService
fun legacySessionImporter(): LegacySessionImporter { fun legacySessionImporter(): LegacySessionImporter {
return legacySessionImporter return legacySessionImporter
} }

View File

@ -218,7 +218,7 @@ class CommonTestHelper(context: Context) {
.createAccount(userName, password, null, it) .createAccount(userName, password, null, it)
} }
// Preform dummy step // Perform dummy step
val registrationResult = doSync<RegistrationResult> { val registrationResult = doSync<RegistrationResult> {
matrix.authenticationService matrix.authenticationService
.getRegistrationWizard() .getRegistrationWizard()

View File

@ -25,8 +25,16 @@ import org.matrix.android.sdk.internal.di.MatrixComponent
import org.matrix.android.sdk.internal.di.MatrixModule import org.matrix.android.sdk.internal.di.MatrixModule
import org.matrix.android.sdk.internal.di.MatrixScope import org.matrix.android.sdk.internal.di.MatrixScope
import org.matrix.android.sdk.internal.di.NetworkModule import org.matrix.android.sdk.internal.di.NetworkModule
import org.matrix.android.sdk.internal.raw.RawModule
@Component(modules = [TestModule::class, MatrixModule::class, NetworkModule::class, AuthModule::class, TestNetworkModule::class]) @Component(modules = [
TestModule::class,
MatrixModule::class,
NetworkModule::class,
AuthModule::class,
RawModule::class,
TestNetworkModule::class
])
@MatrixScope @MatrixScope
internal interface TestMatrixComponent : MatrixComponent { internal interface TestMatrixComponent : MatrixComponent {

View File

@ -18,7 +18,7 @@ package org.matrix.android.sdk.internal.crypto
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.extensions.tryThis import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
@ -212,7 +212,7 @@ class UnwedgingTest : InstrumentedTest {
mTestHelper.waitWithLatch { mTestHelper.waitWithLatch {
mTestHelper.retryPeriodicallyWithLatch(it) { mTestHelper.retryPeriodicallyWithLatch(it) {
// we should get back the key and be able to decrypt // we should get back the key and be able to decrypt
val result = tryThis { val result = tryOrNull {
bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "") bobSession.cryptoService().decryptEvent(messagesReceivedByBob[0].root, "")
} }
Timber.i("## CRYPTO | testUnwedging: decrypt result ${result?.clearEvent}") Timber.i("## CRYPTO | testUnwedging: decrypt result ${result?.clearEvent}")

View File

@ -20,7 +20,7 @@ import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.NoOpMatrixCallback
import org.matrix.android.sdk.api.extensions.tryThis import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
@ -227,7 +227,7 @@ class WithHeldTests : InstrumentedTest {
mTestHelper.retryPeriodicallyWithLatch(latch) { mTestHelper.retryPeriodicallyWithLatch(latch) {
val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimeLineEvent(eventId)?.also { val timeLineEvent = bobSecondSession.getRoom(testData.roomId)?.getTimeLineEvent(eventId)?.also {
// try to decrypt and force key request // try to decrypt and force key request
tryThis { bobSecondSession.cryptoService().decryptEvent(it.root, "") } tryOrNull { bobSecondSession.cryptoService().decryptEvent(it.root, "") }
} }
sessionId = timeLineEvent?.root?.content?.toModel<EncryptedEventContent>()?.sessionId sessionId = timeLineEvent?.root?.content?.toModel<EncryptedEventContent>()?.sessionId
timeLineEvent != null timeLineEvent != null

View File

@ -25,6 +25,7 @@ import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.BuildConfig
import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.legacy.LegacySessionImporter import org.matrix.android.sdk.api.legacy.LegacySessionImporter
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.internal.SessionManager import org.matrix.android.sdk.internal.SessionManager
import org.matrix.android.sdk.internal.di.DaggerMatrixComponent import org.matrix.android.sdk.internal.di.DaggerMatrixComponent
import org.matrix.android.sdk.internal.network.UserAgentHolder import org.matrix.android.sdk.internal.network.UserAgentHolder
@ -42,6 +43,7 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
@Inject internal lateinit var legacySessionImporter: LegacySessionImporter @Inject internal lateinit var legacySessionImporter: LegacySessionImporter
@Inject internal lateinit var authenticationService: AuthenticationService @Inject internal lateinit var authenticationService: AuthenticationService
@Inject internal lateinit var rawService: RawService
@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
@ -62,6 +64,8 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
return authenticationService return authenticationService
} }
fun rawService() = rawService
fun legacySessionImporter(): LegacySessionImporter { fun legacySessionImporter(): LegacySessionImporter {
return legacySessionImporter return legacySessionImporter
} }

View File

@ -17,13 +17,11 @@
package org.matrix.android.sdk.api.auth.data package org.matrix.android.sdk.api.auth.data
// Either a list of supported login types, or an error if the homeserver is outdated
sealed class LoginFlowResult { sealed class LoginFlowResult {
data class Success( data class Success(
val supportedLoginTypes: List<String>, val supportedLoginTypes: List<String>,
val isLoginAndRegistrationSupported: Boolean, val isLoginAndRegistrationSupported: Boolean,
val homeServerUrl: String val homeServerUrl: String,
val isOutdatedHomeserver: Boolean
) : LoginFlowResult() ) : LoginFlowResult()
object OutdatedHomeserver : LoginFlowResult()
} }

View File

@ -42,9 +42,6 @@ import org.matrix.android.sdk.api.util.JsonDict
* } * }
* ] * ]
* } * }
* "im.vector.riot.jitsi": {
* "preferredDomain": "https://jitsi.riot.im/"
* }
* } * }
* </pre> * </pre>
*/ */
@ -57,24 +54,5 @@ data class WellKnown(
val identityServer: WellKnownBaseConfig? = null, val identityServer: WellKnownBaseConfig? = null,
@Json(name = "m.integrations") @Json(name = "m.integrations")
val integrations: JsonDict? = null, val integrations: JsonDict? = null
@Json(name = "im.vector.riot.e2ee")
val e2eAdminSetting: E2EWellKnownConfig? = null,
@Json(name = "im.vector.riot.jitsi")
val jitsiServer: WellKnownPreferredConfig? = null
)
@JsonClass(generateAdapter = true)
data class E2EWellKnownConfig(
@Json(name = "default")
val e2eDefault: Boolean = true
)
@JsonClass(generateAdapter = true)
data class WellKnownPreferredConfig(
@Json(name = "preferredDomain")
val preferredDomain: String? = null
) )

View File

@ -19,7 +19,7 @@ package org.matrix.android.sdk.api.extensions
import timber.log.Timber import timber.log.Timber
inline fun <A> tryThis(message: String? = null, operation: () -> A): A? { inline fun <A> tryOrNull(message: String? = null, operation: () -> A): A? {
return try { return try {
operation() operation()
} catch (any: Throwable) { } catch (any: Throwable) {

View File

@ -17,7 +17,7 @@
package org.matrix.android.sdk.api.failure package org.matrix.android.sdk.api.failure
import org.matrix.android.sdk.api.extensions.tryThis import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.MoshiProvider
import java.io.IOException import java.io.IOException
@ -49,7 +49,7 @@ fun Throwable.isInvalidPassword(): Boolean {
*/ */
fun Throwable.toRegistrationFlowResponse(): RegistrationFlowResponse? { fun Throwable.toRegistrationFlowResponse(): RegistrationFlowResponse? {
return if (this is Failure.OtherServerError && this.httpCode == 401) { return if (this is Failure.OtherServerError && this.httpCode == 401) {
tryThis { tryOrNull {
MoshiProvider.providesMoshi() MoshiProvider.providesMoshi()
.adapter(RegistrationFlowResponse::class.java) .adapter(RegistrationFlowResponse::class.java)
.fromJson(this.errorBody) .fromJson(this.errorBody)

View File

@ -132,6 +132,8 @@ data class MatrixError(
const val M_CANNOT_LEAVE_SERVER_NOTICE_ROOM = "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM" const val M_CANNOT_LEAVE_SERVER_NOTICE_ROOM = "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM"
/** (Not documented yet) */ /** (Not documented yet) */
const val M_WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION" const val M_WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION"
/** (Not documented yet) */
const val M_WEAK_PASSWORD = "M_WEAK_PASSWORD"
const val M_TERMS_NOT_SIGNED = "M_TERMS_NOT_SIGNED" const val M_TERMS_NOT_SIGNED = "M_TERMS_NOT_SIGNED"

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) 2020 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.raw
sealed class RawCacheStrategy {
// Data is always fetched from the server
object NoCache: RawCacheStrategy()
// Once data is retrieved, it is stored for the provided amount of time.
// In case of error, and if strict is set to false, the cache can be returned if available
data class TtlCache(val validityDurationInMillis: Long, val strict: Boolean): RawCacheStrategy()
// Once retrieved, the data is stored in cache and will be always get from the cache
object InfiniteCache: RawCacheStrategy()
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2020 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.raw
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.util.Cancelable
/**
* Useful methods to fetch raw data from the server. The access token will not be used to fetched the data
*/
interface RawService {
/**
* Get a URL, either from cache or from the remote server, depending on the cache strategy
*/
fun getUrl(url: String,
rawCacheStrategy: RawCacheStrategy,
matrixCallback: MatrixCallback<String>): Cancelable
/**
* Specific case for the well-known file. Cache validity is 8 hours
*/
fun getWellknown(userId: String, matrixCallback: MatrixCallback<String>): Cancelable
/**
* Clear all the cache data
*/
fun clearCache(matrixCallback: MatrixCallback<Unit>): Cancelable
}

View File

@ -110,7 +110,7 @@ interface Session :
* This does not work in doze mode :/ * This does not work in doze mode :/
* If battery optimization is on it can work in app standby but that's all :/ * If battery optimization is on it can work in app standby but that's all :/
*/ */
fun startAutomaticBackgroundSync(repeatDelay: Long = 30_000L) fun startAutomaticBackgroundSync(timeOutInSeconds: Long, repeatDelayInSeconds: Long)
fun stopAnyBackgroundSync() fun stopAnyBackgroundSync()

View File

@ -33,16 +33,7 @@ data class HomeServerCapabilities(
/** /**
* Default identity server url, provided in Wellknown * Default identity server url, provided in Wellknown
*/ */
val defaultIdentityServerUrl: String? = null, val defaultIdentityServerUrl: String? = null
/**
* Option to allow homeserver admins to set the default E2EE behaviour back to disabled for DMs / private rooms
* (as it was before) for various environments where this is desired.
*/
val adminE2EByDefault: Boolean = true,
/**
* Preferred Jitsi domain, provided in Wellknown
*/
val preferredJitsiDomain: String? = null
) { ) {
companion object { companion object {
const val MAX_UPLOAD_FILE_SIZE_UNKNOWN = -1L const val MAX_UPLOAD_FILE_SIZE_UNKNOWN = -1L

View File

@ -0,0 +1,33 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.room.summary
import org.matrix.android.sdk.api.session.events.model.EventType
object RoomSummaryConstants {
val PREVIEWABLE_TYPES = listOf(
// TODO filter message type (KEY_VERIFICATION_READY, etc.)
EventType.MESSAGE,
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
EventType.CALL_ANSWER,
EventType.ENCRYPTED,
EventType.STICKER,
EventType.REACTION
)
}

View File

@ -0,0 +1,40 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.room.timeline
data class TimelineEventFilters(
/**
* A flag to filter edit events
*/
val filterEdits: Boolean = false,
/**
* A flag to filter redacted events
*/
val filterRedacted: Boolean = false,
/**
* A flag to filter useless events, such as membership events without any change
*/
val filterUseless: Boolean = false,
/**
* A flag to filter by types. It should be used with [allowedTypes] field
*/
val filterTypes: Boolean = false,
/**
* If [filterTypes] is true, the list of types allowed by the list.
*/
val allowedTypes: List<String> = emptyList()
)

View File

@ -26,25 +26,9 @@ data class TimelineSettings(
*/ */
val initialSize: Int, val initialSize: Int,
/** /**
* A flag to filter edit events * Filters for timeline event
*/ */
val filterEdits: Boolean = false, val filters: TimelineEventFilters = TimelineEventFilters(),
/**
* A flag to filter redacted events
*/
val filterRedacted: Boolean = false,
/**
* A flag to filter useless events, such as membership events without any change
*/
val filterUseless: Boolean = false,
/**
* A flag to filter by types. It should be used with [allowedTypes] field
*/
val filterTypes: Boolean = false,
/**
* If [filterTypes] is true, the list of types allowed by the list.
*/
val allowedTypes: List<String> = emptyList(),
/** /**
* If true, will build read receipts for each event. * If true, will build read receipts for each event.
*/ */

View File

@ -273,16 +273,16 @@ internal class DefaultAuthenticationService @Inject constructor(
} }
private suspend fun getLoginFlowResult(authAPI: AuthAPI, versions: Versions, homeServerUrl: String): LoginFlowResult { private suspend fun getLoginFlowResult(authAPI: AuthAPI, versions: Versions, homeServerUrl: String): LoginFlowResult {
return if (versions.isSupportedBySdk()) { // Get the login flow
// Get the login flow val loginFlowResponse = executeRequest<LoginFlowResponse>(null) {
val loginFlowResponse = executeRequest<LoginFlowResponse>(null) { apiCall = authAPI.getLoginFlows()
apiCall = authAPI.getLoginFlows()
}
LoginFlowResult.Success(loginFlowResponse.flows.orEmpty().mapNotNull { it.type }, versions.isLoginAndRegistrationSupportedBySdk(), homeServerUrl)
} else {
// Not supported
LoginFlowResult.OutdatedHomeserver
} }
return LoginFlowResult.Success(
loginFlowResponse.flows.orEmpty().mapNotNull { it.type },
versions.isLoginAndRegistrationSupportedBySdk(),
homeServerUrl,
!versions.isSupportedBySdk()
)
} }
override fun getRegistrationWizard(): RegistrationWizard { override fun getRegistrationWizard(): RegistrationWizard {

View File

@ -18,10 +18,9 @@
package org.matrix.android.sdk.internal.crypto package org.matrix.android.sdk.internal.crypto
import android.content.Context import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.failure.shouldBeRetried import org.matrix.android.sdk.api.failure.shouldBeRetried
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
@ -32,28 +31,29 @@ import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.model.rest.ShareRequestCancellation import org.matrix.android.sdk.internal.crypto.model.rest.ShareRequestCancellation
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.worker.getSessionComponent import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.greenrobot.eventbus.EventBus import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
internal class CancelGossipRequestWorker(context: Context, internal class CancelGossipRequestWorker(context: Context,
params: WorkerParameters) params: WorkerParameters)
: CoroutineWorker(context, params) { : SessionSafeCoroutineWorker<CancelGossipRequestWorker.Params>(context, params, Params::class.java) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class Params( internal data class Params(
val sessionId: String, override val sessionId: String,
val requestId: String, val requestId: String,
val recipients: Map<String, List<String>> val recipients: Map<String, List<String>>,
) { override val lastFailureMessage: String? = null
) : SessionWorkerParams {
companion object { companion object {
fun fromRequest(sessionId: String, request: OutgoingGossipingRequest): Params { fun fromRequest(sessionId: String, request: OutgoingGossipingRequest): Params {
return Params( return Params(
sessionId = sessionId, sessionId = sessionId,
requestId = request.requestId, requestId = request.requestId,
recipients = request.recipients recipients = request.recipients,
lastFailureMessage = null
) )
} }
} }
@ -64,18 +64,11 @@ internal class CancelGossipRequestWorker(context: Context,
@Inject lateinit var eventBus: EventBus @Inject lateinit var eventBus: EventBus
@Inject lateinit var credentials: Credentials @Inject lateinit var credentials: Credentials
override suspend fun doWork(): Result { override fun injectWith(injector: SessionComponent) {
val errorOutputData = Data.Builder().putBoolean("failed", true).build() injector.inject(this)
val params = WorkerParamsFactory.fromData<Params>(inputData) }
?: return Result.success(errorOutputData)
val sessionComponent = getSessionComponent(params.sessionId)
?: return Result.success(errorOutputData).also {
// TODO, can this happen? should I update local echo?
Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}")
}
sessionComponent.inject(this)
override suspend fun doSafeWork(params: Params): Result {
val localId = LocalEcho.createLocalEchoId() val localId = LocalEcho.createLocalEchoId()
val contentMap = MXUsersDevicesMap<Any>() val contentMap = MXUsersDevicesMap<Any>()
val toDeviceContent = ShareRequestCancellation( val toDeviceContent = ShareRequestCancellation(
@ -107,13 +100,17 @@ internal class CancelGossipRequestWorker(context: Context,
) )
cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.CANCELLED) cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.CANCELLED)
return Result.success() return Result.success()
} catch (exception: Throwable) { } catch (throwable: Throwable) {
return if (exception.shouldBeRetried()) { return if (throwable.shouldBeRetried()) {
Result.retry() Result.retry()
} else { } else {
cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.FAILED_TO_CANCEL) cryptoStore.updateOutgoingGossipingRequestState(params.requestId, OutgoingGossipingRequestState.FAILED_TO_CANCEL)
Result.success(errorOutputData) buildErrorResult(params, throwable.localizedMessage ?: "error")
} }
} }
} }
override fun buildErrorParams(params: Params, message: String): Params {
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
}
} }

View File

@ -27,10 +27,16 @@ import androidx.lifecycle.LiveData
import com.squareup.moshi.Types import com.squareup.moshi.Types
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import dagger.Lazy import dagger.Lazy
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.NoOpMatrixCallback
import org.matrix.android.sdk.api.crypto.MXCryptoConfig import org.matrix.android.sdk.api.crypto.MXCryptoConfig
import org.matrix.android.sdk.api.extensions.tryThis import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.listeners.ProgressListener
import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.CryptoService
@ -102,12 +108,6 @@ import org.matrix.android.sdk.internal.task.launchToCallback
import org.matrix.android.sdk.internal.util.JsonCanonicalizer import org.matrix.android.sdk.internal.util.JsonCanonicalizer
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import org.matrix.android.sdk.internal.util.fetchCopied import org.matrix.android.sdk.internal.util.fetchCopied
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.matrix.olm.OlmManager import org.matrix.olm.OlmManager
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@ -345,13 +345,13 @@ internal class DefaultCryptoService @Inject constructor(
// Open the store // Open the store
cryptoStore.open() cryptoStore.open()
// this can throw if no network // this can throw if no network
tryThis { tryOrNull {
uploadDeviceKeys() uploadDeviceKeys()
} }
oneTimeKeysUploader.maybeUploadOneTimeKeys() oneTimeKeysUploader.maybeUploadOneTimeKeys()
// this can throw if no backup // this can throw if no backup
tryThis { tryOrNull {
keysBackupService.checkAndStartKeysBackup() keysBackupService.checkAndStartKeysBackup()
} }
} }
@ -1072,7 +1072,11 @@ internal class DefaultCryptoService @Inject constructor(
throw Exception("Error") throw Exception("Error")
} }
megolmSessionDataImporter.handle(importedSessions, true, progressListener) megolmSessionDataImporter.handle(
megolmSessionsData = importedSessions,
fromBackup = false,
progressListener = progressListener
)
} }
}.foldToCallback(callback) }.foldToCallback(callback)
} }

View File

@ -126,7 +126,7 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
* @param request the request * @param request the request
*/ */
private fun sendOutgoingGossipingRequest(request: OutgoingGossipingRequest) { private fun sendOutgoingGossipingRequest(request: OutgoingGossipingRequest) {
Timber.v("## CRYPTO - GOSSIP sendOutgoingRoomKeyRequest() : Requesting keys $request") Timber.v("## CRYPTO - GOSSIP sendOutgoingGossipingRequest() : Requesting keys $request")
val params = SendGossipRequestWorker.Params( val params = SendGossipRequestWorker.Params(
sessionId = sessionId, sessionId = sessionId,

View File

@ -18,10 +18,9 @@
package org.matrix.android.sdk.internal.crypto package org.matrix.android.sdk.internal.crypto
import android.content.Context import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.failure.shouldBeRetried import org.matrix.android.sdk.api.failure.shouldBeRetried
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
@ -34,40 +33,34 @@ import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyShareRequest
import org.matrix.android.sdk.internal.crypto.model.rest.SecretShareRequest import org.matrix.android.sdk.internal.crypto.model.rest.SecretShareRequest
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.worker.getSessionComponent import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.greenrobot.eventbus.EventBus import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
internal class SendGossipRequestWorker(context: Context, internal class SendGossipRequestWorker(context: Context,
params: WorkerParameters) params: WorkerParameters)
: CoroutineWorker(context, params) { : SessionSafeCoroutineWorker<SendGossipRequestWorker.Params>(context, params, Params::class.java) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class Params( internal data class Params(
val sessionId: String, override val sessionId: String,
val keyShareRequest: OutgoingRoomKeyRequest? = null, val keyShareRequest: OutgoingRoomKeyRequest? = null,
val secretShareRequest: OutgoingSecretRequest? = null val secretShareRequest: OutgoingSecretRequest? = null,
) override val lastFailureMessage: String? = null
) : SessionWorkerParams
@Inject lateinit var sendToDeviceTask: SendToDeviceTask @Inject lateinit var sendToDeviceTask: SendToDeviceTask
@Inject lateinit var cryptoStore: IMXCryptoStore @Inject lateinit var cryptoStore: IMXCryptoStore
@Inject lateinit var eventBus: EventBus @Inject lateinit var eventBus: EventBus
@Inject lateinit var credentials: Credentials @Inject lateinit var credentials: Credentials
override suspend fun doWork(): Result { override fun injectWith(injector: SessionComponent) {
val errorOutputData = Data.Builder().putBoolean("failed", true).build() injector.inject(this)
val params = WorkerParamsFactory.fromData<Params>(inputData) }
?: return Result.success(errorOutputData)
val sessionComponent = getSessionComponent(params.sessionId)
?: return Result.success(errorOutputData).also {
// TODO, can this happen? should I update local echo?
Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}")
}
sessionComponent.inject(this)
override suspend fun doSafeWork(params: Params): Result {
val localId = LocalEcho.createLocalEchoId() val localId = LocalEcho.createLocalEchoId()
val contentMap = MXUsersDevicesMap<Any>() val contentMap = MXUsersDevicesMap<Any>()
val eventType: String val eventType: String
@ -121,7 +114,7 @@ internal class SendGossipRequestWorker(context: Context,
} }
} }
else -> { else -> {
return Result.success(errorOutputData).also { return buildErrorResult(params, "Unknown empty gossiping request").also {
Timber.e("Unknown empty gossiping request: $params") Timber.e("Unknown empty gossiping request: $params")
} }
} }
@ -137,13 +130,17 @@ internal class SendGossipRequestWorker(context: Context,
) )
cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.SENT) cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.SENT)
return Result.success() return Result.success()
} catch (exception: Throwable) { } catch (throwable: Throwable) {
return if (exception.shouldBeRetried()) { return if (throwable.shouldBeRetried()) {
Result.retry() Result.retry()
} else { } else {
cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.FAILED_TO_SEND) cryptoStore.updateOutgoingGossipingRequestState(requestId, OutgoingGossipingRequestState.FAILED_TO_SEND)
Result.success(errorOutputData) buildErrorResult(params, throwable.localizedMessage ?: "error")
} }
} }
} }
override fun buildErrorParams(params: Params, message: String): Params {
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
}
} }

View File

@ -18,10 +18,9 @@
package org.matrix.android.sdk.internal.crypto package org.matrix.android.sdk.internal.crypto
import android.content.Context import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.failure.shouldBeRetried import org.matrix.android.sdk.api.failure.shouldBeRetried
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
@ -34,22 +33,23 @@ import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent import org.matrix.android.sdk.internal.crypto.model.event.SecretSendEventContent
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.worker.getSessionComponent import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.greenrobot.eventbus.EventBus import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
internal class SendGossipWorker(context: Context, internal class SendGossipWorker(context: Context,
params: WorkerParameters) params: WorkerParameters)
: CoroutineWorker(context, params) { : SessionSafeCoroutineWorker<SendGossipWorker.Params>(context, params, Params::class.java) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class Params( internal data class Params(
val sessionId: String, override val sessionId: String,
val secretValue: String, val secretValue: String,
val request: IncomingSecretShareRequest val request: IncomingSecretShareRequest,
) override val lastFailureMessage: String? = null
) : SessionWorkerParams
@Inject lateinit var sendToDeviceTask: SendToDeviceTask @Inject lateinit var sendToDeviceTask: SendToDeviceTask
@Inject lateinit var cryptoStore: IMXCryptoStore @Inject lateinit var cryptoStore: IMXCryptoStore
@ -58,18 +58,11 @@ internal class SendGossipWorker(context: Context,
@Inject lateinit var messageEncrypter: MessageEncrypter @Inject lateinit var messageEncrypter: MessageEncrypter
@Inject lateinit var ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction @Inject lateinit var ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction
override suspend fun doWork(): Result { override fun injectWith(injector: SessionComponent) {
val errorOutputData = Data.Builder().putBoolean("failed", true).build() injector.inject(this)
val params = WorkerParamsFactory.fromData<Params>(inputData) }
?: return Result.success(errorOutputData)
val sessionComponent = getSessionComponent(params.sessionId)
?: return Result.success(errorOutputData).also {
// TODO, can this happen? should I update local echo?
Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}")
}
sessionComponent.inject(this)
override suspend fun doSafeWork(params: Params): Result {
val localId = LocalEcho.createLocalEchoId() val localId = LocalEcho.createLocalEchoId()
val eventType: String = EventType.SEND_SECRET val eventType: String = EventType.SEND_SECRET
@ -81,7 +74,7 @@ internal class SendGossipWorker(context: Context,
val requestingUserId = params.request.userId ?: "" val requestingUserId = params.request.userId ?: ""
val requestingDeviceId = params.request.deviceId ?: "" val requestingDeviceId = params.request.deviceId ?: ""
val deviceInfo = cryptoStore.getUserDevice(requestingUserId, requestingDeviceId) val deviceInfo = cryptoStore.getUserDevice(requestingUserId, requestingDeviceId)
?: return Result.success(errorOutputData).also { ?: return buildErrorResult(params, "Unknown deviceInfo, cannot send message").also {
cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED) cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED)
Timber.e("Unknown deviceInfo, cannot send message, sessionId: ${params.request.deviceId}") Timber.e("Unknown deviceInfo, cannot send message, sessionId: ${params.request.deviceId}")
} }
@ -94,7 +87,7 @@ internal class SendGossipWorker(context: Context,
if (olmSessionResult?.sessionId == null) { if (olmSessionResult?.sessionId == null) {
// no session with this device, probably because there // no session with this device, probably because there
// were no one-time keys. // were no one-time keys.
return Result.success(errorOutputData).also { return buildErrorResult(params, "no session with this device").also {
cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED) cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED)
Timber.e("no session with this device, probably because there were no one-time keys.") Timber.e("no session with this device, probably because there were no one-time keys.")
} }
@ -130,13 +123,17 @@ internal class SendGossipWorker(context: Context,
) )
cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.ACCEPTED) cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.ACCEPTED)
return Result.success() return Result.success()
} catch (exception: Throwable) { } catch (throwable: Throwable) {
return if (exception.shouldBeRetried()) { return if (throwable.shouldBeRetried()) {
Result.retry() Result.retry()
} else { } else {
cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED) cryptoStore.updateGossipingRequestState(params.request, GossipingRequestState.FAILED_TO_ACCEPTED)
Result.success(errorOutputData) buildErrorResult(params, throwable.localizedMessage ?: "error")
} }
} }
} }
override fun buildErrorParams(params: Params, message: String): Params {
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
}
} }

View File

@ -39,7 +39,7 @@ internal class MegolmSessionDataImporter @Inject constructor(private val olmDevi
* Must be call on the crypto coroutine thread * Must be call on the crypto coroutine thread
* *
* @param megolmSessionsData megolm sessions. * @param megolmSessionsData megolm sessions.
* @param backUpKeys true to back up them to the homeserver. * @param fromBackup true if the imported keys are already backed up on the server.
* @param progressListener the progress listener * @param progressListener the progress listener
* @return import room keys result * @return import room keys result
*/ */

View File

@ -20,6 +20,10 @@ package org.matrix.android.sdk.internal.crypto.store.db
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.Sort
import io.realm.kotlin.where
import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.auth.data.Credentials
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
@ -85,10 +89,6 @@ import org.matrix.android.sdk.internal.database.mapper.ContentMapper
import org.matrix.android.sdk.internal.di.CryptoDatabase import org.matrix.android.sdk.internal.di.CryptoDatabase
import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.MoshiProvider
import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.SessionScope
import io.realm.Realm
import io.realm.RealmConfiguration
import io.realm.Sort
import io.realm.kotlin.where
import org.matrix.olm.OlmAccount import org.matrix.olm.OlmAccount
import org.matrix.olm.OlmException import org.matrix.olm.OlmException
import timber.log.Timber import timber.log.Timber
@ -372,6 +372,7 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) { override fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) {
Timber.v("## CRYPTO | *** storePrivateKeysInfo ${msk != null}, ${usk != null}, ${ssk != null}")
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction(realmConfiguration) { realm ->
realm.where<CryptoMetadataEntity>().findFirst()?.apply { realm.where<CryptoMetadataEntity>().findFirst()?.apply {
xSignMasterPrivateKey = msk xSignMasterPrivateKey = msk
@ -407,6 +408,7 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun storeMSKPrivateKey(msk: String?) { override fun storeMSKPrivateKey(msk: String?) {
Timber.v("## CRYPTO | *** storeMSKPrivateKey ${msk != null} ")
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction(realmConfiguration) { realm ->
realm.where<CryptoMetadataEntity>().findFirst()?.apply { realm.where<CryptoMetadataEntity>().findFirst()?.apply {
xSignMasterPrivateKey = msk xSignMasterPrivateKey = msk
@ -415,6 +417,7 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun storeSSKPrivateKey(ssk: String?) { override fun storeSSKPrivateKey(ssk: String?) {
Timber.v("## CRYPTO | *** storeSSKPrivateKey ${ssk != null} ")
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction(realmConfiguration) { realm ->
realm.where<CryptoMetadataEntity>().findFirst()?.apply { realm.where<CryptoMetadataEntity>().findFirst()?.apply {
xSignSelfSignedPrivateKey = ssk xSignSelfSignedPrivateKey = ssk
@ -423,6 +426,7 @@ internal class RealmCryptoStore @Inject constructor(
} }
override fun storeUSKPrivateKey(usk: String?) { override fun storeUSKPrivateKey(usk: String?) {
Timber.v("## CRYPTO | *** storeUSKPrivateKey ${usk != null} ")
doRealmTransaction(realmConfiguration) { realm -> doRealmTransaction(realmConfiguration) { realm ->
realm.where<CryptoMetadataEntity>().findFirst()?.apply { realm.where<CryptoMetadataEntity>().findFirst()?.apply {
xSignUserPrivateKey = usk xSignUserPrivateKey = usk
@ -541,7 +545,7 @@ internal class RealmCryptoStore @Inject constructor(
deviceId = it.deviceId deviceId = it.deviceId
) )
} }
monarchy.writeAsync { realm -> doRealmTransactionAsync(realmConfiguration) { realm ->
realm.where<MyDeviceLastSeenInfoEntity>().findAll().deleteAllFromRealm() realm.where<MyDeviceLastSeenInfoEntity>().findAll().deleteAllFromRealm()
entities.forEach { entities.forEach {
realm.insertOrUpdate(it) realm.insertOrUpdate(it)
@ -1191,7 +1195,7 @@ internal class RealmCryptoStore @Inject constructor(
.findAll() .findAll()
.mapNotNull { entity -> .mapNotNull { entity ->
when (entity.type) { when (entity.type) {
GossipRequestType.KEY -> { GossipRequestType.KEY -> {
IncomingRoomKeyRequest( IncomingRoomKeyRequest(
userId = entity.otherUserId, userId = entity.otherUserId,
deviceId = entity.otherDeviceId, deviceId = entity.otherDeviceId,

View File

@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.crypto.store.db
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.squareup.moshi.Types import com.squareup.moshi.Types
import org.matrix.android.sdk.api.extensions.tryThis import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.crypto.model.MXDeviceInfo import org.matrix.android.sdk.internal.crypto.model.MXDeviceInfo
import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper import org.matrix.android.sdk.internal.crypto.model.OlmInboundGroupSessionWrapper
@ -398,7 +398,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
?.addField(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, Long::class.java) ?.addField(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, Long::class.java)
?.setNullable(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, true) ?.setNullable(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, true)
?.transform { deviceInfoEntity -> ?.transform { deviceInfoEntity ->
tryThis { tryOrNull {
deviceInfoEntity.setLong(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, now) deviceInfoEntity.setLong(DeviceInfoEntityFields.FIRST_TIME_SEEN_LOCAL_TS, now)
} }
} }

View File

@ -17,7 +17,7 @@
package org.matrix.android.sdk.internal.crypto.store.db.model package org.matrix.android.sdk.internal.crypto.store.db.model
import org.matrix.android.sdk.api.extensions.tryThis import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.internal.crypto.GossipRequestType import org.matrix.android.sdk.internal.crypto.GossipRequestType
import org.matrix.android.sdk.internal.crypto.GossipingRequestState import org.matrix.android.sdk.internal.crypto.GossipingRequestState
import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest
@ -45,7 +45,7 @@ internal open class IncomingGossipingRequestEntity(@Index var requestId: String?
var type: GossipRequestType var type: GossipRequestType
get() { get() {
return tryThis { typeStr?.let { GossipRequestType.valueOf(it) } } ?: GossipRequestType.KEY return tryOrNull { typeStr?.let { GossipRequestType.valueOf(it) } } ?: GossipRequestType.KEY
} }
set(value) { set(value) {
typeStr = value.name typeStr = value.name
@ -55,7 +55,7 @@ internal open class IncomingGossipingRequestEntity(@Index var requestId: String?
var requestState: GossipingRequestState var requestState: GossipingRequestState
get() { get() {
return tryThis { GossipingRequestState.valueOf(requestStateStr) } return tryOrNull { GossipingRequestState.valueOf(requestStateStr) }
?: GossipingRequestState.NONE ?: GossipingRequestState.NONE
} }
set(value) { set(value) {

View File

@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.crypto.store.db.model
import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Types import com.squareup.moshi.Types
import org.matrix.android.sdk.api.extensions.tryThis import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.internal.crypto.GossipRequestType import org.matrix.android.sdk.internal.crypto.GossipRequestType
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequest import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequest
import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestState import org.matrix.android.sdk.internal.crypto.OutgoingGossipingRequestState
@ -47,7 +47,7 @@ internal open class OutgoingGossipingRequestEntity(
var type: GossipRequestType var type: GossipRequestType
get() { get() {
return tryThis { typeStr?.let { GossipRequestType.valueOf(it) } } ?: GossipRequestType.KEY return tryOrNull { typeStr?.let { GossipRequestType.valueOf(it) } } ?: GossipRequestType.KEY
} }
set(value) { set(value) {
typeStr = value.name typeStr = value.name
@ -57,7 +57,7 @@ internal open class OutgoingGossipingRequestEntity(
var requestState: OutgoingGossipingRequestState var requestState: OutgoingGossipingRequestState
get() { get() {
return tryThis { OutgoingGossipingRequestState.valueOf(requestStateStr) } return tryOrNull { OutgoingGossipingRequestState.valueOf(requestStateStr) }
?: OutgoingGossipingRequestState.UNSENT ?: OutgoingGossipingRequestState.UNSENT
} }
set(value) { set(value) {

View File

@ -17,17 +17,17 @@
package org.matrix.android.sdk.internal.crypto.verification package org.matrix.android.sdk.internal.crypto.verification
import android.content.Context import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.Data import androidx.work.Data
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.failure.shouldBeRetried import org.matrix.android.sdk.api.failure.shouldBeRetried
import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask import org.matrix.android.sdk.internal.crypto.tasks.SendVerificationMessageTask
import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import org.matrix.android.sdk.internal.worker.getSessionComponent
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -37,56 +37,56 @@ import javax.inject.Inject
*/ */
internal class SendVerificationMessageWorker(context: Context, internal class SendVerificationMessageWorker(context: Context,
params: WorkerParameters) params: WorkerParameters)
: CoroutineWorker(context, params) { : SessionSafeCoroutineWorker<SendVerificationMessageWorker.Params>(context, params, Params::class.java) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class Params( internal data class Params(
override val sessionId: String, override val sessionId: String,
val event: Event, val eventId: String,
override val lastFailureMessage: String? = null override val lastFailureMessage: String? = null
) : SessionWorkerParams ) : SessionWorkerParams
@Inject @Inject lateinit var sendVerificationMessageTask: SendVerificationMessageTask
lateinit var sendVerificationMessageTask: SendVerificationMessageTask @Inject lateinit var localEchoRepository: LocalEchoRepository
@Inject lateinit var cryptoService: CryptoService
@Inject lateinit var cancelSendTracker: CancelSendTracker
@Inject override fun injectWith(injector: SessionComponent) {
lateinit var cryptoService: CryptoService injector.inject(this)
}
override suspend fun doWork(): Result { override suspend fun doSafeWork(params: Params): Result {
val errorOutputData = Data.Builder().putBoolean(OUTPUT_KEY_FAILED, true).build() val localEvent = localEchoRepository.getUpToDateEcho(params.eventId) ?: return buildErrorResult(params, "Event not found")
val params = WorkerParamsFactory.fromData<Params>(inputData) val localEventId = localEvent.eventId ?: ""
?: return Result.success(errorOutputData) val roomId = localEvent.roomId ?: ""
if (cancelSendTracker.isCancelRequestedFor(localEventId, roomId)) {
return Result.success()
.also {
cancelSendTracker.markCancelled(localEventId, roomId)
Timber.e("## SendEvent: Event sending has been cancelled $localEventId")
}
}
val sessionComponent = getSessionComponent(params.sessionId)
?: return Result.success(errorOutputData).also {
// TODO, can this happen? should I update local echo?
Timber.e("Unknown Session, cannot send message, sessionId: ${params.sessionId}")
}
sessionComponent.inject(this)
val localId = params.event.eventId ?: ""
return try { return try {
val eventId = sendVerificationMessageTask.execute( val resultEventId = sendVerificationMessageTask.execute(
SendVerificationMessageTask.Params( SendVerificationMessageTask.Params(
event = params.event, event = localEvent,
cryptoService = cryptoService cryptoService = cryptoService
) )
) )
Result.success(Data.Builder().putString(localId, eventId).build()) Result.success(Data.Builder().putString(localEventId, resultEventId).build())
} catch (exception: Throwable) { } catch (throwable: Throwable) {
if (exception.shouldBeRetried()) { if (throwable.shouldBeRetried()) {
Result.retry() Result.retry()
} else { } else {
Result.success(errorOutputData) buildErrorResult(params, throwable.localizedMessage ?: "error")
} }
} }
} }
companion object { override fun buildErrorParams(params: Params, message: String): Params {
private const val OUTPUT_KEY_FAILED = "failed" return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
fun hasFailed(outputData: Data): Boolean {
return outputData.getBoolean(OUTPUT_KEY_FAILED, false)
}
} }
} }

View File

@ -55,14 +55,14 @@ internal fun getEmojiForCode(code: Int): EmojiRepresentation {
31 -> EmojiRepresentation("🤖", R.string.verification_emoji_robot, R.drawable.ic_verification_robot) 31 -> EmojiRepresentation("🤖", R.string.verification_emoji_robot, R.drawable.ic_verification_robot)
32 -> EmojiRepresentation("🎩", R.string.verification_emoji_hat, R.drawable.ic_verification_hat) 32 -> EmojiRepresentation("🎩", R.string.verification_emoji_hat, R.drawable.ic_verification_hat)
33 -> EmojiRepresentation("👓", R.string.verification_emoji_glasses, R.drawable.ic_verification_glasses) 33 -> EmojiRepresentation("👓", R.string.verification_emoji_glasses, R.drawable.ic_verification_glasses)
34 -> EmojiRepresentation("🔧", R.string.verification_emoji_wrench, R.drawable.ic_verification_wrench) 34 -> EmojiRepresentation("🔧", R.string.verification_emoji_spanner, R.drawable.ic_verification_spanner)
35 -> EmojiRepresentation("🎅", R.string.verification_emoji_santa, R.drawable.ic_verification_santa) 35 -> EmojiRepresentation("🎅", R.string.verification_emoji_santa, R.drawable.ic_verification_santa)
36 -> EmojiRepresentation("👍", R.string.verification_emoji_thumbsup, R.drawable.ic_verification_thumbs_up) 36 -> EmojiRepresentation("👍", R.string.verification_emoji_thumbs_up, R.drawable.ic_verification_thumbs_up)
37 -> EmojiRepresentation("☂️", R.string.verification_emoji_umbrella, R.drawable.ic_verification_umbrella) 37 -> EmojiRepresentation("☂️", R.string.verification_emoji_umbrella, R.drawable.ic_verification_umbrella)
38 -> EmojiRepresentation("", R.string.verification_emoji_hourglass, R.drawable.ic_verification_hourglass) 38 -> EmojiRepresentation("", R.string.verification_emoji_hourglass, R.drawable.ic_verification_hourglass)
39 -> EmojiRepresentation("", R.string.verification_emoji_clock, R.drawable.ic_verification_clock) 39 -> EmojiRepresentation("", R.string.verification_emoji_clock, R.drawable.ic_verification_clock)
40 -> EmojiRepresentation("🎁", R.string.verification_emoji_gift, R.drawable.ic_verification_gift) 40 -> EmojiRepresentation("🎁", R.string.verification_emoji_gift, R.drawable.ic_verification_gift)
41 -> EmojiRepresentation("💡", R.string.verification_emoji_lightbulb, R.drawable.ic_verification_light_bulb) 41 -> EmojiRepresentation("💡", R.string.verification_emoji_light_bulb, R.drawable.ic_verification_light_bulb)
42 -> EmojiRepresentation("📕", R.string.verification_emoji_book, R.drawable.ic_verification_book) 42 -> EmojiRepresentation("📕", R.string.verification_emoji_book, R.drawable.ic_verification_book)
43 -> EmojiRepresentation("✏️", R.string.verification_emoji_pencil, R.drawable.ic_verification_pencil) 43 -> EmojiRepresentation("✏️", R.string.verification_emoji_pencil, R.drawable.ic_verification_pencil)
44 -> EmojiRepresentation("📎", R.string.verification_emoji_paperclip, R.drawable.ic_verification_paperclip) 44 -> EmojiRepresentation("📎", R.string.verification_emoji_paperclip, R.drawable.ic_verification_paperclip)
@ -74,7 +74,7 @@ internal fun getEmojiForCode(code: Int): EmojiRepresentation {
50 -> EmojiRepresentation("🏁", R.string.verification_emoji_flag, R.drawable.ic_verification_flag) 50 -> EmojiRepresentation("🏁", R.string.verification_emoji_flag, R.drawable.ic_verification_flag)
51 -> EmojiRepresentation("🚂", R.string.verification_emoji_train, R.drawable.ic_verification_train) 51 -> EmojiRepresentation("🚂", R.string.verification_emoji_train, R.drawable.ic_verification_train)
52 -> EmojiRepresentation("🚲", R.string.verification_emoji_bicycle, R.drawable.ic_verification_bicycle) 52 -> EmojiRepresentation("🚲", R.string.verification_emoji_bicycle, R.drawable.ic_verification_bicycle)
53 -> EmojiRepresentation("✈️", R.string.verification_emoji_airplane, R.drawable.ic_verification_airplane) 53 -> EmojiRepresentation("✈️", R.string.verification_emoji_aeroplane, R.drawable.ic_verification_aeroplane)
54 -> EmojiRepresentation("🚀", R.string.verification_emoji_rocket, R.drawable.ic_verification_rocket) 54 -> EmojiRepresentation("🚀", R.string.verification_emoji_rocket, R.drawable.ic_verification_rocket)
55 -> EmojiRepresentation("🏆", R.string.verification_emoji_trophy, R.drawable.ic_verification_trophy) 55 -> EmojiRepresentation("🏆", R.string.verification_emoji_trophy, R.drawable.ic_verification_trophy)
56 -> EmojiRepresentation("", R.string.verification_emoji_ball, R.drawable.ic_verification_ball) 56 -> EmojiRepresentation("", R.string.verification_emoji_ball, R.drawable.ic_verification_ball)
@ -82,7 +82,7 @@ internal fun getEmojiForCode(code: Int): EmojiRepresentation {
58 -> EmojiRepresentation("🎺", R.string.verification_emoji_trumpet, R.drawable.ic_verification_trumpet) 58 -> EmojiRepresentation("🎺", R.string.verification_emoji_trumpet, R.drawable.ic_verification_trumpet)
59 -> EmojiRepresentation("🔔", R.string.verification_emoji_bell, R.drawable.ic_verification_bell) 59 -> EmojiRepresentation("🔔", R.string.verification_emoji_bell, R.drawable.ic_verification_bell)
60 -> EmojiRepresentation("", R.string.verification_emoji_anchor, R.drawable.ic_verification_anchor) 60 -> EmojiRepresentation("", R.string.verification_emoji_anchor, R.drawable.ic_verification_anchor)
61 -> EmojiRepresentation("🎧", R.string.verification_emoji_headphone, R.drawable.ic_verification_headphone) 61 -> EmojiRepresentation("🎧", R.string.verification_emoji_headphones, R.drawable.ic_verification_headphones)
62 -> EmojiRepresentation("📁", R.string.verification_emoji_folder, R.drawable.ic_verification_folder) 62 -> EmojiRepresentation("📁", R.string.verification_emoji_folder, R.drawable.ic_verification_folder)
/* 63 */ else -> EmojiRepresentation("📌", R.string.verification_emoji_pin, R.drawable.ic_verification_pin) /* 63 */ else -> EmojiRepresentation("📌", R.string.verification_emoji_pin, R.drawable.ic_verification_pin)
} }

View File

@ -22,6 +22,9 @@ import androidx.work.Data
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import androidx.work.Operation import androidx.work.Operation
import androidx.work.WorkInfo import androidx.work.WorkInfo
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.R import org.matrix.android.sdk.R
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest import org.matrix.android.sdk.api.session.crypto.verification.ValidVerificationInfoRequest
@ -51,10 +54,8 @@ import org.matrix.android.sdk.internal.di.WorkManagerProvider
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.util.StringProvider import org.matrix.android.sdk.internal.util.StringProvider
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.util.UUID import java.util.UUID
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -87,7 +88,7 @@ internal class VerificationTransportRoomMessage(
val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params( val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params(
sessionId = sessionId, sessionId = sessionId,
event = event eventId = event.eventId ?: ""
)) ))
val enqueueInfo = enqueueSendWork(workerParams) val enqueueInfo = enqueueSendWork(workerParams)
@ -115,20 +116,30 @@ internal class VerificationTransportRoomMessage(
val observer = object : Observer<List<WorkInfo>> { val observer = object : Observer<List<WorkInfo>> {
override fun onChanged(workInfoList: List<WorkInfo>?) { override fun onChanged(workInfoList: List<WorkInfo>?) {
workInfoList workInfoList
?.filter { it.state == WorkInfo.State.SUCCEEDED }
?.firstOrNull { it.id == enqueueInfo.second } ?.firstOrNull { it.id == enqueueInfo.second }
?.let { wInfo -> ?.let { wInfo ->
if (SendVerificationMessageWorker.hasFailed(wInfo.outputData)) { when (wInfo.state) {
Timber.e("## SAS verification [${tx?.transactionId}] failed to send verification message in state : ${tx?.state}") WorkInfo.State.FAILED -> {
tx?.cancel(onErrorReason) tx?.cancel(onErrorReason)
} else { workLiveData.removeObserver(this)
if (onDone != null) { }
onDone() WorkInfo.State.SUCCEEDED -> {
} else { if (SessionSafeCoroutineWorker.hasFailed(wInfo.outputData)) {
tx?.state = nextState Timber.e("## SAS verification [${tx?.transactionId}] failed to send verification message in state : ${tx?.state}")
tx?.cancel(onErrorReason)
} else {
if (onDone != null) {
onDone()
} else {
tx?.state = nextState
}
}
workLiveData.removeObserver(this)
}
else -> {
// nop
} }
} }
workLiveData.removeObserver(this)
} }
} }
} }
@ -174,7 +185,7 @@ internal class VerificationTransportRoomMessage(
val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params( val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params(
sessionId = sessionId, sessionId = sessionId,
event = event eventId = event.eventId ?: ""
)) ))
val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SendVerificationMessageWorker>() val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SendVerificationMessageWorker>()
@ -184,7 +195,7 @@ internal class VerificationTransportRoomMessage(
.build() .build()
workManagerProvider.workManager workManagerProvider.workManager
.beginUniqueWork("${roomId}_VerificationWork", ExistingWorkPolicy.APPEND, workRequest) .beginUniqueWork("${roomId}_VerificationWork", ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
.enqueue() .enqueue()
// I cannot just listen to the given work request, because when used in a uniqueWork, // I cannot just listen to the given work request, because when used in a uniqueWork,
@ -199,7 +210,7 @@ internal class VerificationTransportRoomMessage(
?.filter { it.state == WorkInfo.State.SUCCEEDED } ?.filter { it.state == WorkInfo.State.SUCCEEDED }
?.firstOrNull { it.id == workRequest.id } ?.firstOrNull { it.id == workRequest.id }
?.let { wInfo -> ?.let { wInfo ->
if (SendVerificationMessageWorker.hasFailed(wInfo.outputData)) { if (SessionSafeCoroutineWorker.hasFailed(wInfo.outputData)) {
callback(null, null) callback(null, null)
} else { } else {
val eventId = wInfo.outputData.getString(localId) val eventId = wInfo.outputData.getString(localId)
@ -229,7 +240,7 @@ internal class VerificationTransportRoomMessage(
) )
val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params( val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params(
sessionId = sessionId, sessionId = sessionId,
event = event eventId = event.eventId ?: ""
)) ))
enqueueSendWork(workerParams) enqueueSendWork(workerParams)
} }
@ -249,7 +260,7 @@ internal class VerificationTransportRoomMessage(
) )
val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params( val workerParams = WorkerParamsFactory.toData(SendVerificationMessageWorker.Params(
sessionId = sessionId, sessionId = sessionId,
event = event eventId = event.eventId ?: ""
)) ))
val enqueueInfo = enqueueSendWork(workerParams) val enqueueInfo = enqueueSendWork(workerParams)
@ -280,7 +291,7 @@ internal class VerificationTransportRoomMessage(
.setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS) .setBackoffCriteria(BackoffPolicy.LINEAR, 2_000L, TimeUnit.MILLISECONDS)
.build() .build()
return workManagerProvider.workManager return workManagerProvider.workManager
.beginUniqueWork(uniqueQueueName(), ExistingWorkPolicy.APPEND, workRequest) .beginUniqueWork(uniqueQueueName(), ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
.enqueue() to workRequest.id .enqueue() to workRequest.id
} }

View File

@ -16,31 +16,52 @@
*/ */
package org.matrix.android.sdk.internal.database package org.matrix.android.sdk.internal.database
import com.zhuinden.monarchy.Monarchy
import io.realm.Realm import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
suspend fun <T> awaitTransaction(config: RealmConfiguration, transaction: suspend (realm: Realm) -> T) = withContext(Dispatchers.Default) { internal fun <T> CoroutineScope.asyncTransaction(monarchy: Monarchy, transaction: suspend (realm: Realm) -> T) {
Realm.getInstance(config).use { bgRealm -> asyncTransaction(monarchy.realmConfiguration, transaction)
bgRealm.beginTransaction() }
val result: T
try { internal fun <T> CoroutineScope.asyncTransaction(realmConfiguration: RealmConfiguration, transaction: suspend (realm: Realm) -> T) {
val start = System.currentTimeMillis() launch {
result = transaction(bgRealm) awaitTransaction(realmConfiguration, transaction)
if (isActive) { }
bgRealm.commitTransaction() }
val end = System.currentTimeMillis()
val time = end - start private val realmSemaphore = Semaphore(1)
Timber.v("Execute transaction in $time millis")
} suspend fun <T> awaitTransaction(config: RealmConfiguration, transaction: suspend (realm: Realm) -> T): T {
} finally { return realmSemaphore.withPermit {
if (bgRealm.isInTransaction) { withContext(Dispatchers.IO) {
bgRealm.cancelTransaction() Realm.getInstance(config).use { bgRealm ->
} bgRealm.beginTransaction()
} val result: T
result try {
val start = System.currentTimeMillis()
result = transaction(bgRealm)
if (isActive) {
bgRealm.commitTransaction()
val end = System.currentTimeMillis()
val time = end - start
Timber.v("Execute transaction in $time millis")
}
} finally {
if (bgRealm.isInTransaction) {
bgRealm.cancelTransaction()
}
}
result
}
}
} }
} }

View File

@ -0,0 +1,35 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.database
import io.realm.Realm
import java.io.Closeable
internal class RealmInstanceWrapper(private val realm: Realm, private val closeRealmOnClose: Boolean) : Closeable {
override fun close() {
if (closeRealmOnClose) {
realm.close()
}
}
fun <R> withRealm(block: (Realm) -> R): R {
return use {
block(it.realm)
}
}
}

View File

@ -0,0 +1,72 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.database
import android.os.Looper
import androidx.annotation.MainThread
import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.SessionLifecycleObserver
import org.matrix.android.sdk.internal.session.SessionScope
import javax.inject.Inject
import kotlin.concurrent.getOrSet
/**
* This class keeps an instance of realm open in the main thread so you can grab it whenever you want to get a realm
* instance. This does check each time if you are on the main thread or not and returns the appropriate realm instance.
*/
@SessionScope
internal class RealmSessionProvider @Inject constructor(@SessionDatabase private val monarchy: Monarchy)
: SessionLifecycleObserver {
private val realmThreadLocal = ThreadLocal<Realm>()
/**
* Allow you to execute a block with an opened realm. It automatically closes it if necessary (ie. when not in main thread)
*/
fun <R> withRealm(block: (Realm) -> R): R {
return getRealmWrapper().withRealm(block)
}
@MainThread
override fun onStart() {
realmThreadLocal.getOrSet {
Realm.getInstance(monarchy.realmConfiguration)
}
}
@MainThread
override fun onStop() {
realmThreadLocal.get()?.close()
realmThreadLocal.remove()
}
private fun getRealmWrapper(): RealmInstanceWrapper {
val isOnMainThread = isOnMainThread()
val realm = if (isOnMainThread) {
realmThreadLocal.getOrSet {
Realm.getInstance(monarchy.realmConfiguration)
}
} else {
Realm.getInstance(monarchy.realmConfiguration)
}
return RealmInstanceWrapper(realm, closeRealmOnClose = !isOnMainThread)
}
private fun isOnMainThread() = Looper.myLooper() == Looper.getMainLooper()
}

View File

@ -28,7 +28,7 @@ import javax.inject.Inject
class RealmSessionStoreMigration @Inject constructor() : RealmMigration { class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
companion object { companion object {
const val SESSION_STORE_SCHEMA_VERSION = 4L const val SESSION_STORE_SCHEMA_VERSION = 5L
} }
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
@ -38,6 +38,7 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
if (oldVersion <= 1) migrateTo2(realm) if (oldVersion <= 1) migrateTo2(realm)
if (oldVersion <= 2) migrateTo3(realm) if (oldVersion <= 2) migrateTo3(realm)
if (oldVersion <= 3) migrateTo4(realm) if (oldVersion <= 3) migrateTo4(realm)
if (oldVersion <= 4) migrateTo5(realm)
} }
private fun migrateTo1(realm: DynamicRealm) { private fun migrateTo1(realm: DynamicRealm) {
@ -54,16 +55,16 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
private fun migrateTo2(realm: DynamicRealm) { private fun migrateTo2(realm: DynamicRealm) {
Timber.d("Step 1 -> 2") Timber.d("Step 1 -> 2")
realm.schema.get("HomeServerCapabilitiesEntity") realm.schema.get("HomeServerCapabilitiesEntity")
?.addField(HomeServerCapabilitiesEntityFields.ADMIN_E2_E_BY_DEFAULT, Boolean::class.java) ?.addField("adminE2EByDefault", Boolean::class.java)
?.transform { obj -> ?.transform { obj ->
obj.setBoolean(HomeServerCapabilitiesEntityFields.ADMIN_E2_E_BY_DEFAULT, true) obj.setBoolean("adminE2EByDefault", true)
} }
} }
private fun migrateTo3(realm: DynamicRealm) { private fun migrateTo3(realm: DynamicRealm) {
Timber.d("Step 2 -> 3") Timber.d("Step 2 -> 3")
realm.schema.get("HomeServerCapabilitiesEntity") realm.schema.get("HomeServerCapabilitiesEntity")
?.addField(HomeServerCapabilitiesEntityFields.PREFERRED_JITSI_DOMAIN, String::class.java) ?.addField("preferredJitsiDomain", String::class.java)
?.transform { obj -> ?.transform { obj ->
// Schedule a refresh of the capabilities // Schedule a refresh of the capabilities
obj.setLong(HomeServerCapabilitiesEntityFields.LAST_UPDATED_TIMESTAMP, 0) obj.setLong(HomeServerCapabilitiesEntityFields.LAST_UPDATED_TIMESTAMP, 0)
@ -82,4 +83,11 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
.setRequired(PendingThreePidEntityFields.SID, true) .setRequired(PendingThreePidEntityFields.SID, true)
.addField(PendingThreePidEntityFields.SUBMIT_URL, String::class.java) .addField(PendingThreePidEntityFields.SUBMIT_URL, String::class.java)
} }
private fun migrateTo5(realm: DynamicRealm) {
Timber.d("Step 4 -> 5")
realm.schema.get("HomeServerCapabilitiesEntity")
?.removeField("adminE2EByDefault")
?.removeField("preferredJitsiDomain")
}
} }

View File

@ -20,21 +20,34 @@ package org.matrix.android.sdk.internal.database.mapper
import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE import org.matrix.android.sdk.api.util.JSON_DICT_PARAMETERIZED_TYPE
import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.MoshiProvider
import org.matrix.android.sdk.internal.network.parsing.CheckNumberType
internal object ContentMapper { internal object ContentMapper {
private val moshi = MoshiProvider.providesMoshi() private val moshi = MoshiProvider.providesMoshi()
private val adapter = moshi.adapter<Content>(JSON_DICT_PARAMETERIZED_TYPE) private val castJsonNumberMoshi by lazy {
// We are adding the CheckNumberType as we are serializing/deserializing multiple time in a row
// and we lost typing information doing so.
// We don't want this check to be done on all adapters, so we create a new moshi just for that.
MoshiProvider.providesMoshi()
.newBuilder()
.add(CheckNumberType.JSON_ADAPTER_FACTORY)
.build()
}
fun map(content: String?): Content? { fun map(content: String?, castJsonNumbers: Boolean = false): Content? {
return content?.let { return content?.let {
adapter.fromJson(it) if (castJsonNumbers) {
castJsonNumberMoshi
} else {
moshi
}.adapter<Content>(JSON_DICT_PARAMETERIZED_TYPE).fromJson(it)
} }
} }
fun map(content: Content?): String? { fun map(content: Content?): String? {
return content?.let { return content?.let {
adapter.toJson(it) moshi.adapter<Content>(JSON_DICT_PARAMETERIZED_TYPE).toJson(it)
} }
} }
} }

View File

@ -54,7 +54,7 @@ internal object EventMapper {
return eventEntity return eventEntity
} }
fun map(eventEntity: EventEntity): Event { fun map(eventEntity: EventEntity, castJsonNumbers: Boolean = false): Event {
val ud = eventEntity.unsignedData val ud = eventEntity.unsignedData
?.takeIf { it.isNotBlank() } ?.takeIf { it.isNotBlank() }
?.let { ?.let {
@ -69,8 +69,8 @@ internal object EventMapper {
return Event( return Event(
type = eventEntity.type, type = eventEntity.type,
eventId = eventEntity.eventId, eventId = eventEntity.eventId,
content = ContentMapper.map(eventEntity.content), content = ContentMapper.map(eventEntity.content, castJsonNumbers),
prevContent = ContentMapper.map(eventEntity.prevContent), prevContent = ContentMapper.map(eventEntity.prevContent, castJsonNumbers),
originServerTs = eventEntity.originServerTs, originServerTs = eventEntity.originServerTs,
senderId = eventEntity.sender, senderId = eventEntity.sender,
stateKey = eventEntity.stateKey, stateKey = eventEntity.stateKey,
@ -96,8 +96,8 @@ internal object EventMapper {
} }
} }
internal fun EventEntity.asDomain(): Event { internal fun EventEntity.asDomain(castJsonNumbers: Boolean = false): Event {
return EventMapper.map(this) return EventMapper.map(this, castJsonNumbers)
} }
internal fun Event.toEntity(roomId: String, sendState: SendState, ageLocalTs: Long?): EventEntity { internal fun Event.toEntity(roomId: String, sendState: SendState, ageLocalTs: Long?): EventEntity {

View File

@ -30,9 +30,7 @@ internal object HomeServerCapabilitiesMapper {
canChangePassword = entity.canChangePassword, canChangePassword = entity.canChangePassword,
maxUploadFileSize = entity.maxUploadFileSize, maxUploadFileSize = entity.maxUploadFileSize,
lastVersionIdentityServerSupported = entity.lastVersionIdentityServerSupported, lastVersionIdentityServerSupported = entity.lastVersionIdentityServerSupported,
defaultIdentityServerUrl = entity.defaultIdentityServerUrl, defaultIdentityServerUrl = entity.defaultIdentityServerUrl
adminE2EByDefault = entity.adminE2EByDefault,
preferredJitsiDomain = entity.preferredJitsiDomain
) )
} }
} }

View File

@ -18,26 +18,24 @@
package org.matrix.android.sdk.internal.database.mapper package org.matrix.android.sdk.internal.database.mapper
import org.matrix.android.sdk.api.session.room.model.ReadReceipt import org.matrix.android.sdk.api.session.room.model.ReadReceipt
import org.matrix.android.sdk.internal.database.RealmSessionProvider
import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity
import org.matrix.android.sdk.internal.database.model.UserEntity import org.matrix.android.sdk.internal.database.model.UserEntity
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
import io.realm.Realm
import io.realm.RealmConfiguration
import javax.inject.Inject import javax.inject.Inject
internal class ReadReceiptsSummaryMapper @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration) { internal class ReadReceiptsSummaryMapper @Inject constructor(private val realmSessionProvider: RealmSessionProvider) {
fun map(readReceiptsSummaryEntity: ReadReceiptsSummaryEntity?): List<ReadReceipt> { fun map(readReceiptsSummaryEntity: ReadReceiptsSummaryEntity?): List<ReadReceipt> {
if (readReceiptsSummaryEntity == null) { if (readReceiptsSummaryEntity == null) {
return emptyList() return emptyList()
} }
return Realm.getInstance(realmConfiguration).use { realm -> return realmSessionProvider.withRealm { realm ->
val readReceipts = readReceiptsSummaryEntity.readReceipts val readReceipts = readReceiptsSummaryEntity.readReceipts
readReceipts readReceipts
.mapNotNull { .mapNotNull {
val user = UserEntity.where(realm, it.userId).findFirst() val user = UserEntity.where(realm, it.userId).findFirst()
?: return@mapNotNull null ?: return@mapNotNull null
ReadReceipt(user.asDomain(), it.originServerTs.toLong()) ReadReceipt(user.asDomain(), it.originServerTs.toLong())
} }
} }

View File

@ -17,17 +17,15 @@
package org.matrix.android.sdk.internal.database.model package org.matrix.android.sdk.internal.database.model
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
import io.realm.RealmObject import io.realm.RealmObject
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
internal open class HomeServerCapabilitiesEntity( internal open class HomeServerCapabilitiesEntity(
var canChangePassword: Boolean = true, var canChangePassword: Boolean = true,
var maxUploadFileSize: Long = HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN, var maxUploadFileSize: Long = HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN,
var lastVersionIdentityServerSupported: Boolean = false, var lastVersionIdentityServerSupported: Boolean = false,
var defaultIdentityServerUrl: String? = null, var defaultIdentityServerUrl: String? = null,
var adminE2EByDefault: Boolean = true, var lastUpdatedTimestamp: Long = 0L
var lastUpdatedTimestamp: Long = 0L,
var preferredJitsiDomain: String? = null
) : RealmObject() { ) : RealmObject() {
companion object companion object

View File

@ -0,0 +1,31 @@
/*
* Copyright 2019 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.database.model
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
internal open class RawCacheEntity(
@PrimaryKey
var url: String = "",
var data: String = "",
var lastUpdatedTimestamp: Long = 0L
) : RealmObject() {
companion object
}

View File

@ -0,0 +1,40 @@
/*
* Copyright 2019 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.database.query
import io.realm.Realm
import io.realm.kotlin.createObject
import io.realm.kotlin.where
import org.matrix.android.sdk.internal.database.model.RawCacheEntity
import org.matrix.android.sdk.internal.database.model.RawCacheEntityFields
/**
* Get the current RawCacheEntity, return null if it does not exist
*/
internal fun RawCacheEntity.Companion.get(realm: Realm, url: String): RawCacheEntity? {
return realm.where<RawCacheEntity>()
.equalTo(RawCacheEntityFields.URL, url)
.findFirst()
}
/**
* Get the current RawCacheEntity, create one if it does not exist
*/
internal fun RawCacheEntity.Companion.getOrCreate(realm: Realm, url: String): RawCacheEntity {
return get(realm, url) ?: realm.createObject(url)
}

View File

@ -17,17 +17,18 @@
package org.matrix.android.sdk.internal.database.query package org.matrix.android.sdk.internal.database.query
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.database.model.ChunkEntity
import org.matrix.android.sdk.internal.database.model.RoomEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
import io.realm.Realm import io.realm.Realm
import io.realm.RealmList import io.realm.RealmList
import io.realm.RealmQuery import io.realm.RealmQuery
import io.realm.RealmResults import io.realm.RealmResults
import io.realm.Sort import io.realm.Sort
import io.realm.kotlin.where import io.realm.kotlin.where
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.TimelineEventFilters
import org.matrix.android.sdk.internal.database.model.ChunkEntity
import org.matrix.android.sdk.internal.database.model.RoomEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
internal fun TimelineEventEntity.Companion.where(realm: Realm, roomId: String, eventId: String): RealmQuery<TimelineEventEntity> { internal fun TimelineEventEntity.Companion.where(realm: Realm, roomId: String, eventId: String): RealmQuery<TimelineEventEntity> {
return realm.where<TimelineEventEntity>() return realm.where<TimelineEventEntity>()
@ -56,16 +57,10 @@ internal fun TimelineEventEntity.Companion.findWithSenderMembershipEvent(realm:
internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm, internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm,
roomId: String, roomId: String,
includesSending: Boolean, includesSending: Boolean,
filterContentRelation: Boolean = false, filters: TimelineEventFilters = TimelineEventFilters()): TimelineEventEntity? {
filterTypes: List<String> = emptyList()): TimelineEventEntity? {
val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: return null val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: return null
val sendingTimelineEvents = roomEntity.sendingTimelineEvents.where().filterTypes(filterTypes) val sendingTimelineEvents = roomEntity.sendingTimelineEvents.where().filterEvents(filters)
val liveEvents = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)?.timelineEvents?.where()?.filterTypes(filterTypes) val liveEvents = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)?.timelineEvents?.where()?.filterEvents(filters)
if (filterContentRelation) {
liveEvents
?.not()?.like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT)
?.not()?.like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.RESPONSE)
}
val query = if (includesSending && sendingTimelineEvents.findAll().isNotEmpty()) { val query = if (includesSending && sendingTimelineEvents.findAll().isNotEmpty()) {
sendingTimelineEvents sendingTimelineEvents
} else { } else {
@ -76,6 +71,24 @@ internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm,
?.findFirst() ?.findFirst()
} }
internal fun RealmQuery<TimelineEventEntity>.filterEvents(filters: TimelineEventFilters): RealmQuery<TimelineEventEntity> {
if (filters.filterTypes) {
`in`(TimelineEventEntityFields.ROOT.TYPE, filters.allowedTypes.toTypedArray())
}
if (filters.filterUseless) {
not()
.equalTo(TimelineEventEntityFields.ROOT.IS_USELESS, true)
}
if (filters.filterEdits) {
not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT)
not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.RESPONSE)
}
if (filters.filterRedacted) {
not().like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, TimelineEventFilter.Unsigned.REDACTED)
}
return this
}
internal fun RealmQuery<TimelineEventEntity>.filterTypes(filterTypes: List<String>): RealmQuery<TimelineEventEntity> { internal fun RealmQuery<TimelineEventEntity>.filterTypes(filterTypes: List<String>): RealmQuery<TimelineEventEntity> {
return if (filterTypes.isEmpty()) { return if (filterTypes.isEmpty()) {
this this

View File

@ -23,6 +23,10 @@ import javax.inject.Qualifier
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
internal annotation class AuthDatabase internal annotation class AuthDatabase
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
internal annotation class GlobalDatabase
@Qualifier @Qualifier
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
internal annotation class SessionDatabase internal annotation class SessionDatabase

View File

@ -22,22 +22,30 @@ import android.content.res.Resources
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import dagger.BindsInstance import dagger.BindsInstance
import dagger.Component import dagger.Component
import okhttp3.OkHttpClient
import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.MatrixConfiguration import org.matrix.android.sdk.api.MatrixConfiguration
import org.matrix.android.sdk.api.auth.AuthenticationService import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.internal.SessionManager import org.matrix.android.sdk.internal.SessionManager
import org.matrix.android.sdk.internal.auth.AuthModule import org.matrix.android.sdk.internal.auth.AuthModule
import org.matrix.android.sdk.internal.auth.SessionParamsStore import org.matrix.android.sdk.internal.auth.SessionParamsStore
import org.matrix.android.sdk.internal.raw.RawModule
import org.matrix.android.sdk.internal.session.MockHttpInterceptor import org.matrix.android.sdk.internal.session.MockHttpInterceptor
import org.matrix.android.sdk.internal.session.TestInterceptor import org.matrix.android.sdk.internal.session.TestInterceptor
import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import okhttp3.OkHttpClient
import org.matrix.olm.OlmManager import org.matrix.olm.OlmManager
import java.io.File import java.io.File
@Component(modules = [MatrixModule::class, NetworkModule::class, AuthModule::class, NoOpTestModule::class]) @Component(modules = [
MatrixModule::class,
NetworkModule::class,
AuthModule::class,
RawModule::class,
NoOpTestModule::class
])
@MatrixScope @MatrixScope
internal interface MatrixComponent { internal interface MatrixComponent {
@ -53,6 +61,8 @@ internal interface MatrixComponent {
fun authenticationService(): AuthenticationService fun authenticationService(): AuthenticationService
fun rawService(): RawService
fun context(): Context fun context(): Context
fun matrixConfiguration(): MatrixConfiguration fun matrixConfiguration(): MatrixConfiguration

View File

@ -24,7 +24,7 @@ import okio.BufferedSink
import okio.ForwardingSink import okio.ForwardingSink
import okio.Sink import okio.Sink
import okio.buffer import okio.buffer
import org.matrix.android.sdk.api.extensions.tryThis import org.matrix.android.sdk.api.extensions.tryOrNull
import java.io.IOException import java.io.IOException
internal class ProgressRequestBody(private val delegate: RequestBody, internal class ProgressRequestBody(private val delegate: RequestBody,
@ -40,7 +40,7 @@ internal class ProgressRequestBody(private val delegate: RequestBody,
override fun isDuplex() = delegate.isDuplex() override fun isDuplex() = delegate.isDuplex()
val length = tryThis { delegate.contentLength() } ?: -1 val length = tryOrNull { delegate.contentLength() } ?: -1
override fun contentLength() = length override fun contentLength() = length

View File

@ -49,7 +49,7 @@ interface CheckNumberType {
val numberAsString = reader.nextString() val numberAsString = reader.nextString()
val decimal = BigDecimal(numberAsString) val decimal = BigDecimal(numberAsString)
if (decimal.scale() <= 0) { if (decimal.scale() <= 0) {
decimal.intValueExact() decimal.longValueExact()
} else { } else {
decimal.toDouble() decimal.toDouble()
} }

View File

@ -0,0 +1,41 @@
/*
* Copyright 2019 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.raw
import com.zhuinden.monarchy.Monarchy
import io.realm.kotlin.where
import org.matrix.android.sdk.internal.database.model.RawCacheEntity
import org.matrix.android.sdk.internal.di.GlobalDatabase
import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction
import javax.inject.Inject
internal interface CleanRawCacheTask : Task<Unit, Unit>
internal class DefaultCleanRawCacheTask @Inject constructor(
@GlobalDatabase private val monarchy: Monarchy
) : CleanRawCacheTask {
override suspend fun execute(params: Unit) {
monarchy.awaitTransaction { realm ->
realm.where<RawCacheEntity>()
.findAll()
.deleteAllFromRealm()
}
}
}

View File

@ -0,0 +1,101 @@
/*
* Copyright 2019 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.raw
import com.zhuinden.monarchy.Monarchy
import okhttp3.ResponseBody
import org.matrix.android.sdk.api.raw.RawCacheStrategy
import org.matrix.android.sdk.internal.database.model.RawCacheEntity
import org.matrix.android.sdk.internal.database.query.get
import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.di.GlobalDatabase
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction
import java.util.Date
import javax.inject.Inject
internal interface GetUrlTask : Task<GetUrlTask.Params, String> {
data class Params(
val url: String,
val rawCacheStrategy: RawCacheStrategy
)
}
internal class DefaultGetUrlTask @Inject constructor(
private val rawAPI: RawAPI,
@GlobalDatabase private val monarchy: Monarchy
) : GetUrlTask {
override suspend fun execute(params: GetUrlTask.Params): String {
return when (params.rawCacheStrategy) {
RawCacheStrategy.NoCache -> doRequest(params.url)
is RawCacheStrategy.TtlCache -> doRequestWithCache(
params.url,
params.rawCacheStrategy.validityDurationInMillis,
params.rawCacheStrategy.strict
)
RawCacheStrategy.InfiniteCache -> doRequestWithCache(
params.url,
Long.MAX_VALUE,
true
)
}
}
private suspend fun doRequest(url: String): String {
return executeRequest<ResponseBody>(null) {
apiCall = rawAPI.getUrl(url)
}
.string()
}
private suspend fun doRequestWithCache(url: String, validityDurationInMillis: Long, strict: Boolean): String {
// Get data from cache
var dataFromCache: String? = null
var isCacheValid = false
monarchy.doWithRealm { realm ->
val entity = RawCacheEntity.get(realm, url)
dataFromCache = entity?.data
isCacheValid = entity != null && Date().time < entity.lastUpdatedTimestamp + validityDurationInMillis
}
if (dataFromCache != null && isCacheValid) {
return dataFromCache as String
}
// No cache or outdated cache
val data = try {
doRequest(url)
} catch (throwable: Throwable) {
// In case of error, we can return value from cache even if outdated
return dataFromCache
?.takeIf { !strict }
?: throw throwable
}
// Store cache
monarchy.awaitTransaction { realm ->
val rawCacheEntity = RawCacheEntity.getOrCreate(realm, url)
rawCacheEntity.data = data
rawCacheEntity.lastUpdatedTimestamp = Date().time
}
return data
}
}

View File

@ -0,0 +1,60 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.raw
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.raw.RawCacheStrategy
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.configureWith
import java.util.concurrent.TimeUnit
import javax.inject.Inject
internal class DefaultRawService @Inject constructor(
private val taskExecutor: TaskExecutor,
private val getUrlTask: GetUrlTask,
private val cleanRawCacheTask: CleanRawCacheTask
) : RawService {
override fun getUrl(url: String,
rawCacheStrategy: RawCacheStrategy,
matrixCallback: MatrixCallback<String>): Cancelable {
return getUrlTask
.configureWith(GetUrlTask.Params(url, rawCacheStrategy)) {
callback = matrixCallback
}
.executeBy(taskExecutor)
}
override fun getWellknown(userId: String,
matrixCallback: MatrixCallback<String>): Cancelable {
val homeServerDomain = userId.substringAfter(":")
return getUrl(
"https://$homeServerDomain/.well-known/matrix/client",
RawCacheStrategy.TtlCache(TimeUnit.HOURS.toMillis(8), false),
matrixCallback
)
}
override fun clearCache(matrixCallback: MatrixCallback<Unit>): Cancelable {
return cleanRawCacheTask
.configureWith(Unit) {
callback = matrixCallback
}
.executeBy(taskExecutor)
}
}

View File

@ -1,5 +1,6 @@
/* /*
* Copyright 2019 New Vector Ltd * Copyright 2019 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,16 +15,16 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.app.features.home package org.matrix.android.sdk.internal.raw
import androidx.annotation.ColorRes import io.realm.annotations.RealmModule
import im.vector.app.R import org.matrix.android.sdk.internal.database.model.RawCacheEntity
@ColorRes /**
fun getColorFromRoomId(roomId: String?): Int { * Realm module for global classes
return when ((roomId?.toList()?.sumBy { it.toInt() } ?: 0) % 3) { */
1 -> R.color.riotx_avatar_fill_2 @RealmModule(library = true,
2 -> R.color.riotx_avatar_fill_3 classes = [
else -> R.color.riotx_avatar_fill_1 RawCacheEntity::class
} ])
} internal class GlobalRealmModule

View File

@ -0,0 +1,29 @@
/*
* Copyright 2020 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.matrix.android.sdk.internal.raw
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Url
internal interface RawAPI {
@GET
fun getUrl(@Url url: String): Call<ResponseBody>
}

View File

@ -0,0 +1,81 @@
/*
* Copyright 2020 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.matrix.android.sdk.internal.raw
import com.zhuinden.monarchy.Monarchy
import dagger.Binds
import dagger.Lazy
import dagger.Module
import dagger.Provides
import io.realm.RealmConfiguration
import okhttp3.OkHttpClient
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.internal.database.RealmKeysUtils
import org.matrix.android.sdk.internal.di.GlobalDatabase
import org.matrix.android.sdk.internal.di.MatrixScope
import org.matrix.android.sdk.internal.di.Unauthenticated
import org.matrix.android.sdk.internal.network.RetrofitFactory
@Module
internal abstract class RawModule {
@Module
companion object {
private const val DB_ALIAS = "matrix-sdk-global"
@JvmStatic
@Provides
@GlobalDatabase
fun providesMonarchy(@GlobalDatabase realmConfiguration: RealmConfiguration): Monarchy {
return Monarchy.Builder()
.setRealmConfiguration(realmConfiguration)
.build()
}
@JvmStatic
@Provides
@GlobalDatabase
@MatrixScope
fun providesRealmConfiguration(realmKeysUtils: RealmKeysUtils): RealmConfiguration {
return RealmConfiguration.Builder()
.apply {
realmKeysUtils.configureEncryption(this, DB_ALIAS)
}
.name("matrix-sdk-global.realm")
.modules(GlobalRealmModule())
.build()
}
@Provides
@JvmStatic
fun providesRawAPI(@Unauthenticated okHttpClient: Lazy<OkHttpClient>,
retrofitFactory: RetrofitFactory): RawAPI {
return retrofitFactory.create(okHttpClient, "https://example.org").create(RawAPI::class.java)
}
}
@Binds
abstract fun bindRawService(service: DefaultRawService): RawService
@Binds
abstract fun bindGetUrlTask(task: DefaultGetUrlTask): GetUrlTask
@Binds
abstract fun bindCleanRawCacheTask(task: DefaultCleanRawCacheTask): CleanRawCacheTask
}

View File

@ -23,7 +23,7 @@ import android.webkit.MimeTypeMap
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import arrow.core.Try import arrow.core.Try
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.extensions.tryThis import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.api.session.content.ContentUrlResolver
import org.matrix.android.sdk.api.session.file.FileService import org.matrix.android.sdk.api.session.file.FileService
import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Cancelable
@ -144,11 +144,13 @@ internal class DefaultFileService @Inject constructor(
if (elementToDecrypt != null) { if (elementToDecrypt != null) {
Timber.v("## FileService: decrypt file") Timber.v("## FileService: decrypt file")
val decryptSuccess = MXEncryptedAttachments.decryptAttachment( val decryptSuccess = destFile.outputStream().buffered().use {
source.inputStream(), MXEncryptedAttachments.decryptAttachment(
elementToDecrypt, source.inputStream(),
destFile.outputStream().buffered() elementToDecrypt,
) it
)
}
response.close() response.close()
if (!decryptSuccess) { if (!decryptSuccess) {
return@flatMap Try.Failure(IllegalStateException("Decryption error")) return@flatMap Try.Failure(IllegalStateException("Decryption error"))
@ -172,7 +174,7 @@ internal class DefaultFileService @Inject constructor(
} }
} }
toNotify?.forEach { otherCallbacks -> toNotify?.forEach { otherCallbacks ->
tryThis { otherCallbacks.onFailure(it) } tryOrNull { otherCallbacks.onFailure(it) }
} }
}, { file -> }, { file ->
callback.onSuccess(file) callback.onSuccess(file)
@ -184,7 +186,7 @@ internal class DefaultFileService @Inject constructor(
} }
Timber.v("## FileService additional to notify ${toNotify?.size ?: 0} ") Timber.v("## FileService additional to notify ${toNotify?.size ?: 0} ")
toNotify?.forEach { otherCallbacks -> toNotify?.forEach { otherCallbacks ->
tryThis { otherCallbacks.onSuccess(file) } tryOrNull { otherCallbacks.onSuccess(file) }
} }
}) })
}.toCancelable() }.toCancelable()

View File

@ -166,8 +166,8 @@ internal class DefaultSession @Inject constructor(
SyncWorker.requireBackgroundSync(workManagerProvider, sessionId) SyncWorker.requireBackgroundSync(workManagerProvider, sessionId)
} }
override fun startAutomaticBackgroundSync(repeatDelay: Long) { override fun startAutomaticBackgroundSync(timeOutInSeconds: Long, repeatDelayInSeconds: Long) {
SyncWorker.automaticallyBackgroundSync(workManagerProvider, sessionId, 0, repeatDelay) SyncWorker.automaticallyBackgroundSync(workManagerProvider, sessionId, timeOutInSeconds, repeatDelayInSeconds)
} }
override fun stopAnyBackgroundSync() { override fun stopAnyBackgroundSync() {

View File

@ -47,6 +47,7 @@ import org.matrix.android.sdk.internal.crypto.secrets.DefaultSharedSecretStorage
import org.matrix.android.sdk.internal.crypto.verification.VerificationMessageProcessor import org.matrix.android.sdk.internal.crypto.verification.VerificationMessageProcessor
import org.matrix.android.sdk.internal.database.DatabaseCleaner import org.matrix.android.sdk.internal.database.DatabaseCleaner
import org.matrix.android.sdk.internal.database.EventInsertLiveObserver import org.matrix.android.sdk.internal.database.EventInsertLiveObserver
import org.matrix.android.sdk.internal.database.RealmSessionProvider
import org.matrix.android.sdk.internal.database.SessionRealmConfigurationFactory import org.matrix.android.sdk.internal.database.SessionRealmConfigurationFactory
import org.matrix.android.sdk.internal.di.Authenticated import org.matrix.android.sdk.internal.di.Authenticated
import org.matrix.android.sdk.internal.di.DeviceId import org.matrix.android.sdk.internal.di.DeviceId
@ -325,23 +326,27 @@ internal abstract class SessionModule {
@Binds @Binds
@IntoSet @IntoSet
abstract fun bindIntegrationManager(observer: IntegrationManager): SessionLifecycleObserver abstract fun bindIntegrationManager(manager: IntegrationManager): SessionLifecycleObserver
@Binds @Binds
@IntoSet @IntoSet
abstract fun bindWidgetUrlFormatter(observer: DefaultWidgetURLFormatter): SessionLifecycleObserver abstract fun bindWidgetUrlFormatter(formatter: DefaultWidgetURLFormatter): SessionLifecycleObserver
@Binds @Binds
@IntoSet @IntoSet
abstract fun bindShieldTrustUpdated(observer: ShieldTrustUpdater): SessionLifecycleObserver abstract fun bindShieldTrustUpdated(updater: ShieldTrustUpdater): SessionLifecycleObserver
@Binds @Binds
@IntoSet @IntoSet
abstract fun bindIdentityService(observer: DefaultIdentityService): SessionLifecycleObserver abstract fun bindIdentityService(service: DefaultIdentityService): SessionLifecycleObserver
@Binds @Binds
@IntoSet @IntoSet
abstract fun bindDatabaseCleaner(observer: DatabaseCleaner): SessionLifecycleObserver abstract fun bindDatabaseCleaner(cleaner: DatabaseCleaner): SessionLifecycleObserver
@Binds
@IntoSet
abstract fun bindRealmSessionProvider(provider: RealmSessionProvider): SessionLifecycleObserver
@Binds @Binds
abstract fun bindInitialSyncProgressService(service: DefaultInitialSyncProgressService): InitialSyncProgressService abstract fun bindInitialSyncProgressService(service: DefaultInitialSyncProgressService): InitialSyncProgressService

View File

@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.session.call
import android.os.SystemClock import android.os.SystemClock
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.extensions.tryThis import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.call.CallSignalingService import org.matrix.android.sdk.api.session.call.CallSignalingService
import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.session.call.CallsListener import org.matrix.android.sdk.api.session.call.CallsListener
@ -210,7 +210,7 @@ internal class DefaultCallSignalingService @Inject constructor(
private fun onCallHangup(hangup: CallHangupContent) { private fun onCallHangup(hangup: CallHangupContent) {
callListeners.toList().forEach { callListeners.toList().forEach {
tryThis { tryOrNull {
it.onCallHangupReceived(hangup) it.onCallHangupReceived(hangup)
} }
} }
@ -218,7 +218,7 @@ internal class DefaultCallSignalingService @Inject constructor(
private fun onCallAnswer(answer: CallAnswerContent) { private fun onCallAnswer(answer: CallAnswerContent) {
callListeners.toList().forEach { callListeners.toList().forEach {
tryThis { tryOrNull {
it.onCallAnswerReceived(answer) it.onCallAnswerReceived(answer)
} }
} }
@ -226,7 +226,7 @@ internal class DefaultCallSignalingService @Inject constructor(
private fun onCallManageByOtherSession(callId: String) { private fun onCallManageByOtherSession(callId: String) {
callListeners.toList().forEach { callListeners.toList().forEach {
tryThis { tryOrNull {
it.onCallManagedByOtherSession(callId) it.onCallManagedByOtherSession(callId)
} }
} }
@ -237,7 +237,7 @@ internal class DefaultCallSignalingService @Inject constructor(
if (incomingCall.otherUserId == userId) return if (incomingCall.otherUserId == userId) return
callListeners.toList().forEach { callListeners.toList().forEach {
tryThis { tryOrNull {
it.onCallInviteReceived(incomingCall, invite) it.onCallInviteReceived(incomingCall, invite)
} }
} }
@ -245,7 +245,7 @@ internal class DefaultCallSignalingService @Inject constructor(
private fun onCallIceCandidate(incomingCall: MxCall, candidates: CallCandidatesContent) { private fun onCallIceCandidate(incomingCall: MxCall, candidates: CallCandidatesContent) {
callListeners.toList().forEach { callListeners.toList().forEach {
tryThis { tryOrNull {
it.onCallIceCandidateReceived(incomingCall, candidates) it.onCallIceCandidateReceived(incomingCall, candidates)
} }
} }

View File

@ -32,7 +32,7 @@ import okhttp3.RequestBody.Companion.toRequestBody
import okio.BufferedSink import okio.BufferedSink
import okio.source import okio.source
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.extensions.tryThis import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.content.ContentUrlResolver import org.matrix.android.sdk.api.session.content.ContentUrlResolver
import org.matrix.android.sdk.internal.di.Authenticated import org.matrix.android.sdk.internal.di.Authenticated
import org.matrix.android.sdk.internal.network.ProgressRequestBody import org.matrix.android.sdk.internal.network.ProgressRequestBody
@ -96,7 +96,7 @@ internal class FileUploader @Inject constructor(@Authenticated
inputStream.copyTo(it) inputStream.copyTo(it)
} }
return uploadFile(workingFile, filename, mimeType, progressListener).also { return uploadFile(workingFile, filename, mimeType, progressListener).also {
tryThis { workingFile.delete() } tryOrNull { workingFile.delete() }
} }
} }

View File

@ -0,0 +1,125 @@
/*
* Copyright (c) 2020 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.content
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Matrix
import androidx.exifinterface.media.ExifInterface
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
import java.util.UUID
import javax.inject.Inject
internal class ImageCompressor @Inject constructor() {
suspend fun compress(
context: Context,
imageFile: File,
desiredWidth: Int,
desiredHeight: Int,
desiredQuality: Int = 80): File {
return withContext(Dispatchers.IO) {
val compressedBitmap = BitmapFactory.Options().run {
inJustDecodeBounds = true
decodeBitmap(imageFile, this)
inSampleSize = calculateInSampleSize(outWidth, outHeight, desiredWidth, desiredHeight)
inJustDecodeBounds = false
decodeBitmap(imageFile, this)?.let {
rotateBitmap(imageFile, it)
}
} ?: return@withContext imageFile
val destinationFile = createDestinationFile(context)
runCatching {
destinationFile.outputStream().use {
compressedBitmap.compress(Bitmap.CompressFormat.JPEG, desiredQuality, it)
}
}
return@withContext destinationFile
}
}
private fun rotateBitmap(file: File, bitmap: Bitmap): Bitmap {
file.inputStream().use { inputStream ->
try {
ExifInterface(inputStream).let { exifInfo ->
val orientation = exifInfo.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
val matrix = Matrix()
when (orientation) {
ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270f)
ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180f)
ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90f)
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.preScale(-1f, 1f)
ExifInterface.ORIENTATION_FLIP_VERTICAL -> matrix.preScale(1f, -1f)
ExifInterface.ORIENTATION_TRANSPOSE -> {
matrix.preRotate(-90f)
matrix.preScale(-1f, 1f)
}
ExifInterface.ORIENTATION_TRANSVERSE -> {
matrix.preRotate(90f)
matrix.preScale(-1f, 1f)
}
else -> return bitmap
}
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
}
} catch (e: Exception) {
Timber.e(e, "Cannot read orientation")
}
}
return bitmap
}
// https://developer.android.com/topic/performance/graphics/load-bitmap
private fun calculateInSampleSize(width: Int, height: Int, desiredWidth: Int, desiredHeight: Int): Int {
var inSampleSize = 1
if (width > desiredWidth || height > desiredHeight) {
val halfHeight: Int = height / 2
val halfWidth: Int = width / 2
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while (halfHeight / inSampleSize >= desiredHeight && halfWidth / inSampleSize >= desiredWidth) {
inSampleSize *= 2
}
}
return inSampleSize
}
private fun decodeBitmap(file: File, options: BitmapFactory.Options = BitmapFactory.Options()): Bitmap? {
return try {
file.inputStream().use { inputStream ->
BitmapFactory.decodeStream(inputStream, null, options)
}
} catch (e: Exception) {
Timber.e(e, "Cannot decode Bitmap")
null
}
}
private fun createDestinationFile(context: Context): File {
return File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir)
}
}

View File

@ -19,14 +19,10 @@ package org.matrix.android.sdk.internal.session.content
import android.content.Context import android.content.Context
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import id.zelory.compressor.Compressor import org.matrix.android.sdk.api.extensions.tryOrNull
import id.zelory.compressor.constraint.default
import org.matrix.android.sdk.api.extensions.tryThis
import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
@ -36,13 +32,18 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments
import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo import org.matrix.android.sdk.internal.crypto.model.rest.EncryptedFileInfo
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.network.ProgressRequestBody import org.matrix.android.sdk.internal.network.ProgressRequestBody
import org.matrix.android.sdk.internal.session.DefaultFileService import org.matrix.android.sdk.internal.session.DefaultFileService
import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker
import org.matrix.android.sdk.internal.session.room.send.LocalEchoIdentifiers
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import org.matrix.android.sdk.internal.worker.getSessionComponent
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.util.UUID import java.util.UUID
@ -58,12 +59,13 @@ private data class NewImageAttributes(
* Possible previous worker: None * Possible previous worker: None
* Possible next worker : Always [MultipleEventSendingDispatcherWorker] * Possible next worker : Always [MultipleEventSendingDispatcherWorker]
*/ */
internal class UploadContentWorker(val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { internal class UploadContentWorker(val context: Context, params: WorkerParameters)
: SessionSafeCoroutineWorker<UploadContentWorker.Params>(context, params, Params::class.java) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class Params( internal data class Params(
override val sessionId: String, override val sessionId: String,
val events: List<Event>, val localEchoIds: List<LocalEchoIdentifiers>,
val attachment: ContentAttachmentData, val attachment: ContentAttachmentData,
val isEncrypted: Boolean, val isEncrypted: Boolean,
val compressBeforeSending: Boolean, val compressBeforeSending: Boolean,
@ -74,20 +76,15 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
@Inject lateinit var contentUploadStateTracker: DefaultContentUploadStateTracker @Inject lateinit var contentUploadStateTracker: DefaultContentUploadStateTracker
@Inject lateinit var fileService: DefaultFileService @Inject lateinit var fileService: DefaultFileService
@Inject lateinit var cancelSendTracker: CancelSendTracker @Inject lateinit var cancelSendTracker: CancelSendTracker
@Inject lateinit var imageCompressor: ImageCompressor
@Inject lateinit var localEchoRepository: LocalEchoRepository
override suspend fun doWork(): Result { override fun injectWith(injector: SessionComponent) {
val params = WorkerParamsFactory.fromData<Params>(inputData) injector.inject(this)
?: return Result.success() }
.also { Timber.e("Unable to parse work parameters") }
override suspend fun doSafeWork(params: Params): Result {
Timber.v("Starting upload media work with params $params") Timber.v("Starting upload media work with params $params")
if (params.lastFailureMessage != null) {
// Transmit the error
return Result.success(inputData)
.also { Timber.e("Work cancelled due to input error from parent") }
}
// Just defensive code to ensure that we never have an uncaught exception that could break the queue // Just defensive code to ensure that we never have an uncaught exception that could break the queue
return try { return try {
internalDoWork(params) internalDoWork(params)
@ -97,21 +94,21 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
} }
} }
override fun buildErrorParams(params: Params, message: String): Params {
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
}
private suspend fun internalDoWork(params: Params): Result { private suspend fun internalDoWork(params: Params): Result {
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() val allCancelled = params.localEchoIds.all { cancelSendTracker.isCancelRequestedFor(it.eventId, it.roomId) }
sessionComponent.inject(this)
val attachment = params.attachment
var newImageAttributes: NewImageAttributes? = null
val allCancelled = params.events.all { cancelSendTracker.isCancelRequestedFor(it.eventId, it.roomId) }
if (allCancelled) { if (allCancelled) {
// there is no point in uploading the image! // there is no point in uploading the image!
return Result.success(inputData) return Result.success(inputData)
.also { Timber.e("## Send: Work cancelled by user") } .also { Timber.e("## Send: Work cancelled by user") }
} }
val attachment = params.attachment
val filesToDelete = mutableListOf<File>()
try { try {
val inputStream = context.contentResolver.openInputStream(attachment.queryUri) val inputStream = context.contentResolver.openInputStream(attachment.queryUri)
?: return Result.success( ?: return Result.success(
@ -124,44 +121,15 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
// always use a temporary file, it guaranties that we could report progress on upload and simplifies the flows // always use a temporary file, it guaranties that we could report progress on upload and simplifies the flows
val workingFile = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir) val workingFile = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir)
workingFile.outputStream().use { .also { filesToDelete.add(it) }
inputStream.copyTo(it) workingFile.outputStream().use { outputStream ->
} inputStream.use { inputStream ->
inputStream.copyTo(outputStream)
// inputStream.use {
var uploadedThumbnailUrl: String? = null
var uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? = null
ThumbnailExtractor.extractThumbnail(context, params.attachment)?.let { thumbnailData ->
val thumbnailProgressListener = object : ProgressRequestBody.Listener {
override fun onProgress(current: Long, total: Long) {
notifyTracker(params) { contentUploadStateTracker.setProgressThumbnail(it, current, total) }
}
}
try {
val contentUploadResponse = if (params.isEncrypted) {
Timber.v("Encrypt thumbnail")
notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) }
val encryptionResult = MXEncryptedAttachments.encryptAttachment(thumbnailData.bytes.inputStream(), thumbnailData.mimeType)
uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo
fileUploader.uploadByteArray(encryptionResult.encryptedByteArray,
"thumb_${attachment.name}",
"application/octet-stream",
thumbnailProgressListener)
} else {
fileUploader.uploadByteArray(thumbnailData.bytes,
"thumb_${attachment.name}",
thumbnailData.mimeType,
thumbnailProgressListener)
}
uploadedThumbnailUrl = contentUploadResponse.contentUri
} catch (t: Throwable) {
Timber.e(t, "Thumbnail update failed")
} }
} }
val uploadThumbnailResult = dealWithThumbnail(params)
val progressListener = object : ProgressRequestBody.Listener { val progressListener = object : ProgressRequestBody.Listener {
override fun onProgress(current: Long, total: Long) { override fun onProgress(current: Long, total: Long) {
notifyTracker(params) { notifyTracker(params) {
@ -177,40 +145,37 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null
return try { return try {
val fileToUplaod: File val fileToUpload: File
var newImageAttributes: NewImageAttributes? = null
if (attachment.type == ContentAttachmentData.Type.IMAGE && params.compressBeforeSending) { if (attachment.type == ContentAttachmentData.Type.IMAGE && params.compressBeforeSending) {
// Compressor library works with File instead of Uri for now. Since Scoped Storage doesn't allow us to access files directly, we should fileToUpload = imageCompressor.compress(context, workingFile, MAX_IMAGE_SIZE, MAX_IMAGE_SIZE)
// copy it to a cache folder by using InputStream and OutputStream. .also { compressedFile ->
// https://github.com/zetbaitsu/Compressor/pull/150 // Get new Bitmap size
// As soon as the above PR is merged, we can use attachment.queryUri instead of creating a cacheFile. compressedFile.inputStream().use {
val compressedFile = Compressor.compress(context, workingFile) { val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
default( val bitmap = BitmapFactory.decodeStream(it, null, options)
width = MAX_IMAGE_SIZE, val fileSize = bitmap?.byteCount ?: 0
height = MAX_IMAGE_SIZE newImageAttributes = NewImageAttributes(
) options.outWidth,
} options.outHeight,
fileSize
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } )
BitmapFactory.decodeFile(compressedFile.absolutePath, options) }
val fileSize = compressedFile.length().toInt() }
newImageAttributes = NewImageAttributes( .also { filesToDelete.add(it) }
options.outWidth,
options.outHeight,
fileSize
)
fileToUplaod = compressedFile
} else { } else {
fileToUplaod = workingFile fileToUpload = workingFile
} }
val contentUploadResponse = if (params.isEncrypted) { val contentUploadResponse = if (params.isEncrypted) {
Timber.v("## FileService: Encrypt file") Timber.v("## FileService: Encrypt file")
val tmpEncrypted = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir) val tmpEncrypted = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir)
.also { filesToDelete.add(it) }
uploadedFileEncryptedFileInfo = uploadedFileEncryptedFileInfo =
MXEncryptedAttachments.encrypt(fileToUplaod.inputStream(), attachment.getSafeMimeType(), tmpEncrypted) { read, total -> MXEncryptedAttachments.encrypt(fileToUpload.inputStream(), attachment.getSafeMimeType(), tmpEncrypted) { read, total ->
notifyTracker(params) { notifyTracker(params) {
contentUploadStateTracker.setEncrypting(it, read.toLong(), total.toLong()) contentUploadStateTracker.setEncrypting(it, read.toLong(), total.toLong())
} }
@ -220,14 +185,10 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
fileUploader fileUploader
.uploadFile(tmpEncrypted, attachment.name, "application/octet-stream", progressListener) .uploadFile(tmpEncrypted, attachment.name, "application/octet-stream", progressListener)
.also {
// we can delete?
tryThis { tmpEncrypted.delete() }
}
} else { } else {
Timber.v("## FileService: Clear file") Timber.v("## FileService: Clear file")
fileUploader fileUploader
.uploadFile(fileToUplaod, attachment.name, attachment.getSafeMimeType(), progressListener) .uploadFile(fileToUpload, attachment.name, attachment.getSafeMimeType(), progressListener)
} }
Timber.v("## FileService: Update cache storage for ${contentUploadResponse.contentUri}") Timber.v("## FileService: Update cache storage for ${contentUploadResponse.contentUri}")
@ -237,14 +198,14 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
} }
Timber.v("## FileService: cache storage updated") Timber.v("## FileService: cache storage updated")
} catch (failure: Throwable) { } catch (failure: Throwable) {
Timber.e(failure, "## FileService: Failed to update fileservice cache") Timber.e(failure, "## FileService: Failed to update file cache")
} }
handleSuccess(params, handleSuccess(params,
contentUploadResponse.contentUri, contentUploadResponse.contentUri,
uploadedFileEncryptedFileInfo, uploadedFileEncryptedFileInfo,
uploadedThumbnailUrl, uploadThumbnailResult?.uploadedThumbnailUrl,
uploadedThumbnailEncryptedFileInfo, uploadThumbnailResult?.uploadedThumbnailEncryptedFileInfo,
newImageAttributes) newImageAttributes)
} catch (t: Throwable) { } catch (t: Throwable) {
Timber.e(t, "## FileService: ERROR ${t.localizedMessage}") Timber.e(t, "## FileService: ERROR ${t.localizedMessage}")
@ -252,17 +213,62 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "## FileService: ERROR") Timber.e(e, "## FileService: ERROR")
notifyTracker(params) { contentUploadStateTracker.setFailure(it, e) } return handleFailure(params, e)
return Result.success( } finally {
WorkerParamsFactory.toData( // Delete all temporary files
params.copy( filesToDelete.forEach {
lastFailureMessage = e.localizedMessage tryOrNull { it.delete() }
) }
)
)
} }
} }
private data class UploadThumbnailResult(
val uploadedThumbnailUrl: String,
val uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo?
)
/**
* If appropriate, it will create and upload a thumbnail
*/
private suspend fun dealWithThumbnail(params: Params): UploadThumbnailResult? {
return ThumbnailExtractor.extractThumbnail(context, params.attachment)
?.let { thumbnailData ->
val thumbnailProgressListener = object : ProgressRequestBody.Listener {
override fun onProgress(current: Long, total: Long) {
notifyTracker(params) { contentUploadStateTracker.setProgressThumbnail(it, current, total) }
}
}
try {
if (params.isEncrypted) {
Timber.v("Encrypt thumbnail")
notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) }
val encryptionResult = MXEncryptedAttachments.encryptAttachment(thumbnailData.bytes.inputStream(), thumbnailData.mimeType)
val contentUploadResponse = fileUploader.uploadByteArray(encryptionResult.encryptedByteArray,
"thumb_${params.attachment.name}",
"application/octet-stream",
thumbnailProgressListener)
UploadThumbnailResult(
contentUploadResponse.contentUri,
encryptionResult.encryptedFileInfo
)
} else {
val contentUploadResponse = fileUploader.uploadByteArray(thumbnailData.bytes,
"thumb_${params.attachment.name}",
thumbnailData.mimeType,
thumbnailProgressListener)
UploadThumbnailResult(
contentUploadResponse.contentUri,
null
)
}
} catch (t: Throwable) {
Timber.e(t, "Thumbnail upload failed")
null
}
}
}
private fun handleFailure(params: Params, failure: Throwable): Result { private fun handleFailure(params: Params, failure: Throwable): Result {
notifyTracker(params) { contentUploadStateTracker.setFailure(it, failure) } notifyTracker(params) { contentUploadStateTracker.setFailure(it, failure) }
@ -275,46 +281,48 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
) )
} }
private fun handleSuccess(params: Params, private suspend fun handleSuccess(params: Params,
attachmentUrl: String, attachmentUrl: String,
encryptedFileInfo: EncryptedFileInfo?, encryptedFileInfo: EncryptedFileInfo?,
thumbnailUrl: String?, thumbnailUrl: String?,
thumbnailEncryptedFileInfo: EncryptedFileInfo?, thumbnailEncryptedFileInfo: EncryptedFileInfo?,
newImageAttributes: NewImageAttributes?): Result { newImageAttributes: NewImageAttributes?): Result {
notifyTracker(params) { contentUploadStateTracker.setSuccess(it) } notifyTracker(params) { contentUploadStateTracker.setSuccess(it) }
params.localEchoIds.forEach {
updateEvent(it.eventId, attachmentUrl, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo, newImageAttributes)
}
val updatedEvents = params.events val sendParams = MultipleEventSendingDispatcherWorker.Params(
.map { sessionId = params.sessionId,
updateEvent(it, attachmentUrl, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo, newImageAttributes) localEchoIds = params.localEchoIds,
} isEncrypted = params.isEncrypted
)
val sendParams = MultipleEventSendingDispatcherWorker.Params(params.sessionId, updatedEvents, params.isEncrypted)
return Result.success(WorkerParamsFactory.toData(sendParams)).also { return Result.success(WorkerParamsFactory.toData(sendParams)).also {
Timber.v("## handleSuccess $attachmentUrl, work is stopped $isStopped") Timber.v("## handleSuccess $attachmentUrl, work is stopped $isStopped")
} }
} }
private fun updateEvent(event: Event, private suspend fun updateEvent(eventId: String,
url: String, url: String,
encryptedFileInfo: EncryptedFileInfo?, encryptedFileInfo: EncryptedFileInfo?,
thumbnailUrl: String? = null, thumbnailUrl: String? = null,
thumbnailEncryptedFileInfo: EncryptedFileInfo?, thumbnailEncryptedFileInfo: EncryptedFileInfo?,
newImageAttributes: NewImageAttributes?): Event { newImageAttributes: NewImageAttributes?) {
val messageContent: MessageContent = event.content.toModel() ?: return event localEchoRepository.updateEcho(eventId) { _, event ->
val updatedContent = when (messageContent) { val messageContent: MessageContent? = event.asDomain().content.toModel()
is MessageImageContent -> messageContent.update(url, encryptedFileInfo, newImageAttributes) val updatedContent = when (messageContent) {
is MessageVideoContent -> messageContent.update(url, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo) is MessageImageContent -> messageContent.update(url, encryptedFileInfo, newImageAttributes)
is MessageFileContent -> messageContent.update(url, encryptedFileInfo) is MessageVideoContent -> messageContent.update(url, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo)
is MessageAudioContent -> messageContent.update(url, encryptedFileInfo) is MessageFileContent -> messageContent.update(url, encryptedFileInfo)
else -> messageContent is MessageAudioContent -> messageContent.update(url, encryptedFileInfo)
else -> messageContent
}
event.content = ContentMapper.map(updatedContent.toContent())
} }
return event.copy(content = updatedContent.toContent())
} }
private fun notifyTracker(params: Params, function: (String) -> Unit) { private fun notifyTracker(params: Params, function: (String) -> Unit) {
params.events params.localEchoIds.forEach { function.invoke(it.eventId) }
.mapNotNull { it.eventId }
.forEach { eventId -> function.invoke(eventId) }
} }
private fun MessageImageContent.update(url: String, private fun MessageImageContent.update(url: String,

View File

@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.session.download
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import org.matrix.android.sdk.api.extensions.tryThis import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker import org.matrix.android.sdk.api.session.file.ContentDownloadStateTracker
import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.SessionScope
import timber.log.Timber import timber.log.Timber
@ -76,7 +76,7 @@ internal class DefaultContentDownloadStateTracker @Inject constructor() : Progre
Timber.v("## DL Progress Error code:$errorCode") Timber.v("## DL Progress Error code:$errorCode")
updateState(url, ContentDownloadStateTracker.State.Failure(errorCode)) updateState(url, ContentDownloadStateTracker.State.Failure(errorCode))
listeners[url]?.forEach { listeners[url]?.forEach {
tryThis { it.onDownloadStateUpdate(ContentDownloadStateTracker.State.Failure(errorCode)) } tryOrNull { it.onDownloadStateUpdate(ContentDownloadStateTracker.State.Failure(errorCode)) }
} }
} }
} }
@ -84,7 +84,7 @@ internal class DefaultContentDownloadStateTracker @Inject constructor() : Progre
private fun updateState(url: String, state: ContentDownloadStateTracker.State) { private fun updateState(url: String, state: ContentDownloadStateTracker.State) {
states[url] = state states[url] = state
listeners[url]?.forEach { listeners[url]?.forEach {
tryThis { it.onDownloadStateUpdate(state) } tryOrNull { it.onDownloadStateUpdate(state) }
} }
} }
} }

View File

@ -18,20 +18,19 @@
package org.matrix.android.sdk.internal.session.group package org.matrix.android.sdk.internal.session.group
import android.content.Context import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import org.matrix.android.sdk.internal.worker.getSessionComponent
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
/** /**
* Possible previous worker: None * Possible previous worker: None
* Possible next worker : None * Possible next worker : None
*/ */
internal class GetGroupDataWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { internal class GetGroupDataWorker(context: Context, params: WorkerParameters)
: SessionSafeCoroutineWorker<GetGroupDataWorker.Params>(context, params, Params::class.java) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class Params( internal data class Params(
@ -41,13 +40,11 @@ internal class GetGroupDataWorker(context: Context, params: WorkerParameters) :
@Inject lateinit var getGroupDataTask: GetGroupDataTask @Inject lateinit var getGroupDataTask: GetGroupDataTask
override suspend fun doWork(): Result { override fun injectWith(injector: SessionComponent) {
val params = WorkerParamsFactory.fromData<Params>(inputData) injector.inject(this)
?: return Result.failure() }
.also { Timber.e("Unable to parse work parameters") }
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() override suspend fun doSafeWork(params: Params): Result {
sessionComponent.inject(this)
return runCatching { return runCatching {
getGroupDataTask.execute(GetGroupDataTask.Params.FetchAllActive) getGroupDataTask.execute(GetGroupDataTask.Params.FetchAllActive)
}.fold( }.fold(
@ -55,4 +52,8 @@ internal class GetGroupDataWorker(context: Context, params: WorkerParameters) :
{ Result.retry() } { Result.retry() }
) )
} }
override fun buildErrorParams(params: Params, message: String): Params {
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
}
} }

View File

@ -18,6 +18,7 @@
package org.matrix.android.sdk.internal.session.homeserver package org.matrix.android.sdk.internal.session.homeserver
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.wellknown.WellknownResult import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
@ -32,7 +33,6 @@ import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationMan
import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction import org.matrix.android.sdk.internal.util.awaitTransaction
import org.matrix.android.sdk.internal.wellknown.GetWellknownTask import org.matrix.android.sdk.internal.wellknown.GetWellknownTask
import org.greenrobot.eventbus.EventBus
import timber.log.Timber import timber.log.Timber
import java.util.Date import java.util.Date
import javax.inject.Inject import javax.inject.Inject
@ -109,16 +109,12 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
if (getWellknownResult != null && getWellknownResult is WellknownResult.Prompt) { if (getWellknownResult != null && getWellknownResult is WellknownResult.Prompt) {
homeServerCapabilitiesEntity.defaultIdentityServerUrl = getWellknownResult.identityServerUrl homeServerCapabilitiesEntity.defaultIdentityServerUrl = getWellknownResult.identityServerUrl
homeServerCapabilitiesEntity.adminE2EByDefault = getWellknownResult.wellKnown.e2eAdminSetting?.e2eDefault ?: true
homeServerCapabilitiesEntity.preferredJitsiDomain = getWellknownResult.wellKnown.jitsiServer?.preferredDomain
// We are also checking for integration manager configurations // We are also checking for integration manager configurations
val config = configExtractor.extract(getWellknownResult.wellKnown) val config = configExtractor.extract(getWellknownResult.wellKnown)
if (config != null) { if (config != null) {
Timber.v("Extracted integration config : $config") Timber.v("Extracted integration config : $config")
realm.insertOrUpdate(config) realm.insertOrUpdate(config)
} }
} else {
homeServerCapabilitiesEntity.adminE2EByDefault = true
} }
homeServerCapabilitiesEntity.lastUpdatedTimestamp = Date().time homeServerCapabilitiesEntity.lastUpdatedTimestamp = Date().time
} }

View File

@ -23,7 +23,7 @@ import androidx.lifecycle.LifecycleRegistry
import dagger.Lazy import dagger.Lazy
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.auth.data.SessionParams import org.matrix.android.sdk.api.auth.data.SessionParams
import org.matrix.android.sdk.api.extensions.tryThis import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.failure.MatrixError
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
@ -113,7 +113,7 @@ internal class DefaultIdentityService @Inject constructor(
// Url has changed, we have to reset our store, update internal configuration and notify listeners // Url has changed, we have to reset our store, update internal configuration and notify listeners
identityStore.setUrl(baseUrl) identityStore.setUrl(baseUrl)
updateIdentityAPI(baseUrl) updateIdentityAPI(baseUrl)
listeners.toList().forEach { tryThis { it.onIdentityServerChange() } } listeners.toList().forEach { tryOrNull { it.onIdentityServerChange() } }
} }
} }
@ -236,7 +236,7 @@ internal class DefaultIdentityService @Inject constructor(
private suspend fun updateAccountData(url: String?) { private suspend fun updateAccountData(url: String?) {
// Also notify the listener // Also notify the listener
withContext(coroutineDispatchers.main) { withContext(coroutineDispatchers.main) {
listeners.toList().forEach { tryThis { it.onIdentityServerChange() } } listeners.toList().forEach { tryOrNull { it.onIdentityServerChange() } }
} }
updateUserAccountDataTask.execute(UpdateUserAccountDataTask.IdentityParams( updateUserAccountDataTask.execute(UpdateUserAccountDataTask.IdentityParams(

View File

@ -23,6 +23,7 @@ import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.task.Task
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.internal.util.awaitTransaction
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -39,7 +40,7 @@ internal class DefaultRefreshUserThreePidsTask @Inject constructor(private val p
Timber.d("Get ${accountThreePidsResponse.threePids?.size} threePids") Timber.d("Get ${accountThreePidsResponse.threePids?.size} threePids")
// Store the list in DB // Store the list in DB
monarchy.writeAsync { realm -> monarchy.awaitTransaction { realm ->
realm.where(UserThreePidEntity::class.java).findAll().deleteAllFromRealm() realm.where(UserThreePidEntity::class.java).findAll().deleteAllFromRealm()
accountThreePidsResponse.threePids?.forEach { accountThreePidsResponse.threePids?.forEach {
val entity = UserThreePidEntity( val entity = UserThreePidEntity(

View File

@ -17,10 +17,10 @@
package org.matrix.android.sdk.internal.session.pushers package org.matrix.android.sdk.internal.session.pushers
import android.content.Context import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.pushers.PusherState import org.matrix.android.sdk.api.session.pushers.PusherState
import org.matrix.android.sdk.internal.database.mapper.toEntity import org.matrix.android.sdk.internal.database.mapper.toEntity
@ -28,16 +28,14 @@ import org.matrix.android.sdk.internal.database.model.PusherEntity
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.util.awaitTransaction import org.matrix.android.sdk.internal.util.awaitTransaction
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import org.matrix.android.sdk.internal.worker.getSessionComponent
import org.greenrobot.eventbus.EventBus
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
internal class AddHttpPusherWorker(context: Context, params: WorkerParameters) internal class AddHttpPusherWorker(context: Context, params: WorkerParameters)
: CoroutineWorker(context, params) { : SessionSafeCoroutineWorker<AddHttpPusherWorker.Params>(context, params, Params::class.java) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class Params( internal data class Params(
@ -50,14 +48,11 @@ internal class AddHttpPusherWorker(context: Context, params: WorkerParameters)
@Inject @SessionDatabase lateinit var monarchy: Monarchy @Inject @SessionDatabase lateinit var monarchy: Monarchy
@Inject lateinit var eventBus: EventBus @Inject lateinit var eventBus: EventBus
override suspend fun doWork(): Result { override fun injectWith(injector: SessionComponent) {
val params = WorkerParamsFactory.fromData<Params>(inputData) injector.inject(this)
?: return Result.failure() }
.also { Timber.e("Unable to parse work parameters") }
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
sessionComponent.inject(this)
override suspend fun doSafeWork(params: Params): Result {
val pusher = params.pusher val pusher = params.pusher
if (pusher.pushKey.isBlank()) { if (pusher.pushKey.isBlank()) {
@ -82,6 +77,10 @@ internal class AddHttpPusherWorker(context: Context, params: WorkerParameters)
} }
} }
override fun buildErrorParams(params: Params, message: String): Params {
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
}
private suspend fun setPusher(pusher: JsonPusher) { private suspend fun setPusher(pusher: JsonPusher) {
executeRequest<Unit>(eventBus) { executeRequest<Unit>(eventBus) {
apiCall = pushersAPI.setPusher(pusher) apiCall = pushersAPI.setPusher(pusher)

View File

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

View File

@ -17,17 +17,16 @@
package org.matrix.android.sdk.internal.session.room package org.matrix.android.sdk.internal.session.room
import com.zhuinden.monarchy.Monarchy import io.realm.Realm
import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.internal.database.RealmSessionProvider
import org.matrix.android.sdk.internal.database.model.RoomEntity import org.matrix.android.sdk.internal.database.model.RoomEntity
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
import io.realm.Realm
import javax.inject.Inject import javax.inject.Inject
internal interface RoomGetter { internal interface RoomGetter {
@ -38,18 +37,18 @@ internal interface RoomGetter {
@SessionScope @SessionScope
internal class DefaultRoomGetter @Inject constructor( internal class DefaultRoomGetter @Inject constructor(
@SessionDatabase private val monarchy: Monarchy, private val realmSessionProvider: RealmSessionProvider,
private val roomFactory: RoomFactory private val roomFactory: RoomFactory
) : RoomGetter { ) : RoomGetter {
override fun getRoom(roomId: String): Room? { override fun getRoom(roomId: String): Room? {
return Realm.getInstance(monarchy.realmConfiguration).use { realm -> return realmSessionProvider.withRealm { realm ->
createRoom(realm, roomId) createRoom(realm, roomId)
} }
} }
override fun getDirectRoomWith(otherUserId: String): Room? { override fun getDirectRoomWith(otherUserId: String): Room? {
return Realm.getInstance(monarchy.realmConfiguration).use { realm -> return realmSessionProvider.withRealm { realm ->
RoomSummaryEntity.where(realm) RoomSummaryEntity.where(realm)
.equalTo(RoomSummaryEntityFields.IS_DIRECT, true) .equalTo(RoomSummaryEntityFields.IS_DIRECT, true)
.equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name) .equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name)

View File

@ -202,13 +202,13 @@ internal class DefaultRelationService @AssistedInject constructor(
private fun createEncryptEventWork(event: Event, keepKeys: List<String>?): OneTimeWorkRequest { private fun createEncryptEventWork(event: Event, keepKeys: List<String>?): OneTimeWorkRequest {
// Same parameter // Same parameter
val params = EncryptEventWorker.Params(sessionId, event, keepKeys) val params = EncryptEventWorker.Params(sessionId, event.eventId!!, keepKeys)
val sendWorkData = WorkerParamsFactory.toData(params) val sendWorkData = WorkerParamsFactory.toData(params)
return timeLineSendEventWorkCommon.createWork<EncryptEventWorker>(sendWorkData, true) return timeLineSendEventWorkCommon.createWork<EncryptEventWorker>(sendWorkData, true)
} }
private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, event = event) val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, eventId = event.eventId!!)
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
return timeLineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain) return timeLineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)
} }

View File

@ -17,7 +17,6 @@
package org.matrix.android.sdk.internal.session.room.relation package org.matrix.android.sdk.internal.session.room.relation
import android.content.Context import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
@ -27,45 +26,38 @@ import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent
import org.matrix.android.sdk.api.session.room.model.relation.ReactionInfo import org.matrix.android.sdk.api.session.room.model.relation.ReactionInfo
import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
import org.matrix.android.sdk.internal.session.room.send.SendResponse import org.matrix.android.sdk.internal.session.room.send.SendResponse
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import org.matrix.android.sdk.internal.worker.getSessionComponent
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
// TODO This is not used. Delete? // TODO This is not used. Delete?
internal class SendRelationWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { internal class SendRelationWorker(context: Context, params: WorkerParameters)
: SessionSafeCoroutineWorker<SendRelationWorker.Params>(context, params, Params::class.java) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class Params( internal data class Params(
override val sessionId: String, override val sessionId: String,
val roomId: String, val roomId: String,
val event: Event, val eventId: String,
val relationType: String? = null, val relationType: String? = null,
override val lastFailureMessage: String? = null override val lastFailureMessage: String? = null
) : SessionWorkerParams ) : SessionWorkerParams
@Inject lateinit var roomAPI: RoomAPI @Inject lateinit var roomAPI: RoomAPI
@Inject lateinit var eventBus: EventBus @Inject lateinit var eventBus: EventBus
@Inject lateinit var localEchoRepository: LocalEchoRepository
override suspend fun doWork(): Result { override fun injectWith(injector: SessionComponent) {
val params = WorkerParamsFactory.fromData<Params>(inputData) injector.inject(this)
?: return Result.failure() }
.also { Timber.e("Unable to parse work parameters") }
if (params.lastFailureMessage != null) { override suspend fun doSafeWork(params: Params): Result {
// Transmit the error val localEvent = localEchoRepository.getUpToDateEcho(params.eventId)
return Result.success(inputData) if (localEvent?.eventId == null) {
.also { Timber.e("Work cancelled due to input error from parent") }
}
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
sessionComponent.inject(this)
val localEvent = params.event
if (localEvent.eventId == null) {
return Result.failure() return Result.failure()
} }
val relationContent = localEvent.content.toModel<ReactionContent>() val relationContent = localEvent.content.toModel<ReactionContent>()
@ -88,6 +80,10 @@ internal class SendRelationWorker(context: Context, params: WorkerParameters) :
} }
} }
override fun buildErrorParams(params: Params, message: String): Params {
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
}
private suspend fun sendRelation(roomId: String, relationType: String, relatedEventId: String, localEvent: Event) { private suspend fun sendRelation(roomId: String, relationType: String, relatedEventId: String, localEvent: Event) {
executeRequest<SendResponse>(eventBus) { executeRequest<SendResponse>(eventBus) {
apiCall = roomAPI.sendRelation( apiCall = roomAPI.sendRelation(

View File

@ -336,7 +336,7 @@ internal class DefaultSendService @AssistedInject constructor(
private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
// Same parameter // Same parameter
return EncryptEventWorker.Params(sessionId, event) return EncryptEventWorker.Params(sessionId, event.eventId ?: "")
.let { WorkerParamsFactory.toData(it) } .let { WorkerParamsFactory.toData(it) }
.let { .let {
workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>() workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>()
@ -360,7 +360,10 @@ internal class DefaultSendService @AssistedInject constructor(
attachment: ContentAttachmentData, attachment: ContentAttachmentData,
isRoomEncrypted: Boolean, isRoomEncrypted: Boolean,
compressBeforeSending: Boolean): OneTimeWorkRequest { compressBeforeSending: Boolean): OneTimeWorkRequest {
val uploadMediaWorkerParams = UploadContentWorker.Params(sessionId, allLocalEchos, attachment, isRoomEncrypted, compressBeforeSending) val localEchoIds = allLocalEchos.map {
LocalEchoIdentifiers(it.roomId!!, it.eventId!!)
}
val uploadMediaWorkerParams = UploadContentWorker.Params(sessionId, localEchoIds, attachment, isRoomEncrypted, compressBeforeSending)
val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams) val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams)
return workManagerProvider.matrixOneTimeWorkRequestBuilder<UploadContentWorker>() return workManagerProvider.matrixOneTimeWorkRequestBuilder<UploadContentWorker>()

View File

@ -18,21 +18,23 @@
package org.matrix.android.sdk.internal.session.room.send package org.matrix.android.sdk.internal.session.room.send
import android.content.Context import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.internal.util.awaitCallback
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import org.matrix.android.sdk.internal.worker.getSessionComponent
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -41,12 +43,12 @@ import javax.inject.Inject
* Possible next worker : Always [SendEventWorker] * Possible next worker : Always [SendEventWorker]
*/ */
internal class EncryptEventWorker(context: Context, params: WorkerParameters) internal class EncryptEventWorker(context: Context, params: WorkerParameters)
: CoroutineWorker(context, params) { : SessionSafeCoroutineWorker<EncryptEventWorker.Params>(context, params, Params::class.java) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class Params( internal data class Params(
override val sessionId: String, override val sessionId: String,
val event: Event, val eventId: String,
/** Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to) */ /** Do not encrypt these keys, keep them as is in encrypted content (e.g. m.relates_to) */
val keepKeys: List<String>? = null, val keepKeys: List<String>? = null,
override val lastFailureMessage: String? = null override val lastFailureMessage: String? = null
@ -56,24 +58,15 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
@Inject lateinit var localEchoRepository: LocalEchoRepository @Inject lateinit var localEchoRepository: LocalEchoRepository
@Inject lateinit var cancelSendTracker: CancelSendTracker @Inject lateinit var cancelSendTracker: CancelSendTracker
override suspend fun doWork(): Result { override fun injectWith(injector: SessionComponent) {
Timber.v("Start Encrypt work") injector.inject(this)
val params = WorkerParamsFactory.fromData<Params>(inputData) }
?: return Result.success()
.also { Timber.e("Unable to parse work parameters") }
Timber.v("## SendEvent: Start Encrypt work for event ${params.event.eventId}") override suspend fun doSafeWork(params: Params): Result {
if (params.lastFailureMessage != null) { Timber.v("## SendEvent: Start Encrypt work for event ${params.eventId}")
// Transmit the error
return Result.success(inputData)
.also { Timber.e("Work cancelled due to input error from parent") }
}
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() val localEvent = localEchoRepository.getUpToDateEcho(params.eventId)
sessionComponent.inject(this) if (localEvent?.eventId == null) {
val localEvent = params.event
if (localEvent.eventId == null) {
return Result.success() return Result.success()
} }
@ -106,15 +99,10 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
modifiedContent[toKeep] = it modifiedContent[toKeep] = it
} }
} }
val safeResult = result.copy(eventContent = modifiedContent)
val encryptedEvent = localEvent.copy(
type = safeResult.eventType,
content = safeResult.eventContent
)
// Better handling of local echo, to avoid decrypting transition on remote echo // Better handling of local echo, to avoid decrypting transition on remote echo
// Should I only do it for text messages? // Should I only do it for text messages?
if (result.eventContent["algorithm"] == MXCRYPTO_ALGORITHM_MEGOLM) { val decryptionLocalEcho = if (result.eventContent["algorithm"] == MXCRYPTO_ALGORITHM_MEGOLM) {
val decryptionLocalEcho = MXEventDecryptionResult( MXEventDecryptionResult(
clearEvent = Event( clearEvent = Event(
type = localEvent.type, type = localEvent.type,
content = localEvent.content, content = localEvent.content,
@ -124,10 +112,18 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
senderCurve25519Key = result.eventContent["sender_key"] as? String, senderCurve25519Key = result.eventContent["sender_key"] as? String,
claimedEd25519Key = crypto.getMyDevice().fingerprint() claimedEd25519Key = crypto.getMyDevice().fingerprint()
) )
localEchoRepository.updateEncryptedEcho(localEvent.eventId, safeResult.eventContent, decryptionLocalEcho) } else {
null
}
localEchoRepository.updateEcho(localEvent.eventId) { _, localEcho ->
localEcho.type = EventType.ENCRYPTED
localEcho.content = ContentMapper.map(modifiedContent)
decryptionLocalEcho?.also {
localEcho.setDecryptionResult(it)
}
} }
val nextWorkerParams = SendEventWorker.Params(sessionId = params.sessionId, event = encryptedEvent) val nextWorkerParams = SendEventWorker.Params(sessionId = params.sessionId, eventId = params.eventId)
return Result.success(WorkerParamsFactory.toData(nextWorkerParams)) return Result.success(WorkerParamsFactory.toData(nextWorkerParams))
} else { } else {
val sendState = when (error) { val sendState = when (error) {
@ -138,10 +134,14 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters)
// always return success, or the chain will be stuck for ever! // always return success, or the chain will be stuck for ever!
val nextWorkerParams = SendEventWorker.Params( val nextWorkerParams = SendEventWorker.Params(
sessionId = params.sessionId, sessionId = params.sessionId,
event = localEvent, eventId = localEvent.eventId,
lastFailureMessage = error?.localizedMessage ?: "Error" lastFailureMessage = error?.localizedMessage ?: "Error"
) )
return Result.success(WorkerParamsFactory.toData(nextWorkerParams)) return Result.success(WorkerParamsFactory.toData(nextWorkerParams))
} }
} }
override fun buildErrorParams(params: Params, message: String): Params {
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
}
} }

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.room.send
import com.squareup.moshi.JsonClass
/**
* This is used as a holder to pass necessary data to some workers params.
*/
@JsonClass(generateAdapter = true)
internal data class LocalEchoIdentifiers(val roomId: String, val eventId: String)

View File

@ -18,7 +18,8 @@
package org.matrix.android.sdk.internal.session.room.send package org.matrix.android.sdk.internal.session.room.send
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.session.events.model.Content import io.realm.Realm
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
@ -26,10 +27,11 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult import org.matrix.android.sdk.internal.database.RealmSessionProvider
import org.matrix.android.sdk.internal.database.asyncTransaction
import org.matrix.android.sdk.internal.database.helper.nextId import org.matrix.android.sdk.internal.database.helper.nextId
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.mapper.toEntity import org.matrix.android.sdk.internal.database.mapper.toEntity
import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.model.EventInsertEntity import org.matrix.android.sdk.internal.database.model.EventInsertEntity
@ -42,13 +44,14 @@ import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater
import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimeline import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimeline
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.util.awaitTransaction import org.matrix.android.sdk.internal.util.awaitTransaction
import io.realm.Realm
import org.greenrobot.eventbus.EventBus
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
internal class LocalEchoRepository @Inject constructor(@SessionDatabase private val monarchy: Monarchy, internal class LocalEchoRepository @Inject constructor(@SessionDatabase private val monarchy: Monarchy,
private val taskExecutor: TaskExecutor,
private val realmSessionProvider: RealmSessionProvider,
private val roomSummaryUpdater: RoomSummaryUpdater, private val roomSummaryUpdater: RoomSummaryUpdater,
private val eventBus: EventBus, private val eventBus: EventBus,
private val timelineEventMapper: TimelineEventMapper) { private val timelineEventMapper: TimelineEventMapper) {
@ -59,7 +62,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
if (event.eventId == null) { if (event.eventId == null) {
throw IllegalStateException("You should have set an eventId for your event") throw IllegalStateException("You should have set an eventId for your event")
} }
val timelineEventEntity = Realm.getInstance(monarchy.realmConfiguration).use { realm -> val timelineEventEntity = realmSessionProvider.withRealm { realm ->
val eventEntity = event.toEntity(roomId, SendState.UNSENT, System.currentTimeMillis()) val eventEntity = event.toEntity(roomId, SendState.UNSENT, System.currentTimeMillis())
val roomMemberHelper = RoomMemberHelper(realm, roomId) val roomMemberHelper = RoomMemberHelper(realm, roomId)
val myUser = roomMemberHelper.getLastRoomMember(senderId) val myUser = roomMemberHelper.getLastRoomMember(senderId)
@ -75,12 +78,12 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
} }
val timelineEvent = timelineEventMapper.map(timelineEventEntity) val timelineEvent = timelineEventMapper.map(timelineEventEntity)
eventBus.post(DefaultTimeline.OnLocalEchoCreated(roomId = roomId, timelineEvent = timelineEvent)) eventBus.post(DefaultTimeline.OnLocalEchoCreated(roomId = roomId, timelineEvent = timelineEvent))
monarchy.writeAsync { realm -> taskExecutor.executorScope.asyncTransaction(monarchy) { realm ->
val eventInsertEntity = EventInsertEntity(event.eventId, event.type).apply { val eventInsertEntity = EventInsertEntity(event.eventId, event.type).apply {
this.insertType = EventInsertType.LOCAL_ECHO this.insertType = EventInsertType.LOCAL_ECHO
} }
realm.insert(eventInsertEntity) realm.insert(eventInsertEntity)
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return@writeAsync val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return@asyncTransaction
roomEntity.sendingTimelineEvents.add(0, timelineEventEntity) roomEntity.sendingTimelineEvents.add(0, timelineEventEntity)
roomSummaryUpdater.updateSendingInformation(realm, roomId) roomSummaryUpdater.updateSendingInformation(realm, roomId)
} }
@ -88,30 +91,41 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
fun updateSendState(eventId: String, sendState: SendState) { fun updateSendState(eventId: String, sendState: SendState) {
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Update local state of $eventId to ${sendState.name}") Timber.v("## SendEvent: [${System.currentTimeMillis()}] Update local state of $eventId to ${sendState.name}")
monarchy.writeAsync { realm -> updateEchoAsync(eventId) { realm, sendingEventEntity ->
if (sendState == SendState.SENT && sendingEventEntity.sendState == SendState.SYNCED) {
// If already synced, do not put as sent
} else {
sendingEventEntity.sendState = sendState
}
roomSummaryUpdater.updateSendingInformation(realm, sendingEventEntity.roomId)
}
}
suspend fun updateEcho(eventId: String, block: (realm: Realm, eventEntity: EventEntity) -> Unit) {
monarchy.awaitTransaction { realm ->
val sendingEventEntity = EventEntity.where(realm, eventId).findFirst() val sendingEventEntity = EventEntity.where(realm, eventId).findFirst()
if (sendingEventEntity != null) { if (sendingEventEntity != null) {
if (sendState == SendState.SENT && sendingEventEntity.sendState == SendState.SYNCED) { block(realm, sendingEventEntity)
// If already synced, do not put as sent
} else {
sendingEventEntity.sendState = sendState
}
roomSummaryUpdater.updateSendingInformation(realm, sendingEventEntity.roomId)
} }
} }
} }
fun updateEncryptedEcho(eventId: String, encryptedContent: Content, mxEventDecryptionResult: MXEventDecryptionResult) { fun updateEchoAsync(eventId: String, block: (realm: Realm, eventEntity: EventEntity) -> Unit) {
monarchy.writeAsync { realm -> taskExecutor.executorScope.asyncTransaction(monarchy) { realm ->
val sendingEventEntity = EventEntity.where(realm, eventId).findFirst() val sendingEventEntity = EventEntity.where(realm, eventId).findFirst()
if (sendingEventEntity != null) { if (sendingEventEntity != null) {
sendingEventEntity.type = EventType.ENCRYPTED block(realm, sendingEventEntity)
sendingEventEntity.content = ContentMapper.map(encryptedContent)
sendingEventEntity.setDecryptionResult(mxEventDecryptionResult)
} }
} }
} }
suspend fun getUpToDateEcho(eventId: String): Event? {
// We are using awaitTransaction here to make sure this executes after other transactions
return monarchy.awaitTransaction { realm ->
EventEntity.where(realm, eventId).findFirst()?.asDomain(castJsonNumbers = true)
}
}
suspend fun deleteFailedEcho(roomId: String, localEcho: TimelineEvent) { suspend fun deleteFailedEcho(roomId: String, localEcho: TimelineEvent) {
deleteFailedEcho(roomId, localEcho.eventId) deleteFailedEcho(roomId, localEcho.eventId)
} }
@ -149,8 +163,8 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
return getAllEventsWithStates(roomId, SendState.HAS_FAILED_STATES) return getAllEventsWithStates(roomId, SendState.HAS_FAILED_STATES)
} }
fun getAllEventsWithStates(roomId: String, states : List<SendState>): List<TimelineEvent> { fun getAllEventsWithStates(roomId: String, states: List<SendState>): List<TimelineEvent> {
return Realm.getInstance(monarchy.realmConfiguration).use { realm -> return realmSessionProvider.withRealm { realm ->
TimelineEventEntity TimelineEventEntity
.findAllInRoomWithSendStates(realm, roomId, states) .findAllInRoomWithSendStates(realm, roomId, states)
.sortedByDescending { it.displayIndex } .sortedByDescending { it.displayIndex }

View File

@ -19,18 +19,17 @@ package org.matrix.android.sdk.internal.session.room.send
import android.content.Context import android.content.Context
import androidx.work.BackoffPolicy import androidx.work.BackoffPolicy
import androidx.work.CoroutineWorker
import androidx.work.OneTimeWorkRequest import androidx.work.OneTimeWorkRequest
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.di.WorkManagerProvider
import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.session.content.UploadContentWorker import org.matrix.android.sdk.internal.session.content.UploadContentWorker
import org.matrix.android.sdk.internal.session.room.timeline.TimelineSendEventWorkCommon import org.matrix.android.sdk.internal.session.room.timeline.TimelineSendEventWorkCommon
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import org.matrix.android.sdk.internal.worker.getSessionComponent
import org.matrix.android.sdk.internal.worker.startChain import org.matrix.android.sdk.internal.worker.startChain
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -43,12 +42,12 @@ import javax.inject.Inject
* Possible next worker : None, but it will post new work to send events, encrypted or not * Possible next worker : None, but it will post new work to send events, encrypted or not
*/ */
internal class MultipleEventSendingDispatcherWorker(context: Context, params: WorkerParameters) internal class MultipleEventSendingDispatcherWorker(context: Context, params: WorkerParameters)
: CoroutineWorker(context, params) { : SessionSafeCoroutineWorker<MultipleEventSendingDispatcherWorker.Params>(context, params, Params::class.java) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class Params( internal data class Params(
override val sessionId: String, override val sessionId: String,
val events: List<Event>, val localEchoIds: List<LocalEchoIdentifiers>,
val isEncrypted: Boolean, val isEncrypted: Boolean,
override val lastFailureMessage: String? = null override val lastFailureMessage: String? = null
) : SessionWorkerParams ) : SessionWorkerParams
@ -57,46 +56,48 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo
@Inject lateinit var timelineSendEventWorkCommon: TimelineSendEventWorkCommon @Inject lateinit var timelineSendEventWorkCommon: TimelineSendEventWorkCommon
@Inject lateinit var localEchoRepository: LocalEchoRepository @Inject lateinit var localEchoRepository: LocalEchoRepository
override suspend fun doWork(): Result { override fun doOnError(params: Params): Result {
Timber.v("## SendEvent: Start dispatch sending multiple event work") params.localEchoIds.forEach { localEchoIds ->
val params = WorkerParamsFactory.fromData<Params>(inputData) localEchoRepository.updateSendState(localEchoIds.eventId, SendState.UNDELIVERED)
?: return Result.success()
.also { Timber.e("Unable to parse work parameters") }
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
sessionComponent.inject(this)
if (params.lastFailureMessage != null) {
params.events.forEach { event ->
event.eventId?.let { localEchoRepository.updateSendState(it, SendState.UNDELIVERED) }
}
// Transmit the error if needed?
return Result.success(inputData)
.also { Timber.e("## SendEvent: Work cancelled due to input error from parent ${params.lastFailureMessage}") }
} }
return super.doOnError(params)
}
override fun injectWith(injector: SessionComponent) {
injector.inject(this)
}
override suspend fun doSafeWork(params: Params): Result {
Timber.v("## SendEvent: Start dispatch sending multiple event work")
// Create a work for every event // Create a work for every event
params.events.forEach { event -> params.localEchoIds.forEach { localEchoIds ->
val roomId = localEchoIds.roomId
val eventId = localEchoIds.eventId
if (params.isEncrypted) { if (params.isEncrypted) {
localEchoRepository.updateSendState(event.eventId ?: "", SendState.ENCRYPTING) localEchoRepository.updateSendState(eventId, SendState.ENCRYPTING)
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule encrypt and send event ${event.eventId}") Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule encrypt and send event $eventId")
val encryptWork = createEncryptEventWork(params.sessionId, event, true) val encryptWork = createEncryptEventWork(params.sessionId, eventId, true)
// Note that event will be replaced by the result of the previous work // Note that event will be replaced by the result of the previous work
val sendWork = createSendEventWork(params.sessionId, event, false) val sendWork = createSendEventWork(params.sessionId, eventId, false)
timelineSendEventWorkCommon.postSequentialWorks(event.roomId!!, encryptWork, sendWork) timelineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, sendWork)
} else { } else {
localEchoRepository.updateSendState(event.eventId ?: "", SendState.SENDING) localEchoRepository.updateSendState(eventId, SendState.SENDING)
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule send event ${event.eventId}") Timber.v("## SendEvent: [${System.currentTimeMillis()}] Schedule send event $eventId")
val sendWork = createSendEventWork(params.sessionId, event, true) val sendWork = createSendEventWork(params.sessionId, eventId, true)
timelineSendEventWorkCommon.postWork(event.roomId!!, sendWork) timelineSendEventWorkCommon.postWork(roomId, sendWork)
} }
} }
return Result.success() return Result.success()
} }
private fun createEncryptEventWork(sessionId: String, event: Event, startChain: Boolean): OneTimeWorkRequest { override fun buildErrorParams(params: Params, message: String): Params {
val params = EncryptEventWorker.Params(sessionId, event) return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
}
private fun createEncryptEventWork(sessionId: String, eventId: String, startChain: Boolean): OneTimeWorkRequest {
val params = EncryptEventWorker.Params(sessionId, eventId)
val sendWorkData = WorkerParamsFactory.toData(params) val sendWorkData = WorkerParamsFactory.toData(params)
return workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>() return workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>()
@ -107,8 +108,8 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo
.build() .build()
} }
private fun createSendEventWork(sessionId: String, event: Event, startChain: Boolean): OneTimeWorkRequest { private fun createSendEventWork(sessionId: String, eventId: String, startChain: Boolean): OneTimeWorkRequest {
val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, event = event) val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, eventId = eventId)
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
return timelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain) return timelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)

View File

@ -17,24 +17,24 @@
package org.matrix.android.sdk.internal.session.room.send package org.matrix.android.sdk.internal.session.room.send
import android.content.Context import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import org.matrix.android.sdk.internal.worker.getSessionComponent
import org.greenrobot.eventbus.EventBus
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
/** /**
* Possible previous worker: None * Possible previous worker: None
* Possible next worker : None * Possible next worker : None
*/ */
internal class RedactEventWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { internal class RedactEventWorker(context: Context, params: WorkerParameters)
: SessionSafeCoroutineWorker<RedactEventWorker.Params>(context, params, Params::class.java) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class Params( internal data class Params(
@ -49,20 +49,11 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters) : C
@Inject lateinit var roomAPI: RoomAPI @Inject lateinit var roomAPI: RoomAPI
@Inject lateinit var eventBus: EventBus @Inject lateinit var eventBus: EventBus
override suspend fun doWork(): Result { override fun injectWith(injector: SessionComponent) {
val params = WorkerParamsFactory.fromData<Params>(inputData) injector.inject(this)
?: return Result.failure() }
.also { Timber.e("Unable to parse work parameters") }
if (params.lastFailureMessage != null) {
// Transmit the error
return Result.success(inputData)
.also { Timber.e("Work cancelled due to input error from parent") }
}
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
sessionComponent.inject(this)
override suspend fun doSafeWork(params: Params): Result {
val eventId = params.eventId val eventId = params.eventId
return runCatching { return runCatching {
executeRequest<SendResponse>(eventBus) { executeRequest<SendResponse>(eventBus) {
@ -91,4 +82,8 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters) : C
} }
) )
} }
override fun buildErrorParams(params: Params, message: String): Params {
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
}
} }

View File

@ -56,7 +56,7 @@ internal class RoomEventSender @Inject constructor(
private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
// Same parameter // Same parameter
val params = EncryptEventWorker.Params(sessionId, event) val params = EncryptEventWorker.Params(sessionId, event.eventId!!)
val sendWorkData = WorkerParamsFactory.toData(params) val sendWorkData = WorkerParamsFactory.toData(params)
return workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>() return workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>()
@ -68,7 +68,7 @@ internal class RoomEventSender @Inject constructor(
} }
private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, event = event) val sendContentWorkerParams = SendEventWorker.Params(sessionId = sessionId, eventId = event.eventId!!)
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
return timelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain) return timelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)

View File

@ -18,19 +18,19 @@
package org.matrix.android.sdk.internal.session.room.send package org.matrix.android.sdk.internal.session.room.send
import android.content.Context import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import io.realm.RealmConfiguration
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.failure.shouldBeRetried import org.matrix.android.sdk.api.failure.shouldBeRetried
import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.session.room.RoomAPI
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import org.matrix.android.sdk.internal.worker.getSessionComponent
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -42,35 +42,29 @@ import javax.inject.Inject
*/ */
internal class SendEventWorker(context: Context, internal class SendEventWorker(context: Context,
params: WorkerParameters) params: WorkerParameters)
: CoroutineWorker(context, params) { : SessionSafeCoroutineWorker<SendEventWorker.Params>(context, params, Params::class.java) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class Params( internal data class Params(
override val sessionId: String, override val sessionId: String,
override val lastFailureMessage: String? = null, override val lastFailureMessage: String? = null,
val event: Event? = null, val eventId: String
// Keep for compat at the moment, will be removed later
val eventId: String? = null
) : SessionWorkerParams ) : SessionWorkerParams
@Inject lateinit var localEchoRepository: LocalEchoRepository @Inject lateinit var localEchoRepository: LocalEchoRepository
@Inject lateinit var roomAPI: RoomAPI @Inject lateinit var roomAPI: RoomAPI
@Inject lateinit var eventBus: EventBus @Inject lateinit var eventBus: EventBus
@Inject lateinit var cancelSendTracker: CancelSendTracker @Inject lateinit var cancelSendTracker: CancelSendTracker
@SessionDatabase @Inject lateinit var realmConfiguration: RealmConfiguration
override suspend fun doWork(): Result { override fun injectWith(injector: SessionComponent) {
val params = WorkerParamsFactory.fromData<Params>(inputData) injector.inject(this)
?: return Result.success() }
.also { Timber.e("## SendEvent: Unable to parse work parameters") }
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
sessionComponent.inject(this)
val event = params.event override suspend fun doSafeWork(params: Params): Result {
val event = localEchoRepository.getUpToDateEcho(params.eventId)
if (event?.eventId == null || event.roomId == null) { if (event?.eventId == null || event.roomId == null) {
// Old way of sending localEchoRepository.updateSendState(params.eventId, SendState.UNDELIVERED)
if (params.eventId != null) {
localEchoRepository.updateSendState(params.eventId, SendState.UNDELIVERED)
}
return Result.success() return Result.success()
.also { Timber.e("Work cancelled due to bad input data") } .also { Timber.e("Work cancelled due to bad input data") }
} }
@ -106,6 +100,10 @@ internal class SendEventWorker(context: Context,
} }
} }
override fun buildErrorParams(params: Params, message: String): Params {
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
}
private suspend fun sendEvent(eventId: String, roomId: String, type: String, content: Content?) { private suspend fun sendEvent(eventId: String, roomId: String, type: String, content: Content?) {
localEchoRepository.updateSendState(eventId, SendState.SENDING) localEchoRepository.updateSendState(eventId, SendState.SENDING)
executeRequest<SendResponse>(eventBus) { executeRequest<SendResponse>(eventBus) {

View File

@ -20,24 +20,26 @@ package org.matrix.android.sdk.internal.session.room.state
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.kotlin.where
import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.database.RealmSessionProvider
import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.query.process import org.matrix.android.sdk.internal.query.process
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.kotlin.where
import javax.inject.Inject import javax.inject.Inject
internal class StateEventDataSource @Inject constructor(@SessionDatabase private val monarchy: Monarchy) { internal class StateEventDataSource @Inject constructor(@SessionDatabase private val monarchy: Monarchy,
private val realmSessionProvider: RealmSessionProvider) {
fun getStateEvent(roomId: String, eventType: String, stateKey: QueryStringValue): Event? { fun getStateEvent(roomId: String, eventType: String, stateKey: QueryStringValue): Event? {
return Realm.getInstance(monarchy.realmConfiguration).use { realm -> return realmSessionProvider.withRealm { realm ->
buildStateEventQuery(realm, roomId, setOf(eventType), stateKey).findFirst()?.root?.asDomain() buildStateEventQuery(realm, roomId, setOf(eventType), stateKey).findFirst()?.root?.asDomain()
} }
} }
@ -53,7 +55,7 @@ internal class StateEventDataSource @Inject constructor(@SessionDatabase private
} }
fun getStateEvents(roomId: String, eventTypes: Set<String>, stateKey: QueryStringValue): List<Event> { fun getStateEvents(roomId: String, eventTypes: Set<String>, stateKey: QueryStringValue): List<Event> {
return Realm.getInstance(monarchy.realmConfiguration).use { realm -> return realmSessionProvider.withRealm { realm ->
buildStateEventQuery(realm, roomId, eventTypes, stateKey) buildStateEventQuery(realm, roomId, eventTypes, stateKey)
.findAll() .findAll()
.mapNotNull { .mapNotNull {

View File

@ -0,0 +1,43 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.room.summary
import io.realm.Realm
import org.matrix.android.sdk.api.session.room.summary.RoomSummaryConstants
import org.matrix.android.sdk.api.session.room.timeline.TimelineEventFilters
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.query.latestEvent
internal object RoomSummaryEventsHelper {
private val previewFilters = TimelineEventFilters(
filterTypes = true,
allowedTypes = RoomSummaryConstants.PREVIEWABLE_TYPES,
filterUseless = true,
filterRedacted = false,
filterEdits = true
)
fun getLatestPreviewableEvent(realm: Realm, roomId: String): TimelineEventEntity? {
return TimelineEventEntity.latestEvent(
realm = realm,
roomId = roomId,
includesSending = true,
filters = previewFilters
)
}
}

View File

@ -18,6 +18,8 @@
package org.matrix.android.sdk.internal.session.room.summary package org.matrix.android.sdk.internal.session.room.summary
import dagger.Lazy import dagger.Lazy
import io.realm.Realm
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
@ -40,7 +42,6 @@ import org.matrix.android.sdk.internal.database.query.findAllInRoomWithSendState
import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.getOrNull import org.matrix.android.sdk.internal.database.query.getOrNull
import org.matrix.android.sdk.internal.database.query.isEventRead import org.matrix.android.sdk.internal.database.query.isEventRead
import org.matrix.android.sdk.internal.database.query.latestEvent
import org.matrix.android.sdk.internal.database.query.whereType import org.matrix.android.sdk.internal.database.query.whereType
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.session.room.RoomAvatarResolver import org.matrix.android.sdk.internal.session.room.RoomAvatarResolver
@ -49,8 +50,6 @@ import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper
import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor import org.matrix.android.sdk.internal.session.room.timeline.TimelineEventDecryptor
import org.matrix.android.sdk.internal.session.sync.model.RoomSyncSummary import org.matrix.android.sdk.internal.session.sync.model.RoomSyncSummary
import org.matrix.android.sdk.internal.session.sync.model.RoomSyncUnreadNotifications import org.matrix.android.sdk.internal.session.sync.model.RoomSyncUnreadNotifications
import io.realm.Realm
import org.greenrobot.eventbus.EventBus
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -61,28 +60,6 @@ internal class RoomSummaryUpdater @Inject constructor(
private val timelineEventDecryptor: Lazy<TimelineEventDecryptor>, private val timelineEventDecryptor: Lazy<TimelineEventDecryptor>,
private val eventBus: EventBus) { private val eventBus: EventBus) {
companion object {
// TODO: maybe allow user of SDK to give that list
val PREVIEWABLE_TYPES = listOf(
// TODO filter message type (KEY_VERIFICATION_READY, etc.)
EventType.MESSAGE,
EventType.STATE_ROOM_NAME,
EventType.STATE_ROOM_TOPIC,
EventType.STATE_ROOM_AVATAR,
EventType.STATE_ROOM_MEMBER,
EventType.STATE_ROOM_HISTORY_VISIBILITY,
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
EventType.CALL_ANSWER,
EventType.ENCRYPTED,
EventType.STATE_ROOM_ENCRYPTION,
EventType.STATE_ROOM_THIRD_PARTY_INVITE,
EventType.STICKER,
EventType.REACTION,
EventType.STATE_ROOM_CREATE
)
}
fun update(realm: Realm, fun update(realm: Realm,
roomId: String, roomId: String,
membership: Membership? = null, membership: Membership? = null,
@ -110,9 +87,6 @@ internal class RoomSummaryUpdater @Inject constructor(
roomSummaryEntity.membership = membership roomSummaryEntity.membership = membership
} }
val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true,
filterTypes = PREVIEWABLE_TYPES, filterContentRelation = true)
val lastNameEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_NAME, stateKey = "")?.root val lastNameEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_NAME, stateKey = "")?.root
val lastTopicEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_TOPIC, stateKey = "")?.root val lastTopicEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_TOPIC, stateKey = "")?.root
val lastCanonicalAliasEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_CANONICAL_ALIAS, stateKey = "")?.root val lastCanonicalAliasEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_CANONICAL_ALIAS, stateKey = "")?.root
@ -123,6 +97,8 @@ internal class RoomSummaryUpdater @Inject constructor(
.contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"") .contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
.findFirst() .findFirst()
val latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0
// avoid this call if we are sure there are unread events // avoid this call if we are sure there are unread events
|| !isEventRead(realm.configuration, userId, roomId, latestPreviewableEvent?.eventId) || !isEventRead(realm.configuration, userId, roomId, latestPreviewableEvent?.eventId)
@ -178,8 +154,7 @@ internal class RoomSummaryUpdater @Inject constructor(
fun updateSendingInformation(realm: Realm, roomId: String) { fun updateSendingInformation(realm: Realm, roomId: String) {
val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId) val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId)
roomSummaryEntity.updateHasFailedSending() roomSummaryEntity.updateHasFailedSending()
roomSummaryEntity.latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, roomSummaryEntity.latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
filterTypes = PREVIEWABLE_TYPES, filterContentRelation = true)
} }
fun updateShieldTrust(realm: Realm, fun updateShieldTrust(realm: Realm,

View File

@ -17,33 +17,6 @@
package org.matrix.android.sdk.internal.session.room.timeline package org.matrix.android.sdk.internal.session.room.timeline
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import org.matrix.android.sdk.api.util.CancelableBag
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
import org.matrix.android.sdk.internal.database.model.ChunkEntity
import org.matrix.android.sdk.internal.database.model.ChunkEntityFields
import org.matrix.android.sdk.internal.database.model.RoomEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
import org.matrix.android.sdk.internal.database.query.TimelineEventFilter
import org.matrix.android.sdk.internal.database.query.findAllInRoomWithSendStates
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.database.query.whereRoomId
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.configureWith
import org.matrix.android.sdk.internal.util.Debouncer
import org.matrix.android.sdk.internal.util.createBackgroundHandler
import org.matrix.android.sdk.internal.util.createUIHandler
import io.realm.OrderedCollectionChangeSet import io.realm.OrderedCollectionChangeSet
import io.realm.OrderedRealmCollectionChangeListener import io.realm.OrderedRealmCollectionChangeListener
import io.realm.Realm import io.realm.Realm
@ -54,6 +27,35 @@ import io.realm.Sort
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import org.matrix.android.sdk.api.util.CancelableBag
import org.matrix.android.sdk.internal.database.RealmSessionProvider
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
import org.matrix.android.sdk.internal.database.model.ChunkEntity
import org.matrix.android.sdk.internal.database.model.ChunkEntityFields
import org.matrix.android.sdk.internal.database.model.RoomEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
import org.matrix.android.sdk.internal.database.query.filterEvents
import org.matrix.android.sdk.internal.database.query.findAllInRoomWithSendStates
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.database.query.whereRoomId
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.configureWith
import org.matrix.android.sdk.internal.util.Debouncer
import org.matrix.android.sdk.internal.util.createBackgroundHandler
import org.matrix.android.sdk.internal.util.createUIHandler
import timber.log.Timber import timber.log.Timber
import java.util.Collections import java.util.Collections
import java.util.UUID import java.util.UUID
@ -76,7 +78,8 @@ internal class DefaultTimeline(
private val settings: TimelineSettings, private val settings: TimelineSettings,
private val hiddenReadReceipts: TimelineHiddenReadReceipts, private val hiddenReadReceipts: TimelineHiddenReadReceipts,
private val eventBus: EventBus, private val eventBus: EventBus,
private val eventDecryptor: TimelineEventDecryptor private val eventDecryptor: TimelineEventDecryptor,
private val realmSessionProvider: RealmSessionProvider
) : Timeline, TimelineHiddenReadReceipts.Delegate { ) : Timeline, TimelineHiddenReadReceipts.Delegate {
data class OnNewTimelineEvents(val roomId: String, val eventIds: List<String>) data class OnNewTimelineEvents(val roomId: String, val eventIds: List<String>)
@ -136,13 +139,13 @@ internal class DefaultTimeline(
} }
override fun pendingEventCount(): Int { override fun pendingEventCount(): Int {
return Realm.getInstance(realmConfiguration).use { return realmSessionProvider.withRealm {
RoomEntity.where(it, roomId).findFirst()?.sendingTimelineEvents?.count() ?: 0 RoomEntity.where(it, roomId).findFirst()?.sendingTimelineEvents?.count() ?: 0
} }
} }
override fun failedToDeliverEventCount(): Int { override fun failedToDeliverEventCount(): Int {
return Realm.getInstance(realmConfiguration).use { return realmSessionProvider.withRealm {
TimelineEventEntity.findAllInRoomWithSendStates(it, roomId, SendState.HAS_FAILED_STATES).count() TimelineEventEntity.findAllInRoomWithSendStates(it, roomId, SendState.HAS_FAILED_STATES).count()
} }
} }
@ -182,7 +185,7 @@ internal class DefaultTimeline(
} }
private fun TimelineSettings.shouldHandleHiddenReadReceipts(): Boolean { private fun TimelineSettings.shouldHandleHiddenReadReceipts(): Boolean {
return buildReadReceipts && (filterEdits || filterTypes) return buildReadReceipts && (filters.filterEdits || filters.filterTypes)
} }
override fun dispose() { override fun dispose() {
@ -239,7 +242,7 @@ internal class DefaultTimeline(
return eventId return eventId
} }
// Otherwise, we should check if the event is in the db, but is hidden because of filters // Otherwise, we should check if the event is in the db, but is hidden because of filters
return Realm.getInstance(realmConfiguration).use { localRealm -> return realmSessionProvider.withRealm { localRealm ->
val nonFilteredEvents = buildEventQuery(localRealm) val nonFilteredEvents = buildEventQuery(localRealm)
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING)
.findAll() .findAll()
@ -317,23 +320,36 @@ internal class DefaultTimeline(
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
fun onLocalEchoCreated(onLocalEchoCreated: OnLocalEchoCreated) { fun onLocalEchoCreated(onLocalEchoCreated: OnLocalEchoCreated) {
if (isLive && onLocalEchoCreated.roomId == roomId) { if (isLive && onLocalEchoCreated.roomId == roomId) {
listeners.forEach { // do not add events that would have been filtered
it.onNewTimelineEvents(listOf(onLocalEchoCreated.timelineEvent.eventId)) if (listOf(onLocalEchoCreated.timelineEvent).filterEventsWithSettings().isNotEmpty()) {
listeners.forEach {
it.onNewTimelineEvents(listOf(onLocalEchoCreated.timelineEvent.eventId))
}
Timber.v("On local echo created: ${onLocalEchoCreated.timelineEvent.eventId}")
inMemorySendingEvents.add(0, onLocalEchoCreated.timelineEvent)
postSnapshot()
} }
Timber.v("On local echo created: $onLocalEchoCreated")
inMemorySendingEvents.add(0, onLocalEchoCreated.timelineEvent)
postSnapshot()
} }
} }
// Private methods ***************************************************************************** // 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 tryOrNull {
// Update the relation of existing event builtEventsIdMap[eventId]?.let { builtIndex ->
builtEvents[builtIndex]?.let { te -> // Update the relation of existing event
builtEvents[builtIndex] = builder(te) builtEvents[builtIndex]?.let { te ->
true val rebuiltEvent = builder(te)
// If rebuilt event is filtered its returned as null and should be removed.
if (rebuiltEvent == null) {
builtEventsIdMap.remove(eventId)
builtEventsIdMap.entries.filter { it.value > builtIndex }.forEach { it.setValue(it.value - 1) }
builtEvents.removeAt(builtIndex)
} else {
builtEvents[builtIndex] = rebuiltEvent
}
true
}
} }
} ?: false } ?: false
} }
@ -408,14 +424,14 @@ internal class DefaultTimeline(
private fun getState(direction: Timeline.Direction): State { private fun getState(direction: Timeline.Direction): State {
return when (direction) { return when (direction) {
Timeline.Direction.FORWARDS -> forwardsState.get() Timeline.Direction.FORWARDS -> forwardsState.get()
Timeline.Direction.BACKWARDS -> backwardsState.get() Timeline.Direction.BACKWARDS -> backwardsState.get()
} }
} }
private fun updateState(direction: Timeline.Direction, update: (State) -> State) { private fun updateState(direction: Timeline.Direction, update: (State) -> State) {
val stateReference = when (direction) { val stateReference = when (direction) {
Timeline.Direction.FORWARDS -> forwardsState Timeline.Direction.FORWARDS -> forwardsState
Timeline.Direction.BACKWARDS -> backwardsState Timeline.Direction.BACKWARDS -> backwardsState
} }
val currentValue = stateReference.get() val currentValue = stateReference.get()
@ -484,7 +500,8 @@ internal class DefaultTimeline(
val eventEntity = results[index] val eventEntity = results[index]
eventEntity?.eventId?.let { eventId -> eventEntity?.eventId?.let { eventId ->
postSnapshot = rebuildEvent(eventId) { postSnapshot = rebuildEvent(eventId) {
buildTimelineEvent(eventEntity) val builtEvent = buildTimelineEvent(eventEntity)
listOf(builtEvent).filterEventsWithSettings().firstOrNull()
} || postSnapshot } || postSnapshot
} }
} }
@ -725,10 +742,10 @@ internal class DefaultTimeline(
return object : MatrixCallback<TokenChunkEventPersistor.Result> { return object : MatrixCallback<TokenChunkEventPersistor.Result> {
override fun onSuccess(data: TokenChunkEventPersistor.Result) { override fun onSuccess(data: TokenChunkEventPersistor.Result) {
when (data) { when (data) {
TokenChunkEventPersistor.Result.SUCCESS -> { TokenChunkEventPersistor.Result.SUCCESS -> {
Timber.v("Success fetching $limit items $direction from pagination request") Timber.v("Success fetching $limit items $direction from pagination request")
} }
TokenChunkEventPersistor.Result.REACHED_END -> { TokenChunkEventPersistor.Result.REACHED_END -> {
postSnapshot() postSnapshot()
} }
TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE -> TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE ->
@ -754,39 +771,24 @@ internal class DefaultTimeline(
} }
private fun RealmQuery<TimelineEventEntity>.filterEventsWithSettings(): RealmQuery<TimelineEventEntity> { private fun RealmQuery<TimelineEventEntity>.filterEventsWithSettings(): RealmQuery<TimelineEventEntity> {
if (settings.filterTypes) { return filterEvents(settings.filters)
`in`(TimelineEventEntityFields.ROOT.TYPE, settings.allowedTypes.toTypedArray())
}
if (settings.filterUseless) {
not()
.equalTo(TimelineEventEntityFields.ROOT.IS_USELESS, true)
}
if (settings.filterEdits) {
not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT)
not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.RESPONSE)
}
if (settings.filterRedacted) {
not().like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, TimelineEventFilter.Unsigned.REDACTED)
}
return this
} }
private fun List<TimelineEvent>.filterEventsWithSettings(): List<TimelineEvent> { private fun List<TimelineEvent>.filterEventsWithSettings(): List<TimelineEvent> {
return filter { return filter {
val filterType = !settings.filterTypes || settings.allowedTypes.contains(it.root.type) val filterType = !settings.filters.filterTypes || settings.filters.allowedTypes.contains(it.root.type)
if (!filterType) return@filter false if (!filterType) return@filter false
val filterEdits = if (settings.filterEdits && it.root.type == EventType.MESSAGE) { val filterEdits = if (settings.filters.filterEdits && it.root.type == EventType.MESSAGE) {
val messageContent = it.root.content.toModel<MessageContent>() val messageContent = it.root.content.toModel<MessageContent>()
messageContent?.relatesTo?.type != RelationType.REPLACE messageContent?.relatesTo?.type != RelationType.REPLACE && messageContent?.relatesTo?.type != RelationType.RESPONSE
} else { } else {
true true
} }
if (!filterEdits) return@filter false if (!filterEdits) return@filter false
val filterRedacted = !settings.filterRedacted || it.root.isRedacted() val filterRedacted = settings.filters.filterRedacted && it.root.isRedacted()
!filterRedacted
filterRedacted
} }
} }

View File

@ -22,6 +22,9 @@ import androidx.lifecycle.Transformations
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import io.realm.Sort
import io.realm.kotlin.where
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.session.events.model.isImageMessage import org.matrix.android.sdk.api.session.events.model.isImageMessage
import org.matrix.android.sdk.api.session.events.model.isVideoMessage import org.matrix.android.sdk.api.session.events.model.isVideoMessage
import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.Timeline
@ -30,7 +33,7 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineService
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.crypto.store.db.doWithRealm import org.matrix.android.sdk.internal.database.RealmSessionProvider
import org.matrix.android.sdk.internal.database.mapper.ReadReceiptsSummaryMapper import org.matrix.android.sdk.internal.database.mapper.ReadReceiptsSummaryMapper
import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
@ -38,13 +41,10 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.util.fetchCopyMap
import io.realm.Sort
import io.realm.kotlin.where
import org.greenrobot.eventbus.EventBus
internal class DefaultTimelineService @AssistedInject constructor(@Assisted private val roomId: String, internal class DefaultTimelineService @AssistedInject constructor(@Assisted private val roomId: String,
@SessionDatabase private val monarchy: Monarchy, @SessionDatabase private val monarchy: Monarchy,
private val realmSessionProvider: RealmSessionProvider,
private val eventBus: EventBus, private val eventBus: EventBus,
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val contextOfEventTask: GetContextOfEventTask, private val contextOfEventTask: GetContextOfEventTask,
@ -73,17 +73,17 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
hiddenReadReceipts = TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings), hiddenReadReceipts = TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings),
eventBus = eventBus, eventBus = eventBus,
eventDecryptor = eventDecryptor, eventDecryptor = eventDecryptor,
fetchTokenAndPaginateTask = fetchTokenAndPaginateTask fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
realmSessionProvider = realmSessionProvider
) )
} }
override fun getTimeLineEvent(eventId: String): TimelineEvent? { override fun getTimeLineEvent(eventId: String): TimelineEvent? {
return monarchy return realmSessionProvider.withRealm { realm ->
.fetchCopyMap({ TimelineEventEntity.where(realm, roomId = roomId, eventId = eventId).findFirst()?.let {
TimelineEventEntity.where(it, roomId = roomId, eventId = eventId).findFirst() timelineEventMapper.map(it)
}, { entity, _ -> }
timelineEventMapper.map(entity) }
})
} }
override fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>> { override fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>> {
@ -98,7 +98,7 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
override fun getAttachmentMessages(): List<TimelineEvent> { override fun getAttachmentMessages(): List<TimelineEvent> {
// TODO pretty bad query.. maybe we should denormalize clear type in base? // TODO pretty bad query.. maybe we should denormalize clear type in base?
return doWithRealm(monarchy.realmConfiguration) { realm -> return realmSessionProvider.withRealm { realm ->
realm.where<TimelineEventEntity>() realm.where<TimelineEventEntity>()
.equalTo(TimelineEventEntityFields.ROOM_ID, roomId) .equalTo(TimelineEventEntityFields.ROOM_ID, roomId)
.sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING) .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.ASCENDING)

View File

@ -18,6 +18,10 @@
package org.matrix.android.sdk.internal.session.room.timeline package org.matrix.android.sdk.internal.session.room.timeline
import android.util.SparseArray import android.util.SparseArray
import io.realm.OrderedRealmCollectionChangeListener
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.RealmResults
import org.matrix.android.sdk.api.session.room.model.ReadReceipt import org.matrix.android.sdk.api.session.room.model.ReadReceipt
import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
import org.matrix.android.sdk.internal.database.mapper.ReadReceiptsSummaryMapper import org.matrix.android.sdk.internal.database.mapper.ReadReceiptsSummaryMapper
@ -27,10 +31,6 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields
import org.matrix.android.sdk.internal.database.query.TimelineEventFilter import org.matrix.android.sdk.internal.database.query.TimelineEventFilter
import org.matrix.android.sdk.internal.database.query.whereInRoom import org.matrix.android.sdk.internal.database.query.whereInRoom
import io.realm.OrderedRealmCollectionChangeListener
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.RealmResults
/** /**
* This class is responsible for handling the read receipts for hidden events (check [TimelineSettings] to see filtering). * This class is responsible for handling the read receipts for hidden events (check [TimelineSettings] to see filtering).
@ -151,23 +151,24 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu
private fun RealmQuery<ReadReceiptsSummaryEntity>.filterReceiptsWithSettings(): RealmQuery<ReadReceiptsSummaryEntity> { private fun RealmQuery<ReadReceiptsSummaryEntity>.filterReceiptsWithSettings(): RealmQuery<ReadReceiptsSummaryEntity> {
beginGroup() beginGroup()
var needOr = false var needOr = false
if (settings.filterTypes) { if (settings.filters.filterTypes) {
not().`in`("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.TYPE}", settings.allowedTypes.toTypedArray()) val allowedTypes = settings.filters.allowedTypes.toTypedArray()
not().`in`("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.TYPE}", allowedTypes)
needOr = true needOr = true
} }
if (settings.filterUseless) { if (settings.filters.filterUseless) {
if (needOr) or() if (needOr) or()
equalTo("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.IS_USELESS}", true) equalTo("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.IS_USELESS}", true)
needOr = true needOr = true
} }
if (settings.filterEdits) { if (settings.filters.filterEdits) {
if (needOr) or() if (needOr) or()
like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", TimelineEventFilter.Content.EDIT) like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", TimelineEventFilter.Content.EDIT)
or() or()
like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", TimelineEventFilter.Content.RESPONSE) like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", TimelineEventFilter.Content.RESPONSE)
needOr = true needOr = true
} }
if (settings.filterRedacted) { if (settings.filters.filterRedacted) {
if (needOr) or() if (needOr) or()
like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.UNSIGNED_DATA}", TimelineEventFilter.Unsigned.REDACTED) like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.UNSIGNED_DATA}", TimelineEventFilter.Unsigned.REDACTED)
} }

View File

@ -18,6 +18,7 @@
package org.matrix.android.sdk.internal.session.room.timeline package org.matrix.android.sdk.internal.session.room.timeline
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
@ -32,19 +33,16 @@ import org.matrix.android.sdk.internal.database.model.ChunkEntity
import org.matrix.android.sdk.internal.database.model.EventInsertType import org.matrix.android.sdk.internal.database.model.EventInsertType
import org.matrix.android.sdk.internal.database.model.RoomEntity import org.matrix.android.sdk.internal.database.model.RoomEntity
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore
import org.matrix.android.sdk.internal.database.query.create import org.matrix.android.sdk.internal.database.query.create
import org.matrix.android.sdk.internal.database.query.find import org.matrix.android.sdk.internal.database.query.find
import org.matrix.android.sdk.internal.database.query.findAllIncludingEvents import org.matrix.android.sdk.internal.database.query.findAllIncludingEvents
import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfRoom
import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.latestEvent
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryEventsHelper
import org.matrix.android.sdk.internal.util.awaitTransaction import org.matrix.android.sdk.internal.util.awaitTransaction
import io.realm.Realm
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -177,12 +175,7 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
currentChunk.isLastForward = true currentChunk.isLastForward = true
currentLastForwardChunk?.deleteOnCascade() currentLastForwardChunk?.deleteOnCascade()
RoomSummaryEntity.where(realm, roomId).findFirst()?.apply { RoomSummaryEntity.where(realm, roomId).findFirst()?.apply {
latestPreviewableEvent = TimelineEventEntity.latestEvent( latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
realm,
roomId,
includesSending = true,
filterTypes = RoomSummaryUpdater.PREVIEWABLE_TYPES
)
} }
} }
} else { } else {
@ -249,13 +242,7 @@ internal class TokenChunkEventPersistor @Inject constructor(@SessionDatabase pri
val shouldUpdateSummary = roomSummaryEntity.latestPreviewableEvent == null val shouldUpdateSummary = roomSummaryEntity.latestPreviewableEvent == null
|| (chunksToDelete.isNotEmpty() && currentChunk.isLastForward && direction == PaginationDirection.FORWARDS) || (chunksToDelete.isNotEmpty() && currentChunk.isLastForward && direction == PaginationDirection.FORWARDS)
if (shouldUpdateSummary) { if (shouldUpdateSummary) {
val latestPreviewableEvent = TimelineEventEntity.latestEvent( roomSummaryEntity.latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
realm,
roomId,
includesSending = true,
filterTypes = RoomSummaryUpdater.PREVIEWABLE_TYPES
)
roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent
} }
if (currentChunk.isValid) { if (currentChunk.isValid) {
RoomEntity.where(realm, roomId).findFirst()?.addOrUpdate(currentChunk) RoomEntity.where(realm, roomId).findFirst()?.addOrUpdate(currentChunk)

View File

@ -131,7 +131,7 @@ internal class SyncResponseHandler @Inject constructor(@SessionDatabase private
/** /**
* At the moment we don't get any group data through the sync, so we poll where every hour. * At the moment we don't get any group data through the sync, so we poll where every hour.
You can also force to refetch group data using [Group] API. * You can also force to refetch group data using [Group] API.
*/ */
private fun scheduleGroupDataFetchingIfNeeded(groupsSyncResponse: GroupsSyncResponse) { private fun scheduleGroupDataFetchingIfNeeded(groupsSyncResponse: GroupsSyncResponse) {
val groupIds = ArrayList<String>() val groupIds = ArrayList<String>()

View File

@ -32,7 +32,7 @@ import javax.inject.Inject
internal interface SyncTask : Task<SyncTask.Params, Unit> { internal interface SyncTask : Task<SyncTask.Params, Unit> {
data class Params(var timeout: Long = 30_000L) data class Params(var timeout: Long = 6_000L)
} }
internal class DefaultSyncTask @Inject constructor( internal class DefaultSyncTask @Inject constructor(

View File

@ -19,7 +19,14 @@ package org.matrix.android.sdk.internal.session.sync.job
import android.app.Service import android.app.Service
import android.content.Intent import android.content.Intent
import android.os.IBinder import android.os.IBinder
import android.os.PowerManager
import androidx.core.content.getSystemService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.isTokenError import org.matrix.android.sdk.api.failure.isTokenError
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.sync.SyncState import org.matrix.android.sdk.api.session.sync.SyncState
@ -28,10 +35,6 @@ import org.matrix.android.sdk.internal.session.sync.SyncTask
import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@ -46,6 +49,11 @@ abstract class SyncService : Service() {
private var sessionId: String? = null private var sessionId: String? = null
private var mIsSelfDestroyed: Boolean = false private var mIsSelfDestroyed: Boolean = false
private var syncTimeoutSeconds: Int = 6
private var syncDelaySeconds: Int = 60
private var periodic: Boolean = false
private var preventReschedule: Boolean = false
private var isInitialSync: Boolean = false private var isInitialSync: Boolean = false
private lateinit var session: Session private lateinit var session: Session
private lateinit var syncTask: SyncTask private lateinit var syncTask: SyncTask
@ -59,27 +67,60 @@ abstract class SyncService : Service() {
private val serviceScope = CoroutineScope(SupervisorJob()) private val serviceScope = CoroutineScope(SupervisorJob())
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Timber.i("onStartCommand $intent") Timber.i("## Sync: onStartCommand [$this] $intent with action: ${intent?.action}")
val isInit = initialize(intent)
if (isInit) { // We should start we have to ensure we fulfill contract to show notification
onStart(isInitialSync) // for foreground service (as per design for this service)
doSyncIfNotAlreadyRunning() // TODO can we check if it's really in foreground
} else { onStart(isInitialSync)
// We should start and stop as we have to ensure to call Service.startForeground() when (intent?.action) {
onStart(isInitialSync) ACTION_STOP -> {
stopMe() Timber.i("## Sync: stop command received")
// If it was periodic we ensure that it will not reschedule itself
preventReschedule = true
// we don't want to cancel initial syncs, let it finish
if (!isInitialSync) {
stopMe()
}
}
else -> {
val isInit = initialize(intent)
if (isInit) {
periodic = intent?.getBooleanExtra(EXTRA_PERIODIC, false) ?: false
val onNetworkBack = intent?.getBooleanExtra(EXTRA_NETWORK_BACK_RESTART, false) ?: false
Timber.d("## Sync: command received, periodic: $periodic networkBack: $onNetworkBack")
if (onNetworkBack && !backgroundDetectionObserver.isInBackground) {
// the restart after network occurs while the app is in foreground
// so just stop. It will be restarted when entering background
preventReschedule = true
stopMe()
} else {
// default is syncing
doSyncIfNotAlreadyRunning()
}
} else {
Timber.d("## Sync: Failed to initialize service")
stopMe()
}
}
} }
// No intent just start the service, an alarm will should call with intent
return START_STICKY // It's ok to be not sticky because we will explicitly start it again on the next alarm?
return START_NOT_STICKY
} }
override fun onDestroy() { override fun onDestroy() {
Timber.i("## onDestroy() : $this") Timber.i("## Sync: onDestroy() [$this] periodic:$periodic preventReschedule:$preventReschedule")
if (!mIsSelfDestroyed) { if (!mIsSelfDestroyed) {
Timber.w("## Destroy by the system : $this") Timber.d("## Sync: Destroy by the system : $this")
} }
serviceScope.coroutineContext.cancelChildren()
isRunning.set(false) isRunning.set(false)
// Cancelling the context will trigger the catch close the doSync try
serviceScope.coroutineContext.cancelChildren()
if (!preventReschedule && periodic && sessionId != null && backgroundDetectionObserver.isInBackground) {
Timber.d("## Sync: Reschedule service in $syncDelaySeconds sec")
onRescheduleAsked(sessionId ?: "", false, syncTimeoutSeconds, syncDelaySeconds)
}
super.onDestroy() super.onDestroy()
} }
@ -90,9 +131,15 @@ abstract class SyncService : Service() {
private fun doSyncIfNotAlreadyRunning() { private fun doSyncIfNotAlreadyRunning() {
if (isRunning.get()) { if (isRunning.get()) {
Timber.i("Received a start while was already syncing... ignore") Timber.i("## Sync: Received a start while was already syncing... ignore")
} else { } else {
isRunning.set(true) isRunning.set(true)
// Acquire a lock to give enough time for the sync :/
getSystemService<PowerManager>()?.run {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "riotx:fdroidSynclock").apply {
acquire((syncTimeoutSeconds * 1000L + 10_000L))
}
}
serviceScope.launch(coroutineDispatchers.io) { serviceScope.launch(coroutineDispatchers.io) {
doSync() doSync()
} }
@ -100,9 +147,10 @@ abstract class SyncService : Service() {
} }
private suspend fun doSync() { private suspend fun doSync() {
Timber.v("Execute sync request with timeout 0") Timber.v("## Sync: Execute sync request with timeout $syncTimeoutSeconds seconds")
val params = SyncTask.Params(TIME_OUT) val params = SyncTask.Params(syncTimeoutSeconds * 1000L)
try { try {
// never do that in foreground, let the syncThread work
syncTask.execute(params) syncTask.execute(params)
// Start sync if we were doing an initial sync and the syncThread is not launched yet // Start sync if we were doing an initial sync and the syncThread is not launched yet
if (isInitialSync && session.getSyncState() == SyncState.Idle) { if (isInitialSync && session.getSyncState() == SyncState.Idle) {
@ -111,28 +159,34 @@ abstract class SyncService : Service() {
} }
stopMe() stopMe()
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
Timber.e(throwable) Timber.e(throwable, "## Sync: sync service did fail ${isRunning.get()}")
if (throwable.isTokenError()) { if (throwable.isTokenError()) {
stopMe() // no need to retry
} else { preventReschedule = true
Timber.v("Should be rescheduled to avoid wasting resources")
sessionId?.also {
onRescheduleAsked(it, isInitialSync, delay = 10_000L)
}
stopMe()
} }
if (throwable is Failure.NetworkConnection) {
// Network is off, no need to reschedule endless alarms :/
preventReschedule = true
// Instead start a work to restart background sync when network is back
onNetworkError(sessionId ?: "", isInitialSync, syncTimeoutSeconds, syncDelaySeconds)
}
// JobCancellation could be caught here when onDestroy cancels the coroutine context
if (isRunning.get()) stopMe()
} }
} }
private fun initialize(intent: Intent?): Boolean { private fun initialize(intent: Intent?): Boolean {
if (intent == null) { if (intent == null) {
Timber.d("## Sync: initialize intent is null")
return false return false
} }
val matrix = Matrix.getInstance(applicationContext) val matrix = Matrix.getInstance(applicationContext)
val safeSessionId = intent.getStringExtra(EXTRA_SESSION_ID) ?: return false val safeSessionId = intent.getStringExtra(EXTRA_SESSION_ID) ?: return false
syncTimeoutSeconds = intent.getIntExtra(EXTRA_TIMEOUT_SECONDS, 6)
syncDelaySeconds = intent.getIntExtra(EXTRA_DELAY_SECONDS, 60)
try { try {
val sessionComponent = matrix.sessionManager.getSessionComponent(safeSessionId) val sessionComponent = matrix.sessionManager.getSessionComponent(safeSessionId)
?: throw IllegalStateException("You should have a session to make it work") ?: throw IllegalStateException("## Sync: You should have a session to make it work")
session = sessionComponent.session() session = sessionComponent.session()
sessionId = safeSessionId sessionId = safeSessionId
syncTask = sessionComponent.syncTask() syncTask = sessionComponent.syncTask()
@ -143,14 +197,16 @@ abstract class SyncService : Service() {
backgroundDetectionObserver = matrix.backgroundDetectionObserver backgroundDetectionObserver = matrix.backgroundDetectionObserver
return true return true
} catch (exception: Exception) { } catch (exception: Exception) {
Timber.e(exception, "An exception occurred during initialisation") Timber.e(exception, "## Sync: An exception occurred during initialisation")
return false return false
} }
} }
abstract fun onStart(isInitialSync: Boolean) abstract fun onStart(isInitialSync: Boolean)
abstract fun onRescheduleAsked(sessionId: String, isInitialSync: Boolean, delay: Long) abstract fun onRescheduleAsked(sessionId: String, isInitialSync: Boolean, timeout: Int, delay: Int)
abstract fun onNetworkError(sessionId: String, isInitialSync: Boolean, timeout: Int, delay: Int)
override fun onBind(intent: Intent?): IBinder? { override fun onBind(intent: Intent?): IBinder? {
return null return null
@ -158,6 +214,11 @@ abstract class SyncService : Service() {
companion object { companion object {
const val EXTRA_SESSION_ID = "EXTRA_SESSION_ID" const val EXTRA_SESSION_ID = "EXTRA_SESSION_ID"
private const val TIME_OUT = 0L const val EXTRA_TIMEOUT_SECONDS = "EXTRA_TIMEOUT_SECONDS"
const val EXTRA_DELAY_SECONDS = "EXTRA_DELAY_SECONDS"
const val EXTRA_PERIODIC = "EXTRA_PERIODIC"
const val EXTRA_NETWORK_BACK_RESTART = "EXTRA_NETWORK_BACK_RESTART"
const val ACTION_STOP = "ACTION_STOP"
} }
} }

View File

@ -18,23 +18,24 @@ package org.matrix.android.sdk.internal.session.sync.job
import android.content.Context import android.content.Context
import androidx.work.BackoffPolicy import androidx.work.BackoffPolicy
import androidx.work.CoroutineWorker
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.failure.isTokenError import org.matrix.android.sdk.api.failure.isTokenError
import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.di.WorkManagerProvider
import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker import org.matrix.android.sdk.internal.network.NetworkConnectivityChecker
import org.matrix.android.sdk.internal.session.SessionComponent
import org.matrix.android.sdk.internal.session.sync.SyncTask import org.matrix.android.sdk.internal.session.sync.SyncTask
import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
import org.matrix.android.sdk.internal.worker.SessionWorkerParams import org.matrix.android.sdk.internal.worker.SessionWorkerParams
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import org.matrix.android.sdk.internal.worker.getSessionComponent
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
private const val DEFAULT_LONG_POOL_TIMEOUT = 0L private const val DEFAULT_LONG_POOL_TIMEOUT = 6L
private const val DEFAULT_DELAY_TIMEOUT = 30_000L
/** /**
* Possible previous worker: None * Possible previous worker: None
@ -42,44 +43,59 @@ private const val DEFAULT_LONG_POOL_TIMEOUT = 0L
*/ */
internal class SyncWorker(context: Context, internal class SyncWorker(context: Context,
workerParameters: WorkerParameters workerParameters: WorkerParameters
) : CoroutineWorker(context, workerParameters) { ) : SessionSafeCoroutineWorker<SyncWorker.Params>(context, workerParameters, Params::class.java) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class Params( internal data class Params(
override val sessionId: String, override val sessionId: String,
val timeout: Long = DEFAULT_LONG_POOL_TIMEOUT, val timeout: Long = DEFAULT_LONG_POOL_TIMEOUT,
val automaticallyRetry: Boolean = false, val delay: Long = DEFAULT_DELAY_TIMEOUT,
val periodic: Boolean = false,
override val lastFailureMessage: String? = null override val lastFailureMessage: String? = null
) : SessionWorkerParams ) : SessionWorkerParams
@Inject lateinit var syncTask: SyncTask @Inject lateinit var syncTask: SyncTask
@Inject lateinit var taskExecutor: TaskExecutor @Inject lateinit var taskExecutor: TaskExecutor
@Inject lateinit var networkConnectivityChecker: NetworkConnectivityChecker @Inject lateinit var networkConnectivityChecker: NetworkConnectivityChecker
@Inject lateinit var workManagerProvider: WorkManagerProvider
override suspend fun doWork(): Result { override fun injectWith(injector: SessionComponent) {
injector.inject(this)
}
override suspend fun doSafeWork(params: Params): Result {
Timber.i("Sync work starting") Timber.i("Sync work starting")
val params = WorkerParamsFactory.fromData<Params>(inputData)
?: return Result.success()
.also { Timber.e("Unable to parse work parameters") }
val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success()
sessionComponent.inject(this)
return runCatching { return runCatching {
doSync(params.timeout) doSync(params.timeout)
}.fold( }.fold(
{ Result.success() }, {
Result.success().also {
if (params.periodic) {
// we want to schedule another one after delay
automaticallyBackgroundSync(workManagerProvider, params.sessionId, params.timeout, params.delay)
}
}
},
{ failure -> { failure ->
if (failure.isTokenError() || !params.automaticallyRetry) { if (failure.isTokenError()) {
Result.failure() Result.failure()
} else { } else {
// If the worker was stopped (when going back in foreground), a JobCancellation exception is sent
// but in this case the result is ignored, as the work is considered stopped,
// so don't worry of the retry here for this case
Result.retry() Result.retry()
} }
} }
) )
} }
override fun buildErrorParams(params: Params, message: String): Params {
return params.copy(lastFailureMessage = params.lastFailureMessage ?: message)
}
private suspend fun doSync(timeout: Long) { private suspend fun doSync(timeout: Long) {
val taskParams = SyncTask.Params(timeout) val taskParams = SyncTask.Params(timeout * 1000)
syncTask.execute(taskParams) syncTask.execute(taskParams)
} }
@ -87,25 +103,27 @@ internal class SyncWorker(context: Context,
private const val BG_SYNC_WORK_NAME = "BG_SYNCP" private const val BG_SYNC_WORK_NAME = "BG_SYNCP"
fun requireBackgroundSync(workManagerProvider: WorkManagerProvider, sessionId: String, serverTimeout: Long = 0) { fun requireBackgroundSync(workManagerProvider: WorkManagerProvider, sessionId: String, serverTimeout: Long = 0) {
val data = WorkerParamsFactory.toData(Params(sessionId, serverTimeout, false)) val data = WorkerParamsFactory.toData(Params(sessionId, serverTimeout, 0L, false))
val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SyncWorker>() val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(WorkManagerProvider.workConstraints) .setConstraints(WorkManagerProvider.workConstraints)
.setBackoffCriteria(BackoffPolicy.LINEAR, 1_000, TimeUnit.MILLISECONDS) .setBackoffCriteria(BackoffPolicy.LINEAR, 1_000, TimeUnit.MILLISECONDS)
.setInputData(data) .setInputData(data)
.build() .build()
workManagerProvider.workManager workManagerProvider.workManager
.enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.REPLACE, workRequest) .enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
} }
fun automaticallyBackgroundSync(workManagerProvider: WorkManagerProvider, sessionId: String, serverTimeout: Long = 0, delay: Long = 30_000) { fun automaticallyBackgroundSync(workManagerProvider: WorkManagerProvider, sessionId: String, serverTimeout: Long = 0, delayInSeconds: Long = 30) {
val data = WorkerParamsFactory.toData(Params(sessionId, serverTimeout, true)) val data = WorkerParamsFactory.toData(Params(sessionId, serverTimeout, delayInSeconds, true))
val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SyncWorker>() val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(WorkManagerProvider.workConstraints) .setConstraints(WorkManagerProvider.workConstraints)
.setInputData(data) .setInputData(data)
.setBackoffCriteria(BackoffPolicy.LINEAR, delay, TimeUnit.MILLISECONDS) .setBackoffCriteria(BackoffPolicy.LINEAR, 1_000, TimeUnit.MILLISECONDS)
.setInitialDelay(delayInSeconds, TimeUnit.SECONDS)
.build() .build()
workManagerProvider.workManager workManagerProvider.workManager
.enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.REPLACE, workRequest) .enqueueUniqueWork(BG_SYNC_WORK_NAME, ExistingWorkPolicy.APPEND_OR_REPLACE, workRequest)
} }
fun stopAnyBackgroundSync(workManagerProvider: WorkManagerProvider) { fun stopAnyBackgroundSync(workManagerProvider: WorkManagerProvider) {

View File

@ -23,9 +23,11 @@ import androidx.paging.DataSource
import androidx.paging.LivePagedListBuilder import androidx.paging.LivePagedListBuilder
import androidx.paging.PagedList import androidx.paging.PagedList
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import io.realm.Case
import org.matrix.android.sdk.api.session.user.model.User import org.matrix.android.sdk.api.session.user.model.User
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.database.RealmSessionProvider
import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.model.IgnoredUserEntity import org.matrix.android.sdk.internal.database.model.IgnoredUserEntity
import org.matrix.android.sdk.internal.database.model.IgnoredUserEntityFields import org.matrix.android.sdk.internal.database.model.IgnoredUserEntityFields
@ -33,11 +35,10 @@ import org.matrix.android.sdk.internal.database.model.UserEntity
import org.matrix.android.sdk.internal.database.model.UserEntityFields import org.matrix.android.sdk.internal.database.model.UserEntityFields
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.util.fetchCopied
import io.realm.Case
import javax.inject.Inject import javax.inject.Inject
internal class UserDataSource @Inject constructor(@SessionDatabase private val monarchy: Monarchy) { internal class UserDataSource @Inject constructor(@SessionDatabase private val monarchy: Monarchy,
private val realmSessionProvider: RealmSessionProvider) {
private val realmDataSourceFactory: Monarchy.RealmDataSourceFactory<UserEntity> by lazy { private val realmDataSourceFactory: Monarchy.RealmDataSourceFactory<UserEntity> by lazy {
monarchy.createDataSourceFactory { realm -> monarchy.createDataSourceFactory { realm ->
@ -58,10 +59,10 @@ internal class UserDataSource @Inject constructor(@SessionDatabase private val m
} }
fun getUser(userId: String): User? { fun getUser(userId: String): User? {
val userEntity = monarchy.fetchCopied { UserEntity.where(it, userId).findFirst() } return realmSessionProvider.withRealm {
?: return null val userEntity = UserEntity.where(it, userId).findFirst()
userEntity?.asDomain()
return userEntity.asDomain() }
} }
fun getUserLive(userId: String): LiveData<Optional<User>> { fun getUserLive(userId: String): LiveData<Optional<User>> {

View File

@ -20,18 +20,20 @@ package org.matrix.android.sdk.internal.session.user.accountdata
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import io.realm.Realm
import io.realm.RealmQuery
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.database.RealmSessionProvider
import org.matrix.android.sdk.internal.database.mapper.AccountDataMapper import org.matrix.android.sdk.internal.database.mapper.AccountDataMapper
import org.matrix.android.sdk.internal.database.model.UserAccountDataEntity import org.matrix.android.sdk.internal.database.model.UserAccountDataEntity
import org.matrix.android.sdk.internal.database.model.UserAccountDataEntityFields import org.matrix.android.sdk.internal.database.model.UserAccountDataEntityFields
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
import io.realm.Realm
import io.realm.RealmQuery
import javax.inject.Inject import javax.inject.Inject
internal class AccountDataDataSource @Inject constructor(@SessionDatabase private val monarchy: Monarchy, internal class AccountDataDataSource @Inject constructor(@SessionDatabase private val monarchy: Monarchy,
private val realmSessionProvider: RealmSessionProvider,
private val accountDataMapper: AccountDataMapper) { private val accountDataMapper: AccountDataMapper) {
fun getAccountDataEvent(type: String): UserAccountDataEvent? { fun getAccountDataEvent(type: String): UserAccountDataEvent? {
@ -45,10 +47,9 @@ internal class AccountDataDataSource @Inject constructor(@SessionDatabase privat
} }
fun getAccountDataEvents(types: Set<String>): List<UserAccountDataEvent> { fun getAccountDataEvents(types: Set<String>): List<UserAccountDataEvent> {
return monarchy.fetchAllMappedSync( return realmSessionProvider.withRealm {
{ accountDataEventsQuery(it, types) }, accountDataEventsQuery(it, types).findAll().map(accountDataMapper::map)
accountDataMapper::map }
)
} }
fun getLiveAccountDataEvents(types: Set<String>): LiveData<List<UserAccountDataEvent>> { fun getLiveAccountDataEvents(types: Set<String>): LiveData<List<UserAccountDataEvent>> {

View File

@ -202,6 +202,6 @@ internal class WidgetManager @Inject constructor(private val integrationManager:
stateKey = QueryStringValue.NoCondition stateKey = QueryStringValue.NoCondition
) )
val powerLevelsContent = powerLevelsEvent?.content?.toModel<PowerLevelsContent>() ?: return false val powerLevelsContent = powerLevelsEvent?.content?.toModel<PowerLevelsContent>() ?: return false
return PowerLevelsHelper(powerLevelsContent).isUserAllowedToSend(userId, true, null) return PowerLevelsHelper(powerLevelsContent).isUserAllowedToSend(userId, true, EventType.STATE_ROOM_WIDGET_LEGACY)
} }
} }

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