1
0
mirror of https://github.com/vector-im/riotX-android synced 2025-10-06 00:02:48 +02:00

Compare commits

...

317 Commits

Author SHA1 Message Date
Benoit Marty
071db7a4d8 With icon from design team 2020-09-10 17:06:32 +02:00
Benoit Marty
2c747cc847 Show switch if collapsed or for invites 2020-09-10 14:41:54 +02:00
Benoit Marty
87322ce8b0 Rename ids 2020-09-10 14:11:58 +02:00
Benoit Marty
5286e6983c Experiment Grid view on Home 2020-09-09 19:44:45 +02:00
Benoit Marty
94e43475e2 Merge pull request #2040 from vector-im/feature/date_formatting
Feature/date formatting
2020-09-09 12:16:18 +02:00
Benoit Marty
b97d922808 ktlint 2020-09-09 11:33:22 +02:00
ganfra
18dcd6b9b1 Date format: add more comments and fix wrong format kind usage 2020-09-09 11:32:56 +02:00
ganfra
c6178e504f Clean files and update CHANGES 2020-09-09 11:32:56 +02:00
ganfra
0ff28c4f50 Date formatting: try to generalise usage of VectorDateFormatter and get proper formatting for Date + Time 2020-09-09 11:32:16 +02:00
ganfra
73ab32fd92 Start reworking date formatting 2020-09-09 11:32:16 +02:00
Benoit Marty
bf0b6d738a Version++ 2020-09-08 17:49:26 +02:00
Benoit Marty
83a06b9657 Merge branch 'release/1.0.6' into develop 2020-09-08 17:47:15 +02:00
Benoit Marty
9b5bd3e226 Prepare release 1.0.6 2020-09-08 17:47:03 +02:00
Benoit Marty
135fcab558 Merge pull request #2074 from vector-im/feature/cleanup
Cleanup and documentation after #2047
2020-09-08 17:39:02 +02:00
Benoit Marty
f990600aea Cleanup and documentation after #2047 2020-09-08 12:40:21 +02:00
Benoit Marty
22cd6ae239 Merge pull request #2047 from vector-im/feature/universal_link_navigation
Handle room, user, group, email verification links by converting them to permalinks.
2020-09-08 12:21:54 +02:00
Benoit Marty
00b53ee577 Merge pull request #2053 from vector-im/feature/fix_poll_reply
Feature/fix poll reply
2020-09-08 12:19:32 +02:00
Benoit Marty
f65e96e7b4 Merge pull request #2069 from etherealvisage/develop
Support data-mx-[bg-]color attribute on <font> tags in formatted messages.
2020-09-08 10:09:25 +02:00
Benoit Marty
f98844db02 Privacy 2020-09-08 10:04:27 +02:00
Benoit Marty
c59665e017 typo 2020-09-08 09:52:51 +02:00
Valere
7fe4148384 update change log 2020-09-08 09:51:55 +02:00
Valere
a544ae264b Avoid showing debug notice format for event preview 2020-09-08 09:51:35 +02:00
Valere
c895f87e26 Prevent reply to poll local echo (relation to echo not supported yet) 2020-09-08 09:51:35 +02:00
Onuray Sahin
e65558958d Code review refactoring. 2020-09-08 10:45:00 +03:00
Valere
eb5088c699 Fix / sending poll event appears forever 2020-09-08 09:32:37 +02:00
Benoit Marty
3755d866b1 Merge pull request #2067 from vector-im/feature/contact_crash
Android 6: App crash when read Contact permission is granted (#2064)
2020-09-08 08:46:27 +02:00
ethereal
8291dfc188 Support data-mx-color attribute in <font> tags as well. 2020-09-07 14:52:30 -04:00
Onuray Sahin
c41c91b0e7 Update changelog. 2020-09-07 20:53:11 +03:00
Onuray Sahin
3367ed6765 Fix support of riot.im/develop urls. 2020-09-07 20:02:06 +03:00
Benoit Marty
4654e39417 Update after Ganfra's review 2020-09-07 17:14:09 +02:00
Onuray Sahin
6dd4d4d906 Merge branch 'develop' into feature/universal_link_navigation 2020-09-07 17:48:52 +03:00
Onuray Sahin
cf3fecd425 Changelog added.
Fixes #1795
2020-09-07 17:48:12 +03:00
Benoit Marty
be9a91e3fe Android 6: App crash when read Contact permission is granted (#2064) 2020-09-07 16:33:12 +02:00
Onuray Sahin
fc51097ed8 develop and staging subdomains are added. 2020-09-07 17:17:58 +03:00
Benoit Marty
751c870a4a Merge pull request #2049 from vector-im/feature/image_compression
Image compression
2020-09-07 15:21:26 +02:00
Benoit Marty
de8e325193 Merge branch 'develop' into feature/image_compression 2020-09-07 15:21:15 +02:00
Benoit Marty
e2feac3dde Merge pull request #2056 from vector-im/feature/small_doc_change
Feature/small doc change
2020-09-07 15:19:55 +02:00
Benoit Marty
d7b0b2f785 Merge pull request #2072 from vector-im/feature/weblate_fixes
Weblate fixes and other stuff done during locked Weblate
2020-09-07 15:00:04 +02:00
Benoit Marty
326c863148 Emoji SAS: Step 4: changelog (#1909) 2020-09-07 14:30:21 +02:00
Benoit Marty
c42920d9aa Emoji SAS: Step 3: Make the code compile (#1909) 2020-09-07 14:28:43 +02:00
Benoit Marty
fb5c1bb163 Emoji SAS: Step 2: Run import_sas_strings.py (#1909) 2020-09-07 14:21:10 +02:00
Benoit Marty
4f695a6d8f Emoji SAS: Step 1: Remove legacy strings (#1909) 2020-09-07 14:19:55 +02:00
Benoit Marty
bc389d371d Move string def to app module instead of SDK (#1994) 2020-09-07 13:38:12 +02:00
Benoit Marty
3756c3a191 Fix lint issues 2020-09-07 13:32:32 +02:00
Benoit Marty
9e3caf603d Lint: "..." -> 2020-09-07 13:25:11 +02:00
Benoit Marty
74e2ffc4c4 Format string files 2020-09-07 13:24:33 +02:00
Benoit Marty
6e019dbd44 Merge pull request #2071 from RiotTranslateBot/weblate-element-android-element-app
Update from Weblate
2020-09-07 13:13:13 +02:00
Onuray Sahin
4d7cd7319b Handle new mobile config url. 2020-09-07 12:19:34 +03:00
Weblate
edf7761d49 Merge branch 'origin/develop' into Weblate. 2020-09-07 08:57:45 +00:00
LinAGKar
a590bc96cd Translated using Weblate (Swedish)
Currently translated at 100.0% (235 of 235 strings)

Translation: Element Android/Element Android Sdk
Translate-URL: https://translate.riot.im/projects/element-android/element-sdk/sv/
2020-09-07 08:57:34 +00:00
Benoit Marty
6c6bba68ff Merge pull request #2062 from vector-im/feature/fix_no_verif
Feature/fix no verif
2020-09-07 10:49:38 +02:00
ethereal
5fbcf348f5 Support data-mx-bg-color attribute on <font> tags in formatted messages.
Signed-off-by: Kestrel Williams-King <ethereal@ethv.net>
2020-09-07 01:13:32 -04:00
LinAGKar
58f5baa5f6 Added translation using Weblate (Swedish) 2020-09-06 06:49:13 +00:00
PPNplus
398dcb1036 Translated using Weblate (Thai)
Currently translated at 66.7% (2 of 3 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.riot.im/projects/element-android/element-store/th/
2020-09-05 09:42:47 +00:00
PPNplus
30e7b761ae Translated using Weblate (Thai)
Currently translated at 4.7% (87 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/th/
2020-09-05 09:42:47 +00:00
Eduardo F
4d105c278b Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (1849 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/pt_BR/
2020-09-05 09:42:44 +00:00
Benoit Marty
f019e4a246 Merge pull request #1995 from notramo/patch-1
Mention thousands of public servers in Play Store
2020-09-04 21:35:10 +02:00
Benoit Marty
4499e34f44 Merge pull request #2030 from vector-im/feature/fix_call_proximity_sensor
Turn the screen off while using ear speaker
2020-09-04 21:25:59 +02:00
Benoit Marty
fd1bb84770 typo 2020-09-04 20:47:19 +02:00
Benoit Marty
ac4c111ad3 Refacto: let's matrixItem be part of the constructor 2020-09-04 20:42:35 +02:00
Benoit Marty
254eb26211 Verification popup won't show 2020-09-04 20:38:32 +02:00
Benoit Marty
f689871fc0 Code symmetry 2020-09-04 18:45:49 +02:00
Benoit Marty
8ead371603 Fix crash with WakeLock 2020-09-04 18:43:59 +02:00
Benoit Marty
8045d61e1f Code cleanup 2020-09-04 18:41:13 +02:00
Onuray Sahin
e790c35270 Use stringProvider. 2020-09-04 17:19:54 +02:00
Onuray Sahin
914ec895ee Use ContextCompat.getSystemService function. 2020-09-04 17:19:54 +02:00
Onuray Sahin
bd05484b2d Initialize fields in the constructor. 2020-09-04 17:19:54 +02:00
Onuray Sahin
7872838056 Changelog added. 2020-09-04 17:19:54 +02:00
Onuray Sahin
b89b3db077 Use proximity manager to turn the screen off while the ear speaker is used. 2020-09-04 17:19:18 +02:00
Onuray Sahin
8e3d83579b Remove attachBaseContext to fix the crash. 2020-09-04 17:19:18 +02:00
Onuray Sahin
96845d31db Proximity sensor implementation. 2020-09-04 17:19:18 +02:00
Benoit Marty
89fa2ece43 Fix compilation issue (I'm tired) 2020-09-04 15:46:16 +02:00
Benoit Marty
82e3adebbc Improve wording and fix typo 2020-09-04 14:23:56 +02:00
Benoit Marty
2490d4d638 Generic email-provider hots. 2020-09-04 14:18:47 +02:00
Benoit Marty
55f04906ac Correct markdown formatting 2020-09-04 14:17:15 +02:00
Onuray Sahin
f186a00515 Support legacy riot.im url. 2020-09-04 14:20:03 +03:00
Benoit Marty
0bd7e40a22 Rework: split long method, extract thumbnail management 2020-09-04 12:33:10 +02:00
Benoit Marty
ad984b26fb Move variable declaration 2020-09-04 12:22:38 +02:00
Benoit Marty
3a659a9f3b Ensure temporary files are deleted even in case of Exception 2020-09-04 12:03:20 +02:00
Benoit Marty
0217e79324 Ensure input stream is closed 2020-09-04 11:08:44 +02:00
Benoit Marty
544bff9f4f For only with Files now 2020-09-04 11:07:06 +02:00
PPNplus
a4fdf1802b Translated using Weblate (Thai)
Currently translated at 1.5% (28 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/th/
2020-09-04 08:43:08 +00:00
LinAGKar
2c4f7d38a2 Translated using Weblate (Swedish)
Currently translated at 100.0% (1849 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/sv/
2020-09-04 08:43:07 +00:00
Peter Vágner
48a9e1ff9f Translated using Weblate (Slovak)
Currently translated at 70.0% (1294 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/sk/
2020-09-04 08:43:06 +00:00
rkfg
c4b6d52657 Translated using Weblate (Russian)
Currently translated at 100.0% (1849 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/ru/
2020-09-04 08:42:43 +00:00
call_xz
43b291d2d0 Translated using Weblate (Japanese)
Currently translated at 49.9% (923 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/ja/
2020-09-04 08:42:43 +00:00
Benoit Marty
b31178683c typo 2020-09-04 09:37:49 +02:00
Benoit Marty
38631eb70e Format 2020-09-04 09:37:10 +02:00
Benoit Marty
6246fd98c3 Cleanup 2020-09-04 09:36:18 +02:00
Benoit Marty
71aa315f2a Cleanup 2020-09-04 09:36:18 +02:00
Benoit Marty
af6a94d08e Support for image compression on Android 10 2020-09-04 09:36:18 +02:00
Valere
f6c7f3eed1 Merge pull request #1889 from vector-im/feature/enhance_big_files
Feature/enhance big files
2020-09-03 17:09:55 +02:00
Valere
e0c5377968 Merge branch 'develop' into feature/enhance_big_files 2020-09-03 17:09:40 +02:00
Valere
0c39495e3f FIx / unneeded code 2020-09-03 15:03:07 +02:00
Valere
7c638798c7 Code review 2020-09-03 14:53:13 +02:00
Benoit Marty
05ec5bde93 Merge pull request #2020 from vector-im/feature/add_email
Add email and phone numbers
2020-09-03 14:33:09 +02:00
Benoit Marty
d2d372d140 Merge pull request #2043 from vector-im/feature/cleanup_after_2002
Cleanup after #2002
2020-09-03 11:15:45 +02:00
Benoit Marty
e9b3ab91a0 Merge pull request #1902 from vector-im/feature/verify_one_session
Feature/verify one session
2020-09-03 11:11:22 +02:00
Benoit Marty
633b12f66d Propose to verify the current session if the 4S contains secret, even if there is no other sessions 2020-09-03 10:00:01 +02:00
Benoit Marty
2efe5a420c Do not propose to verify the current session if there is only one session (#1901) 2020-09-03 09:54:03 +02:00
Benoit Marty
96c7f57ea0 Remove useless member 2020-09-03 09:52:57 +02:00
Benoit Marty
6af879fe2a Rename class 2020-09-03 09:52:14 +02:00
Benoit Marty
b935a6557f Move state to a dedicated file 2020-09-03 09:46:17 +02:00
Valere
8340d5e71f Fix tests 2020-09-03 09:38:40 +02:00
Valere
8103081e0e Fix / Support open and view of sending attachment 2020-09-03 09:31:53 +02:00
Benoit Marty
be3157b6f5 Do not strip new lines. Many tests are not passing, we should work more on it... 2020-09-03 09:24:44 +02:00
Benoit Marty
ae9afcc393 Add test to cover line break 2020-09-03 09:16:37 +02:00
Benoit Marty
e73480c0ef Riot -> Element 2020-09-03 09:06:12 +02:00
Benoit Marty
e02b9b7736 Changelog 2020-09-02 19:02:36 +02:00
Benoit Marty
8c801ae078 API change: encrypted files are now decrypted internally, no need to expose decryptStream() anymore 2020-09-02 19:01:26 +02:00
Onuray Sahin
a151f42495 Handle room, user, group links by converting them to permalinks. 2020-09-02 18:26:08 +03:00
Valere
40f7dc4824 Merge remote-tracking branch 'origin/feature/enhance_big_files' into feature/enhance_big_files 2020-09-02 16:42:54 +02:00
Valere
3b8c61a87e FIx / interceptors and stream closed 2020-09-02 16:06:48 +02:00
Benoit Marty
4ef1f9c4d9 Avoid copy paste of code 2020-09-02 12:47:41 +02:00
Benoit Marty
93cb6bd26e Avoid null type 2020-09-02 12:37:06 +02:00
Benoit Marty
7c33bf2742 Remove Done TODO 2020-09-02 12:31:22 +02:00
Benoit Marty
e5e67fbcbb Internal class and Copyright 2020-09-02 12:29:53 +02:00
Benoit Marty
95219c7934 typo 2020-09-02 12:29:39 +02:00
Benoit Marty
53744982f0 Update Javadoc 2020-09-02 12:29:18 +02:00
Benoit Marty
6d24aa75d0 Format file (no other change) 2020-09-02 12:25:50 +02:00
Benoit Marty
76c79f9f75 Move Base64 methods to a dedicated file 2020-09-02 12:06:21 +02:00
Benoit Marty
28081aa7d2 Cleanup: rename parameters, make some fields private, add Javadoc, fix copy paste error 2020-09-02 12:03:03 +02:00
Benoit Marty
d0532bb9a9 Revert some change from #2002 2020-09-02 09:06:34 +02:00
Benoit Marty
9389cfe7a3 Add comment for history 2020-09-02 09:04:16 +02:00
Benoit Marty
484fd61706 Make MarkdownParserTest tests pass again 2020-09-02 09:04:16 +02:00
Benoit Marty
6c943571fc Remove useless TextContentRenderer 2020-09-01 22:22:27 +02:00
Leon Schmidt
29123ac726 Bugfix/markdown parsing (#2002)
Removed replacing of newlines in already parsed HTML documents

Signed-off-by: Leon Schmidt <mail@leon.wtf>

Co-authored-by: Benoit Marty <benoitm@matrix.org>
2020-09-01 22:02:16 +02:00
Benoit Marty
44cb8cdeca Merge pull request #2042 from vector-im/feature/builkite_location_and_pipeline_location_change
Update of Buildkite configuration
2020-09-01 21:53:11 +02:00
Benoit Marty
9e20f7db1a Update of Buildkite configuration:
New pipeline location: https://github.com/matrix-org/pipelines/blob/master/element-android/pipeline.yml
New build location: https://buildkite.com/matrix-dot-org/element-android
2020-09-01 18:52:03 +02:00
Benoit Marty
971320a56c Restore setting removed by mistake 2020-09-01 18:38:38 +02:00
Benoit Marty
4290d22465 Merge pull request #2035 from vector-im/feature/fix_crashes_attachment_viewer
Crash / Attachment viewer: Cannot draw a recycled Bitmap
2020-09-01 18:33:39 +02:00
Benoit Marty
96a3b25adb Merge branch 'develop' into feature/fix_crashes_attachment_viewer 2020-09-01 18:32:55 +02:00
Valere
3dc69b60c4 Fix / Log could leak file name 2020-09-01 17:31:54 +02:00
Benoit Marty
bdde638967 Merge pull request #2028 from vector-im/feature/relax_is_discovery
Relax IS dicovery
2020-09-01 15:39:28 +02:00
Benoit Marty
8157644ff8 Merge pull request #2014 from vector-im/feature/fix_call_sound_device
Fix: Loudspeaker is always used
2020-09-01 15:33:06 +02:00
ziriSut
4008339963 Translated using Weblate (Kabyle)
Currently translated at 56.7% (1049 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/kab/
2020-09-01 13:08:35 +00:00
Benoit Marty
c6bd6c17fc Auto review 2020-09-01 14:55:12 +02:00
Benoit Marty
1a452a6cd7 Improve error for non supported SSO flow to delete device or add MSISDN 2020-09-01 12:58:53 +02:00
Benoit Marty
35f854de5d Also request focus (keyboard is not shown on my device :/) 2020-09-01 10:45:48 +02:00
Benoit Marty
99343294c6 Rename class 2020-09-01 10:09:28 +02:00
Benoit Marty
56261bd741 Add a trick to reset EditText field when recycled. 2020-09-01 10:07:04 +02:00
Benoit Marty
1d53b48c8a Inline wrong token error 2020-08-31 23:11:58 +02:00
Benoit Marty
5594489b69 Optimisation and cleanup 2020-08-31 22:20:59 +02:00
Benoit Marty
ddb858380e Code quality 2020-08-31 21:46:55 +02:00
Benoit Marty
bf5c1e9d8f Add phone numbers to account 2020-08-31 20:53:37 +02:00
Benoit Marty
931eeac548 Add MSISDN (WIP) 2020-08-31 18:07:25 +02:00
Valere
f34c5d6674 Crash / Attachment viewer: Cannot draw a recycled Bitmap 2020-08-31 17:50:17 +02:00
Benoit Marty
9c05efa15d Fix migration issue. Also do not delete Realm file in case of migration issue when debugging. 2020-08-31 16:46:56 +02:00
Benoit Marty
bd12c89a3c Move the DB version number to the Migration class, I think it has more sense. Also increase the value. 2020-08-31 16:29:34 +02:00
Valere
bf4f869524 rebase fix 2020-08-31 16:25:40 +02:00
Valere
dd09c4a72d post rebase fix 2020-08-31 16:21:27 +02:00
Valere
55dcba6f36 Fix progress of message with attachment 2020-08-31 16:21:27 +02:00
Valere
4e7790966f Always use temp file before sending 2020-08-31 16:21:27 +02:00
Valere
bfcbb9ff4f Rebase post matrix sdk package renaming 2020-08-31 16:21:27 +02:00
Benoit Marty
cc57a73f23 Cleanup and split long lines 2020-08-31 16:20:12 +02:00
Valere
d6f96e3d64 Fix test + cleaning 2020-08-31 16:20:12 +02:00
Valere
5f76f182f6 Fix clear glide in recycler view 2020-08-31 16:20:12 +02:00
Valere
31eccf5f1c Cleaning 2020-08-31 16:20:12 +02:00
Valere
9d2ea19d7d Upgrade to worker 2.4.0 2020-08-31 16:20:12 +02:00
Valere
a888e1e80e Support cancel sending and resend event with attachments
Avoid auto retry for medium and big files
2020-08-31 16:20:12 +02:00
Valere
8b8855d2d5 FIx / Audio icon not shown after download 2020-08-31 16:20:12 +02:00
Valere
de53166193 Use file service in glide loader (avoid re-dl after send) 2020-08-31 16:20:12 +02:00
Valere
caf0ac1c9f Add event sending indicator for attachment 2020-08-31 16:20:12 +02:00
Valere
299cd9ced3 Fix / preview/edit was shown for movies and gif 2020-08-31 16:20:12 +02:00
Valere
11c8da3717 Improve upload/dl mem for big files + report ecryption progress 2020-08-31 16:20:12 +02:00
Benoit Marty
e309b30203 Implement: Adding MSISDN (WIP) 2020-08-31 16:16:18 +02:00
Benoit Marty
5a21249022 Doc: Adding MSISDN 2020-08-31 15:08:59 +02:00
Benoit Marty
3c3c51e6fd Doc: more generic to handle MSISDN 2020-08-31 14:48:48 +02:00
Benoit Marty
ee3e10a4b0 Add threePid: improve UI for phone numbers 2020-08-31 14:46:05 +02:00
Benoit Marty
e92cf38cde Add threePid: improve UX (remove dialog) 2020-08-31 14:30:47 +02:00
Benoit Marty
dbd080ca6c Merge pull request #2018 from vector-im/feature/moar_sonar_fixes
Feature/moar sonar fixes
2020-08-31 12:10:00 +02:00
Valere
aedcf3006a Relax IS dicovery
fixes #2027
2020-08-31 12:08:37 +02:00
LinAGKar
6a475ae85c Translated using Weblate (Swedish)
Currently translated at 100.0% (1849 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/sv/
2020-08-30 07:42:56 +00:00
ziriSut
0ad637fa16 Translated using Weblate (Kabyle)
Currently translated at 100.0% (235 of 235 strings)

Translation: Element Android/Element Android Sdk
Translate-URL: https://translate.riot.im/projects/element-android/element-sdk/kab/
2020-08-30 07:42:56 +00:00
ziriSut
ee98a2da03 Translated using Weblate (Kabyle)
Currently translated at 54.1% (1001 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/kab/
2020-08-30 07:42:55 +00:00
Balázs Meskó
183d928e21 Translated using Weblate (Hungarian)
Currently translated at 100.0% (1849 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/hu/
2020-08-30 07:42:43 +00:00
Rww Folo
807c6f0cb5 Added translation using Weblate (Bengali (Bangladesh)) 2020-08-29 15:53:08 +00:00
Kahina Messaoudi
72bb140b70 Translated using Weblate (Kabyle)
Currently translated at 41.3% (763 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/kab/
2020-08-29 13:46:02 +00:00
ziriSut
a5678a752b Translated using Weblate (Kabyle)
Currently translated at 41.3% (763 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/kab/
2020-08-29 13:46:02 +00:00
Benoit Marty
58938a239e Format phone number 2020-08-28 19:07:41 +02:00
Benoit Marty
da4695ff2a Improve UI of threePid screen 2020-08-28 18:35:04 +02:00
Benoit Marty
9fecc1b992 Reduce button width 2020-08-28 18:05:59 +02:00
Benoit Marty
e8e1330cd5 Cleanup 2020-08-28 18:05:07 +02:00
Benoit Marty
1af45ede62 Add some guard, and allow to cancel adding 3pid 2020-08-28 17:55:38 +02:00
Benoit Marty
f6f9373aeb Cleanup 2020-08-28 17:06:17 +02:00
Benoit Marty
36a9b80040 i18n for M_THREEPID_AUTH_FAILED 2020-08-28 17:05:40 +02:00
Benoit Marty
175a5ab824 List phone numbers and emails added to the Matrix account, and add Email to account (#44) 2020-08-28 16:45:09 +02:00
Benoit Marty
46d3608ccb RiotX -> Element 2020-08-28 11:25:16 +02:00
Benoit Marty
b11eced4f1 Fix a refresh problem on the user avatar 2020-08-28 11:12:29 +02:00
Benoit Marty
66dfcbc2f8 Add documentation on the flows 2020-08-28 10:59:51 +02:00
Benoit Marty
71de2d9adc Update README.md 2020-08-28 09:23:55 +02:00
Benoit Marty
3f30636808 Matrix SDK is now exported 2020-08-28 09:23:08 +02:00
Benoit Marty
ee9c73fde1 Sonar: rename variables and parameter to follow naming convention 2020-08-28 08:58:57 +02:00
Onuray Sahin
7d76264b25 Merge branch 'develop' into feature/fix_call_sound_device 2020-08-28 02:53:46 +03:00
Marcelo Filho
22771a84f1 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (235 of 235 strings)

Translation: Element Android/Element Android Sdk
Translate-URL: https://translate.riot.im/projects/element-android/element-sdk/pt_BR/
2020-08-27 22:42:34 +00:00
Marcelo Filho
a107bdd849 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (1849 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/pt_BR/
2020-08-27 22:42:34 +00:00
random
9fbf97f4cb Translated using Weblate (Italian)
Currently translated at 100.0% (1849 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/it/
2020-08-27 22:42:34 +00:00
Tirifto
812563f68b Translated using Weblate (Esperanto)
Currently translated at 25.5% (471 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/eo/
2020-08-27 22:42:33 +00:00
Benoit Marty
d3f50ee6c3 Fix a crash that would occur if it was not dead code 2020-08-27 22:46:30 +02:00
Benoit Marty
9dbe9c7286 Typo in comment 2020-08-27 22:44:55 +02:00
Benoit Marty
fbcc7aa211 Merge pull request #1993 from vector-im/feature/fix_call_in_background
Don't pause the sync thread if there is an active or pending call.
2020-08-27 22:41:46 +02:00
Benoit Marty
a0af769d7c Merge pull request #2016 from vector-im/feature/sonar_fixes
Sonar fixes
2020-08-27 21:39:56 +02:00
Benoit Marty
ef4f930ba2 Don't pause the sync thread if there is an active or pending call.
But pause the sync thread when there is no active call and the app is the background.
Authors: Onuray, I just rebased and squashed all the commit
2020-08-27 18:20:06 +02:00
Benoit Marty
8369003bdf Sonar: rename project name and project key 2020-08-27 18:16:29 +02:00
Benoit Marty
02145eaa06 Github repository has been renamed 2020-08-27 18:16:29 +02:00
Benoit Marty
a857d8e306 Fix bugs detected by sonar 2020-08-27 18:16:29 +02:00
Benoit Marty
cb64f472fe Remove commented out code and useless code 2020-08-27 18:15:53 +02:00
Benoit Marty
cd28ad4c07 Remove dead code (QrCode animation) 2020-08-27 18:15:53 +02:00
Benoit Marty
b69616117f Sonar: skip project diff-match-patch 2020-08-27 18:15:53 +02:00
Benoit Marty
59fa2e28c2 Fix sonar analysis version name issue 2020-08-27 18:15:53 +02:00
Benoit Marty
0f1e348ac4 Merge pull request #2010 from vector-im/feature/fix_send_room_v6
Feature/fix send room v6
2020-08-27 18:14:58 +02:00
Benoit Marty
67bde947f9 Merge pull request #2009 from vector-im/feature/joining_room
Feature/joining room
2020-08-27 18:11:42 +02:00
Onuray Sahin
d9009540dd Don't try to use bluetooth headset if it is not connected. 2020-08-27 16:43:11 +03:00
ganfra
28a1cf6982 Join room: clean up after benoit's remarks 2020-08-27 11:44:21 +02:00
ganfra
9c1c9f96e1 Room v6: finish cleaning up 2020-08-27 10:34:40 +02:00
aevw
7657c0f905 Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.9% (1848 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/pt_BR/
2020-08-27 00:29:21 +00:00
Marcelo Filho
d8a0142aee Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.9% (1848 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/pt_BR/
2020-08-27 00:29:20 +00:00
strix aluco
eddc681d64 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (3 of 3 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.riot.im/projects/element-android/element-store/uk/
2020-08-26 20:42:44 +00:00
vejetaryenvampir
87a97bc8ec Translated using Weblate (Turkish)
Currently translated at 64.4% (1191 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/tr/
2020-08-26 20:42:44 +00:00
LinAGKar
ae81ea2bbc Translated using Weblate (Swedish)
Currently translated at 100.0% (1849 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/sv/
2020-08-26 20:42:44 +00:00
Marcelo Filho
90e580245a Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (235 of 235 strings)

Translation: Element Android/Element Android Sdk
Translate-URL: https://translate.riot.im/projects/element-android/element-sdk/pt_BR/
2020-08-26 20:42:44 +00:00
aevw
8372c9e3ed Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.7% (1843 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/pt_BR/
2020-08-26 20:42:43 +00:00
Marcelo Filho
1fcb8d712b Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.7% (1843 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/pt_BR/
2020-08-26 20:42:42 +00:00
Imre Kristoffer Eilertsen
9761566b28 Translated using Weblate (Norwegian Bokmål)
Currently translated at 44.1% (815 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/nb_NO/
2020-08-26 20:42:41 +00:00
Kahina Messaoudi
f26c58fa22 Translated using Weblate (Kabyle)
Currently translated at 100.0% (3 of 3 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.riot.im/projects/element-android/element-store/kab/
2020-08-26 20:42:31 +00:00
Tirifto
6d73f7b767 Translated using Weblate (Esperanto)
Currently translated at 25.1% (465 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/eo/
2020-08-26 20:42:31 +00:00
ganfra
3c0177c2dd Update CHANGES 2020-08-26 19:25:20 +02:00
ganfra
dc4135b506 Remove unnecessary code now we have an other way to keep number types 2020-08-26 19:21:41 +02:00
ganfra
cefdbe1d08 Add CheckNumberType in json to fix sending in room v6 2020-08-26 19:21:14 +02:00
Benoit Marty
95e80f0263 Merge pull request #2001 from vector-im/feature/mention_display_name
Fix mention display name
2020-08-26 17:35:23 +02:00
Benoit Marty
013f51f0c4 Merge branch 'develop' into feature/mention_display_name 2020-08-26 17:35:16 +02:00
Benoit Marty
75911ba4c0 Merge pull request #2008 from vector-im/feature/sas_emoji
Create a script to import SAS strings (#1909)
2020-08-26 17:34:43 +02:00
Benoit Marty
b3edc439cf Merge pull request #1997 from vector-im/feature/switch_language
Switch language and font size settings fixes
2020-08-26 17:34:26 +02:00
Benoit Marty
7ef08579ef Merge branch 'develop' into feature/switch_language 2020-08-26 17:34:17 +02:00
ganfra
b39a86edbd Merge branch 'develop' into feature/joining_room 2020-08-26 17:06:31 +02:00
ganfra
9c151417a8 Update CHANGES 2020-08-26 17:03:51 +02:00
ganfra
5d8d9cb19c Clean files 2020-08-26 17:02:24 +02:00
ganfra
b986bfd509 Permalink: handle via parameters 2020-08-26 16:37:48 +02:00
Benoit Marty
28ae10a4aa Merge pull request #2007 from vector-im/feature/icons
Feature/icons
2020-08-26 16:04:36 +02:00
Benoit Marty
39fa7d850a Merge branch 'develop' into feature/icons 2020-08-26 16:04:27 +02:00
Benoit Marty
25ea221d47 Create a script to import SAS strings (#1909)
It will be run at the next Weblate sync, since we have to remove previous translations
2020-08-26 16:02:58 +02:00
Benoit Marty
a4e495edd3 Merge pull request #2006 from vector-im/feature/long_click
Add long click gesture to copy userId, user display name, room name, …
2020-08-26 13:23:31 +02:00
Benoit Marty
8b009304ad Merge pull request #2004 from vector-im/feature/fix_ucrop
Increase version of uCrop library which supports Android 10.
2020-08-26 13:22:56 +02:00
Benoit Marty
249f268fb5 Fix bad color for settings icon on Android < 24 (#1786) 2020-08-26 12:55:49 +02:00
Benoit Marty
9f3f981ab0 Use cached Views 2020-08-26 12:54:01 +02:00
Benoit Marty
ef912e066b Remove unused resource 2020-08-26 12:29:46 +02:00
Benoit Marty
d014872e3b Add long click gesture to copy userId, user display name, room name, room topic and room alias (#1774) 2020-08-26 11:57:27 +02:00
Onuray Sahin
776d892d27 Increase version of uCrop library which supports Android 10. 2020-08-26 12:27:12 +03:00
ganfra
9970398cf2 Join room: add "UnknownItem" to public room list 2020-08-25 20:20:14 +02:00
Benoit Marty
a5f537f251 Merge pull request #1996 from johnjohndoe/file-extensions
Use File extension functions to make code more concise.
2020-08-25 18:09:00 +02:00
Benoit Marty
6376ba2516 Improve wording (visible in debug screen) 2020-08-25 17:48:12 +02:00
Benoit Marty
5e0fc3e17f Rename class 2020-08-25 17:34:43 +02:00
Benoit Marty
1a068ee191 Add more tests 2020-08-25 17:34:02 +02:00
Benoit Marty
8226f60fad Refactor test to avoid duplicated code 2020-08-25 17:31:51 +02:00
Benoit Marty
2e618999d9 Words containing my name should not trigger notifications (Fixes #1781)
It adds a specific behavior for rule with id RuleIds.RULE_ID_CONTAIN_USER_NAME
2020-08-25 17:26:54 +02:00
Benoit Marty
bf7a096a18 Refactor: Remove duplicated declaration for Actions 2020-08-25 17:01:55 +02:00
Benoit Marty
0d9885a1e0 Refactor: Remove duplicated declaration for RuleIds 2020-08-25 16:53:12 +02:00
Benoit Marty
aca8fd7f3d Refactor: Kotlin style 2020-08-25 16:18:44 +02:00
Benoit Marty
f5ea4fb6ac Refactor: Extract method to dedicated files 2020-08-25 16:15:42 +02:00
Benoit Marty
7b5c74f81c Refactor: Condition is now an interface 2020-08-25 16:14:14 +02:00
Benoit Marty
558e11a364 Refactor: move Kind to its own file 2020-08-25 16:01:34 +02:00
Weblate
3d5b3c65de Merge branch 'origin/develop' into Weblate. 2020-08-25 11:15:37 +00:00
LinAGKar
ed9e590498 Translated using Weblate (Swedish)
Currently translated at 100.0% (1849 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/sv/
2020-08-25 11:15:37 +00:00
tctovsli
70fda1009b Translated using Weblate (Norwegian Bokmål)
Currently translated at 33.1% (612 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/nb_NO/
2020-08-25 11:15:36 +00:00
Kahina Messaoudi
ceae1f1ad8 Translated using Weblate (Kabyle)
Currently translated at 25.2% (466 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/kab/
2020-08-25 11:15:33 +00:00
ganfra
8f1eff8782 Start reusing RoomPreview for joining room not joined 2020-08-25 12:35:35 +02:00
ganfra
c1c8c04567 Room detail: Fix roomId not set up when activity is restored 2020-08-25 12:31:55 +02:00
LinAGKar
31b4785a4d Translated using Weblate (Swedish)
Currently translated at 100.0% (1849 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/sv/
2020-08-24 17:43:19 +00:00
Kahina Messaoudi
8e5656729b Translated using Weblate (Kabyle)
Currently translated at 33.3% (1 of 3 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.riot.im/projects/element-android/element-store/kab/
2020-08-24 17:42:31 +00:00
ziriSut
51893fff09 Translated using Weblate (Kabyle)
Currently translated at 99.1% (233 of 235 strings)

Translation: Element Android/Element Android Sdk
Translate-URL: https://translate.riot.im/projects/element-android/element-sdk/kab/
2020-08-24 17:42:31 +00:00
Kahina Messaoudi
25d8ba2699 Translated using Weblate (Kabyle)
Currently translated at 99.1% (233 of 235 strings)

Translation: Element Android/Element Android Sdk
Translate-URL: https://translate.riot.im/projects/element-android/element-sdk/kab/
2020-08-24 17:42:31 +00:00
Kahina Messaoudi
fd5e4b78da Translated using Weblate (Kabyle)
Currently translated at 24.6% (454 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/kab/
2020-08-24 17:42:31 +00:00
@a2sc:matrix.org
f45d0122b0 Translated using Weblate (German)
Currently translated at 100.0% (1849 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/de/
2020-08-24 17:42:30 +00:00
aWeinzierl
01cfa4496c Translated using Weblate (German)
Currently translated at 100.0% (1849 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/de/
2020-08-24 17:42:30 +00:00
linsui
82c8b532c0 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (1849 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/zh_Hans/
2020-08-24 17:42:30 +00:00
Slavi Pantaleev
ee7f79b40c Translated using Weblate (Bulgarian)
Currently translated at 72.3% (170 of 235 strings)

Translation: Element Android/Element Android Sdk
Translate-URL: https://translate.riot.im/projects/element-android/element-sdk/bg/
2020-08-24 17:42:28 +00:00
Benoit Marty
cb7a1bc9c3 Fix FontSize issue (#1483, #1787) 2020-08-24 18:32:08 +02:00
Benoit Marty
52cf4d24d3 Fix switch language issue 2020-08-24 18:32:08 +02:00
Tobias Preuss
ed98613b2d Use File extension functions to make code more concise.
+ This change replaces a few usages of ByteArrayInputStream, FileInputStream,
  FileOutputStream with their equivalent Kotlin extension functions.
2020-08-24 18:01:30 +02:00
notramo
252df0f7a9 Mention thousands of public servers in Play Store 2020-08-24 14:51:10 +00:00
Benoit Marty
116e6fb3c0 Call restart() extension 2020-08-24 15:50:47 +02:00
Benoit Marty
efa3aa5cf8 Stop using internal context of VectorLocal in CallHeadsUpActionReceiver... 2020-08-24 15:08:52 +02:00
Benoit Marty
69e9a79ac1 Inject StringProvider instead of Context 2020-08-24 14:49:42 +02:00
Benoit Marty
96cf5d2105 Cleanup: propoerly inject things to PushRulesFragment and move PushRulesController to its own file 2020-08-24 14:32:43 +02:00
Benoit Marty
f745e22a52 Merge pull request #1983 from vector-im/feature/live_display_name
Make user displayName live in the setting, it should fix #1926
2020-08-24 13:43:08 +02:00
Benoit Marty
50495ef604 Improve algo 2020-08-24 11:12:19 +02:00
Benoit Marty
84854d9382 Add entry in changelog 2020-08-24 10:14:00 +02:00
Benoit Marty
328dc9ea5b Merge pull request #1979 from vector-im/feature/update_dependencies
Update some dependencies, including coroutines for working debugging
2020-08-24 10:11:57 +02:00
Kahina Messaoudi
30b92efcc6 Translated using Weblate (Kabyle)
Currently translated at 24.4% (452 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/kab/
2020-08-23 17:15:04 +00:00
ziriSut
e87bd16b6b Translated using Weblate (Kabyle)
Currently translated at 24.4% (452 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/kab/
2020-08-23 17:15:02 +00:00
Kahina Messaoudi
8b41b014ee Translated using Weblate (Kabyle)
Currently translated at 96.6% (227 of 235 strings)

Translation: Element Android/Element Android Sdk
Translate-URL: https://translate.riot.im/projects/element-android/element-sdk/kab/
2020-08-23 15:46:17 +00:00
ziriSut
601cf10fb4 Translated using Weblate (Kabyle)
Currently translated at 96.6% (227 of 235 strings)

Translation: Element Android/Element Android Sdk
Translate-URL: https://translate.riot.im/projects/element-android/element-sdk/kab/
2020-08-23 15:46:16 +00:00
Kahina Messaoudi
f02ee2af27 Translated using Weblate (Kabyle)
Currently translated at 72.3% (170 of 235 strings)

Translation: Element Android/Element Android Sdk
Translate-URL: https://translate.riot.im/projects/element-android/element-sdk/kab/
2020-08-23 14:38:55 +00:00
ziriSut
c490d6bcd1 Translated using Weblate (Kabyle)
Currently translated at 72.3% (170 of 235 strings)

Translation: Element Android/Element Android Sdk
Translate-URL: https://translate.riot.im/projects/element-android/element-sdk/kab/
2020-08-23 14:38:55 +00:00
ziriSut
64b6b069a4 Translated using Weblate (Kabyle)
Currently translated at 69.4% (163 of 235 strings)

Translation: Element Android/Element Android Sdk
Translate-URL: https://translate.riot.im/projects/element-android/element-sdk/kab/
2020-08-23 14:32:38 +00:00
Kahina Messaoudi
edd455a772 Translated using Weblate (Kabyle)
Currently translated at 69.4% (163 of 235 strings)

Translation: Element Android/Element Android Sdk
Translate-URL: https://translate.riot.im/projects/element-android/element-sdk/kab/
2020-08-23 14:32:38 +00:00
Kahina Messaoudi
a54c1af7cc Translated using Weblate (Kabyle)
Currently translated at 39.6% (93 of 235 strings)

Translation: Element Android/Element Android Sdk
Translate-URL: https://translate.riot.im/projects/element-android/element-sdk/kab/
2020-08-23 12:09:01 +00:00
ziriSut
9e1fe22c19 Translated using Weblate (Kabyle)
Currently translated at 39.6% (93 of 235 strings)

Translation: Element Android/Element Android Sdk
Translate-URL: https://translate.riot.im/projects/element-android/element-sdk/kab/
2020-08-23 12:09:01 +00:00
Kahina Messaoudi
bd033ed5f4 Translated using Weblate (Kabyle)
Currently translated at 7.2% (17 of 235 strings)

Translation: Element Android/Element Android Sdk
Translate-URL: https://translate.riot.im/projects/element-android/element-sdk/kab/
2020-08-23 06:37:58 +00:00
Slimane Selyan AMIRI
2fd9333fb0 Translated using Weblate (Kabyle)
Currently translated at 7.2% (17 of 235 strings)

Translation: Element Android/Element Android Sdk
Translate-URL: https://translate.riot.im/projects/element-android/element-sdk/kab/
2020-08-23 06:37:57 +00:00
Slimane Selyan AMIRI
6249d5a85e Translated using Weblate (Kabyle)
Currently translated at 0.3% (5 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/kab/
2020-08-23 06:35:09 +00:00
Slimane Selyan AMIRI
385de03f40 Added translation using Weblate (Kabyle) 2020-08-22 18:05:34 +00:00
Slimane Selyan AMIRI
5bfb2c211a Added translation using Weblate (Kabyle) 2020-08-22 18:04:55 +00:00
LinAGKar
7ee4a8fc0a Translated using Weblate (Swedish)
Currently translated at 61.0% (1127 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/sv/
2020-08-22 15:42:55 +00:00
AnonymousWebHacker
c73f544a01 Translated using Weblate (Spanish)
Currently translated at 81.6% (1508 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/es/
2020-08-22 15:42:38 +00:00
Nikita Epifanov
370d57cfbc Translated using Weblate (Russian)
Currently translated at 100.0% (1849 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/ru/
2020-08-22 15:42:28 +00:00
Marcelo Filho
4380d34645 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (3 of 3 strings)

Translation: Element Android/Element Android Store
Translate-URL: https://translate.riot.im/projects/element-android/element-store/pt_BR/
2020-08-22 15:42:27 +00:00
aWeinzierl
471497e721 Translated using Weblate (German)
Currently translated at 99.7% (1843 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/de/
2020-08-22 15:42:27 +00:00
Jeff Huang
16809a53ab Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (1849 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/zh_Hant/
2020-08-22 15:42:26 +00:00
Slavi Pantaleev
6ad0098767 Translated using Weblate (Bulgarian)
Currently translated at 75.4% (1394 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/bg/
2020-08-22 15:42:25 +00:00
Priit Jõerüüt
eea17f0905 Translated using Weblate (Estonian)
Currently translated at 100.0% (1849 of 1849 strings)

Translation: Element Android/Element Android App
Translate-URL: https://translate.riot.im/projects/element-android/element-app/et/
2020-08-22 11:08:31 +00:00
Benoit Marty
bd238dcbfa Make user displayName live in the setting, it should fix #1926 2020-08-21 22:24:29 +02:00
Benoit Marty
20e2b3beb8 Merge pull request #1978 from vector-im/feature/ttl
Improve #1971, especially be robust about system time change
2020-08-21 18:12:35 +02:00
ganfra
1f890324b4 Update some dependencies, including coroutines for working debugging 2020-08-21 17:17:51 +02:00
Benoit Marty
04fa7d9628 Version++ 2020-08-21 17:04:05 +02:00
Benoit Marty
7fd7970ec1 Merge branch 'release/1.0.5' into develop 2020-08-21 17:02:46 +02:00
Benoit Marty
008bc0116d Improve #1971, especially be robust about system time change 2020-08-21 16:36:45 +02:00
374 changed files with 12894 additions and 4352 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -27,4 +27,9 @@ class AnimatedImageViewHolder constructor(itemView: View) :
val imageLoaderProgress: ProgressBar = itemView.findViewById(R.id.imageLoaderProgress)
internal val target = DefaultImageLoaderTarget(this, this.touchImageView)
override fun onRecycled() {
super.onRecycled()
touchImageView.setImageDrawable(null)
}
}

View File

@@ -50,6 +50,7 @@ internal class DefaultImageLoaderTarget(val holder: AnimatedImageViewHolder, pri
override fun onLoadFailed(uid: String, errorDrawable: Drawable?) {
if (holder.boundResourceUid != uid) return
holder.imageLoaderProgress.isVisible = false
holder.touchImageView.setImageDrawable(errorDrawable)
}
override fun onResourceCleared(uid: String, placeholder: Drawable?) {
@@ -77,11 +78,13 @@ internal class DefaultImageLoaderTarget(val holder: AnimatedImageViewHolder, pri
override fun onResourceLoading(uid: String, placeholder: Drawable?) {
if (holder.boundResourceUid != uid) return
holder.imageLoaderProgress.isVisible = true
holder.touchImageView.setImageDrawable(placeholder)
}
override fun onLoadFailed(uid: String, errorDrawable: Drawable?) {
if (holder.boundResourceUid != uid) return
holder.imageLoaderProgress.isVisible = false
holder.touchImageView.setImageDrawable(errorDrawable)
}
override fun onResourceCleared(uid: String, placeholder: Drawable?) {

View File

@@ -35,6 +35,7 @@ interface VideoLoaderTarget {
fun onVideoFileLoading(uid: String)
fun onVideoFileLoadFailed(uid: String)
fun onVideoFileReady(uid: String, file: File)
fun onVideoURLReady(uid: String, path: String)
}
internal class DefaultVideoLoaderTarget(val holder: VideoViewHolder, private val contextView: ImageView) : VideoLoaderTarget {
@@ -47,6 +48,8 @@ internal class DefaultVideoLoaderTarget(val holder: VideoViewHolder, private val
}
override fun onThumbnailResourceCleared(uid: String, placeholder: Drawable?) {
if (holder.boundResourceUid != uid) return
holder.thumbnailImage.setImageDrawable(placeholder)
}
override fun onThumbnailResourceReady(uid: String, resource: Drawable) {
@@ -68,9 +71,19 @@ internal class DefaultVideoLoaderTarget(val holder: VideoViewHolder, private val
override fun onVideoFileReady(uid: String, file: File) {
if (holder.boundResourceUid != uid) return
arrangeForVideoReady()
holder.videoReady(file)
}
override fun onVideoURLReady(uid: String, path: String) {
if (holder.boundResourceUid != uid) return
arrangeForVideoReady()
holder.videoReady(path)
}
private fun arrangeForVideoReady() {
holder.thumbnailImage.isVisible = false
holder.loaderProgressBar.isVisible = false
holder.videoView.isVisible = true
holder.videoReady(file)
}
}

View File

@@ -16,6 +16,7 @@
package im.vector.lib.attachmentviewer
import android.util.Log
import android.view.View
import android.widget.ImageView
import android.widget.ProgressBar
@@ -65,6 +66,13 @@ class VideoViewHolder constructor(itemView: View) :
}
}
fun videoReady(path: String) {
mVideoPath = path
if (isSelected) {
startPlaying()
}
}
fun videoFileLoadError() {
}
@@ -118,8 +126,13 @@ class VideoViewHolder constructor(itemView: View) :
eventListener?.get()?.onEvent(AttachmentEvents.VideoEvent(isPlaying, progress, duration))
}
}
try {
videoView.setVideoPath(mVideoPath)
} catch (failure: Throwable) {
// Couldn't open
Log.v(VideoViewHolder::class.java.name, "Failed to start video")
}
videoView.setVideoPath(mVideoPath)
if (!wasPaused) {
videoView.start()
if (progress > 0) {

View File

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

View File

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

285
docs/add_threePids.md Normal file
View File

@@ -0,0 +1,285 @@
# Adding and removing ThreePids to an account
## Add email
### User enter the email
> POST https://homeserver.org/_matrix/client/r0/account/3pid/email/requestToken
```json
{
"email": "alice@email-provider.org",
"client_secret": "TixzvOnw7nLEUdiQEmkHzkXKrY4HhiGh",
"send_attempt": 1
}
```
#### The email is already added to an account
400
```json
{
"errcode": "M_THREEPID_IN_USE",
"error": "Email is already in use"
}
```
#### The email is free
Wording: "We've sent you an email to verify your address. Please follow the instructions there and then click the button below."
200
```json
{
"sid": "bxyDHuJKsdkjMlTJ"
}
```
## User receive an e-mail
> [homeserver.org] Validate your email
>
> A request to add an email address to your Matrix account has been received. If this was you, please click the link below to confirm adding this email:
https://homeserver.org/_matrix/client/unstable/add_threepid/email/submit_token?token=WUnEhQAmJrXupdEbXgdWvnVIKaGYZFsU&client_secret=TixzvOnw7nLEUdiQEmkHzkXKrY4HhiGh&sid=bxyDHuJKsdkjMlTJ
>
> If this was not you, you can safely ignore this email. Thank you.
### User clicks on the link
The browser displays the following message:
> Your email has now been validated, please return to your client. You may now close this window.
### User returns on Element
User clicks on CONTINUE
> POST https://homeserver.org/_matrix/client/r0/account/3pid/add
```json
{
"sid": "bxyDHuJKsdkjMlTJ",
"client_secret": "TixzvOnw7nLEUdiQEmkHzkXKrY4HhiGh"
}
```
401 User Interactive Authentication
```json
{
"session": "ppvvnozXCQZFaggUBlHJYPjA",
"flows": [
{
"stages": [
"m.login.password"
]
}
],
"params": {
}
}
```
### User enters his password
POST https://homeserver.org/_matrix/client/r0/account/3pid/add
```json
{
"sid": "bxyDHuJKsdkjMlTJ",
"client_secret": "TixzvOnw7nLEUdiQEmkHzkXKrY4HhiGh",
"auth": {
"session": "ppvvnozXCQZFaggUBlHJYPjA",
"type": "m.login.password",
"user": "@benoitx:matrix.org",
"password": "weak_password"
}
}
```
#### The link has not been clicked
400
```json
{
"errcode": "M_THREEPID_AUTH_FAILED",
"error": "No validated 3pid session found"
}
```
#### Wrong password
401
```json
{
"session": "fXHOvoQsPMhEebVqTnIrzZJN",
"flows": [
{
"stages": [
"m.login.password"
]
}
],
"params": {
},
"completed":[
],
"error": "Invalid password",
"errcode": "M_FORBIDDEN"
}
```
#### The link has been clicked and the account password is correct
200
```json
{}
```
## Remove email
### User want to remove an email from his account
> POST https://homeserver.org/_matrix/client/r0/account/3pid/delete
```json
{
"medium": "email",
"address": "alice@email-provider.org"
}
```
#### Email was not bound to an identity server
200
```json
{
"id_server_unbind_result": "no-support"
}
```
#### Email was bound to an identity server
200
```json
{
"id_server_unbind_result": "success"
}
```
## Add phone number
> POST https://homeserver.org/_matrix/client/r0/account/3pid/msisdn/requestToken
```json
{
"country": "FR",
"phone_number": "611223344",
"client_secret": "f1K29wFZBEr4RZYatu7xj8nEbXiVpr7J",
"send_attempt": 1
}
```
Note that the phone number is sent without `+` and without the country code
#### The phone number is already added to an account
400
```json
{
"errcode": "M_THREEPID_IN_USE",
"error": "MSISDN is already in use"
}
```
#### The phone number is free
Wording: "A text message has been sent to +33611223344. Please enter the verification code it contains."
200
```json
{
"msisdn": "33651547677",
"intl_fmt": "+33 6 51 54 76 77",
"success": true,
"sid": "253299954",
"submit_url": "https://homeserver.org/_matrix/client/unstable/add_threepid/msisdn/submit_token"
}
```
## User receive a text message
> Riot
> Your Riot validation code is 892541, please enter this into the app
### User enter the code to the app
#### Wrong code
> POST https://homeserver.org/_matrix/client/unstable/add_threepid/msisdn/submit_token
```json
{
"sid": "253299954",
"client_secret": "f1K29wFZBEr4RZYatu7xj8nEbXiVpr7J",
"token": "111111"
}
```
400
```json
{
"errcode": "M_UNKNOWN",
"error": "Error contacting the identity server"
}
```
This is not an ideal, but the client will display a hint to check the entered code to the user.
#### Correct code
> POST https://homeserver.org/_matrix/client/unstable/add_threepid/msisdn/submit_token
```json
{
"sid": "253299954",
"client_secret": "f1K29wFZBEr4RZYatu7xj8nEbXiVpr7J",
"token": "892541"
}
```
200
```json
{
"success": true
}
```
Then the app call `https://homeserver.org/_matrix/client/r0/account/3pid/add` as per adding an email and follow the same UIS flow
## Remove phone number
### User wants to remove a phone number from his account
This is the same request and response than to remove email, but with this body:
```json
{
"medium": "msisdn",
"address": "33611223344"
}
```
Note that the phone number is provided without `+`, but with the country code.

View File

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

View File

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

View File

@@ -18,9 +18,13 @@
package org.matrix.android.sdk.rx
import androidx.paging.PagedList
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.functions.Function3
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
@@ -43,10 +47,6 @@ import org.matrix.android.sdk.api.util.toOptional
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo
import org.matrix.android.sdk.api.session.accountdata.UserAccountDataEvent
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.functions.Function3
class RxSession(private val session: Session) {
@@ -110,6 +110,11 @@ class RxSession(private val session: Session) {
.startWithCallable { session.getThreePids() }
}
fun livePendingThreePIds(): Observable<List<ThreePid>> {
return session.getPendingThreePidsLive().asObservable()
.startWithCallable { session.getPendingThreePids() }
}
fun createRoom(roomParams: CreateRoomParams): Single<String> = singleBuilder {
session.createRoom(roomParams, it)
}

View File

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

View File

@@ -23,15 +23,12 @@ import androidx.work.WorkManager
import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.BuildConfig
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.common.DaggerTestMatrixComponent
import org.matrix.android.sdk.api.legacy.LegacySessionImporter
import org.matrix.android.sdk.common.DaggerTestMatrixComponent
import org.matrix.android.sdk.internal.SessionManager
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments
import org.matrix.android.sdk.internal.network.UserAgentHolder
import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver
import org.matrix.olm.OlmManager
import java.io.InputStream
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
@@ -96,9 +93,5 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
fun getSdkVersion(): String {
return BuildConfig.VERSION_NAME + " (" + BuildConfig.GIT_SDK_REVISION + ")"
}
fun decryptStream(inputStream: InputStream?, elementToDecrypt: ElementToDecrypt): InputStream? {
return MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt)
}
}
}

View File

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

View File

@@ -17,23 +17,22 @@
package org.matrix.android.sdk.internal.session.room.send
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.matrix.android.sdk.InstrumentedTest
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
import org.commonmark.renderer.text.TextContentRenderer
import org.junit.Assert.assertEquals
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest
/**
* It will not be possible to test all combinations. For the moment I add a few tests, then, depending on the problem discovered in the wild,
* we can add more tests to cover the edge cases.
* Some tests are suffixed with `_not_passing`, maybe one day we will fix them...
* Riot-Web should be used as a reference for expected results, but not always. Especially Riot-Web add lots of `\n` in the
* formatted body, which is quite useless.
* Also Riot-Web does not provide plain text body when formatted text is provided. The body contains what the user has entered.
* Element Web should be used as a reference for expected results, but not always.
* Also Element Web does not provide plain text body when formatted text is provided. The body contains what the user has entered. We are doing
* the same to be able to edit messages (See #1939)
* See https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes
*/
@Suppress("SpellCheckingInspection")
@@ -46,8 +45,7 @@ class MarkdownParserTest : InstrumentedTest {
*/
private val markdownParser = MarkdownParser(
Parser.builder().build(),
HtmlRenderer.builder().build(),
TextContentRenderer.builder().build()
HtmlRenderer.builder().build()
)
@Test
@@ -83,6 +81,15 @@ class MarkdownParserTest : InstrumentedTest {
)
}
@Test
fun parseBoldNewLines() {
testTypeNewLines(
name = "bold",
markdownPattern = "**",
htmlExpectedTag = "strong"
)
}
@Test
fun parseItalic() {
testType(
@@ -92,14 +99,23 @@ class MarkdownParserTest : InstrumentedTest {
)
}
@Test
fun parseItalicNewLines() {
testTypeNewLines(
name = "italic",
markdownPattern = "*",
htmlExpectedTag = "em"
)
}
@Test
fun parseItalic2() {
// Riot-Web format
"_italic_".let { markdownParser.parse(it) }.expect("italic", "<em>italic</em>")
// Element Web format
"_italic_".let { markdownParser.parse(it).expect(it, "<em>italic</em>") }
}
/**
* Note: the test is not passing, it does not work on Riot-Web neither
* Note: the test is not passing, it does not work on Element Web neither
*/
@Test
fun parseStrike_not_passing() {
@@ -110,14 +126,30 @@ class MarkdownParserTest : InstrumentedTest {
)
}
@Test
fun parseStrikeNewLines() {
testTypeNewLines(
name = "strike",
markdownPattern = "~~",
htmlExpectedTag = "del"
)
}
@Test
fun parseCode() {
testType(
name = "code",
markdownPattern = "`",
htmlExpectedTag = "code",
plainTextPrefix = "\"",
plainTextSuffix = "\""
htmlExpectedTag = "code"
)
}
@Test
fun parseCodeNewLines() {
testTypeNewLines(
name = "code",
markdownPattern = "`",
htmlExpectedTag = "code"
)
}
@@ -126,9 +158,16 @@ class MarkdownParserTest : InstrumentedTest {
testType(
name = "code",
markdownPattern = "``",
htmlExpectedTag = "code",
plainTextPrefix = "\"",
plainTextSuffix = "\""
htmlExpectedTag = "code"
)
}
@Test
fun parseCode2NewLines() {
testTypeNewLines(
name = "code",
markdownPattern = "``",
htmlExpectedTag = "code"
)
}
@@ -137,78 +176,85 @@ class MarkdownParserTest : InstrumentedTest {
testType(
name = "code",
markdownPattern = "```",
htmlExpectedTag = "code",
plainTextPrefix = "\"",
plainTextSuffix = "\""
htmlExpectedTag = "code"
)
}
@Test
fun parseCode3NewLines() {
testTypeNewLines(
name = "code",
markdownPattern = "```",
htmlExpectedTag = "code"
)
}
@Test
fun parseUnorderedList() {
"- item1".let { markdownParser.parse(it).expect(it, "<ul><li>item1</li></ul>") }
"- item1\n- item2".let { markdownParser.parse(it).expect(it, "<ul><li>item1</li><li>item2</li></ul>") }
"- item1".let { markdownParser.parse(it).expect(it, "<ul>\n<li>item1</li>\n</ul>") }
"- item1\n- item2".let { markdownParser.parse(it).expect(it, "<ul>\n<li>item1</li>\n<li>item2</li>\n</ul>") }
}
@Test
fun parseOrderedList() {
"1. item1".let { markdownParser.parse(it).expect(it, "<ol><li>item1</li></ol>") }
"1. item1\n2. item2".let { markdownParser.parse(it).expect(it, "<ol><li>item1</li><li>item2</li></ol>") }
"1. item1".let { markdownParser.parse(it).expect(it, "<ol>\n<li>item1</li>\n</ol>") }
"1. item1\n2. item2".let { markdownParser.parse(it).expect(it, "<ol>\n<li>item1</li>\n<li>item2</li>\n</ol>") }
}
@Test
fun parseHorizontalLine() {
"---".let { markdownParser.parse(it) }.expect("***", "<hr />")
"---".let { markdownParser.parse(it).expect(it, "<hr />") }
}
@Test
fun parseH2AndContent() {
"a\n---\nb".let { markdownParser.parse(it) }.expect("a\nb", "<h2>a</h2><p>b</p>")
"a\n---\nb".let { markdownParser.parse(it).expect(it, "<h2>a</h2>\n<p>b</p>") }
}
@Test
fun parseQuote() {
"> quoted".let { markdownParser.parse(it) }.expect("«quoted»", "<blockquote><p>quoted</p></blockquote>")
"> quoted".let { markdownParser.parse(it).expect(it, "<blockquote>\n<p>quoted</p>\n</blockquote>") }
}
@Test
fun parseQuote_not_passing() {
"> quoted\nline2".let { markdownParser.parse(it) }.expect("«quoted\nline2»", "<blockquote><p>quoted<br/>line2</p></blockquote>")
"> quoted\nline2".let { markdownParser.parse(it).expect(it, "<blockquote><p>quoted<br />line2</p></blockquote>") }
}
@Test
fun parseBoldItalic() {
"*italic* **bold**".let { markdownParser.parse(it) }.expect("italic bold", "<em>italic</em> <strong>bold</strong>")
"**bold** *italic*".let { markdownParser.parse(it) }.expect("bold italic", "<strong>bold</strong> <em>italic</em>")
"*italic* **bold**".let { markdownParser.parse(it).expect(it, "<em>italic</em> <strong>bold</strong>") }
"**bold** *italic*".let { markdownParser.parse(it).expect(it, "<strong>bold</strong> <em>italic</em>") }
}
@Test
fun parseHead() {
"# head1".let { markdownParser.parse(it) }.expect("head1", "<h1>head1</h1>")
"## head2".let { markdownParser.parse(it) }.expect("head2", "<h2>head2</h2>")
"### head3".let { markdownParser.parse(it) }.expect("head3", "<h3>head3</h3>")
"#### head4".let { markdownParser.parse(it) }.expect("head4", "<h4>head4</h4>")
"##### head5".let { markdownParser.parse(it) }.expect("head5", "<h5>head5</h5>")
"###### head6".let { markdownParser.parse(it) }.expect("head6", "<h6>head6</h6>")
"# head1".let { markdownParser.parse(it).expect(it, "<h1>head1</h1>") }
"## head2".let { markdownParser.parse(it).expect(it, "<h2>head2</h2>") }
"### head3".let { markdownParser.parse(it).expect(it, "<h3>head3</h3>") }
"#### head4".let { markdownParser.parse(it).expect(it, "<h4>head4</h4>") }
"##### head5".let { markdownParser.parse(it).expect(it, "<h5>head5</h5>") }
"###### head6".let { markdownParser.parse(it).expect(it, "<h6>head6</h6>") }
}
@Test
fun parseHeads() {
"# head1\n# head2".let { markdownParser.parse(it) }.expect("head1\nhead2", "<h1>head1</h1><h1>head2</h1>")
"# head1\n# head2".let { markdownParser.parse(it).expect(it, "<h1>head1</h1>\n<h1>head2</h1>") }
}
@Test
fun parseBoldNewLines_not_passing() {
"**bold**\nline2".let { markdownParser.parse(it) }.expect("bold\nline2", "<strong>bold</strong><br />line2")
"**bold**\nline2".let { markdownParser.parse(it).expect(it, "<strong>bold</strong><br />line2") }
}
@Test
fun parseLinks() {
"[link](target)".let { markdownParser.parse(it) }.expect(""""link" (target)""", """<a href="target">link</a>""")
"[link](target)".let { markdownParser.parse(it).expect(it, """<a href="target">link</a>""") }
}
@Test
fun parseParagraph() {
"# head\ncontent".let { markdownParser.parse(it) }.expect("head\ncontent", "<h1>head</h1><p>content</p>")
"# head\ncontent".let { markdownParser.parse(it).expect(it, "<h1>head</h1>\n<p>content</p>") }
}
private fun testIdentity(text: String) {
@@ -217,59 +263,93 @@ class MarkdownParserTest : InstrumentedTest {
private fun testType(name: String,
markdownPattern: String,
htmlExpectedTag: String,
plainTextPrefix: String = "",
plainTextSuffix: String = "") {
htmlExpectedTag: String) {
// Test simple case
"$markdownPattern$name$markdownPattern"
.let { markdownParser.parse(it) }
.expect(expectedText = "$plainTextPrefix$name$plainTextSuffix",
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag>")
.let {
markdownParser.parse(it)
.expect(expectedText = it,
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag>")
}
// Test twice the same tag
"$markdownPattern$name$markdownPattern and $markdownPattern$name bis$markdownPattern"
.let { markdownParser.parse(it) }
.expect(expectedText = "$plainTextPrefix$name$plainTextSuffix and $plainTextPrefix$name bis$plainTextSuffix",
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag> and <$htmlExpectedTag>$name bis</$htmlExpectedTag>")
.let {
markdownParser.parse(it)
.expect(expectedText = it,
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag> and <$htmlExpectedTag>$name bis</$htmlExpectedTag>")
}
val textBefore = "a"
val textAfter = "b"
// With sticked text before
"$textBefore$markdownPattern$name$markdownPattern"
.let { markdownParser.parse(it) }
.expect(expectedText = "$textBefore$plainTextPrefix$name$plainTextSuffix",
expectedFormattedText = "$textBefore<$htmlExpectedTag>$name</$htmlExpectedTag>")
.let {
markdownParser.parse(it)
.expect(expectedText = it,
expectedFormattedText = "$textBefore<$htmlExpectedTag>$name</$htmlExpectedTag>")
}
// With text before and space
"$textBefore $markdownPattern$name$markdownPattern"
.let { markdownParser.parse(it) }
.expect(expectedText = "$textBefore $plainTextPrefix$name$plainTextSuffix",
expectedFormattedText = "$textBefore <$htmlExpectedTag>$name</$htmlExpectedTag>")
.let {
markdownParser.parse(it)
.expect(expectedText = it,
expectedFormattedText = "$textBefore <$htmlExpectedTag>$name</$htmlExpectedTag>")
}
// With sticked text after
"$markdownPattern$name$markdownPattern$textAfter"
.let { markdownParser.parse(it) }
.expect(expectedText = "$plainTextPrefix$name$plainTextSuffix$textAfter",
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag>$textAfter")
.let {
markdownParser.parse(it)
.expect(expectedText = it,
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag>$textAfter")
}
// With space and text after
"$markdownPattern$name$markdownPattern $textAfter"
.let { markdownParser.parse(it) }
.expect(expectedText = "$plainTextPrefix$name$plainTextSuffix $textAfter",
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag> $textAfter")
.let {
markdownParser.parse(it)
.expect(expectedText = it,
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag> $textAfter")
}
// With sticked text before and text after
"$textBefore$markdownPattern$name$markdownPattern$textAfter"
.let { markdownParser.parse(it) }
.expect(expectedText = "$textBefore$plainTextPrefix$name$plainTextSuffix$textAfter",
expectedFormattedText = "a<$htmlExpectedTag>$name</$htmlExpectedTag>$textAfter")
.let {
markdownParser.parse(it)
.expect(expectedText = it,
expectedFormattedText = "a<$htmlExpectedTag>$name</$htmlExpectedTag>$textAfter")
}
// With text before and after, with spaces
"$textBefore $markdownPattern$name$markdownPattern $textAfter"
.let { markdownParser.parse(it) }
.expect(expectedText = "$textBefore $plainTextPrefix$name$plainTextSuffix $textAfter",
expectedFormattedText = "$textBefore <$htmlExpectedTag>$name</$htmlExpectedTag> $textAfter")
.let {
markdownParser.parse(it)
.expect(expectedText = it,
expectedFormattedText = "$textBefore <$htmlExpectedTag>$name</$htmlExpectedTag> $textAfter")
}
}
private fun testTypeNewLines(name: String,
markdownPattern: String,
htmlExpectedTag: String) {
// With new line inside the block
"$markdownPattern$name\n$name$markdownPattern"
.let {
markdownParser.parse(it)
.expect(expectedText = it,
expectedFormattedText = "<$htmlExpectedTag>$name<br />$name</$htmlExpectedTag>")
}
// With new line between two blocks
"$markdownPattern$name$markdownPattern\n$markdownPattern$name$markdownPattern"
.let {
markdownParser.parse(it)
.expect(expectedText = it,
expectedFormattedText = "<$htmlExpectedTag>$name</$htmlExpectedTag><$htmlExpectedTag>$name</$htmlExpectedTag>")
}
}
private fun TextContent.expect(expectedText: String, expectedFormattedText: String?) {

View File

@@ -26,13 +26,10 @@ import org.matrix.android.sdk.BuildConfig
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.legacy.LegacySessionImporter
import org.matrix.android.sdk.internal.SessionManager
import org.matrix.android.sdk.internal.crypto.attachments.ElementToDecrypt
import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments
import org.matrix.android.sdk.internal.di.DaggerMatrixComponent
import org.matrix.android.sdk.internal.network.UserAgentHolder
import org.matrix.android.sdk.internal.util.BackgroundDetectionObserver
import org.matrix.olm.OlmManager
import java.io.InputStream
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
@@ -97,9 +94,5 @@ class Matrix private constructor(context: Context, matrixConfiguration: MatrixCo
fun getSdkVersion(): String {
return BuildConfig.VERSION_NAME + " (" + BuildConfig.GIT_SDK_REVISION + ")"
}
fun decryptStream(inputStream: InputStream?, elementToDecrypt: ElementToDecrypt): InputStream? {
return MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt)
}
}
}

View File

@@ -45,7 +45,7 @@ sealed class WellknownResult {
/**
* Inform the user that auto-discovery failed due to invalid/empty data and PROMPT for the parameter.
*/
object FailPrompt : WellknownResult()
data class FailPrompt(val homeServerUrl: String?, val wellKnown: WellKnown?) : WellknownResult()
/**
* Inform the user that auto-discovery did not return any usable URLs. Do not continue further with the current login process.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -33,7 +33,7 @@ interface ContentUploadStateTracker {
object Idle : State()
object EncryptingThumbnail : State()
data class UploadingThumbnail(val current: Long, val total: Long) : State()
object Encrypting : State()
data class Encrypting(val current: Long, val total: Long) : State()
data class Uploading(val current: Long, val total: Long) : State()
object Success : State()
data class Failure(val throwable: Throwable) : State()

View File

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

View File

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

View File

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

View File

@@ -83,4 +83,43 @@ interface ProfileService {
* @param refreshData set to true to fetch data from the homeserver
*/
fun getThreePidsLive(refreshData: Boolean): LiveData<List<ThreePid>>
/**
* Get the pending 3Pids, i.e. ThreePids that have requested a token, but not yet validated by the user.
*/
fun getPendingThreePids(): List<ThreePid>
/**
* Get the pending 3Pids Live
*/
fun getPendingThreePidsLive(): LiveData<List<ThreePid>>
/**
* Add a 3Pids. This is the first step to add a ThreePid to an account. Then the threePid will be added to the pending threePid list.
*/
fun addThreePid(threePid: ThreePid, matrixCallback: MatrixCallback<Unit>): Cancelable
/**
* Validate a code received by text message
*/
fun submitSmsCode(threePid: ThreePid.Msisdn, code: String, matrixCallback: MatrixCallback<Unit>): Cancelable
/**
* Finalize adding a 3Pids. Call this method once the user has validated that he owns the ThreePid
*/
fun finalizeAddingThreePid(threePid: ThreePid,
uiaSession: String?,
accountPassword: String?,
matrixCallback: MatrixCallback<Unit>): Cancelable
/**
* Cancel adding a threepid. It will remove locally stored data about this ThreePid
*/
fun cancelAddingThreePid(threePid: ThreePid,
matrixCallback: MatrixCallback<Unit>): Cancelable
/**
* Remove a 3Pid from the Matrix account.
*/
fun deleteThreePid(threePid: ThreePid, matrixCallback: MatrixCallback<Unit>): Cancelable
}

View File

@@ -110,13 +110,13 @@ interface SendService {
* Schedule this message to be resent
* @param localEcho the unsent local echo
*/
fun resendTextMessage(localEcho: TimelineEvent): Cancelable?
fun resendTextMessage(localEcho: TimelineEvent): Cancelable
/**
* Schedule this message to be resent
* @param localEcho the unsent local echo
*/
fun resendMediaMessage(localEcho: TimelineEvent): Cancelable?
fun resendMediaMessage(localEcho: TimelineEvent): Cancelable
/**
* Remove this failed message from the timeline
@@ -124,8 +124,16 @@ interface SendService {
*/
fun deleteFailedEcho(localEcho: TimelineEvent)
/**
* Delete all the events in one of the sending states
*/
fun clearSendingQueue()
/**
* Cancel sending a specific event. It has to be in one of the sending states
*/
fun cancelSend(eventId: String)
/**
* Resend all failed messages one by one (and keep order)
*/

View File

@@ -37,7 +37,8 @@ enum class SendState {
internal companion object {
val HAS_FAILED_STATES = listOf(UNDELIVERED, FAILED_UNKNOWN_DEVICES)
val IS_SENT_STATES = listOf(SENT, SYNCED)
val IS_SENDING_STATES = listOf(UNSENT, ENCRYPTING, SENDING)
val IS_PROGRESSING_STATES = listOf(ENCRYPTING, SENDING)
val IS_SENDING_STATES = IS_PROGRESSING_STATES + UNSENT
val PENDING_STATES = IS_SENDING_STATES + HAS_FAILED_STATES
}
@@ -45,5 +46,7 @@ enum class SendState {
fun hasFailed() = HAS_FAILED_STATES.contains(this)
fun isInProgress() = IS_PROGRESSING_STATES.contains(this)
fun isSending() = IS_SENDING_STATES.contains(this)
}

View File

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

View File

@@ -17,6 +17,9 @@
package org.matrix.android.sdk.internal.auth.registration
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import okhttp3.OkHttpClient
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
@@ -33,9 +36,6 @@ import org.matrix.android.sdk.internal.auth.db.PendingSessionData
import org.matrix.android.sdk.internal.network.RetrofitFactory
import org.matrix.android.sdk.internal.task.launchToCallback
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import okhttp3.OkHttpClient
/**
* This class execute the registration request and is responsible to keep the session of interactive authentication
@@ -193,7 +193,7 @@ internal class DefaultRegistrationWizard(
val registrationParams = pendingSessionData.currentThreePidData?.registrationParams
?: throw IllegalStateException("developer error, no pending three pid")
val safeCurrentData = pendingSessionData.currentThreePidData ?: throw IllegalStateException("developer error, call createAccount() method first")
val url = safeCurrentData.addThreePidRegistrationResponse.submitUrl ?: throw IllegalStateException("Missing url the send the code")
val url = safeCurrentData.addThreePidRegistrationResponse.submitUrl ?: throw IllegalStateException("Missing url to send the code")
val validationBody = ValidationCodeBody(
clientSecret = pendingSessionData.clientSecret,
sid = safeCurrentData.addThreePidRegistrationResponse.sid,

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,69 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.crypto.attachments
import android.util.Base64
import org.matrix.android.sdk.internal.util.base64ToUnpaddedBase64
import java.io.FilterInputStream
import java.io.IOException
import java.io.InputStream
import java.security.MessageDigest
class MatrixDigestCheckInputStream(
inputStream: InputStream?,
private val expectedDigest: String
) : FilterInputStream(inputStream) {
private val digest = MessageDigest.getInstance("SHA-256")
@Throws(IOException::class)
override fun read(): Int {
val b = `in`.read()
if (b >= 0) {
digest.update(b.toByte())
}
if (b == -1) {
ensureDigest()
}
return b
}
@Throws(IOException::class)
override fun read(
b: ByteArray,
off: Int,
len: Int): Int {
val n = `in`.read(b, off, len)
if (n > 0) {
digest.update(b, off, n)
}
if (n == -1) {
ensureDigest()
}
return n
}
@Throws(IOException::class)
private fun ensureDigest() {
val currentDigestValue = base64ToUnpaddedBase64(Base64.encodeToString(digest.digest(), Base64.DEFAULT))
if (currentDigestValue != expectedDigest) {
throw IOException("Bad digest")
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,18 +20,24 @@ package org.matrix.android.sdk.internal.database
import io.realm.DynamicRealm
import io.realm.RealmMigration
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
import timber.log.Timber
import javax.inject.Inject
class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
companion object {
const val SESSION_STORE_SCHEMA_VERSION = 4L
}
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.v("Migrating Realm Session from $oldVersion to $newVersion")
if (oldVersion <= 0) migrateTo1(realm)
if (oldVersion <= 1) migrateTo2(realm)
if (oldVersion <= 2) migrateTo3(realm)
if (oldVersion <= 3) migrateTo4(realm)
}
private fun migrateTo1(realm: DynamicRealm) {
@@ -63,4 +69,17 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
obj.setLong(HomeServerCapabilitiesEntityFields.LAST_UPDATED_TIMESTAMP, 0)
}
}
private fun migrateTo4(realm: DynamicRealm) {
Timber.d("Step 3 -> 4")
realm.schema.create("PendingThreePidEntity")
.addField(PendingThreePidEntityFields.CLIENT_SECRET, String::class.java)
.setRequired(PendingThreePidEntityFields.CLIENT_SECRET, true)
.addField(PendingThreePidEntityFields.EMAIL, String::class.java)
.addField(PendingThreePidEntityFields.MSISDN, String::class.java)
.addField(PendingThreePidEntityFields.SEND_ATTEMPT, Int::class.java)
.addField(PendingThreePidEntityFields.SID, String::class.java)
.setRequired(PendingThreePidEntityFields.SID, true)
.addField(PendingThreePidEntityFields.SUBMIT_URL, String::class.java)
}
}

View File

@@ -19,13 +19,14 @@ package org.matrix.android.sdk.internal.database
import android.content.Context
import androidx.core.content.edit
import io.realm.Realm
import io.realm.RealmConfiguration
import org.matrix.android.sdk.BuildConfig
import org.matrix.android.sdk.internal.database.model.SessionRealmModule
import org.matrix.android.sdk.internal.di.SessionFilesDirectory
import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.di.UserMd5
import org.matrix.android.sdk.internal.session.SessionModule
import io.realm.Realm
import io.realm.RealmConfiguration
import timber.log.Timber
import java.io.File
import javax.inject.Inject
@@ -46,20 +47,16 @@ internal class SessionRealmConfigurationFactory @Inject constructor(
val migration: RealmSessionStoreMigration,
context: Context) {
companion object {
const val SESSION_STORE_SCHEMA_VERSION = 3L
}
// Keep legacy preferences name for compatibility reason
private val sharedPreferences = context.getSharedPreferences("im.vector.matrix.android.realm", Context.MODE_PRIVATE)
fun create(): RealmConfiguration {
val shouldClearRealm = sharedPreferences.getBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", false)
if (shouldClearRealm) {
Timber.v("************************************************************")
Timber.v("The realm file session was corrupted and couldn't be loaded.")
Timber.v("The file has been deleted to recover.")
Timber.v("************************************************************")
Timber.e("************************************************************")
Timber.e("The realm file session was corrupted and couldn't be loaded.")
Timber.e("The file has been deleted to recover.")
Timber.e("************************************************************")
deleteRealmFiles()
}
sharedPreferences.edit {
@@ -74,7 +71,7 @@ internal class SessionRealmConfigurationFactory @Inject constructor(
realmKeysUtils.configureEncryption(this, SessionModule.getKeyAlias(userMd5))
}
.modules(SessionRealmModule())
.schemaVersion(SESSION_STORE_SCHEMA_VERSION)
.schemaVersion(RealmSessionStoreMigration.SESSION_STORE_SCHEMA_VERSION)
.migration(migration)
.build()
@@ -90,6 +87,11 @@ internal class SessionRealmConfigurationFactory @Inject constructor(
// Delete all the realm files of the session
private fun deleteRealmFiles() {
if (BuildConfig.DEBUG) {
Timber.e("No op because it is a debug build")
return
}
listOf(REALM_NAME, "$REALM_NAME.lock", "$REALM_NAME.note", "$REALM_NAME.management").forEach { file ->
try {
File(directory, file).deleteRecursively()

View File

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

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2019 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.database.model
import io.realm.RealmObject
/**
* This class is used to store pending threePid data, when user wants to add a threePid to his account
*/
internal open class PendingThreePidEntity(
var email: String? = null,
var msisdn: String? = null,
var clientSecret: String = "",
var sendAttempt: Int = 0,
var sid: String = "",
var submitUrl: String? = null
) : RealmObject()

View File

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

View File

@@ -24,6 +24,7 @@ import okio.BufferedSink
import okio.ForwardingSink
import okio.Sink
import okio.buffer
import org.matrix.android.sdk.api.extensions.tryThis
import java.io.IOException
internal class ProgressRequestBody(private val delegate: RequestBody,
@@ -35,15 +36,13 @@ internal class ProgressRequestBody(private val delegate: RequestBody,
return delegate.contentType()
}
override fun contentLength(): Long {
try {
return delegate.contentLength()
} catch (e: IOException) {
e.printStackTrace()
}
override fun isOneShot() = delegate.isOneShot()
return -1
}
override fun isDuplex() = delegate.isDuplex()
val length = tryThis { delegate.contentLength() } ?: -1
override fun contentLength() = length
@Throws(IOException::class)
override fun writeTo(sink: BufferedSink) {

View File

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

View File

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

View File

@@ -143,20 +143,22 @@ internal class DefaultFileService @Inject constructor(
Timber.v("Response size ${response.body?.contentLength()} - Stream available: ${!source.exhausted()}")
if (elementToDecrypt != null) {
Timber.v("## decrypt file")
val decryptedStream = MXEncryptedAttachments.decryptAttachment(source.inputStream(), elementToDecrypt)
Timber.v("## FileService: decrypt file")
val decryptSuccess = MXEncryptedAttachments.decryptAttachment(
source.inputStream(),
elementToDecrypt,
destFile.outputStream().buffered()
)
response.close()
if (decryptedStream == null) {
if (!decryptSuccess) {
return@flatMap Try.Failure(IllegalStateException("Decryption error"))
} else {
decryptedStream.use {
writeToFile(decryptedStream, destFile)
}
}
} else {
writeToFile(source.inputStream(), destFile)
response.close()
}
} else {
Timber.v("## FileService: cache hit for $url")
}
Try.just(copyFile(destFile, downloadMode))

View File

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

View File

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

View File

@@ -74,8 +74,8 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU
updateState(key, progressData)
}
internal fun setEncrypting(key: String) {
val progressData = ContentUploadStateTracker.State.Encrypting
internal fun setEncrypting(key: String, current: Long, total: Long) {
val progressData = ContentUploadStateTracker.State.Encrypting(current, total)
updateState(key, progressData)
}

View File

@@ -23,13 +23,16 @@ import com.squareup.moshi.Moshi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okio.BufferedSink
import okio.source
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.extensions.tryThis
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
import org.matrix.android.sdk.internal.di.Authenticated
import org.matrix.android.sdk.internal.network.ProgressRequestBody
@@ -38,6 +41,7 @@ import org.matrix.android.sdk.internal.network.toFailure
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.util.UUID
import javax.inject.Inject
internal class FileUploader @Inject constructor(@Authenticated
@@ -54,7 +58,21 @@ internal class FileUploader @Inject constructor(@Authenticated
filename: String?,
mimeType: String?,
progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
val uploadBody = file.asRequestBody(mimeType?.toMediaTypeOrNull())
val uploadBody = object : RequestBody() {
override fun contentLength() = file.length()
// Disable okhttp auto resend for 'large files'
override fun isOneShot() = contentLength() == 0L || contentLength() >= 1_000_000
override fun contentType(): MediaType? {
return mimeType?.toMediaTypeOrNull()
}
override fun writeTo(sink: BufferedSink) {
file.source().use { sink.writeAll(it) }
}
}
return upload(uploadBody, filename, progressListener)
}
@@ -70,14 +88,18 @@ internal class FileUploader @Inject constructor(@Authenticated
filename: String?,
mimeType: String?,
progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
return withContext(Dispatchers.IO) {
val inputStream = context.contentResolver.openInputStream(uri) ?: throw FileNotFoundException()
inputStream.use {
uploadByteArray(it.readBytes(), filename, mimeType, progressListener)
}
val inputStream = withContext(Dispatchers.IO) {
context.contentResolver.openInputStream(uri)
} ?: throw FileNotFoundException()
val workingFile = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir)
workingFile.outputStream().use {
inputStream.copyTo(it)
}
return uploadFile(workingFile, filename, mimeType, progressListener).also {
tryThis { workingFile.delete() }
}
}
private suspend fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): ContentUploadResponse {
val urlBuilder = uploadUrl.toHttpUrlOrNull()?.newBuilder() ?: throw RuntimeException()

View File

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

View File

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

View File

@@ -61,19 +61,23 @@ internal class DefaultContentDownloadStateTracker @Inject constructor() : Progre
// private fun URL.toKey() = toString()
override fun update(url: String, bytesRead: Long, contentLength: Long, done: Boolean) {
Timber.v("## DL Progress url:$url read:$bytesRead total:$contentLength done:$done")
if (done) {
updateState(url, ContentDownloadStateTracker.State.Success)
} else {
updateState(url, ContentDownloadStateTracker.State.Downloading(bytesRead, contentLength, contentLength == -1L))
mainHandler.post {
Timber.v("## DL Progress url:$url read:$bytesRead total:$contentLength done:$done")
if (done) {
updateState(url, ContentDownloadStateTracker.State.Success)
} else {
updateState(url, ContentDownloadStateTracker.State.Downloading(bytesRead, contentLength, contentLength == -1L))
}
}
}
override fun error(url: String, errorCode: Int) {
Timber.v("## DL Progress Error code:$errorCode")
updateState(url, ContentDownloadStateTracker.State.Failure(errorCode))
listeners[url]?.forEach {
tryThis { it.onDownloadStateUpdate(ContentDownloadStateTracker.State.Failure(errorCode)) }
mainHandler.post {
Timber.v("## DL Progress Error code:$errorCode")
updateState(url, ContentDownloadStateTracker.State.Failure(errorCode))
listeners[url]?.forEach {
tryThis { it.onDownloadStateUpdate(ContentDownloadStateTracker.State.Failure(errorCode)) }
}
}
}

View File

@@ -23,7 +23,6 @@ import org.matrix.android.sdk.api.session.identity.FoundThreePid
import org.matrix.android.sdk.api.session.identity.IdentityServiceError
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.identity.toMedium
import org.matrix.android.sdk.internal.crypto.attachments.MXEncryptedAttachments.base64ToBase64Url
import org.matrix.android.sdk.internal.crypto.tools.withOlmUtility
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.network.executeRequest
@@ -32,6 +31,7 @@ import org.matrix.android.sdk.internal.session.identity.model.IdentityHashDetail
import org.matrix.android.sdk.internal.session.identity.model.IdentityLookUpParams
import org.matrix.android.sdk.internal.session.identity.model.IdentityLookUpResponse
import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.base64ToBase64Url
import java.util.Locale
import javax.inject.Inject

View File

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

View File

@@ -0,0 +1,47 @@
/*
* Copyright (c) 2020 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.profile
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class AddEmailBody(
/**
* Required. A unique string generated by the client, and used to identify the validation attempt.
* It must be a string consisting of the characters [0-9a-zA-Z.=_-]. Its length must not exceed
* 255 characters and it must not be empty.
*/
@Json(name = "client_secret")
val clientSecret: String,
/**
* Required. The email address to validate.
*/
@Json(name = "email")
val email: String,
/**
* Required. The server will only send an email if the send_attempt is a number greater than the most
* recent one which it has seen, scoped to that email + client_secret pair. This is to avoid repeatedly
* sending the same email in the case of request retries between the POSTing user and the identity server.
* The client should increment this value if they desire a new email (e.g. a reminder) to be sent.
* If they do not, the server should respond with success but not resend the email.
*/
@Json(name = "send_attempt")
val sendAttempt: Int
)

View File

@@ -0,0 +1,30 @@
/*
* Copyright (c) 2020 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.profile
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class AddEmailResponse(
/**
* Required. The session ID. Session IDs are opaque strings that must consist entirely
* of the characters [0-9a-zA-Z.=_-]. Their length must not exceed 255 characters and they must not be empty.
*/
@Json(name = "sid")
val sid: String
)

View File

@@ -0,0 +1,54 @@
/*
* Copyright (c) 2020 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.profile
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class AddMsisdnBody(
/**
* Required. A unique string generated by the client, and used to identify the validation attempt.
* It must be a string consisting of the characters [0-9a-zA-Z.=_-]. Its length must not exceed
* 255 characters and it must not be empty.
*/
@Json(name = "client_secret")
val clientSecret: String,
/**
* Required. The two-letter uppercase ISO-3166-1 alpha-2 country code that the number in
* phone_number should be parsed as if it were dialled from.
*/
@Json(name = "country")
val country: String,
/**
* Required. The phone number to validate.
*/
@Json(name = "phone_number")
val phoneNumber: String,
/**
* Required. The server will only send an SMS if the send_attempt is a number greater than the most
* recent one which it has seen, scoped to that country + phone_number + client_secret triple. This
* is to avoid repeatedly sending the same SMS in the case of request retries between the POSTing user
* and the identity server. The client should increment this value if they desire a new SMS (e.g. a
* reminder) to be sent.
*/
@Json(name = "send_attempt")
val sendAttempt: Int
)

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2019 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.profile
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class AddMsisdnResponse(
/**
* Required. The session ID. Session IDs are opaque strings that must consist entirely of the characters [0-9a-zA-Z.=_-].
* Their length must not exceed 255 characters and they must not be empty.
*/
@Json(name = "sid")
val sid: String,
/**
* An optional field containing a URL where the client must submit the validation token to, with identical parameters to the Identity
* Service API's POST /validate/email/submitToken endpoint (without the requirement for an access token).
* The homeserver must send this token to the user (if applicable), who should then be prompted to provide it to the client.
*
* If this field is not present, the client can assume that verification will happen without the client's involvement provided
* the homeserver advertises this specification version in the /versions response (ie: r0.5.0).
*/
@Json(name = "submit_url")
val submitUrl: String? = null,
/* ==========================================================================================
* It seems that the homeserver is sending more data, we may need it
* ========================================================================================== */
@Json(name = "msisdn")
val msisdn: String? = null,
@Json(name = "intl_fmt")
val formattedMsisdn: String? = null,
@Json(name = "success")
val success: Boolean? = null
)

View File

@@ -0,0 +1,111 @@
/*
* Copyright (c) 2020 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.profile
import com.google.i18n.phonenumbers.PhoneNumberUtil
import com.zhuinden.monarchy.Monarchy
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction
import java.util.UUID
import javax.inject.Inject
internal abstract class AddThreePidTask : Task<AddThreePidTask.Params, Unit> {
data class Params(
val threePid: ThreePid
)
}
internal class DefaultAddThreePidTask @Inject constructor(
private val profileAPI: ProfileAPI,
@SessionDatabase private val monarchy: Monarchy,
private val pendingThreePidMapper: PendingThreePidMapper,
private val eventBus: EventBus) : AddThreePidTask() {
override suspend fun execute(params: Params) {
when (params.threePid) {
is ThreePid.Email -> addEmail(params.threePid)
is ThreePid.Msisdn -> addMsisdn(params.threePid)
}
}
private suspend fun addEmail(threePid: ThreePid.Email) {
val clientSecret = UUID.randomUUID().toString()
val sendAttempt = 1
val result = executeRequest<AddEmailResponse>(eventBus) {
val body = AddEmailBody(
clientSecret = clientSecret,
email = threePid.email,
sendAttempt = sendAttempt
)
apiCall = profileAPI.addEmail(body)
}
// Store as a pending three pid
monarchy.awaitTransaction { realm ->
PendingThreePid(
threePid = threePid,
clientSecret = clientSecret,
sendAttempt = sendAttempt,
sid = result.sid,
submitUrl = null
)
.let { pendingThreePidMapper.map(it) }
.let { realm.copyToRealm(it) }
}
}
private suspend fun addMsisdn(threePid: ThreePid.Msisdn) {
val clientSecret = UUID.randomUUID().toString()
val sendAttempt = 1
// Get country code and national number from the phone number
val phoneNumber = threePid.msisdn
val phoneNumberUtil = PhoneNumberUtil.getInstance()
val parsedNumber = phoneNumberUtil.parse(phoneNumber, null)
val countryCode = parsedNumber.countryCode
val country = phoneNumberUtil.getRegionCodeForCountryCode(countryCode)
val result = executeRequest<AddMsisdnResponse>(eventBus) {
val body = AddMsisdnBody(
clientSecret = clientSecret,
country = country,
phoneNumber = parsedNumber.nationalNumber.toString(),
sendAttempt = sendAttempt
)
apiCall = profileAPI.addMsisdn(body)
}
// Store as a pending three pid
monarchy.awaitTransaction { realm ->
PendingThreePid(
threePid = threePid,
clientSecret = clientSecret,
sendAttempt = sendAttempt,
sid = result.sid,
submitUrl = result.submitUrl
)
.let { pendingThreePidMapper.map(it) }
.let { realm.copyToRealm(it) }
}
}
}

View File

@@ -28,6 +28,7 @@ import org.matrix.android.sdk.api.session.profile.ProfileService
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity
import org.matrix.android.sdk.internal.database.model.UserThreePidEntity
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.content.FileUploader
@@ -44,6 +45,11 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
private val getProfileInfoTask: GetProfileInfoTask,
private val setDisplayNameTask: SetDisplayNameTask,
private val setAvatarUrlTask: SetAvatarUrlTask,
private val addThreePidTask: AddThreePidTask,
private val validateSmsCodeTask: ValidateSmsCodeTask,
private val finalizeAddingThreePidTask: FinalizeAddingThreePidTask,
private val deleteThreePidTask: DeleteThreePidTask,
private val pendingThreePidMapper: PendingThreePidMapper,
private val fileUploader: FileUploader) : ProfileService {
override fun getDisplayName(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable {
@@ -116,9 +122,7 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
override fun getThreePidsLive(refreshData: Boolean): LiveData<List<ThreePid>> {
if (refreshData) {
// Force a refresh of the values
refreshUserThreePidsTask
.configureWith()
.executeBy(taskExecutor)
refreshThreePids()
}
return monarchy.findAllMappedWithChanges(
@@ -126,6 +130,95 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
{ it.asDomain() }
)
}
private fun refreshThreePids() {
refreshUserThreePidsTask
.configureWith()
.executeBy(taskExecutor)
}
override fun getPendingThreePids(): List<ThreePid> {
return monarchy.fetchAllMappedSync(
{ it.where<PendingThreePidEntity>() },
{ pendingThreePidMapper.map(it).threePid }
)
}
override fun getPendingThreePidsLive(): LiveData<List<ThreePid>> {
return monarchy.findAllMappedWithChanges(
{ it.where<PendingThreePidEntity>() },
{ pendingThreePidMapper.map(it).threePid }
)
}
override fun addThreePid(threePid: ThreePid, matrixCallback: MatrixCallback<Unit>): Cancelable {
return addThreePidTask
.configureWith(AddThreePidTask.Params(threePid)) {
callback = matrixCallback
}
.executeBy(taskExecutor)
}
override fun submitSmsCode(threePid: ThreePid.Msisdn, code: String, matrixCallback: MatrixCallback<Unit>): Cancelable {
return validateSmsCodeTask
.configureWith(ValidateSmsCodeTask.Params(threePid, code)) {
callback = matrixCallback
}
.executeBy(taskExecutor)
}
override fun finalizeAddingThreePid(threePid: ThreePid,
uiaSession: String?,
accountPassword: String?,
matrixCallback: MatrixCallback<Unit>): Cancelable {
return finalizeAddingThreePidTask
.configureWith(FinalizeAddingThreePidTask.Params(
threePid = threePid,
session = uiaSession,
accountPassword = accountPassword,
userWantsToCancel = false
)) {
callback = alsoRefresh(matrixCallback)
}
.executeBy(taskExecutor)
}
override fun cancelAddingThreePid(threePid: ThreePid, matrixCallback: MatrixCallback<Unit>): Cancelable {
return finalizeAddingThreePidTask
.configureWith(FinalizeAddingThreePidTask.Params(
threePid = threePid,
session = null,
accountPassword = null,
userWantsToCancel = true
)) {
callback = alsoRefresh(matrixCallback)
}
.executeBy(taskExecutor)
}
/**
* Wrap the callback to fetch 3Pids from the server in case of success
*/
private fun alsoRefresh(callback: MatrixCallback<Unit>): MatrixCallback<Unit> {
return object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
override fun onSuccess(data: Unit) {
refreshThreePids()
callback.onSuccess(data)
}
}
}
override fun deleteThreePid(threePid: ThreePid, matrixCallback: MatrixCallback<Unit>): Cancelable {
return deleteThreePidTask
.configureWith(DeleteThreePidTask.Params(threePid)) {
callback = alsoRefresh(matrixCallback)
}
.executeBy(taskExecutor)
}
}
private fun UserThreePidEntity.asDomain(): ThreePid {

View File

@@ -0,0 +1,32 @@
/*
* Copyright (c) 2020 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.profile
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class DeleteThreePidBody(
/**
* Required. The medium of the third party identifier being removed. One of: ["email", "msisdn"]
*/
@Json(name = "medium") val medium: String,
/**
* Required. The third party address being removed.
*/
@Json(name = "address") val address: String
)

View File

@@ -0,0 +1,32 @@
/*
* Copyright (c) 2020 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.profile
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class DeleteThreePidResponse(
/**
* Required. An indicator as to whether or not the homeserver was able to unbind the 3PID from
* the identity server. success indicates that the identity server has unbound the identifier
* whereas no-support indicates that the identity server refuses to support the request or the
* homeserver was not able to determine an identity server to unbind from. One of: ["no-support", "success"]
*/
@Json(name = "id_server_unbind_result")
val idServerUnbindResult: String? = null
)

View File

@@ -0,0 +1,48 @@
/*
* Copyright (c) 2020 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.profile
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.identity.toMedium
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject
internal abstract class DeleteThreePidTask : Task<DeleteThreePidTask.Params, Unit> {
data class Params(
val threePid: ThreePid
)
}
internal class DefaultDeleteThreePidTask @Inject constructor(
private val profileAPI: ProfileAPI,
private val eventBus: EventBus) : DeleteThreePidTask() {
override suspend fun execute(params: Params) {
executeRequest<DeleteThreePidResponse>(eventBus) {
val body = DeleteThreePidBody(
medium = params.threePid.toMedium(),
address = params.threePid.value
)
apiCall = profileAPI.deleteThreePid(body)
}
// We do not really care about the result for the moment
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright (c) 2020 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.profile
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
@JsonClass(generateAdapter = true)
internal data class FinalizeAddThreePidBody(
/**
* Required. The client secret used in the session with the homeserver.
*/
@Json(name = "client_secret")
val clientSecret: String,
/**
* Required. The session identifier given by the homeserver.
*/
@Json(name = "sid")
val sid: String,
/**
* Additional authentication information for the user-interactive authentication API.
*/
@Json(name = "auth")
val auth: UserPasswordAuth?
)

View File

@@ -0,0 +1,97 @@
/*
* Copyright (c) 2020 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.profile
import com.zhuinden.monarchy.Monarchy
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction
import javax.inject.Inject
internal abstract class FinalizeAddingThreePidTask : Task<FinalizeAddingThreePidTask.Params, Unit> {
data class Params(
val threePid: ThreePid,
val session: String?,
val accountPassword: String?,
val userWantsToCancel: Boolean
)
}
internal class DefaultFinalizeAddingThreePidTask @Inject constructor(
private val profileAPI: ProfileAPI,
@SessionDatabase private val monarchy: Monarchy,
private val pendingThreePidMapper: PendingThreePidMapper,
@UserId private val userId: String,
private val eventBus: EventBus) : FinalizeAddingThreePidTask() {
override suspend fun execute(params: Params) {
if (params.userWantsToCancel.not()) {
// Get the required pending data
val pendingThreePids = monarchy.fetchAllMappedSync(
{ it.where(PendingThreePidEntity::class.java) },
{ pendingThreePidMapper.map(it) }
)
.firstOrNull { it.threePid == params.threePid }
?: throw IllegalArgumentException("unknown threepid")
try {
executeRequest<Unit>(eventBus) {
val body = FinalizeAddThreePidBody(
clientSecret = pendingThreePids.clientSecret,
sid = pendingThreePids.sid,
auth = if (params.session != null && params.accountPassword != null) {
UserPasswordAuth(
session = params.session,
user = userId,
password = params.accountPassword
)
} else null
)
apiCall = profileAPI.finalizeAddThreePid(body)
}
} catch (throwable: Throwable) {
throw throwable.toRegistrationFlowResponse()
?.let { Failure.RegistrationFlowError(it) }
?: throwable
}
}
cleanupDatabase(params)
}
private suspend fun cleanupDatabase(params: Params) {
// Delete the pending three pid
monarchy.awaitTransaction { realm ->
realm.where(PendingThreePidEntity::class.java)
.equalTo(PendingThreePidEntityFields.EMAIL, params.threePid.value)
.or()
.equalTo(PendingThreePidEntityFields.MSISDN, params.threePid.value)
.findAll()
.deleteAllFromRealm()
}
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.profile
import org.matrix.android.sdk.api.session.identity.ThreePid
internal data class PendingThreePid(
val threePid: ThreePid,
val clientSecret: String,
val sendAttempt: Int,
// For Msisdn and Email
val sid: String,
// For Msisdn only
val submitUrl: String?
)

View File

@@ -0,0 +1,47 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.profile
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity
import javax.inject.Inject
internal class PendingThreePidMapper @Inject constructor() {
fun map(entity: PendingThreePidEntity): PendingThreePid {
return PendingThreePid(
threePid = entity.email?.let { ThreePid.Email(it) }
?: entity.msisdn?.let { ThreePid.Msisdn(it) }
?: error("Invalid data"),
clientSecret = entity.clientSecret,
sendAttempt = entity.sendAttempt,
sid = entity.sid,
submitUrl = entity.submitUrl
)
}
fun map(domain: PendingThreePid): PendingThreePidEntity {
return PendingThreePidEntity(
email = domain.threePid.takeIf { it is ThreePid.Email }?.value,
msisdn = domain.threePid.takeIf { it is ThreePid.Msisdn }?.value,
clientSecret = domain.clientSecret,
sendAttempt = domain.sendAttempt,
sid = domain.sid,
submitUrl = domain.submitUrl
)
}
}

View File

@@ -19,6 +19,8 @@
package org.matrix.android.sdk.internal.session.profile
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.auth.registration.SuccessResult
import org.matrix.android.sdk.internal.auth.registration.ValidationCodeBody
import org.matrix.android.sdk.internal.network.NetworkConstants
import retrofit2.Call
import retrofit2.http.Body
@@ -26,9 +28,9 @@ import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path
import retrofit2.http.Url
internal interface ProfileAPI {
/**
* Get the combined profile information for this user.
* This API may be used to fetch the user's own profile information or other users; either locally or on remote homeservers.
@@ -71,4 +73,35 @@ internal interface ProfileAPI {
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_UNSTABLE + "account/3pid/unbind")
fun unbindThreePid(@Body body: UnbindThreePidBody): Call<UnbindThreePidResponse>
/**
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-account-3pid-email-requesttoken
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid/email/requestToken")
fun addEmail(@Body body: AddEmailBody): Call<AddEmailResponse>
/**
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-account-3pid-msisdn-requesttoken
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid/msisdn/requestToken")
fun addMsisdn(@Body body: AddMsisdnBody): Call<AddMsisdnResponse>
/**
* Validate Msisdn code (same model than for Identity server API)
*/
@POST
fun validateMsisdn(@Url url: String,
@Body params: ValidationCodeBody): Call<SuccessResult>
/**
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-account-3pid-add
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid/add")
fun finalizeAddThreePid(@Body body: FinalizeAddThreePidBody): Call<Unit>
/**
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-account-3pid-delete
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid/delete")
fun deleteThreePid(@Body body: DeleteThreePidBody): Call<DeleteThreePidResponse>
}

View File

@@ -58,4 +58,16 @@ internal abstract class ProfileModule {
@Binds
abstract fun bindSetAvatarUrlTask(task: DefaultSetAvatarUrlTask): SetAvatarUrlTask
@Binds
abstract fun bindAddThreePidTask(task: DefaultAddThreePidTask): AddThreePidTask
@Binds
abstract fun bindValidateSmsCodeTask(task: DefaultValidateSmsCodeTask): ValidateSmsCodeTask
@Binds
abstract fun bindFinalizeAddingThreePidTask(task: DefaultFinalizeAddingThreePidTask): FinalizeAddingThreePidTask
@Binds
abstract fun bindDeleteThreePidTask(task: DefaultDeleteThreePidTask): DeleteThreePidTask
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2019 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.profile
import com.zhuinden.monarchy.Monarchy
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.internal.auth.registration.SuccessResult
import org.matrix.android.sdk.internal.auth.registration.ValidationCodeBody
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject
internal interface ValidateSmsCodeTask : Task<ValidateSmsCodeTask.Params, Unit> {
data class Params(
val threePid: ThreePid.Msisdn,
val code: String
)
}
internal class DefaultValidateSmsCodeTask @Inject constructor(
private val profileAPI: ProfileAPI,
@SessionDatabase
private val monarchy: Monarchy,
private val pendingThreePidMapper: PendingThreePidMapper,
private val eventBus: EventBus
) : ValidateSmsCodeTask {
override suspend fun execute(params: ValidateSmsCodeTask.Params) {
// Search the pending ThreePid
val pendingThreePids = monarchy.fetchAllMappedSync(
{ it.where(PendingThreePidEntity::class.java) },
{ pendingThreePidMapper.map(it) }
)
.firstOrNull { it.threePid == params.threePid }
?: throw IllegalArgumentException("unknown threepid")
val url = pendingThreePids.submitUrl ?: throw IllegalArgumentException("invalid threepid")
val body = ValidationCodeBody(
clientSecret = pendingThreePids.clientSecret,
sid = pendingThreePids.sid,
code = params.code
)
val result = executeRequest<SuccessResult>(eventBus) {
apiCall = profileAPI.validateMsisdn(url, body)
}
if (!result.isSuccess()) {
throw Failure.SuccessError
}
}
}

View File

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

View File

@@ -130,21 +130,6 @@ internal interface RoomAPI {
@Body content: Content?
): Call<SendResponse>
/**
* Send an event to a room.
*
* @param txId the transaction Id
* @param roomId the room id
* @param eventType the event type
* @param content the event content as string
*/
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/send/{eventType}/{txId}")
fun send(@Path("txId") txId: String,
@Path("roomId") roomId: String,
@Path("eventType") eventType: String,
@Body content: String?
): Call<SendResponse>
/**
* Get the context surrounding an event.
*
@@ -235,9 +220,9 @@ internal interface RoomAPI {
*/
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/send_relation/{parent_id}/{relation_type}/{event_type}")
fun sendRelation(@Path("roomId") roomId: String,
@Path("parentId") parent_id: String,
@Path("parent_id") parentId: String,
@Path("relation_type") relationType: String,
@Path("eventType") eventType: String,
@Path("event_type") eventType: String,
@Body content: Content?
): Call<SendResponse>
@@ -311,16 +296,16 @@ internal interface RoomAPI {
* This cannot be undone.
* Users may redact their own events, and any user with a power level greater than or equal to the redact power level of the room may redact events there.
*
* @param txId the transaction Id
* @param roomId the room id
* @param eventId the event to delete
* @param txId the transaction Id
* @param roomId the room id
* @param eventId the event to delete
* @param reason json containing reason key {"reason": "Indecent material"}
*/
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/redact/{eventId}/{txnId}")
fun redactEvent(
@Path("txnId") txId: String,
@Path("roomId") roomId: String,
@Path("eventId") parent_id: String,
@Path("eventId") eventId: String,
@Body reason: Map<String, String>
): Call<SendResponse>

View File

@@ -20,6 +20,8 @@ package org.matrix.android.sdk.internal.session.room
import dagger.Binds
import dagger.Module
import dagger.Provides
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
import org.matrix.android.sdk.api.session.file.FileService
import org.matrix.android.sdk.api.session.room.RoomDirectoryService
import org.matrix.android.sdk.api.session.room.RoomService
@@ -75,9 +77,6 @@ import org.matrix.android.sdk.internal.session.room.typing.DefaultSendTypingTask
import org.matrix.android.sdk.internal.session.room.typing.SendTypingTask
import org.matrix.android.sdk.internal.session.room.uploads.DefaultGetUploadsTask
import org.matrix.android.sdk.internal.session.room.uploads.GetUploadsTask
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
import org.commonmark.renderer.text.TextContentRenderer
import retrofit2.Retrofit
@Module
@@ -105,14 +104,6 @@ internal abstract class RoomModule {
.builder()
.build()
}
@Provides
@JvmStatic
fun providesTextContentRenderer(): TextContentRenderer {
return TextContentRenderer
.builder()
.build()
}
}
@Binds

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,62 @@
/*
* Copyright (c) 2020 New Vector Ltd
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.room.send
import org.matrix.android.sdk.internal.session.SessionScope
import javax.inject.Inject
/**
* We cannot use work manager cancellation mechanism because cancelling a work will just ignore
* any follow up send that was already queued.
* We use this class to track cancel requests, the workers will look for this to check for cancellation request
* and just ignore the work request and continue by returning success.
*
* Known limitation, for now requests are not persisted
*/
@SessionScope
internal class CancelSendTracker @Inject constructor() {
data class Request(
val localId: String,
val roomId: String
)
private val cancellingRequests = ArrayList<Request>()
fun markLocalEchoForCancel(eventId: String, roomId: String) {
synchronized(cancellingRequests) {
cancellingRequests.add(Request(eventId, roomId))
}
}
fun isCancelRequestedFor(eventId: String?, roomId: String?): Boolean {
val index = synchronized(cancellingRequests) {
cancellingRequests.indexOfFirst { it.localId == eventId && it.roomId == roomId }
}
return index != -1
}
fun markCancelled(eventId: String, roomId: String) {
synchronized(cancellingRequests) {
val index = cancellingRequests.indexOfFirst { it.localId == eventId && it.roomId == roomId }
if (index != -1) {
cancellingRequests.removeAt(index)
}
}
}
}

View File

@@ -17,24 +17,35 @@
package org.matrix.android.sdk.internal.session.room.send
import android.net.Uri
import androidx.work.BackoffPolicy
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.Operation
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import org.matrix.android.sdk.api.session.crypto.CryptoService
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.isImageMessage
import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage
import org.matrix.android.sdk.api.session.events.model.isTextMessage
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent
import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent
import org.matrix.android.sdk.api.session.room.model.message.OptionItem
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
import org.matrix.android.sdk.api.session.room.send.SendService
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.CancelableBag
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.api.util.NoOpCancellable
import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.di.WorkManagerProvider
import org.matrix.android.sdk.internal.session.content.UploadContentWorker
@@ -44,7 +55,6 @@ import org.matrix.android.sdk.internal.util.CancelableWork
import org.matrix.android.sdk.internal.worker.AlwaysSuccessfulWorker
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import org.matrix.android.sdk.internal.worker.startChain
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
@@ -60,7 +70,8 @@ internal class DefaultSendService @AssistedInject constructor(
private val cryptoService: CryptoService,
private val taskExecutor: TaskExecutor,
private val localEchoRepository: LocalEchoRepository,
private val roomEventSender: RoomEventSender
private val roomEventSender: RoomEventSender,
private val cancelSendTracker: CancelSendTracker
) : SendService {
@AssistedInject.Factory
@@ -127,48 +138,83 @@ internal class DefaultSendService @AssistedInject constructor(
.let { timelineSendEventWorkCommon.postWork(roomId, it) }
}
override fun resendTextMessage(localEcho: TimelineEvent): Cancelable? {
override fun resendTextMessage(localEcho: TimelineEvent): Cancelable {
if (localEcho.root.isTextMessage() && localEcho.root.sendState.hasFailed()) {
localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
return sendEvent(localEcho.root)
}
return null
return NoOpCancellable
}
override fun resendMediaMessage(localEcho: TimelineEvent): Cancelable? {
if (localEcho.root.isImageMessage() && localEcho.root.sendState.hasFailed()) {
// TODO this need a refactoring of attachement sending
// val clearContent = localEcho.root.getClearContent()
// val messageContent = clearContent?.toModel<MessageContent>() ?: return null
// when (messageContent.type) {
// MessageType.MSGTYPE_IMAGE -> {
// val imageContent = clearContent.toModel<MessageImageContent>() ?: return null
// val url = imageContent.url ?: return null
// if (url.startsWith("mxc://")) {
// //TODO
// } else {
// //The image has not yet been sent
// val attachmentData = ContentAttachmentData(
// size = imageContent.info!!.size.toLong(),
// mimeType = imageContent.info.mimeType!!,
// width = imageContent.info.width.toLong(),
// height = imageContent.info.height.toLong(),
// name = imageContent.body,
// path = imageContent.url,
// type = ContentAttachmentData.Type.IMAGE
// )
// monarchy.runTransactionSync {
// EventEntity.where(it,eventId = localEcho.root.eventId ?: "").findFirst()?.let {
// it.sendState = SendState.UNSENT
// }
// }
// return internalSendMedia(localEcho.root,attachmentData)
// }
// }
// }
return null
override fun resendMediaMessage(localEcho: TimelineEvent): Cancelable {
if (localEcho.root.sendState.hasFailed()) {
val clearContent = localEcho.root.getClearContent()
val messageContent = clearContent?.toModel<MessageContent>() as? MessageWithAttachmentContent ?: return NoOpCancellable
val url = messageContent.getFileUrl() ?: return NoOpCancellable
if (url.startsWith("mxc://")) {
// We need to resend only the message as the attachment is ok
localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
return sendEvent(localEcho.root)
}
// we need to resend the media
return when (messageContent) {
is MessageImageContent -> {
// The image has not yet been sent
val attachmentData = ContentAttachmentData(
size = messageContent.info!!.size.toLong(),
mimeType = messageContent.info.mimeType!!,
width = messageContent.info.width.toLong(),
height = messageContent.info.height.toLong(),
name = messageContent.body,
queryUri = Uri.parse(messageContent.url),
type = ContentAttachmentData.Type.IMAGE
)
localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
internalSendMedia(listOf(localEcho.root), attachmentData, true)
}
is MessageVideoContent -> {
val attachmentData = ContentAttachmentData(
size = messageContent.videoInfo?.size ?: 0L,
mimeType = messageContent.mimeType,
width = messageContent.videoInfo?.width?.toLong(),
height = messageContent.videoInfo?.height?.toLong(),
duration = messageContent.videoInfo?.duration?.toLong(),
name = messageContent.body,
queryUri = Uri.parse(messageContent.url),
type = ContentAttachmentData.Type.VIDEO
)
localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
internalSendMedia(listOf(localEcho.root), attachmentData, true)
}
is MessageFileContent -> {
val attachmentData = ContentAttachmentData(
size = messageContent.info!!.size,
mimeType = messageContent.info.mimeType!!,
name = messageContent.body,
queryUri = Uri.parse(messageContent.url),
type = ContentAttachmentData.Type.FILE
)
localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
internalSendMedia(listOf(localEcho.root), attachmentData, true)
}
is MessageAudioContent -> {
val attachmentData = ContentAttachmentData(
size = messageContent.audioInfo?.size ?: 0,
duration = messageContent.audioInfo?.duration?.toLong() ?: 0L,
mimeType = messageContent.audioInfo?.mimeType,
name = messageContent.body,
queryUri = Uri.parse(messageContent.url),
type = ContentAttachmentData.Type.AUDIO
)
localEchoRepository.updateSendState(localEcho.eventId, SendState.UNSENT)
internalSendMedia(listOf(localEcho.root), attachmentData, true)
}
else -> NoOpCancellable
}
}
return null
return NoOpCancellable
}
override fun deleteFailedEcho(localEcho: TimelineEvent) {
@@ -196,16 +242,34 @@ internal class DefaultSendService @AssistedInject constructor(
}
}
override fun cancelSend(eventId: String) {
cancelSendTracker.markLocalEchoForCancel(eventId, roomId)
taskExecutor.executorScope.launch {
localEchoRepository.deleteFailedEcho(roomId, eventId)
}
}
override fun resendAllFailedMessages() {
taskExecutor.executorScope.launch {
val eventsToResend = localEchoRepository.getAllFailedEventsToResend(roomId)
eventsToResend.forEach {
sendEvent(it)
if (it.root.isTextMessage()) {
resendTextMessage(it)
} else if (it.root.isAttachmentMessage()) {
resendMediaMessage(it)
}
}
localEchoRepository.updateSendState(roomId, eventsToResend.mapNotNull { it.eventId }, SendState.UNSENT)
localEchoRepository.updateSendState(roomId, eventsToResend.map { it.eventId }, SendState.UNSENT)
}
}
// override fun failAllPendingMessages() {
// taskExecutor.executorScope.launch {
// val eventsToResend = localEchoRepository.getAllEventsWithStates(roomId, SendState.PENDING_STATES)
// localEchoRepository.updateSendState(roomId, eventsToResend.map { it.eventId }, SendState.UNDELIVERED)
// }
// }
override fun sendMedia(attachment: ContentAttachmentData,
compressBeforeSending: Boolean,
roomIds: Set<String>): Cancelable {

View File

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

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