forked from GitHub-Mirror/riotX-android
Compare commits
501 Commits
feature/fi
...
develop
Author | SHA1 | Date | |
---|---|---|---|
|
f2c8d4ad02 | ||
|
be524472ec | ||
|
1b82a1a24d | ||
|
cf0b331c3b | ||
|
2a92a3dc80 | ||
|
012840abba | ||
|
a5975a099e | ||
|
38da4b9ee5 | ||
|
242e60fcaa | ||
|
a23be05cbf | ||
|
ed39b02924 | ||
|
fe931b5361 | ||
|
90d9cd0587 | ||
|
9cedb18921 | ||
|
e89ba7b87b | ||
|
902657c22a | ||
|
eec2abf164 | ||
|
6879cc8ca8 | ||
|
fd6bbbd3b5 | ||
|
0ff0b014a9 | ||
|
fdc9e84dd5 | ||
|
58f878fca9 | ||
|
88095e4bd9 | ||
|
47d22a3d5e | ||
|
28e82cb8ea | ||
|
35817245cb | ||
|
75266f42bb | ||
|
95c4c9ce56 | ||
|
ce5570105d | ||
|
188a9aebfa | ||
|
c95223f5d2 | ||
|
ef0362ba9c | ||
|
ea242f6737 | ||
|
cbc08d834b | ||
|
0ab6b33fb6 | ||
|
1b394527b6 | ||
|
a8f1388721 | ||
|
166be4e289 | ||
|
b49ccefe63 | ||
|
825760d17e | ||
|
b5af62c3ea | ||
|
a51d96bf00 | ||
|
7e142d201d | ||
|
2be6058971 | ||
|
49d73f360e | ||
|
bd88d85a21 | ||
|
be4fc5cce6 | ||
|
704da1be55 | ||
|
5d002532d3 | ||
|
d4161e9a1a | ||
|
7966ebef03 | ||
|
ed5faca5d2 | ||
|
8ca829d538 | ||
|
e7819ce678 | ||
|
5402902bc2 | ||
|
bc1350aaf5 | ||
|
fd74e3dfb1 | ||
|
e0628da1cb | ||
|
aa4e74e986 | ||
|
6cc0c0672e | ||
|
501474b720 | ||
|
e11c66035c | ||
|
3d2d219d79 | ||
|
63af03bedd | ||
|
d3827b8673 | ||
|
4ca2531e47 | ||
|
4e8dc72439 | ||
|
25a4240a5a | ||
|
b9cfda23b6 | ||
|
06dcf75a32 | ||
|
21deb2551d | ||
|
70639f180c | ||
|
1dbb02a80d | ||
|
825463d9cd | ||
|
c313ce78cb | ||
|
39f58d048b | ||
|
3f792c7a84 | ||
|
347dcb469a | ||
|
79fb1985aa | ||
|
e216cd15a8 | ||
|
37fde374b3 | ||
|
f7b471f141 | ||
|
93fd56a7ca | ||
|
5a9d88e791 | ||
|
eaf6a9923a | ||
|
d98567045c | ||
|
b4ce8748cb | ||
|
9d5433a857 | ||
|
6d4ee83e65 | ||
|
6e44cca17d | ||
|
0a73887c70 | ||
|
7fef063e15 | ||
|
24f391dac0 | ||
|
81c7f694d6 | ||
|
80e2fc0ca3 | ||
|
9f53406e99 | ||
|
3584658c36 | ||
|
12a0cbb400 | ||
|
20437446b4 | ||
|
35229882e3 | ||
|
a04f4421f6 | ||
|
af1e81f65e | ||
|
63f6081fa5 | ||
|
ee2e575211 | ||
|
0949d29f9c | ||
|
23466fb5a4 | ||
|
7f09e64d63 | ||
|
585f0ba4b7 | ||
|
245fbe86d9 | ||
|
456908c851 | ||
|
b79fdf6a85 | ||
|
d9f448c9aa | ||
|
7a6fc4936b | ||
|
d82fd10f3b | ||
|
4009f2c176 | ||
|
15c4b03340 | ||
|
7b5dff3dcf | ||
|
357123743f | ||
|
bb04af1e2c | ||
|
2f94fbd7eb | ||
|
f2a3bdb68e | ||
|
097e9714ff | ||
|
acae0fad3e | ||
|
4deb7eb865 | ||
|
f910cd6f97 | ||
|
652ac81fa1 | ||
|
99f4196388 | ||
|
c0b94f4111 | ||
|
1462fa0484 | ||
|
dafdc1d3ad | ||
|
394b89e76b | ||
|
0db8e7da43 | ||
|
dae8b5c196 | ||
|
215324a03e | ||
|
d3ce4c491c | ||
|
ed6d28bd3b | ||
|
c2e053b62b | ||
|
c450849cc3 | ||
|
fe884dba2d | ||
|
3fa4dbaa25 | ||
|
4a74f58516 | ||
|
c413321a22 | ||
|
d696bd2830 | ||
|
a2b6bd0f62 | ||
|
9cc922a8a2 | ||
|
c36d1bcd06 | ||
|
85499c6b33 | ||
|
8076eab4b5 | ||
|
d47c0f5ebc | ||
|
fd09a1224e | ||
|
c300c50093 | ||
|
77c4355aed | ||
|
1a92562182 | ||
|
9c390dcc0c | ||
|
95089b91b8 | ||
|
eb446d7b49 | ||
|
dc4786ecf0 | ||
|
e245023add | ||
|
90fad23493 | ||
|
000db4b192 | ||
|
2a16c36a59 | ||
|
ef6c1cfc63 | ||
|
4b4156996d | ||
|
087cc0e6e3 | ||
|
f4df27c2dc | ||
|
ab25980c4e | ||
|
77b402ce70 | ||
|
2763fbb496 | ||
|
6deba31111 | ||
|
ff6ce8a4b7 | ||
|
d0cff219aa | ||
|
65f0af918f | ||
|
ac38a6461c | ||
|
9a1e16a170 | ||
|
9e5c70dda3 | ||
|
0255696c88 | ||
|
76a9625f25 | ||
|
5af6bf3762 | ||
|
507bc2f622 | ||
|
125eacb20b | ||
|
6176520805 | ||
|
3aea0a50ca | ||
|
ab87a3caea | ||
|
c58328f94e | ||
|
03974c8bdf | ||
|
151ae7f4dd | ||
|
a34b053efe | ||
|
02e342849f | ||
|
b59017938b | ||
|
2c81e41288 | ||
|
cb44ab547c | ||
|
6d01a570fd | ||
|
4a2bf0d6c6 | ||
|
36af8a6a9f | ||
|
40a68c3e9f | ||
|
1a4ec34bb2 | ||
|
10490e3aa6 | ||
|
cd6624a8a6 | ||
|
3965218bf9 | ||
|
d78ff7ab08 | ||
|
cb274d6a33 | ||
|
c00dbce536 | ||
|
db88caf7fa | ||
|
c3d945d6bb | ||
|
4c128602b2 | ||
|
d609c49b31 | ||
|
001603cf9a | ||
|
d87ee32422 | ||
|
f0671b9e73 | ||
|
e218691bf2 | ||
|
9c67036c08 | ||
|
62657538af | ||
|
5438207fba | ||
|
fe88aaffbd | ||
|
21ba72e5e7 | ||
|
d48ae967bd | ||
|
0afde3b021 | ||
|
49ae954183 | ||
|
64bee91f7a | ||
|
4341b0d0f5 | ||
|
51fdccb393 | ||
|
7e3b300130 | ||
|
a98b324c89 | ||
|
977721881f | ||
|
838003b68a | ||
|
7d41352918 | ||
|
077396a832 | ||
|
32b79bd50e | ||
|
844f6d16a4 | ||
|
fc9ef579ca | ||
|
77fa5af1b8 | ||
|
2948018453 | ||
|
90d25ff45e | ||
|
173452d38c | ||
|
a9f9083745 | ||
|
22dc2a6790 | ||
|
927cd7285d | ||
|
4d5bdecec6 | ||
|
0be987ac0d | ||
|
4bfaa00be4 | ||
|
8e78d8a58d | ||
|
e3e86c0a41 | ||
|
8a5fddd952 | ||
|
208460850e | ||
|
0ddef67cc9 | ||
|
896e582a9c | ||
|
477920f411 | ||
|
c647648e79 | ||
|
b654025a3b | ||
|
786a7d7560 | ||
|
b935b9311e | ||
|
8e12f71535 | ||
|
7eea2ccfb4 | ||
|
c32ef02a12 | ||
|
3651ec4870 | ||
|
87de7bd3e6 | ||
|
9494174c33 | ||
|
b7e0b400fb | ||
|
a8f06f609b | ||
|
d469299f42 | ||
|
9bdea5b325 | ||
|
2f01ad99b3 | ||
|
bb3b5788ba | ||
|
45f7d3e9c4 | ||
|
0f7a56d005 | ||
|
63d2861bc8 | ||
|
6bbc784c29 | ||
|
c6fd625761 | ||
|
d8092abc4e | ||
|
6effb90361 | ||
|
42584fc55a | ||
|
30d9ddb3e8 | ||
|
020c32bb1a | ||
|
efd973208f | ||
|
30a6c98c08 | ||
|
1440080d04 | ||
|
61bb4c0427 | ||
|
3c25088243 | ||
|
fc1c0caea3 | ||
|
8901a5e09a | ||
|
25f1d21bc7 | ||
|
4d2ab9fa31 | ||
|
0289d2ee87 | ||
|
222201cc64 | ||
|
b15dea6de3 | ||
|
2ba83e456d | ||
|
1822fc4fbb | ||
|
e6dd1fbfec | ||
|
e2ea76f871 | ||
|
9182f2ce4e | ||
|
34d14eb304 | ||
|
3625c462f0 | ||
|
fe69206340 | ||
|
f9885fd04c | ||
|
316c8ec27e | ||
|
41465450d8 | ||
|
bd009caaf1 | ||
|
33252c3b65 | ||
|
10e4d0190f | ||
|
b77310fe92 | ||
|
919dec4a56 | ||
|
43b3680774 | ||
|
bfb5fce809 | ||
|
1f3731aae7 | ||
|
52dced43ff | ||
|
ff80c3c8d5 | ||
|
34e4d27573 | ||
|
6522148e63 | ||
|
252b2ea30a | ||
|
f493ce44f2 | ||
|
c4c5069ee5 | ||
|
423125b5d9 | ||
|
9e3d29b7d7 | ||
|
f65becf7c0 | ||
|
80a61cf6b5 | ||
|
77056aff94 | ||
|
65e123d87f | ||
|
d0b145d031 | ||
|
98306e223b | ||
|
c9fe1adb77 | ||
|
1b95336ad3 | ||
|
f007fb04b8 | ||
|
141434e8f8 | ||
|
b8669d5ed2 | ||
|
7a08a11b19 | ||
|
54b1d18812 | ||
|
3aa30e5f15 | ||
|
ddf4a81905 | ||
|
794fd650a4 | ||
|
9a57a02996 | ||
|
7e8cd07e1e | ||
|
d613abf4b4 | ||
|
06699eaefc | ||
|
e5082f662c | ||
|
c8ab53e39c | ||
|
d424a135a9 | ||
|
e6409d4c60 | ||
|
19c7de687e | ||
|
1918302297 | ||
|
92e3a02389 | ||
|
0a54801fcc | ||
|
228ee52563 | ||
|
e6c74dc1fe | ||
|
fe82ad2002 | ||
|
f66739491a | ||
|
c5dc9d4a9a | ||
|
8f858f8119 | ||
|
6e036c24b8 | ||
|
5e832e07cd | ||
|
e9700e04d8 | ||
|
c19b1f917f | ||
|
4281b5967a | ||
|
aa743d8469 | ||
|
a09850b16c | ||
|
6cb94dd4d6 | ||
|
c9931e3ba3 | ||
|
fc302c1b5a | ||
|
34ac987494 | ||
|
24b2387703 | ||
|
8a0c9ae9b0 | ||
|
a79227424f | ||
|
ffe0b9712c | ||
|
d92c090c30 | ||
|
1a4157a663 | ||
|
fa81d1a9c7 | ||
|
dba4df6836 | ||
|
4aae1f78d8 | ||
|
8159a52bd7 | ||
|
95d83db90c | ||
|
ac5b0af63e | ||
|
e80473903e | ||
|
d08778c674 | ||
|
0919b9460d | ||
|
66a018c79e | ||
|
dcd64de4b8 | ||
|
a0bd206308 | ||
|
ba589e7961 | ||
|
5dc83d64c1 | ||
|
9a4eb8e9a4 | ||
|
058e7153a1 | ||
|
d7b2371854 | ||
|
b0c939866f | ||
|
a07f8b615e | ||
|
12bd85e0a9 | ||
|
1b82ed5abb | ||
|
c13ab62187 | ||
|
ea77686746 | ||
|
8a5612be3d | ||
|
d24ce27903 | ||
|
2099965508 | ||
|
829e8da8dc | ||
|
e149ee53de | ||
|
b73d3b15f8 | ||
|
61d7f23870 | ||
|
b5650b2b8f | ||
|
8777d13d8b | ||
|
d52613d723 | ||
|
7ce476f858 | ||
|
dd07f5c2a6 | ||
|
7e6e09bc19 | ||
|
1d11a163af | ||
|
57bd103de8 | ||
|
25bc5001f9 | ||
|
e4c52484b1 | ||
|
a30da07fd1 | ||
|
ee27d3e047 | ||
|
7096094224 | ||
|
443fb41d18 | ||
|
94b4351e19 | ||
|
e90aeff417 | ||
|
e50dd265d4 | ||
|
4521ea14ee | ||
|
535b41d818 | ||
|
21357a1ec7 | ||
|
8c872caf78 | ||
|
62a81a556e | ||
|
568e8c8bc0 | ||
|
98a7652403 | ||
|
78951b9155 | ||
|
8c86a653b2 | ||
|
ea0526821e | ||
|
c503445092 | ||
|
205af8b122 | ||
|
3abb7c8de6 | ||
|
a40510da3b | ||
|
a6ab4a349d | ||
|
79a704d240 | ||
|
e5adf174a8 | ||
|
f01e796271 | ||
|
302d23ba96 | ||
|
03050c3f25 | ||
|
cbfd2af74b | ||
|
f3fab0dc08 | ||
|
4a512d2425 | ||
|
07f80f43bd | ||
|
87dec337d8 | ||
|
b37877746a | ||
|
b0e5612bdc | ||
|
25b0cd0e4b | ||
|
2800d86a57 | ||
|
01bc0de2c2 | ||
|
857a4c5a26 | ||
|
063c35380a | ||
|
5322251bc0 | ||
|
c21b9df9a5 | ||
|
f2a52f0253 | ||
|
baaf493cb4 | ||
|
6cbd6d3a33 | ||
|
72e5aa981a | ||
|
c0f085cdf8 | ||
|
10bc2297d4 | ||
|
8fa5e63b07 | ||
|
9d0c50907c | ||
|
e5958983d8 | ||
|
ab23ec3f35 | ||
|
a79a6443e7 | ||
|
9ff24cbf2a | ||
|
2eee25bbc1 | ||
|
2a2431e490 | ||
|
4041e2e8ca | ||
|
031c4e5746 | ||
|
b4ea85fc76 | ||
|
480f14902d | ||
|
20c8e8d922 | ||
|
9cdecced57 | ||
|
60d46538de | ||
|
223295c2f1 | ||
|
f789fb275d | ||
|
a7c12aeb93 | ||
|
0ca9a5f68b | ||
|
842345df9b | ||
|
7d5c31c510 | ||
|
1ee1c31b9c | ||
|
e9eada77f9 | ||
|
93ce0cc5e9 | ||
|
eefd09d022 | ||
|
ef597cc67a | ||
|
5d171e0240 | ||
|
39070820be | ||
|
1fdad38b9d | ||
|
f41c0311fa | ||
|
a476ac71da | ||
|
4b971a9e67 | ||
|
bc2d321a84 | ||
|
9adeab6bae | ||
|
0f3a63e366 | ||
|
d90698fe92 | ||
|
af0af6e260 | ||
|
6e71fb565a | ||
|
6c66ab1568 | ||
|
0d329f0338 | ||
|
2f66321c2a | ||
|
5b102485bc | ||
|
698fc35704 | ||
|
37199da52f | ||
|
1c69d8e425 | ||
|
11bf00030d | ||
|
9378d30601 | ||
|
41ed4b23d8 | ||
|
de9a5a3d12 | ||
|
19202cfca6 |
@ -14,7 +14,7 @@ steps:
|
||||
- "./gradlew clean lintGplayRelease assembleGplayDebug --stacktrace"
|
||||
artifact_paths:
|
||||
- "vector/build/outputs/apk/gplay/debug/*.apk"
|
||||
branches: "develop feature/*"
|
||||
branches: "!master"
|
||||
plugins:
|
||||
- docker#v3.1.0:
|
||||
image: "runmymind/docker-android-sdk"
|
||||
@ -28,7 +28,7 @@ steps:
|
||||
- "./gradlew clean lintFdroidRelease assembleFdroidDebug --stacktrace"
|
||||
artifact_paths:
|
||||
- "vector/build/outputs/apk/fdroid/debug/*.apk"
|
||||
branches: "develop feature/*"
|
||||
branches: "!master"
|
||||
plugins:
|
||||
- docker#v3.1.0:
|
||||
image: "runmymind/docker-android-sdk"
|
||||
@ -45,7 +45,7 @@ steps:
|
||||
branches: "master"
|
||||
plugins:
|
||||
- docker#v3.1.0:
|
||||
image: "runmymind/docker-android-sdk"
|
||||
image: "runmymind/docker-android-sdk"
|
||||
|
||||
# Code quality
|
||||
|
||||
|
94
CHANGES.md
94
CHANGES.md
@ -1,24 +1,106 @@
|
||||
Changes in RiotX 0.XX (2019-XX-XX)
|
||||
Changes in RiotX 0.5.0 (2019-XX-XX)
|
||||
===================================================
|
||||
|
||||
Features:
|
||||
- Contextual action menu for messages in room
|
||||
-
|
||||
|
||||
Improvements:
|
||||
-
|
||||
- Reduce default release build log level, and lab option to enable more logs.
|
||||
|
||||
Other changes:
|
||||
-
|
||||
|
||||
Bugfix:
|
||||
-
|
||||
- Fix crash due to missing informationData (#535)
|
||||
- Progress in initial sync dialog is decreasing for a step and should not (#532)
|
||||
|
||||
Translations:
|
||||
-
|
||||
|
||||
Build:
|
||||
-
|
||||
- Fix issue with version name (#533)
|
||||
- Fix rendering issue of accepted third party invitation event
|
||||
|
||||
Changes in RiotX 0.4.0 (2019-XX-XX)
|
||||
===================================================
|
||||
|
||||
Features:
|
||||
- Display read receipts in timeline (#81)
|
||||
|
||||
Improvements:
|
||||
- Reactions: Reinstate the ability to react with non-unicode keys (#307)
|
||||
|
||||
Bugfix:
|
||||
- Fix text diff linebreak display (#441)
|
||||
- Date change message repeats for each redaction until a normal message (#358)
|
||||
- Slide-in reply icon is distorted (#423)
|
||||
- Regression / e2e replies not encrypted
|
||||
- Some video won't play
|
||||
- Privacy: remove log of notifiable event (#519)
|
||||
- Fix crash with EmojiCompat (#530)
|
||||
|
||||
Changes in RiotX 0.3.0 (2019-08-08)
|
||||
===================================================
|
||||
|
||||
Features:
|
||||
- Create Direct Room flow
|
||||
- Handle `/markdown` command
|
||||
|
||||
Improvements:
|
||||
- UI for pending edits (#193)
|
||||
- UX image preview screen transition (#393)
|
||||
- Basic support for resending failed messages (retry/remove)
|
||||
- Enable proper cancellation of suspending functions (including db transaction)
|
||||
- Enhances network connectivity checks in SDK
|
||||
- Add "View Edit History" item in the message bottom sheet (#401)
|
||||
- Cancel sync request on pause and timeout to 0 after pause (#404)
|
||||
|
||||
Other changes:
|
||||
- Show sync progress also in room detail screen (#403)
|
||||
|
||||
Bugfix:
|
||||
- Edited message: link confusion when (edited) appears in body (#398)
|
||||
- Close detail room screen when the room is left with another client (#256)
|
||||
- Clear notification for a room left on another client
|
||||
- Fix messages with empty `in_reply_to` not rendering (#447)
|
||||
- Fix clear cache (#408) and Logout (#205)
|
||||
- Fix `(edited)` link can be copied to clipboard (#402)
|
||||
|
||||
Build:
|
||||
- Split APK: generate one APK per arch, to reduce APK size of about 30%
|
||||
|
||||
|
||||
Changes in RiotX 0.2.0 (2019-07-18)
|
||||
===================================================
|
||||
|
||||
Features:
|
||||
- Message Editing: View edit history (#121)
|
||||
- Rooms filtering (#304)
|
||||
- Edit in encrypted room
|
||||
|
||||
Improvements:
|
||||
- Handle click on redacted events: view source and create permalink
|
||||
- Improve long tap menu: reply on top, more compact (#368)
|
||||
- Quick reply in timeline with swipe gesture (#167)
|
||||
- Improve edit of replies
|
||||
- Improve performance on Room Members and Users management (#381)
|
||||
|
||||
Other changes:
|
||||
- migrate from rxbinding 2 to rxbinding 3
|
||||
|
||||
Bugfix:
|
||||
- Fix regression on permalink click
|
||||
- Fix crash reported by the PlayStore (#341)
|
||||
- Fix Chat composer separator color in dark/black theme
|
||||
- Fix bad layout for room directory filter (#349)
|
||||
- Fix Copying link from a message shouldn't open context menu (#364)
|
||||
|
||||
Changes in RiotX 0.1.0 (2019-07-11)
|
||||
===================================================
|
||||
|
||||
First release!
|
||||
|
||||
Mode details here: https://medium.com/@RiotChat/introducing-the-riotx-beta-for-android-b17952e8f771
|
||||
|
||||
|
||||
=======================================================
|
||||
@ -26,7 +108,7 @@ Build:
|
||||
=======================================================
|
||||
|
||||
|
||||
Changes in RiotX 0.XX (2019-XX-XX)
|
||||
Changes in RiotX 0.0.0 (2019-XX-XX)
|
||||
===================================================
|
||||
|
||||
Features:
|
||||
|
29
README.md
29
README.md
@ -1,4 +1,4 @@
|
||||
[](https://buildkite.com/matrix-dot-org/riotx-android)
|
||||
[](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop)
|
||||
[](https://translate.riot.im/engage/riot-android/?utm_source=widget)
|
||||
[](https://matrix.to/#/#riotx:matrix.org)
|
||||
[](https://sonarcloud.io/dashboard?id=vector.android.riotx)
|
||||
@ -7,14 +7,31 @@
|
||||
|
||||
# RiotX Android
|
||||
|
||||
RiotX is an Android Matrix Client currently in development. The application is not yet available on the PlayStore.
|
||||
RiotX is an Android Matrix Client currently in beta but in active development.
|
||||
|
||||
It's based on a new Matrix SDK, written in Kotlin.
|
||||
It is a total rewrite of [Riot-Android](https://github.com/vector-im/riot-android) with a new user experience. RiotX will become the official replacement as soon as all features are implemented.
|
||||
|
||||
Download nightly build here: [](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop)
|
||||
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" alt="Get it on Google Play" height="60">](https://play.google.com/store/apps/details?id=im.vector.riotx)
|
||||
|
||||
Nightly build: [](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop)
|
||||
|
||||
# New Android SDK
|
||||
|
||||
RiotX is based on a new Android SDK fully written in Kotlin (like RiotX). In order to make the early development as fast as possible, RiotX and the new SDK currently share the same git repository. We will make separate repos once the API is stable enough.
|
||||
|
||||
|
||||
# Roadmap
|
||||
|
||||
The current target is to release an application out of beta with the same level of features (and even more) as Riot.
|
||||
The roadmap has 3 phases:
|
||||
|
||||
- [phase 0](https://github.com/vector-im/riotX-android/labels/phase0): Prototyping / Project setup
|
||||
- [phase 1](https://github.com/vector-im/riotX-android/labels/phase1): Beta release to the Play Store
|
||||
- [phase 2](https://github.com/vector-im/riotX-android/labels/phase2): Out of beta
|
||||
|
||||
Matrix Room: [](https://matrix.to/#/#riotx:matrix.org)
|
||||
|
||||
## Contributing
|
||||
|
||||
Please refer to [CONTRIBUTING.md](https://github.com/vector-im/riotX-android/blob/develop/CONTRIBUTING.md) if you want to contribute the Matrix on Android projects!
|
||||
Please refer to [CONTRIBUTING.md](https://github.com/vector-im/riotX-android/blob/develop/CONTRIBUTING.md) if you want to contribute on Matrix Android projects!
|
||||
|
||||
Come chat with the community in the dedicated Matrix [room](https://matrix.to/#/#riotx:matrix.org).
|
||||
|
67
build.gradle
67
build.gradle
@ -1,9 +1,9 @@
|
||||
import javax.tools.JavaCompiler
|
||||
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.3.21'
|
||||
ext.koin_version = '1.0.2'
|
||||
// TODO ext.koin_version = '2.0.0-GA'
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
@ -26,16 +26,47 @@ buildscript {
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
maven { url "http://dl.bintray.com/piasy/maven" }
|
||||
maven { url 'https://jitpack.io' }
|
||||
// For olm library. This has to be declared first, to ensure that Olm library is not downloaded from another repo
|
||||
maven {
|
||||
url 'https://jitpack.io'
|
||||
content {
|
||||
// Use this repo only for olm library
|
||||
includeGroupByRegex "org\\.matrix\\.gitlab\\.matrix-org"
|
||||
// And also for FilePicker
|
||||
includeGroupByRegex "com\\.github\\.jaiselrahman"
|
||||
// And monarchy
|
||||
includeGroupByRegex "com\\.github\\.Zhuinden"
|
||||
}
|
||||
}
|
||||
maven {
|
||||
url "http://dl.bintray.com/piasy/maven"
|
||||
content {
|
||||
includeGroupByRegex "com\\.github\\.piasy"
|
||||
}
|
||||
}
|
||||
maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
|
||||
google()
|
||||
jcenter()
|
||||
// For Olm SDK
|
||||
maven {
|
||||
url 'https://jitpack.io'
|
||||
url 'https://repo.adobe.com/nexus/content/repositories/public/'
|
||||
content {
|
||||
includeGroupByRegex "diff_match_patch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).all {
|
||||
options.compilerArgs += [
|
||||
'-Adagger.gradle.incremental=enabled'
|
||||
]
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
extensions.findByName("kapt")?.arguments {
|
||||
arg("dagger.gradle.incremental", "enabled")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
task clean(type: Delete) {
|
||||
@ -44,6 +75,10 @@ task clean(type: Delete) {
|
||||
|
||||
apply plugin: 'org.sonarqube'
|
||||
|
||||
// To run a sonar analysis:
|
||||
// Run './gradlew sonarqube -Dsonar.login=<REPLACE_WITH_SONAR_KEY>'
|
||||
// The SONAR_KEY is stored in passbolt
|
||||
|
||||
sonarqube {
|
||||
properties {
|
||||
property "sonar.projectName", "RiotX-Android"
|
||||
@ -69,3 +104,23 @@ project(":vector") {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//project(":matrix-sdk-android") {
|
||||
// sonarqube {
|
||||
// properties {
|
||||
// property "sonar.sources", project(":matrix-sdk-android").android.sourceSets.main.java.srcDirs
|
||||
// // exclude source code from analyses separated by a colon (:)
|
||||
// // property "sonar.exclusions", "**/*.*"
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//project(":matrix-sdk-android-rx") {
|
||||
// sonarqube {
|
||||
// properties {
|
||||
// property "sonar.sources", project(":matrix-sdk-android-rx").android.sourceSets.main.java.srcDirs
|
||||
// // exclude source code from analyses separated by a colon (:)
|
||||
// // property "sonar.exclusions", "**/*.*"
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
@ -1,4 +1,4 @@
|
||||
This document aims to describe how Riot X android displays notifications to the end user. It also clarifies notifications and background settings in the app.
|
||||
This document aims to describe how RiotX android displays notifications to the end user. It also clarifies notifications and background settings in the app.
|
||||
|
||||
# Table of Contents
|
||||
1. [Prerequisites Knowledge](#prerequisites-knowledge)
|
||||
@ -50,7 +50,7 @@ By default, this is 0, so the server will return immediately even if the respons
|
||||
|
||||
**delay** is a client preference. When the server responds to a sync request, the client waits for `delay`before calling a new sync.
|
||||
|
||||
When the Riot X Android app is open (i.e in foreground state), the default timeout is 30 seconds, and delay is 0.
|
||||
When the RiotX Android app is open (i.e in foreground state), the default timeout is 30 seconds, and delay is 0.
|
||||
|
||||
## How does a mobile app receives push notification
|
||||
|
||||
@ -86,7 +86,7 @@ This need some disambiguation, because it is the source of common confusion:
|
||||
In order to send a push to a mobile, App developers need to have a server that will use the FCM APIs, and these APIs requires authentication!
|
||||
This server is called a **Push Gateway** in the matrix world
|
||||
|
||||
That means that Riot X Android, a matrix client created by New Vector, is using a **Push Gateway** with the needed credentials (FCM API secret Key) in order to send push to the New Vector client.
|
||||
That means that RiotX Android, a matrix client created by New Vector, is using a **Push Gateway** with the needed credentials (FCM API secret Key) in order to send push to the New Vector client.
|
||||
|
||||
If you create your own matrix client, you will also need to deploy an instance of a **Push Gateway** with the credentials needed to use FCM for your app.
|
||||
|
||||
@ -223,7 +223,7 @@ Upon reception of the FCM push, RiotX will perform a sync call to the Home Serve
|
||||
* The sync generates additional notifications (e.g an encrypted message where the user is mentioned detected locally)
|
||||
* The sync takes too long and the process is killed before completion, or network is not reliable and the sync fails.
|
||||
|
||||
Riot X implements several strategies in these cases (TODO document)
|
||||
RiotX implements several strategies in these cases (TODO document)
|
||||
|
||||
## FCM Fallback mode
|
||||
|
||||
|
@ -33,13 +33,14 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation project(":matrix-sdk-android")
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0-alpha01'
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0-beta01'
|
||||
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
|
||||
// Paging
|
||||
implementation "androidx.paging:paging-runtime-ktx:2.1.0"
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'androidx.test:runner:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
|
||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
}
|
||||
|
@ -20,6 +20,8 @@ import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.android.MainThreadDisposable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
|
||||
private class LiveDataObservable<T>(
|
||||
private val liveData: LiveData<T>,
|
||||
@ -57,5 +59,5 @@ private class LiveDataObservable<T>(
|
||||
}
|
||||
|
||||
fun <T> LiveData<T>.asObservable(): Observable<T> {
|
||||
return LiveDataObservable(this)
|
||||
return LiveDataObservable(this).observeOn(Schedulers.computation())
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.rx
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import io.reactivex.CompletableEmitter
|
||||
import io.reactivex.SingleEmitter
|
||||
|
||||
internal class MatrixCallbackCompletable<T>(private val completableEmitter: CompletableEmitter) : MatrixCallback<T> {
|
||||
|
||||
override fun onSuccess(data: T) {
|
||||
completableEmitter.onComplete()
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
completableEmitter.tryOnError(failure)
|
||||
}
|
||||
}
|
||||
|
||||
fun Cancelable.toCompletable(completableEmitter: CompletableEmitter) {
|
||||
completableEmitter.setCancellable {
|
||||
this.cancel()
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.rx
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import io.reactivex.SingleEmitter
|
||||
|
||||
internal class MatrixCallbackSingle<T>(private val singleEmitter: SingleEmitter<T>) : MatrixCallback<T> {
|
||||
|
||||
override fun onSuccess(data: T) {
|
||||
singleEmitter.onSuccess(data)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
singleEmitter.tryOnError(failure)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Cancelable.toSingle(singleEmitter: SingleEmitter<T>) {
|
||||
singleEmitter.setCancellable {
|
||||
this.cancel()
|
||||
}
|
||||
}
|
@ -18,14 +18,16 @@ package im.vector.matrix.rx
|
||||
|
||||
import im.vector.matrix.android.api.session.room.Room
|
||||
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
|
||||
import im.vector.matrix.android.api.session.room.model.ReadReceipt
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
|
||||
class RxRoom(private val room: Room) {
|
||||
|
||||
fun liveRoomSummary(): Observable<RoomSummary> {
|
||||
return room.liveRoomSummary.asObservable()
|
||||
return room.liveRoomSummary().asObservable()
|
||||
}
|
||||
|
||||
fun liveRoomMemberIds(): Observable<List<String>> {
|
||||
@ -40,6 +42,18 @@ class RxRoom(private val room: Room) {
|
||||
return room.liveTimeLineEvent(eventId).asObservable()
|
||||
}
|
||||
|
||||
fun loadRoomMembersIfNeeded(): Single<Unit> = Single.create {
|
||||
room.loadRoomMembersIfNeeded(MatrixCallbackSingle(it)).toSingle(it)
|
||||
}
|
||||
|
||||
fun joinRoom(viaServers: List<String> = emptyList()): Single<Unit> = Single.create {
|
||||
room.join(viaServers, MatrixCallbackSingle(it)).toSingle(it)
|
||||
}
|
||||
|
||||
fun liveEventReadReceipts(eventId: String): Observable<List<ReadReceipt>> {
|
||||
return room.getEventReadReceiptsLive(eventId).asObservable()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun Room.rx(): RxRoom {
|
||||
|
@ -16,12 +16,16 @@
|
||||
|
||||
package im.vector.matrix.rx
|
||||
|
||||
import androidx.paging.PagedList
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||
import im.vector.matrix.android.api.session.pushers.Pusher
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||
import im.vector.matrix.android.api.session.sync.SyncState
|
||||
import im.vector.matrix.android.api.session.user.model.User
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
|
||||
class RxSession(private val session: Session) {
|
||||
|
||||
@ -41,6 +45,28 @@ class RxSession(private val session: Session) {
|
||||
return session.livePushers().asObservable()
|
||||
}
|
||||
|
||||
fun liveUsers(): Observable<List<User>> {
|
||||
return session.liveUsers().asObservable()
|
||||
}
|
||||
|
||||
fun livePagedUsers(filter: String? = null): Observable<PagedList<User>> {
|
||||
return session.livePagedUsers(filter).asObservable()
|
||||
}
|
||||
|
||||
fun createRoom(roomParams: CreateRoomParams): Single<String> = Single.create {
|
||||
session.createRoom(roomParams, MatrixCallbackSingle(it)).toSingle(it)
|
||||
}
|
||||
|
||||
fun searchUsersDirectory(search: String,
|
||||
limit: Int,
|
||||
excludedUserIds: Set<String>): Single<List<User>> = Single.create {
|
||||
session.searchUsersDirectory(search, limit, excludedUserIds, MatrixCallbackSingle(it)).toSingle(it)
|
||||
}
|
||||
|
||||
fun joinRoom(roomId: String, viaServers: List<String> = emptyList()): Single<Unit> = Single.create {
|
||||
session.joinRoom(roomId, viaServers, MatrixCallbackSingle(it)).toSingle(it)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun Session.rx(): RxSession {
|
||||
|
@ -6,20 +6,14 @@ apply plugin: 'realm-android'
|
||||
apply plugin: 'okreplay'
|
||||
|
||||
buildscript {
|
||||
|
||||
repositories {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath "io.realm:realm-gradle-plugin:5.9.0"
|
||||
classpath "io.realm:realm-gradle-plugin:5.12.0"
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
|
||||
androidExtensions {
|
||||
experimental = true
|
||||
}
|
||||
@ -33,6 +27,8 @@ android {
|
||||
targetSdkVersion 28
|
||||
versionCode 1
|
||||
versionName "0.0.1"
|
||||
// Multidex is useful for tests
|
||||
multiDexEnabled true
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
buildConfigField "String", "GIT_SDK_REVISION", "\"${gitRevision()}\""
|
||||
@ -66,6 +62,11 @@ android {
|
||||
lintOptions {
|
||||
lintConfig file("lint.xml")
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
static def gitRevision() {
|
||||
@ -86,30 +87,29 @@ static def gitRevisionDate() {
|
||||
dependencies {
|
||||
|
||||
def arrow_version = "0.8.0"
|
||||
def support_version = '1.1.0-alpha03'
|
||||
def support_version = '1.1.0-beta01'
|
||||
def moshi_version = '1.8.0'
|
||||
def lifecycle_version = '2.0.0'
|
||||
def coroutines_version = "1.0.1"
|
||||
def markwon_version = '3.0.0'
|
||||
def daggerVersion = '2.23.1'
|
||||
|
||||
implementation fileTree(dir: 'libs', include: ['*.aar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||
|
||||
implementation "androidx.appcompat:appcompat:$support_version"
|
||||
implementation "androidx.recyclerview:recyclerview:$support_version"
|
||||
implementation "androidx.appcompat:appcompat:1.1.0-rc01"
|
||||
implementation "androidx.recyclerview:recyclerview:1.1.0-beta01"
|
||||
|
||||
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
|
||||
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
|
||||
|
||||
// Network
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.4.0'
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.6.0'
|
||||
implementation 'com.squareup.retrofit2:converter-moshi:2.4.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.11.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.14.1'
|
||||
implementation 'com.squareup.okhttp3:logging-interceptor:3.10.0'
|
||||
implementation 'com.novoda:merlin:1.1.6'
|
||||
implementation 'com.novoda:merlin:1.2.0'
|
||||
implementation "com.squareup.moshi:moshi-adapters:$moshi_version"
|
||||
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
|
||||
|
||||
@ -120,14 +120,11 @@ dependencies {
|
||||
kapt 'dk.ilios:realmfieldnameshelper:1.1.1'
|
||||
|
||||
// Work
|
||||
implementation "androidx.work:work-runtime-ktx:2.1.0-beta01"
|
||||
implementation "androidx.work:work-runtime-ktx:2.1.0-rc01"
|
||||
|
||||
// FP
|
||||
implementation "io.arrow-kt:arrow-core:$arrow_version"
|
||||
implementation "io.arrow-kt:arrow-instances-core:$arrow_version"
|
||||
implementation "io.arrow-kt:arrow-effects:$arrow_version"
|
||||
implementation "io.arrow-kt:arrow-effects-instances:$arrow_version"
|
||||
implementation "io.arrow-kt:arrow-integration-retrofit-adapter:$arrow_version"
|
||||
|
||||
// olm lib is now hosted by jitpack: https://jitpack.io/#org.matrix.gitlab.matrix-org/olm
|
||||
implementation 'org.matrix.gitlab.matrix-org:olm:3.1.2'
|
||||
@ -148,18 +145,16 @@ dependencies {
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.robolectric:robolectric:4.0.2'
|
||||
testImplementation "org.koin:koin-test:$koin_version"
|
||||
//testImplementation 'org.robolectric:shadows-support-v4:3.0'
|
||||
testImplementation "io.mockk:mockk:1.8.13.kotlin13"
|
||||
testImplementation 'org.amshove.kluent:kluent-android:1.44'
|
||||
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||
|
||||
androidTestImplementation "org.koin:koin-test:$koin_version"
|
||||
androidTestImplementation 'androidx.test:core:1.1.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.1.1'
|
||||
androidTestImplementation 'androidx.test:rules:1.1.1'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
|
||||
androidTestImplementation 'androidx.test:core:1.2.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||
androidTestImplementation 'androidx.test:rules:1.2.0'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
androidTestImplementation 'org.amshove.kluent:kluent-android:1.44'
|
||||
androidTestImplementation "io.mockk:mockk-android:1.8.13.kotlin13"
|
||||
androidTestImplementation "androidx.arch.core:core-testing:$lifecycle_version"
|
||||
|
Binary file not shown.
@ -16,10 +16,10 @@
|
||||
|
||||
package im.vector.matrix.android;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
@ -19,4 +19,4 @@ package im.vector.matrix.android
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
|
||||
internal val testCoroutineDispatchers = MatrixCoroutineDispatchers(Main, Main, Main, Main)
|
||||
internal val testCoroutineDispatchers = MatrixCoroutineDispatchers(Main, Main, Main, Main, Main)
|
@ -19,25 +19,18 @@ package im.vector.matrix.android.auth
|
||||
import androidx.test.annotation.UiThreadTest
|
||||
import androidx.test.rule.GrantPermissionRule
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.InstrumentedTest
|
||||
import im.vector.matrix.android.OkReplayRuleChainNoActivity
|
||||
import im.vector.matrix.android.api.auth.Authenticator
|
||||
import im.vector.matrix.android.internal.auth.AuthModule
|
||||
import im.vector.matrix.android.internal.di.MatrixModule
|
||||
import im.vector.matrix.android.internal.di.NetworkModule
|
||||
import okreplay.*
|
||||
import org.junit.ClassRule
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.koin.standalone.StandAloneContext.loadKoinModules
|
||||
import org.koin.standalone.inject
|
||||
import org.koin.test.KoinTest
|
||||
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
internal class AuthenticatorTest : InstrumentedTest, KoinTest {
|
||||
internal class AuthenticatorTest : InstrumentedTest {
|
||||
|
||||
lateinit var authenticator: Authenticator
|
||||
lateinit var okReplayInterceptor: OkReplayInterceptor
|
||||
|
@ -21,7 +21,7 @@ import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.store.db.RealmCryptoStoreModule
|
||||
import io.realm.RealmConfiguration
|
||||
import java.util.*
|
||||
import kotlin.random.Random
|
||||
|
||||
internal class CryptoStoreHelper {
|
||||
|
||||
@ -35,7 +35,7 @@ internal class CryptoStoreHelper {
|
||||
}
|
||||
|
||||
fun createCredential() = Credentials(
|
||||
userId = "userId_" + Random().nextInt(),
|
||||
userId = "userId_" + Random.nextInt(),
|
||||
homeServer = "http://matrix.org",
|
||||
accessToken = "access_token",
|
||||
refreshToken = null,
|
||||
|
@ -52,7 +52,7 @@ internal class JsonCanonicalizerTest : InstrumentedTest {
|
||||
|
||||
@Test
|
||||
fun realSampleTest() {
|
||||
assertEquals("""{"algorithms":["m.megolm.v1.aes-sha2","m.olm.v1.curve25519-aes-sha2"],"device_id":"VSCUNFSOUI","keys":{"curve25519:VSCUNFSOUI":"utyOjnhiQ73qNhi9HlN0OgWIowe5gthTS8r0r9TcJ3o","ed25519:VSCUNFSOUI":"qNhEt+Yggaajet0hX/FjTRLfySgs65ldYyomm7PIx6U"},"user_id":"@benoitx:matrix.org"}""",
|
||||
assertEquals("""{"algorithms":["m.megolm.v1.aes-sha2","m.olm.v1.curve25519-aes-sha2"],"device_id":"VSCUNFSOUI","keys":{"curve25519:VSCUNFSOUI":"utyOjnhiQ73qNhi9HlN0OgWIowe5gthTS8r0r9TcJ3o","ed25519:VSCUNFSOUI":"qNhEt+Yggaajet0hX\/FjTRLfySgs65ldYyomm7PIx6U"},"user_id":"@benoitx:matrix.org"}""",
|
||||
JsonCanonicalizer.canonicalize("""{"algorithms":["m.megolm.v1.aes-sha2","m.olm.v1.curve25519-aes-sha2"],"device_id":"VSCUNFSOUI","user_id":"@benoitx:matrix.org","keys":{"curve25519:VSCUNFSOUI":"utyOjnhiQ73qNhi9HlN0OgWIowe5gthTS8r0r9TcJ3o","ed25519:VSCUNFSOUI":"qNhEt+Yggaajet0hX/FjTRLfySgs65ldYyomm7PIx6U"}}"""))
|
||||
}
|
||||
|
||||
|
@ -19,11 +19,7 @@ package im.vector.matrix.android.session.room.timeline
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.InstrumentedTest
|
||||
import im.vector.matrix.android.internal.database.helper.add
|
||||
import im.vector.matrix.android.internal.database.helper.addAll
|
||||
import im.vector.matrix.android.internal.database.helper.isUnlinked
|
||||
import im.vector.matrix.android.internal.database.helper.lastStateIndex
|
||||
import im.vector.matrix.android.internal.database.helper.merge
|
||||
import im.vector.matrix.android.internal.database.helper.*
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
|
||||
import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeListOfEvents
|
||||
@ -59,7 +55,7 @@ internal class ChunkEntityTest : InstrumentedTest {
|
||||
val chunk: ChunkEntity = realm.createObject()
|
||||
val fakeEvent = createFakeMessageEvent()
|
||||
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
|
||||
chunk.events.size shouldEqual 1
|
||||
chunk.timelineEvents.size shouldEqual 1
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,7 +66,7 @@ internal class ChunkEntityTest : InstrumentedTest {
|
||||
val fakeEvent = createFakeMessageEvent()
|
||||
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
|
||||
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
|
||||
chunk.events.size shouldEqual 1
|
||||
chunk.timelineEvents.size shouldEqual 1
|
||||
}
|
||||
}
|
||||
|
||||
@ -126,7 +122,7 @@ internal class ChunkEntityTest : InstrumentedTest {
|
||||
chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
|
||||
chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
|
||||
chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS)
|
||||
chunk1.events.size shouldEqual 60
|
||||
chunk1.timelineEvents.size shouldEqual 60
|
||||
}
|
||||
}
|
||||
|
||||
@ -142,7 +138,7 @@ internal class ChunkEntityTest : InstrumentedTest {
|
||||
chunk1.addAll("roomId", eventsForChunk1, PaginationDirection.FORWARDS)
|
||||
chunk2.addAll("roomId", eventsForChunk2, PaginationDirection.BACKWARDS)
|
||||
chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS)
|
||||
chunk1.events.size shouldEqual 40
|
||||
chunk1.timelineEvents.size shouldEqual 40
|
||||
chunk1.isLastForward.shouldBeTrue()
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
package im.vector.matrix.android.session.room.timeline
|
||||
|
||||
import arrow.core.Try
|
||||
import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask
|
||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
|
||||
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor
|
||||
@ -24,7 +23,7 @@ import kotlin.random.Random
|
||||
|
||||
internal class FakeGetContextOfEventTask constructor(private val tokenChunkEventPersistor: TokenChunkEventPersistor) : GetContextOfEventTask {
|
||||
|
||||
override suspend fun execute(params: GetContextOfEventTask.Params): Try<TokenChunkEventPersistor.Result> {
|
||||
override suspend fun execute(params: GetContextOfEventTask.Params): TokenChunkEventPersistor.Result {
|
||||
val fakeEvents = RoomDataHelper.createFakeListOfEvents(30)
|
||||
val tokenChunkEvent = FakeTokenChunkEvent(
|
||||
Random.nextLong(System.currentTimeMillis()).toString(),
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
package im.vector.matrix.android.session.room.timeline
|
||||
|
||||
import arrow.core.Try
|
||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationTask
|
||||
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor
|
||||
import javax.inject.Inject
|
||||
@ -24,7 +23,7 @@ import kotlin.random.Random
|
||||
|
||||
internal class FakePaginationTask @Inject constructor(private val tokenChunkEventPersistor: TokenChunkEventPersistor) : PaginationTask {
|
||||
|
||||
override suspend fun execute(params: PaginationTask.Params): Try<TokenChunkEventPersistor.Result> {
|
||||
override suspend fun execute(params: PaginationTask.Params): TokenChunkEventPersistor.Result {
|
||||
val fakeEvents = RoomDataHelper.createFakeListOfEvents(30)
|
||||
val tokenChunkEvent = FakeTokenChunkEvent(params.from, Random.nextLong(System.currentTimeMillis()).toString(), fakeEvents)
|
||||
return tokenChunkEventPersistor.insertInDb(tokenChunkEvent, params.roomId, params.direction)
|
||||
|
@ -18,25 +18,6 @@ package im.vector.matrix.android.session.room.timeline
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.InstrumentedTest
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.internal.crypto.CryptoManager
|
||||
import im.vector.matrix.android.internal.database.model.SessionRealmModule
|
||||
import im.vector.matrix.android.internal.session.room.EventRelationExtractor
|
||||
import im.vector.matrix.android.internal.session.room.membership.SenderRoomMemberExtractor
|
||||
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline
|
||||
import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory
|
||||
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.testCoroutineDispatchers
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import org.amshove.kluent.shouldEqual
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
internal class TimelineTest : InstrumentedTest {
|
||||
|
||||
|
@ -87,7 +87,8 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
|
||||
val matrixConfiguration = (appContext as MatrixConfiguration.Provider).providesMatrixConfiguration()
|
||||
instance = Matrix(appContext, matrixConfiguration)
|
||||
} else {
|
||||
throw IllegalStateException("Matrix is not initialized properly. You should call Matrix.initialize or let your application implements MatrixConfiguration.Provider.")
|
||||
throw IllegalStateException("Matrix is not initialized properly." +
|
||||
" You should call Matrix.initialize or let your application implements MatrixConfiguration.Provider.")
|
||||
}
|
||||
}
|
||||
return instance
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
package im.vector.matrix.android.api
|
||||
|
||||
import java.util.regex.Pattern
|
||||
|
||||
/**
|
||||
* This class contains pattern to match the different Matrix ids
|
||||
@ -29,31 +28,31 @@ object MatrixPatterns {
|
||||
// regex pattern to find matrix user ids in a string.
|
||||
// See https://matrix.org/speculator/spec/HEAD/appendices.html#historical-user-ids
|
||||
private const val MATRIX_USER_IDENTIFIER_REGEX = "@[A-Z0-9\\x21-\\x39\\x3B-\\x7F]+$DOMAIN_REGEX"
|
||||
private val PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER = Pattern.compile(MATRIX_USER_IDENTIFIER_REGEX, Pattern.CASE_INSENSITIVE)
|
||||
val PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER = MATRIX_USER_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE)
|
||||
|
||||
// regex pattern to find room ids in a string.
|
||||
private const val MATRIX_ROOM_IDENTIFIER_REGEX = "![A-Z0-9]+$DOMAIN_REGEX"
|
||||
private val PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER = Pattern.compile(MATRIX_ROOM_IDENTIFIER_REGEX, Pattern.CASE_INSENSITIVE)
|
||||
private val PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER = MATRIX_ROOM_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE)
|
||||
|
||||
// regex pattern to find room aliases in a string.
|
||||
private const val MATRIX_ROOM_ALIAS_REGEX = "#[A-Z0-9._%#@=+-]+$DOMAIN_REGEX"
|
||||
private val PATTERN_CONTAIN_MATRIX_ALIAS = Pattern.compile(MATRIX_ROOM_ALIAS_REGEX, Pattern.CASE_INSENSITIVE)
|
||||
private val PATTERN_CONTAIN_MATRIX_ALIAS = MATRIX_ROOM_ALIAS_REGEX.toRegex(RegexOption.IGNORE_CASE)
|
||||
|
||||
// regex pattern to find message ids in a string.
|
||||
private const val MATRIX_EVENT_IDENTIFIER_REGEX = "\\$[A-Z0-9]+$DOMAIN_REGEX"
|
||||
private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER = Pattern.compile(MATRIX_EVENT_IDENTIFIER_REGEX, Pattern.CASE_INSENSITIVE)
|
||||
private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER = MATRIX_EVENT_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE)
|
||||
|
||||
// regex pattern to find message ids in a string.
|
||||
private const val MATRIX_EVENT_IDENTIFIER_V3_REGEX = "\\$[A-Z0-9/+]+"
|
||||
private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3 = Pattern.compile(MATRIX_EVENT_IDENTIFIER_V3_REGEX, Pattern.CASE_INSENSITIVE)
|
||||
private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3 = MATRIX_EVENT_IDENTIFIER_V3_REGEX.toRegex(RegexOption.IGNORE_CASE)
|
||||
|
||||
// Ref: https://matrix.org/docs/spec/rooms/v4#event-ids
|
||||
private const val MATRIX_EVENT_IDENTIFIER_V4_REGEX = "\\$[A-Z0-9\\-_]+"
|
||||
private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4 = Pattern.compile(MATRIX_EVENT_IDENTIFIER_V4_REGEX, Pattern.CASE_INSENSITIVE)
|
||||
private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4 = MATRIX_EVENT_IDENTIFIER_V4_REGEX.toRegex(RegexOption.IGNORE_CASE)
|
||||
|
||||
// regex pattern to find group ids in a string.
|
||||
private const val MATRIX_GROUP_IDENTIFIER_REGEX = "\\+[A-Z0-9=_\\-./]+$DOMAIN_REGEX"
|
||||
private val PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER = Pattern.compile(MATRIX_GROUP_IDENTIFIER_REGEX, Pattern.CASE_INSENSITIVE)
|
||||
private val PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER = MATRIX_GROUP_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE)
|
||||
|
||||
// regex pattern to find permalink with message id.
|
||||
// Android does not support in URL so extract it.
|
||||
@ -62,16 +61,16 @@ object MatrixPatterns {
|
||||
const val SEP_REGEX = "/"
|
||||
|
||||
private const val LINK_TO_ROOM_ID_REGEXP = PERMALINK_BASE_REGEX + MATRIX_ROOM_IDENTIFIER_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX
|
||||
private val PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ID = Pattern.compile(LINK_TO_ROOM_ID_REGEXP, Pattern.CASE_INSENSITIVE)
|
||||
private val PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ID = LINK_TO_ROOM_ID_REGEXP.toRegex(RegexOption.IGNORE_CASE)
|
||||
|
||||
private const val LINK_TO_ROOM_ALIAS_REGEXP = PERMALINK_BASE_REGEX + MATRIX_ROOM_ALIAS_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX
|
||||
private val PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ALIAS = Pattern.compile(LINK_TO_ROOM_ALIAS_REGEXP, Pattern.CASE_INSENSITIVE)
|
||||
private val PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ALIAS = LINK_TO_ROOM_ALIAS_REGEXP.toRegex(RegexOption.IGNORE_CASE)
|
||||
|
||||
private const val LINK_TO_APP_ROOM_ID_REGEXP = APP_BASE_REGEX + MATRIX_ROOM_IDENTIFIER_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX
|
||||
private val PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ID = Pattern.compile(LINK_TO_APP_ROOM_ID_REGEXP, Pattern.CASE_INSENSITIVE)
|
||||
private val PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ID = LINK_TO_APP_ROOM_ID_REGEXP.toRegex(RegexOption.IGNORE_CASE)
|
||||
|
||||
private const val LINK_TO_APP_ROOM_ALIAS_REGEXP = APP_BASE_REGEX + MATRIX_ROOM_ALIAS_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX
|
||||
private val PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ALIAS = Pattern.compile(LINK_TO_APP_ROOM_ALIAS_REGEXP, Pattern.CASE_INSENSITIVE)
|
||||
private val PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ALIAS = LINK_TO_APP_ROOM_ALIAS_REGEXP.toRegex(RegexOption.IGNORE_CASE)
|
||||
|
||||
// list of patterns to find some matrix item.
|
||||
val MATRIX_PATTERNS = listOf(
|
||||
@ -93,7 +92,7 @@ object MatrixPatterns {
|
||||
* @return true if the string is a valid user id
|
||||
*/
|
||||
fun isUserId(str: String?): Boolean {
|
||||
return str != null && PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER.matcher(str).matches()
|
||||
return str != null && str matches PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER
|
||||
}
|
||||
|
||||
/**
|
||||
@ -103,7 +102,7 @@ object MatrixPatterns {
|
||||
* @return true if the string is a valid room Id
|
||||
*/
|
||||
fun isRoomId(str: String?): Boolean {
|
||||
return str != null && PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER.matcher(str).matches()
|
||||
return str != null && str matches PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER
|
||||
}
|
||||
|
||||
/**
|
||||
@ -113,7 +112,7 @@ object MatrixPatterns {
|
||||
* @return true if the string is a valid room alias.
|
||||
*/
|
||||
fun isRoomAlias(str: String?): Boolean {
|
||||
return str != null && PATTERN_CONTAIN_MATRIX_ALIAS.matcher(str).matches()
|
||||
return str != null && str matches PATTERN_CONTAIN_MATRIX_ALIAS
|
||||
}
|
||||
|
||||
/**
|
||||
@ -124,9 +123,9 @@ object MatrixPatterns {
|
||||
*/
|
||||
fun isEventId(str: String?): Boolean {
|
||||
return str != null
|
||||
&& (PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER.matcher(str).matches()
|
||||
|| PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3.matcher(str).matches()
|
||||
|| PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4.matcher(str).matches())
|
||||
&& (str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER
|
||||
|| str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V3
|
||||
|| str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -136,6 +135,25 @@ object MatrixPatterns {
|
||||
* @return true if the string is a valid group id.
|
||||
*/
|
||||
fun isGroupId(str: String?): Boolean {
|
||||
return str != null && PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER.matcher(str).matches()
|
||||
return str != null && str matches PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract server name from a matrix id
|
||||
*
|
||||
* @param matrixId
|
||||
* @return null if not found or if matrixId is null
|
||||
*/
|
||||
fun extractServerNameFromId(matrixId: String?): String? {
|
||||
if (matrixId == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val index = matrixId.indexOf(":")
|
||||
|
||||
return if (index == -1) {
|
||||
null
|
||||
} else matrixId.substring(index + 1)
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -36,17 +36,15 @@ interface Authenticator {
|
||||
*/
|
||||
fun authenticate(homeServerConnectionConfig: HomeServerConnectionConfig, login: String, password: String, callback: MatrixCallback<Session>): Cancelable
|
||||
|
||||
//TODO remove this method. Shouldn't be managed like that.
|
||||
/**
|
||||
* Check if there is an active [Session].
|
||||
* Check if there is an authenticated [Session].
|
||||
* @return true if there is at least one active session.
|
||||
*/
|
||||
fun hasAuthenticatedSessions(): Boolean
|
||||
|
||||
//TODO remove this method. Shouldn't be managed like that.
|
||||
/**
|
||||
* Get the last active [Session], if there is an active session.
|
||||
* @return the lastActive session if any, or null
|
||||
* Get the last authenticated [Session], if there is an active session.
|
||||
* @return the last active session if any, or null
|
||||
*/
|
||||
fun getLastAuthenticatedSession(): Session?
|
||||
|
||||
@ -58,7 +56,4 @@ interface Authenticator {
|
||||
* @return the associated session if any, or null
|
||||
*/
|
||||
fun getSession(sessionParams: SessionParams): Session?
|
||||
|
||||
|
||||
|
||||
}
|
@ -31,7 +31,7 @@ import okhttp3.TlsVersion
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class HomeServerConnectionConfig(
|
||||
val homeServerUri: Uri,
|
||||
val identityServerUri: Uri,
|
||||
val identityServerUri: Uri? = null,
|
||||
val antiVirusServerUri: Uri? = null,
|
||||
val allowedFingerprints: MutableList<Fingerprint> = ArrayList(),
|
||||
val shouldPin: Boolean = false,
|
||||
@ -48,7 +48,7 @@ data class HomeServerConnectionConfig(
|
||||
class Builder {
|
||||
|
||||
private lateinit var homeServerUri: Uri
|
||||
private lateinit var identityServerUri: Uri
|
||||
private var identityServerUri: Uri? = null
|
||||
private var antiVirusServerUri: Uri? = null
|
||||
private val allowedFingerprints: MutableList<Fingerprint> = ArrayList()
|
||||
private var shouldPin: Boolean = false
|
||||
|
@ -17,7 +17,6 @@
|
||||
package im.vector.matrix.android.api.comparators
|
||||
|
||||
import im.vector.matrix.android.api.interfaces.DatedObject
|
||||
import java.util.*
|
||||
|
||||
object DatedObjectComparators {
|
||||
|
||||
|
@ -19,7 +19,7 @@ package im.vector.matrix.android.api.extensions
|
||||
import im.vector.matrix.android.api.comparators.DatedObjectComparators
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||
import java.util.*
|
||||
import java.util.Collections
|
||||
|
||||
/* ==========================================================================================
|
||||
* MXDeviceInfo
|
||||
|
@ -38,7 +38,7 @@ sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) {
|
||||
|
||||
data class RegistrationFlowError(val registrationFlowResponse: RegistrationFlowResponse) : Failure(RuntimeException(registrationFlowResponse.toString()))
|
||||
|
||||
data class CryptoError(val error: MXCryptoError) : Failure(RuntimeException(error.toString()))
|
||||
data class CryptoError(val error: MXCryptoError) : Failure(error)
|
||||
|
||||
abstract class FeatureFailure : Failure()
|
||||
|
||||
|
@ -26,8 +26,13 @@ import com.squareup.moshi.JsonClass
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MatrixError(
|
||||
@Json(name = "errcode") val code: String,
|
||||
@Json(name = "error") val message: String
|
||||
) {
|
||||
@Json(name = "error") val message: String,
|
||||
|
||||
@Json(name = "consent_uri") val consentUri: String? = null,
|
||||
// RESOURCE_LIMIT_EXCEEDED data
|
||||
@Json(name = "limit_type") val limitType: String? = null,
|
||||
@Json(name = "admin_contact") val adminUri: String? = null) {
|
||||
|
||||
|
||||
companion object {
|
||||
const val FORBIDDEN = "M_FORBIDDEN"
|
||||
@ -55,5 +60,8 @@ data class MatrixError(
|
||||
const val M_CONSENT_NOT_GIVEN = "M_CONSENT_NOT_GIVEN"
|
||||
const val RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED"
|
||||
const val WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION"
|
||||
|
||||
// Possible value for "limit_type"
|
||||
const val LIMIT_TYPE_MAU = "monthly_active_user"
|
||||
}
|
||||
}
|
@ -37,15 +37,13 @@ object MatrixLinkify {
|
||||
}
|
||||
val text = spannable.toString()
|
||||
var hasMatch = false
|
||||
for (index in MatrixPatterns.MATRIX_PATTERNS.indices) {
|
||||
val pattern = MatrixPatterns.MATRIX_PATTERNS[index]
|
||||
val matcher = pattern.matcher(spannable)
|
||||
while (matcher.find()) {
|
||||
for (pattern in MatrixPatterns.MATRIX_PATTERNS) {
|
||||
for (match in pattern.findAll(spannable)) {
|
||||
hasMatch = true
|
||||
val startPos = matcher.start(0)
|
||||
val startPos = match.range.first
|
||||
if (startPos == 0 || text[startPos - 1] != '/') {
|
||||
val endPos = matcher.end(0)
|
||||
val url = text.substring(matcher.start(0), matcher.end(0))
|
||||
val endPos = match.range.last + 1
|
||||
val url = text.substring(match.range)
|
||||
val span = MatrixPermalinkSpan(url, callback)
|
||||
spannable.setSpan(span, startPos, endPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
@ -53,5 +51,5 @@ object MatrixLinkify {
|
||||
}
|
||||
return hasMatch
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -18,6 +18,7 @@ package im.vector.matrix.android.api.permalinks
|
||||
|
||||
import android.text.style.ClickableSpan
|
||||
import android.view.View
|
||||
import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan.Callback
|
||||
|
||||
/**
|
||||
* This MatrixPermalinkSpan is a clickable span which use a [Callback] to communicate back.
|
||||
|
@ -20,7 +20,6 @@ import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import timber.log.Timber
|
||||
import java.util.regex.Pattern
|
||||
|
||||
@ -35,16 +34,16 @@ class ContainsDisplayNameCondition : Condition(Kind.contains_display_name) {
|
||||
}
|
||||
|
||||
fun isSatisfied(event: Event, displayName: String): Boolean {
|
||||
//TODO the spec says:
|
||||
// Matches any message whose content is unencrypted and contains the user's current display name
|
||||
var message = when (event.type) {
|
||||
EventType.MESSAGE -> {
|
||||
EventType.MESSAGE -> {
|
||||
event.content.toModel<MessageContent>()
|
||||
}
|
||||
// EventType.ENCRYPTED -> {
|
||||
// event.root.getClearContent()?.toModel<MessageContent>()
|
||||
// }
|
||||
else -> null
|
||||
//TODO the spec says:
|
||||
// Matches any message whose content is unencrypted and contains the user's current display name
|
||||
// EventType.ENCRYPTED -> {
|
||||
// event.root.getClearContent()?.toModel<MessageContent>()
|
||||
// }
|
||||
else -> null
|
||||
} ?: return false
|
||||
|
||||
return caseInsensitiveFind(displayName, message.body)
|
||||
|
@ -18,6 +18,7 @@ package im.vector.matrix.android.api.pushrules
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.pushrules.rest.PushRule
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
|
||||
interface PushRuleService {
|
||||
|
||||
@ -31,7 +32,7 @@ interface PushRuleService {
|
||||
|
||||
//TODO update rule
|
||||
|
||||
fun updatePushRuleEnableStatus(kind: String, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback<Unit>)
|
||||
fun updatePushRuleEnableStatus(kind: String, pushRule: PushRule, enabled: Boolean, callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
fun addPushRuleListener(listener: PushRuleListener)
|
||||
|
||||
@ -41,6 +42,7 @@ interface PushRuleService {
|
||||
|
||||
interface PushRuleListener {
|
||||
fun onMatchRule(event: Event, actions: List<Action>)
|
||||
fun onRoomLeft(roomId: String)
|
||||
fun batchFinish()
|
||||
}
|
||||
}
|
@ -18,18 +18,17 @@ package im.vector.matrix.android.api.pushrules
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.room.RoomService
|
||||
import timber.log.Timber
|
||||
import java.util.regex.Pattern
|
||||
|
||||
private val regex = Pattern.compile("^(==|<=|>=|<|>)?(\\d*)$")
|
||||
private val regex = Regex("^(==|<=|>=|<|>)?(\\d*)$")
|
||||
|
||||
class RoomMemberCountCondition(val `is`: String) : Condition(Kind.room_member_count) {
|
||||
class RoomMemberCountCondition(val iz: String) : Condition(Kind.room_member_count) {
|
||||
|
||||
override fun isSatisfied(conditionResolver: ConditionResolver): Boolean {
|
||||
return conditionResolver.resolveRoomMemberCountCondition(this)
|
||||
}
|
||||
|
||||
override fun technicalDescription(): String {
|
||||
return "Room member count is $`is`"
|
||||
return "Room member count is $iz"
|
||||
}
|
||||
|
||||
fun isSatisfied(event: Event, session: RoomService?): Boolean {
|
||||
@ -56,12 +55,9 @@ class RoomMemberCountCondition(val `is`: String) : Condition(Kind.room_member_co
|
||||
*/
|
||||
private fun parseIsField(): Pair<String?, Int>? {
|
||||
try {
|
||||
val match = regex.matcher(`is`)
|
||||
if (match.find()) {
|
||||
val prefix = match.group(1)
|
||||
val count = match.group(2).toInt()
|
||||
return prefix to count
|
||||
}
|
||||
val match = regex.find(iz) ?: return null
|
||||
val (prefix, count) = match.destructured
|
||||
return prefix to count.toInt()
|
||||
} catch (t: Throwable) {
|
||||
Timber.d(t)
|
||||
}
|
||||
|
@ -40,7 +40,8 @@ data class PushCondition(
|
||||
/**
|
||||
* Required for room_member_count conditions.
|
||||
* A decimal integer optionally prefixed by one of, ==, <, >, >= or <=.
|
||||
* A prefix of < matches rooms where the member count is strictly less than the given number and so forth. If no prefix is present, this parameter defaults to ==.
|
||||
* A prefix of < matches rooms where the member count is strictly less than the given number and so forth.
|
||||
* If no prefix is present, this parameter defaults to ==.
|
||||
*/
|
||||
@Json(name = "is") val iz: String? = null
|
||||
) {
|
||||
|
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.matrix.android.api.session
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.lifecycle.LiveData
|
||||
|
||||
interface InitialSyncProgressService {
|
||||
|
||||
fun getInitialSyncProgressStatus() : LiveData<Status?>
|
||||
|
||||
data class Status(
|
||||
@StringRes val statusText: Int,
|
||||
val percentProgress: Int = 0
|
||||
)
|
||||
}
|
@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.cache.CacheService
|
||||
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.file.FileService
|
||||
import im.vector.matrix.android.api.session.group.GroupService
|
||||
import im.vector.matrix.android.api.session.pushers.PushersService
|
||||
import im.vector.matrix.android.api.session.room.RoomDirectoryService
|
||||
@ -46,15 +47,20 @@ interface Session :
|
||||
CacheService,
|
||||
SignOutService,
|
||||
FilterService,
|
||||
FileService,
|
||||
PushRuleService,
|
||||
PushersService {
|
||||
PushersService,
|
||||
InitialSyncProgressService {
|
||||
|
||||
/**
|
||||
* The params associated to the session
|
||||
*/
|
||||
val sessionParams: SessionParams
|
||||
|
||||
val myUserId : String
|
||||
/**
|
||||
* Useful shortcut to get access to the userId
|
||||
*/
|
||||
val myUserId: String
|
||||
get() = sessionParams.credentials.userId
|
||||
|
||||
|
||||
@ -81,7 +87,7 @@ interface Session :
|
||||
/**
|
||||
* This method start the sync thread.
|
||||
*/
|
||||
fun startSync()
|
||||
fun startSync(fromForeground : Boolean)
|
||||
|
||||
/**
|
||||
* This method stop the sync thread.
|
||||
|
@ -28,10 +28,11 @@ interface ContentUploadStateTracker {
|
||||
|
||||
sealed class State {
|
||||
object Idle : State()
|
||||
data class ProgressData(val current: Long, val total: Long) : State()
|
||||
object EncryptingThumbnail : State()
|
||||
data class UploadingThumbnail(val current: Long, val total: Long) : State()
|
||||
object Encrypting : State()
|
||||
data class Uploading(val current: Long, val total: Long) : State()
|
||||
object Success : State()
|
||||
object Failure : State()
|
||||
data class Failure(val throwable: Throwable) : State()
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -85,6 +85,8 @@ interface CryptoService {
|
||||
|
||||
fun addRoomKeysRequestListener(listener: RoomKeysRequestListener)
|
||||
|
||||
fun removeRoomKeysRequestListener(listener: RoomKeysRequestListener)
|
||||
|
||||
fun getDevicesList(callback: MatrixCallback<DevicesListResponse>)
|
||||
|
||||
fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int
|
||||
@ -96,9 +98,10 @@ interface CryptoService {
|
||||
roomId: String,
|
||||
callback: MatrixCallback<MXEncryptEventContentResult>)
|
||||
|
||||
fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult?
|
||||
@Throws(MXCryptoError::class)
|
||||
fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult
|
||||
|
||||
fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult?>)
|
||||
fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>)
|
||||
|
||||
fun getEncryptionAlgorithm(roomId: String): String?
|
||||
|
||||
|
@ -18,106 +18,65 @@
|
||||
|
||||
package im.vector.matrix.android.api.session.crypto
|
||||
|
||||
import android.text.TextUtils
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import org.matrix.olm.OlmException
|
||||
|
||||
/**
|
||||
* Represents a crypto error response.
|
||||
*/
|
||||
class MXCryptoError(var code: String,
|
||||
var message: String) {
|
||||
sealed class MXCryptoError : Throwable() {
|
||||
|
||||
/**
|
||||
* Describe the error with more details
|
||||
*/
|
||||
private var mDetailedErrorDescription: String? = null
|
||||
data class Base(val errorType: ErrorType,
|
||||
val technicalMessage: String,
|
||||
/**
|
||||
* Describe the error with more details
|
||||
*/
|
||||
val detailedErrorDescription: String? = null) : MXCryptoError()
|
||||
|
||||
/**
|
||||
* Data exception.
|
||||
* Some exceptions provide some data to describe the exception
|
||||
*/
|
||||
var mExceptionData: Any? = null
|
||||
data class OlmError(val olmException: OlmException) : MXCryptoError()
|
||||
|
||||
/**
|
||||
* @return true if the current error is an olm one.
|
||||
*/
|
||||
val isOlmError: Boolean
|
||||
get() = OLM_ERROR_CODE == code
|
||||
data class UnknownDevice(val deviceList: MXUsersDevicesMap<MXDeviceInfo>) : MXCryptoError()
|
||||
|
||||
|
||||
/**
|
||||
* @return the detailed error description
|
||||
*/
|
||||
val detailedErrorDescription: String?
|
||||
get() = if (TextUtils.isEmpty(mDetailedErrorDescription)) {
|
||||
message
|
||||
} else mDetailedErrorDescription
|
||||
|
||||
/**
|
||||
* Create a crypto error
|
||||
*
|
||||
* @param code the error code (see XX_ERROR_CODE)
|
||||
* @param shortErrorDescription the short error description
|
||||
* @param detailedErrorDescription the detailed error description
|
||||
*/
|
||||
constructor(code: String, shortErrorDescription: String, detailedErrorDescription: String?) : this(code, shortErrorDescription) {
|
||||
mDetailedErrorDescription = detailedErrorDescription
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a crypto error
|
||||
*
|
||||
* @param code the error code (see XX_ERROR_CODE)
|
||||
* @param shortErrorDescription the short error description
|
||||
* @param detailedErrorDescription the detailed error description
|
||||
* @param exceptionData the exception data
|
||||
*/
|
||||
constructor(code: String, shortErrorDescription: String, detailedErrorDescription: String?, exceptionData: Any) : this(code, shortErrorDescription) {
|
||||
mDetailedErrorDescription = detailedErrorDescription
|
||||
mExceptionData = exceptionData
|
||||
enum class ErrorType {
|
||||
ENCRYPTING_NOT_ENABLED,
|
||||
UNABLE_TO_ENCRYPT,
|
||||
UNABLE_TO_DECRYPT,
|
||||
UNKNOWN_INBOUND_SESSION_ID,
|
||||
INBOUND_SESSION_MISMATCH_ROOM_ID,
|
||||
MISSING_FIELDS,
|
||||
BAD_EVENT_FORMAT,
|
||||
MISSING_SENDER_KEY,
|
||||
MISSING_CIPHER_TEXT,
|
||||
BAD_DECRYPTED_FORMAT,
|
||||
NOT_INCLUDE_IN_RECIPIENTS,
|
||||
BAD_RECIPIENT,
|
||||
BAD_RECIPIENT_KEY,
|
||||
FORWARDED_MESSAGE,
|
||||
BAD_ROOM,
|
||||
BAD_ENCRYPTED_MESSAGE,
|
||||
DUPLICATED_MESSAGE_INDEX,
|
||||
MISSING_PROPERTY,
|
||||
OLM,
|
||||
UNKNOWN_DEVICES,
|
||||
UNKNOWN_MESSAGE_INDEX
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Error codes
|
||||
* Resource for technicalMessage
|
||||
*/
|
||||
const val ENCRYPTING_NOT_ENABLED_ERROR_CODE = "ENCRYPTING_NOT_ENABLED"
|
||||
const val UNABLE_TO_ENCRYPT_ERROR_CODE = "UNABLE_TO_ENCRYPT"
|
||||
const val UNABLE_TO_DECRYPT_ERROR_CODE = "UNABLE_TO_DECRYPT"
|
||||
const val UNKNOWN_INBOUND_SESSION_ID_ERROR_CODE = "UNKNOWN_INBOUND_SESSION_ID"
|
||||
const val INBOUND_SESSION_MISMATCH_ROOM_ID_ERROR_CODE = "INBOUND_SESSION_MISMATCH_ROOM_ID"
|
||||
const val MISSING_FIELDS_ERROR_CODE = "MISSING_FIELDS"
|
||||
const val MISSING_CIPHER_TEXT_ERROR_CODE = "MISSING_CIPHER_TEXT"
|
||||
const val NOT_INCLUDE_IN_RECIPIENTS_ERROR_CODE = "NOT_INCLUDE_IN_RECIPIENTS"
|
||||
const val BAD_RECIPIENT_ERROR_CODE = "BAD_RECIPIENT"
|
||||
const val BAD_RECIPIENT_KEY_ERROR_CODE = "BAD_RECIPIENT_KEY"
|
||||
const val FORWARDED_MESSAGE_ERROR_CODE = "FORWARDED_MESSAGE"
|
||||
const val BAD_ROOM_ERROR_CODE = "BAD_ROOM"
|
||||
const val BAD_ENCRYPTED_MESSAGE_ERROR_CODE = "BAD_ENCRYPTED_MESSAGE"
|
||||
const val DUPLICATED_MESSAGE_INDEX_ERROR_CODE = "DUPLICATED_MESSAGE_INDEX"
|
||||
const val MISSING_PROPERTY_ERROR_CODE = "MISSING_PROPERTY"
|
||||
const val OLM_ERROR_CODE = "OLM_ERROR_CODE"
|
||||
const val UNKNOWN_DEVICES_CODE = "UNKNOWN_DEVICES_CODE"
|
||||
const val UNKNOWN_MESSAGE_INDEX = "UNKNOWN_MESSAGE_INDEX"
|
||||
|
||||
/**
|
||||
* short error reasons
|
||||
*/
|
||||
const val UNABLE_TO_DECRYPT = "Unable to decrypt"
|
||||
const val UNABLE_TO_ENCRYPT = "Unable to encrypt"
|
||||
|
||||
/**
|
||||
* Detailed error reasons
|
||||
*/
|
||||
const val ENCRYPTING_NOT_ENABLED_REASON = "Encryption not enabled"
|
||||
const val UNABLE_TO_ENCRYPT_REASON = "Unable to encrypt %s"
|
||||
const val UNABLE_TO_DECRYPT_REASON = "Unable to decrypt %1\$s. Algorithm: %2\$s"
|
||||
const val OLM_REASON = "OLM error: %1\$s"
|
||||
const val DETAILLED_OLM_REASON = "Unable to decrypt %1\$s. OLM error: %2\$s"
|
||||
const val DETAILED_OLM_REASON = "Unable to decrypt %1\$s. OLM error: %2\$s"
|
||||
const val UNKNOWN_INBOUND_SESSION_ID_REASON = "Unknown inbound session id"
|
||||
const val INBOUND_SESSION_MISMATCH_ROOM_ID_REASON = "Mismatched room_id for inbound group session (expected %1\$s, was %2\$s)"
|
||||
const val MISSING_FIELDS_REASON = "Missing fields in input"
|
||||
const val BAD_EVENT_FORMAT_TEXT_REASON = "Bad event format"
|
||||
const val MISSING_SENDER_KEY_TEXT_REASON = "Missing senderKey"
|
||||
const val MISSING_CIPHER_TEXT_REASON = "Missing ciphertext"
|
||||
const val BAD_DECRYPTED_FORMAT_TEXT_REASON = "Bad decrypted event format"
|
||||
const val NOT_INCLUDED_IN_RECIPIENT_REASON = "Not included in recipients"
|
||||
const val BAD_RECIPIENT_REASON = "Message was intended for %1\$s"
|
||||
const val BAD_RECIPIENT_KEY_REASON = "Message not intended for this device"
|
||||
@ -126,7 +85,9 @@ class MXCryptoError(var code: String,
|
||||
const val BAD_ENCRYPTED_MESSAGE_REASON = "Bad Encrypted Message"
|
||||
const val DUPLICATE_MESSAGE_INDEX_REASON = "Duplicate message index, possible replay attack %1\$s"
|
||||
const val ERROR_MISSING_PROPERTY_REASON = "No '%1\$s' property. Cannot prevent unknown-key attack"
|
||||
const val UNKNOWN_DEVICES_REASON = "This room contains unknown devices which have not been verified.\n" + "We strongly recommend you verify them before continuing."
|
||||
const val NO_MORE_ALGORITHM_REASON = "Room was previously configured to use encryption, but is no longer." + " Perhaps the homeserver is hiding the configuration event."
|
||||
const val UNKNOWN_DEVICES_REASON = "This room contains unknown devices which have not been verified.\n" +
|
||||
"We strongly recommend you verify them before continuing."
|
||||
const val NO_MORE_ALGORITHM_REASON = "Room was previously configured to use encryption, but is no longer." +
|
||||
" Perhaps the homeserver is hiding the configuration event."
|
||||
}
|
||||
}
|
||||
}
|
@ -40,7 +40,8 @@ interface KeysBackupService {
|
||||
* @param keysBackupCreationInfo the info object from [prepareKeysBackupVersion].
|
||||
* @param callback Asynchronous callback
|
||||
*/
|
||||
fun createKeysBackupVersion(keysBackupCreationInfo: MegolmBackupCreationInfo, callback: MatrixCallback<KeysVersion>)
|
||||
fun createKeysBackupVersion(keysBackupCreationInfo: MegolmBackupCreationInfo,
|
||||
callback: MatrixCallback<KeysVersion>)
|
||||
|
||||
/**
|
||||
* Facility method to get the total number of locally stored keys
|
||||
@ -58,7 +59,8 @@ interface KeysBackupService {
|
||||
* @param progressListener the callback to follow the progress
|
||||
* @param callback the main callback
|
||||
*/
|
||||
fun backupAllGroupSessions(progressListener: ProgressListener?, callback: MatrixCallback<Unit>?)
|
||||
fun backupAllGroupSessions(progressListener: ProgressListener?,
|
||||
callback: MatrixCallback<Unit>?)
|
||||
|
||||
/**
|
||||
* Check trust on a key backup version.
|
||||
@ -66,7 +68,8 @@ interface KeysBackupService {
|
||||
* @param keysBackupVersion the backup version to check.
|
||||
* @param callback block called when the operations completes.
|
||||
*/
|
||||
fun getKeysBackupTrust(keysBackupVersion: KeysVersionResult, callback: MatrixCallback<KeysBackupVersionTrust>)
|
||||
fun getKeysBackupTrust(keysBackupVersion: KeysVersionResult,
|
||||
callback: MatrixCallback<KeysBackupVersionTrust>)
|
||||
|
||||
/**
|
||||
* Return the current progress of the backup
|
||||
@ -80,7 +83,8 @@ interface KeysBackupService {
|
||||
* @param version the backup version
|
||||
* @param callback
|
||||
*/
|
||||
fun getVersion(version: String, callback: MatrixCallback<KeysVersionResult?>)
|
||||
fun getVersion(version: String,
|
||||
callback: MatrixCallback<KeysVersionResult?>)
|
||||
|
||||
/**
|
||||
* This method fetches the last backup version on the server, then compare to the currently backup version use.
|
||||
@ -114,7 +118,9 @@ interface KeysBackupService {
|
||||
* @param progressListener a progress listener, as generating private key from password may take a while
|
||||
* @param callback Asynchronous callback
|
||||
*/
|
||||
fun prepareKeysBackupVersion(password: String?, progressListener: ProgressListener?, callback: MatrixCallback<MegolmBackupCreationInfo>)
|
||||
fun prepareKeysBackupVersion(password: String?,
|
||||
progressListener: ProgressListener?,
|
||||
callback: MatrixCallback<MegolmBackupCreationInfo>)
|
||||
|
||||
/**
|
||||
* Delete a keys backup version. It will delete all backed up keys on the server, and the backup itself.
|
||||
@ -123,7 +129,8 @@ interface KeysBackupService {
|
||||
* @param version the backup version to delete.
|
||||
* @param callback Asynchronous callback
|
||||
*/
|
||||
fun deleteBackup(version: String, callback: MatrixCallback<Unit>?)
|
||||
fun deleteBackup(version: String,
|
||||
callback: MatrixCallback<Unit>?)
|
||||
|
||||
/**
|
||||
* Ask if the backup on the server contains keys that we may do not have locally.
|
||||
@ -139,7 +146,9 @@ interface KeysBackupService {
|
||||
* @param trust the trust to set to the keys backup.
|
||||
* @param callback block called when the operations completes.
|
||||
*/
|
||||
fun trustKeysBackupVersion(keysBackupVersion: KeysVersionResult, trust: Boolean, callback: MatrixCallback<Unit>)
|
||||
fun trustKeysBackupVersion(keysBackupVersion: KeysVersionResult,
|
||||
trust: Boolean,
|
||||
callback: MatrixCallback<Unit>)
|
||||
|
||||
/**
|
||||
* Set trust on a keys backup version.
|
||||
@ -148,7 +157,9 @@ interface KeysBackupService {
|
||||
* @param recoveryKey the recovery key to challenge with the key backup public key.
|
||||
* @param callback block called when the operations completes.
|
||||
*/
|
||||
fun trustKeysBackupVersionWithRecoveryKey(keysBackupVersion: KeysVersionResult, recoveryKey: String, callback: MatrixCallback<Unit>)
|
||||
fun trustKeysBackupVersionWithRecoveryKey(keysBackupVersion: KeysVersionResult,
|
||||
recoveryKey: String,
|
||||
callback: MatrixCallback<Unit>)
|
||||
|
||||
/**
|
||||
* Set trust on a keys backup version.
|
||||
@ -157,7 +168,9 @@ interface KeysBackupService {
|
||||
* @param password the pass phrase to challenge with the keyBackupVersion public key.
|
||||
* @param callback block called when the operations completes.
|
||||
*/
|
||||
fun trustKeysBackupVersionWithPassphrase(keysBackupVersion: KeysVersionResult, password: String, callback: MatrixCallback<Unit>)
|
||||
fun trustKeysBackupVersionWithPassphrase(keysBackupVersion: KeysVersionResult,
|
||||
password: String,
|
||||
callback: MatrixCallback<Unit>)
|
||||
|
||||
/**
|
||||
* Restore a backup with a recovery key from a given backup version stored on the homeserver.
|
||||
@ -169,7 +182,11 @@ interface KeysBackupService {
|
||||
* @param stepProgressListener the step progress listener
|
||||
* @param callback Callback. It provides the number of found keys and the number of successfully imported keys.
|
||||
*/
|
||||
fun restoreKeysWithRecoveryKey(keysVersionResult: KeysVersionResult, recoveryKey: String, roomId: String?, sessionId: String?, stepProgressListener: StepProgressListener?, callback: MatrixCallback<ImportRoomKeysResult>)
|
||||
fun restoreKeysWithRecoveryKey(keysVersionResult: KeysVersionResult,
|
||||
recoveryKey: String, roomId: String?,
|
||||
sessionId: String?,
|
||||
stepProgressListener: StepProgressListener?,
|
||||
callback: MatrixCallback<ImportRoomKeysResult>)
|
||||
|
||||
/**
|
||||
* Restore a backup with a password from a given backup version stored on the homeserver.
|
||||
@ -181,7 +198,12 @@ interface KeysBackupService {
|
||||
* @param stepProgressListener the step progress listener
|
||||
* @param callback Callback. It provides the number of found keys and the number of successfully imported keys.
|
||||
*/
|
||||
fun restoreKeyBackupWithPassword(keysBackupVersion: KeysVersionResult, password: String, roomId: String?, sessionId: String?, stepProgressListener: StepProgressListener?, callback: MatrixCallback<ImportRoomKeysResult>)
|
||||
fun restoreKeyBackupWithPassword(keysBackupVersion: KeysVersionResult,
|
||||
password: String,
|
||||
roomId: String?,
|
||||
sessionId: String?,
|
||||
stepProgressListener: StepProgressListener?,
|
||||
callback: MatrixCallback<ImportRoomKeysResult>)
|
||||
|
||||
val keysBackupVersion: KeysVersionResult?
|
||||
val currentBackupVersion: String?
|
||||
|
@ -20,12 +20,14 @@ import android.text.TextUtils
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
import im.vector.matrix.android.api.util.JsonDict
|
||||
import im.vector.matrix.android.internal.crypto.MXEventDecryptionResult
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import org.json.JSONObject
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
typealias Content = JsonDict
|
||||
|
||||
@ -79,6 +81,17 @@ data class Event(
|
||||
@Json(name = "redacts") val redacts: String? = null
|
||||
) {
|
||||
|
||||
|
||||
@Transient
|
||||
var mxDecryptionResult: OlmDecryptionResult? = null
|
||||
|
||||
@Transient
|
||||
var mCryptoError: MXCryptoError.ErrorType? = null
|
||||
|
||||
@Transient
|
||||
var sendState: SendState = SendState.UNKNOWN
|
||||
|
||||
|
||||
/**
|
||||
* Check if event is a state event.
|
||||
* @return true if event is state event.
|
||||
@ -91,42 +104,6 @@ data class Event(
|
||||
// Crypto
|
||||
//==============================================================================================================
|
||||
|
||||
/**
|
||||
* For encrypted events, the plaintext payload for the event.
|
||||
* This is a small MXEvent instance with typically value for `type` and 'content' fields.
|
||||
*/
|
||||
@Transient
|
||||
var mClearEvent: Event? = null
|
||||
private set
|
||||
|
||||
/**
|
||||
* Curve25519 key which we believe belongs to the sender of the event.
|
||||
* See `senderKey` property.
|
||||
*/
|
||||
@Transient
|
||||
private var mSenderCurve25519Key: String? = null
|
||||
|
||||
/**
|
||||
* Ed25519 key which the sender of this event (for olm) or the creator of the megolm session (for megolm) claims to own.
|
||||
* See `claimedEd25519Key` property.
|
||||
*/
|
||||
@Transient
|
||||
private var mClaimedEd25519Key: String? = null
|
||||
|
||||
/**
|
||||
* Curve25519 keys of devices involved in telling us about the senderCurve25519Key and claimedEd25519Key.
|
||||
* See `forwardingCurve25519KeyChain` property.
|
||||
*/
|
||||
@Transient
|
||||
private var mForwardingCurve25519KeyChain: List<String> = ArrayList()
|
||||
|
||||
/**
|
||||
* Decryption error
|
||||
*/
|
||||
@Transient
|
||||
var mCryptoError: MXCryptoError? = null
|
||||
private set
|
||||
|
||||
/**
|
||||
* @return true if this event is encrypted.
|
||||
*/
|
||||
@ -134,102 +111,107 @@ data class Event(
|
||||
return TextUtils.equals(type, EventType.ENCRYPTED)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the clear data on this event.
|
||||
* This is used after decrypting an event; it should not be used by applications.
|
||||
*
|
||||
* @param decryptionResult the decryption result, including the plaintext and some key info.
|
||||
*/
|
||||
internal fun setClearData(decryptionResult: MXEventDecryptionResult?) {
|
||||
mClearEvent = null
|
||||
if (decryptionResult != null) {
|
||||
if (decryptionResult.clearEvent != null) {
|
||||
val adapter = MoshiProvider.providesMoshi().adapter(Event::class.java)
|
||||
mClearEvent = adapter.fromJsonValue(decryptionResult.clearEvent)
|
||||
|
||||
if (mClearEvent != null) {
|
||||
mSenderCurve25519Key = decryptionResult.senderCurve25519Key
|
||||
mClaimedEd25519Key = decryptionResult.claimedEd25519Key
|
||||
mForwardingCurve25519KeyChain = decryptionResult.forwardingCurve25519KeyChain
|
||||
|
||||
// For encrypted events with relation, the m.relates_to is kept in clear, so we need to put it back
|
||||
// in the clear event
|
||||
try {
|
||||
content?.get("m.relates_to")?.let { clearRelates ->
|
||||
mClearEvent = mClearEvent?.copy(
|
||||
content = HashMap(mClearEvent!!.content).apply {
|
||||
this["m.relates_to"] = clearRelates
|
||||
}
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Unable to restore 'm.relates_to' the clear event")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
mCryptoError = null
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The curve25519 key that sent this event.
|
||||
*/
|
||||
fun getSenderKey(): String? {
|
||||
return mClearEvent?.mSenderCurve25519Key ?: mSenderCurve25519Key
|
||||
return mxDecryptionResult?.senderKey
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The additional keys the sender of this encrypted event claims to possess.
|
||||
*/
|
||||
fun getKeysClaimed(): Map<String, String> {
|
||||
val res = HashMap<String, String>()
|
||||
|
||||
val claimedEd25519Key = if (null != mClearEvent) mClearEvent!!.mClaimedEd25519Key else mClaimedEd25519Key
|
||||
|
||||
if (null != claimedEd25519Key) {
|
||||
res["ed25519"] = claimedEd25519Key
|
||||
}
|
||||
|
||||
return res
|
||||
return mxDecryptionResult?.keysClaimed ?: HashMap()
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the event type
|
||||
*/
|
||||
fun getClearType(): String {
|
||||
return mClearEvent?.type ?: type
|
||||
return mxDecryptionResult?.payload?.get("type")?.toString() ?: type
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the event content
|
||||
*/
|
||||
fun getClearContent(): Content? {
|
||||
return mClearEvent?.content ?: content
|
||||
return mxDecryptionResult?.payload?.get("content") as? Content ?: content
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the linked crypto error
|
||||
*/
|
||||
fun getCryptoError(): MXCryptoError? {
|
||||
return mCryptoError
|
||||
fun toContentStringWithIndent(): String {
|
||||
val contentMap = toContent()?.toMutableMap() ?: HashMap()
|
||||
return JSONObject(contentMap).toString(4)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the linked crypto error
|
||||
*
|
||||
* @param error the new crypto error.
|
||||
*/
|
||||
fun setCryptoError(error: MXCryptoError?) {
|
||||
mCryptoError = error
|
||||
if (null != error) {
|
||||
mClearEvent = null
|
||||
}
|
||||
fun toClearContentStringWithIndent(): String? {
|
||||
val contentMap = this.mxDecryptionResult?.payload?.toMutableMap()
|
||||
val adapter = MoshiProvider.providesMoshi().adapter(Map::class.java)
|
||||
return contentMap?.let { JSONObject(adapter.toJson(it)).toString(4) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if the event is redacted
|
||||
*/
|
||||
fun isRedacted() = unsignedData?.redactedEvent != null
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Event
|
||||
|
||||
if (type != other.type) return false
|
||||
if (eventId != other.eventId) return false
|
||||
if (content != other.content) return false
|
||||
if (prevContent != other.prevContent) return false
|
||||
if (originServerTs != other.originServerTs) return false
|
||||
if (senderId != other.senderId) return false
|
||||
if (stateKey != other.stateKey) return false
|
||||
if (roomId != other.roomId) return false
|
||||
if (unsignedData != other.unsignedData) return false
|
||||
if (redacts != other.redacts) return false
|
||||
if (mxDecryptionResult != other.mxDecryptionResult) return false
|
||||
if (mCryptoError != other.mCryptoError) return false
|
||||
if (sendState != other.sendState) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = type.hashCode()
|
||||
result = 31 * result + (eventId?.hashCode() ?: 0)
|
||||
result = 31 * result + (content?.hashCode() ?: 0)
|
||||
result = 31 * result + (prevContent?.hashCode() ?: 0)
|
||||
result = 31 * result + (originServerTs?.hashCode() ?: 0)
|
||||
result = 31 * result + (senderId?.hashCode() ?: 0)
|
||||
result = 31 * result + (stateKey?.hashCode() ?: 0)
|
||||
result = 31 * result + (roomId?.hashCode() ?: 0)
|
||||
result = 31 * result + (unsignedData?.hashCode() ?: 0)
|
||||
result = 31 * result + (redacts?.hashCode() ?: 0)
|
||||
result = 31 * result + (mxDecryptionResult?.hashCode() ?: 0)
|
||||
result = 31 * result + (mCryptoError?.hashCode() ?: 0)
|
||||
result = 31 * result + sendState.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
fun Event.isTextMessage(): Boolean {
|
||||
return getClearType() == EventType.MESSAGE
|
||||
&& when (getClearContent()?.toModel<MessageContent>()?.type) {
|
||||
MessageType.MSGTYPE_TEXT,
|
||||
MessageType.MSGTYPE_EMOTE,
|
||||
MessageType.MSGTYPE_NOTICE -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun Event.isImageMessage(): Boolean {
|
||||
return getClearType() == EventType.MESSAGE
|
||||
&& when (getClearContent()?.toModel<MessageContent>()?.type) {
|
||||
MessageType.MSGTYPE_IMAGE -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
@ -17,7 +17,7 @@ package im.vector.matrix.android.api.session.events.model
|
||||
|
||||
|
||||
/**
|
||||
* Constants defining known event relation types from Matrix specifications.
|
||||
* Constants defining known event relation types from Matrix specifications
|
||||
*/
|
||||
object RelationType {
|
||||
|
||||
@ -25,7 +25,7 @@ object RelationType {
|
||||
const val ANNOTATION = "m.annotation"
|
||||
/** Lets you define an event which replaces an existing event.*/
|
||||
const val REPLACE = "m.replace"
|
||||
/** ets you define an event which references an existing event.*/
|
||||
/** Lets you define an event which references an existing event.*/
|
||||
const val REFERENCE = "m.reference"
|
||||
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.file
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
|
||||
import java.io.File
|
||||
|
||||
|
||||
/**
|
||||
* This interface defines methods to get files.
|
||||
*/
|
||||
interface FileService {
|
||||
|
||||
enum class DownloadMode {
|
||||
/**
|
||||
* Download file in external storage
|
||||
*/
|
||||
TO_EXPORT,
|
||||
/**
|
||||
* Download file in cache
|
||||
*/
|
||||
FOR_INTERNAL_USE
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a file.
|
||||
* Result will be a decrypted file, stored in the cache folder. id parameter will be used to create a sub folder to avoid name collision.
|
||||
* You can pass the eventId
|
||||
*/
|
||||
fun downloadFile(
|
||||
downloadMode: DownloadMode,
|
||||
id: String,
|
||||
fileName: String,
|
||||
url: String?,
|
||||
elementToDecrypt: ElementToDecrypt?,
|
||||
callback: MatrixCallback<File>)
|
||||
}
|
@ -17,7 +17,7 @@ package im.vector.matrix.android.api.session.pushers
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import java.util.*
|
||||
import java.util.UUID
|
||||
|
||||
|
||||
interface PushersService {
|
||||
|
@ -47,8 +47,8 @@ interface Room :
|
||||
* A live [RoomSummary] associated with the room
|
||||
* You can observe this summary to get dynamic data from this room.
|
||||
*/
|
||||
val liveRoomSummary: LiveData<RoomSummary>
|
||||
fun liveRoomSummary(): LiveData<RoomSummary>
|
||||
|
||||
val roomSummary: RoomSummary?
|
||||
fun roomSummary(): RoomSummary?
|
||||
|
||||
}
|
@ -30,20 +30,17 @@ interface RoomDirectoryService {
|
||||
/**
|
||||
* Get rooms from directory
|
||||
*/
|
||||
fun getPublicRooms(server: String?,
|
||||
publicRoomsParams: PublicRoomsParams,
|
||||
callback: MatrixCallback<PublicRoomsResponse>): Cancelable
|
||||
fun getPublicRooms(server: String?, publicRoomsParams: PublicRoomsParams, callback: MatrixCallback<PublicRoomsResponse>): Cancelable
|
||||
|
||||
/**
|
||||
* Join a room by id
|
||||
*/
|
||||
fun joinRoom(roomId: String,
|
||||
callback: MatrixCallback<Unit>)
|
||||
fun joinRoom(roomId: String, callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Fetches the overall metadata about protocols supported by the homeserver.
|
||||
* Includes both the available protocols and all fields required for queries against each protocol.
|
||||
*/
|
||||
fun getThirdPartyProtocol(callback: MatrixCallback<Map<String, ThirdPartyProtocol>>)
|
||||
fun getThirdPartyProtocol(callback: MatrixCallback<Map<String, ThirdPartyProtocol>>): Cancelable
|
||||
|
||||
}
|
@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
|
||||
/**
|
||||
* This interface defines methods to get rooms. It's implemented at the session level.
|
||||
@ -27,10 +28,18 @@ import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||
interface RoomService {
|
||||
|
||||
/**
|
||||
* Create a room
|
||||
* Create a room asynchronously
|
||||
*/
|
||||
fun createRoom(createRoomParams: CreateRoomParams,
|
||||
callback: MatrixCallback<String>)
|
||||
fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback<String>): Cancelable
|
||||
|
||||
/**
|
||||
* Join a room by id
|
||||
* @param roomId the roomId of the room to join
|
||||
* @param viaServers the servers to attempt to join the room through. One of the servers must be participating in the room.
|
||||
*/
|
||||
fun joinRoom(roomId: String,
|
||||
viaServers: List<String> = emptyList(),
|
||||
callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Get a room from a roomId
|
||||
|
@ -14,16 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.session;
|
||||
package im.vector.matrix.android.api.session.room.failure
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
|
||||
import javax.inject.Scope;
|
||||
sealed class CreateRoomFailure : Failure.FeatureFailure() {
|
||||
|
||||
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
object CreatedWithTimeout: CreateRoomFailure()
|
||||
|
||||
@Scope
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
public @interface SessionScope {}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.room.failure
|
||||
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
|
||||
sealed class JoinRoomFailure : Failure.FeatureFailure() {
|
||||
|
||||
object JoinedWithTimeout : JoinRoomFailure()
|
||||
|
||||
}
|
@ -30,7 +30,7 @@ interface MembershipService {
|
||||
* This methods load all room members if it was done yet.
|
||||
* @return a [Cancelable]
|
||||
*/
|
||||
fun loadRoomMembersIfNeeded(): Cancelable
|
||||
fun loadRoomMembersIfNeeded(matrixCallback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Return the roomMember with userId or null.
|
||||
@ -52,16 +52,17 @@ interface MembershipService {
|
||||
/**
|
||||
* Invite a user in the room
|
||||
*/
|
||||
fun invite(userId: String, callback: MatrixCallback<Unit>)
|
||||
fun invite(userId: String, callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Join the room, or accept an invitation.
|
||||
*/
|
||||
fun join(callback: MatrixCallback<Unit>)
|
||||
|
||||
fun join(viaServers: List<String> = emptyList(), callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Leave the room, or reject an invitation.
|
||||
*/
|
||||
fun leave(callback: MatrixCallback<Unit>)
|
||||
fun leave(callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
}
|
@ -16,8 +16,9 @@
|
||||
|
||||
package im.vector.matrix.android.api.session.room.model
|
||||
|
||||
import im.vector.matrix.android.api.session.user.model.User
|
||||
|
||||
data class ReadReceipt(
|
||||
val userId: String,
|
||||
val eventId: String,
|
||||
val user: User,
|
||||
val originServerTs: Long
|
||||
)
|
@ -34,5 +34,10 @@ data class RoomSummary(
|
||||
val notificationCount: Int = 0,
|
||||
val highlightCount: Int = 0,
|
||||
val tags: List<RoomTag> = emptyList(),
|
||||
val membership: Membership = Membership.NONE
|
||||
)
|
||||
val membership: Membership = Membership.NONE,
|
||||
val versioningState: VersioningState = VersioningState.NONE
|
||||
) {
|
||||
|
||||
val isVersioned: Boolean
|
||||
get() = versioningState != VersioningState.NONE
|
||||
}
|
@ -14,8 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.home.room
|
||||
package im.vector.matrix.android.api.session.room.model
|
||||
|
||||
import im.vector.riotx.core.utils.RxStore
|
||||
|
||||
class VisibleRoomStore : RxStore<String>()
|
||||
enum class VersioningState {
|
||||
NONE,
|
||||
UPGRADED_ROOM_NOT_JOINED,
|
||||
UPGRADED_ROOM_JOINED
|
||||
}
|
@ -29,7 +29,6 @@ import im.vector.matrix.android.api.session.room.model.PowerLevels
|
||||
import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
|
||||
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
|
||||
import im.vector.matrix.android.internal.auth.data.ThreePidMedium
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Parameter to create a room, with facilities functions to configure it
|
||||
@ -128,12 +127,12 @@ class CreateRoomParams {
|
||||
contentMap["algorithm"] = algorithm
|
||||
|
||||
val algoEvent = Event(type = EventType.ENCRYPTION,
|
||||
stateKey = "",
|
||||
content = contentMap.toContent()
|
||||
stateKey = "",
|
||||
content = contentMap.toContent()
|
||||
)
|
||||
|
||||
if (null == initialStates) {
|
||||
initialStates = Arrays.asList<Event>(algoEvent)
|
||||
initialStates = mutableListOf(algoEvent)
|
||||
} else {
|
||||
initialStates!!.add(algoEvent)
|
||||
}
|
||||
@ -162,11 +161,11 @@ class CreateRoomParams {
|
||||
contentMap["history_visibility"] = historyVisibility
|
||||
|
||||
val historyVisibilityEvent = Event(type = EventType.STATE_HISTORY_VISIBILITY,
|
||||
stateKey = "",
|
||||
content = contentMap.toContent())
|
||||
stateKey = "",
|
||||
content = contentMap.toContent())
|
||||
|
||||
if (null == initialStates) {
|
||||
initialStates = Arrays.asList<Event>(historyVisibilityEvent)
|
||||
initialStates = mutableListOf(historyVisibilityEvent)
|
||||
} else {
|
||||
initialStates!!.add(historyVisibilityEvent)
|
||||
}
|
||||
@ -202,21 +201,15 @@ class CreateRoomParams {
|
||||
*/
|
||||
fun isDirect(): Boolean {
|
||||
return preset == CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT
|
||||
&& isDirect == true
|
||||
&& (1 == getInviteCount() || 1 == getInvite3PidCount())
|
||||
&& isDirect == true
|
||||
&& (1 == getInviteCount() || 1 == getInvite3PidCount())
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the first invited user id
|
||||
*/
|
||||
fun getFirstInvitedUserId(): String? {
|
||||
if (0 != getInviteCount()) {
|
||||
return invitedUserIds!![0]
|
||||
}
|
||||
|
||||
return if (0 != getInvite3PidCount()) {
|
||||
invite3pids!![0].address
|
||||
} else null
|
||||
return invitedUserIds?.firstOrNull() ?: invite3pids?.firstOrNull()?.address
|
||||
}
|
||||
|
||||
/**
|
||||
@ -229,14 +222,13 @@ class CreateRoomParams {
|
||||
credentials: Credentials,
|
||||
ids: List<String>) {
|
||||
for (id in ids) {
|
||||
if (Patterns.EMAIL_ADDRESS.matcher(id).matches()) {
|
||||
if (Patterns.EMAIL_ADDRESS.matcher(id).matches() && hsConfig.identityServerUri != null) {
|
||||
if (null == invite3pids) {
|
||||
invite3pids = ArrayList()
|
||||
}
|
||||
|
||||
val pid = Invite3Pid(idServer = hsConfig.identityServerUri.host!!,
|
||||
medium = ThreePidMedium.EMAIL,
|
||||
address = id)
|
||||
medium = ThreePidMedium.EMAIL,
|
||||
address = id)
|
||||
|
||||
invite3pids!!.add(pid)
|
||||
} else if (isUserId(id)) {
|
||||
|
@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.matrix.android.api.session.room.model.create
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* A link to an old room in case of room versioning
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Predecessor(
|
||||
@Json(name = "room_id") val roomId: String? = null,
|
||||
@Json(name = "event_id") val eventId: String? = null
|
||||
)
|
@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.room.model.create
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Content of a m.room.create type event
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class RoomCreateContent(
|
||||
@Json(name = "creator") val creator: String? = null,
|
||||
@Json(name = "room_version") val roomVersion: String? = null,
|
||||
@Json(name = "predecessor") val predecessor: Predecessor? = null
|
||||
)
|
||||
|
||||
|
@ -42,7 +42,7 @@ data class MessageAudioContent(
|
||||
/**
|
||||
* Required. Required if the file is not encrypted. The URL (typically MXC URI) to the audio clip.
|
||||
*/
|
||||
@Json(name = "url") val url: String? = null,
|
||||
@Json(name = "url") override val url: String? = null,
|
||||
|
||||
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||
@Json(name = "m.new_content") override val newContent: Content? = null,
|
||||
@ -51,4 +51,4 @@ data class MessageAudioContent(
|
||||
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
|
||||
*/
|
||||
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
|
||||
) : MessageEncyptedContent
|
||||
) : MessageEncryptedContent
|
||||
|
@ -25,4 +25,9 @@ interface MessageContent {
|
||||
val body: String
|
||||
val relatesTo: RelationDefaultContent?
|
||||
val newContent: Content?
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun MessageContent?.isReply(): Boolean {
|
||||
return this?.relatesTo?.inReplyTo?.eventId != null
|
||||
}
|
||||
|
@ -20,8 +20,18 @@ import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
|
||||
|
||||
|
||||
/**
|
||||
* Interface for message which can contains encrypted data
|
||||
* Interface for message which can contains an encrypted file
|
||||
*/
|
||||
interface MessageEncyptedContent : MessageContent {
|
||||
interface MessageEncryptedContent : MessageContent {
|
||||
/**
|
||||
* Required. Required if the file is unencrypted. The URL (typically MXC URI) to the image.
|
||||
*/
|
||||
val url: String?
|
||||
|
||||
val encryptedFileInfo: EncryptedFileInfo?
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the url of the encrypted file or of the file
|
||||
*/
|
||||
fun MessageEncryptedContent.getFileUrl() = encryptedFileInfo?.url ?: url
|
@ -16,6 +16,7 @@
|
||||
|
||||
package im.vector.matrix.android.api.session.room.model.message
|
||||
|
||||
import android.content.ClipDescription
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.api.session.events.model.Content
|
||||
@ -47,10 +48,22 @@ data class MessageFileContent(
|
||||
/**
|
||||
* Required. Required if the file is unencrypted. The URL (typically MXC URI) to the file.
|
||||
*/
|
||||
@Json(name = "url") val url: String? = null,
|
||||
@Json(name = "url") override val url: String? = null,
|
||||
|
||||
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||
@Json(name = "m.new_content") override val newContent: Content? = null,
|
||||
|
||||
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
|
||||
) : MessageEncyptedContent
|
||||
) : MessageEncryptedContent {
|
||||
|
||||
fun getMimeType(): String {
|
||||
// Mimetype default to plain text, should not be used
|
||||
return encryptedFileInfo?.mimetype
|
||||
?: info?.mimeType
|
||||
?: ClipDescription.MIMETYPE_TEXT_PLAIN
|
||||
}
|
||||
|
||||
fun getFileName(): String {
|
||||
return filename ?: body
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ data class MessageImageContent(
|
||||
/**
|
||||
* Required. Required if the file is unencrypted. The URL (typically MXC URI) to the image.
|
||||
*/
|
||||
@Json(name = "url") val url: String? = null,
|
||||
@Json(name = "url") override val url: String? = null,
|
||||
|
||||
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||
@Json(name = "m.new_content") override val newContent: Content? = null,
|
||||
@ -52,4 +52,4 @@ data class MessageImageContent(
|
||||
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
|
||||
*/
|
||||
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
|
||||
) : MessageEncyptedContent
|
||||
) : MessageEncryptedContent
|
||||
|
@ -42,7 +42,7 @@ data class MessageVideoContent(
|
||||
/**
|
||||
* Required. Required if the file is unencrypted. The URL (typically MXC URI) to the video clip.
|
||||
*/
|
||||
@Json(name = "url") val url: String? = null,
|
||||
@Json(name = "url") override val url: String? = null,
|
||||
|
||||
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||
@Json(name = "m.new_content") override val newContent: Content? = null,
|
||||
@ -51,4 +51,4 @@ data class MessageVideoContent(
|
||||
* Required if the file is encrypted. Information on the encrypted file, as specified in End-to-end encryption.
|
||||
*/
|
||||
@Json(name = "file") override val encryptedFileInfo: EncryptedFileInfo? = null
|
||||
) : MessageEncyptedContent
|
||||
) : MessageEncryptedContent
|
||||
|
@ -16,7 +16,10 @@
|
||||
|
||||
package im.vector.matrix.android.api.session.room.model.relation
|
||||
|
||||
import im.vector.matrix.android.api.session.events.model.RelationType
|
||||
|
||||
interface RelationContent {
|
||||
/** See [RelationType] for known possible values */
|
||||
val type: String?
|
||||
val eventId: String?
|
||||
val inReplyTo: ReplyToContent?
|
||||
|
@ -16,8 +16,10 @@
|
||||
package im.vector.matrix.android.api.session.room.model.relation
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
|
||||
/**
|
||||
@ -51,7 +53,8 @@ interface RelationService {
|
||||
* @param reaction the reaction (preferably emoji)
|
||||
* @param targetEventId the id of the event being reacted
|
||||
*/
|
||||
fun sendReaction(reaction: String, targetEventId: String): Cancelable
|
||||
fun sendReaction(reaction: String,
|
||||
targetEventId: String): Cancelable
|
||||
|
||||
|
||||
/**
|
||||
@ -60,7 +63,9 @@ interface RelationService {
|
||||
* @param targetEventId the id of the event being reacted
|
||||
* @param myUserId used to know if a reaction event was made by the user
|
||||
*/
|
||||
fun undoReaction(reaction: String, targetEventId: String, myUserId: String)//: Cancelable
|
||||
fun undoReaction(reaction: String,
|
||||
targetEventId: String,
|
||||
myUserId: String)//: Cancelable
|
||||
|
||||
|
||||
/**
|
||||
@ -69,7 +74,30 @@ interface RelationService {
|
||||
* @param newBodyText The edited body
|
||||
* @param compatibilityBodyText The text that will appear on clients that don't support yet edition
|
||||
*/
|
||||
fun editTextMessage(targetEventId: String, newBodyText: String, newBodyAutoMarkdown: Boolean, compatibilityBodyText: String = "* $newBodyText"): Cancelable
|
||||
fun editTextMessage(targetEventId: String,
|
||||
msgType: String,
|
||||
newBodyText: String,
|
||||
newBodyAutoMarkdown: Boolean,
|
||||
compatibilityBodyText: String = "* $newBodyText"): Cancelable
|
||||
|
||||
|
||||
/**
|
||||
* Edit a reply. This is a special case because replies contains fallback text as a prefix.
|
||||
* This method will take the new body (stripped from fallbacks) and re-add them before sending.
|
||||
* @param replyToEdit The event to edit
|
||||
* @param originalTimelineEvent the message that this reply (being edited) is relating to
|
||||
* @param newBodyText The edited body (stripped from in reply to content)
|
||||
* @param compatibilityBodyText The text that will appear on clients that don't support yet edition
|
||||
*/
|
||||
fun editReply(replyToEdit: TimelineEvent,
|
||||
originalTimelineEvent: TimelineEvent,
|
||||
newBodyText: String,
|
||||
compatibilityBodyText: String = "* $newBodyText"): Cancelable
|
||||
|
||||
/**
|
||||
* Get's the edit history of the given event
|
||||
*/
|
||||
fun fetchEditHistory(eventId: String, callback: MatrixCallback<List<Event>>)
|
||||
|
||||
|
||||
/**
|
||||
@ -77,8 +105,13 @@ interface RelationService {
|
||||
* https://matrix.org/docs/spec/client_server/r0.4.0.html#id350
|
||||
* @param eventReplied the event referenced by the reply
|
||||
* @param replyText the reply text
|
||||
* @param autoMarkdown If true, the SDK will generate a formatted HTML message from the body text if markdown syntax is present
|
||||
*/
|
||||
fun replyToMessage(eventReplied: Event, replyText: String): Cancelable?
|
||||
fun replyToMessage(eventReplied: TimelineEvent,
|
||||
replyText: String,
|
||||
autoMarkdown: Boolean = false): Cancelable?
|
||||
|
||||
fun getEventSummaryLive(eventId: String): LiveData<EventAnnotationsSummary>
|
||||
|
||||
|
||||
}
|
@ -21,5 +21,5 @@ import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ReplyToContent(
|
||||
@Json(name = "event_id") val eventId: String
|
||||
)
|
||||
@Json(name = "event_id") val eventId: String? = null
|
||||
)
|
||||
|
@ -67,7 +67,8 @@ data class PublicRoom(
|
||||
var worldReadable: Boolean = false,
|
||||
|
||||
/**
|
||||
* Required. Whether guest users may join the room and participate in it. If they can, they will be subject to ordinary power level rules like any other user.
|
||||
* Required. Whether guest users may join the room and participate in it. If they can,
|
||||
* they will be subject to ordinary power level rules like any other user.
|
||||
*/
|
||||
@Json(name = "guest_can_join")
|
||||
var guestCanJoin: Boolean = false,
|
||||
|
@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.matrix.android.api.session.room.model.tombstone
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Class to contains Tombstone information
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class RoomTombstoneContent(
|
||||
@Json(name = "body") val body: String? = null,
|
||||
@Json(name = "replacement_room") val replacementRoom: String?
|
||||
)
|
@ -16,7 +16,9 @@
|
||||
|
||||
package im.vector.matrix.android.api.session.room.read
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.room.model.ReadReceipt
|
||||
|
||||
/**
|
||||
* This interface defines methods to handle read receipts and read marker in a room. It's implemented at the room level.
|
||||
@ -39,4 +41,6 @@ interface ReadService {
|
||||
fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback<Unit>)
|
||||
|
||||
fun isEventRead(eventId: String): Boolean
|
||||
|
||||
fun getEventReadReceiptsLive(eventId: String): LiveData<List<ReadReceipt>>
|
||||
}
|
@ -19,6 +19,7 @@ package im.vector.matrix.android.api.session.room.send
|
||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
|
||||
|
||||
@ -42,7 +43,7 @@ interface SendService {
|
||||
* @param formattedText The formatted body using MessageType#FORMAT_MATRIX_HTML
|
||||
* @return a [Cancelable]
|
||||
*/
|
||||
fun sendFormattedTextMessage(text: String,formattedText: String): Cancelable
|
||||
fun sendFormattedTextMessage(text: String, formattedText: String): Cancelable
|
||||
|
||||
/**
|
||||
* Method to send a media asynchronously.
|
||||
@ -65,4 +66,31 @@ interface SendService {
|
||||
*/
|
||||
fun redactEvent(event: Event, reason: String?): Cancelable
|
||||
|
||||
|
||||
/**
|
||||
* Schedule this message to be resent
|
||||
* @param localEcho the unsent local echo
|
||||
*/
|
||||
fun resendTextMessage(localEcho: TimelineEvent): Cancelable?
|
||||
|
||||
/**
|
||||
* Schedule this message to be resent
|
||||
* @param localEcho the unsent local echo
|
||||
*/
|
||||
fun resendMediaMessage(localEcho: TimelineEvent): Cancelable?
|
||||
|
||||
|
||||
/**
|
||||
* Remove this failed message from the timeline
|
||||
* @param localEcho the unsent local echo
|
||||
*/
|
||||
fun deleteFailedEcho(localEcho: TimelineEvent)
|
||||
|
||||
fun clearSendingQueue()
|
||||
|
||||
/**
|
||||
* Resend all failed messages one by one (and keep order)
|
||||
*/
|
||||
fun resendAllFailedMessages()
|
||||
|
||||
}
|
@ -16,6 +16,7 @@
|
||||
|
||||
package im.vector.matrix.android.api.session.room.send
|
||||
|
||||
|
||||
enum class SendState {
|
||||
UNKNOWN,
|
||||
// the event has not been sent
|
||||
@ -33,12 +34,19 @@ enum class SendState {
|
||||
// the event failed to be sent because some unknown devices have been found while encrypting it
|
||||
FAILED_UNKNOWN_DEVICES;
|
||||
|
||||
fun isSent(): Boolean {
|
||||
return this == SENT || this == SYNCED
|
||||
internal companion object {
|
||||
val HAS_FAILED_STATES = listOf(UNDELIVERED, FAILED_UNKNOWN_DEVICES)
|
||||
val IS_SENT_STATES = listOf(SENT, SYNCED)
|
||||
val IS_SENDING_STATES = listOf(UNSENT, ENCRYPTING, SENDING)
|
||||
val PENDING_STATES = IS_SENDING_STATES + HAS_FAILED_STATES
|
||||
}
|
||||
|
||||
fun hasFailed(): Boolean {
|
||||
return this == UNDELIVERED || this == FAILED_UNKNOWN_DEVICES
|
||||
}
|
||||
fun isSent() = IS_SENT_STATES.contains(this)
|
||||
|
||||
fun hasFailed() = HAS_FAILED_STATES.contains(this)
|
||||
|
||||
fun isSending() = IS_SENDING_STATES.contains(this)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -17,6 +17,7 @@
|
||||
package im.vector.matrix.android.api.session.room.state
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
|
||||
interface StateService {
|
||||
|
||||
@ -25,4 +26,6 @@ interface StateService {
|
||||
*/
|
||||
fun updateTopic(topic: String, callback: MatrixCallback<Unit>)
|
||||
|
||||
fun getStateEvent(eventType: String): Event?
|
||||
|
||||
}
|
@ -56,6 +56,9 @@ interface Timeline {
|
||||
*/
|
||||
fun paginate(direction: Direction, count: Int)
|
||||
|
||||
fun pendingEventCount() : Int
|
||||
|
||||
fun failedToDeliverEventCount() : Int
|
||||
|
||||
interface Listener {
|
||||
/**
|
||||
|
@ -18,8 +18,13 @@ package im.vector.matrix.android.api.session.room.timeline
|
||||
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
import im.vector.matrix.android.api.session.room.model.ReadReceipt
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.isReply
|
||||
import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply
|
||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||
|
||||
/**
|
||||
* This data class is a wrapper around an Event. It allows to get useful data in the context of a timeline.
|
||||
@ -28,14 +33,13 @@ import im.vector.matrix.android.api.session.room.send.SendState
|
||||
*/
|
||||
data class TimelineEvent(
|
||||
val root: Event,
|
||||
val localId: String,
|
||||
val localId: Long,
|
||||
val displayIndex: Int,
|
||||
val senderName: String?,
|
||||
val isUniqueDisplayName: Boolean,
|
||||
val senderAvatar: String?,
|
||||
val sendState: SendState,
|
||||
val hasClearEventFlag: Boolean = false,
|
||||
val annotations: EventAnnotationsSummary? = null
|
||||
val annotations: EventAnnotationsSummary? = null,
|
||||
val readReceipts: List<ReadReceipt> = emptyList()
|
||||
) {
|
||||
|
||||
val metadata = HashMap<String, Any>()
|
||||
@ -63,8 +67,8 @@ data class TimelineEvent(
|
||||
"$name (${root.senderId})"
|
||||
}
|
||||
}
|
||||
?: root.senderId
|
||||
?: ""
|
||||
?: root.senderId
|
||||
?: ""
|
||||
}
|
||||
|
||||
/**
|
||||
@ -81,3 +85,27 @@ data class TimelineEvent(
|
||||
return EventType.ENCRYPTED == root.type
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tells if the event has been edited
|
||||
*/
|
||||
fun TimelineEvent.hasBeenEdited() = annotations?.editSummary != null
|
||||
|
||||
/**
|
||||
* Get last MessageContent, after a possible edition
|
||||
*/
|
||||
fun TimelineEvent.getLastMessageContent(): MessageContent? = annotations?.editSummary?.aggregatedContent?.toModel()
|
||||
?: root.getClearContent().toModel()
|
||||
|
||||
|
||||
fun TimelineEvent.getTextEditableContent(): String? {
|
||||
val originalContent = root.getClearContent().toModel<MessageContent>() ?: return null
|
||||
val isReply = originalContent.isReply() || root.content.toModel<EncryptedEventContent>()?.relatesTo?.inReplyTo?.eventId != null
|
||||
val lastContent = getLastMessageContent()
|
||||
return if (isReply) {
|
||||
return extractUsefulTextFromReply(lastContent?.body ?: "")
|
||||
} else {
|
||||
lastContent?.body ?: ""
|
||||
}
|
||||
}
|
||||
|
@ -25,12 +25,12 @@ interface TimelineService {
|
||||
|
||||
/**
|
||||
* Instantiate a [Timeline] with an optional initial eventId, to be used with permalink.
|
||||
* You can filter the type you want to grab with the allowedTypes param.
|
||||
* You can also configure some settings with the [settings] param.
|
||||
* @param eventId the optional initial eventId.
|
||||
* @param allowedTypes the optional filter types
|
||||
* @param settings settings to configure the timeline.
|
||||
* @return the instantiated timeline
|
||||
*/
|
||||
fun createTimeline(eventId: String?, allowedTypes: List<String>? = null): Timeline
|
||||
fun createTimeline(eventId: String?, settings: TimelineSettings): Timeline
|
||||
|
||||
|
||||
fun getTimeLineEvent(eventId: String): TimelineEvent?
|
||||
|
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.room.timeline
|
||||
|
||||
/**
|
||||
* Data class holding setting values for a [Timeline] instance.
|
||||
*/
|
||||
data class TimelineSettings(
|
||||
/**
|
||||
* The initial number of events to retrieve from cache. You might get less events if you don't have loaded enough yet.
|
||||
*/
|
||||
val initialSize: Int,
|
||||
/**
|
||||
* A flag to filter edit events
|
||||
*/
|
||||
val filterEdits: 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.
|
||||
*/
|
||||
val buildReadReceipts: Boolean = true
|
||||
|
||||
)
|
@ -18,7 +18,7 @@ package im.vector.matrix.android.api.session.sync
|
||||
|
||||
sealed class SyncState {
|
||||
object IDLE : SyncState()
|
||||
data class RUNNING(val catchingUp: Boolean) : SyncState()
|
||||
data class RUNNING(val afterPause: Boolean) : SyncState()
|
||||
object PAUSED : SyncState()
|
||||
object KILLING : SyncState()
|
||||
object KILLED : SyncState()
|
||||
|
@ -17,7 +17,10 @@
|
||||
package im.vector.matrix.android.api.session.user
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.paging.PagedList
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.user.model.User
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
|
||||
/**
|
||||
* This interface defines methods to get users. It's implemented at the session level.
|
||||
@ -31,11 +34,34 @@ interface UserService {
|
||||
*/
|
||||
fun getUser(userId: String): User?
|
||||
|
||||
/**
|
||||
* Search list of users on server directory.
|
||||
* @param search the searched term
|
||||
* @param limit the max number of users to return
|
||||
* @param excludedUserIds the user ids to filter from the search
|
||||
* @param callback the async callback
|
||||
* @return Cancelable
|
||||
*/
|
||||
fun searchUsersDirectory(search: String, limit: Int, excludedUserIds: Set<String>, callback: MatrixCallback<List<User>>): Cancelable
|
||||
|
||||
/**
|
||||
* Observe a live user from a userId
|
||||
* @param userId the userId to look for.
|
||||
* @return a Livedata of user with userId
|
||||
*/
|
||||
fun observeUser(userId: String): LiveData<User?>
|
||||
fun liveUser(userId: String): LiveData<User?>
|
||||
|
||||
/**
|
||||
* Observe a live list of users sorted alphabetically
|
||||
* @return a Livedata of users
|
||||
*/
|
||||
fun liveUsers(): LiveData<List<User>>
|
||||
|
||||
/**
|
||||
* Observe a live [PagedList] of users sorted alphabetically. You can filter the users.
|
||||
* @param filter the filter. It will look into userId and displayName.
|
||||
* @return a Livedata of users
|
||||
*/
|
||||
fun livePagedUsers(filter: String? = null): LiveData<PagedList<User>>
|
||||
|
||||
}
|
@ -16,20 +16,9 @@
|
||||
|
||||
package im.vector.matrix.android.api.util
|
||||
|
||||
class CancelableBag : Cancelable {
|
||||
|
||||
private val cancelableList = ArrayList<Cancelable>()
|
||||
|
||||
fun add(cancelable: Cancelable) {
|
||||
cancelableList.add(cancelable)
|
||||
}
|
||||
|
||||
class CancelableBag : Cancelable, MutableList<Cancelable> by ArrayList() {
|
||||
override fun cancel() {
|
||||
cancelableList.forEach { it.cancel() }
|
||||
forEach { it.cancel() }
|
||||
clear()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun Cancelable.addTo(cancelables: CancelableBag) {
|
||||
cancelables.add(this)
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.matrix.android.api.util
|
||||
|
||||
|
||||
object ContentUtils {
|
||||
fun extractUsefulTextFromReply(repliedBody: String): String {
|
||||
val lines = repliedBody.lines()
|
||||
var wellFormed = repliedBody.startsWith(">")
|
||||
var endOfPreviousFound = false
|
||||
val usefullines = ArrayList<String>()
|
||||
lines.forEach {
|
||||
if (it == "") {
|
||||
endOfPreviousFound = true
|
||||
return@forEach
|
||||
}
|
||||
if (!endOfPreviousFound) {
|
||||
wellFormed = wellFormed && it.startsWith(">")
|
||||
} else {
|
||||
usefullines.add(it)
|
||||
}
|
||||
}
|
||||
return usefullines.joinToString("\n").takeIf { wellFormed } ?: repliedBody
|
||||
}
|
||||
|
||||
fun extractUsefulTextFromHtmlReply(repliedBody: String): String {
|
||||
if (repliedBody.startsWith("<mx-reply>")) {
|
||||
val closingTagIndex = repliedBody.lastIndexOf("</mx-reply>")
|
||||
if (closingTagIndex != -1)
|
||||
return repliedBody.substring(closingTagIndex + "</mx-reply>".length).trim()
|
||||
}
|
||||
return repliedBody
|
||||
}
|
||||
}
|
@ -21,13 +21,4 @@ import im.vector.matrix.android.api.MatrixCallback
|
||||
/**
|
||||
* Simple MatrixCallback implementation which delegate its calls to another callback
|
||||
*/
|
||||
open class MatrixCallbackDelegate<T>(private val callback: MatrixCallback<T>) : MatrixCallback<T> {
|
||||
|
||||
override fun onSuccess(data: T) {
|
||||
callback.onSuccess(data)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
callback.onFailure(failure)
|
||||
}
|
||||
}
|
||||
open class MatrixCallbackDelegate<T>(private val callback: MatrixCallback<T>) : MatrixCallback<T> by callback
|
@ -27,7 +27,7 @@ import java.math.BigInteger
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.KeyStore
|
||||
import java.security.SecureRandom
|
||||
import java.util.*
|
||||
import java.util.Calendar
|
||||
import javax.crypto.*
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
@ -479,12 +479,7 @@ object SecretStoringUtils {
|
||||
val output = Cipher.getInstance(RSA_MODE)
|
||||
output.init(Cipher.DECRYPT_MODE, privateKeyEntry.privateKey)
|
||||
|
||||
val bos = ByteArrayOutputStream()
|
||||
CipherInputStream(encrypted, output).use {
|
||||
it.copyTo(bos)
|
||||
}
|
||||
|
||||
return bos.toByteArray()
|
||||
return CipherInputStream(encrypted, output).use { it.readBytes() }
|
||||
}
|
||||
|
||||
private fun formatMExtract(bis: InputStream): Pair<ByteArray, ByteArray> {
|
||||
@ -495,14 +490,7 @@ object SecretStoringUtils {
|
||||
val iv = ByteArray(ivSize)
|
||||
bis.read(iv, 0, ivSize)
|
||||
|
||||
|
||||
val bos = ByteArrayOutputStream()
|
||||
var next = bis.read()
|
||||
while (next != -1) {
|
||||
bos.write(next)
|
||||
next = bis.read()
|
||||
}
|
||||
val encrypted = bos.toByteArray()
|
||||
val encrypted = bis.readBytes()
|
||||
return Pair(iv, encrypted)
|
||||
}
|
||||
|
||||
@ -530,14 +518,7 @@ object SecretStoringUtils {
|
||||
val iv = ByteArray(ivSize)
|
||||
bis.read(iv)
|
||||
|
||||
val bos = ByteArrayOutputStream()
|
||||
|
||||
var next = bis.read()
|
||||
while (next != -1) {
|
||||
bos.write(next)
|
||||
next = bis.read()
|
||||
}
|
||||
val encrypted = bos.toByteArray()
|
||||
val encrypted = bis.readBytes()
|
||||
return Triple(encryptedKey, iv, encrypted)
|
||||
}
|
||||
|
||||
@ -579,14 +560,7 @@ object SecretStoringUtils {
|
||||
val iv = ByteArray(ivSize)
|
||||
bis.read(iv)
|
||||
|
||||
val bos = ByteArrayOutputStream()
|
||||
|
||||
var next = bis.read()
|
||||
while (next != -1) {
|
||||
bos.write(next)
|
||||
next = bis.read()
|
||||
}
|
||||
val encrypted = bos.toByteArray()
|
||||
val encrypted = bis.readBytes()
|
||||
return Triple(salt, iv, encrypted)
|
||||
}
|
||||
}
|
@ -50,16 +50,10 @@ internal class SessionManager @Inject constructor(private val matrixComponent: M
|
||||
}
|
||||
|
||||
private fun getOrCreateSessionComponent(sessionParams: SessionParams): SessionComponent {
|
||||
val userId = sessionParams.credentials.userId
|
||||
if (sessionComponents.containsKey(userId)) {
|
||||
return sessionComponents[userId]!!
|
||||
return sessionComponents.getOrPut(sessionParams.credentials.userId) {
|
||||
DaggerSessionComponent
|
||||
.factory()
|
||||
.create(matrixComponent, sessionParams)
|
||||
}
|
||||
return DaggerSessionComponent
|
||||
.factory()
|
||||
.create(matrixComponent, sessionParams)
|
||||
.also {
|
||||
sessionComponents[sessionParams.credentials.userId] = it
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -21,6 +21,7 @@ import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
|
||||
import im.vector.matrix.android.internal.network.NetworkConstants
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.Headers
|
||||
import retrofit2.http.POST
|
||||
|
||||
/**
|
||||
@ -30,9 +31,11 @@ internal interface AuthAPI {
|
||||
|
||||
/**
|
||||
* Pass params to the server for the current login phase.
|
||||
* Set all the timeouts to 1 minute
|
||||
*
|
||||
* @param loginParams the login parameters
|
||||
*/
|
||||
@Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000")
|
||||
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login")
|
||||
fun login(@Body loginParams: PasswordLoginParams): Call<Credentials>
|
||||
|
||||
|
@ -68,7 +68,9 @@ internal class DefaultAuthenticator @Inject constructor(@Unauthenticated
|
||||
callback: MatrixCallback<Session>): Cancelable {
|
||||
|
||||
val job = GlobalScope.launch(coroutineDispatchers.main) {
|
||||
val sessionOrFailure = authenticate(homeServerConnectionConfig, login, password)
|
||||
val sessionOrFailure = runCatching {
|
||||
authenticate(homeServerConnectionConfig, login, password)
|
||||
}
|
||||
sessionOrFailure.foldToCallback(callback)
|
||||
}
|
||||
return CancelableCoroutine(job)
|
||||
@ -85,16 +87,12 @@ internal class DefaultAuthenticator @Inject constructor(@Unauthenticated
|
||||
} else {
|
||||
PasswordLoginParams.userIdentifier(login, password, "Mobile")
|
||||
}
|
||||
executeRequest<Credentials> {
|
||||
val credentials = executeRequest<Credentials> {
|
||||
apiCall = authAPI.login(loginParams)
|
||||
}.map {
|
||||
val sessionParams = SessionParams(it, homeServerConnectionConfig)
|
||||
sessionParamsStore.save(sessionParams)
|
||||
sessionParams
|
||||
}.map {
|
||||
sessionManager.getOrCreateSession(it)
|
||||
}
|
||||
|
||||
val sessionParams = SessionParams(credentials, homeServerConnectionConfig)
|
||||
sessionParamsStore.save(sessionParams)
|
||||
sessionManager.getOrCreateSession(sessionParams)
|
||||
}
|
||||
|
||||
private fun buildAuthAPI(homeServerConnectionConfig: HomeServerConnectionConfig): AuthAPI {
|
||||
|
@ -27,7 +27,7 @@ internal interface SessionParamsStore {
|
||||
|
||||
fun getAll(): List<SessionParams>
|
||||
|
||||
fun save(sessionParams: SessionParams): Try<SessionParams>
|
||||
fun save(sessionParams: SessionParams): Try<Unit>
|
||||
|
||||
fun delete(userId: String): Try<Unit>
|
||||
|
||||
|
@ -62,7 +62,7 @@ internal class RealmSessionParamsStore @Inject constructor(private val mapper: S
|
||||
return sessionParams
|
||||
}
|
||||
|
||||
override fun save(sessionParams: SessionParams): Try<SessionParams> {
|
||||
override fun save(sessionParams: SessionParams): Try<Unit> {
|
||||
return Try {
|
||||
val entity = mapper.map(sessionParams)
|
||||
if (entity != null) {
|
||||
@ -72,7 +72,6 @@ internal class RealmSessionParamsStore @Inject constructor(private val mapper: S
|
||||
}
|
||||
realm.close()
|
||||
}
|
||||
sessionParams
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,6 @@ import com.squareup.moshi.Moshi
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
|
||||
import im.vector.matrix.android.api.auth.data.SessionParams
|
||||
import im.vector.matrix.android.internal.di.MatrixScope
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {
|
||||
|
@ -76,7 +76,7 @@ internal abstract class CryptoModule {
|
||||
@Provides
|
||||
fun providesCryptoStore(@CryptoDatabase
|
||||
realmConfiguration: RealmConfiguration, credentials: Credentials): IMXCryptoStore {
|
||||
return RealmCryptoStore(false /* TODO*/,
|
||||
return RealmCryptoStore(
|
||||
realmConfiguration,
|
||||
credentials)
|
||||
}
|
||||
@ -105,7 +105,7 @@ internal abstract class CryptoModule {
|
||||
}
|
||||
|
||||
@Binds
|
||||
abstract fun bindCryptoService(cryptoManager: CryptoManager): CryptoService
|
||||
abstract fun bindCryptoService(cryptoService: DefaultCryptoService): CryptoService
|
||||
|
||||
@Binds
|
||||
abstract fun bindDeleteDeviceTask(deleteDeviceTask: DefaultDeleteDeviceTask): DeleteDeviceTask
|
||||
@ -168,8 +168,10 @@ internal abstract class CryptoModule {
|
||||
abstract fun bindSendToDeviceTask(sendToDeviceTask: DefaultSendToDeviceTask): SendToDeviceTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindClaimOneTimeKeysForUsersDeviceTask(claimOneTimeKeysForUsersDevice: DefaultClaimOneTimeKeysForUsersDevice): ClaimOneTimeKeysForUsersDeviceTask
|
||||
abstract fun bindClaimOneTimeKeysForUsersDeviceTask(claimOneTimeKeysForUsersDevice: DefaultClaimOneTimeKeysForUsersDevice)
|
||||
: ClaimOneTimeKeysForUsersDeviceTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindDeleteDeviceWithUserPasswordTask(deleteDeviceWithUserPasswordTask: DefaultDeleteDeviceWithUserPasswordTask): DeleteDeviceWithUserPasswordTask
|
||||
abstract fun bindDeleteDeviceWithUserPasswordTask(deleteDeviceWithUserPasswordTask: DefaultDeleteDeviceWithUserPasswordTask)
|
||||
: DeleteDeviceWithUserPasswordTask
|
||||
}
|
||||
|
@ -21,10 +21,10 @@ package im.vector.matrix.android.internal.crypto
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.text.TextUtils
|
||||
import arrow.core.Try
|
||||
import com.squareup.moshi.Types
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import dagger.Lazy
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
@ -72,15 +72,15 @@ import im.vector.matrix.android.internal.session.room.membership.RoomMembers
|
||||
import im.vector.matrix.android.internal.session.sync.model.SyncResponse
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import im.vector.matrix.android.internal.util.JsonCanonicalizer
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import im.vector.matrix.android.internal.util.fetchCopied
|
||||
import kotlinx.coroutines.*
|
||||
import org.matrix.olm.OlmManager
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.inject.Inject
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* A `CryptoService` class instance manages the end-to-end crypto for a session.
|
||||
@ -93,12 +93,12 @@ import kotlin.coroutines.EmptyCoroutineContext
|
||||
* Specially, it tracks all room membership changes events in order to do keys updates.
|
||||
*/
|
||||
@SessionScope
|
||||
internal class CryptoManager @Inject constructor(
|
||||
internal class DefaultCryptoService @Inject constructor(
|
||||
// Olm Manager
|
||||
private val olmManager: OlmManager,
|
||||
// The credentials,
|
||||
private val credentials: Credentials,
|
||||
private val myDeviceInfoHolder: MyDeviceInfoHolder,
|
||||
private val myDeviceInfoHolder: Lazy<MyDeviceInfoHolder>,
|
||||
// the crypto store
|
||||
private val cryptoStore: IMXCryptoStore,
|
||||
// Olm device
|
||||
@ -166,22 +166,25 @@ internal class CryptoManager @Inject constructor(
|
||||
|
||||
override fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>) {
|
||||
setDeviceNameTask
|
||||
.configureWith(SetDeviceNameTask.Params(deviceId, deviceName))
|
||||
.dispatchTo(callback)
|
||||
.configureWith(SetDeviceNameTask.Params(deviceId, deviceName)) {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun deleteDevice(deviceId: String, callback: MatrixCallback<Unit>) {
|
||||
deleteDeviceTask
|
||||
.configureWith(DeleteDeviceTask.Params(deviceId))
|
||||
.dispatchTo(callback)
|
||||
.configureWith(DeleteDeviceTask.Params(deviceId)) {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback<Unit>) {
|
||||
deleteDeviceWithUserPasswordTask
|
||||
.configureWith(DeleteDeviceWithUserPasswordTask.Params(deviceId, authSession, password))
|
||||
.dispatchTo(callback)
|
||||
.configureWith(DeleteDeviceWithUserPasswordTask.Params(deviceId, authSession, password)) {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
@ -190,13 +193,14 @@ internal class CryptoManager @Inject constructor(
|
||||
}
|
||||
|
||||
override fun getMyDevice(): MXDeviceInfo {
|
||||
return myDeviceInfoHolder.myDevice
|
||||
return myDeviceInfoHolder.get().myDevice
|
||||
}
|
||||
|
||||
override fun getDevicesList(callback: MatrixCallback<DevicesListResponse>) {
|
||||
getDevicesTask
|
||||
.configureWith(Unit)
|
||||
.dispatchTo(callback)
|
||||
.configureWith {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
@ -241,47 +245,48 @@ internal class CryptoManager @Inject constructor(
|
||||
* @param isInitialSync true if it starts from an initial sync
|
||||
*/
|
||||
fun start(isInitialSync: Boolean) {
|
||||
CoroutineScope(coroutineDispatchers.crypto).launch {
|
||||
if (isStarted.get() || isStarting.get()) {
|
||||
return
|
||||
}
|
||||
isStarting.set(true)
|
||||
GlobalScope.launch(coroutineDispatchers.crypto) {
|
||||
internalStart(isInitialSync)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun internalStart(isInitialSync: Boolean) {
|
||||
if (isStarted.get() || isStarting.get()) {
|
||||
return
|
||||
}
|
||||
isStarting.set(true)
|
||||
// Open the store
|
||||
cryptoStore.open()
|
||||
uploadDeviceKeys()
|
||||
.flatMap { oneTimeKeysUploader.maybeUploadOneTimeKeys() }
|
||||
.fold(
|
||||
{
|
||||
Timber.e("Start failed: $it")
|
||||
delay(1000)
|
||||
isStarting.set(false)
|
||||
internalStart(isInitialSync)
|
||||
},
|
||||
{
|
||||
outgoingRoomKeyRequestManager.start()
|
||||
keysBackup.checkAndStartKeysBackup()
|
||||
if (isInitialSync) {
|
||||
// refresh the devices list for each known room members
|
||||
deviceListManager.invalidateAllDeviceLists()
|
||||
deviceListManager.refreshOutdatedDeviceLists()
|
||||
} else {
|
||||
incomingRoomKeyRequestManager.processReceivedRoomKeyRequests()
|
||||
}
|
||||
isStarting.set(false)
|
||||
isStarted.set(true)
|
||||
}
|
||||
)
|
||||
runCatching {
|
||||
uploadDeviceKeys()
|
||||
oneTimeKeysUploader.maybeUploadOneTimeKeys()
|
||||
outgoingRoomKeyRequestManager.start()
|
||||
keysBackup.checkAndStartKeysBackup()
|
||||
if (isInitialSync) {
|
||||
// refresh the devices list for each known room members
|
||||
deviceListManager.invalidateAllDeviceLists()
|
||||
deviceListManager.refreshOutdatedDeviceLists()
|
||||
} else {
|
||||
incomingRoomKeyRequestManager.processReceivedRoomKeyRequests()
|
||||
}
|
||||
}.fold(
|
||||
{
|
||||
isStarting.set(false)
|
||||
isStarted.set(true)
|
||||
},
|
||||
{
|
||||
Timber.e("Start failed: $it")
|
||||
delay(1000)
|
||||
isStarting.set(false)
|
||||
internalStart(isInitialSync)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the crypto
|
||||
*/
|
||||
fun close() {
|
||||
fun close() = runBlocking(coroutineDispatchers.crypto) {
|
||||
olmDevice.release()
|
||||
cryptoStore.close()
|
||||
outgoingRoomKeyRequestManager.stop()
|
||||
@ -312,7 +317,7 @@ internal class CryptoManager @Inject constructor(
|
||||
* @param syncResponse the syncResponse
|
||||
*/
|
||||
fun onSyncCompleted(syncResponse: SyncResponse) {
|
||||
CoroutineScope(coroutineDispatchers.crypto).launch {
|
||||
GlobalScope.launch(coroutineDispatchers.crypto) {
|
||||
if (syncResponse.deviceLists != null) {
|
||||
deviceListManager.handleDeviceListsChanges(syncResponse.deviceLists.changed, syncResponse.deviceLists.left)
|
||||
}
|
||||
@ -337,7 +342,7 @@ internal class CryptoManager @Inject constructor(
|
||||
* @return the device info, or null if not found / unsupported algorithm / crypto released
|
||||
*/
|
||||
override fun deviceWithIdentityKey(senderKey: String, algorithm: String): MXDeviceInfo? {
|
||||
return if (!TextUtils.equals(algorithm, MXCRYPTO_ALGORITHM_MEGOLM) && !TextUtils.equals(algorithm, MXCRYPTO_ALGORITHM_OLM)) {
|
||||
return if (algorithm != MXCRYPTO_ALGORITHM_MEGOLM && algorithm != MXCRYPTO_ALGORITHM_OLM) {
|
||||
// We only deal in olm keys
|
||||
null
|
||||
} else cryptoStore.deviceWithIdentityKey(senderKey)
|
||||
@ -350,8 +355,8 @@ internal class CryptoManager @Inject constructor(
|
||||
* @param deviceId the device id
|
||||
*/
|
||||
override fun getDeviceInfo(userId: String, deviceId: String?): MXDeviceInfo? {
|
||||
return if (!TextUtils.isEmpty(userId) && !TextUtils.isEmpty(deviceId)) {
|
||||
cryptoStore.getUserDevice(deviceId!!, userId)
|
||||
return if (userId.isNotEmpty() && !deviceId.isNullOrEmpty()) {
|
||||
cryptoStore.getUserDevice(deviceId, userId)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@ -387,12 +392,12 @@ internal class CryptoManager @Inject constructor(
|
||||
var isUpdated = false
|
||||
val deviceIds = devicesIdListByUserId[userId]
|
||||
|
||||
for (deviceId in deviceIds!!) {
|
||||
deviceIds?.forEach { deviceId ->
|
||||
val device = storedDeviceIDs[deviceId]
|
||||
|
||||
// assume if the device is either verified or blocked
|
||||
// it means that the device is known
|
||||
if (null != device && device.isUnknown) {
|
||||
if (device?.isUnknown == true) {
|
||||
device.verified = MXDeviceInfo.DEVICE_VERIFICATION_UNVERIFIED
|
||||
isUpdated = true
|
||||
}
|
||||
@ -436,7 +441,7 @@ internal class CryptoManager @Inject constructor(
|
||||
// (for now at least. Maybe we should alert the user somehow?)
|
||||
val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId)
|
||||
|
||||
if (!TextUtils.isEmpty(existingAlgorithm) && !TextUtils.equals(existingAlgorithm, algorithm)) {
|
||||
if (!existingAlgorithm.isNullOrEmpty() && existingAlgorithm != algorithm) {
|
||||
Timber.e("## setEncryptionInRoom() : Ignoring m.room.encryption event which requests a change of config in $roomId")
|
||||
return false
|
||||
}
|
||||
@ -444,7 +449,7 @@ internal class CryptoManager @Inject constructor(
|
||||
val encryptingClass = MXCryptoAlgorithms.hasEncryptorClassForAlgorithm(algorithm)
|
||||
|
||||
if (!encryptingClass) {
|
||||
Timber.e("## setEncryptionInRoom() : Unable to encrypt with " + algorithm!!)
|
||||
Timber.e("## setEncryptionInRoom() : Unable to encrypt room ${roomId} with $algorithm")
|
||||
return false
|
||||
}
|
||||
|
||||
@ -532,7 +537,7 @@ internal class CryptoManager @Inject constructor(
|
||||
eventType: String,
|
||||
roomId: String,
|
||||
callback: MatrixCallback<MXEncryptEventContentResult>) {
|
||||
CoroutineScope(coroutineDispatchers.crypto).launch {
|
||||
GlobalScope.launch(coroutineDispatchers.crypto) {
|
||||
if (!isStarted()) {
|
||||
Timber.v("## encryptEventContent() : wait after e2e init")
|
||||
internalStart(false)
|
||||
@ -555,21 +560,23 @@ internal class CryptoManager @Inject constructor(
|
||||
if (safeAlgorithm != null) {
|
||||
val t0 = System.currentTimeMillis()
|
||||
Timber.v("## encryptEventContent() starts")
|
||||
safeAlgorithm.encryptEventContent(eventContent, eventType, userIds)
|
||||
runCatching {
|
||||
safeAlgorithm.encryptEventContent(eventContent, eventType, userIds)
|
||||
}
|
||||
.fold(
|
||||
{ callback.onFailure(it) },
|
||||
{
|
||||
Timber.v("## encryptEventContent() : succeeds after " + (System.currentTimeMillis() - t0) + " ms")
|
||||
Timber.v("## encryptEventContent() : succeeds after ${System.currentTimeMillis() - t0} ms")
|
||||
callback.onSuccess(MXEncryptEventContentResult(it, EventType.ENCRYPTED))
|
||||
}
|
||||
},
|
||||
{ callback.onFailure(it) }
|
||||
|
||||
)
|
||||
} else {
|
||||
val algorithm = getEncryptionAlgorithm(roomId)
|
||||
val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON,
|
||||
algorithm ?: MXCryptoError.NO_MORE_ALGORITHM_REASON)
|
||||
Timber.e("## encryptEventContent() : $reason")
|
||||
callback.onFailure(Failure.CryptoError(MXCryptoError(MXCryptoError.UNABLE_TO_ENCRYPT_ERROR_CODE,
|
||||
MXCryptoError.UNABLE_TO_ENCRYPT, reason)))
|
||||
callback.onFailure(Failure.CryptoError(MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_ENCRYPT, reason)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -579,15 +586,12 @@ internal class CryptoManager @Inject constructor(
|
||||
*
|
||||
* @param event the raw event.
|
||||
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
|
||||
* @return the MXEventDecryptionResult data, or null in case of error
|
||||
* @return the MXEventDecryptionResult data, or throw in case of error
|
||||
*/
|
||||
@Throws(MXDecryptionException::class)
|
||||
override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult? {
|
||||
@Throws(MXCryptoError::class)
|
||||
override fun decryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||
return runBlocking {
|
||||
internalDecryptEvent(event, timeline).fold(
|
||||
{ throw it },
|
||||
{ it }
|
||||
)
|
||||
internalDecryptEvent(event, timeline)
|
||||
}
|
||||
}
|
||||
|
||||
@ -598,10 +602,12 @@ internal class CryptoManager @Inject constructor(
|
||||
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
|
||||
* @param callback the callback to return data or null
|
||||
*/
|
||||
override fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult?>) {
|
||||
GlobalScope.launch(EmptyCoroutineContext) {
|
||||
val result = withContext(coroutineDispatchers.crypto) {
|
||||
internalDecryptEvent(event, timeline)
|
||||
override fun decryptEventAsync(event: Event, timeline: String, callback: MatrixCallback<MXEventDecryptionResult>) {
|
||||
GlobalScope.launch {
|
||||
val result = runCatching {
|
||||
withContext(coroutineDispatchers.crypto) {
|
||||
internalDecryptEvent(event, timeline)
|
||||
}
|
||||
}
|
||||
result.foldToCallback(callback)
|
||||
}
|
||||
@ -612,22 +618,22 @@ internal class CryptoManager @Inject constructor(
|
||||
*
|
||||
* @param event the raw event.
|
||||
* @param timeline the id of the timeline where the event is decrypted. It is used to prevent replay attack.
|
||||
* @return the MXEventDecryptionResult data, or null in case of error wrapped into [Try]
|
||||
* @return the MXEventDecryptionResult data, or null in case of error
|
||||
*/
|
||||
private suspend fun internalDecryptEvent(event: Event, timeline: String): Try<MXEventDecryptionResult?> {
|
||||
return Try {
|
||||
val eventContent = event.content
|
||||
if (eventContent == null) {
|
||||
Timber.e("## decryptEvent : empty event content")
|
||||
return@Try null
|
||||
}
|
||||
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, eventContent["algorithm"] as String)
|
||||
private suspend fun internalDecryptEvent(event: Event, timeline: String): MXEventDecryptionResult {
|
||||
val eventContent = event.content
|
||||
if (eventContent == null) {
|
||||
Timber.e("## decryptEvent : empty event content")
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON)
|
||||
} else {
|
||||
val algorithm = eventContent["algorithm"]?.toString()
|
||||
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(event.roomId, algorithm)
|
||||
if (alg == null) {
|
||||
val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, eventContent["algorithm"] as String)
|
||||
val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm)
|
||||
Timber.e("## decryptEvent() : $reason")
|
||||
throw MXDecryptionException(MXCryptoError(MXCryptoError.UNABLE_TO_DECRYPT_ERROR_CODE, MXCryptoError.UNABLE_TO_DECRYPT, reason))
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason)
|
||||
} else {
|
||||
alg.decryptEvent(event, timeline)
|
||||
return alg.decryptEvent(event, timeline)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -647,7 +653,7 @@ internal class CryptoManager @Inject constructor(
|
||||
* @param event the event
|
||||
*/
|
||||
fun onToDeviceEvent(event: Event) {
|
||||
CoroutineScope(coroutineDispatchers.crypto).launch {
|
||||
GlobalScope.launch(coroutineDispatchers.crypto) {
|
||||
when (event.getClearType()) {
|
||||
EventType.ROOM_KEY, EventType.FORWARDED_ROOM_KEY -> {
|
||||
onRoomKeyEvent(event)
|
||||
@ -669,13 +675,13 @@ internal class CryptoManager @Inject constructor(
|
||||
*/
|
||||
private fun onRoomKeyEvent(event: Event) {
|
||||
val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return
|
||||
if (TextUtils.isEmpty(roomKeyContent.roomId) || TextUtils.isEmpty(roomKeyContent.algorithm)) {
|
||||
if (roomKeyContent.roomId.isNullOrEmpty() || roomKeyContent.algorithm.isNullOrEmpty()) {
|
||||
Timber.e("## onRoomKeyEvent() : missing fields")
|
||||
return
|
||||
}
|
||||
val alg = roomDecryptorProvider.getOrCreateRoomDecryptor(roomKeyContent.roomId, roomKeyContent.algorithm)
|
||||
if (alg == null) {
|
||||
Timber.e("## onRoomKeyEvent() : Unable to handle keys for " + roomKeyContent.algorithm)
|
||||
Timber.e("## onRoomKeyEvent() : Unable to handle keys for ${roomKeyContent.algorithm}")
|
||||
return
|
||||
}
|
||||
alg.onRoomKeyEvent(event, keysBackup)
|
||||
@ -687,14 +693,15 @@ internal class CryptoManager @Inject constructor(
|
||||
* @param event the encryption event.
|
||||
*/
|
||||
private fun onRoomEncryptionEvent(roomId: String, event: Event) {
|
||||
CoroutineScope(coroutineDispatchers.crypto).launch {
|
||||
GlobalScope.launch(coroutineDispatchers.crypto) {
|
||||
val params = LoadRoomMembersTask.Params(roomId)
|
||||
loadRoomMembersTask
|
||||
.execute(params)
|
||||
.map { allLoaded ->
|
||||
val userIds = getRoomUserIds(roomId)
|
||||
setEncryptionInRoom(roomId, event.content!!["algorithm"] as String, true, userIds)
|
||||
}
|
||||
try {
|
||||
loadRoomMembersTask.execute(params)
|
||||
val userIds = getRoomUserIds(roomId)
|
||||
setEncryptionInRoom(roomId, event.content?.get("algorithm")?.toString(), true, userIds)
|
||||
} catch (throwable: Throwable) {
|
||||
Timber.e(throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -736,7 +743,7 @@ internal class CryptoManager @Inject constructor(
|
||||
val membership = roomMember?.membership
|
||||
if (membership == Membership.JOIN) {
|
||||
// make sure we are tracking the deviceList for this user.
|
||||
deviceListManager.startTrackingDeviceList(Arrays.asList(userId))
|
||||
deviceListManager.startTrackingDeviceList(listOf(userId))
|
||||
} else if (membership == Membership.INVITE
|
||||
&& shouldEncryptForInvitedMembers(roomId)
|
||||
&& cryptoConfig.enableEncryptionForInvitedMembers) {
|
||||
@ -745,7 +752,7 @@ internal class CryptoManager @Inject constructor(
|
||||
// know what other servers are in the room at the time they've been invited.
|
||||
// They therefore will not send device updates if a user logs in whilst
|
||||
// their state is invite.
|
||||
deviceListManager.startTrackingDeviceList(Arrays.asList(userId))
|
||||
deviceListManager.startTrackingDeviceList(listOf(userId))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -761,10 +768,10 @@ internal class CryptoManager @Inject constructor(
|
||||
/**
|
||||
* Upload my user's device keys.
|
||||
*/
|
||||
private suspend fun uploadDeviceKeys(): Try<KeysUploadResponse> {
|
||||
private suspend fun uploadDeviceKeys(): KeysUploadResponse {
|
||||
// Prepare the device keys data to send
|
||||
// Sign it
|
||||
val canonicalJson = MoshiProvider.getCanonicalJson(Map::class.java, getMyDevice().signalableJSONDictionary())
|
||||
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, getMyDevice().signalableJSONDictionary())
|
||||
getMyDevice().signatures = objectSigner.signObject(canonicalJson)
|
||||
|
||||
// For now, we set the device id explicitly, as we may not be using the
|
||||
@ -780,7 +787,11 @@ internal class CryptoManager @Inject constructor(
|
||||
* @param callback the exported keys
|
||||
*/
|
||||
override fun exportRoomKeys(password: String, callback: MatrixCallback<ByteArray>) {
|
||||
exportRoomKeys(password, MXMegolmExportEncryption.DEFAULT_ITERATION_COUNT, callback)
|
||||
GlobalScope.launch(coroutineDispatchers.main) {
|
||||
runCatching {
|
||||
exportRoomKeys(password, MXMegolmExportEncryption.DEFAULT_ITERATION_COUNT)
|
||||
}.fold(callback::onSuccess, callback::onFailure)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -790,30 +801,16 @@ internal class CryptoManager @Inject constructor(
|
||||
* @param anIterationCount the encryption iteration count (0 means no encryption)
|
||||
* @param callback the exported keys
|
||||
*/
|
||||
private fun exportRoomKeys(password: String, anIterationCount: Int, callback: MatrixCallback<ByteArray>) {
|
||||
GlobalScope.launch(coroutineDispatchers.main) {
|
||||
withContext(coroutineDispatchers.crypto) {
|
||||
Try {
|
||||
val iterationCount = Math.max(0, anIterationCount)
|
||||
private suspend fun exportRoomKeys(password: String, anIterationCount: Int): ByteArray {
|
||||
return withContext(coroutineDispatchers.crypto) {
|
||||
val iterationCount = max(0, anIterationCount)
|
||||
|
||||
val exportedSessions = ArrayList<MegolmSessionData>()
|
||||
val exportedSessions = cryptoStore.getInboundGroupSessions().mapNotNull { it.exportKeys() }
|
||||
|
||||
val inboundGroupSessions = cryptoStore.getInboundGroupSessions()
|
||||
val adapter = MoshiProvider.providesMoshi()
|
||||
.adapter(List::class.java)
|
||||
|
||||
for (session in inboundGroupSessions) {
|
||||
val megolmSessionData = session.exportKeys()
|
||||
|
||||
if (null != megolmSessionData) {
|
||||
exportedSessions.add(megolmSessionData)
|
||||
}
|
||||
}
|
||||
|
||||
val adapter = MoshiProvider.providesMoshi()
|
||||
.adapter(List::class.java)
|
||||
|
||||
MXMegolmExportEncryption.encryptMegolmKeyFile(adapter.toJson(exportedSessions), password, iterationCount)
|
||||
}
|
||||
}.foldToCallback(callback)
|
||||
MXMegolmExportEncryption.encryptMegolmKeyFile(adapter.toJson(exportedSessions), password, iterationCount)
|
||||
}
|
||||
}
|
||||
|
||||
@ -838,7 +835,7 @@ internal class CryptoManager @Inject constructor(
|
||||
val roomKeys = MXMegolmExportEncryption.decryptMegolmKeyFile(roomKeysAsArray, password)
|
||||
val t1 = System.currentTimeMillis()
|
||||
|
||||
Timber.v("## importRoomKeys : decryptMegolmKeyFile done in " + (t1 - t0) + " ms")
|
||||
Timber.v("## importRoomKeys : decryptMegolmKeyFile done in ${t1 - t0} ms")
|
||||
|
||||
val importedSessions = MoshiProvider.providesMoshi()
|
||||
.adapter<List<MegolmSessionData>>(Types.newParameterizedType(List::class.java, MegolmSessionData::class.java))
|
||||
@ -846,7 +843,7 @@ internal class CryptoManager @Inject constructor(
|
||||
|
||||
val t2 = System.currentTimeMillis()
|
||||
|
||||
Timber.v("## importRoomKeys : JSON parsing " + (t2 - t1) + " ms")
|
||||
Timber.v("## importRoomKeys : JSON parsing ${t2 - t1} ms")
|
||||
|
||||
if (importedSessions == null) {
|
||||
throw Exception("Error")
|
||||
@ -870,29 +867,26 @@ internal class CryptoManager @Inject constructor(
|
||||
/**
|
||||
* Check if the user ids list have some unknown devices.
|
||||
* A success means there is no unknown devices.
|
||||
* If there are some unknown devices, a MXCryptoError.UNKNOWN_DEVICES_CODE exception is triggered.
|
||||
* If there are some unknown devices, a MXCryptoError.UnknownDevice exception is triggered.
|
||||
*
|
||||
* @param userIds the user ids list
|
||||
* @param callback the asynchronous callback.
|
||||
*/
|
||||
fun checkUnknownDevices(userIds: List<String>, callback: MatrixCallback<Unit>) {
|
||||
// force the refresh to ensure that the devices list is up-to-date
|
||||
CoroutineScope(coroutineDispatchers.crypto).launch {
|
||||
deviceListManager
|
||||
.downloadKeys(userIds, true)
|
||||
GlobalScope.launch(coroutineDispatchers.crypto) {
|
||||
runCatching { deviceListManager.downloadKeys(userIds, true) }
|
||||
.fold(
|
||||
{ callback.onFailure(it) },
|
||||
{
|
||||
val unknownDevices = getUnknownDevices(it)
|
||||
if (unknownDevices.map.isEmpty()) {
|
||||
callback.onSuccess(Unit)
|
||||
} else {
|
||||
// trigger an an unknown devices exception
|
||||
callback.onFailure(
|
||||
Failure.CryptoError(MXCryptoError(MXCryptoError.UNKNOWN_DEVICES_CODE,
|
||||
MXCryptoError.UNABLE_TO_ENCRYPT, MXCryptoError.UNKNOWN_DEVICES_REASON, unknownDevices)))
|
||||
callback.onFailure(Failure.CryptoError(MXCryptoError.UnknownDevice(unknownDevices)))
|
||||
}
|
||||
}
|
||||
},
|
||||
{ callback.onFailure(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -930,11 +924,8 @@ internal class CryptoManager @Inject constructor(
|
||||
*/
|
||||
// TODO add this info in CryptoRoomEntity?
|
||||
override fun isRoomBlacklistUnverifiedDevices(roomId: String?): Boolean {
|
||||
return if (null != roomId) {
|
||||
cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(roomId)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
return roomId?.let { cryptoStore.getRoomsListBlacklistUnverifiedDevices().contains(it) }
|
||||
?: false
|
||||
}
|
||||
|
||||
/**
|
||||
@ -947,7 +938,7 @@ internal class CryptoManager @Inject constructor(
|
||||
val roomIds = cryptoStore.getRoomsListBlacklistUnverifiedDevices().toMutableList()
|
||||
|
||||
if (add) {
|
||||
if (!roomIds.contains(roomId)) {
|
||||
if (roomId !in roomIds) {
|
||||
roomIds.add(roomId)
|
||||
}
|
||||
} else {
|
||||
@ -992,18 +983,18 @@ internal class CryptoManager @Inject constructor(
|
||||
* @param event the event to decrypt again.
|
||||
*/
|
||||
override fun reRequestRoomKeyForEvent(event: Event) {
|
||||
val wireContent = event.content!!
|
||||
|
||||
val algorithm = wireContent["algorithm"].toString()
|
||||
val senderKey = wireContent["sender_key"].toString()
|
||||
val sessionId = wireContent["session_id"].toString()
|
||||
val wireContent = event.content
|
||||
if (wireContent == null) {
|
||||
Timber.e("## reRequestRoomKeyForEvent Failed to re-request key, null content")
|
||||
return
|
||||
}
|
||||
|
||||
val requestBody = RoomKeyRequestBody()
|
||||
|
||||
requestBody.roomId = event.roomId
|
||||
requestBody.algorithm = algorithm
|
||||
requestBody.senderKey = senderKey
|
||||
requestBody.sessionId = sessionId
|
||||
requestBody.algorithm = wireContent["algorithm"]?.toString()
|
||||
requestBody.senderKey = wireContent["sender_key"]?.toString()
|
||||
requestBody.sessionId = wireContent["session_id"]?.toString()
|
||||
|
||||
outgoingRoomKeyRequestManager.resendRoomKeyRequest(requestBody)
|
||||
}
|
||||
@ -1022,7 +1013,7 @@ internal class CryptoManager @Inject constructor(
|
||||
*
|
||||
* @param listener listener
|
||||
*/
|
||||
fun removeRoomKeysRequestListener(listener: RoomKeysRequestListener) {
|
||||
override fun removeRoomKeysRequestListener(listener: RoomKeysRequestListener) {
|
||||
incomingRoomKeyRequestManager.removeRoomKeysRequestListener(listener)
|
||||
}
|
||||
|
||||
@ -1036,13 +1027,12 @@ internal class CryptoManager @Inject constructor(
|
||||
val unknownDevices = MXUsersDevicesMap<MXDeviceInfo>()
|
||||
val userIds = devicesInRoom.userIds
|
||||
for (userId in userIds) {
|
||||
val deviceIds = devicesInRoom.getUserDeviceIds(userId)
|
||||
for (deviceId in deviceIds!!) {
|
||||
val deviceInfo = devicesInRoom.getObject(deviceId, userId)
|
||||
|
||||
if (deviceInfo!!.isUnknown) {
|
||||
unknownDevices.setObject(deviceInfo, userId, deviceId)
|
||||
}
|
||||
devicesInRoom.getUserDeviceIds(userId)?.forEach { deviceId ->
|
||||
devicesInRoom.getObject(userId, deviceId)
|
||||
?.takeIf { it.isUnknown }
|
||||
?.let {
|
||||
unknownDevices.setObject(userId, deviceId, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1050,16 +1040,18 @@ internal class CryptoManager @Inject constructor(
|
||||
}
|
||||
|
||||
override fun downloadKeys(userIds: List<String>, forceDownload: Boolean, callback: MatrixCallback<MXUsersDevicesMap<MXDeviceInfo>>) {
|
||||
CoroutineScope(coroutineDispatchers.crypto).launch {
|
||||
deviceListManager
|
||||
.downloadKeys(userIds, forceDownload)
|
||||
.foldToCallback(callback)
|
||||
GlobalScope.launch(coroutineDispatchers.crypto) {
|
||||
runCatching {
|
||||
deviceListManager.downloadKeys(userIds, forceDownload)
|
||||
}.foldToCallback(callback)
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearCryptoCache(callback: MatrixCallback<Unit>) {
|
||||
clearCryptoDataTask.configureWith(Unit)
|
||||
.dispatchTo(callback)
|
||||
clearCryptoDataTask
|
||||
.configureWith {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
@ -1075,6 +1067,6 @@ internal class CryptoManager @Inject constructor(
|
||||
* ========================================================================================== */
|
||||
|
||||
override fun toString(): String {
|
||||
return "CryptoManager of " + credentials.userId + " (" + credentials.deviceId + ")"
|
||||
return "DefaultCryptoService of " + credentials.userId + " (" + credentials.deviceId + ")"
|
||||
}
|
||||
}
|
@ -18,10 +18,8 @@
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import android.text.TextUtils
|
||||
import arrow.core.Try
|
||||
import im.vector.matrix.android.api.MatrixPatterns
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.internal.extensions.onError
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
@ -221,7 +219,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||
Timber.v("Device list for $userId now up to date")
|
||||
}
|
||||
// And the response result
|
||||
usersDevicesInfoMap.setObjects(devices, userId)
|
||||
usersDevicesInfoMap.setObjects(userId, devices)
|
||||
}
|
||||
}
|
||||
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
||||
@ -237,9 +235,9 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||
* @param forceDownload Always download the keys even if cached.
|
||||
* @param callback the asynchronous callback
|
||||
*/
|
||||
suspend fun downloadKeys(userIds: List<String>?, forceDownload: Boolean): Try<MXUsersDevicesMap<MXDeviceInfo>> {
|
||||
suspend fun downloadKeys(userIds: List<String>?, forceDownload: Boolean): MXUsersDevicesMap<MXDeviceInfo> {
|
||||
Timber.v("## downloadKeys() : forceDownload $forceDownload : $userIds")
|
||||
// Map from userid -> deviceid -> DeviceInfo
|
||||
// Map from userId -> deviceId -> MXDeviceInfo
|
||||
val stored = MXUsersDevicesMap<MXDeviceInfo>()
|
||||
|
||||
// List of user ids we need to download keys for
|
||||
@ -258,7 +256,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||
val devices = cryptoStore.getUserDevices(userId)
|
||||
// should always be true
|
||||
if (devices != null) {
|
||||
stored.setObjects(devices, userId)
|
||||
stored.setObjects(userId, devices)
|
||||
} else {
|
||||
downloadUsers.add(userId)
|
||||
}
|
||||
@ -268,16 +266,15 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||
}
|
||||
return if (downloadUsers.isEmpty()) {
|
||||
Timber.v("## downloadKeys() : no new user device")
|
||||
Try.just(stored)
|
||||
stored
|
||||
} else {
|
||||
Timber.v("## downloadKeys() : starts")
|
||||
val t0 = System.currentTimeMillis()
|
||||
doKeyDownloadForUsers(downloadUsers)
|
||||
.map {
|
||||
Timber.v("## downloadKeys() : doKeyDownloadForUsers succeeds after " + (System.currentTimeMillis() - t0) + " ms")
|
||||
it.addEntriesFromMap(stored)
|
||||
it
|
||||
}
|
||||
val result = doKeyDownloadForUsers(downloadUsers)
|
||||
Timber.v("## downloadKeys() : doKeyDownloadForUsers succeeds after " + (System.currentTimeMillis() - t0) + " ms")
|
||||
result.also {
|
||||
it.addEntriesFromMap(stored)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -286,60 +283,60 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||
*
|
||||
* @param downloadUsers the user ids list
|
||||
*/
|
||||
private suspend fun doKeyDownloadForUsers(downloadUsers: MutableList<String>): Try<MXUsersDevicesMap<MXDeviceInfo>> {
|
||||
private suspend fun doKeyDownloadForUsers(downloadUsers: MutableList<String>): MXUsersDevicesMap<MXDeviceInfo> {
|
||||
Timber.v("## doKeyDownloadForUsers() : doKeyDownloadForUsers $downloadUsers")
|
||||
// get the user ids which did not already trigger a keys download
|
||||
val filteredUsers = downloadUsers.filter { MatrixPatterns.isUserId(it) }
|
||||
if (filteredUsers.isEmpty()) {
|
||||
// trigger nothing
|
||||
return Try.just(MXUsersDevicesMap())
|
||||
return MXUsersDevicesMap()
|
||||
}
|
||||
val params = DownloadKeysForUsersTask.Params(filteredUsers, syncTokenStore.getLastToken())
|
||||
return downloadKeysForUsersTask.execute(params)
|
||||
.map { response ->
|
||||
Timber.v("## doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users")
|
||||
for (userId in filteredUsers) {
|
||||
val devices = response.deviceKeys?.get(userId)
|
||||
Timber.v("## doKeyDownloadForUsers() : Got keys for $userId : $devices")
|
||||
if (devices != null) {
|
||||
val mutableDevices = HashMap(devices)
|
||||
val deviceIds = ArrayList(mutableDevices.keys)
|
||||
for (deviceId in deviceIds) {
|
||||
// Get the potential previously store device keys for this device
|
||||
val previouslyStoredDeviceKeys = cryptoStore.getUserDevice(deviceId, userId)
|
||||
val deviceInfo = mutableDevices[deviceId]
|
||||
val response = try {
|
||||
downloadKeysForUsersTask.execute(params)
|
||||
} catch (throwable: Throwable) {
|
||||
Timber.e(throwable, "##doKeyDownloadForUsers(): error")
|
||||
onKeysDownloadFailed(filteredUsers)
|
||||
throw throwable
|
||||
}
|
||||
Timber.v("## doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users")
|
||||
for (userId in filteredUsers) {
|
||||
val devices = response.deviceKeys?.get(userId)
|
||||
Timber.v("## doKeyDownloadForUsers() : Got keys for $userId : $devices")
|
||||
if (devices != null) {
|
||||
val mutableDevices = HashMap(devices)
|
||||
val deviceIds = ArrayList(mutableDevices.keys)
|
||||
for (deviceId in deviceIds) {
|
||||
// Get the potential previously store device keys for this device
|
||||
val previouslyStoredDeviceKeys = cryptoStore.getUserDevice(deviceId, userId)
|
||||
val deviceInfo = mutableDevices[deviceId]
|
||||
|
||||
// in some race conditions (like unit tests)
|
||||
// the self device must be seen as verified
|
||||
if (TextUtils.equals(deviceInfo!!.deviceId, credentials.deviceId) && TextUtils.equals(userId, credentials.userId)) {
|
||||
deviceInfo.verified = MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED
|
||||
}
|
||||
// Validate received keys
|
||||
if (!validateDeviceKeys(deviceInfo, userId, deviceId, previouslyStoredDeviceKeys)) {
|
||||
// New device keys are not valid. Do not store them
|
||||
mutableDevices.remove(deviceId)
|
||||
if (null != previouslyStoredDeviceKeys) {
|
||||
// But keep old validated ones if any
|
||||
mutableDevices[deviceId] = previouslyStoredDeviceKeys
|
||||
}
|
||||
} else if (null != previouslyStoredDeviceKeys) {
|
||||
// The verified status is not sync'ed with hs.
|
||||
// This is a client side information, valid only for this client.
|
||||
// So, transfer its previous value
|
||||
mutableDevices[deviceId]!!.verified = previouslyStoredDeviceKeys.verified
|
||||
}
|
||||
}
|
||||
// Update the store
|
||||
// Note that devices which aren't in the response will be removed from the stores
|
||||
cryptoStore.storeUserDevices(userId, mutableDevices)
|
||||
}
|
||||
// in some race conditions (like unit tests)
|
||||
// the self device must be seen as verified
|
||||
if (TextUtils.equals(deviceInfo!!.deviceId, credentials.deviceId) && TextUtils.equals(userId, credentials.userId)) {
|
||||
deviceInfo.verified = MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED
|
||||
}
|
||||
// Validate received keys
|
||||
if (!validateDeviceKeys(deviceInfo, userId, deviceId, previouslyStoredDeviceKeys)) {
|
||||
// New device keys are not valid. Do not store them
|
||||
mutableDevices.remove(deviceId)
|
||||
if (null != previouslyStoredDeviceKeys) {
|
||||
// But keep old validated ones if any
|
||||
mutableDevices[deviceId] = previouslyStoredDeviceKeys
|
||||
}
|
||||
} else if (null != previouslyStoredDeviceKeys) {
|
||||
// The verified status is not sync'ed with hs.
|
||||
// This is a client side information, valid only for this client.
|
||||
// So, transfer its previous value
|
||||
mutableDevices[deviceId]!!.verified = previouslyStoredDeviceKeys.verified
|
||||
}
|
||||
onKeysDownloadSucceed(filteredUsers, response.failures)
|
||||
}
|
||||
.onError {
|
||||
Timber.e(it, "##doKeyDownloadForUsers(): error")
|
||||
onKeysDownloadFailed(filteredUsers)
|
||||
}
|
||||
// Update the store
|
||||
// Note that devices which aren't in the response will be removed from the stores
|
||||
cryptoStore.storeUserDevices(userId, mutableDevices)
|
||||
}
|
||||
}
|
||||
return onKeysDownloadSucceed(filteredUsers, response.failures)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -380,14 +377,14 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||
}
|
||||
|
||||
val signKeyId = "ed25519:" + deviceKeys.deviceId
|
||||
val signKey = deviceKeys.keys!![signKeyId]
|
||||
val signKey = deviceKeys.keys?.get(signKeyId)
|
||||
|
||||
if (null == signKey) {
|
||||
Timber.e("## validateDeviceKeys() : Device " + userId + ":" + deviceKeys.deviceId + " has no ed25519 key")
|
||||
return false
|
||||
}
|
||||
|
||||
val signatureMap = deviceKeys.signatures!![userId]
|
||||
val signatureMap = deviceKeys.signatures?.get(userId)
|
||||
|
||||
if (null == signatureMap) {
|
||||
Timber.e("## validateDeviceKeys() : Device " + userId + ":" + deviceKeys.deviceId + " has no map for " + userId)
|
||||
@ -413,7 +410,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||
|
||||
if (!isVerified) {
|
||||
Timber.e("## validateDeviceKeys() : Unable to verify signature on device " + userId + ":"
|
||||
+ deviceKeys.deviceId + " with error " + errorMessage)
|
||||
+ deviceKeys.deviceId + " with error " + errorMessage)
|
||||
return false
|
||||
}
|
||||
|
||||
@ -424,8 +421,8 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||
//
|
||||
// Should we warn the user about it somehow?
|
||||
Timber.e("## validateDeviceKeys() : WARNING:Ed25519 key for device " + userId + ":"
|
||||
+ deviceKeys.deviceId + " has changed : "
|
||||
+ previouslyStoredDeviceKeys.fingerprint() + " -> " + signKey)
|
||||
+ deviceKeys.deviceId + " has changed : "
|
||||
+ previouslyStoredDeviceKeys.fingerprint() + " -> " + signKey)
|
||||
|
||||
Timber.e("## validateDeviceKeys() : $previouslyStoredDeviceKeys -> $deviceKeys")
|
||||
Timber.e("## validateDeviceKeys() : " + previouslyStoredDeviceKeys.keys + " -> " + deviceKeys.keys)
|
||||
@ -465,15 +462,16 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
|
||||
}
|
||||
|
||||
cryptoStore.saveDeviceTrackingStatuses(deviceTrackingStatuses)
|
||||
doKeyDownloadForUsers(users)
|
||||
.fold(
|
||||
{
|
||||
Timber.e(it, "## refreshOutdatedDeviceLists() : ERROR updating device keys for users $users")
|
||||
},
|
||||
{
|
||||
Timber.v("## refreshOutdatedDeviceLists() : done")
|
||||
}
|
||||
)
|
||||
runCatching {
|
||||
doKeyDownloadForUsers(users)
|
||||
}.fold(
|
||||
{
|
||||
Timber.v("## refreshOutdatedDeviceLists() : done")
|
||||
},
|
||||
{
|
||||
Timber.e(it, "## refreshOutdatedDeviceLists() : ERROR updating device keys for users $users")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -84,7 +84,7 @@ internal class IncomingRoomKeyRequestManager @Inject constructor(
|
||||
Timber.e("## processReceivedRoomKeyRequests() : Ignoring room key request from other user for now")
|
||||
return
|
||||
}
|
||||
// todo: should we queue up requests we don't yet have keys for, in case they turn up later?
|
||||
// TODO: should we queue up requests we don't yet have keys for, in case they turn up later?
|
||||
// if we don't have a decryptor for this room/alg, we don't have
|
||||
// the keys for the requested events, and can drop the requests.
|
||||
val decryptor = roomDecryptorProvider.getRoomDecryptor(roomId, alg)
|
||||
|
@ -1,39 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
* Copyright 2017 Vector Creations Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
|
||||
/**
|
||||
* This class represents a decryption exception
|
||||
*/
|
||||
class MXDecryptionException
|
||||
(
|
||||
/**
|
||||
* the linked crypto error
|
||||
*/
|
||||
val cryptoError: MXCryptoError?
|
||||
) : Exception() {
|
||||
|
||||
override val message: String?
|
||||
get() = cryptoError?.message ?: super.message
|
||||
|
||||
override fun getLocalizedMessage(): String {
|
||||
return cryptoError?.message ?: super.getLocalizedMessage()
|
||||
}
|
||||
}
|
10
matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXEventDecryptionResult.kt
Normal file → Executable file
10
matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXEventDecryptionResult.kt
Normal file → Executable file
@ -1,6 +1,5 @@
|
||||
/*
|
||||
* Copyright 2016 OpenMarket Ltd
|
||||
* Copyright 2017 Vector Creations Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -18,7 +17,6 @@
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import im.vector.matrix.android.api.util.JsonDict
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* The result of a (successful) call to decryptEvent.
|
||||
@ -28,23 +26,23 @@ data class MXEventDecryptionResult(
|
||||
/**
|
||||
* The plaintext payload for the event (typically containing "type" and "content" fields).
|
||||
*/
|
||||
var clearEvent: JsonDict? = null,
|
||||
val clearEvent: JsonDict,
|
||||
|
||||
/**
|
||||
* Key owned by the sender of this event.
|
||||
* See MXEvent.senderKey.
|
||||
*/
|
||||
var senderCurve25519Key: String? = null,
|
||||
val senderCurve25519Key: String? = null,
|
||||
|
||||
/**
|
||||
* Ed25519 key claimed by the sender of this event.
|
||||
* See MXEvent.claimedEd25519Key.
|
||||
*/
|
||||
var claimedEd25519Key: String? = null,
|
||||
val claimedEd25519Key: String? = null,
|
||||
|
||||
/**
|
||||
* List of curve25519 keys involved in telling us about the senderCurve25519Key and
|
||||
* claimedEd25519Key. See MXEvent.forwardingCurve25519KeyChain.
|
||||
*/
|
||||
var forwardingCurve25519KeyChain: List<String> = ArrayList()
|
||||
val forwardingCurve25519KeyChain: List<String> = emptyList()
|
||||
)
|
||||
|
@ -229,7 +229,7 @@ object MXMegolmExportEncryption {
|
||||
throw Exception("Header line not found")
|
||||
}
|
||||
|
||||
val line = fileStr.substring(lineStart, lineEnd).trim { it <= ' ' }
|
||||
val line = fileStr.substring(lineStart, lineEnd).trim()
|
||||
|
||||
// start the next line after the newline
|
||||
lineStart = lineEnd + 1
|
||||
@ -247,9 +247,9 @@ object MXMegolmExportEncryption {
|
||||
val line: String
|
||||
|
||||
if (lineEnd < 0) {
|
||||
line = fileStr.substring(lineStart).trim { it <= ' ' }
|
||||
line = fileStr.substring(lineStart).trim()
|
||||
} else {
|
||||
line = fileStr.substring(lineStart, lineEnd).trim { it <= ' ' }
|
||||
line = fileStr.substring(lineStart, lineEnd).trim()
|
||||
}
|
||||
|
||||
if (TextUtils.equals(line, TRAILER_LINE)) {
|
||||
|
@ -21,20 +21,16 @@ import android.text.TextUtils
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.util.JSON_DICT_PARAMETERIZED_TYPE
|
||||
import im.vector.matrix.android.api.util.JsonDict
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.MXDecryptionResult
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
|
||||
import im.vector.matrix.android.internal.crypto.model.OlmSessionWrapper
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.util.JsonCanonicalizer
|
||||
import im.vector.matrix.android.internal.util.convertFromUTF8
|
||||
import im.vector.matrix.android.internal.util.convertToUTF8
|
||||
import org.matrix.olm.OlmAccount
|
||||
import org.matrix.olm.OlmInboundGroupSession
|
||||
import org.matrix.olm.OlmMessage
|
||||
import org.matrix.olm.OlmOutboundGroupSession
|
||||
import org.matrix.olm.OlmSession
|
||||
import org.matrix.olm.OlmUtility
|
||||
import org.matrix.olm.*
|
||||
import timber.log.Timber
|
||||
import java.net.URLEncoder
|
||||
import java.util.*
|
||||
@ -83,13 +79,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||
//
|
||||
// The first level keys are timeline ids.
|
||||
// The second level keys are strings of form "<senderKey>|<session_id>|<message_index>"
|
||||
// Values are true.
|
||||
private val inboundGroupSessionMessageIndexes: MutableMap<String, MutableMap<String, Boolean>> = HashMap()
|
||||
|
||||
/**
|
||||
* inboundGroupSessionWithId error
|
||||
*/
|
||||
private var inboundGroupSessionWithIdError: MXCryptoError? = null
|
||||
private val inboundGroupSessionMessageIndexes: MutableMap<String, MutableSet<String>> = HashMap()
|
||||
|
||||
init {
|
||||
// Retrieve the account from the store
|
||||
@ -119,13 +109,13 @@ internal class MXOlmDevice @Inject constructor(
|
||||
try {
|
||||
deviceCurve25519Key = olmAccount!!.identityKeys()[OlmAccount.JSON_KEY_IDENTITY_KEY]
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## MXOlmDevice : cannot find " + OlmAccount.JSON_KEY_IDENTITY_KEY + " with error")
|
||||
Timber.e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_IDENTITY_KEY} with error")
|
||||
}
|
||||
|
||||
try {
|
||||
deviceEd25519Key = olmAccount!!.identityKeys()[OlmAccount.JSON_KEY_FINGER_PRINT_KEY]
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## MXOlmDevice : cannot find " + OlmAccount.JSON_KEY_FINGER_PRINT_KEY + " with error")
|
||||
Timber.e(e, "## MXOlmDevice : cannot find ${OlmAccount.JSON_KEY_FINGER_PRINT_KEY} with error")
|
||||
}
|
||||
}
|
||||
|
||||
@ -297,13 +287,13 @@ internal class MXOlmDevice @Inject constructor(
|
||||
|
||||
val res = HashMap<String, String>()
|
||||
|
||||
if (!TextUtils.isEmpty(payloadString)) {
|
||||
res["payload"] = payloadString!!
|
||||
if (!payloadString.isNullOrEmpty()) {
|
||||
res["payload"] = payloadString
|
||||
}
|
||||
|
||||
val sessionIdentifier = olmSession.sessionIdentifier()
|
||||
|
||||
if (!TextUtils.isEmpty(sessionIdentifier)) {
|
||||
if (!sessionIdentifier.isNullOrEmpty()) {
|
||||
res["session_id"] = sessionIdentifier
|
||||
}
|
||||
|
||||
@ -513,24 +503,26 @@ internal class MXOlmDevice @Inject constructor(
|
||||
forwardingCurve25519KeyChain: List<String>,
|
||||
keysClaimed: Map<String, String>,
|
||||
exportFormat: Boolean): Boolean {
|
||||
val existingInboundSession = getInboundGroupSession(sessionId, senderKey, roomId)
|
||||
val session = OlmInboundGroupSessionWrapper(sessionKey, exportFormat)
|
||||
runCatching { getInboundGroupSession(sessionId, senderKey, roomId) }
|
||||
.fold(
|
||||
{
|
||||
// If we already have this session, consider updating it
|
||||
Timber.e("## addInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
|
||||
|
||||
if (null != existingInboundSession) {
|
||||
// If we already have this session, consider updating it
|
||||
Timber.e("## addInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
|
||||
val existingFirstKnown = it.firstKnownIndex!!
|
||||
val newKnownFirstIndex = session.firstKnownIndex
|
||||
|
||||
val existingFirstKnown = existingInboundSession.firstKnownIndex!!
|
||||
val newKnownFirstIndex = session.firstKnownIndex!!
|
||||
|
||||
//If our existing session is better we keep it
|
||||
if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) {
|
||||
if (session.olmInboundGroupSession != null) {
|
||||
session.olmInboundGroupSession!!.releaseSession()
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
//If our existing session is better we keep it
|
||||
if (newKnownFirstIndex != null && existingFirstKnown <= newKnownFirstIndex) {
|
||||
session.olmInboundGroupSession?.releaseSession()
|
||||
return false
|
||||
}
|
||||
},
|
||||
{
|
||||
// Nothing to do in case of error
|
||||
}
|
||||
)
|
||||
|
||||
// sanity check
|
||||
if (null == session.olmInboundGroupSession) {
|
||||
@ -545,7 +537,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||
return false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
session.olmInboundGroupSession!!.releaseSession()
|
||||
session.olmInboundGroupSession?.releaseSession()
|
||||
Timber.e(e, "## addInboundGroupSession : sessionIdentifier() failed")
|
||||
return false
|
||||
}
|
||||
@ -584,13 +576,13 @@ internal class MXOlmDevice @Inject constructor(
|
||||
}
|
||||
|
||||
// sanity check
|
||||
if (null == session || null == session.olmInboundGroupSession) {
|
||||
if (session?.olmInboundGroupSession == null) {
|
||||
Timber.e("## importInboundGroupSession : invalid session")
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
if (!TextUtils.equals(session.olmInboundGroupSession!!.sessionIdentifier(), sessionId)) {
|
||||
if (!TextUtils.equals(session.olmInboundGroupSession?.sessionIdentifier(), sessionId)) {
|
||||
Timber.e("## importInboundGroupSession : ERROR: Mismatched group session ID from senderKey: " + senderKey!!)
|
||||
if (session.olmInboundGroupSession != null) session.olmInboundGroupSession!!.releaseSession()
|
||||
continue
|
||||
@ -601,20 +593,27 @@ internal class MXOlmDevice @Inject constructor(
|
||||
continue
|
||||
}
|
||||
|
||||
val existingOlmSession = getInboundGroupSession(sessionId, senderKey, roomId)
|
||||
if (null != existingOlmSession) {
|
||||
// If we already have this session, consider updating it
|
||||
Timber.e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
|
||||
runCatching { getInboundGroupSession(sessionId, senderKey, roomId) }
|
||||
.fold(
|
||||
{
|
||||
// If we already have this session, consider updating it
|
||||
Timber.e("## importInboundGroupSession() : Update for megolm session $senderKey/$sessionId")
|
||||
|
||||
// For now we just ignore updates. TODO: implement something here
|
||||
if (existingOlmSession.firstKnownIndex!! <= session.firstKnownIndex!!) {
|
||||
//Ignore this, keep existing
|
||||
session.olmInboundGroupSession!!.releaseSession()
|
||||
continue
|
||||
}
|
||||
}
|
||||
// For now we just ignore updates. TODO: implement something here
|
||||
if (it.firstKnownIndex!! <= session.firstKnownIndex!!) {
|
||||
//Ignore this, keep existing
|
||||
session.olmInboundGroupSession!!.releaseSession()
|
||||
} else {
|
||||
sessions.add(session)
|
||||
}
|
||||
Unit
|
||||
},
|
||||
{
|
||||
// Session does not already exist, add it
|
||||
sessions.add(session)
|
||||
}
|
||||
|
||||
sessions.add(session)
|
||||
)
|
||||
}
|
||||
|
||||
store.storeInboundGroupSessions(sessions)
|
||||
@ -644,81 +643,59 @@ internal class MXOlmDevice @Inject constructor(
|
||||
* @param senderKey the base64-encoded curve25519 key of the sender.
|
||||
* @return the decrypting result. Nil if the sessionId is unknown.
|
||||
*/
|
||||
@Throws(MXDecryptionException::class)
|
||||
fun decryptGroupMessage(body: String,
|
||||
roomId: String,
|
||||
timeline: String?,
|
||||
sessionId: String,
|
||||
senderKey: String): MXDecryptionResult? {
|
||||
val result = MXDecryptionResult()
|
||||
senderKey: String): OlmDecryptionResult {
|
||||
val session = getInboundGroupSession(sessionId, senderKey, roomId)
|
||||
|
||||
if (null != session) {
|
||||
// Check that the room id matches the original one for the session. This stops
|
||||
// the HS pretending a message was targeting a different room.
|
||||
if (TextUtils.equals(roomId, session.roomId)) {
|
||||
var errorMessage = ""
|
||||
var decryptResult: OlmInboundGroupSession.DecryptMessageResult? = null
|
||||
try {
|
||||
decryptResult = session.olmInboundGroupSession!!.decryptMessage(body)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## decryptGroupMessage () : decryptMessage failed")
|
||||
errorMessage = e.message ?: ""
|
||||
}
|
||||
|
||||
if (null != decryptResult) {
|
||||
if (null != timeline) {
|
||||
if (!inboundGroupSessionMessageIndexes.containsKey(timeline)) {
|
||||
inboundGroupSessionMessageIndexes[timeline] = HashMap()
|
||||
}
|
||||
|
||||
val messageIndexKey = senderKey + "|" + sessionId + "|" + decryptResult.mIndex
|
||||
|
||||
if (null != inboundGroupSessionMessageIndexes[timeline]!![messageIndexKey]) {
|
||||
val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex)
|
||||
Timber.e("## decryptGroupMessage() : $reason")
|
||||
throw MXDecryptionException(MXCryptoError(MXCryptoError.DUPLICATED_MESSAGE_INDEX_ERROR_CODE,
|
||||
MXCryptoError.UNABLE_TO_DECRYPT, reason))
|
||||
}
|
||||
|
||||
inboundGroupSessionMessageIndexes[timeline]!!.put(messageIndexKey, true)
|
||||
}
|
||||
|
||||
store.storeInboundGroupSessions(listOf(session))
|
||||
try {
|
||||
val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
|
||||
val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage)
|
||||
val payload = adapter.fromJson(payloadString)
|
||||
result.payload = payload
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## decryptGroupMessage() : RLEncoder.encode failed " + e.message)
|
||||
return null
|
||||
}
|
||||
|
||||
if (null == result.payload) {
|
||||
Timber.e("## decryptGroupMessage() : fails to parse the payload")
|
||||
return null
|
||||
}
|
||||
|
||||
result.keysClaimed = session.keysClaimed
|
||||
result.senderKey = senderKey
|
||||
result.forwardingCurve25519KeyChain = session.forwardingCurve25519KeyChain
|
||||
} else {
|
||||
Timber.e("## decryptGroupMessage() : failed to decode the message")
|
||||
throw MXDecryptionException(MXCryptoError(MXCryptoError.OLM_ERROR_CODE, errorMessage, null))
|
||||
}
|
||||
} else {
|
||||
val reason = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId)
|
||||
Timber.e("## decryptGroupMessage() : $reason")
|
||||
throw MXDecryptionException(MXCryptoError(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_ERROR_CODE,
|
||||
MXCryptoError.UNABLE_TO_DECRYPT, reason))
|
||||
// Check that the room id matches the original one for the session. This stops
|
||||
// the HS pretending a message was targeting a different room.
|
||||
if (roomId == session.roomId) {
|
||||
var decryptResult: OlmInboundGroupSession.DecryptMessageResult? = null
|
||||
try {
|
||||
decryptResult = session.olmInboundGroupSession!!.decryptMessage(body)
|
||||
} catch (e: OlmException) {
|
||||
Timber.e(e, "## decryptGroupMessage () : decryptMessage failed")
|
||||
throw MXCryptoError.OlmError(e)
|
||||
}
|
||||
} else {
|
||||
Timber.e("## decryptGroupMessage() : Cannot retrieve inbound group session $sessionId")
|
||||
throw MXDecryptionException(inboundGroupSessionWithIdError)
|
||||
}
|
||||
|
||||
return result
|
||||
if (null != timeline) {
|
||||
val timelineSet = inboundGroupSessionMessageIndexes.getOrPut(timeline) { mutableSetOf() }
|
||||
|
||||
val messageIndexKey = senderKey + "|" + sessionId + "|" + decryptResult.mIndex
|
||||
|
||||
if (timelineSet.contains(messageIndexKey)) {
|
||||
val reason = String.format(MXCryptoError.DUPLICATE_MESSAGE_INDEX_REASON, decryptResult.mIndex)
|
||||
Timber.e("## decryptGroupMessage() : $reason")
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.DUPLICATED_MESSAGE_INDEX, reason)
|
||||
}
|
||||
|
||||
timelineSet.add(messageIndexKey)
|
||||
}
|
||||
|
||||
store.storeInboundGroupSessions(listOf(session))
|
||||
val payload = try {
|
||||
val adapter = MoshiProvider.providesMoshi().adapter<JsonDict>(JSON_DICT_PARAMETERIZED_TYPE)
|
||||
val payloadString = convertFromUTF8(decryptResult.mDecryptedMessage)
|
||||
adapter.fromJson(payloadString)
|
||||
} catch (e: Exception) {
|
||||
Timber.e("## decryptGroupMessage() : fails to parse the payload")
|
||||
throw
|
||||
MXCryptoError.Base(MXCryptoError.ErrorType.BAD_DECRYPTED_FORMAT, MXCryptoError.BAD_DECRYPTED_FORMAT_TEXT_REASON)
|
||||
}
|
||||
|
||||
return OlmDecryptionResult(
|
||||
payload,
|
||||
session.keysClaimed,
|
||||
senderKey,
|
||||
session.forwardingCurve25519KeyChain
|
||||
)
|
||||
} else {
|
||||
val reason = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId)
|
||||
Timber.e("## decryptGroupMessage() : $reason")
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, reason)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -732,7 +709,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
// Utilities
|
||||
// Utilities
|
||||
|
||||
/**
|
||||
* Verify an ed25519 signature on a JSON object.
|
||||
@ -745,7 +722,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||
@Throws(Exception::class)
|
||||
fun verifySignature(key: String, jsonDictionary: Map<String, Any>, signature: String) {
|
||||
// Check signature on the canonical version of the JSON
|
||||
olmUtility!!.verifyEd25519Signature(signature, key, MoshiProvider.getCanonicalJson<Map<*, *>>(Map::class.java, jsonDictionary))
|
||||
olmUtility!!.verifyEd25519Signature(signature, key, JsonCanonicalizer.getCanonicalJson(Map::class.java, jsonDictionary))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -767,9 +744,9 @@ internal class MXOlmDevice @Inject constructor(
|
||||
*/
|
||||
private fun getSessionForDevice(theirDeviceIdentityKey: String, sessionId: String): OlmSessionWrapper? {
|
||||
// sanity check
|
||||
return if (!TextUtils.isEmpty(theirDeviceIdentityKey) && !TextUtils.isEmpty(sessionId)) {
|
||||
return if (theirDeviceIdentityKey.isEmpty() || sessionId.isEmpty()) null else {
|
||||
store.getDeviceSession(sessionId, theirDeviceIdentityKey)
|
||||
} else null
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -782,26 +759,27 @@ internal class MXOlmDevice @Inject constructor(
|
||||
* @param senderKey the base64-encoded curve25519 key of the sender.
|
||||
* @return the inbound group session.
|
||||
*/
|
||||
fun getInboundGroupSession(sessionId: String?, senderKey: String?, roomId: String?): OlmInboundGroupSessionWrapper? {
|
||||
inboundGroupSessionWithIdError = null
|
||||
fun getInboundGroupSession(sessionId: String?, senderKey: String?, roomId: String?): OlmInboundGroupSessionWrapper {
|
||||
if (sessionId.isNullOrBlank() || senderKey.isNullOrBlank()) {
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_SENDER_KEY, MXCryptoError.ERROR_MISSING_PROPERTY_REASON)
|
||||
}
|
||||
|
||||
val session = store.getInboundGroupSession(sessionId!!, senderKey!!)
|
||||
val session = store.getInboundGroupSession(sessionId, senderKey)
|
||||
|
||||
if (null != session) {
|
||||
if (session != null) {
|
||||
// Check that the room id matches the original one for the session. This stops
|
||||
// the HS pretending a message was targeting a different room.
|
||||
if (!TextUtils.equals(roomId, session.roomId)) {
|
||||
val errorDescription = String.format(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_REASON, roomId, session.roomId)
|
||||
Timber.e("## getInboundGroupSession() : $errorDescription")
|
||||
inboundGroupSessionWithIdError = MXCryptoError(MXCryptoError.INBOUND_SESSION_MISMATCH_ROOM_ID_ERROR_CODE,
|
||||
MXCryptoError.UNABLE_TO_DECRYPT, errorDescription)
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.INBOUND_SESSION_MISMATCH_ROOM_ID, errorDescription)
|
||||
} else {
|
||||
return session
|
||||
}
|
||||
} else {
|
||||
Timber.e("## getInboundGroupSession() : Cannot retrieve inbound group session $sessionId")
|
||||
inboundGroupSessionWithIdError = MXCryptoError(MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_ERROR_CODE,
|
||||
MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON, null)
|
||||
throw MXCryptoError.Base(MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID, MXCryptoError.UNKNOWN_INBOUND_SESSION_ID_REASON)
|
||||
}
|
||||
return session
|
||||
}
|
||||
|
||||
/**
|
||||
@ -813,6 +791,6 @@ internal class MXOlmDevice @Inject constructor(
|
||||
* @return true if the unbound session keys are known.
|
||||
*/
|
||||
fun hasInboundSessionKeys(roomId: String, senderKey: String, sessionId: String): Boolean {
|
||||
return null != getInboundGroupSession(sessionId, senderKey, roomId)
|
||||
return runCatching { getInboundGroupSession(sessionId, senderKey, roomId) }.isSuccess
|
||||
}
|
||||
}
|
||||
|
@ -17,8 +17,6 @@
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class ObjectSigner @Inject constructor(private val credentials: Credentials,
|
||||
@ -44,7 +42,9 @@ internal class ObjectSigner @Inject constructor(private val credentials: Credent
|
||||
|
||||
val content = HashMap<String, String>()
|
||||
|
||||
content["ed25519:" + credentials.deviceId] = olmDevice.signMessage(strToSign)!!
|
||||
|
||||
content["ed25519:" + credentials.deviceId] = olmDevice.signMessage(strToSign)
|
||||
?: "" //null reported by rageshake if happens during logout
|
||||
|
||||
result[credentials.userId] = content
|
||||
|
||||
|
@ -16,14 +16,12 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import arrow.core.Try
|
||||
import arrow.instances.`try`.applicativeError.handleError
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.internal.crypto.model.MXKey
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse
|
||||
import im.vector.matrix.android.internal.crypto.tasks.UploadKeysTask
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.util.JsonCanonicalizer
|
||||
import org.matrix.olm.OlmAccount
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
@ -59,13 +57,13 @@ internal class OneTimeKeysUploader @Inject constructor(
|
||||
/**
|
||||
* Check if the OTK must be uploaded.
|
||||
*/
|
||||
suspend fun maybeUploadOneTimeKeys(): Try<Unit> {
|
||||
suspend fun maybeUploadOneTimeKeys() {
|
||||
if (oneTimeKeyCheckInProgress) {
|
||||
return Try.just(Unit)
|
||||
return
|
||||
}
|
||||
if (System.currentTimeMillis() - lastOneTimeKeyCheck < ONE_TIME_KEY_UPLOAD_PERIOD) {
|
||||
// we've done a key upload recently.
|
||||
return Try.just(Unit)
|
||||
return
|
||||
}
|
||||
|
||||
lastOneTimeKeyCheck = System.currentTimeMillis()
|
||||
@ -81,41 +79,31 @@ internal class OneTimeKeysUploader @Inject constructor(
|
||||
// discard the oldest private keys first. This will eventually clean
|
||||
// out stale private keys that won't receive a message.
|
||||
val keyLimit = Math.floor(maxOneTimeKeys / 2.0).toInt()
|
||||
val result = if (oneTimeKeyCount != null) {
|
||||
if (oneTimeKeyCount != null) {
|
||||
uploadOTK(oneTimeKeyCount!!, keyLimit)
|
||||
} else {
|
||||
// ask the server how many keys we have
|
||||
val uploadKeysParams = UploadKeysTask.Params(null, null, credentials.deviceId!!)
|
||||
uploadKeysTask.execute(uploadKeysParams)
|
||||
.flatMap {
|
||||
// We need to keep a pool of one time public keys on the server so that
|
||||
// other devices can start conversations with us. But we can only store
|
||||
// a finite number of private keys in the olm Account object.
|
||||
// To complicate things further then can be a delay between a device
|
||||
// claiming a public one time key from the server and it sending us a
|
||||
// message. We need to keep the corresponding private key locally until
|
||||
// we receive the message.
|
||||
// But that message might never arrive leaving us stuck with duff
|
||||
// private keys clogging up our local storage.
|
||||
// So we need some kind of engineering compromise to balance all of
|
||||
// these factors.
|
||||
// TODO Why we do not set oneTimeKeyCount here?
|
||||
// TODO This is not needed anymore, see https://github.com/matrix-org/matrix-js-sdk/pull/493 (TODO on iOS also)
|
||||
val keyCount = it.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)
|
||||
uploadOTK(keyCount, keyLimit)
|
||||
}
|
||||
val response = uploadKeysTask.execute(uploadKeysParams)
|
||||
// We need to keep a pool of one time public keys on the server so that
|
||||
// other devices can start conversations with us. But we can only store
|
||||
// a finite number of private keys in the olm Account object.
|
||||
// To complicate things further then can be a delay between a device
|
||||
// claiming a public one time key from the server and it sending us a
|
||||
// message. We need to keep the corresponding private key locally until
|
||||
// we receive the message.
|
||||
// But that message might never arrive leaving us stuck with duff
|
||||
// private keys clogging up our local storage.
|
||||
// So we need some kind of engineering compromise to balance all of
|
||||
// these factors.
|
||||
// TODO Why we do not set oneTimeKeyCount here?
|
||||
// TODO This is not needed anymore, see https://github.com/matrix-org/matrix-js-sdk/pull/493 (TODO on iOS also)
|
||||
val keyCount = response.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)
|
||||
uploadOTK(keyCount, keyLimit)
|
||||
}
|
||||
return result
|
||||
.map {
|
||||
Timber.v("## uploadKeys() : success")
|
||||
oneTimeKeyCount = null
|
||||
oneTimeKeyCheckInProgress = false
|
||||
}
|
||||
.handleError {
|
||||
Timber.e(it, "## uploadKeys() : failed")
|
||||
oneTimeKeyCount = null
|
||||
oneTimeKeyCheckInProgress = false
|
||||
}
|
||||
Timber.v("## uploadKeys() : success")
|
||||
oneTimeKeyCount = null
|
||||
oneTimeKeyCheckInProgress = false
|
||||
}
|
||||
|
||||
/**
|
||||
@ -124,33 +112,30 @@ internal class OneTimeKeysUploader @Inject constructor(
|
||||
* @param keyCount the key count
|
||||
* @param keyLimit the limit
|
||||
*/
|
||||
private suspend fun uploadOTK(keyCount: Int, keyLimit: Int): Try<Unit> {
|
||||
private suspend fun uploadOTK(keyCount: Int, keyLimit: Int) {
|
||||
if (keyLimit <= keyCount) {
|
||||
// If we don't need to generate any more keys then we are done.
|
||||
return Try.just(Unit)
|
||||
return
|
||||
}
|
||||
|
||||
val keysThisLoop = Math.min(keyLimit - keyCount, ONE_TIME_KEY_GENERATION_MAX_NUMBER)
|
||||
olmDevice.generateOneTimeKeys(keysThisLoop)
|
||||
return uploadOneTimeKeys()
|
||||
.flatMap {
|
||||
if (it.hasOneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)) {
|
||||
uploadOTK(it.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE), keyLimit)
|
||||
} else {
|
||||
Timber.e("## uploadLoop() : response for uploading keys does not contain one_time_key_counts.signed_curve25519")
|
||||
Try.raise(Exception("response for uploading keys does not contain one_time_key_counts.signed_curve25519"))
|
||||
}
|
||||
}
|
||||
val response = uploadOneTimeKeys()
|
||||
if (response.hasOneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE)) {
|
||||
uploadOTK(response.oneTimeKeyCountsForAlgorithm(MXKey.KEY_SIGNED_CURVE_25519_TYPE), keyLimit)
|
||||
} else {
|
||||
Timber.e("## uploadLoop() : response for uploading keys does not contain one_time_key_counts.signed_curve25519")
|
||||
throw Exception("response for uploading keys does not contain one_time_key_counts.signed_curve25519")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload my user's one time keys.
|
||||
*/
|
||||
private suspend fun uploadOneTimeKeys(): Try<KeysUploadResponse> {
|
||||
private suspend fun uploadOneTimeKeys(): KeysUploadResponse {
|
||||
val oneTimeKeys = olmDevice.getOneTimeKeys()
|
||||
val oneTimeJson = HashMap<String, Any>()
|
||||
|
||||
val curve25519Map = oneTimeKeys!![OlmAccount.JSON_KEY_ONE_TIME_KEY]
|
||||
val curve25519Map = oneTimeKeys?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY)
|
||||
|
||||
if (null != curve25519Map) {
|
||||
for (key_id in curve25519Map.keys) {
|
||||
@ -158,7 +143,7 @@ internal class OneTimeKeysUploader @Inject constructor(
|
||||
k["key"] = curve25519Map.getValue(key_id)
|
||||
|
||||
// the key is also signed
|
||||
val canonicalJson = MoshiProvider.getCanonicalJson(Map::class.java, k)
|
||||
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, k)
|
||||
|
||||
k["signatures"] = objectSigner.signObject(canonicalJson)
|
||||
|
||||
@ -169,13 +154,10 @@ internal class OneTimeKeysUploader @Inject constructor(
|
||||
// For now, we set the device id explicitly, as we may not be using the
|
||||
// same one as used in login.
|
||||
val uploadParams = UploadKeysTask.Params(null, oneTimeJson, credentials.deviceId!!)
|
||||
return uploadKeysTask
|
||||
.execute(uploadParams)
|
||||
.map {
|
||||
lastPublishedOneTimeKeys = oneTimeKeys
|
||||
olmDevice.markKeysAsPublished()
|
||||
it
|
||||
}
|
||||
val response = uploadKeysTask.execute(uploadParams)
|
||||
lastPublishedOneTimeKeys = oneTimeKeys
|
||||
olmDevice.markKeysAsPublished()
|
||||
return response
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -17,7 +17,6 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import android.os.Handler
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
@ -28,9 +27,11 @@ import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.TaskThread
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import im.vector.matrix.android.internal.util.createBackgroundHandler
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.inject.Inject
|
||||
|
||||
@SessionScope
|
||||
@ -47,7 +48,7 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor(
|
||||
|
||||
// sanity check to ensure that we don't end up with two concurrent runs
|
||||
// of sendOutgoingRoomKeyRequestsTimer
|
||||
private var sendOutgoingRoomKeyRequestsRunning: Boolean = false
|
||||
private val sendOutgoingRoomKeyRequestsRunning = AtomicBoolean(false)
|
||||
|
||||
/**
|
||||
* Called when the client is started. Sets background processes running.
|
||||
@ -101,7 +102,9 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor(
|
||||
* @param requestBody requestBody
|
||||
*/
|
||||
fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody) {
|
||||
cancelRoomKeyRequest(requestBody, false)
|
||||
BACKGROUND_HANDLER.post {
|
||||
cancelRoomKeyRequest(requestBody, false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -110,7 +113,9 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor(
|
||||
* @param requestBody requestBody
|
||||
*/
|
||||
fun resendRoomKeyRequest(requestBody: RoomKeyRequestBody) {
|
||||
cancelRoomKeyRequest(requestBody, true)
|
||||
BACKGROUND_HANDLER.post {
|
||||
cancelRoomKeyRequest(requestBody, true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -121,25 +126,31 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor(
|
||||
*/
|
||||
private fun cancelRoomKeyRequest(requestBody: RoomKeyRequestBody, andResend: Boolean) {
|
||||
val req = cryptoStore.getOutgoingRoomKeyRequest(requestBody)
|
||||
?: // no request was made for this key
|
||||
return
|
||||
?: // no request was made for this key
|
||||
return
|
||||
|
||||
Timber.v("cancelRoomKeyRequest: requestId: " + req.requestId + " state: " + req.state + " andResend: " + andResend)
|
||||
|
||||
if (req.state === OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING || req.state === OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND) {
|
||||
// nothing to do here
|
||||
} else if (req.state === OutgoingRoomKeyRequest.RequestState.UNSENT || req.state === OutgoingRoomKeyRequest.RequestState.FAILED) {
|
||||
Timber.v("## cancelRoomKeyRequest() : deleting unnecessary room key request for $requestBody")
|
||||
cryptoStore.deleteOutgoingRoomKeyRequest(req.requestId)
|
||||
} else if (req.state === OutgoingRoomKeyRequest.RequestState.SENT) {
|
||||
if (andResend) {
|
||||
req.state = OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND
|
||||
} else {
|
||||
req.state = OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING
|
||||
when (req.state) {
|
||||
OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING,
|
||||
OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND -> {
|
||||
// nothing to do here
|
||||
}
|
||||
OutgoingRoomKeyRequest.RequestState.UNSENT,
|
||||
OutgoingRoomKeyRequest.RequestState.FAILED -> {
|
||||
Timber.v("## cancelRoomKeyRequest() : deleting unnecessary room key request for $requestBody")
|
||||
cryptoStore.deleteOutgoingRoomKeyRequest(req.requestId)
|
||||
}
|
||||
OutgoingRoomKeyRequest.RequestState.SENT -> {
|
||||
if (andResend) {
|
||||
req.state = OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND
|
||||
} else {
|
||||
req.state = OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING
|
||||
}
|
||||
req.cancellationTxnId = makeTxnId()
|
||||
cryptoStore.updateOutgoingRoomKeyRequest(req)
|
||||
sendOutgoingRoomKeyRequestCancellation(req)
|
||||
}
|
||||
req.cancellationTxnId = makeTxnId()
|
||||
cryptoStore.updateOutgoingRoomKeyRequest(req)
|
||||
sendOutgoingRoomKeyRequestCancellation(req)
|
||||
}
|
||||
}
|
||||
|
||||
@ -148,16 +159,16 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor(
|
||||
* Start the background timer to send queued requests, if the timer isn't already running.
|
||||
*/
|
||||
private fun startTimer() {
|
||||
if (sendOutgoingRoomKeyRequestsRunning) {
|
||||
if (sendOutgoingRoomKeyRequestsRunning.get()) {
|
||||
return
|
||||
}
|
||||
Handler().postDelayed(Runnable {
|
||||
if (sendOutgoingRoomKeyRequestsRunning) {
|
||||
BACKGROUND_HANDLER.postDelayed(Runnable {
|
||||
if (sendOutgoingRoomKeyRequestsRunning.get()) {
|
||||
Timber.v("## startTimer() : RoomKeyRequestSend already in progress!")
|
||||
return@Runnable
|
||||
}
|
||||
|
||||
sendOutgoingRoomKeyRequestsRunning = true
|
||||
sendOutgoingRoomKeyRequestsRunning.set(true)
|
||||
sendOutgoingRoomKeyRequests()
|
||||
}, SEND_KEY_REQUESTS_DELAY_MS.toLong())
|
||||
}
|
||||
@ -167,19 +178,19 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor(
|
||||
// timer will be restarted before the promise resolves).
|
||||
private fun sendOutgoingRoomKeyRequests() {
|
||||
if (!isClientRunning) {
|
||||
sendOutgoingRoomKeyRequestsRunning = false
|
||||
sendOutgoingRoomKeyRequestsRunning.set(false)
|
||||
return
|
||||
}
|
||||
|
||||
Timber.v("## sendOutgoingRoomKeyRequests() : Looking for queued outgoing room key requests")
|
||||
val outgoingRoomKeyRequest = cryptoStore.getOutgoingRoomKeyRequestByState(
|
||||
HashSet<OutgoingRoomKeyRequest.RequestState>(Arrays.asList<OutgoingRoomKeyRequest.RequestState>(OutgoingRoomKeyRequest.RequestState.UNSENT,
|
||||
setOf(OutgoingRoomKeyRequest.RequestState.UNSENT,
|
||||
OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING,
|
||||
OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND)))
|
||||
OutgoingRoomKeyRequest.RequestState.CANCELLATION_PENDING_AND_WILL_RESEND))
|
||||
|
||||
if (null == outgoingRoomKeyRequest) {
|
||||
Timber.e("## sendOutgoingRoomKeyRequests() : No more outgoing room key requests")
|
||||
sendOutgoingRoomKeyRequestsRunning = false
|
||||
sendOutgoingRoomKeyRequestsRunning.set(false)
|
||||
return
|
||||
}
|
||||
|
||||
@ -213,7 +224,7 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor(
|
||||
cryptoStore.updateOutgoingRoomKeyRequest(request)
|
||||
}
|
||||
|
||||
sendOutgoingRoomKeyRequestsRunning = false
|
||||
sendOutgoingRoomKeyRequestsRunning.set(false)
|
||||
startTimer()
|
||||
}
|
||||
|
||||
@ -246,7 +257,7 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor(
|
||||
sendMessageToDevices(roomKeyShareCancellation, request.recipients, request.cancellationTxnId, object : MatrixCallback<Unit> {
|
||||
private fun onDone() {
|
||||
cryptoStore.deleteOutgoingRoomKeyRequest(request.requestId)
|
||||
sendOutgoingRoomKeyRequestsRunning = false
|
||||
sendOutgoingRoomKeyRequestsRunning.set(false)
|
||||
startTimer()
|
||||
}
|
||||
|
||||
@ -285,15 +296,22 @@ internal class OutgoingRoomKeyRequestManager @Inject constructor(
|
||||
val contentMap = MXUsersDevicesMap<Any>()
|
||||
|
||||
for (recipient in recipients) {
|
||||
contentMap.setObject(message, recipient["userId"], recipient["deviceId"]) // TODO Change this two hard coded key to something better
|
||||
// TODO Change this two hard coded key to something better
|
||||
contentMap.setObject(recipient["userId"], recipient["deviceId"], message)
|
||||
}
|
||||
|
||||
sendToDeviceTask.configureWith(SendToDeviceTask.Params(EventType.ROOM_KEY_REQUEST, contentMap, transactionId))
|
||||
.dispatchTo(callback)
|
||||
sendToDeviceTask
|
||||
.configureWith(SendToDeviceTask.Params(EventType.ROOM_KEY_REQUEST, contentMap, transactionId)) {
|
||||
this.callback = callback
|
||||
this.callbackThread = TaskThread.CALLER
|
||||
this.executionThread = TaskThread.CALLER
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SEND_KEY_REQUESTS_DELAY_MS = 500
|
||||
|
||||
private val BACKGROUND_HANDLER = createBackgroundHandler("OutgoingRoomKeyRequest")
|
||||
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user