mirror of
https://github.com/vector-im/riotX-android
synced 2025-10-06 00:02:48 +02:00
Compare commits
310 Commits
new_notifi
...
v1.0.1
Author | SHA1 | Date | |
---|---|---|---|
|
cc4298209b | ||
|
e3f5b7cab3 | ||
|
dde9cdd8ac | ||
|
0d0308d584 | ||
|
a47ff99be7 | ||
|
e619217687 | ||
|
6f5b3371fe | ||
|
f7b9fc3bb1 | ||
|
700a2e9ce3 | ||
|
5176a3e2aa | ||
|
4631cd0e80 | ||
|
60c6512418 | ||
|
12bc821ef3 | ||
|
c82e910c38 | ||
|
7ae0620b20 | ||
|
de32cdb703 | ||
|
12e2a8ffc8 | ||
|
833f64fcec | ||
|
73c051d2b1 | ||
|
da06695ab7 | ||
|
984ffc4fb9 | ||
|
d7b44ba0cf | ||
|
e90fe5cf28 | ||
|
20c7d80bb1 | ||
|
25f8a9418a | ||
|
21b609420b | ||
|
794b89c041 | ||
|
407595e613 | ||
|
cac8099117 | ||
|
4cc3e87d64 | ||
|
3e429490e7 | ||
|
e23b02f2e2 | ||
|
aa5ae45a0c | ||
|
d7558902f7 | ||
|
aa5de1896f | ||
|
e5596f6a97 | ||
|
d638306b04 | ||
|
dba9356472 | ||
|
fb247f8bea | ||
|
c880e2b848 | ||
|
1436477a14 | ||
|
35fe4f7268 | ||
|
a488e88f93 | ||
|
0d51c160eb | ||
|
27207a27ae | ||
|
5082defddf | ||
|
6e0095edb0 | ||
|
d78cd81c6f | ||
|
ef93ef57f9 | ||
|
a600e896a0 | ||
|
993c340c4f | ||
|
87f506f23b | ||
|
44fb53ce6b | ||
|
1d2e374526 | ||
|
5c423d2897 | ||
|
6d0a1ee824 | ||
|
978ab76644 | ||
|
da4e3f3b9c | ||
|
f6d2d05e70 | ||
|
d747d96e77 | ||
|
484b69165b | ||
|
0159e6dc00 | ||
|
8f7aae4980 | ||
|
0f9e26efc5 | ||
|
53f0b77fb0 | ||
|
fca669ff98 | ||
|
e910fc60fe | ||
|
1d2e62fed7 | ||
|
7651eb5c3c | ||
|
a06484c260 | ||
|
9b49e6a817 | ||
|
bef0c7725b | ||
|
7237ec7310 | ||
|
3abe5b8467 | ||
|
0406854fa5 | ||
|
ca6a398a72 | ||
|
5e81ce8e3e | ||
|
8658090736 | ||
|
614ac88567 | ||
|
797dcdb48b | ||
|
3a696ea9a1 | ||
|
1b19cb7449 | ||
|
ea81a5298b | ||
|
ae65eb6a3e | ||
|
67e07a7932 | ||
|
6ef15f3c1c | ||
|
0e3be83da2 | ||
|
8b5b7db241 | ||
|
1fd8a5fa91 | ||
|
34d7a86175 | ||
|
21f1f89a4b | ||
|
159f286b2c | ||
|
06454cc3e0 | ||
|
be27b580d6 | ||
|
f91b6938f5 | ||
|
a7fea8d012 | ||
|
18bc40fb66 | ||
|
1dec4bc96b | ||
|
0e28214b63 | ||
|
ca61751a8b | ||
|
cce1c2252d | ||
|
6a4d887941 | ||
|
4de1759321 | ||
|
139cd051ab | ||
|
33b2abc3b9 | ||
|
c63128cfbd | ||
|
5063188b25 | ||
|
b9f0c176d9 | ||
|
d09c03bff3 | ||
|
f444595845 | ||
|
eedf545409 | ||
|
aba8a3fed1 | ||
|
68d475dc55 | ||
|
ece9fbd3bb | ||
|
602d67155f | ||
|
75ef491e3e | ||
|
0f327fc75f | ||
|
a456f4c6a5 | ||
|
e097bd8117 | ||
|
ded8acc836 | ||
|
d8a0a1d38e | ||
|
e8f28d7ce4 | ||
|
a58bb776f3 | ||
|
4ba1a34f38 | ||
|
3d68b15e60 | ||
|
c78bba803c | ||
|
863c09142f | ||
|
3142442e5c | ||
|
4c1d50d554 | ||
|
25e7bbcd79 | ||
|
4b3a6a883d | ||
|
6c0bb2a949 | ||
|
f714566200 | ||
|
327a596de5 | ||
|
cc4603b61f | ||
|
f51568b331 | ||
|
6ceac578a3 | ||
|
1c733e6661 | ||
|
3842ec6bb0 | ||
|
ab1d652f17 | ||
|
70e90d8542 | ||
|
39e185576c | ||
|
9c402d4d40 | ||
|
37378ca5a6 | ||
|
a35749964c | ||
|
bbbd45efcd | ||
|
246f6bb0d0 | ||
|
a08a1d4f74 | ||
|
7acbd42a45 | ||
|
c6a5d05ffb | ||
|
36b17e9b8c | ||
|
c2cccd8b11 | ||
|
e7804af2f7 | ||
|
0412fabbd2 | ||
|
22959cddb2 | ||
|
7193db8344 | ||
|
352662d19a | ||
|
1afabb69c1 | ||
|
6f4ea83fa9 | ||
|
1c17bd9f5a | ||
|
de5f182f29 | ||
|
aa1843774a | ||
|
9e1c30ec5d | ||
|
31984a57d6 | ||
|
2f0645a94e | ||
|
c57d41863f | ||
|
25bbe9c3d6 | ||
|
c5c3592a4c | ||
|
5a8008a4dc | ||
|
2c5d2ea179 | ||
|
4387fd3327 | ||
|
1f2d5b0d00 | ||
|
253582219c | ||
|
3fc9fe3017 | ||
|
e07a584d66 | ||
|
150d44aafd | ||
|
179474b975 | ||
|
825e21362b | ||
|
0bc51b2ed8 | ||
|
4741169cc7 | ||
|
a8ad57a9b0 | ||
|
8582ad6015 | ||
|
51898a8109 | ||
|
d63f00851a | ||
|
f179fc523d | ||
|
eda29e3fef | ||
|
633548f190 | ||
|
811cbb2e20 | ||
|
6569ee5d10 | ||
|
5f60d7fd3b | ||
|
10f8aebde2 | ||
|
ea771476cc | ||
|
08bc487f17 | ||
|
1b6b71ed98 | ||
|
9f2631110e | ||
|
e8b1e418fa | ||
|
44563e73e2 | ||
|
d3595173b4 | ||
|
14d4b34cee | ||
|
538149233b | ||
|
bcb203f8e0 | ||
|
3c6937ff5a | ||
|
6c0f775c4b | ||
|
ea3e467dc4 | ||
|
5a65eddf59 | ||
|
e979bee920 | ||
|
eff08955f1 | ||
|
9310073c07 | ||
|
28869f4382 | ||
|
cd949e9d38 | ||
|
541e1fc4cc | ||
|
93fe00a299 | ||
|
8814364497 | ||
|
0d9ff4bde3 | ||
|
9c595b6c02 | ||
|
d49d0295a2 | ||
|
da7c971927 | ||
|
548879bd9f | ||
|
0c2516ccf8 | ||
|
332f227bc1 | ||
|
a98b2ecce3 | ||
|
195e2703b9 | ||
|
c1f1620624 | ||
|
f5284e8447 | ||
|
347cf08861 | ||
|
540317639a | ||
|
bdcd96544e | ||
|
8237c949f1 | ||
|
e38cb7c1a6 | ||
|
868d9cf55c | ||
|
aa3e68f3fd | ||
|
bf2d937ad6 | ||
|
e24d5b3ca4 | ||
|
e9778d6feb | ||
|
8c4c909f44 | ||
|
a1db8653ab | ||
|
cc5df1e1d5 | ||
|
87b1394e98 | ||
|
e3c2af2c59 | ||
|
a618a9214e | ||
|
76133ab55e | ||
|
2d4a728af4 | ||
|
4a2a6d34ae | ||
|
75c2dfcd48 | ||
|
6ebedaf540 | ||
|
85e8e652f1 | ||
|
f025811025 | ||
|
3aabb17ea5 | ||
|
f1e5129acb | ||
|
e8dbed1642 | ||
|
6d270dc5f4 | ||
|
2afe642e8b | ||
|
680e62cb98 | ||
|
c5ba746904 | ||
|
702711fc5e | ||
|
0855806ae2 | ||
|
a2c75e7c71 | ||
|
6f996f1f09 | ||
|
ac7a929a1a | ||
|
8313e45737 | ||
|
09ca2361d7 | ||
|
bcfd322b85 | ||
|
9dc831d8e5 | ||
|
9514835232 | ||
|
e93a2d7c5d | ||
|
b2f6476f78 | ||
|
b7d86c3fa4 | ||
|
89506b9e81 | ||
|
51abdb6066 | ||
|
9e60f73bcf | ||
|
1e6d98a121 | ||
|
98d56cb556 | ||
|
8b1a07b8a8 | ||
|
92e809fa6d | ||
|
a0998e4aff | ||
|
804d712848 | ||
|
08cda2ee10 | ||
|
bf03b367f1 | ||
|
c1da4aecd7 | ||
|
38c54e0f2c | ||
|
9ebf87df62 | ||
|
32d2cea7f8 | ||
|
f998cb6b18 | ||
|
9d4e903c4a | ||
|
cfdf5cb552 | ||
|
e859357c6a | ||
|
e7f13c9efe | ||
|
2a68c8d08b | ||
|
b1c088a03b | ||
|
04f0146afd | ||
|
b853397c0a | ||
|
0cfd33fc8b | ||
|
7434aed43f | ||
|
283f32479d | ||
|
f4057ea3fa | ||
|
a7480c1860 | ||
|
3648d6292a | ||
|
332041e13b | ||
|
2f6b38eb39 | ||
|
7b075f138d | ||
|
74a3d7619b | ||
|
fc88892ee6 | ||
|
7aa9f88ceb | ||
|
3db26bcae1 | ||
|
f0dbb92d76 | ||
|
08710978c5 | ||
|
cba9109206 | ||
|
482621e86f | ||
|
8c98125755 | ||
|
997101a44b |
@@ -28,8 +28,8 @@ Even if we try to be able to work on all the functionalities, we have more knowl
|
||||
|
||||
# Other contributors
|
||||
|
||||
First of all, we thank all contributors who use RiotX and report problems on this GitHub project or via the integrated rageshake function.
|
||||
First of all, we thank all contributors who use Element and report problems on this GitHub project or via the integrated rageshake function.
|
||||
|
||||
We do not forget all translators, for their work of translating RiotX into many languages. They are also the authors of RiotX.
|
||||
We do not forget all translators, for their work of translating Element into many languages. They are also the authors of Element.
|
||||
|
||||
Feel free to add your name below, when you contribute to the project!
|
||||
|
95
CHANGES.md
95
CHANGES.md
@@ -1,11 +1,85 @@
|
||||
Changes in Riot.imX 0.91.4 (2020-XX-XX)
|
||||
Changes in Element 1.0.1 (2020-07-28)
|
||||
===================================================
|
||||
|
||||
Improvements 🙌:
|
||||
- Sending events is now retried only 3 times, so we avoid blocking the sending queue too long.
|
||||
- Display warning when fail to send events in room list
|
||||
- Improve UI of edit role action in member profile
|
||||
- Moderation | New screen to display list of banned users in room settings, with unban action
|
||||
|
||||
Bugfix 🐛:
|
||||
- Fix theme issue on Room directory screen (#1613)
|
||||
- Fix notification not dismissing when entering a room
|
||||
- Fix uploads don't work with Room v6 (#1558)
|
||||
- Fix Requesting avatar thumbnails in Element uses wrong http "user-agent" string (#1725)
|
||||
- Fix 404 on EMS (#1761)
|
||||
- Fix Infinite loop at startup when migrating account from Riot (#1699)
|
||||
- Fix Element crashes in loop after initial sync (#1709)
|
||||
- Remove inner mx-reply tags before replying
|
||||
- Fix timeline items not loading when there are only filtered events
|
||||
- Fix "Voice & Video" grayed out in Settings (#1733)
|
||||
- Fix Allow VOIP call in all rooms with 2 participants (even if not DM)
|
||||
- Migration from old client does not enable notifications (#1723)
|
||||
|
||||
Other changes:
|
||||
- i18n deactivated account error
|
||||
|
||||
Changes in Element 1.0.0 (2020-07-15)
|
||||
===================================================
|
||||
|
||||
Features ✨:
|
||||
-
|
||||
- Re-branding: The app is now called Element. New name, new themes, new icons, etc. More details here: https://element.io/blog/welcome-to-element/ (#1691)
|
||||
|
||||
Bugfix 🐛:
|
||||
- Video calls are shown as a voice ones in the timeline (#1676)
|
||||
- Fix regression: not able to create a room without IS configured (#1679)
|
||||
|
||||
Changes in Riot.imX 0.91.5 (2020-07-11)
|
||||
===================================================
|
||||
|
||||
Features ✨:
|
||||
- 3pid invite: it is now possible to invite people by email. An Identity Server has to be configured (#548)
|
||||
|
||||
Improvements 🙌:
|
||||
-
|
||||
- Cleaning chunks with lots of events as long as a threshold has been exceeded (35_000 events in DB) (#1634)
|
||||
- Creating and listening to EventInsertEntity. (#1634)
|
||||
- Handling (almost) properly the groups fetching (#1634)
|
||||
- Improve fullscreen media display (#327)
|
||||
- Setup server recovery banner (#1648)
|
||||
- Set up SSSS from security settings (#1567)
|
||||
- New lab setting to add 'unread notifications' tab to main screen
|
||||
- Render third party invite event (#548)
|
||||
- Display three pid invites in the room members list (#548)
|
||||
|
||||
Bugfix 🐛:
|
||||
- Integration Manager: Wrong URL to review terms if URL in config contains path (#1606)
|
||||
- Regression Composer does not grow, crops out text (#1650)
|
||||
- Bug / Unwanted draft (#698)
|
||||
- All users seems to be able to see the enable encryption option in room settings (#1341)
|
||||
- Leave room only leaves the current version (#1656)
|
||||
- Regression | Share action menu do not work (#1647)
|
||||
- verification issues on transition (#1555)
|
||||
- Fix issue when restoring keys backup using recovery key
|
||||
|
||||
SDK API changes ⚠️:
|
||||
- CreateRoomParams has been updated
|
||||
|
||||
Build 🧱:
|
||||
- Upgrade some dependencies
|
||||
- Revert to build-tools 3.5.3
|
||||
|
||||
Other changes:
|
||||
- Use Intent.ACTION_CREATE_DOCUMENT to save megolm key or recovery key in a txt file
|
||||
- Use `Context#withStyledAttributes` extension function (#1546)
|
||||
|
||||
Changes in Riot.imX 0.91.4 (2020-07-06)
|
||||
===================================================
|
||||
|
||||
Features ✨:
|
||||
- Re-activate Wellknown support with updated UI (#1614)
|
||||
|
||||
Improvements 🙌:
|
||||
- Upload device keys only once to the homeserver and fix crash when no network (#1629)
|
||||
|
||||
Bugfix 🐛:
|
||||
- Fix crash when coming from a notification (#1601)
|
||||
@@ -14,20 +88,11 @@ Bugfix 🐛:
|
||||
- saved images don't show up in gallery (#1324)
|
||||
- Fix reply fallback leaking sender locale (#429)
|
||||
|
||||
Translations 🗣:
|
||||
-
|
||||
|
||||
SDK API changes ⚠️:
|
||||
-
|
||||
|
||||
Build 🧱:
|
||||
- Fix lint false-positive about WorkManger (#1012)
|
||||
- Upgrade build-tools from 3.5.3 to 3.6.6
|
||||
- Fix lint false-positive about WorkManager (#1012)
|
||||
- Upgrade build-tools from 3.5.3 to 3.6.3
|
||||
- Upgrade gradle from 5.4.1 to 5.6.4
|
||||
|
||||
Other changes:
|
||||
-
|
||||
|
||||
Changes in Riot.imX 0.91.3 (2020-07-01)
|
||||
===================================================
|
||||
|
||||
@@ -667,7 +732,7 @@ Mode details here: https://medium.com/@RiotChat/introducing-the-riotx-beta-for-a
|
||||
=======================================================
|
||||
|
||||
|
||||
Changes in RiotX 0.X.0 (2020-XX-XX)
|
||||
Changes in Element 1.X.X (2020-XX-XX)
|
||||
===================================================
|
||||
|
||||
Features ✨:
|
||||
|
@@ -2,9 +2,7 @@
|
||||
|
||||
Please read https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.md
|
||||
|
||||
Android support can be found in this [](https://matrix.to/#/#riot-android:matrix.org) room.
|
||||
|
||||
Dedicated room for RiotX: [](https://matrix.to/#/#riotx:matrix.org)
|
||||
Android support can be found in this [](https://matrix.to/#/#element-android:matrix.org) room.
|
||||
|
||||
# Specific rules for Matrix Android projects
|
||||
|
||||
@@ -37,15 +35,13 @@ Note that if the templates are modified, the only things to do is to restart And
|
||||
|
||||
## Compilation
|
||||
|
||||
For now, the Matrix SDK and the RiotX application are in the same project. So there is no specific thing to do, this project should compile without any special action.
|
||||
For now, the Matrix SDK and the Element application are in the same project. So there is no specific thing to do, this project should compile without any special action.
|
||||
|
||||
## I want to help translating RiotX
|
||||
## I want to help translating Element
|
||||
|
||||
If you want to fix an issue with an English string, please submit a PR.
|
||||
If you want to fix an issue in other languages, or add a missing translation, or even add a new language, please use [Weblate](https://translate.riot.im/projects/riot-android/).
|
||||
|
||||
For the moment, Strings from Riot will be used, there is no dedicated project in Weblate for RiotX.
|
||||
|
||||
## I want to submit a PR to fix an issue
|
||||
|
||||
Please check if a corresponding issue exists. If yes, please let us know in a comment that you're working on it.
|
||||
@@ -101,7 +97,7 @@ Make sure the following commands execute without any error:
|
||||
|
||||
### Tests
|
||||
|
||||
RiotX is currently supported on Android KitKat (API 19+): please test your change on an Android device (or Android emulator) running with API 19. Many issues can happen (including crashes) on older devices.
|
||||
Element is currently supported on Android Lollipop (API 21+): please test your change on an Android device (or Android emulator) running with API 21. Many issues can happen (including crashes) on older devices.
|
||||
Also, if possible, please test your change on a real device. Testing on Android emulator may not be sufficient.
|
||||
|
||||
You should consider adding Unit tests with your PR, and also integration tests (AndroidTest). Please refer to [this document](./docs/integration_tests.md) to install and run the integration test environment.
|
||||
@@ -120,7 +116,7 @@ Please consider accessibility as an important point. As a minimum requirement, i
|
||||
When adding or editing layouts, make sure the layout will render correctly if device uses a RTL (Right To Left) language.
|
||||
You can check this in the layout editor preview by selecting any RTL language (ex: Arabic).
|
||||
|
||||
Also please check that the colors are ok for all the current themes of RiotX. Please use `?attr` instead of `@color` to reference colors in the layout. You can check this in the layout editor preview by selecting all the main themes (`AppTheme.Status`, `AppTheme.Dark`, etc.).
|
||||
Also please check that the colors are ok for all the current themes of Element. Please use `?attr` instead of `@color` to reference colors in the layout. You can check this in the layout editor preview by selecting all the main themes (`AppTheme.Status`, `AppTheme.Dark`, etc.).
|
||||
|
||||
### Authors
|
||||
|
||||
|
26
README.md
26
README.md
@@ -1,38 +1,32 @@
|
||||
[](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop)
|
||||
[](https://translate.riot.im/engage/riot-android/?utm_source=widget)
|
||||
[](https://matrix.to/#/#riotx:matrix.org)
|
||||
[](https://matrix.to/#/#element-android:matrix.org)
|
||||
[](https://sonarcloud.io/dashboard?id=vector.android.riotx)
|
||||
[](https://sonarcloud.io/dashboard?id=vector.android.riotx)
|
||||
[](https://sonarcloud.io/dashboard?id=vector.android.riotx)
|
||||
|
||||
# RiotX Android
|
||||
# Element Android
|
||||
|
||||
RiotX is an Android Matrix Client currently in beta but in active development.
|
||||
Element Android is an Android Matrix Client provided by [Element](https://element.io/).
|
||||
|
||||
It is a total rewrite of [Riot-Android](https://github.com/vector-im/riot-android) with a new user experience. RiotX will become the official replacement as soon as all features are implemented.
|
||||
It is a total rewrite of [Riot-Android](https://github.com/vector-im/riot-android) with a new user experience.
|
||||
|
||||
[<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.riotx)
|
||||
[<img src="resources/img/f-droid-badge.png" alt="Get it on F-Droid" height="60">](https://f-droid.org/app/im.vector.riotx)
|
||||
[<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: [](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop)
|
||||
|
||||
# New Android SDK
|
||||
|
||||
RiotX is based on a new Android SDK fully written in Kotlin (like RiotX). In order to make the early development as fast as possible, RiotX and the new SDK currently share the same git repository. We will make separate repos once the 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. We will make separate repos once the SDK is stable enough.
|
||||
|
||||
# Roadmap
|
||||
|
||||
The current target is to release an application out of beta with the same level of features (and even more) as Riot.
|
||||
The roadmap has 3 phases:
|
||||
|
||||
- [phase 0](https://github.com/vector-im/riotX-android/labels/phase0): Prototyping / Project setup
|
||||
- [phase 1](https://github.com/vector-im/riotX-android/labels/phase1): Beta release to the Play Store
|
||||
- [phase 2](https://github.com/vector-im/riotX-android/labels/phase2): Out of beta
|
||||
|
||||
The version 1.0.0 of Element still misses some features which was previously included in Riot-Android.
|
||||
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!
|
||||
|
||||
Come chat with the community in the dedicated Matrix [room](https://matrix.to/#/#riotx:matrix.org).
|
||||
Come chat with the community in the dedicated Matrix [room](https://matrix.to/#/#element-android:matrix.org).
|
||||
|
1
attachment-viewer/.gitignore
vendored
Normal file
1
attachment-viewer/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
78
attachment-viewer/build.gradle
Normal file
78
attachment-viewer/build.gradle
Normal file
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
maven {
|
||||
url 'https://jitpack.io'
|
||||
content {
|
||||
// PhotoView
|
||||
includeGroupByRegex 'com\\.github\\.chrisbanes'
|
||||
}
|
||||
}
|
||||
jcenter()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.0.0'
|
||||
|
||||
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
|
||||
|
||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation 'androidx.core:core-ktx:1.3.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'com.google.android.material:material:1.1.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.1.0'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.1.0'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
|
||||
}
|
0
attachment-viewer/consumer-rules.pro
Normal file
0
attachment-viewer/consumer-rules.pro
Normal file
21
attachment-viewer/proguard-rules.pro
vendored
Normal file
21
attachment-viewer/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
2
attachment-viewer/src/main/AndroidManifest.xml
Normal file
2
attachment-viewer/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="im.vector.riotx.attachmentviewer" />
|
@@ -14,14 +14,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.session.sync.model.accountdata
|
||||
package im.vector.riotx.attachmentviewer
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.internal.session.integrationmanager.IntegrationProvisioningContent
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class UserAccountDataIntegrationProvisioning(
|
||||
@Json(name = "type") override val type: String = TYPE_INTEGRATION_PROVISIONING,
|
||||
@Json(name = "content") val content: IntegrationProvisioningContent
|
||||
) : UserAccountData()
|
||||
class AnimatedImageViewHolder constructor(itemView: View) :
|
||||
BaseViewHolder(itemView) {
|
||||
|
||||
val touchImageView: ImageView = itemView.findViewById(R.id.imageView)
|
||||
val imageLoaderProgress: ProgressBar = itemView.findViewById(R.id.imageLoaderProgress)
|
||||
|
||||
internal val target = DefaultImageLoaderTarget(this, this.touchImageView)
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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 im.vector.riotx.attachmentviewer
|
||||
|
||||
sealed class AttachmentEvents {
|
||||
data class VideoEvent(val isPlaying: Boolean, val progress: Int, val duration: Int) : AttachmentEvents()
|
||||
}
|
||||
|
||||
interface AttachmentEventListener {
|
||||
fun onEvent(event: AttachmentEvents)
|
||||
}
|
||||
|
||||
sealed class AttachmentCommands {
|
||||
object PauseVideo : AttachmentCommands()
|
||||
object StartVideo : AttachmentCommands()
|
||||
data class SeekTo(val percentProgress: Int) : AttachmentCommands()
|
||||
}
|
@@ -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 im.vector.riotx.attachmentviewer
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
|
||||
sealed class AttachmentInfo(open val uid: String) {
|
||||
data class Image(override val uid: String, val url: String, val data: Any?) : AttachmentInfo(uid)
|
||||
data class AnimatedImage(override val uid: String, val url: String, val data: Any?) : AttachmentInfo(uid)
|
||||
data class Video(override val uid: String, val url: String, val data: Any, val thumbnail: Image?) : AttachmentInfo(uid)
|
||||
// data class Audio(override val uid: String, val url: String, val data: Any) : AttachmentInfo(uid)
|
||||
// data class File(override val uid: String, val url: String, val data: Any) : AttachmentInfo(uid)
|
||||
}
|
||||
|
||||
interface AttachmentSourceProvider {
|
||||
|
||||
fun getItemCount(): Int
|
||||
|
||||
fun getAttachmentInfoAt(position: Int): AttachmentInfo
|
||||
|
||||
fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.Image)
|
||||
|
||||
fun loadImage(target: ImageLoaderTarget, info: AttachmentInfo.AnimatedImage)
|
||||
|
||||
fun loadVideo(target: VideoLoaderTarget, info: AttachmentInfo.Video)
|
||||
|
||||
fun overlayViewAtPosition(context: Context, position: Int): View?
|
||||
|
||||
fun clear(id: String)
|
||||
}
|
@@ -0,0 +1,335 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
* Copyright (C) 2018 stfalcon.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.attachmentviewer
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
import android.view.ScaleGestureDetector
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.widget.ImageView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.GestureDetectorCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.transition.TransitionManager
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import kotlinx.android.synthetic.main.activity_attachment_viewer.*
|
||||
import java.lang.ref.WeakReference
|
||||
import kotlin.math.abs
|
||||
|
||||
abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventListener {
|
||||
|
||||
lateinit var pager2: ViewPager2
|
||||
lateinit var imageTransitionView: ImageView
|
||||
lateinit var transitionImageContainer: ViewGroup
|
||||
|
||||
var topInset = 0
|
||||
var bottomInset = 0
|
||||
var systemUiVisibility = true
|
||||
|
||||
private var overlayView: View? = null
|
||||
set(value) {
|
||||
if (value == overlayView) return
|
||||
overlayView?.let { rootContainer.removeView(it) }
|
||||
rootContainer.addView(value)
|
||||
value?.updatePadding(top = topInset, bottom = bottomInset)
|
||||
field = value
|
||||
}
|
||||
|
||||
private lateinit var swipeDismissHandler: SwipeToDismissHandler
|
||||
private lateinit var directionDetector: SwipeDirectionDetector
|
||||
private lateinit var scaleDetector: ScaleGestureDetector
|
||||
private lateinit var gestureDetector: GestureDetectorCompat
|
||||
|
||||
var currentPosition = 0
|
||||
|
||||
private var swipeDirection: SwipeDirection? = null
|
||||
|
||||
private fun isScaled() = attachmentsAdapter.isScaled(currentPosition)
|
||||
|
||||
private var wasScaled: Boolean = false
|
||||
private var isSwipeToDismissAllowed: Boolean = true
|
||||
private lateinit var attachmentsAdapter: AttachmentsAdapter
|
||||
private var isOverlayWasClicked = false
|
||||
|
||||
// private val shouldDismissToBottom: Boolean
|
||||
// get() = e == null
|
||||
// || !externalTransitionImageView.isRectVisible
|
||||
// || !isAtStartPosition
|
||||
|
||||
private var isImagePagerIdle = true
|
||||
|
||||
fun setSourceProvider(sourceProvider: AttachmentSourceProvider) {
|
||||
attachmentsAdapter.attachmentSourceProvider = sourceProvider
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// This is important for the dispatchTouchEvent, if not we must correct
|
||||
// the touch coordinates
|
||||
window.decorView.systemUiVisibility = (
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
or View.SYSTEM_UI_FLAG_IMMERSIVE)
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
|
||||
|
||||
setContentView(R.layout.activity_attachment_viewer)
|
||||
attachmentPager.orientation = ViewPager2.ORIENTATION_HORIZONTAL
|
||||
attachmentsAdapter = AttachmentsAdapter()
|
||||
attachmentPager.adapter = attachmentsAdapter
|
||||
imageTransitionView = transitionImageView
|
||||
transitionImageContainer = findViewById(R.id.transitionImageContainer)
|
||||
pager2 = attachmentPager
|
||||
directionDetector = createSwipeDirectionDetector()
|
||||
gestureDetector = createGestureDetector()
|
||||
|
||||
attachmentPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageScrollStateChanged(state: Int) {
|
||||
isImagePagerIdle = state == ViewPager2.SCROLL_STATE_IDLE
|
||||
}
|
||||
|
||||
override fun onPageSelected(position: Int) {
|
||||
onSelectedPositionChanged(position)
|
||||
}
|
||||
})
|
||||
|
||||
swipeDismissHandler = createSwipeToDismissHandler()
|
||||
rootContainer.setOnTouchListener(swipeDismissHandler)
|
||||
rootContainer.viewTreeObserver.addOnGlobalLayoutListener { swipeDismissHandler.translationLimit = dismissContainer.height / 4 }
|
||||
|
||||
scaleDetector = createScaleGestureDetector()
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(rootContainer) { _, insets ->
|
||||
overlayView?.updatePadding(top = insets.systemWindowInsetTop, bottom = insets.systemWindowInsetBottom)
|
||||
topInset = insets.systemWindowInsetTop
|
||||
bottomInset = insets.systemWindowInsetBottom
|
||||
insets
|
||||
}
|
||||
}
|
||||
|
||||
fun onSelectedPositionChanged(position: Int) {
|
||||
attachmentsAdapter.recyclerView?.findViewHolderForAdapterPosition(currentPosition)?.let {
|
||||
(it as? BaseViewHolder)?.onSelected(false)
|
||||
}
|
||||
attachmentsAdapter.recyclerView?.findViewHolderForAdapterPosition(position)?.let {
|
||||
(it as? BaseViewHolder)?.onSelected(true)
|
||||
if (it is VideoViewHolder) {
|
||||
it.eventListener = WeakReference(this)
|
||||
}
|
||||
}
|
||||
currentPosition = position
|
||||
overlayView = attachmentsAdapter.attachmentSourceProvider?.overlayViewAtPosition(this@AttachmentViewerActivity, position)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
attachmentsAdapter.onPause(currentPosition)
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
attachmentsAdapter.onResume(currentPosition)
|
||||
}
|
||||
|
||||
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
|
||||
// The zoomable view is configured to disallow interception when image is zoomed
|
||||
|
||||
// Check if the overlay is visible, and wants to handle the click
|
||||
if (overlayView?.isVisible == true && overlayView?.dispatchTouchEvent(ev) == true) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Log.v("ATTACHEMENTS", "================\ndispatchTouchEvent $ev")
|
||||
handleUpDownEvent(ev)
|
||||
|
||||
// Log.v("ATTACHEMENTS", "scaleDetector is in progress ${scaleDetector.isInProgress}")
|
||||
// Log.v("ATTACHEMENTS", "pointerCount ${ev.pointerCount}")
|
||||
// Log.v("ATTACHEMENTS", "wasScaled $wasScaled")
|
||||
if (swipeDirection == null && (scaleDetector.isInProgress || ev.pointerCount > 1 || wasScaled)) {
|
||||
wasScaled = true
|
||||
// Log.v("ATTACHEMENTS", "dispatch to pager")
|
||||
return attachmentPager.dispatchTouchEvent(ev)
|
||||
}
|
||||
|
||||
// Log.v("ATTACHEMENTS", "is current item scaled ${isScaled()}")
|
||||
return (if (isScaled()) super.dispatchTouchEvent(ev) else handleTouchIfNotScaled(ev)).also {
|
||||
// Log.v("ATTACHEMENTS", "\n================")
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUpDownEvent(event: MotionEvent) {
|
||||
// Log.v("ATTACHEMENTS", "handleUpDownEvent $event")
|
||||
if (event.action == MotionEvent.ACTION_UP) {
|
||||
handleEventActionUp(event)
|
||||
}
|
||||
|
||||
if (event.action == MotionEvent.ACTION_DOWN) {
|
||||
handleEventActionDown(event)
|
||||
}
|
||||
|
||||
scaleDetector.onTouchEvent(event)
|
||||
gestureDetector.onTouchEvent(event)
|
||||
}
|
||||
|
||||
private fun handleEventActionDown(event: MotionEvent) {
|
||||
swipeDirection = null
|
||||
wasScaled = false
|
||||
attachmentPager.dispatchTouchEvent(event)
|
||||
|
||||
swipeDismissHandler.onTouch(rootContainer, event)
|
||||
isOverlayWasClicked = dispatchOverlayTouch(event)
|
||||
}
|
||||
|
||||
private fun handleEventActionUp(event: MotionEvent) {
|
||||
// wasDoubleTapped = false
|
||||
swipeDismissHandler.onTouch(rootContainer, event)
|
||||
attachmentPager.dispatchTouchEvent(event)
|
||||
isOverlayWasClicked = dispatchOverlayTouch(event)
|
||||
}
|
||||
|
||||
private fun handleSingleTap(event: MotionEvent, isOverlayWasClicked: Boolean) {
|
||||
// TODO if there is no overlay, we should at least toggle system bars?
|
||||
if (overlayView != null && !isOverlayWasClicked) {
|
||||
toggleOverlayViewVisibility()
|
||||
super.dispatchTouchEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleOverlayViewVisibility() {
|
||||
if (systemUiVisibility) {
|
||||
// we hide
|
||||
TransitionManager.beginDelayedTransition(rootContainer)
|
||||
hideSystemUI()
|
||||
overlayView?.isVisible = false
|
||||
} else {
|
||||
// we show
|
||||
TransitionManager.beginDelayedTransition(rootContainer)
|
||||
showSystemUI()
|
||||
overlayView?.isVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleTouchIfNotScaled(event: MotionEvent): Boolean {
|
||||
// Log.v("ATTACHEMENTS", "handleTouchIfNotScaled $event")
|
||||
directionDetector.handleTouchEvent(event)
|
||||
|
||||
return when (swipeDirection) {
|
||||
SwipeDirection.Up, SwipeDirection.Down -> {
|
||||
if (isSwipeToDismissAllowed && !wasScaled && isImagePagerIdle) {
|
||||
swipeDismissHandler.onTouch(rootContainer, event)
|
||||
} else true
|
||||
}
|
||||
SwipeDirection.Left, SwipeDirection.Right -> {
|
||||
attachmentPager.dispatchTouchEvent(event)
|
||||
}
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSwipeViewMove(translationY: Float, translationLimit: Int) {
|
||||
val alpha = calculateTranslationAlpha(translationY, translationLimit)
|
||||
backgroundView.alpha = alpha
|
||||
dismissContainer.alpha = alpha
|
||||
overlayView?.alpha = alpha
|
||||
}
|
||||
|
||||
private fun dispatchOverlayTouch(event: MotionEvent): Boolean =
|
||||
overlayView
|
||||
?.let { it.isVisible && it.dispatchTouchEvent(event) }
|
||||
?: false
|
||||
|
||||
private fun calculateTranslationAlpha(translationY: Float, translationLimit: Int): Float =
|
||||
1.0f - 1.0f / translationLimit.toFloat() / 4f * abs(translationY)
|
||||
|
||||
private fun createSwipeToDismissHandler()
|
||||
: SwipeToDismissHandler = SwipeToDismissHandler(
|
||||
swipeView = dismissContainer,
|
||||
shouldAnimateDismiss = { shouldAnimateDismiss() },
|
||||
onDismiss = { animateClose() },
|
||||
onSwipeViewMove = ::handleSwipeViewMove)
|
||||
|
||||
private fun createSwipeDirectionDetector() =
|
||||
SwipeDirectionDetector(this) { swipeDirection = it }
|
||||
|
||||
private fun createScaleGestureDetector() =
|
||||
ScaleGestureDetector(this, ScaleGestureDetector.SimpleOnScaleGestureListener())
|
||||
|
||||
private fun createGestureDetector() =
|
||||
GestureDetectorCompat(this, object : GestureDetector.SimpleOnGestureListener() {
|
||||
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
|
||||
if (isImagePagerIdle) {
|
||||
handleSingleTap(e, isOverlayWasClicked)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onDoubleTap(e: MotionEvent?): Boolean {
|
||||
return super.onDoubleTap(e)
|
||||
}
|
||||
})
|
||||
|
||||
override fun onEvent(event: AttachmentEvents) {
|
||||
if (overlayView is AttachmentEventListener) {
|
||||
(overlayView as? AttachmentEventListener)?.onEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun shouldAnimateDismiss(): Boolean = true
|
||||
|
||||
protected open fun animateClose() {
|
||||
window.statusBarColor = Color.TRANSPARENT
|
||||
finish()
|
||||
}
|
||||
|
||||
fun handle(commands: AttachmentCommands) {
|
||||
(attachmentsAdapter.recyclerView?.findViewHolderForAdapterPosition(currentPosition) as? BaseViewHolder)
|
||||
?.handleCommand(commands)
|
||||
}
|
||||
|
||||
private fun hideSystemUI() {
|
||||
systemUiVisibility = false
|
||||
// Enables regular immersive mode.
|
||||
// For "lean back" mode, remove SYSTEM_UI_FLAG_IMMERSIVE.
|
||||
// Or for "sticky immersive," replace it with SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE
|
||||
// Set the content to appear under the system bars so that the
|
||||
// content doesn't resize when the system bars hide and show.
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
// Hide the nav bar and status bar
|
||||
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_FULLSCREEN)
|
||||
}
|
||||
|
||||
// Shows the system bars by removing all the flags
|
||||
// except for the ones that make the content appear under the system bars.
|
||||
private fun showSystemUI() {
|
||||
systemUiVisibility = true
|
||||
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
|
||||
}
|
||||
}
|
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* 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 im.vector.riotx.attachmentviewer
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class AttachmentsAdapter : RecyclerView.Adapter<BaseViewHolder>() {
|
||||
|
||||
var attachmentSourceProvider: AttachmentSourceProvider? = null
|
||||
set(value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
var recyclerView: RecyclerView? = null
|
||||
|
||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||
this.recyclerView = recyclerView
|
||||
}
|
||||
|
||||
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
|
||||
this.recyclerView = null
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val itemView = inflater.inflate(viewType, parent, false)
|
||||
return when (viewType) {
|
||||
R.layout.item_image_attachment -> ZoomableImageViewHolder(itemView)
|
||||
R.layout.item_animated_image_attachment -> AnimatedImageViewHolder(itemView)
|
||||
R.layout.item_video_attachment -> VideoViewHolder(itemView)
|
||||
else -> UnsupportedViewHolder(itemView)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
val info = attachmentSourceProvider!!.getAttachmentInfoAt(position)
|
||||
return when (info) {
|
||||
is AttachmentInfo.Image -> R.layout.item_image_attachment
|
||||
is AttachmentInfo.Video -> R.layout.item_video_attachment
|
||||
is AttachmentInfo.AnimatedImage -> R.layout.item_animated_image_attachment
|
||||
// is AttachmentInfo.Audio -> TODO()
|
||||
// is AttachmentInfo.File -> TODO()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return attachmentSourceProvider?.getItemCount() ?: 0
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
|
||||
attachmentSourceProvider?.getAttachmentInfoAt(position)?.let {
|
||||
holder.bind(it)
|
||||
when (it) {
|
||||
is AttachmentInfo.Image -> {
|
||||
attachmentSourceProvider?.loadImage((holder as ZoomableImageViewHolder).target, it)
|
||||
}
|
||||
is AttachmentInfo.AnimatedImage -> {
|
||||
attachmentSourceProvider?.loadImage((holder as AnimatedImageViewHolder).target, it)
|
||||
}
|
||||
is AttachmentInfo.Video -> {
|
||||
attachmentSourceProvider?.loadVideo((holder as VideoViewHolder).target, it)
|
||||
}
|
||||
// else -> {
|
||||
// // }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewAttachedToWindow(holder: BaseViewHolder) {
|
||||
holder.onAttached()
|
||||
}
|
||||
|
||||
override fun onViewRecycled(holder: BaseViewHolder) {
|
||||
holder.onRecycled()
|
||||
}
|
||||
|
||||
override fun onViewDetachedFromWindow(holder: BaseViewHolder) {
|
||||
holder.onDetached()
|
||||
}
|
||||
|
||||
fun isScaled(position: Int): Boolean {
|
||||
val holder = recyclerView?.findViewHolderForAdapterPosition(position)
|
||||
if (holder is ZoomableImageViewHolder) {
|
||||
return holder.touchImageView.attacher.scale > 1f
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun onPause(position: Int) {
|
||||
val holder = recyclerView?.findViewHolderForAdapterPosition(position) as? BaseViewHolder
|
||||
holder?.entersBackground()
|
||||
}
|
||||
|
||||
fun onResume(position: Int) {
|
||||
val holder = recyclerView?.findViewHolderForAdapterPosition(position) as? BaseViewHolder
|
||||
holder?.entersForeground()
|
||||
}
|
||||
}
|
@@ -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 im.vector.riotx.attachmentviewer
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
abstract class BaseViewHolder constructor(itemView: View) :
|
||||
RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
open fun onRecycled() {
|
||||
boundResourceUid = null
|
||||
}
|
||||
|
||||
open fun onAttached() {}
|
||||
open fun onDetached() {}
|
||||
open fun entersBackground() {}
|
||||
open fun entersForeground() {}
|
||||
open fun onSelected(selected: Boolean) {}
|
||||
|
||||
open fun handleCommand(commands: AttachmentCommands) {}
|
||||
|
||||
var boundResourceUid: String? = null
|
||||
|
||||
open fun bind(attachmentInfo: AttachmentInfo) {
|
||||
boundResourceUid = attachmentInfo.uid
|
||||
}
|
||||
}
|
||||
|
||||
class UnsupportedViewHolder constructor(itemView: View) :
|
||||
BaseViewHolder(itemView)
|
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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 im.vector.riotx.attachmentviewer
|
||||
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
|
||||
interface ImageLoaderTarget {
|
||||
|
||||
fun contextView(): ImageView
|
||||
|
||||
fun onResourceLoading(uid: String, placeholder: Drawable?)
|
||||
|
||||
fun onLoadFailed(uid: String, errorDrawable: Drawable?)
|
||||
|
||||
fun onResourceCleared(uid: String, placeholder: Drawable?)
|
||||
|
||||
fun onResourceReady(uid: String, resource: Drawable)
|
||||
}
|
||||
|
||||
internal class DefaultImageLoaderTarget(val holder: AnimatedImageViewHolder, private val contextView: ImageView)
|
||||
: ImageLoaderTarget {
|
||||
override fun contextView(): ImageView {
|
||||
return contextView
|
||||
}
|
||||
|
||||
override fun onResourceLoading(uid: String, placeholder: Drawable?) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.imageLoaderProgress.isVisible = true
|
||||
}
|
||||
|
||||
override fun onLoadFailed(uid: String, errorDrawable: Drawable?) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.imageLoaderProgress.isVisible = false
|
||||
}
|
||||
|
||||
override fun onResourceCleared(uid: String, placeholder: Drawable?) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.touchImageView.setImageDrawable(placeholder)
|
||||
}
|
||||
|
||||
override fun onResourceReady(uid: String, resource: Drawable) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.imageLoaderProgress.isVisible = false
|
||||
// Glide mess up the view size :/
|
||||
holder.touchImageView.updateLayoutParams {
|
||||
width = LinearLayout.LayoutParams.MATCH_PARENT
|
||||
height = LinearLayout.LayoutParams.MATCH_PARENT
|
||||
}
|
||||
holder.touchImageView.setImageDrawable(resource)
|
||||
if (resource is Animatable) {
|
||||
resource.start()
|
||||
}
|
||||
}
|
||||
|
||||
internal class ZoomableImageTarget(val holder: ZoomableImageViewHolder, private val contextView: ImageView) : ImageLoaderTarget {
|
||||
override fun contextView() = contextView
|
||||
|
||||
override fun onResourceLoading(uid: String, placeholder: Drawable?) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.imageLoaderProgress.isVisible = true
|
||||
}
|
||||
|
||||
override fun onLoadFailed(uid: String, errorDrawable: Drawable?) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.imageLoaderProgress.isVisible = false
|
||||
}
|
||||
|
||||
override fun onResourceCleared(uid: String, placeholder: Drawable?) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.touchImageView.setImageDrawable(placeholder)
|
||||
}
|
||||
|
||||
override fun onResourceReady(uid: String, resource: Drawable) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.imageLoaderProgress.isVisible = false
|
||||
// Glide mess up the view size :/
|
||||
holder.touchImageView.updateLayoutParams {
|
||||
width = LinearLayout.LayoutParams.MATCH_PARENT
|
||||
height = LinearLayout.LayoutParams.MATCH_PARENT
|
||||
}
|
||||
holder.touchImageView.setImageDrawable(resource)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
* Copyright (C) 2018 stfalcon.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.attachmentviewer
|
||||
|
||||
sealed class SwipeDirection {
|
||||
object NotDetected : SwipeDirection()
|
||||
object Up : SwipeDirection()
|
||||
object Down : SwipeDirection()
|
||||
object Left : SwipeDirection()
|
||||
object Right : SwipeDirection()
|
||||
|
||||
companion object {
|
||||
fun fromAngle(angle: Double): SwipeDirection {
|
||||
return when (angle) {
|
||||
in 0.0..45.0 -> Right
|
||||
in 45.0..135.0 -> Up
|
||||
in 135.0..225.0 -> Left
|
||||
in 225.0..315.0 -> Down
|
||||
in 315.0..360.0 -> Right
|
||||
else -> NotDetected
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
* Copyright (C) 2018 stfalcon.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.attachmentviewer
|
||||
|
||||
import android.content.Context
|
||||
import android.view.MotionEvent
|
||||
import kotlin.math.sqrt
|
||||
|
||||
class SwipeDirectionDetector(
|
||||
context: Context,
|
||||
private val onDirectionDetected: (SwipeDirection) -> Unit
|
||||
) {
|
||||
|
||||
private val touchSlop: Int = android.view.ViewConfiguration.get(context).scaledTouchSlop
|
||||
private var startX: Float = 0f
|
||||
private var startY: Float = 0f
|
||||
private var isDetected: Boolean = false
|
||||
|
||||
fun handleTouchEvent(event: MotionEvent) {
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
startX = event.x
|
||||
startY = event.y
|
||||
}
|
||||
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
|
||||
if (!isDetected) {
|
||||
onDirectionDetected(SwipeDirection.NotDetected)
|
||||
}
|
||||
startY = 0.0f
|
||||
startX = startY
|
||||
isDetected = false
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> if (!isDetected && getEventDistance(event) > touchSlop) {
|
||||
isDetected = true
|
||||
onDirectionDetected(getDirection(startX, startY, event.x, event.y))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given two points in the plane p1=(x1, x2) and p2=(y1, y1), this method
|
||||
* returns the direction that an arrow pointing from p1 to p2 would have.
|
||||
*
|
||||
* @param x1 the x position of the first point
|
||||
* @param y1 the y position of the first point
|
||||
* @param x2 the x position of the second point
|
||||
* @param y2 the y position of the second point
|
||||
* @return the direction
|
||||
*/
|
||||
private fun getDirection(x1: Float, y1: Float, x2: Float, y2: Float): SwipeDirection {
|
||||
val angle = getAngle(x1, y1, x2, y2)
|
||||
return SwipeDirection.fromAngle(angle)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the angle between two points in the plane (x1,y1) and (x2, y2)
|
||||
* The angle is measured with 0/360 being the X-axis to the right, angles
|
||||
* increase counter clockwise.
|
||||
*
|
||||
* @param x1 the x position of the first point
|
||||
* @param y1 the y position of the first point
|
||||
* @param x2 the x position of the second point
|
||||
* @param y2 the y position of the second point
|
||||
* @return the angle between two points
|
||||
*/
|
||||
private fun getAngle(x1: Float, y1: Float, x2: Float, y2: Float): Double {
|
||||
val rad = Math.atan2((y1 - y2).toDouble(), (x2 - x1).toDouble()) + Math.PI
|
||||
return (rad * 180 / Math.PI + 180) % 360
|
||||
}
|
||||
|
||||
private fun getEventDistance(ev: MotionEvent): Float {
|
||||
val dx = ev.getX(0) - startX
|
||||
val dy = ev.getY(0) - startY
|
||||
return sqrt((dx * dx + dy * dy).toDouble()).toFloat()
|
||||
}
|
||||
}
|
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
* Copyright (C) 2018 stfalcon.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.attachmentviewer
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Rect
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewPropertyAnimator
|
||||
import android.view.animation.AccelerateInterpolator
|
||||
|
||||
class SwipeToDismissHandler(
|
||||
private val swipeView: View,
|
||||
private val onDismiss: () -> Unit,
|
||||
private val onSwipeViewMove: (translationY: Float, translationLimit: Int) -> Unit,
|
||||
private val shouldAnimateDismiss: () -> Boolean
|
||||
) : View.OnTouchListener {
|
||||
|
||||
companion object {
|
||||
private const val ANIMATION_DURATION = 200L
|
||||
}
|
||||
|
||||
var translationLimit: Int = swipeView.height / 4
|
||||
private var isTracking = false
|
||||
private var startY: Float = 0f
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouch(v: View, event: MotionEvent): Boolean {
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
if (swipeView.hitRect.contains(event.x.toInt(), event.y.toInt())) {
|
||||
isTracking = true
|
||||
}
|
||||
startY = event.y
|
||||
return true
|
||||
}
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
if (isTracking) {
|
||||
isTracking = false
|
||||
onTrackingEnd(v.height)
|
||||
}
|
||||
return true
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (isTracking) {
|
||||
val translationY = event.y - startY
|
||||
swipeView.translationY = translationY
|
||||
onSwipeViewMove(translationY, translationLimit)
|
||||
}
|
||||
return true
|
||||
}
|
||||
else -> {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun initiateDismissToBottom() {
|
||||
animateTranslation(swipeView.height.toFloat())
|
||||
}
|
||||
|
||||
private fun onTrackingEnd(parentHeight: Int) {
|
||||
val animateTo = when {
|
||||
swipeView.translationY < -translationLimit -> -parentHeight.toFloat()
|
||||
swipeView.translationY > translationLimit -> parentHeight.toFloat()
|
||||
else -> 0f
|
||||
}
|
||||
|
||||
if (animateTo != 0f && !shouldAnimateDismiss()) {
|
||||
onDismiss()
|
||||
} else {
|
||||
animateTranslation(animateTo)
|
||||
}
|
||||
}
|
||||
|
||||
private fun animateTranslation(translationTo: Float) {
|
||||
swipeView.animate()
|
||||
.translationY(translationTo)
|
||||
.setDuration(ANIMATION_DURATION)
|
||||
.setInterpolator(AccelerateInterpolator())
|
||||
.setUpdateListener { onSwipeViewMove(swipeView.translationY, translationLimit) }
|
||||
.setAnimatorListener(onAnimationEnd = {
|
||||
if (translationTo != 0f) {
|
||||
onDismiss()
|
||||
}
|
||||
|
||||
// remove the update listener, otherwise it will be saved on the next animation execution:
|
||||
swipeView.animate().setUpdateListener(null)
|
||||
})
|
||||
.start()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ViewPropertyAnimator.setAnimatorListener(
|
||||
onAnimationEnd: ((Animator?) -> Unit)? = null,
|
||||
onAnimationStart: ((Animator?) -> Unit)? = null
|
||||
) = this.setListener(
|
||||
object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
onAnimationEnd?.invoke(animation)
|
||||
}
|
||||
|
||||
override fun onAnimationStart(animation: Animator?) {
|
||||
onAnimationStart?.invoke(animation)
|
||||
}
|
||||
})
|
||||
|
||||
internal val View?.hitRect: Rect
|
||||
get() = Rect().also { this?.getHitRect(it) }
|
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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 im.vector.riotx.attachmentviewer
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.widget.ImageView
|
||||
import androidx.core.view.isVisible
|
||||
import java.io.File
|
||||
|
||||
interface VideoLoaderTarget {
|
||||
fun contextView(): ImageView
|
||||
|
||||
fun onThumbnailResourceLoading(uid: String, placeholder: Drawable?)
|
||||
|
||||
fun onThumbnailLoadFailed(uid: String, errorDrawable: Drawable?)
|
||||
|
||||
fun onThumbnailResourceCleared(uid: String, placeholder: Drawable?)
|
||||
|
||||
fun onThumbnailResourceReady(uid: String, resource: Drawable)
|
||||
|
||||
fun onVideoFileLoading(uid: String)
|
||||
fun onVideoFileLoadFailed(uid: String)
|
||||
fun onVideoFileReady(uid: String, file: File)
|
||||
}
|
||||
|
||||
internal class DefaultVideoLoaderTarget(val holder: VideoViewHolder, private val contextView: ImageView) : VideoLoaderTarget {
|
||||
override fun contextView(): ImageView = contextView
|
||||
|
||||
override fun onThumbnailResourceLoading(uid: String, placeholder: Drawable?) {
|
||||
}
|
||||
|
||||
override fun onThumbnailLoadFailed(uid: String, errorDrawable: Drawable?) {
|
||||
}
|
||||
|
||||
override fun onThumbnailResourceCleared(uid: String, placeholder: Drawable?) {
|
||||
}
|
||||
|
||||
override fun onThumbnailResourceReady(uid: String, resource: Drawable) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.thumbnailImage.setImageDrawable(resource)
|
||||
}
|
||||
|
||||
override fun onVideoFileLoading(uid: String) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.thumbnailImage.isVisible = true
|
||||
holder.loaderProgressBar.isVisible = true
|
||||
holder.videoView.isVisible = false
|
||||
}
|
||||
|
||||
override fun onVideoFileLoadFailed(uid: String) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.videoFileLoadError()
|
||||
}
|
||||
|
||||
override fun onVideoFileReady(uid: String, file: File) {
|
||||
if (holder.boundResourceUid != uid) return
|
||||
holder.thumbnailImage.isVisible = false
|
||||
holder.loaderProgressBar.isVisible = false
|
||||
holder.videoView.isVisible = true
|
||||
holder.videoReady(file)
|
||||
}
|
||||
}
|
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* 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 im.vector.riotx.attachmentviewer
|
||||
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import android.widget.VideoView
|
||||
import androidx.core.view.isVisible
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import java.io.File
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
// TODO, it would be probably better to use a unique media player
|
||||
// for better customization and control
|
||||
// But for now VideoView is enough, it released player when detached, we use a timer to update progress
|
||||
class VideoViewHolder constructor(itemView: View) :
|
||||
BaseViewHolder(itemView) {
|
||||
|
||||
private var isSelected = false
|
||||
private var mVideoPath: String? = null
|
||||
private var progressDisposable: Disposable? = null
|
||||
private var progress: Int = 0
|
||||
private var wasPaused = false
|
||||
|
||||
var eventListener: WeakReference<AttachmentEventListener>? = null
|
||||
|
||||
val thumbnailImage: ImageView = itemView.findViewById(R.id.videoThumbnailImage)
|
||||
val videoView: VideoView = itemView.findViewById(R.id.videoView)
|
||||
val loaderProgressBar: ProgressBar = itemView.findViewById(R.id.videoLoaderProgress)
|
||||
val videoControlIcon: ImageView = itemView.findViewById(R.id.videoControlIcon)
|
||||
val errorTextView: TextView = itemView.findViewById(R.id.videoMediaViewerErrorView)
|
||||
|
||||
internal val target = DefaultVideoLoaderTarget(this, thumbnailImage)
|
||||
|
||||
override fun onRecycled() {
|
||||
super.onRecycled()
|
||||
progressDisposable?.dispose()
|
||||
progressDisposable = null
|
||||
mVideoPath = null
|
||||
}
|
||||
|
||||
fun videoReady(file: File) {
|
||||
mVideoPath = file.path
|
||||
if (isSelected) {
|
||||
startPlaying()
|
||||
}
|
||||
}
|
||||
|
||||
fun videoFileLoadError() {
|
||||
}
|
||||
|
||||
override fun entersBackground() {
|
||||
if (videoView.isPlaying) {
|
||||
progress = videoView.currentPosition
|
||||
progressDisposable?.dispose()
|
||||
progressDisposable = null
|
||||
videoView.stopPlayback()
|
||||
videoView.pause()
|
||||
}
|
||||
}
|
||||
|
||||
override fun entersForeground() {
|
||||
onSelected(isSelected)
|
||||
}
|
||||
|
||||
override fun onSelected(selected: Boolean) {
|
||||
if (!selected) {
|
||||
if (videoView.isPlaying) {
|
||||
progress = videoView.currentPosition
|
||||
videoView.stopPlayback()
|
||||
} else {
|
||||
progress = 0
|
||||
}
|
||||
progressDisposable?.dispose()
|
||||
progressDisposable = null
|
||||
} else {
|
||||
if (mVideoPath != null) {
|
||||
startPlaying()
|
||||
}
|
||||
}
|
||||
isSelected = true
|
||||
}
|
||||
|
||||
private fun startPlaying() {
|
||||
thumbnailImage.isVisible = false
|
||||
loaderProgressBar.isVisible = false
|
||||
videoView.isVisible = true
|
||||
|
||||
videoView.setOnPreparedListener {
|
||||
progressDisposable?.dispose()
|
||||
progressDisposable = Observable.interval(100, TimeUnit.MILLISECONDS)
|
||||
.timeInterval()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
val duration = videoView.duration
|
||||
val progress = videoView.currentPosition
|
||||
val isPlaying = videoView.isPlaying
|
||||
// Log.v("FOO", "isPlaying $isPlaying $progress/$duration")
|
||||
eventListener?.get()?.onEvent(AttachmentEvents.VideoEvent(isPlaying, progress, duration))
|
||||
}
|
||||
}
|
||||
|
||||
videoView.setVideoPath(mVideoPath)
|
||||
if (!wasPaused) {
|
||||
videoView.start()
|
||||
if (progress > 0) {
|
||||
videoView.seekTo(progress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleCommand(commands: AttachmentCommands) {
|
||||
if (!isSelected) return
|
||||
when (commands) {
|
||||
AttachmentCommands.StartVideo -> {
|
||||
wasPaused = false
|
||||
videoView.start()
|
||||
}
|
||||
AttachmentCommands.PauseVideo -> {
|
||||
wasPaused = true
|
||||
videoView.pause()
|
||||
}
|
||||
is AttachmentCommands.SeekTo -> {
|
||||
val duration = videoView.duration
|
||||
if (duration > 0) {
|
||||
val seekDuration = duration * (commands.percentProgress / 100f)
|
||||
videoView.seekTo(seekDuration.toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun bind(attachmentInfo: AttachmentInfo) {
|
||||
super.bind(attachmentInfo)
|
||||
progress = 0
|
||||
wasPaused = false
|
||||
}
|
||||
}
|
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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 im.vector.riotx.attachmentviewer
|
||||
|
||||
import android.view.View
|
||||
import android.widget.ProgressBar
|
||||
import com.github.chrisbanes.photoview.PhotoView
|
||||
|
||||
class ZoomableImageViewHolder constructor(itemView: View) :
|
||||
BaseViewHolder(itemView) {
|
||||
|
||||
val touchImageView: PhotoView = itemView.findViewById(R.id.touchImageView)
|
||||
val imageLoaderProgress: ProgressBar = itemView.findViewById(R.id.imageLoaderProgress)
|
||||
|
||||
init {
|
||||
touchImageView.setAllowParentInterceptOnEdge(false)
|
||||
touchImageView.setOnScaleChangeListener { scaleFactor, _, _ ->
|
||||
// Log.v("ATTACHEMENTS", "scaleFactor $scaleFactor")
|
||||
// It's a bit annoying but when you pitch down the scaling
|
||||
// is not exactly one :/
|
||||
touchImageView.setAllowParentInterceptOnEdge(scaleFactor <= 1.0008f)
|
||||
}
|
||||
touchImageView.setScale(1.0f, true)
|
||||
touchImageView.setAllowParentInterceptOnEdge(true)
|
||||
}
|
||||
|
||||
internal val target = DefaultImageLoaderTarget.ZoomableImageTarget(this, touchImageView)
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/rootContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".AttachmentViewerActivity">
|
||||
|
||||
<View
|
||||
android:id="@+id/backgroundView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:alpha="1"
|
||||
android:background="@android:color/black" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/dismissContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/transitionImageContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:ignore="UselessParent"
|
||||
tools:visibility="invisible">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/transitionImageView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/attachmentPager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/transparent"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="visible" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</FrameLayout>
|
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
android:visibility="visible"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
<ProgressBar
|
||||
android:layout_centerInParent="true"
|
||||
android:id="@+id/imageLoaderProgress"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:visibility="visible"
|
||||
tools:visibility="gone" />
|
||||
|
||||
</RelativeLayout>
|
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<com.github.chrisbanes.photoview.PhotoView
|
||||
android:id="@+id/touchImageView"
|
||||
android:visibility="visible"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
<ProgressBar
|
||||
android:layout_centerInParent="true"
|
||||
android:id="@+id/imageLoaderProgress"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:visibility="visible"
|
||||
tools:visibility="gone" />
|
||||
|
||||
</RelativeLayout>
|
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/videoThumbnailImage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:scaleType="centerInside" />
|
||||
|
||||
<VideoView
|
||||
android:id="@+id/videoView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_centerInParent="true" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/videoControlIcon"
|
||||
android:layout_centerInParent="true"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
android:layout_width="44dp"
|
||||
android:layout_height="44dp"
|
||||
/>
|
||||
|
||||
<ProgressBar
|
||||
android:layout_centerInParent="true"
|
||||
android:id="@+id/videoLoaderProgress"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:visibility="invisible"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/videoMediaViewerErrorView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_margin="16dp"
|
||||
android:textSize="16sp"
|
||||
android:visibility="gone"
|
||||
tools:text="Error"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</RelativeLayout>
|
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/design_default_color_primary">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/testPage"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="1"
|
||||
android:textSize="80sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
|
||||
</RelativeLayout>
|
@@ -1,7 +1,7 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.3.50'
|
||||
ext.kotlin_version = '1.3.72'
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
@@ -10,12 +10,13 @@ buildscript {
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.6.3'
|
||||
// Warning: 3.6.3 leads to infinite gradle builds. Stick to 3.5.3 for the moment
|
||||
classpath 'com.android.tools.build:gradle:3.5.3'
|
||||
classpath 'com.google.gms:google-services:4.3.2'
|
||||
classpath "com.airbnb.okreplay:gradle-plugin:1.5.0"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.7.1'
|
||||
classpath 'com.google.android.gms:oss-licenses-plugin:0.9.5'
|
||||
classpath 'com.google.android.gms:oss-licenses-plugin:0.10.2'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
@@ -38,6 +39,8 @@ allprojects {
|
||||
includeGroupByRegex "com\\.github\\.yalantis"
|
||||
// JsonViewer
|
||||
includeGroupByRegex 'com\\.github\\.BillCarsonFr'
|
||||
// PhotoView
|
||||
includeGroupByRegex 'com\\.github\\.chrisbanes'
|
||||
}
|
||||
}
|
||||
maven {
|
||||
|
@@ -14,7 +14,7 @@ Difference though (list not exhaustive):
|
||||
- Only API v2 is supported (see https://matrix.org/docs/spec/identity_service/latest)
|
||||
- Homeserver has to be up to date to support binding (Versions.isLoginAndRegistrationSupportedBySdk() has to return true)
|
||||
- The SDK managed the session and client secret when binding ThreePid. Those data are not exposed to the client.
|
||||
- The SDK supports incremental sendAttempt (this is not used by RiotX)
|
||||
- The SDK supports incremental sendAttempt (this is not used by Element)
|
||||
- The "Continue" button is now under the information, and not as the same place that the checkbox
|
||||
- The app can cancel a binding. Current data are erased from DB.
|
||||
- The API (IdentityService) is improved.
|
||||
@@ -22,7 +22,7 @@ Difference though (list not exhaustive):
|
||||
|
||||
Missing features (list not exhaustive):
|
||||
- Invite by 3Pid (will be in a dedicated PR)
|
||||
- Add email or phone to account (not P1, can be done on Riot-Web)
|
||||
- Add email or phone to account (not P1, can be done on Element-Web)
|
||||
- List email and phone of the account (could be done in a dedicated PR)
|
||||
- Search contact (not P1)
|
||||
- Logout from identity server when user sign out or deactivate his account.
|
||||
@@ -55,7 +55,7 @@ The list can be found here: https://matrix.org/blog/2019/09/27/privacy-improveme
|
||||
|
||||
- Default identity server URL, from Wellknown data is proposed to the user.
|
||||
- Identity server can be set
|
||||
- Identity server can be changed on another user's device, so when the change is detected (thanks to account data sync) RiotX should properly disconnect from a previous identity server (I think it was not the case in Riot-Android, where we keep the token forever)
|
||||
- Identity server can be changed on another user's device, so when the change is detected (thanks to account data sync) Element should properly disconnect from a previous identity server (I think it was not the case in Riot-Android, where we keep the token forever)
|
||||
- Registration to the identity server is managed with an openId token
|
||||
- Terms of service can be accepted when configuring the identity server.
|
||||
- Terms of service can be accepted after, if they change.
|
||||
|
@@ -1,4 +1,4 @@
|
||||
This document aims to describe how RiotX android displays notifications to the end user. It also clarifies notifications and background settings in the app.
|
||||
This document aims to describe how Element android displays notifications to the end user. It also clarifies notifications and background settings in the app.
|
||||
|
||||
# Table of Contents
|
||||
1. [Prerequisites Knowledge](#prerequisites-knowledge)
|
||||
@@ -9,7 +9,7 @@ This document aims to describe how RiotX android displays notifications to the e
|
||||
* [How does the Home Server knows when to notify a client?](#how-does-the-home-server-knows-when-to-notify-a-client)
|
||||
* [Push vs privacy, and mitigation](#push-vs-privacy-and-mitigation)
|
||||
* [Background processing limitations](#background-processing-limitations)
|
||||
2. [RiotX Notification implementations](#riotx-notification-implementations)
|
||||
2. [Element Notification implementations](#element-notification-implementations)
|
||||
* [Requirements](#requirements)
|
||||
* [Foreground sync mode (Gplay & F-Droid)](#foreground-sync-mode-gplay-f-droid)
|
||||
* [Push (FCM) received in background](#push-fcm-received-in-background)
|
||||
@@ -50,7 +50,7 @@ By default, this is 0, so the server will return immediately even if the respons
|
||||
|
||||
**delay** is a client preference. When the server responds to a sync request, the client waits for `delay`before calling a new sync.
|
||||
|
||||
When the RiotX Android app is open (i.e in foreground state), the default timeout is 30 seconds, and delay is 0.
|
||||
When the Element Android app is open (i.e in foreground state), the default timeout is 30 seconds, and delay is 0.
|
||||
|
||||
## How does a mobile app receives push notification
|
||||
|
||||
@@ -86,7 +86,7 @@ This need some disambiguation, because it is the source of common confusion:
|
||||
In order to send a push to a mobile, App developers need to have a server that will use the FCM APIs, and these APIs requires authentication!
|
||||
This server is called a **Push Gateway** in the matrix world
|
||||
|
||||
That means that RiotX Android, a matrix client created by New Vector, is using a **Push Gateway** with the needed credentials (FCM API secret Key) in order to send push to the New Vector client.
|
||||
That means that Element Android, a matrix client created by New Vector, is using a **Push Gateway** with the needed credentials (FCM API secret Key) in order to send push to the New Vector client.
|
||||
|
||||
If you create your own matrix client, you will also need to deploy an instance of a **Push Gateway** with the credentials needed to use FCM for your app.
|
||||
|
||||
@@ -132,7 +132,7 @@ A Home Server can be configured with default rules (for Direct messages, group m
|
||||
|
||||
There are different kind of push rules, it can be per room (each new message on this room should be notified), it can also define a pattern that a message should match (when you are mentioned, or key word based).
|
||||
|
||||
Notifications have 2 'levels' (`highlighted = true/false sound = default/custom`). In RiotX these notifications level are reflected as Noisy/Silent.
|
||||
Notifications have 2 'levels' (`highlighted = true/false sound = default/custom`). In Element these notifications level are reflected as Noisy/Silent.
|
||||
|
||||
**What about encrypted messages?**
|
||||
|
||||
@@ -158,7 +158,7 @@ In a nutshell, apps can't do much in background now.
|
||||
|
||||
If the devices is not plugged and stays IDLE for a certain amount of time, radio (mobile connectivity) and CPU can/will be turned off.
|
||||
|
||||
For an application like RiotX, where users can receive important information at anytime, the best option is to rely on a push system (Google's Firebase Message a.k.a FCM). FCM high priority push can wake up the device and whitelist an application to perform background task (for a limited but unspecified amount of time).
|
||||
For an application like Element, where users can receive important information at anytime, the best option is to rely on a push system (Google's Firebase Message a.k.a FCM). FCM high priority push can wake up the device and whitelist an application to perform background task (for a limited but unspecified amount of time).
|
||||
|
||||
Notice that this is still evolving, and in future versions application that has been 'background restricted' by users won't be able to wake up even when a high priority push is received. Also high priority notifications could be rate limited (not defined anywhere)
|
||||
|
||||
@@ -167,41 +167,41 @@ The documentation on this subject is vague, and as per our experiments not alway
|
||||
|
||||
It is getting more and more complex to have reliable notifications when FCM is not used.
|
||||
|
||||
# RiotX Notification implementations
|
||||
# Element Notification implementations
|
||||
|
||||
## Requirements
|
||||
|
||||
RiotX Android must work with and without FCM.
|
||||
* The RiotX android app published on F-Droid do not rely on FCM (all related dependencies are not present)
|
||||
* The RiotX android app published on google play rely on FCM, with a fallback mode when FCM registration has failed (e.g outdated or missing Google Play Services)
|
||||
Element Android must work with and without FCM.
|
||||
* The Element android app published on F-Droid do not rely on FCM (all related dependencies are not present)
|
||||
* The Element android app published on google play rely on FCM, with a fallback mode when FCM registration has failed (e.g outdated or missing Google Play Services)
|
||||
|
||||
## Foreground sync mode (Gplay & F-Droid)
|
||||
|
||||
When in foreground, RiotX performs sync continuously with a timeout value set to 10 seconds (see HttpPooling).
|
||||
When in foreground, Element performs sync continuously with a timeout value set to 10 seconds (see HttpPooling).
|
||||
|
||||
As this mode does not need to live beyond the scope of the application, and as per Google recommendation, RiotX uses the internal app resources (Thread and Timers) to perform the syncs.
|
||||
As this mode does not need to live beyond the scope of the application, and as per Google recommendation, Element uses the internal app resources (Thread and Timers) to perform the syncs.
|
||||
|
||||
This mode is turned on when the app enters foreground, and off when enters background.
|
||||
|
||||
In background, and depending on wether push is available or not, RiotX will use different methods to perform the syncs (Workers / Alarms / Service)
|
||||
In background, and depending on wether push is available or not, Element will use different methods to perform the syncs (Workers / Alarms / Service)
|
||||
|
||||
## Push (FCM) received in background
|
||||
|
||||
In order to enable Push, RiotX must first get a push token from the firebase SDK, then register a pusher with this token on the HomeServer.
|
||||
In order to enable Push, Element must first get a push token from the firebase SDK, then register a pusher with this token on the HomeServer.
|
||||
|
||||
When a message should be notified to a user, the user's homeserver notifies the registered `push gateway` for RiotX, that is [sygnal](https://github.com/matrix-org/sygnal) _- The reference implementation for push gateways -_ hosted by matrix.org.
|
||||
When a message should be notified to a user, the user's homeserver notifies the registered `push gateway` for Element, that is [sygnal](https://github.com/matrix-org/sygnal) _- The reference implementation for push gateways -_ hosted by matrix.org.
|
||||
|
||||
This sygnal instance is configured with the required FCM API authentication token, and will then use the FCM API in order to notify the user's device running riotX.
|
||||
This sygnal instance is configured with the required FCM API authentication token, and will then use the FCM API in order to notify the user's device running Element.
|
||||
|
||||
```
|
||||
Homeserver ----> Sygnal (configured for RiotX) ----> FCM ----> RiotX
|
||||
Homeserver ----> Sygnal (configured for Element) ----> FCM ----> Element
|
||||
```
|
||||
|
||||
The push gateway is configured to only send `(eventId,roomId)` in the push payload (for better [privacy](#push-vs-privacy-and-mitigation)).
|
||||
|
||||
RiotX needs then to synchronise with the user's HomeServer, in order to resolve the event and create a notification.
|
||||
Element needs then to synchronise with the user's HomeServer, in order to resolve the event and create a notification.
|
||||
|
||||
As per [Google recommendation](https://android-developers.googleblog.com/2018/09/notifying-your-users-with-fcm.html), RiotX will then use the WorkManager API in order to trigger a background sync.
|
||||
As per [Google recommendation](https://android-developers.googleblog.com/2018/09/notifying-your-users-with-fcm.html), Element will then use the WorkManager API in order to trigger a background sync.
|
||||
|
||||
**Google recommendations:**
|
||||
> We recommend using FCM messages in combination with the WorkManager 1 or JobScheduler API
|
||||
@@ -209,7 +209,7 @@ As per [Google recommendation](https://android-developers.googleblog.com/2018/09
|
||||
> Avoid background services. One common pitfall is using a background service to fetch data in the FCM message handler, since background service will be stopped by the system per recent changes to Google Play Policy
|
||||
|
||||
```
|
||||
Homeserver ----> Sygnal ----> FCM ----> RiotX
|
||||
Homeserver ----> Sygnal ----> FCM ----> Element
|
||||
(Sync) ----> Homeserver
|
||||
<----
|
||||
Display notification
|
||||
@@ -217,24 +217,24 @@ Homeserver ----> Sygnal ----> FCM ----> RiotX
|
||||
|
||||
**Possible outcomes**
|
||||
|
||||
Upon reception of the FCM push, RiotX will perform a sync call to the Home Server, during this process it is possible that:
|
||||
Upon reception of the FCM push, Element will perform a sync call to the Home Server, during this process it is possible that:
|
||||
* Happy path, the sync is performed, the message resolved and displayed in the notification drawer
|
||||
* The notified message is not in the sync. Can happen if a lot of things did happen since the push (`gappy sync`)
|
||||
* The sync generates additional notifications (e.g an encrypted message where the user is mentioned detected locally)
|
||||
* The sync takes too long and the process is killed before completion, or network is not reliable and the sync fails.
|
||||
|
||||
RiotX implements several strategies in these cases (TODO document)
|
||||
Element implements several strategies in these cases (TODO document)
|
||||
|
||||
## FCM Fallback mode
|
||||
|
||||
It is possible that RiotX is not able to get a FCM push token.
|
||||
It is possible that Element is not able to get a FCM push token.
|
||||
Common errors (amoung several others) that can cause that:
|
||||
* Google Play Services is outdated
|
||||
* Google Play Service fails in someways with FCM servers (infamous `SERVICE_NOT_AVAILABLE`)
|
||||
|
||||
If RiotX is able to detect one of this cases, it will notifies it to the users and when possible help him fix it via a dedicated troubleshoot screen.
|
||||
If Element is able to detect one of this cases, it will notifies it to the users and when possible help him fix it via a dedicated troubleshoot screen.
|
||||
|
||||
Meanwhile, in order to offer a minimal service, and as per Google's recommendation for background activities, RiotX will launch periodic background sync in order to stays in sync with servers.
|
||||
Meanwhile, in order to offer a minimal service, and as per Google's recommendation for background activities, Element will launch periodic background sync in order to stays in sync with servers.
|
||||
|
||||
The fallback mode is impacted by all the battery life saving mechanism implemented by android. Meaning that if the app is not used for a certain amount of time (`App-Standby`), or the device stays still and unplugged (`Light Doze`) , the sync will become less frequent.
|
||||
|
||||
@@ -248,7 +248,7 @@ The fallback mode is supposed to be a temporary state waiting for the user to fi
|
||||
|
||||
## F-Droid background Mode
|
||||
|
||||
The F-Droid RiotX flavor has no dependencies to FCM, therefore cannot relies on Push.
|
||||
The F-Droid Element flavor has no dependencies to FCM, therefore cannot relies on Push.
|
||||
|
||||
Also Google's recommended background processing method cannot be applied. This is because all of these methods are affected by IDLE modes, and will result on the user not being notified at all when the app is in a Doze mode (only in maintenance windows that could happens only after hours).
|
||||
|
||||
@@ -262,7 +262,7 @@ F-Droid version will schedule alarms that will then trigger a Broadcast Receiver
|
||||
|
||||
Depending on the system status (or device make), it is still possible that the app is not given enough time to launch the service, or that the radio is still turned off thus preventing the sync to success (that's why Alarms are not recommended for network related tasks).
|
||||
|
||||
That is why on RiotX F-Droid, the broadcast receiver will acquire a temporary WAKE_LOCK for several seconds (thus securing cpu/network), and launch the service in foreground. The service performs the sync.
|
||||
That is why on Element F-Droid, the broadcast receiver will acquire a temporary WAKE_LOCK for several seconds (thus securing cpu/network), and launch the service in foreground. The service performs the sync.
|
||||
|
||||
Note that foreground services require to put a notification informing the user that the app is doing something even if not launched).
|
||||
|
||||
|
@@ -132,7 +132,7 @@ It's worth noting that the response from the homeserver contains the userId of A
|
||||
|
||||
### Login with Msisdn
|
||||
|
||||
Not supported yet in RiotX
|
||||
Not supported yet in Element
|
||||
|
||||
### Login with SSO
|
||||
|
||||
@@ -155,9 +155,9 @@ Not supported yet in RiotX
|
||||
|
||||
In this case, the user can click on "Sign in with SSO" and the native web browser, or a ChromeCustomTab if the device supports it, will be launched on the page
|
||||
|
||||
> https://homeserver.with.sso/_matrix/client/r0/login/sso/redirect?redirectUrl=riotx%3A%2F%2Friotx
|
||||
> https://homeserver.with.sso/_matrix/client/r0/login/sso/redirect?redirectUrl=element%3A%2F%element
|
||||
|
||||
The parameter `redirectUrl` is set to `riotx://riotx`.
|
||||
The parameter `redirectUrl` is set to `element://element`.
|
||||
|
||||
ChromeCustomTabs are an intermediate way to display a WebPage, between a WebView and using the external browser. More info can be found [here](https://developer.chrome.com/multidevice/android/customtabs)
|
||||
|
||||
@@ -167,9 +167,9 @@ During the process, user may be asked to validate an email by clicking on a link
|
||||
|
||||
Once the process is finished, the web page will call the `redirectUrl` with an extra parameter `loginToken`
|
||||
|
||||
> riotx://riotx?loginToken=MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy
|
||||
> element://element?loginToken=MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy
|
||||
|
||||
This navigation is intercepted by RiotX by the `LoginActivity`, which will then ask the homeserver to convert this `loginToken` to an access token
|
||||
This navigation is intercepted by Element by the `LoginActivity`, which will then ask the homeserver to convert this `loginToken` to an access token
|
||||
|
||||
> curl -X POST --data $'{"type":"m.login.token","token":"MDAxOWxvY2F0aW9uIG1vemlsbGEub3JnCjAwMTNpZGVudGlmaWVy"}' 'https://homeserver.with.sso/_matrix/client/r0/login'
|
||||
|
||||
|
@@ -1,5 +1,6 @@
|
||||
Useful links:
|
||||
- https://codelabs.developers.google.com/codelabs/webrtc-web/#0
|
||||
- http://webrtc.github.io/webrtc-org/native-code/android/
|
||||
|
||||
|
||||
╔════════════════════════════════════════════════╗
|
||||
@@ -25,7 +26,7 @@ Useful links:
|
||||
│ │ │ │ │ mx event │ │ │ │
|
||||
│ │ │ └────────────────────┘ │ │ │ │
|
||||
│ │ │ │ │ │ │
|
||||
│ Riot.im │ │ │ │ │ Riot.im │
|
||||
│ Element │ │ │ │ │ Element │
|
||||
┌──│ App │ │ │ │ │ App │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
@@ -102,7 +103,7 @@ Useful links:
|
||||
│ │ ┌────┐ │ │ └────────────────────┘ │ │ │
|
||||
│ │ │ 3 │ │ ┌────────────────────┐ │ │ │ │
|
||||
│ │──────┴────┴───────┼──────────────────┼─▶│ m.call.candidates │ │ │ │
|
||||
│ Riot.im │ │ │ mx event │ │ │ │ Riot.im │
|
||||
│ Element │ │ │ mx event │ │ │ │ Element │
|
||||
│ App │ │ │ └────────────────────┘ │ │ App │
|
||||
│ │ │ │ │ │ │
|
||||
│ │ │ │ │ │ │
|
||||
@@ -194,9 +195,9 @@ Useful links:
|
||||
│ │ │ m.call.invite │───┼────────────────────────────┬────┬───▶│ │
|
||||
┌─────────────────┐ │ │ mx event │ │ │ │ 1 │ │ │
|
||||
│ │ │ │ └────────────────────┘ │ └────┘ │ │
|
||||
│ │ │ ┌────────────────────┐ │ │ │ Riot.im │
|
||||
│ │ │ ┌────────────────────┐ │ │ │ Element │
|
||||
│ │ │ │ │ m.call.candidates │ │ │ App │
|
||||
│ Riot.im │ │ │ mx event │ │ │ │ │
|
||||
│ Element │ │ │ mx event │ │ │ │ │
|
||||
│ App │ │ │ └────────────────────┘ │ │ │
|
||||
│ │ │ ┌────────────────────┐◀──┼─────────────────┼───┬────┬───────────┤ │
|
||||
│ │◀──────────────────┼──────────────────┼──│ m.call.answer │ │ │ 4 │ └──┬──────────────┘
|
||||
@@ -274,7 +275,7 @@ Useful links:
|
||||
│ │ │ │ └────────────────────┘ │ │ │
|
||||
│ │ │ ┌────────────────────┐ │ │ │ │
|
||||
│ │ │ │ │ m.call.candidates │ │ │ │
|
||||
│ Riot.im │ │ │ mx event │ │ │ │ Riot.im │
|
||||
│ Element │ │ │ mx event │ │ │ │ Element │
|
||||
│ App │ │ │ └────────────────────┘ │ ┌────┐ │ App │
|
||||
│ │ │ ┌────────────────────┐ │ │ │ 3 │ │ │
|
||||
│ │◀──────────────────┼┐ │ │ m.call.answer │ │ ┌───────┴────┴────────│ │
|
||||
@@ -369,7 +370,7 @@ Useful links:
|
||||
│ │ │ │ │
|
||||
│ │ │ │ │
|
||||
│ │ │ │ │
|
||||
│ Riot.im │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ Riot.im │
|
||||
│ Element │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ Element │
|
||||
│ App │ │ App │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
|
@@ -8,7 +8,7 @@
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
android.enableJetifier=true
|
||||
android.useAndroidX=true
|
||||
org.gradle.jvmargs=-Xmx8192m
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
|
@@ -39,7 +39,7 @@ dependencies {
|
||||
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
|
||||
// Paging
|
||||
implementation "androidx.paging:paging-runtime-ktx:2.1.0"
|
||||
implementation "androidx.paging:paging-runtime-ktx:2.1.2"
|
||||
|
||||
// Logging
|
||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||
|
@@ -19,6 +19,7 @@ package im.vector.matrix.rx
|
||||
import android.net.Uri
|
||||
import im.vector.matrix.android.api.query.QueryStringValue
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.identity.ThreePid
|
||||
import im.vector.matrix.android.api.session.room.Room
|
||||
import im.vector.matrix.android.api.session.room.members.RoomMemberQueryParams
|
||||
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
|
||||
@@ -71,6 +72,13 @@ class RxRoom(private val room: Room) {
|
||||
}
|
||||
}
|
||||
|
||||
fun liveStateEvents(eventTypes: Set<String>): Observable<List<Event>> {
|
||||
return room.getStateEventsLive(eventTypes).asObservable()
|
||||
.startWithCallable {
|
||||
room.getStateEvents(eventTypes)
|
||||
}
|
||||
}
|
||||
|
||||
fun liveReadMarker(): Observable<Optional<String>> {
|
||||
return room.getReadMarkerLive().asObservable()
|
||||
}
|
||||
@@ -104,6 +112,10 @@ class RxRoom(private val room: Room) {
|
||||
room.invite(userId, reason, it)
|
||||
}
|
||||
|
||||
fun invite3pid(threePid: ThreePid): Completable = completableBuilder<Unit> {
|
||||
room.invite3pid(threePid, it)
|
||||
}
|
||||
|
||||
fun updateTopic(topic: String): Completable = completableBuilder<Unit> {
|
||||
room.updateTopic(topic, it)
|
||||
}
|
||||
|
@@ -17,14 +17,20 @@
|
||||
package im.vector.matrix.rx
|
||||
|
||||
import androidx.paging.PagedList
|
||||
import im.vector.matrix.android.api.extensions.orFalse
|
||||
import im.vector.matrix.android.api.query.QueryStringValue
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
||||
import im.vector.matrix.android.api.session.group.GroupSummaryQueryParams
|
||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||
import im.vector.matrix.android.api.session.identity.ThreePid
|
||||
import im.vector.matrix.android.api.session.pushers.Pusher
|
||||
import im.vector.matrix.android.api.session.room.RoomSummaryQueryParams
|
||||
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||
import im.vector.matrix.android.api.session.sync.SyncState
|
||||
@@ -36,9 +42,10 @@ import im.vector.matrix.android.api.util.toOptional
|
||||
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
|
||||
import im.vector.matrix.android.api.session.accountdata.UserAccountDataEvent
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.functions.Function3
|
||||
|
||||
class RxSession(private val session: Session) {
|
||||
|
||||
@@ -165,6 +172,42 @@ class RxSession(private val session: Session) {
|
||||
session.widgetService().getRoomWidgets(roomId, widgetId, widgetTypes, excludedTypes)
|
||||
}
|
||||
}
|
||||
|
||||
fun liveRoomChangeMembershipState(): Observable<Map<String, ChangeMembershipState>> {
|
||||
return session.getChangeMembershipsLive().asObservable()
|
||||
}
|
||||
|
||||
fun liveSecretSynchronisationInfo(): Observable<SecretsSynchronisationInfo> {
|
||||
return Observable.combineLatest<List<UserAccountDataEvent>, Optional<MXCrossSigningInfo>, Optional<PrivateKeysInfo>, SecretsSynchronisationInfo>(
|
||||
liveAccountData(setOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME)),
|
||||
liveCrossSigningInfo(session.myUserId),
|
||||
liveCrossSigningPrivateKeys(),
|
||||
Function3 { _, crossSigningInfo, pInfo ->
|
||||
// first check if 4S is already setup
|
||||
val is4SSetup = session.sharedSecretStorageService.isRecoverySetup()
|
||||
val isCrossSigningEnabled = crossSigningInfo.getOrNull() != null
|
||||
val isCrossSigningTrusted = crossSigningInfo.getOrNull()?.isTrusted() == true
|
||||
val allPrivateKeysKnown = pInfo.getOrNull()?.allKnown().orFalse()
|
||||
|
||||
val keysBackupService = session.cryptoService().keysBackupService()
|
||||
val currentBackupVersion = keysBackupService.currentBackupVersion
|
||||
val megolmBackupAvailable = currentBackupVersion != null
|
||||
val savedBackupKey = keysBackupService.getKeyBackupRecoveryKeyInfo()
|
||||
|
||||
val megolmKeyKnown = savedBackupKey?.version == currentBackupVersion
|
||||
SecretsSynchronisationInfo(
|
||||
isBackupSetup = is4SSetup,
|
||||
isCrossSigningEnabled = isCrossSigningEnabled,
|
||||
isCrossSigningTrusted = isCrossSigningTrusted,
|
||||
allPrivateKeysKnown = allPrivateKeysKnown,
|
||||
megolmBackupAvailable = megolmBackupAvailable,
|
||||
megolmSecretKnown = megolmKeyKnown,
|
||||
isMegolmKeyIn4S = session.sharedSecretStorageService.isMegolmKeyInBackup()
|
||||
)
|
||||
}
|
||||
)
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
}
|
||||
|
||||
fun Session.rx(): RxSession {
|
||||
|
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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 im.vector.matrix.rx
|
||||
|
||||
data class SecretsSynchronisationInfo(
|
||||
val isBackupSetup: Boolean,
|
||||
val isCrossSigningEnabled: Boolean,
|
||||
val isCrossSigningTrusted: Boolean,
|
||||
val allPrivateKeysKnown: Boolean,
|
||||
val megolmBackupAvailable: Boolean,
|
||||
val megolmSecretKnown: Boolean,
|
||||
val isMegolmKeyIn4S: Boolean
|
||||
)
|
@@ -51,7 +51,6 @@ android {
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
||||
debug {
|
||||
// Set to true to log privacy or sensible data, such as token
|
||||
buildConfigField "boolean", "LOG_PRIVATE_DATA", project.property("vector.debugPrivateData")
|
||||
@@ -117,20 +116,22 @@ dependencies {
|
||||
def markwon_version = '3.1.0'
|
||||
def daggerVersion = '2.25.4'
|
||||
def work_version = '2.3.3'
|
||||
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.1.0"
|
||||
implementation "androidx.core:core-ktx:1.3.0"
|
||||
|
||||
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
|
||||
|
||||
// Network
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.6.2'
|
||||
implementation 'com.squareup.retrofit2:converter-moshi:2.6.2'
|
||||
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
|
||||
implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version"
|
||||
implementation "com.squareup.retrofit2:converter-scalars:$retrofit_version"
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.2.2'
|
||||
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2'
|
||||
implementation "com.squareup.moshi:moshi-adapters:$moshi_version"
|
||||
@@ -205,5 +206,4 @@ dependencies {
|
||||
androidTestImplementation 'net.lachlanmckee:timber-junit-rule:1.0.1'
|
||||
|
||||
androidTestUtil 'androidx.test:orchestrator:1.2.0'
|
||||
|
||||
}
|
||||
|
@@ -65,7 +65,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
||||
val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
|
||||
|
||||
val roomId = mTestHelper.doSync<String> {
|
||||
aliceSession.createRoom(CreateRoomParams(name = "MyRoom"), it)
|
||||
aliceSession.createRoom(CreateRoomParams().apply { name = "MyRoom" }, it)
|
||||
}
|
||||
|
||||
if (encryptedRoom) {
|
||||
@@ -175,7 +175,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
||||
}
|
||||
|
||||
mTestHelper.doSync<Unit> {
|
||||
samSession.joinRoom(room.roomId, null, it)
|
||||
samSession.joinRoom(room.roomId, null, emptyList(), it)
|
||||
}
|
||||
|
||||
return samSession
|
||||
@@ -286,9 +286,11 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
||||
fun createDM(alice: Session, bob: Session): String {
|
||||
val roomId = mTestHelper.doSync<String> {
|
||||
alice.createRoom(
|
||||
CreateRoomParams(invitedUserIds = listOf(bob.myUserId))
|
||||
.setDirectMessage()
|
||||
.enableEncryptionIfInvitedUsersSupportIt(),
|
||||
CreateRoomParams().apply {
|
||||
invitedUserIds.add(bob.myUserId)
|
||||
setDirectMessage()
|
||||
enableEncryptionIfInvitedUsersSupportIt = true
|
||||
},
|
||||
it
|
||||
)
|
||||
}
|
||||
|
@@ -66,7 +66,10 @@ class KeyShareTests : InstrumentedTest {
|
||||
// Create an encrypted room and add a message
|
||||
val roomId = mTestHelper.doSync<String> {
|
||||
aliceSession.createRoom(
|
||||
CreateRoomParams(RoomDirectoryVisibility.PRIVATE).enableEncryptionWithAlgorithm(true),
|
||||
CreateRoomParams().apply {
|
||||
visibility = RoomDirectoryVisibility.PRIVATE
|
||||
enableEncryption()
|
||||
},
|
||||
it
|
||||
)
|
||||
}
|
||||
@@ -285,7 +288,7 @@ class KeyShareTests : InstrumentedTest {
|
||||
mTestHelper.waitWithLatch(60_000) { latch ->
|
||||
val keysBackupService = aliceSession2.cryptoService().keysBackupService()
|
||||
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
Log.d("#TEST", "Recovery :${ keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey}")
|
||||
Log.d("#TEST", "Recovery :${keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey}")
|
||||
keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey == creationInfo.recoveryKey
|
||||
}
|
||||
}
|
||||
|
@@ -35,7 +35,7 @@ import im.vector.matrix.android.common.TestMatrixCallback
|
||||
import im.vector.matrix.android.internal.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
|
||||
import im.vector.matrix.android.internal.crypto.secrets.DefaultSharedSecretStorageService
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
|
||||
import im.vector.matrix.android.api.session.accountdata.UserAccountDataEvent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
@@ -33,7 +33,7 @@ data class MatrixConfiguration(
|
||||
),
|
||||
/**
|
||||
* Optional proxy to connect to the matrix servers
|
||||
* You can create one using for instance Proxy(proxyType, InetSocketAddress(hostname, port)
|
||||
* You can create one using for instance Proxy(proxyType, InetSocketAddress.createUnresolved(hostname, port)
|
||||
*/
|
||||
val proxy: Proxy? = null
|
||||
) {
|
||||
|
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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 im.vector.matrix.android.api.extensions
|
||||
|
||||
fun CharSequence.ensurePrefix(prefix: CharSequence): CharSequence {
|
||||
return when {
|
||||
startsWith(prefix) -> this
|
||||
else -> "$prefix$this"
|
||||
}
|
||||
}
|
@@ -20,6 +20,7 @@ interface LegacySessionImporter {
|
||||
|
||||
/**
|
||||
* Will eventually import a session created by the legacy app.
|
||||
* @return true if a session has been imported
|
||||
*/
|
||||
fun process()
|
||||
fun process(): Boolean
|
||||
}
|
||||
|
@@ -47,6 +47,7 @@ import im.vector.matrix.android.api.session.terms.TermsService
|
||||
import im.vector.matrix.android.api.session.typing.TypingUsersTracker
|
||||
import im.vector.matrix.android.api.session.user.UserService
|
||||
import im.vector.matrix.android.api.session.widgets.WidgetService
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
/**
|
||||
* This interface defines interactions with a session.
|
||||
@@ -205,6 +206,13 @@ interface Session :
|
||||
*/
|
||||
fun removeListener(listener: Listener)
|
||||
|
||||
/**
|
||||
* Will return a OkHttpClient which will manage pinned certificates and Proxy if configured.
|
||||
* It will not add any access-token to the request.
|
||||
* So it is exposed to let the app be able to download image with Glide or any other libraries which accept an OkHttp client.
|
||||
*/
|
||||
fun getOkHttpClient(): OkHttpClient
|
||||
|
||||
/**
|
||||
* A global session listener to get notified for some events.
|
||||
*/
|
||||
|
@@ -21,7 +21,6 @@ import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.events.model.Content
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
|
||||
|
||||
interface AccountDataService {
|
||||
/**
|
||||
|
@@ -14,14 +14,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.session.sync.model.accountdata
|
||||
package im.vector.matrix.android.api.session.accountdata
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.internal.session.integrationmanager.AllowedWidgetsContent
|
||||
import im.vector.matrix.android.api.session.events.model.Content
|
||||
|
||||
/**
|
||||
* This is a simplified Event with just a type and a content.
|
||||
* Currently used types are defined in [UserAccountDataTypes].
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class UserAccountDataAllowedWidgets(
|
||||
@Json(name = "type") override val type: String = TYPE_ALLOWED_WIDGETS,
|
||||
@Json(name = "content") val content: AllowedWidgetsContent
|
||||
) : UserAccountData()
|
||||
data class UserAccountDataEvent(
|
||||
@Json(name = "type") val type: String,
|
||||
@Json(name = "content") val content: Content
|
||||
)
|
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.accountdata
|
||||
|
||||
object UserAccountDataTypes {
|
||||
|
||||
const val TYPE_IGNORED_USER_LIST = "m.ignored_user_list"
|
||||
const val TYPE_DIRECT_MESSAGES = "m.direct"
|
||||
const val TYPE_BREADCRUMBS = "im.vector.setting.breadcrumbs" // Was previously "im.vector.riot.breadcrumb_rooms"
|
||||
const val TYPE_PREVIEW_URLS = "org.matrix.preview_urls"
|
||||
const val TYPE_WIDGETS = "m.widgets"
|
||||
const val TYPE_PUSH_RULES = "m.push_rules"
|
||||
const val TYPE_INTEGRATION_PROVISIONING = "im.vector.setting.integration_provisioning"
|
||||
const val TYPE_ALLOWED_WIDGETS = "im.vector.setting.allowed_widgets"
|
||||
const val TYPE_IDENTITY_SERVER = "m.identity_server"
|
||||
const val TYPE_ACCEPTED_TERMS = "m.accepted_terms"
|
||||
}
|
@@ -61,6 +61,8 @@ interface CrossSigningService {
|
||||
|
||||
fun canCrossSign(): Boolean
|
||||
|
||||
fun allPrivateKeysKnown(): Boolean
|
||||
|
||||
fun trustUser(otherUserId: String,
|
||||
callback: MatrixCallback<Unit>)
|
||||
|
||||
|
@@ -21,9 +21,11 @@ import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
import im.vector.matrix.android.api.util.JsonDict
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import org.json.JSONObject
|
||||
import timber.log.Timber
|
||||
@@ -240,6 +242,18 @@ fun Event.isFileMessage(): Boolean {
|
||||
return getClearType() == EventType.MESSAGE
|
||||
&& when (getClearContent()?.toModel<MessageContent>()?.msgType) {
|
||||
MessageType.MSGTYPE_FILE -> true
|
||||
else -> false
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun Event.getRelationContent(): RelationDefaultContent? {
|
||||
return if (isEncrypted()) {
|
||||
content.toModel<EncryptedEventContent>()?.relatesTo
|
||||
} else {
|
||||
content.toModel<MessageContent>()?.relatesTo
|
||||
}
|
||||
}
|
||||
|
||||
fun Event.isReply(): Boolean {
|
||||
return getRelationContent()?.inReplyTo?.eventId != null
|
||||
}
|
||||
|
@@ -39,5 +39,10 @@ data class UnsignedData(
|
||||
* Optional. The previous content for this event. If there is no previous content, this key will be missing.
|
||||
*/
|
||||
@Json(name = "prev_content") val prevContent: Map<String, Any>? = null,
|
||||
@Json(name = "m.relations") val relations: AggregatedRelations? = null
|
||||
@Json(name = "m.relations") val relations: AggregatedRelations? = null,
|
||||
/**
|
||||
* Optional. The eventId of the previous state event being replaced.
|
||||
*/
|
||||
@Json(name = "replaces_state") val replacesState: String? = null
|
||||
|
||||
)
|
||||
|
@@ -16,9 +16,20 @@
|
||||
|
||||
package im.vector.matrix.android.api.session.group
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
|
||||
/**
|
||||
* This interface defines methods to interact within a group.
|
||||
*/
|
||||
interface Group {
|
||||
val groupId: String
|
||||
|
||||
/**
|
||||
* This methods allows you to refresh data about this group. It will be reflected on the GroupSummary.
|
||||
* The SDK also takes care of refreshing group data every hour.
|
||||
* @param callback : the matrix callback to be notified of success or failure
|
||||
* @return a Cancelable to be able to cancel requests.
|
||||
*/
|
||||
fun fetchGroupData(callback: MatrixCallback<Unit>): Cancelable
|
||||
}
|
||||
|
@@ -34,13 +34,6 @@ interface RoomDirectoryService {
|
||||
publicRoomsParams: PublicRoomsParams,
|
||||
callback: MatrixCallback<PublicRoomsResponse>): Cancelable
|
||||
|
||||
/**
|
||||
* Join a room by id, or room alias
|
||||
*/
|
||||
fun joinRoom(roomIdOrAlias: String,
|
||||
reason: String? = null,
|
||||
callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Fetches the overall metadata about protocols supported by the homeserver.
|
||||
* Includes both the available protocols and all fields required for queries against each protocol.
|
||||
|
@@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session.room
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.room.members.ChangeMembershipState
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
@@ -104,5 +105,13 @@ interface RoomService {
|
||||
searchOnServer: Boolean,
|
||||
callback: MatrixCallback<Optional<String>>): Cancelable
|
||||
|
||||
fun getExistingDirectRoomWithUser(otherUserId: String) : Room?
|
||||
/**
|
||||
* Return a live data of all local changes membership that happened since the session has been opened.
|
||||
* It allows you to track this in your client to known what is currently being processed by the SDK.
|
||||
* It won't know anything about change being done in other client.
|
||||
* Keys are roomId or roomAlias, depending of what you used as parameter for the join/leave action
|
||||
*/
|
||||
fun getChangeMembershipsLive(): LiveData<Map<String, ChangeMembershipState>>
|
||||
|
||||
fun getExistingDirectRoomWithUser(otherUserId: String): Room?
|
||||
}
|
||||
|
@@ -28,6 +28,7 @@ fun roomSummaryQueryParams(init: (RoomSummaryQueryParams.Builder.() -> Unit) = {
|
||||
* [im.vector.matrix.android.api.session.room.Room] and [im.vector.matrix.android.api.session.room.RoomService]
|
||||
*/
|
||||
data class RoomSummaryQueryParams(
|
||||
val roomId: QueryStringValue,
|
||||
val displayName: QueryStringValue,
|
||||
val canonicalAlias: QueryStringValue,
|
||||
val memberships: List<Membership>
|
||||
@@ -35,11 +36,13 @@ data class RoomSummaryQueryParams(
|
||||
|
||||
class Builder {
|
||||
|
||||
var roomId: QueryStringValue = QueryStringValue.IsNotEmpty
|
||||
var displayName: QueryStringValue = QueryStringValue.IsNotEmpty
|
||||
var canonicalAlias: QueryStringValue = QueryStringValue.NoCondition
|
||||
var memberships: List<Membership> = Membership.all()
|
||||
|
||||
fun build() = RoomSummaryQueryParams(
|
||||
roomId = roomId,
|
||||
displayName = displayName,
|
||||
canonicalAlias = canonicalAlias,
|
||||
memberships = memberships
|
||||
|
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.room.members
|
||||
|
||||
sealed class ChangeMembershipState() {
|
||||
object Unknown : ChangeMembershipState()
|
||||
object Joining : ChangeMembershipState()
|
||||
data class FailedJoining(val throwable: Throwable) : ChangeMembershipState()
|
||||
object Joined : ChangeMembershipState()
|
||||
object Leaving : ChangeMembershipState()
|
||||
data class FailedLeaving(val throwable: Throwable) : ChangeMembershipState()
|
||||
object Left : ChangeMembershipState()
|
||||
|
||||
fun isInProgress() = this is Joining || this is Leaving
|
||||
|
||||
fun isSuccessful() = this is Joined || this is Left
|
||||
|
||||
fun isFailed() = this is FailedJoining || this is FailedLeaving
|
||||
}
|
@@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session.room.members
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.identity.ThreePid
|
||||
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
|
||||
@@ -63,6 +64,12 @@ interface MembershipService {
|
||||
reason: String? = null,
|
||||
callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Invite a user with email or phone number in the room
|
||||
*/
|
||||
fun invite3pid(threePid: ThreePid,
|
||||
callback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Ban a user from the room
|
||||
*/
|
||||
|
@@ -53,7 +53,8 @@ data class RoomSummary constructor(
|
||||
val typingUsers: List<SenderInfo>,
|
||||
val inviterId: String? = null,
|
||||
val breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS,
|
||||
val roomEncryptionTrustLevel: RoomEncryptionTrustLevel? = null
|
||||
val roomEncryptionTrustLevel: RoomEncryptionTrustLevel? = null,
|
||||
val hasFailedSending: Boolean = false
|
||||
) {
|
||||
|
||||
val isVersioned: Boolean
|
||||
@@ -66,7 +67,7 @@ data class RoomSummary constructor(
|
||||
get() = tags.any { it.name == RoomTag.ROOM_TAG_FAVOURITE }
|
||||
|
||||
val canStartCall: Boolean
|
||||
get() = isDirect && joinedMembersCount == 2
|
||||
get() = joinedMembersCount == 2
|
||||
|
||||
companion object {
|
||||
const val NOT_IN_BREADCRUMBS = -1
|
||||
|
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.room.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Class representing the EventType.STATE_ROOM_THIRD_PARTY_INVITE state event content
|
||||
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#m-room-third-party-invite
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class RoomThirdPartyInviteContent(
|
||||
/**
|
||||
* Required. A user-readable string which represents the user who has been invited.
|
||||
* This should not contain the user's third party ID, as otherwise when the invite
|
||||
* is accepted it would leak the association between the matrix ID and the third party ID.
|
||||
*/
|
||||
@Json(name = "display_name") val displayName: String,
|
||||
|
||||
/**
|
||||
* Required. A URL which can be fetched, with querystring public_key=public_key, to validate
|
||||
* whether the key has been revoked. The URL must return a JSON object containing a boolean property named 'valid'.
|
||||
*/
|
||||
@Json(name = "key_validity_url") val keyValidityUrl: String,
|
||||
|
||||
/**
|
||||
* Required. A base64-encoded ed25519 key with which token must be signed (though a signature from any entry in
|
||||
* public_keys is also sufficient). This exists for backwards compatibility.
|
||||
*/
|
||||
@Json(name = "public_key") val publicKey: String,
|
||||
|
||||
/**
|
||||
* Keys with which the token may be signed.
|
||||
*/
|
||||
@Json(name = "public_keys") val publicKeys: List<PublicKeys> = emptyList()
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PublicKeys(
|
||||
/**
|
||||
* An optional URL which can be fetched, with querystring public_key=public_key, to validate whether the key
|
||||
* has been revoked. The URL must return a JSON object containing a boolean property named 'valid'. If this URL
|
||||
* is absent, the key must be considered valid indefinitely.
|
||||
*/
|
||||
@Json(name = "key_validity_url") val keyValidityUrl: String? = null,
|
||||
|
||||
/**
|
||||
* Required. A base-64 encoded ed25519 key with which token may be signed.
|
||||
*/
|
||||
@Json(name = "public_key") val publicKey: String
|
||||
)
|
@@ -59,5 +59,5 @@ data class CallInviteContent(
|
||||
}
|
||||
}
|
||||
|
||||
fun isVideo(): Boolean = offer?.sdp?.contains(Offer.SDP_VIDEO) == true
|
||||
fun isVideo() = offer?.sdp?.contains(Offer.SDP_VIDEO) == true
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
* 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.
|
||||
@@ -16,253 +16,102 @@
|
||||
|
||||
package im.vector.matrix.android.api.session.room.model.create
|
||||
|
||||
import android.util.Patterns
|
||||
import androidx.annotation.CheckResult
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.api.MatrixPatterns.isUserId
|
||||
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toContent
|
||||
import im.vector.matrix.android.api.session.identity.ThreePid
|
||||
import im.vector.matrix.android.api.session.room.model.PowerLevelsContent
|
||||
import im.vector.matrix.android.api.session.room.model.RoomDirectoryVisibility
|
||||
import im.vector.matrix.android.api.session.room.model.RoomHistoryVisibility
|
||||
import im.vector.matrix.android.internal.auth.data.ThreePidMedium
|
||||
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Parameter to create a room, with facilities functions to configure it
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CreateRoomParams(
|
||||
/**
|
||||
* A public visibility indicates that the room will be shown in the published room list.
|
||||
* A private visibility will hide the room from the published room list.
|
||||
* Rooms default to private visibility if this key is not included.
|
||||
* NB: This should not be confused with join_rules which also uses the word public. One of: ["public", "private"]
|
||||
*/
|
||||
@Json(name = "visibility")
|
||||
val visibility: RoomDirectoryVisibility? = null,
|
||||
|
||||
/**
|
||||
* The desired room alias local part. If this is included, a room alias will be created and mapped to the newly created room.
|
||||
* The alias will belong on the same homeserver which created the room.
|
||||
* For example, if this was set to "foo" and sent to the homeserver "example.com" the complete room alias would be #foo:example.com.
|
||||
*/
|
||||
@Json(name = "room_alias_name")
|
||||
val roomAliasName: String? = null,
|
||||
|
||||
/**
|
||||
* If this is included, an m.room.name event will be sent into the room to indicate the name of the room.
|
||||
* See Room Events for more information on m.room.name.
|
||||
*/
|
||||
@Json(name = "name")
|
||||
val name: String? = null,
|
||||
|
||||
/**
|
||||
* If this is included, an m.room.topic event will be sent into the room to indicate the topic for the room.
|
||||
* See Room Events for more information on m.room.topic.
|
||||
*/
|
||||
@Json(name = "topic")
|
||||
val topic: String? = null,
|
||||
|
||||
/**
|
||||
* A list of user IDs to invite to the room.
|
||||
* This will tell the server to invite everyone in the list to the newly created room.
|
||||
*/
|
||||
@Json(name = "invite")
|
||||
val invitedUserIds: List<String>? = null,
|
||||
|
||||
/**
|
||||
* A list of objects representing third party IDs to invite into the room.
|
||||
*/
|
||||
@Json(name = "invite_3pid")
|
||||
val invite3pids: List<Invite3Pid>? = null,
|
||||
|
||||
/**
|
||||
* Extra keys to be added to the content of the m.room.create.
|
||||
* The server will clobber the following keys: creator.
|
||||
* Future versions of the specification may allow the server to clobber other keys.
|
||||
*/
|
||||
@Json(name = "creation_content")
|
||||
val creationContent: Any? = null,
|
||||
|
||||
/**
|
||||
* A list of state events to set in the new room.
|
||||
* This allows the user to override the default state events set in the new room.
|
||||
* The expected format of the state events are an object with type, state_key and content keys set.
|
||||
* Takes precedence over events set by presets, but gets overridden by name and topic keys.
|
||||
*/
|
||||
@Json(name = "initial_state")
|
||||
val initialStates: List<Event>? = null,
|
||||
|
||||
/**
|
||||
* Convenience parameter for setting various default state events based on a preset. Must be either:
|
||||
* private_chat => join_rules is set to invite. history_visibility is set to shared.
|
||||
* trusted_private_chat => join_rules is set to invite. history_visibility is set to shared. All invitees are given the same power level as the
|
||||
* room creator.
|
||||
* public_chat: => join_rules is set to public. history_visibility is set to shared.
|
||||
*/
|
||||
@Json(name = "preset")
|
||||
val preset: CreateRoomPreset? = null,
|
||||
|
||||
/**
|
||||
* This flag makes the server set the is_direct flag on the m.room.member events sent to the users in invite and invite_3pid.
|
||||
* See Direct Messaging for more information.
|
||||
*/
|
||||
@Json(name = "is_direct")
|
||||
val isDirect: Boolean? = null,
|
||||
|
||||
/**
|
||||
* The power level content to override in the default power level event
|
||||
*/
|
||||
@Json(name = "power_level_content_override")
|
||||
val powerLevelContentOverride: PowerLevelsContent? = null
|
||||
) {
|
||||
@Transient
|
||||
internal var enableEncryptionIfInvitedUsersSupportIt: Boolean = false
|
||||
private set
|
||||
// TODO Give a way to include other initial states
|
||||
class CreateRoomParams {
|
||||
/**
|
||||
* A public visibility indicates that the room will be shown in the published room list.
|
||||
* A private visibility will hide the room from the published room list.
|
||||
* Rooms default to private visibility if this key is not included.
|
||||
* NB: This should not be confused with join_rules which also uses the word public. One of: ["public", "private"]
|
||||
*/
|
||||
var visibility: RoomDirectoryVisibility? = null
|
||||
|
||||
/**
|
||||
* After calling this method, when the room will be created, if cross-signing is enabled and we can get keys for every invited users,
|
||||
* The desired room alias local part. If this is included, a room alias will be created and mapped to the newly created room.
|
||||
* The alias will belong on the same homeserver which created the room.
|
||||
* For example, if this was set to "foo" and sent to the homeserver "example.com" the complete room alias would be #foo:example.com.
|
||||
*/
|
||||
var roomAliasName: String? = null
|
||||
|
||||
/**
|
||||
* If this is not null, an m.room.name event will be sent into the room to indicate the name of the room.
|
||||
* See Room Events for more information on m.room.name.
|
||||
*/
|
||||
var name: String? = null
|
||||
|
||||
/**
|
||||
* If this is not null, an m.room.topic event will be sent into the room to indicate the topic for the room.
|
||||
* See Room Events for more information on m.room.topic.
|
||||
*/
|
||||
var topic: String? = null
|
||||
|
||||
/**
|
||||
* A list of user IDs to invite to the room.
|
||||
* This will tell the server to invite everyone in the list to the newly created room.
|
||||
*/
|
||||
val invitedUserIds = mutableListOf<String>()
|
||||
|
||||
/**
|
||||
* A list of objects representing third party IDs to invite into the room.
|
||||
*/
|
||||
val invite3pids = mutableListOf<ThreePid>()
|
||||
|
||||
/**
|
||||
* If set to true, when the room will be created, if cross-signing is enabled and we can get keys for every invited users,
|
||||
* the encryption will be enabled on the created room
|
||||
* @param value true to activate this behavior.
|
||||
* @return this, to allow chaining methods
|
||||
*/
|
||||
fun enableEncryptionIfInvitedUsersSupportIt(value: Boolean = true): CreateRoomParams {
|
||||
enableEncryptionIfInvitedUsersSupportIt = value
|
||||
return this
|
||||
}
|
||||
var enableEncryptionIfInvitedUsersSupportIt: Boolean = false
|
||||
|
||||
/**
|
||||
* Add the crypto algorithm to the room creation parameters.
|
||||
*
|
||||
* @param enable true to enable encryption.
|
||||
* @param algorithm the algorithm, default to [MXCRYPTO_ALGORITHM_MEGOLM], which is actually the only supported algorithm for the moment
|
||||
* @return a modified copy of the CreateRoomParams object, or this if there is no modification
|
||||
* Convenience parameter for setting various default state events based on a preset. Must be either:
|
||||
* private_chat => join_rules is set to invite. history_visibility is set to shared.
|
||||
* trusted_private_chat => join_rules is set to invite. history_visibility is set to shared. All invitees are given the same power level as the
|
||||
* room creator.
|
||||
* public_chat: => join_rules is set to public. history_visibility is set to shared.
|
||||
*/
|
||||
@CheckResult
|
||||
fun enableEncryptionWithAlgorithm(enable: Boolean = true,
|
||||
algorithm: String = MXCRYPTO_ALGORITHM_MEGOLM): CreateRoomParams {
|
||||
// Remove the existing value if any.
|
||||
val newInitialStates = initialStates
|
||||
?.filter { it.type != EventType.STATE_ROOM_ENCRYPTION }
|
||||
|
||||
return if (algorithm == MXCRYPTO_ALGORITHM_MEGOLM) {
|
||||
if (enable) {
|
||||
val contentMap = mapOf("algorithm" to algorithm)
|
||||
|
||||
val algoEvent = Event(
|
||||
type = EventType.STATE_ROOM_ENCRYPTION,
|
||||
stateKey = "",
|
||||
content = contentMap.toContent()
|
||||
)
|
||||
|
||||
copy(
|
||||
initialStates = newInitialStates.orEmpty() + algoEvent
|
||||
)
|
||||
} else {
|
||||
return copy(
|
||||
initialStates = newInitialStates
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Timber.e("Unsupported algorithm: $algorithm")
|
||||
this
|
||||
}
|
||||
}
|
||||
var preset: CreateRoomPreset? = null
|
||||
|
||||
/**
|
||||
* Force the history visibility in the room creation parameters.
|
||||
*
|
||||
* @param historyVisibility the expected history visibility, set null to remove any existing value.
|
||||
* @return a modified copy of the CreateRoomParams object
|
||||
* This flag makes the server set the is_direct flag on the m.room.member events sent to the users in invite and invite_3pid.
|
||||
* See Direct Messaging for more information.
|
||||
*/
|
||||
@CheckResult
|
||||
fun setHistoryVisibility(historyVisibility: RoomHistoryVisibility?): CreateRoomParams {
|
||||
// Remove the existing value if any.
|
||||
val newInitialStates = initialStates
|
||||
?.filter { it.type != EventType.STATE_ROOM_HISTORY_VISIBILITY }
|
||||
var isDirect: Boolean? = null
|
||||
|
||||
if (historyVisibility != null) {
|
||||
val contentMap = mapOf("history_visibility" to historyVisibility)
|
||||
/**
|
||||
* Extra keys to be added to the content of the m.room.create.
|
||||
* The server will clobber the following keys: creator.
|
||||
* Future versions of the specification may allow the server to clobber other keys.
|
||||
*/
|
||||
var creationContent: Any? = null
|
||||
|
||||
val historyVisibilityEvent = Event(
|
||||
type = EventType.STATE_ROOM_HISTORY_VISIBILITY,
|
||||
stateKey = "",
|
||||
content = contentMap.toContent())
|
||||
|
||||
return copy(
|
||||
initialStates = newInitialStates.orEmpty() + historyVisibilityEvent
|
||||
)
|
||||
} else {
|
||||
return copy(
|
||||
initialStates = newInitialStates
|
||||
)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* The power level content to override in the default power level event
|
||||
*/
|
||||
var powerLevelContentOverride: PowerLevelsContent? = null
|
||||
|
||||
/**
|
||||
* Mark as a direct message room.
|
||||
* @return a modified copy of the CreateRoomParams object
|
||||
*/
|
||||
@CheckResult
|
||||
fun setDirectMessage(): CreateRoomParams {
|
||||
return copy(
|
||||
preset = CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT,
|
||||
isDirect = true
|
||||
)
|
||||
fun setDirectMessage() {
|
||||
preset = CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT
|
||||
isDirect = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if the created room can be a direct chat one.
|
||||
*
|
||||
* @return true if it is a direct chat
|
||||
* Supported value: MXCRYPTO_ALGORITHM_MEGOLM
|
||||
*/
|
||||
fun isDirect(): Boolean {
|
||||
return preset == CreateRoomPreset.PRESET_TRUSTED_PRIVATE_CHAT
|
||||
&& isDirect == true
|
||||
}
|
||||
var algorithm: String? = null
|
||||
private set
|
||||
|
||||
/**
|
||||
* @return the first invited user id
|
||||
*/
|
||||
fun getFirstInvitedUserId(): String? {
|
||||
return invitedUserIds?.firstOrNull() ?: invite3pids?.firstOrNull()?.address
|
||||
}
|
||||
var historyVisibility: RoomHistoryVisibility? = null
|
||||
|
||||
/**
|
||||
* Add some ids to the room creation
|
||||
* ids might be a matrix id or an email address.
|
||||
*
|
||||
* @param ids the participant ids to add.
|
||||
* @return a modified copy of the CreateRoomParams object
|
||||
*/
|
||||
@CheckResult
|
||||
fun addParticipantIds(hsConfig: HomeServerConnectionConfig,
|
||||
userId: String,
|
||||
ids: List<String>): CreateRoomParams {
|
||||
return copy(
|
||||
invite3pids = (invite3pids.orEmpty() + ids
|
||||
.takeIf { hsConfig.identityServerUri != null }
|
||||
?.filter { id -> Patterns.EMAIL_ADDRESS.matcher(id).matches() }
|
||||
?.map { id ->
|
||||
Invite3Pid(
|
||||
idServer = hsConfig.identityServerUri!!.host!!,
|
||||
medium = ThreePidMedium.EMAIL,
|
||||
address = id
|
||||
)
|
||||
}
|
||||
.orEmpty())
|
||||
.distinct(),
|
||||
invitedUserIds = (invitedUserIds.orEmpty() + ids
|
||||
.filter { id -> isUserId(id) }
|
||||
// do not invite oneself
|
||||
.filter { id -> id != userId })
|
||||
.distinct()
|
||||
)
|
||||
// TODO add phonenumbers when it will be available
|
||||
fun enableEncryption() {
|
||||
algorithm = MXCRYPTO_ALGORITHM_MEGOLM
|
||||
}
|
||||
}
|
||||
|
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.room.model.create
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Invite3Pid(
|
||||
/**
|
||||
* Required.
|
||||
* The hostname+port of the identity server which should be used for third party identifier lookups.
|
||||
*/
|
||||
@Json(name = "id_server")
|
||||
val idServer: String,
|
||||
|
||||
/**
|
||||
* Required.
|
||||
* The kind of address being passed in the address field, for example email.
|
||||
*/
|
||||
val medium: String,
|
||||
|
||||
/**
|
||||
* Required.
|
||||
* The invitee's third party identifier.
|
||||
*/
|
||||
val address: String
|
||||
)
|
@@ -25,7 +25,3 @@ interface MessageContent {
|
||||
val relatesTo: RelationDefaultContent?
|
||||
val newContent: Content?
|
||||
}
|
||||
|
||||
fun MessageContent?.isReply(): Boolean {
|
||||
return this?.relatesTo?.inReplyTo?.eventId != null
|
||||
}
|
||||
|
@@ -17,7 +17,6 @@
|
||||
|
||||
package im.vector.matrix.android.api.session.room.powerlevels
|
||||
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.room.model.PowerLevelsContent
|
||||
|
||||
/**
|
||||
@@ -124,59 +123,4 @@ class PowerLevelsHelper(private val powerLevelsContent: PowerLevelsContent) {
|
||||
else -> Role.Moderator.value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user have the necessary power level to change room name
|
||||
* @param userId the id of the user to check for.
|
||||
* @return true if able to change room name
|
||||
*/
|
||||
fun isUserAbleToChangeRoomName(userId: String): Boolean {
|
||||
val powerLevel = getUserPowerLevelValue(userId)
|
||||
val minPowerLevel = powerLevelsContent.events[EventType.STATE_ROOM_NAME] ?: powerLevelsContent.stateDefault
|
||||
return powerLevel >= minPowerLevel
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user have the necessary power level to change room topic
|
||||
* @param userId the id of the user to check for.
|
||||
* @return true if able to change room topic
|
||||
*/
|
||||
fun isUserAbleToChangeRoomTopic(userId: String): Boolean {
|
||||
val powerLevel = getUserPowerLevelValue(userId)
|
||||
val minPowerLevel = powerLevelsContent.events[EventType.STATE_ROOM_TOPIC] ?: powerLevelsContent.stateDefault
|
||||
return powerLevel >= minPowerLevel
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user have the necessary power level to change room canonical alias
|
||||
* @param userId the id of the user to check for.
|
||||
* @return true if able to change room canonical alias
|
||||
*/
|
||||
fun isUserAbleToChangeRoomCanonicalAlias(userId: String): Boolean {
|
||||
val powerLevel = getUserPowerLevelValue(userId)
|
||||
val minPowerLevel = powerLevelsContent.events[EventType.STATE_ROOM_CANONICAL_ALIAS] ?: powerLevelsContent.stateDefault
|
||||
return powerLevel >= minPowerLevel
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user have the necessary power level to change room history readability
|
||||
* @param userId the id of the user to check for.
|
||||
* @return true if able to change room history readability
|
||||
*/
|
||||
fun isUserAbleToChangeRoomHistoryReadability(userId: String): Boolean {
|
||||
val powerLevel = getUserPowerLevelValue(userId)
|
||||
val minPowerLevel = powerLevelsContent.events[EventType.STATE_ROOM_HISTORY_VISIBILITY] ?: powerLevelsContent.stateDefault
|
||||
return powerLevel >= minPowerLevel
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user have the necessary power level to change room avatar
|
||||
* @param userId the id of the user to check for.
|
||||
* @return true if able to change room avatar
|
||||
*/
|
||||
fun isUserAbleToChangeRoomAvatar(userId: String): Boolean {
|
||||
val powerLevel = getUserPowerLevelValue(userId)
|
||||
val minPowerLevel = powerLevelsContent.events[EventType.STATE_ROOM_AVATAR] ?: powerLevelsContent.stateDefault
|
||||
return powerLevel >= minPowerLevel
|
||||
}
|
||||
}
|
||||
|
@@ -20,15 +20,16 @@ import im.vector.matrix.android.BuildConfig
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.RelationType
|
||||
import im.vector.matrix.android.api.session.events.model.getRelationContent
|
||||
import im.vector.matrix.android.api.session.events.model.isReply
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.EventAnnotationsSummary
|
||||
import im.vector.matrix.android.api.session.room.model.ReadReceipt
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageStickerContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.isReply
|
||||
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
|
||||
import im.vector.matrix.android.api.session.room.sender.SenderInfo
|
||||
import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply
|
||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||
|
||||
/**
|
||||
* This data class is a wrapper around an Event. It allows to get useful data in the context of a timeline.
|
||||
@@ -88,11 +89,18 @@ data class TimelineEvent(
|
||||
*/
|
||||
fun TimelineEvent.hasBeenEdited() = annotations?.editSummary != null
|
||||
|
||||
/**
|
||||
* Get the relation content if any
|
||||
*/
|
||||
fun TimelineEvent.getRelationContent(): RelationDefaultContent? {
|
||||
return root.getRelationContent()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the eventId which was edited by this event if any
|
||||
*/
|
||||
fun TimelineEvent.getEditedEventId(): String? {
|
||||
return root.getClearContent().toModel<MessageContent>()?.relatesTo?.takeIf { it.type == RelationType.REPLACE }?.eventId
|
||||
return getRelationContent()?.takeIf { it.type == RelationType.REPLACE }?.eventId
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -121,11 +129,16 @@ fun TimelineEvent.getLastMessageBody(): String? {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if it's a reply
|
||||
*/
|
||||
fun TimelineEvent.isReply(): Boolean {
|
||||
return root.isReply()
|
||||
}
|
||||
|
||||
fun TimelineEvent.getTextEditableContent(): String? {
|
||||
val originalContent = root.getClearContent().toModel<MessageContent>() ?: return null
|
||||
val isReply = originalContent.isReply() || root.content.toModel<EncryptedEventContent>()?.relatesTo?.inReplyTo?.eventId != null
|
||||
val lastContent = getLastMessageContent()
|
||||
return if (isReply) {
|
||||
return if (isReply()) {
|
||||
return extractUsefulTextFromReply(lastContent?.body ?: "")
|
||||
} else {
|
||||
lastContent?.body ?: ""
|
||||
|
@@ -39,4 +39,6 @@ interface TimelineService {
|
||||
fun getTimeLineEvent(eventId: String): TimelineEvent?
|
||||
|
||||
fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>>
|
||||
|
||||
fun getAttachmentMessages() : List<TimelineEvent>
|
||||
}
|
||||
|
@@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session.securestorage
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.listeners.ProgressListener
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
||||
@@ -124,6 +125,13 @@ interface SharedSecretStorageService {
|
||||
) is IntegrityResult.Success
|
||||
}
|
||||
|
||||
fun isMegolmKeyInBackup(): Boolean {
|
||||
return checkShouldBeAbleToAccessSecrets(
|
||||
secretNames = listOf(KEYBACKUP_SECRET_SSSS_NAME),
|
||||
keyId = null
|
||||
) is IntegrityResult.Success
|
||||
}
|
||||
|
||||
fun checkShouldBeAbleToAccessSecrets(secretNames: List<String>, keyId: String?): IntegrityResult
|
||||
|
||||
fun requestSecret(name: String, myOtherDeviceId: String)
|
||||
|
@@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.auth.login
|
||||
import dagger.Lazy
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.internal.auth.AuthAPI
|
||||
import im.vector.matrix.android.internal.auth.SessionCreator
|
||||
@@ -27,6 +28,7 @@ import im.vector.matrix.android.internal.di.Unauthenticated
|
||||
import im.vector.matrix.android.internal.network.RetrofitFactory
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.network.httpclient.addSocketFactory
|
||||
import im.vector.matrix.android.internal.network.ssl.UnrecognizedCertificateException
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import okhttp3.OkHttpClient
|
||||
import javax.inject.Inject
|
||||
@@ -49,13 +51,28 @@ internal class DefaultDirectLoginTask @Inject constructor(
|
||||
|
||||
override suspend fun execute(params: DirectLoginTask.Params): Session {
|
||||
val client = buildClient(params.homeServerConnectionConfig)
|
||||
val authAPI = retrofitFactory.create(client, params.homeServerConnectionConfig.homeServerUri.toString())
|
||||
val homeServerUrl = params.homeServerConnectionConfig.homeServerUri.toString()
|
||||
|
||||
val authAPI = retrofitFactory.create(client, homeServerUrl)
|
||||
.create(AuthAPI::class.java)
|
||||
|
||||
val loginParams = PasswordLoginParams.userIdentifier(params.userId, params.password, params.deviceName)
|
||||
|
||||
val credentials = executeRequest<Credentials>(null) {
|
||||
apiCall = authAPI.login(loginParams)
|
||||
val credentials = try {
|
||||
executeRequest<Credentials>(null) {
|
||||
apiCall = authAPI.login(loginParams)
|
||||
}
|
||||
} catch (throwable: Throwable) {
|
||||
when (throwable) {
|
||||
is UnrecognizedCertificateException -> {
|
||||
throw Failure.UnrecognizedCertificateFailure(
|
||||
homeServerUrl,
|
||||
throwable.fingerprint
|
||||
)
|
||||
}
|
||||
else ->
|
||||
throw throwable
|
||||
}
|
||||
}
|
||||
|
||||
return sessionCreator.createSession(credentials, params.homeServerConnectionConfig)
|
||||
|
@@ -70,7 +70,6 @@ import im.vector.matrix.android.internal.crypto.model.event.RoomKeyWithHeldConte
|
||||
import im.vector.matrix.android.internal.crypto.model.event.SecretSendEventContent
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeysUploadResponse
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.RoomKeyRequestBody
|
||||
import im.vector.matrix.android.internal.crypto.model.toRest
|
||||
import im.vector.matrix.android.internal.crypto.repository.WarnOnUnknownDeviceRepository
|
||||
@@ -98,6 +97,7 @@ import im.vector.matrix.android.internal.session.sync.model.SyncResponse
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.TaskThread
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import im.vector.matrix.android.internal.task.launchToCallback
|
||||
import im.vector.matrix.android.internal.util.JsonCanonicalizer
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import im.vector.matrix.android.internal.util.fetchCopied
|
||||
@@ -340,11 +340,14 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
}
|
||||
|
||||
fun ensureDevice() {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
cryptoCoroutineScope.launchToCallback(coroutineDispatchers.crypto, NoOpMatrixCallback()) {
|
||||
// Open the store
|
||||
cryptoStore.open()
|
||||
// TODO why do that everytime? we should mark that it was done
|
||||
uploadDeviceKeys()
|
||||
// this can throw if no network
|
||||
tryThis {
|
||||
uploadDeviceKeys()
|
||||
}
|
||||
|
||||
oneTimeKeysUploader.maybeUploadOneTimeKeys()
|
||||
// this can throw if no backup
|
||||
tryThis {
|
||||
@@ -389,7 +392,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
// } else {
|
||||
|
||||
// Why would we do that? it will be called at end of syn
|
||||
incomingGossipingRequestManager.processReceivedGossipingRequests()
|
||||
incomingGossipingRequestManager.processReceivedGossipingRequests()
|
||||
// }
|
||||
}.fold(
|
||||
{
|
||||
@@ -888,7 +891,7 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
*/
|
||||
private fun handleSDKLevelGossip(secretName: String?, secretValue: String): Boolean {
|
||||
return when (secretName) {
|
||||
MASTER_KEY_SSSS_NAME -> {
|
||||
MASTER_KEY_SSSS_NAME -> {
|
||||
crossSigningService.onSecretMSKGossip(secretValue)
|
||||
true
|
||||
}
|
||||
@@ -980,7 +983,11 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
/**
|
||||
* Upload my user's device keys.
|
||||
*/
|
||||
private suspend fun uploadDeviceKeys(): KeysUploadResponse {
|
||||
private suspend fun uploadDeviceKeys() {
|
||||
if (cryptoStore.getDeviceKeysUploaded()) {
|
||||
Timber.d("Keys already uploaded, nothing to do")
|
||||
return
|
||||
}
|
||||
// Prepare the device keys data to send
|
||||
// Sign it
|
||||
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, getMyDevice().signalableJSONDictionary())
|
||||
@@ -991,7 +998,9 @@ internal class DefaultCryptoService @Inject constructor(
|
||||
)
|
||||
|
||||
val uploadDeviceKeysParams = UploadKeysTask.Params(rest, null)
|
||||
return uploadKeysTask.execute(uploadDeviceKeysParams)
|
||||
uploadKeysTask.execute(uploadDeviceKeysParams)
|
||||
|
||||
cryptoStore.setDeviceKeysUploaded(true)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -71,8 +71,8 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
|
||||
delay(1500)
|
||||
cryptoStore.getOrAddOutgoingSecretShareRequest(secretName, recipients)?.let {
|
||||
// TODO check if there is already one that is being sent?
|
||||
if (it.state == OutgoingGossipingRequestState.SENDING || it.state == OutgoingGossipingRequestState.SENT) {
|
||||
Timber.v("## CRYPTO - GOSSIP sendSecretShareRequest() : we already request for that session: $it")
|
||||
if (it.state == OutgoingGossipingRequestState.SENDING /**|| it.state == OutgoingGossipingRequestState.SENT*/) {
|
||||
Timber.v("## CRYPTO - GOSSIP sendSecretShareRequest() : we are already sending for that session: $it")
|
||||
return@launch
|
||||
}
|
||||
|
||||
|
@@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.crypto.crosssigning
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.extensions.orFalse
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
@@ -507,6 +508,11 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||
&& cryptoStore.getCrossSigningPrivateKeys()?.user != null
|
||||
}
|
||||
|
||||
override fun allPrivateKeysKnown(): Boolean {
|
||||
return checkSelfTrust().isVerified()
|
||||
&& cryptoStore.getCrossSigningPrivateKeys()?.allKnown().orFalse()
|
||||
}
|
||||
|
||||
override fun trustUser(otherUserId: String, callback: MatrixCallback<Unit>) {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
|
||||
Timber.d("## CrossSigning - Mark user $userId as trusted ")
|
||||
|
@@ -433,4 +433,7 @@ internal interface IMXCryptoStore {
|
||||
fun getOutgoingSecretRequest(secretName: String): OutgoingSecretRequest?
|
||||
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
|
||||
fun getGossipingEventsTrail(): List<Event>
|
||||
|
||||
fun setDeviceKeysUploaded(uploaded: Boolean)
|
||||
fun getDeviceKeysUploaded(): Boolean
|
||||
}
|
||||
|
@@ -20,4 +20,6 @@ data class PrivateKeysInfo(
|
||||
val master: String? = null,
|
||||
val selfSigned: String? = null,
|
||||
val user: String? = null
|
||||
)
|
||||
) {
|
||||
fun allKnown() = master != null && selfSigned != null && user != null
|
||||
}
|
||||
|
@@ -842,6 +842,18 @@ internal class RealmCryptoStore @Inject constructor(
|
||||
} ?: false
|
||||
}
|
||||
|
||||
override fun setDeviceKeysUploaded(uploaded: Boolean) {
|
||||
doRealmTransaction(realmConfiguration) {
|
||||
it.where<CryptoMetadataEntity>().findFirst()?.deviceKeysSentToServer = uploaded
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDeviceKeysUploaded(): Boolean {
|
||||
return doWithRealm(realmConfiguration) {
|
||||
it.where<CryptoMetadataEntity>().findFirst()?.deviceKeysSentToServer
|
||||
} ?: false
|
||||
}
|
||||
|
||||
override fun setRoomsListBlacklistUnverifiedDevices(roomIds: List<String>) {
|
||||
doRealmTransaction(realmConfiguration) {
|
||||
// Reset all
|
||||
|
@@ -54,7 +54,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
||||
// 0, 1, 2: legacy Riot-Android
|
||||
// 3: migrate to RiotX schema
|
||||
// 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6)
|
||||
const val CRYPTO_STORE_SCHEMA_VERSION = 10L
|
||||
const val CRYPTO_STORE_SCHEMA_VERSION = 11L
|
||||
}
|
||||
|
||||
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
||||
@@ -70,6 +70,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
||||
if (oldVersion <= 7) migrateTo8(realm)
|
||||
if (oldVersion <= 8) migrateTo9(realm)
|
||||
if (oldVersion <= 9) migrateTo10(realm)
|
||||
if (oldVersion <= 10) migrateTo11(realm)
|
||||
}
|
||||
|
||||
private fun migrateTo1Legacy(realm: DynamicRealm) {
|
||||
@@ -446,4 +447,11 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
||||
.addField(SharedSessionEntityFields.CHAIN_INDEX, Long::class.java)
|
||||
.setNullable(SharedSessionEntityFields.CHAIN_INDEX, true)
|
||||
}
|
||||
|
||||
// Version 11L added deviceKeysSentToServer boolean to CryptoMetadataEntity
|
||||
private fun migrateTo11(realm: DynamicRealm) {
|
||||
Timber.d("Step 10 -> 11")
|
||||
realm.schema.get("CryptoMetadataEntity")
|
||||
?.addField(CryptoMetadataEntityFields.DEVICE_KEYS_SENT_TO_SERVER, Boolean::class.java)
|
||||
}
|
||||
}
|
||||
|
@@ -36,6 +36,9 @@ internal open class CryptoMetadataEntity(
|
||||
// The keys backup version currently used. Null means no backup.
|
||||
var backupVersion: String? = null,
|
||||
|
||||
// The device keys has been sent to the homeserver
|
||||
var deviceKeysSentToServer: Boolean = false,
|
||||
|
||||
var xSignMasterPrivateKey: String? = null,
|
||||
var xSignUserPrivateKey: String? = null,
|
||||
var xSignSelfSignedPrivateKey: String? = null,
|
||||
|
@@ -19,7 +19,7 @@ import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
import im.vector.matrix.android.internal.crypto.model.MXEncryptEventContentResult
|
||||
import im.vector.matrix.android.internal.session.room.send.LocalEchoUpdater
|
||||
import im.vector.matrix.android.internal.session.room.send.LocalEchoRepository
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import im.vector.matrix.android.internal.util.awaitCallback
|
||||
import javax.inject.Inject
|
||||
@@ -35,7 +35,7 @@ internal interface EncryptEventTask : Task<EncryptEventTask.Params, Event> {
|
||||
|
||||
internal class DefaultEncryptEventTask @Inject constructor(
|
||||
// private val crypto: CryptoService
|
||||
private val localEchoUpdater: LocalEchoUpdater
|
||||
private val localEchoRepository: LocalEchoRepository
|
||||
) : EncryptEventTask {
|
||||
override suspend fun execute(params: EncryptEventTask.Params): Event {
|
||||
if (!params.crypto.isRoomEncrypted(params.roomId)) return params.event
|
||||
@@ -44,7 +44,7 @@ internal class DefaultEncryptEventTask @Inject constructor(
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
localEchoUpdater.updateSendState(localEvent.eventId, SendState.ENCRYPTING)
|
||||
localEchoRepository.updateSendState(localEvent.eventId, SendState.ENCRYPTING)
|
||||
|
||||
val localMutableContent = localEvent.content?.toMutableMap() ?: mutableMapOf()
|
||||
params.keepKeys?.forEach {
|
||||
|
@@ -1,161 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.tasks
|
||||
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationService
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageRelationContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationReadyContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationStartContent
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import im.vector.matrix.android.internal.crypto.verification.DefaultVerificationService
|
||||
import im.vector.matrix.android.internal.di.DeviceId
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import timber.log.Timber
|
||||
import java.util.ArrayList
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface RoomVerificationUpdateTask : Task<RoomVerificationUpdateTask.Params, Unit> {
|
||||
data class Params(
|
||||
val events: List<Event>,
|
||||
val verificationService: DefaultVerificationService,
|
||||
val cryptoService: CryptoService
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultRoomVerificationUpdateTask @Inject constructor(
|
||||
@UserId private val userId: String,
|
||||
@DeviceId private val deviceId: String?,
|
||||
private val cryptoService: CryptoService) : RoomVerificationUpdateTask {
|
||||
|
||||
companion object {
|
||||
// XXX what about multi-account?
|
||||
private val transactionsHandledByOtherDevice = ArrayList<String>()
|
||||
}
|
||||
|
||||
override suspend fun execute(params: RoomVerificationUpdateTask.Params) {
|
||||
// TODO ignore initial sync or back pagination?
|
||||
|
||||
params.events.forEach { event ->
|
||||
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}")
|
||||
|
||||
// If the request is in the future by more than 5 minutes or more than 10 minutes in the past,
|
||||
// the message should be ignored by the receiver.
|
||||
|
||||
if (!VerificationService.isValidRequest(event.ageLocalTs
|
||||
?: event.originServerTs)) return@forEach Unit.also {
|
||||
Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated")
|
||||
}
|
||||
|
||||
// decrypt if needed?
|
||||
if (event.isEncrypted() && event.mxDecryptionResult == null) {
|
||||
// TODO use a global event decryptor? attache to session and that listen to new sessionId?
|
||||
// for now decrypt sync
|
||||
try {
|
||||
val result = cryptoService.decryptEvent(event, "")
|
||||
event.mxDecryptionResult = OlmDecryptionResult(
|
||||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||
)
|
||||
} catch (e: MXCryptoError) {
|
||||
Timber.e("## SAS Failed to decrypt event: ${event.eventId}")
|
||||
params.verificationService.onPotentiallyInterestingEventRoomFailToDecrypt(event)
|
||||
}
|
||||
}
|
||||
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}")
|
||||
|
||||
// Relates to is not encrypted
|
||||
val relatesToEventId = event.content.toModel<MessageRelationContent>()?.relatesTo?.eventId
|
||||
|
||||
if (event.senderId == userId) {
|
||||
// If it's send from me, we need to keep track of Requests or Start
|
||||
// done from another device of mine
|
||||
|
||||
if (EventType.MESSAGE == event.getClearType()) {
|
||||
val msgType = event.getClearContent().toModel<MessageContent>()?.msgType
|
||||
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) {
|
||||
event.getClearContent().toModel<MessageVerificationRequestContent>()?.let {
|
||||
if (it.fromDevice != deviceId) {
|
||||
// The verification is requested from another device
|
||||
Timber.v("## SAS Verification live observer: Transaction requested from other device tid:${event.eventId} ")
|
||||
event.eventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (EventType.KEY_VERIFICATION_START == event.getClearType()) {
|
||||
event.getClearContent().toModel<MessageVerificationStartContent>()?.let {
|
||||
if (it.fromDevice != deviceId) {
|
||||
// The verification is started from another device
|
||||
Timber.v("## SAS Verification live observer: Transaction started by other device tid:$relatesToEventId ")
|
||||
relatesToEventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
|
||||
params.verificationService.onRoomRequestHandledByOtherDevice(event)
|
||||
}
|
||||
}
|
||||
} else if (EventType.KEY_VERIFICATION_READY == event.getClearType()) {
|
||||
event.getClearContent().toModel<MessageVerificationReadyContent>()?.let {
|
||||
if (it.fromDevice != deviceId) {
|
||||
// The verification is started from another device
|
||||
Timber.v("## SAS Verification live observer: Transaction started by other device tid:$relatesToEventId ")
|
||||
relatesToEventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
|
||||
params.verificationService.onRoomRequestHandledByOtherDevice(event)
|
||||
}
|
||||
}
|
||||
} else if (EventType.KEY_VERIFICATION_CANCEL == event.getClearType() || EventType.KEY_VERIFICATION_DONE == event.getClearType()) {
|
||||
relatesToEventId?.let {
|
||||
transactionsHandledByOtherDevice.remove(it)
|
||||
params.verificationService.onRoomRequestHandledByOtherDevice(event)
|
||||
}
|
||||
}
|
||||
|
||||
Timber.v("## SAS Verification ignoring message sent by me: ${event.eventId} type: ${event.getClearType()}")
|
||||
return@forEach
|
||||
}
|
||||
|
||||
if (relatesToEventId != null && transactionsHandledByOtherDevice.contains(relatesToEventId)) {
|
||||
// Ignore this event, it is directed to another of my devices
|
||||
Timber.v("## SAS Verification live observer: Ignore Transaction handled by other device tid:$relatesToEventId ")
|
||||
return@forEach
|
||||
}
|
||||
when (event.getClearType()) {
|
||||
EventType.KEY_VERIFICATION_START,
|
||||
EventType.KEY_VERIFICATION_ACCEPT,
|
||||
EventType.KEY_VERIFICATION_KEY,
|
||||
EventType.KEY_VERIFICATION_MAC,
|
||||
EventType.KEY_VERIFICATION_CANCEL,
|
||||
EventType.KEY_VERIFICATION_READY,
|
||||
EventType.KEY_VERIFICATION_DONE -> {
|
||||
params.verificationService.onRoomEvent(event)
|
||||
}
|
||||
EventType.MESSAGE -> {
|
||||
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel<MessageContent>()?.msgType) {
|
||||
params.verificationService.onRoomRequestReceived(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -20,7 +20,7 @@ import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.session.room.RoomAPI
|
||||
import im.vector.matrix.android.internal.session.room.send.LocalEchoUpdater
|
||||
import im.vector.matrix.android.internal.session.room.send.LocalEchoRepository
|
||||
import im.vector.matrix.android.internal.session.room.send.SendResponse
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
@@ -34,7 +34,7 @@ internal interface SendEventTask : Task<SendEventTask.Params, String> {
|
||||
}
|
||||
|
||||
internal class DefaultSendEventTask @Inject constructor(
|
||||
private val localEchoUpdater: LocalEchoUpdater,
|
||||
private val localEchoRepository: LocalEchoRepository,
|
||||
private val encryptEventTask: DefaultEncryptEventTask,
|
||||
private val roomAPI: RoomAPI,
|
||||
private val eventBus: EventBus) : SendEventTask {
|
||||
@@ -44,7 +44,7 @@ internal class DefaultSendEventTask @Inject constructor(
|
||||
val localId = event.eventId!!
|
||||
|
||||
try {
|
||||
localEchoUpdater.updateSendState(localId, SendState.SENDING)
|
||||
localEchoRepository.updateSendState(localId, SendState.SENDING)
|
||||
val executeRequest = executeRequest<SendResponse>(eventBus) {
|
||||
apiCall = roomAPI.send(
|
||||
localId,
|
||||
@@ -53,10 +53,10 @@ internal class DefaultSendEventTask @Inject constructor(
|
||||
eventType = event.type
|
||||
)
|
||||
}
|
||||
localEchoUpdater.updateSendState(localId, SendState.SENT)
|
||||
localEchoRepository.updateSendState(localId, SendState.SENT)
|
||||
return executeRequest.eventId
|
||||
} catch (e: Throwable) {
|
||||
localEchoUpdater.updateSendState(localId, SendState.UNDELIVERED)
|
||||
localEchoRepository.updateSendState(localId, SendState.UNDELIVERED)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
@@ -20,7 +20,7 @@ import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.session.room.RoomAPI
|
||||
import im.vector.matrix.android.internal.session.room.send.LocalEchoUpdater
|
||||
import im.vector.matrix.android.internal.session.room.send.LocalEchoRepository
|
||||
import im.vector.matrix.android.internal.session.room.send.SendResponse
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
@@ -34,7 +34,7 @@ internal interface SendVerificationMessageTask : Task<SendVerificationMessageTas
|
||||
}
|
||||
|
||||
internal class DefaultSendVerificationMessageTask @Inject constructor(
|
||||
private val localEchoUpdater: LocalEchoUpdater,
|
||||
private val localEchoRepository: LocalEchoRepository,
|
||||
private val encryptEventTask: DefaultEncryptEventTask,
|
||||
private val roomAPI: RoomAPI,
|
||||
private val eventBus: EventBus) : SendVerificationMessageTask {
|
||||
@@ -44,7 +44,7 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
|
||||
val localId = event.eventId!!
|
||||
|
||||
try {
|
||||
localEchoUpdater.updateSendState(localId, SendState.SENDING)
|
||||
localEchoRepository.updateSendState(localId, SendState.SENDING)
|
||||
val executeRequest = executeRequest<SendResponse>(eventBus) {
|
||||
apiCall = roomAPI.send(
|
||||
localId,
|
||||
@@ -53,10 +53,10 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
|
||||
eventType = event.type
|
||||
)
|
||||
}
|
||||
localEchoUpdater.updateSendState(localId, SendState.SENT)
|
||||
localEchoRepository.updateSendState(localId, SendState.SENT)
|
||||
return executeRequest.eventId
|
||||
} catch (e: Throwable) {
|
||||
localEchoUpdater.updateSendState(localId, SendState.UNDELIVERED)
|
||||
localEchoRepository.updateSendState(localId, SendState.UNDELIVERED)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
@@ -1234,7 +1234,7 @@ internal class DefaultVerificationService @Inject constructor(
|
||||
)
|
||||
|
||||
// We can SCAN or SHOW QR codes only if cross-signing is enabled
|
||||
val methodValues = if (crossSigningService.isCrossSigningVerified()) {
|
||||
val methodValues = if (crossSigningService.isCrossSigningInitialized()) {
|
||||
// Add reciprocate method if application declares it can scan or show QR codes
|
||||
// Not sure if it ok to do that (?)
|
||||
val reciprocateMethod = methods
|
||||
|
@@ -1,73 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.matrix.android.internal.crypto.verification
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultRoomVerificationUpdateTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.RoomVerificationUpdateTask
|
||||
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
|
||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.query.whereTypes
|
||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import io.realm.OrderedCollectionChangeSet
|
||||
import io.realm.RealmConfiguration
|
||||
import io.realm.RealmResults
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class VerificationMessageLiveObserver @Inject constructor(
|
||||
@SessionDatabase realmConfiguration: RealmConfiguration,
|
||||
private val roomVerificationUpdateTask: DefaultRoomVerificationUpdateTask,
|
||||
private val cryptoService: CryptoService,
|
||||
private val verificationService: DefaultVerificationService,
|
||||
private val taskExecutor: TaskExecutor
|
||||
) : RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
|
||||
|
||||
override val query = Monarchy.Query {
|
||||
EventEntity.whereTypes(it, listOf(
|
||||
EventType.KEY_VERIFICATION_START,
|
||||
EventType.KEY_VERIFICATION_ACCEPT,
|
||||
EventType.KEY_VERIFICATION_KEY,
|
||||
EventType.KEY_VERIFICATION_MAC,
|
||||
EventType.KEY_VERIFICATION_CANCEL,
|
||||
EventType.KEY_VERIFICATION_DONE,
|
||||
EventType.KEY_VERIFICATION_READY,
|
||||
EventType.MESSAGE,
|
||||
EventType.ENCRYPTED)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onChange(results: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) {
|
||||
// Should we ignore when it's an initial sync?
|
||||
val events = changeSet.insertions
|
||||
.asSequence()
|
||||
.mapNotNull { results[it]?.asDomain() }
|
||||
.filterNot {
|
||||
// ignore local echos
|
||||
LocalEcho.isLocalEchoId(it.eventId ?: "")
|
||||
}
|
||||
.toList()
|
||||
|
||||
roomVerificationUpdateTask.configureWith(
|
||||
RoomVerificationUpdateTask.Params(events, verificationService, cryptoService)
|
||||
).executeBy(taskExecutor)
|
||||
}
|
||||
}
|
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.matrix.android.internal.crypto.verification
|
||||
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationService
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageRelationContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationReadyContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationStartContent
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import im.vector.matrix.android.internal.database.model.EventInsertType
|
||||
import im.vector.matrix.android.internal.di.DeviceId
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
import im.vector.matrix.android.internal.session.EventInsertLiveProcessor
|
||||
import io.realm.Realm
|
||||
import timber.log.Timber
|
||||
import java.util.ArrayList
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class VerificationMessageProcessor @Inject constructor(
|
||||
private val cryptoService: CryptoService,
|
||||
private val verificationService: DefaultVerificationService,
|
||||
@UserId private val userId: String,
|
||||
@DeviceId private val deviceId: String?
|
||||
) : EventInsertLiveProcessor {
|
||||
|
||||
private val transactionsHandledByOtherDevice = ArrayList<String>()
|
||||
|
||||
private val allowedTypes = listOf(
|
||||
EventType.KEY_VERIFICATION_START,
|
||||
EventType.KEY_VERIFICATION_ACCEPT,
|
||||
EventType.KEY_VERIFICATION_KEY,
|
||||
EventType.KEY_VERIFICATION_MAC,
|
||||
EventType.KEY_VERIFICATION_CANCEL,
|
||||
EventType.KEY_VERIFICATION_DONE,
|
||||
EventType.KEY_VERIFICATION_READY,
|
||||
EventType.MESSAGE,
|
||||
EventType.ENCRYPTED
|
||||
)
|
||||
|
||||
override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean {
|
||||
if (insertType != EventInsertType.INCREMENTAL_SYNC) {
|
||||
return false
|
||||
}
|
||||
return allowedTypes.contains(eventType) && !LocalEcho.isLocalEchoId(eventId)
|
||||
}
|
||||
|
||||
override suspend fun process(realm: Realm, event: Event) {
|
||||
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}")
|
||||
|
||||
// If the request is in the future by more than 5 minutes or more than 10 minutes in the past,
|
||||
// the message should be ignored by the receiver.
|
||||
|
||||
if (!VerificationService.isValidRequest(event.ageLocalTs
|
||||
?: event.originServerTs)) return Unit.also {
|
||||
Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated")
|
||||
}
|
||||
|
||||
// decrypt if needed?
|
||||
if (event.isEncrypted() && event.mxDecryptionResult == null) {
|
||||
// TODO use a global event decryptor? attache to session and that listen to new sessionId?
|
||||
// for now decrypt sync
|
||||
try {
|
||||
val result = cryptoService.decryptEvent(event, "")
|
||||
event.mxDecryptionResult = OlmDecryptionResult(
|
||||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
keysClaimed = result.claimedEd25519Key?.let { mapOf("ed25519" to it) },
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||
)
|
||||
} catch (e: MXCryptoError) {
|
||||
Timber.e("## SAS Failed to decrypt event: ${event.eventId}")
|
||||
verificationService.onPotentiallyInterestingEventRoomFailToDecrypt(event)
|
||||
}
|
||||
}
|
||||
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}")
|
||||
|
||||
// Relates to is not encrypted
|
||||
val relatesToEventId = event.content.toModel<MessageRelationContent>()?.relatesTo?.eventId
|
||||
|
||||
if (event.senderId == userId) {
|
||||
// If it's send from me, we need to keep track of Requests or Start
|
||||
// done from another device of mine
|
||||
|
||||
if (EventType.MESSAGE == event.getClearType()) {
|
||||
val msgType = event.getClearContent().toModel<MessageContent>()?.msgType
|
||||
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) {
|
||||
event.getClearContent().toModel<MessageVerificationRequestContent>()?.let {
|
||||
if (it.fromDevice != deviceId) {
|
||||
// The verification is requested from another device
|
||||
Timber.v("## SAS Verification live observer: Transaction requested from other device tid:${event.eventId} ")
|
||||
event.eventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (EventType.KEY_VERIFICATION_START == event.getClearType()) {
|
||||
event.getClearContent().toModel<MessageVerificationStartContent>()?.let {
|
||||
if (it.fromDevice != deviceId) {
|
||||
// The verification is started from another device
|
||||
Timber.v("## SAS Verification live observer: Transaction started by other device tid:$relatesToEventId ")
|
||||
relatesToEventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
|
||||
verificationService.onRoomRequestHandledByOtherDevice(event)
|
||||
}
|
||||
}
|
||||
} else if (EventType.KEY_VERIFICATION_READY == event.getClearType()) {
|
||||
event.getClearContent().toModel<MessageVerificationReadyContent>()?.let {
|
||||
if (it.fromDevice != deviceId) {
|
||||
// The verification is started from another device
|
||||
Timber.v("## SAS Verification live observer: Transaction started by other device tid:$relatesToEventId ")
|
||||
relatesToEventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
|
||||
verificationService.onRoomRequestHandledByOtherDevice(event)
|
||||
}
|
||||
}
|
||||
} else if (EventType.KEY_VERIFICATION_CANCEL == event.getClearType() || EventType.KEY_VERIFICATION_DONE == event.getClearType()) {
|
||||
relatesToEventId?.let {
|
||||
transactionsHandledByOtherDevice.remove(it)
|
||||
verificationService.onRoomRequestHandledByOtherDevice(event)
|
||||
}
|
||||
}
|
||||
|
||||
Timber.v("## SAS Verification ignoring message sent by me: ${event.eventId} type: ${event.getClearType()}")
|
||||
return
|
||||
}
|
||||
|
||||
if (relatesToEventId != null && transactionsHandledByOtherDevice.contains(relatesToEventId)) {
|
||||
// Ignore this event, it is directed to another of my devices
|
||||
Timber.v("## SAS Verification live observer: Ignore Transaction handled by other device tid:$relatesToEventId ")
|
||||
return
|
||||
}
|
||||
when (event.getClearType()) {
|
||||
EventType.KEY_VERIFICATION_START,
|
||||
EventType.KEY_VERIFICATION_ACCEPT,
|
||||
EventType.KEY_VERIFICATION_KEY,
|
||||
EventType.KEY_VERIFICATION_MAC,
|
||||
EventType.KEY_VERIFICATION_CANCEL,
|
||||
EventType.KEY_VERIFICATION_READY,
|
||||
EventType.KEY_VERIFICATION_DONE -> {
|
||||
verificationService.onRoomEvent(event)
|
||||
}
|
||||
EventType.MESSAGE -> {
|
||||
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == event.getClearContent().toModel<MessageContent>()?.msgType) {
|
||||
verificationService.onRoomRequestReceived(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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 im.vector.matrix.android.internal.database
|
||||
|
||||
import im.vector.matrix.android.internal.database.helper.nextDisplayIndex
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntityFields
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
|
||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||
import im.vector.matrix.android.internal.session.SessionLifecycleObserver
|
||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val MAX_NUMBER_OF_EVENTS_IN_DB = 35_000L
|
||||
private const val MIN_NUMBER_OF_EVENTS_BY_CHUNK = 300
|
||||
|
||||
/**
|
||||
* This class makes sure to stay under a maximum number of events as it makes Realm to be unusable when listening to events
|
||||
* when the database is getting too big. This will try incrementally to remove the biggest chunks until we get below the threshold.
|
||||
* We make sure to still have a minimum number of events so it's not becoming unusable.
|
||||
* So this won't work for users with a big number of very active rooms.
|
||||
*/
|
||||
internal class DatabaseCleaner @Inject constructor(@SessionDatabase private val realmConfiguration: RealmConfiguration,
|
||||
private val taskExecutor: TaskExecutor) : SessionLifecycleObserver {
|
||||
|
||||
override fun onStart() {
|
||||
taskExecutor.executorScope.launch(Dispatchers.Default) {
|
||||
awaitTransaction(realmConfiguration) { realm ->
|
||||
val allRooms = realm.where(RoomEntity::class.java).findAll()
|
||||
Timber.v("There are ${allRooms.size} rooms in this session")
|
||||
cleanUp(realm, MAX_NUMBER_OF_EVENTS_IN_DB / 2L)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun cleanUp(realm: Realm, threshold: Long) {
|
||||
val numberOfEvents = realm.where(EventEntity::class.java).findAll().size
|
||||
val numberOfTimelineEvents = realm.where(TimelineEventEntity::class.java).findAll().size
|
||||
Timber.v("Number of events in db: $numberOfEvents | Number of timeline events in db: $numberOfTimelineEvents")
|
||||
if (threshold <= MIN_NUMBER_OF_EVENTS_BY_CHUNK || numberOfTimelineEvents < MAX_NUMBER_OF_EVENTS_IN_DB) {
|
||||
Timber.v("Db is low enough")
|
||||
} else {
|
||||
val thresholdChunks = realm.where(ChunkEntity::class.java)
|
||||
.greaterThan(ChunkEntityFields.NUMBER_OF_TIMELINE_EVENTS, threshold)
|
||||
.findAll()
|
||||
|
||||
Timber.v("There are ${thresholdChunks.size} chunks to clean with more than $threshold events")
|
||||
for (chunk in thresholdChunks) {
|
||||
val maxDisplayIndex = chunk.nextDisplayIndex(PaginationDirection.FORWARDS)
|
||||
val thresholdDisplayIndex = maxDisplayIndex - threshold
|
||||
val eventsToRemove = chunk.timelineEvents.where().lessThan(TimelineEventEntityFields.DISPLAY_INDEX, thresholdDisplayIndex).findAll()
|
||||
Timber.v("There are ${eventsToRemove.size} events to clean in chunk: ${chunk.identifier()} from room ${chunk.room?.first()?.roomId}")
|
||||
chunk.numberOfTimelineEvents = chunk.numberOfTimelineEvents - eventsToRemove.size
|
||||
eventsToRemove.forEach {
|
||||
val canDeleteRoot = it.root?.stateKey == null
|
||||
if (canDeleteRoot) {
|
||||
it.root?.deleteFromRealm()
|
||||
}
|
||||
it.readReceipts?.readReceipts?.deleteAllFromRealm()
|
||||
it.readReceipts?.deleteFromRealm()
|
||||
it.annotations?.apply {
|
||||
editSummary?.deleteFromRealm()
|
||||
pollResponseSummary?.deleteFromRealm()
|
||||
referencesSummaryEntity?.deleteFromRealm()
|
||||
reactionsSummary.deleteAllFromRealm()
|
||||
}
|
||||
it.annotations?.deleteFromRealm()
|
||||
it.readReceipts?.deleteFromRealm()
|
||||
it.deleteFromRealm()
|
||||
}
|
||||
// We reset the prevToken so we will need to fetch again.
|
||||
chunk.prevToken = null
|
||||
}
|
||||
cleanUp(realm, (threshold / 1.5).toLong())
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* 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 im.vector.matrix.android.internal.database
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.crypto.MXCryptoError
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
|
||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.model.EventInsertEntity
|
||||
import im.vector.matrix.android.internal.database.model.EventInsertEntityFields
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.di.SessionDatabase
|
||||
import im.vector.matrix.android.internal.session.EventInsertLiveProcessor
|
||||
import io.realm.RealmConfiguration
|
||||
import io.realm.RealmResults
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class EventInsertLiveObserver @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration,
|
||||
private val processors: Set<@JvmSuppressWildcards EventInsertLiveProcessor>,
|
||||
private val cryptoService: CryptoService)
|
||||
: RealmLiveEntityObserver<EventInsertEntity>(realmConfiguration) {
|
||||
|
||||
override val query = Monarchy.Query<EventInsertEntity> {
|
||||
it.where(EventInsertEntity::class.java)
|
||||
}
|
||||
|
||||
override fun onChange(results: RealmResults<EventInsertEntity>) {
|
||||
if (!results.isLoaded || results.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val idsToDeleteAfterProcess = ArrayList<String>()
|
||||
val filteredEvents = ArrayList<EventInsertEntity>(results.size)
|
||||
Timber.v("EventInsertEntity updated with ${results.size} results in db")
|
||||
results.forEach {
|
||||
if (shouldProcess(it)) {
|
||||
// don't use copy from realm over there
|
||||
val copiedEvent = EventInsertEntity(
|
||||
eventId = it.eventId,
|
||||
eventType = it.eventType
|
||||
).apply {
|
||||
insertType = it.insertType
|
||||
}
|
||||
filteredEvents.add(copiedEvent)
|
||||
}
|
||||
idsToDeleteAfterProcess.add(it.eventId)
|
||||
}
|
||||
observerScope.launch {
|
||||
awaitTransaction(realmConfiguration) { realm ->
|
||||
Timber.v("##Transaction: There are ${filteredEvents.size} events to process ")
|
||||
filteredEvents.forEach { eventInsert ->
|
||||
val eventId = eventInsert.eventId
|
||||
val event = EventEntity.where(realm, eventId).findFirst()
|
||||
if (event == null) {
|
||||
Timber.v("Event $eventId not found")
|
||||
return@forEach
|
||||
}
|
||||
val domainEvent = event.asDomain()
|
||||
decryptIfNeeded(domainEvent)
|
||||
processors.filter {
|
||||
it.shouldProcess(eventId, domainEvent.getClearType(), eventInsert.insertType)
|
||||
}.forEach {
|
||||
it.process(realm, domainEvent)
|
||||
}
|
||||
}
|
||||
realm.where(EventInsertEntity::class.java)
|
||||
.`in`(EventInsertEntityFields.EVENT_ID, idsToDeleteAfterProcess.toTypedArray())
|
||||
.findAll()
|
||||
.deleteAllFromRealm()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun decryptIfNeeded(event: Event) {
|
||||
if (event.isEncrypted() && event.mxDecryptionResult == null) {
|
||||
try {
|
||||
val result = cryptoService.decryptEvent(event, event.roomId ?: "")
|
||||
event.mxDecryptionResult = OlmDecryptionResult(
|
||||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
|
||||
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
|
||||
)
|
||||
} catch (e: MXCryptoError) {
|
||||
Timber.v("Failed to decrypt event")
|
||||
// TODO -> we should keep track of this and retry, or some processing will never be handled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldProcess(eventInsertEntity: EventInsertEntity): Boolean {
|
||||
return processors.any {
|
||||
it.shouldProcess(eventInsertEntity.eventId, eventInsertEntity.eventType, eventInsertEntity.insertType)
|
||||
}
|
||||
}
|
||||
}
|
@@ -19,27 +19,28 @@ package im.vector.matrix.android.internal.database
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.internal.session.SessionLifecycleObserver
|
||||
import im.vector.matrix.android.internal.util.createBackgroundHandler
|
||||
import io.realm.OrderedRealmCollectionChangeListener
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmChangeListener
|
||||
import io.realm.RealmConfiguration
|
||||
import io.realm.RealmObject
|
||||
import io.realm.RealmResults
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.android.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.cancelChildren
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
internal interface LiveEntityObserver: SessionLifecycleObserver
|
||||
internal interface LiveEntityObserver : SessionLifecycleObserver
|
||||
|
||||
internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val realmConfiguration: RealmConfiguration)
|
||||
: LiveEntityObserver, OrderedRealmCollectionChangeListener<RealmResults<T>> {
|
||||
: LiveEntityObserver, RealmChangeListener<RealmResults<T>> {
|
||||
|
||||
private companion object {
|
||||
val BACKGROUND_HANDLER = createBackgroundHandler("LIVE_ENTITY_BACKGROUND")
|
||||
}
|
||||
|
||||
protected val observerScope = CoroutineScope(SupervisorJob())
|
||||
protected val observerScope = CoroutineScope(SupervisorJob() + BACKGROUND_HANDLER.asCoroutineDispatcher())
|
||||
protected abstract val query: Monarchy.Query<T>
|
||||
private val isStarted = AtomicBoolean(false)
|
||||
private val backgroundRealm = AtomicReference<Realm>()
|
||||
|
@@ -115,6 +115,7 @@ internal fun ChunkEntity.addTimelineEvent(roomId: String,
|
||||
true
|
||||
}
|
||||
}
|
||||
numberOfTimelineEvents++
|
||||
timelineEvents.add(timelineEventEntity)
|
||||
}
|
||||
|
||||
@@ -122,17 +123,18 @@ private fun computeIsUnique(
|
||||
realm: Realm,
|
||||
roomId: String,
|
||||
isLastForward: Boolean,
|
||||
myRoomMemberContent: RoomMemberContent,
|
||||
senderRoomMemberContent: RoomMemberContent,
|
||||
roomMemberContentsByUser: Map<String, RoomMemberContent?>
|
||||
): Boolean {
|
||||
val isHistoricalUnique = roomMemberContentsByUser.values.find {
|
||||
it != myRoomMemberContent && it?.displayName == myRoomMemberContent.displayName
|
||||
it != senderRoomMemberContent && it?.displayName == senderRoomMemberContent.displayName
|
||||
} == null
|
||||
return if (isLastForward) {
|
||||
val isLiveUnique = RoomMemberSummaryEntity
|
||||
.where(realm, roomId)
|
||||
.equalTo(RoomMemberSummaryEntityFields.DISPLAY_NAME, myRoomMemberContent.displayName)
|
||||
.findAll().none {
|
||||
.equalTo(RoomMemberSummaryEntityFields.DISPLAY_NAME, senderRoomMemberContent.displayName)
|
||||
.findAll()
|
||||
.none {
|
||||
!roomMemberContentsByUser.containsKey(it.userId)
|
||||
}
|
||||
isHistoricalUnique && isLiveUnique
|
||||
|
@@ -19,7 +19,7 @@ package im.vector.matrix.android.internal.database.mapper
|
||||
import com.squareup.moshi.Moshi
|
||||
import im.vector.matrix.android.api.util.JSON_DICT_PARAMETERIZED_TYPE
|
||||
import im.vector.matrix.android.internal.database.model.UserAccountDataEntity
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
|
||||
import im.vector.matrix.android.api.session.accountdata.UserAccountDataEvent
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class AccountDataMapper @Inject constructor(moshi: Moshi) {
|
||||
|
@@ -45,7 +45,6 @@ internal object EventMapper {
|
||||
eventEntity.redacts = event.redacts
|
||||
eventEntity.age = event.unsignedData?.age ?: event.originServerTs
|
||||
eventEntity.unsignedData = uds
|
||||
|
||||
eventEntity.decryptionResultJson = event.mxDecryptionResult?.let {
|
||||
MoshiProvider.providesMoshi().adapter<OlmDecryptionResult>(OlmDecryptionResult::class.java).toJson(it)
|
||||
}
|
||||
|
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.database.mapper
|
||||
|
||||
import im.vector.matrix.android.api.session.group.Group
|
||||
import im.vector.matrix.android.internal.database.model.GroupEntity
|
||||
import im.vector.matrix.android.internal.session.group.DefaultGroup
|
||||
|
||||
internal object GroupMapper {
|
||||
|
||||
fun map(groupEntity: GroupEntity): Group {
|
||||
return DefaultGroup(
|
||||
groupEntity.groupId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun GroupEntity.asDomain(): Group {
|
||||
return GroupMapper.map(this)
|
||||
}
|
@@ -62,7 +62,8 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
|
||||
encryptionEventTs = roomSummaryEntity.encryptionEventTs,
|
||||
breadcrumbsIndex = roomSummaryEntity.breadcrumbsIndex,
|
||||
roomEncryptionTrustLevel = roomSummaryEntity.roomEncryptionTrustLevel,
|
||||
inviterId = roomSummaryEntity.inviterId
|
||||
inviterId = roomSummaryEntity.inviterId,
|
||||
hasFailedSending = roomSummaryEntity.hasFailedSending
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -27,6 +27,7 @@ internal open class ChunkEntity(@Index var prevToken: String? = null,
|
||||
@Index var nextToken: String? = null,
|
||||
var stateEvents: RealmList<EventEntity> = RealmList(),
|
||||
var timelineEvents: RealmList<TimelineEventEntity> = RealmList(),
|
||||
var numberOfTimelineEvents: Long = 0,
|
||||
// Only one chunk will have isLastForward == true
|
||||
@Index var isLastForward: Boolean = false,
|
||||
@Index var isLastBackward: Boolean = false
|
||||
|
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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 im.vector.matrix.android.internal.database.model
|
||||
|
||||
import io.realm.RealmObject
|
||||
|
||||
/**
|
||||
* This class is used to get notification on new events being inserted. It's to avoid realm getting slow when listening to insert
|
||||
* in EventEntity table.
|
||||
*/
|
||||
internal open class EventInsertEntity(var eventId: String = "",
|
||||
var eventType: String = ""
|
||||
) : RealmObject() {
|
||||
|
||||
private var insertTypeStr: String = EventInsertType.INCREMENTAL_SYNC.name
|
||||
var insertType: EventInsertType
|
||||
get() {
|
||||
return EventInsertType.valueOf(insertTypeStr)
|
||||
}
|
||||
set(value) {
|
||||
insertTypeStr = value.name
|
||||
}
|
||||
}
|
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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 im.vector.matrix.android.internal.database.model;
|
||||
|
||||
public enum EventInsertType {
|
||||
INITIAL_SYNC,
|
||||
INCREMENTAL_SYNC,
|
||||
PAGINATION,
|
||||
LOCAL_ECHO
|
||||
}
|
@@ -22,8 +22,7 @@ import io.realm.annotations.PrimaryKey
|
||||
|
||||
/**
|
||||
* This class is used to store group info (groupId and membership) from the sync response.
|
||||
* Then [im.vector.matrix.android.internal.session.group.GroupSummaryUpdater] observes change and
|
||||
* makes requests to fetch group information from the homeserver
|
||||
* Then GetGroupDataTask is called regularly to fetch group information from the homeserver.
|
||||
*/
|
||||
internal open class GroupEntity(@PrimaryKey var groupId: String = "")
|
||||
: RealmObject() {
|
||||
|
@@ -24,7 +24,7 @@ import io.realm.annotations.PrimaryKey
|
||||
internal open class RoomMemberSummaryEntity(@PrimaryKey var primaryKey: String = "",
|
||||
@Index var userId: String = "",
|
||||
@Index var roomId: String = "",
|
||||
var displayName: String? = null,
|
||||
@Index var displayName: String? = null,
|
||||
var avatarUrl: String? = null,
|
||||
var reason: String? = null,
|
||||
var isDirect: Boolean = false
|
||||
|
@@ -51,7 +51,8 @@ internal open class RoomSummaryEntity(
|
||||
var isEncrypted: Boolean = false,
|
||||
var encryptionEventTs: Long? = 0,
|
||||
var roomEncryptionTrustLevelStr: String? = null,
|
||||
var inviterId: String? = null
|
||||
var inviterId: String? = null,
|
||||
var hasFailedSending: Boolean = false
|
||||
) : RealmObject() {
|
||||
|
||||
private var membershipStr: String = Membership.NONE.name
|
||||
|
@@ -25,6 +25,7 @@ import io.realm.annotations.RealmModule
|
||||
classes = [
|
||||
ChunkEntity::class,
|
||||
EventEntity::class,
|
||||
EventInsertEntity::class,
|
||||
TimelineEventEntity::class,
|
||||
FilterEntity::class,
|
||||
GroupEntity::class,
|
||||
|
@@ -18,16 +18,28 @@ package im.vector.matrix.android.internal.database.query
|
||||
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
||||
import im.vector.matrix.android.internal.database.model.EventInsertEntity
|
||||
import im.vector.matrix.android.internal.database.model.EventInsertType
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmList
|
||||
import io.realm.RealmQuery
|
||||
import io.realm.kotlin.where
|
||||
|
||||
internal fun EventEntity.copyToRealmOrIgnore(realm: Realm): EventEntity {
|
||||
return realm.where<EventEntity>()
|
||||
.equalTo(EventEntityFields.EVENT_ID, eventId)
|
||||
.equalTo(EventEntityFields.ROOM_ID, roomId)
|
||||
.findFirst() ?: realm.copyToRealm(this)
|
||||
internal fun EventEntity.copyToRealmOrIgnore(realm: Realm, insertType: EventInsertType): EventEntity {
|
||||
val eventEntity = realm.where<EventEntity>()
|
||||
.equalTo(EventEntityFields.EVENT_ID, eventId)
|
||||
.equalTo(EventEntityFields.ROOM_ID, roomId)
|
||||
.findFirst()
|
||||
return if (eventEntity == null) {
|
||||
val insertEntity = EventInsertEntity(eventId = eventId, eventType = type).apply {
|
||||
this.insertType = insertType
|
||||
}
|
||||
realm.insert(insertEntity)
|
||||
// copy this event entity and return it
|
||||
realm.copyToRealm(this)
|
||||
} else {
|
||||
eventEntity
|
||||
}
|
||||
}
|
||||
|
||||
internal fun EventEntity.Companion.where(realm: Realm, eventId: String): RealmQuery<EventEntity> {
|
||||
|
@@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.database.query
|
||||
import im.vector.matrix.android.api.session.room.model.Membership
|
||||
import im.vector.matrix.android.internal.database.model.GroupEntity
|
||||
import im.vector.matrix.android.internal.database.model.GroupEntityFields
|
||||
import im.vector.matrix.android.internal.query.process
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmQuery
|
||||
import io.realm.kotlin.where
|
||||
@@ -28,10 +29,6 @@ internal fun GroupEntity.Companion.where(realm: Realm, groupId: String): RealmQu
|
||||
.equalTo(GroupEntityFields.GROUP_ID, groupId)
|
||||
}
|
||||
|
||||
internal fun GroupEntity.Companion.where(realm: Realm, membership: Membership? = null): RealmQuery<GroupEntity> {
|
||||
val query = realm.where<GroupEntity>()
|
||||
if (membership != null) {
|
||||
query.equalTo(GroupEntityFields.MEMBERSHIP_STR, membership.name)
|
||||
}
|
||||
return query
|
||||
internal fun GroupEntity.Companion.where(realm: Realm, memberships: List<Membership>): RealmQuery<GroupEntity> {
|
||||
return realm.where<GroupEntity>().process(GroupEntityFields.MEMBERSHIP_STR, memberships)
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user