Merge branch 'develop' into feature/improve_perf

This commit is contained in:
ganfra 2020-06-25 10:57:46 +02:00
commit f28e3ca504
206 changed files with 7331 additions and 556 deletions

View file

@ -7,6 +7,7 @@
<w>ciphertext</w> <w>ciphertext</w>
<w>coroutine</w> <w>coroutine</w>
<w>decryptor</w> <w>decryptor</w>
<w>displayname</w>
<w>emoji</w> <w>emoji</w>
<w>emojis</w> <w>emojis</w>
<w>fdroid</w> <w>fdroid</w>

View file

@ -1,4 +1,37 @@
Changes in RiotX 0.22.0 (2020-XX-XX) Changes in RiotX 0.23.0 (2020-XX-XX)
===================================================
Features ✨:
- Call with WebRTC support (##611)
- Add capability to change the display name (#1529)
Improvements 🙌:
- "Add Matrix app" menu is now always visible (#1495)
Bugfix 🐛:
- Fix dark theme issue on login screen (#1097)
- Incomplete predicate in RealmCryptoStore#getOutgoingRoomKeyRequest (#1519)
- User could not redact message that they have sent (#1543)
- Use vendor prefix for non merged MSC (#1537)
Translations 🗣:
-
SDK API changes ⚠️:
-
Build 🧱:
- Enable code optimization (Proguard)
- SDK is now API level 21 minimum, and so RiotX (#405)
Other changes:
- Use `SharedPreferences#edit` extension function consistently (#1545)
- Use `retrofit2.Call.awaitResponse` extension provided by Retrofit 2. (#1526)
- Fix minor typo in contribution guide (#1512)
- Fix self-assignment of callback in `DefaultRoomPushRuleService#setRoomNotificationState` (#1520)
- Random housekeeping clean-ups indicated by Lint (#1520, #1541)
Changes in RiotX 0.22.0 (2020-06-15)
=================================================== ===================================================
Features ✨: Features ✨:
@ -23,15 +56,7 @@ Bugfix 🐛:
- Fix status bar icon contrast on API in [21,23[ - Fix status bar icon contrast on API in [21,23[
- Wrong /query request (#1444) - Wrong /query request (#1444)
- Make Credentials.homeServer optional because it is deprecated (#1443) - Make Credentials.homeServer optional because it is deprecated (#1443)
- Fix issue on dark themes, after alert popup dismiss
Translations 🗣:
-
SDK API changes ⚠️:
-
Build 🧱:
-
Other changes: Other changes:
- Send plain text in the body of events containing formatted body, as per https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes - Send plain text in the body of events containing formatted body, as per https://matrix.org/docs/spec/client_server/latest#m-room-message-msgtypes

View file

@ -19,7 +19,11 @@ An Android Studio template has been added to the project to help creating all fi
To install the template (to be done only once): To install the template (to be done only once):
- Go to folder `./tools/template`. - Go to folder `./tools/template`.
- Run the script `./configure.sh`. - Mac OSX: Run the script `./configure.sh`.
Linux: Run `ANDROID_STUDIO=/path/to/android-studio ./configure`
- e.g. `ANDROID_STUDIO=/usr/local/android-studio ./configure`
- Restart Android Studio. - Restart Android Studio.
To create a new screen: To create a new screen:
@ -27,7 +31,7 @@ To create a new screen:
- Then right click on the package, and select `New/New Vector/RiotX Feature`. - Then right click on the package, and select `New/New Vector/RiotX Feature`.
- Follow the Wizard, especially replace `Main` by something more relevant to your feature. - Follow the Wizard, especially replace `Main` by something more relevant to your feature.
- Click on `Finish`. - Click on `Finish`.
- Remainning steps are described as TODO in the generated files, or will be pointed out by the compilator, or at runtime :) - Remaining steps are described as TODO in the generated files, or will be pointed out by the compilator, or at runtime :)
Note that if the templates are modified, the only things to do is to restart Android Studio for the change to take effect. Note that if the templates are modified, the only things to do is to restart Android Studio for the change to take effect.

View file

@ -11,8 +11,8 @@ RiotX is an Android Matrix Client currently in beta but in active development.
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. RiotX will become the official replacement as soon as all features are implemented.
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" alt="Get it on Google Play" height="60">](https://play.google.com/store/apps/details?id=im.vector.riotx) [<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="https://f-droid.org/badge/get-it-on.png" alt="Get it on F-Droid" height="60">](https://f-droid.org/app/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)
Nightly build: [![Buildkite](https://badge.buildkite.com/657d3db27364448d69d54f66c690f7788bc6aa80a7628e37f3.svg?branch=develop)](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop) Nightly build: [![Buildkite](https://badge.buildkite.com/657d3db27364448d69d54f66c690f7788bc6aa80a7628e37f3.svg?branch=develop)](https://buildkite.com/matrix-dot-org/riotx-android/builds?branch=develop)

420
docs/voip_signaling.md Normal file
View file

@ -0,0 +1,420 @@
╔════════════════════════════════════════════════╗
║ ║
║A] Placing a call offer ║
║ ║
╚════════════════════════════════════════════════╝
┌───────────────┐
│ Matrix │
├───────────────┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
┌─────────────────┐ │ ┌───────────────────────────┐ ┌─────────────────┐
│ Caller │ │ Signaling Room │ │ │ Callee │
└─────────────────┘ │ ├───────────────────────────┤ └─────────────────┘
┌────┐ │ │ │
│ 3 │ │ │ ┌────────────────────┐ │
┌─────────────────┐──────┴────┴──────────────────────────┼─▶│ m.call.invite │ │ │ ┌─────────────────┐
│ │ │ │ │ mx event │ │ │ │
│ │ │ └────────────────────┘ │ │ │ │
│ │ │ │ │ │ │
│ Riot.im │ │ │ │ │ Riot.im │
┌──│ App │ │ │ │ │ App │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ └─────────────────┘ │ │ │ └─────────────────┘
┌────┤ ▲ │ │ │
│ 1 │ ├────┐ │ └───────────────────────────┘
└────┤ │ 2 │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
│ ┌──┴────┴─────────┐ ┌─────────────────┐
│ │ │ │ │
│ │ │ │ │
│ │ WebRtc │ │ WebRtc │
└─▶│ │ │ │
│ │ │ │
│ │ │ │
└─────────────────┘ └─────────────────┘
┌────┐
│ 1 │ The Caller app get access to system resources (camera, mic), eventually stun/turn servers, define some
└────┘ constrains (video quality, format) and pass it to WebRtc in order to create a Peer Call offer
┌────┐
│ 2 │ The WebRtc layer creates a call Offer (sdp) that needs to be sent to callee
└────┘
┌────┐ The app layer, takes the webrtc offer, encapsulate it in a matrix event adds a callId and send it to the other
│ 3 │ user via the room
└────┘
┌──────────────┐
│ mx event │
├──────────────┴────────┐
│ type: m.call.invite │
│ + callId │
│ │
│ ┌──────────────────┐ │
│ │ webrtc sdp │ │
│ └──────────────────┘ │
└───────────────────────┘
╔════════════════════════════════════════════════╗
║ ║
║B] Sending connection establishment info ║
║ ║
╚════════════════════════════════════════════════╝
┌───────────────┐
│ Matrix │
├───────────────┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
┌─────────────────┐ │ ┌───────────────────────────┐ ┌─────────────────┐
│ Caller │ │ Signaling Room │ │ │ Callee │
└─────────────────┘ │ ├───────────────────────────┤ └─────────────────┘
│ ┌────────────────────┐ │ │
│ │ │ m.call.invite │ │
┌─────────────────┐ │ │ mx event │ │ │ ┌─────────────────┐
│ │ ┌────┐ │ │ └────────────────────┘ │ │ │
│ │ │ 3 │ │ ┌────────────────────┐ │ │ │ │
│ │──────┴────┴───────┼──────────────────┼─▶│ m.call.candidates │ │ │ │
│ Riot.im │ │ │ mx event │ │ │ │ Riot.im │
│ App │ │ │ └────────────────────┘ │ │ App │
│ │ │ │ │ │ │
│ │ │ │ │ │ │
│ │ │ │ │ │ │
└─────────────────┘ │ │ │ └─────────────────┘
▲ │ │ │
├────┐ │ └───────────────────────────┘
│ 2 │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
┌───────┴────┴────┐ ┌─────────────────┐
│ │ │ │
│ │ │ │
│ WebRtc │ ┌───────────────┐ │ WebRtc │
│ │ │ Stun / Turn │ │ │
│ │ ├───────────────┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ │
│ │ │ │ │
└─────────────────┘ │ └─────────────────┘
▲ │
│ │
└──────────┬────┬───────────▶ │
┌───────────────┐ │ 1 │ │
│ │ └────┘ │
│ Network Stack │ │
│ │ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
│ │
└───────────────┘
┌────┐
│ 1 │ The WebRtc layer gathers information on how it can be reach by the other peer directly (Ice candidates)
└────┘
┌──────────────────────────────────────────────────────────────────┐
│candidate:1 1 tcp 1518149375 127.0.0.1 35990 typ host │
└──────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│candidate:2 1 UDP 2130706431 192.168.1.102 1816 typ host │
└──────────────────────────────────────────────────────────────────┘
┌────┐
│ 2 │ The WebRTC layer notifies the App layer when it finds new Ice Candidates
└────┘
┌────┐ The app layer, takes the ice candidates, encapsulate them in one or several matrix event adds the callId and
│ 3 │ send it to the other user via the room
└────┘
┌──────────────┐
│ mx event │
├──────────────┴────────────────────────┐
│ type: m.call.candidates │
│ │
│ +CallId │
│ │
│ ┌──────────────────┐ │
│ │ice candidate sdp │ │
│ └──────────────────┘ │
│ ┌──────────────────┐ │
│ │ice candidate sdp │ │
│ └──────────────────┘ │
│ ┌──────────────────┐ │
│ │ice candidate sdp │ │
│ └──────────────────┘ │
└───────────────────────────────────────┘
╔════════════════════════════════════════════════╗
║ ║
║C] Receiving a call offer ║
║ ║
╚════════════════════════════════════════════════╝
┌───────────────┐
│ Matrix │
├───────────────┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
│ ┌─────────────────┐
│ │ Callee │
┌─────────────────┐ │ ┌───────────────────────────┐ └─────────────────┘
│ Caller │ │ Signaling Room │ │
└─────────────────┘ │ ├───────────────────────────┤
│ ┌────────────────────┐ │ │ ┌─────────────────┐
│ │ │ m.call.invite │───┼────────────────────────────┬────┬───▶│ │
┌─────────────────┐ │ │ mx event │ │ │ │ 1 │ │ │
│ │ │ │ └────────────────────┘ │ └────┘ │ │
│ │ │ ┌────────────────────┐ │ │ │ Riot.im │
│ │ │ │ │ m.call.candidates │ │ │ App │
│ Riot.im │ │ │ mx event │ │ │ │ │
│ App │ │ │ └────────────────────┘ │ │ │
│ │ │ ┌────────────────────┐◀──┼─────────────────┼───┬────┬───────────┤ │
│ │◀──────────────────┼──────────────────┼──│ m.call.answer │ │ │ 4 │ └──┬──────────────┘
│ │ │ │ mx event │ │ │ └────┘ ├────┐ ▲
└────┬────────────┘ │ │ └────────────────────┘ │ │ 2 │ ├────┐
│ │ │ │ ├────┘ │ 3 │
│ │ └───────────────────────────┘ ┌──▼─────────┴────┤
┌────▼────────────┐ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ │
│ │ │ │
│ │ │ WebRtc │
│ WebRtc │ │ ┌──┴─────────────────┐
│ │ │ │ caller offer │
┌──────────┴─────────┐ │ │ └──┬─────────────────┘
│ callee answer │ │ └─────────────────┘
└────────────────────┴───────┘
┌────┐
│ 1 │ Bob receives a call.invite event in a room, then creates a WebRTC peer connection object
└────┘
┌────┐
│ 2 │ The encapsulated call offer sdp from the mx event is transmitted to WebRTC
└────┘
┌────┐
│ 3 │ WebRTC then creates a call answer for the offer and send it back to app layer
└────┘
┌────┐ The app layer, takes the webrtc answer, encapsulate it in a matrix event adds a callId and send it to the
│ 3 │ other user via the room
└────┘
┌──────────────┐
│ mx event │
├──────────────┴────────┐
│ type: m.call.answer │
│ + callId │
│ │
│ ┌──────────────────┐ │
│ │ webrtc sdp │ │
│ └──────────────────┘ │
└───────────────────────┘
╔════════════════════════════════════════════════╗
║ ║
║D] Callee sends connection establishment info ║
║ ║
╚════════════════════════════════════════════════╝
┌───────────────┐
│ Matrix │
├───────────────┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
┌─────────────────┐ │ ┌───────────────────────────┐ ┌─────────────────┐
│ Caller │ │ Signaling Room │ │ │ Callee │
└─────────────────┘ │ ├───────────────────────────┤ └─────────────────┘
│ ┌────────────────────┐ │ │
│ │ │ m.call.invite │ │
┌─────────────────┐ │ │ mx event │ │ │ ┌─────────────────┐
│ │ │ │ └────────────────────┘ │ │ │
│ │ │ ┌────────────────────┐ │ │ │ │
│ │ │ │ │ m.call.candidates │ │ │ │
│ Riot.im │ │ │ mx event │ │ │ │ Riot.im │
│ App │ │ │ └────────────────────┘ │ ┌────┐ │ App │
│ │ │ ┌────────────────────┐ │ │ │ 3 │ │ │
│ │◀──────────────────┼┐ │ │ m.call.answer │ │ ┌───────┴────┴────────│ │
│ │ │ │ │ mx event │ │ ││ │ │
└─────────────────┘ ││ │ └────────────────────┘ │ │ └─────────────────┘
│ │ │ ┌────────────────────┐ │ ││ ▲
│ │└─────────────────┼──│ m.call.candidates │ │ │ ├────┐
▼ │ │ mx event │◀──┼────────────────┘│ │ 2 │
┌─────────────────┐ │ │ └────────────────────┘ │ ┌────┴────┴───────┐
│ │ └───────────────────────────┘ │ │ │
│ │ │ │ │
│ WebRtc │ │ │ WebRtc │
│ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ ┌───┴────────────────┐
│ │ │ │ caller offer │
┌────────┴───────────┐ │ │ └───┬────────────────┘
│ callee answer ├─────┘ ┌───────────────┐ └─────────────────┘
├────────────────────┤ │ Stun / Turn │ ▲
│ callee ice │ ├───────────────┴ ─ ─ ─ ─ ─ ─ ─ ─ ┌────┐ │
│ candidates │ │ │ 1 │ │
└────────────────────┘ │ ├────┴──┴───────┐
│ │ │
│ │ Network Stack │
│◀─────────────────────┤ │
│ │ │
│ └───────────────┘
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
┌────┐
│ 1 │ The WebRtc layer gathers information on how it can be reach by the other peer directly (Ice candidates)
└────┘
┌──────────────────────────────────────────────────────────────────┐
│candidate:1 1 tcp 1518149375 127.0.0.1 35990 typ host │
└──────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│candidate:2 1 UDP 2130706431 192.168.1.102 1816 typ host │
└──────────────────────────────────────────────────────────────────┘
┌────┐
│ 2 │ The WebRTC layer notifies the App layer when it finds new Ice Candidates
└────┘
┌────┐ The app layer, takes the ice candidates, encapsulate them in one or several matrix event adds the callId and
│ 3 │ send it to the other user via the room
└────┘
┌──────────────┐
│ mx event │
├──────────────┴────────────────────────┐
│ type: m.call.candidates │
│ │
│ +CallId │
│ │
│ ┌──────────────────┐ │
│ │ice candidate sdp │ │
│ └──────────────────┘ │
│ ┌──────────────────┐ │
│ │ice candidate sdp │ │
│ └──────────────────┘ │
│ ┌──────────────────┐ │
│ │ice candidate sdp │ │
│ └──────────────────┘ │
└───────────────────────────────────────┘
╔════════════════════════════════════════════════╗
║ ║
║D] Caller Callee connection ║
║ ║
╚════════════════════════════════════════════════╝
┌───────────────┐
┌─────────────────┐ │ Matrix │ ┌─────────────────┐
│ Caller │ ├───────────────┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ Callee │
└─────────────────┘ │ └─────────────────┘
┌─────────────────┐ │ ┌─────────────────┐
│ │ │ │ │
│ │ │ │ │
│ │ │ │ │
│ Riot.im │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ Riot.im │
│ App │ │ App │
│ │ │ │
│ │ │ │
│ │ │ │
└─────────────────┘ └─────────────────┘
┌───────────────┐
│ Internet │
├───────────────┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
┌─────────────────┐ │ ┌─────────────────┐
│ │ │ │ │
│ ├───────────────────────────────────────────────────────────────────────────────────┴─────────────────────┤ │
│ WebRtc │█████████████████████████████████████████████████████████████████████████████████████████████████████████│ WebRtc │
┌─────────────┴──────┐ ├────────────────────────────────────────┬──────────────────────────┬───────────────┬─────────────────────┤ ┌─────┴──────────────┐
│ callee answer │ │ │ │ Video / Audio Stream │ │ │ caller offer │
├────────────────────┤ │ └──────────────────────────┘ │ │ ├────────────────────┤
│ callee ice ├──────────┘ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ └───────────┤ caller ice │
│ candidates │ │ candidates │
└────────────────────┘ └────────────────────┘
┌─────────────────────────────────────────────────────┐
│ │░
│ If connection is impossible (firewall), and a turn │░
│server is available, connection could happen through │░
│ a relay │░
│ │░
└─────────────────────────────────────────────────────┘░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
┌───────────────┐
│ Internet │
└─┬─────────────┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
┌─────────────────┐ │ ┌─────────────────┐
│ │ │ ┌─────────────────────────┐ │ │
│ ├───────────────────────────────────────┐│ │ │ │ │
│ WebRtc │███████████████████████████████████████││ │ │ WebRtc │
│ ├───────────────────────────────────────┘│ │ │ │ │
│ │ ┌────────┴─────────────────┐ │ Relay │┌─────────────────────────────────────┤ │
┌───────────────┴────┐ │ │ Video / Audio Stream │ │ ││█████████████████████████████████████│ ┌───────┴────────────┐
│ callee answer ├────────────┘ └────────┬─────────────────┘ │ │└─────────────────────────────────────┴─────────┤ caller offer │
├────────────────────┤ │ │ │ ├────────────────────┤
│ callee ice │ │ │ │ │ caller ice │
│ candidates │ └─────────────────────────┘ │ │ candidates │
└────────────────────┘ │ └────────────────────┘
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─

View file

@ -6,7 +6,7 @@ android {
compileSdkVersion 29 compileSdkVersion 29
defaultConfig { defaultConfig {
minSdkVersion 16 minSdkVersion 21
targetSdkVersion 29 targetSdkVersion 29
versionCode 1 versionCode 1
versionName "1.0" versionName "1.0"

View file

@ -23,7 +23,7 @@ android {
testOptions.unitTests.includeAndroidResources = true testOptions.unitTests.includeAndroidResources = true
defaultConfig { defaultConfig {
minSdkVersion 16 minSdkVersion 21
targetSdkVersion 29 targetSdkVersion 29
versionCode 1 versionCode 1
versionName "0.0.1" versionName "0.0.1"
@ -35,6 +35,10 @@ android {
resValue "string", "git_sdk_revision", "\"${gitRevision()}\"" resValue "string", "git_sdk_revision", "\"${gitRevision()}\""
resValue "string", "git_sdk_revision_unix_date", "\"${gitRevisionUnixDate()}\"" resValue "string", "git_sdk_revision_unix_date", "\"${gitRevisionUnixDate()}\""
resValue "string", "git_sdk_revision_date", "\"${gitRevisionDate()}\"" resValue "string", "git_sdk_revision_date", "\"${gitRevisionDate()}\""
defaultConfig {
consumerProguardFiles 'proguard-rules.pro'
}
} }
buildTypes { buildTypes {
@ -49,9 +53,6 @@ android {
release { release {
buildConfigField "boolean", "LOG_PRIVATE_DATA", "false" buildConfigField "boolean", "LOG_PRIVATE_DATA", "false"
buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.NONE" buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.NONE"
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
} }
} }
@ -113,6 +114,7 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
implementation "androidx.appcompat:appcompat:1.1.0" implementation "androidx.appcompat:appcompat:1.1.0"
implementation "androidx.core:core-ktx:1.1.0"
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
@ -161,6 +163,10 @@ dependencies {
// Phone number https://github.com/google/libphonenumber // Phone number https://github.com/google/libphonenumber
implementation 'com.googlecode.libphonenumber:libphonenumber:8.10.23' implementation 'com.googlecode.libphonenumber:libphonenumber:8.10.23'
// Web RTC
// TODO meant for development purposes only. See http://webrtc.github.io/webrtc-org/native-code/android/
implementation 'org.webrtc:google-webrtc:1.0.+'
debugImplementation 'com.airbnb.okreplay:okreplay:1.5.0' debugImplementation 'com.airbnb.okreplay:okreplay:1.5.0'
releaseImplementation 'com.airbnb.okreplay:noop:1.5.0' releaseImplementation 'com.airbnb.okreplay:noop:1.5.0'
androidTestImplementation 'com.airbnb.okreplay:espresso:1.5.0' androidTestImplementation 'com.airbnb.okreplay:espresso:1.5.0'

View file

@ -19,3 +19,45 @@
# If you keep the line number information, uncomment this to # If you keep the line number information, uncomment this to
# hide the original source file name. # hide the original source file name.
#-renamesourcefileattribute SourceFile #-renamesourcefileattribute SourceFile
### EVENT BUS ###
-keepattributes *Annotation*
-keepclassmembers class * {
@org.greenrobot.eventbus.Subscribe <methods>;
}
-keep enum org.greenrobot.eventbus.ThreadMode { *; }
### MOSHI ###
# JSR 305 annotations are for embedding nullability information.
-dontwarn javax.annotation.**
-keepclasseswithmembers class * {
@com.squareup.moshi.* <methods>;
}
-keep @com.squareup.moshi.JsonQualifier interface *
# Enum field names are used by the integrated EnumJsonAdapter.
# values() is synthesized by the Kotlin compiler and is used by EnumJsonAdapter indirectly
# Annotate enums with @JsonClass(generateAdapter = false) to use them with Moshi.
-keepclassmembers @com.squareup.moshi.JsonClass class * extends java.lang.Enum {
<fields>;
**[] values();
}
-keep class kotlin.reflect.jvm.internal.impl.builtins.BuiltInsLoaderImpl
-keepclassmembers class kotlin.Metadata {
public <methods>;
}
### OKHTTP for Android Studio ###
-keep class okhttp3.Headers { *; }
-keep interface okhttp3.Interceptor.* { *; }
### OLM JNI ###
-keep class org.matrix.olm.** { *; }

View file

@ -241,14 +241,14 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
val eventWireContent = event.content.toContent() val eventWireContent = event.content.toContent()
assertNotNull(eventWireContent) assertNotNull(eventWireContent)
assertNull(eventWireContent.get("body")) assertNull(eventWireContent["body"])
assertEquals(MXCRYPTO_ALGORITHM_MEGOLM, eventWireContent.get("algorithm")) assertEquals(MXCRYPTO_ALGORITHM_MEGOLM, eventWireContent["algorithm"])
assertNotNull(eventWireContent.get("ciphertext")) assertNotNull(eventWireContent["ciphertext"])
assertNotNull(eventWireContent.get("session_id")) assertNotNull(eventWireContent["session_id"])
assertNotNull(eventWireContent.get("sender_key")) assertNotNull(eventWireContent["sender_key"])
assertEquals(senderSession.sessionParams.deviceId, eventWireContent.get("device_id")) assertEquals(senderSession.sessionParams.deviceId, eventWireContent["device_id"])
assertNotNull(event.eventId) assertNotNull(event.eventId)
assertEquals(roomId, event.roomId) assertEquals(roomId, event.roomId)
@ -257,7 +257,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
val eventContent = event.toContent() val eventContent = event.toContent()
assertNotNull(eventContent) assertNotNull(eventContent)
assertEquals(clearMessage, eventContent.get("body")) assertEquals(clearMessage, eventContent["body"])
assertEquals(senderSession.myUserId, event.senderId) assertEquals(senderSession.myUserId, event.senderId)
} }

View file

@ -90,7 +90,7 @@ class KeyShareTests : InstrumentedTest {
} catch (failure: Throwable) { } catch (failure: Throwable) {
} }
val outgoingRequestBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequest() val outgoingRequestsBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
// Try to request // Try to request
aliceSession2.cryptoService().requestRoomKeyForEvent(receivedEvent.root) aliceSession2.cryptoService().requestRoomKeyForEvent(receivedEvent.root)
@ -100,10 +100,10 @@ class KeyShareTests : InstrumentedTest {
var outGoingRequestId: String? = null var outGoingRequestId: String? = null
mTestHelper.retryPeriodicallyWithLatch(waitLatch) { mTestHelper.retryPeriodicallyWithLatch(waitLatch) {
aliceSession2.cryptoService().getOutgoingRoomKeyRequest() aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
.filter { req -> .filter { req ->
// filter out request that was known before // filter out request that was known before
!outgoingRequestBefore.any { req.requestId == it.requestId } !outgoingRequestsBefore.any { req.requestId == it.requestId }
} }
.let { .let {
val outgoing = it.firstOrNull { it.sessionId == eventMegolmSessionId } val outgoing = it.firstOrNull { it.sessionId == eventMegolmSessionId }
@ -115,10 +115,10 @@ class KeyShareTests : InstrumentedTest {
Log.v("TEST", "=======> Outgoing requet Id is $outGoingRequestId") Log.v("TEST", "=======> Outgoing requet Id is $outGoingRequestId")
val outgoingRequestAfter = aliceSession2.cryptoService().getOutgoingRoomKeyRequest() val outgoingRequestAfter = aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
// We should have a new request // We should have a new request
Assert.assertTrue(outgoingRequestAfter.size > outgoingRequestBefore.size) Assert.assertTrue(outgoingRequestAfter.size > outgoingRequestsBefore.size)
Assert.assertNotNull(outgoingRequestAfter.first { it.sessionId == eventMegolmSessionId }) Assert.assertNotNull(outgoingRequestAfter.first { it.sessionId == eventMegolmSessionId })
// The first session should see an incoming request // The first session should see an incoming request
@ -126,7 +126,7 @@ class KeyShareTests : InstrumentedTest {
mTestHelper.waitWithLatch { latch -> mTestHelper.waitWithLatch { latch ->
mTestHelper.retryPeriodicallyWithLatch(latch) { mTestHelper.retryPeriodicallyWithLatch(latch) {
// DEBUG LOGS // DEBUG LOGS
aliceSession.cryptoService().getIncomingRoomKeyRequest().let { aliceSession.cryptoService().getIncomingRoomKeyRequests().let {
Log.v("TEST", "Incoming request Session 1 (looking for $outGoingRequestId)") Log.v("TEST", "Incoming request Session 1 (looking for $outGoingRequestId)")
Log.v("TEST", "=========================") Log.v("TEST", "=========================")
it.forEach { keyRequest -> it.forEach { keyRequest ->
@ -135,7 +135,7 @@ class KeyShareTests : InstrumentedTest {
Log.v("TEST", "=========================") Log.v("TEST", "=========================")
} }
val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequest().firstOrNull { it.requestId == outGoingRequestId } val incoming = aliceSession.cryptoService().getIncomingRoomKeyRequests().firstOrNull { it.requestId == outGoingRequestId }
incoming?.state == GossipingRequestState.REJECTED incoming?.state == GossipingRequestState.REJECTED
} }
} }
@ -155,7 +155,7 @@ class KeyShareTests : InstrumentedTest {
mTestHelper.waitWithLatch { latch -> mTestHelper.waitWithLatch { latch ->
mTestHelper.retryPeriodicallyWithLatch(latch) { mTestHelper.retryPeriodicallyWithLatch(latch) {
aliceSession.cryptoService().getIncomingRoomKeyRequest().let { aliceSession.cryptoService().getIncomingRoomKeyRequests().let {
Log.v("TEST", "Incoming request Session 1") Log.v("TEST", "Incoming request Session 1")
Log.v("TEST", "=========================") Log.v("TEST", "=========================")
it.forEach { it.forEach {
@ -171,7 +171,7 @@ class KeyShareTests : InstrumentedTest {
Thread.sleep(6_000) Thread.sleep(6_000)
mTestHelper.waitWithLatch { latch -> mTestHelper.waitWithLatch { latch ->
mTestHelper.retryPeriodicallyWithLatch(latch) { mTestHelper.retryPeriodicallyWithLatch(latch) {
aliceSession2.cryptoService().getOutgoingRoomKeyRequest().let { aliceSession2.cryptoService().getOutgoingRoomKeyRequests().let {
it.any { it.requestBody?.sessionId == eventMegolmSessionId && it.state == OutgoingGossipingRequestState.CANCELLED } it.any { it.requestBody?.sessionId == eventMegolmSessionId && it.state == OutgoingGossipingRequestState.CANCELLED }
} }
} }
@ -252,7 +252,7 @@ class KeyShareTests : InstrumentedTest {
} }
}) })
val txId: String = "m.testVerif12" val txId = "m.testVerif12"
aliceVerificationService2.beginKeyVerification(VerificationMethod.SAS, aliceSession1.myUserId, aliceSession1.sessionParams.deviceId aliceVerificationService2.beginKeyVerification(VerificationMethod.SAS, aliceSession1.myUserId, aliceSession1.sessionParams.deviceId
?: "", txId) ?: "", txId)

View file

@ -144,7 +144,7 @@ class QuadSTests : InstrumentedTest {
val secretAccountData = assertAccountData(aliceSession, "secret.of.life") val secretAccountData = assertAccountData(aliceSession, "secret.of.life")
val encryptedContent = secretAccountData.content.get("encrypted") as? Map<*, *> val encryptedContent = secretAccountData.content["encrypted"] as? Map<*, *>
assertNotNull("Element should be encrypted", encryptedContent) assertNotNull("Element should be encrypted", encryptedContent)
assertNotNull("Secret should be encrypted with default key", encryptedContent?.get(keyId)) assertNotNull("Secret should be encrypted with default key", encryptedContent?.get(keyId))

View file

@ -69,7 +69,7 @@ data class HomeServerConnectionConfig(
*/ */
fun withHomeServerUri(hsUri: Uri): Builder { fun withHomeServerUri(hsUri: Uri): Builder {
if (hsUri.scheme != "http" && hsUri.scheme != "https") { if (hsUri.scheme != "http" && hsUri.scheme != "https") {
throw RuntimeException("Invalid home server URI: " + hsUri) throw RuntimeException("Invalid home server URI: $hsUri")
} }
// ensure trailing / // ensure trailing /
val hsString = hsUri.toString().ensureTrailingSlash() val hsString = hsUri.toString().ensureTrailingSlash()

View file

@ -16,10 +16,15 @@
package im.vector.matrix.android.api.extensions package im.vector.matrix.android.api.extensions
inline fun <A> tryThis(operation: () -> A): A? { import timber.log.Timber
inline fun <A> tryThis(message: String? = null, operation: () -> A): A? {
return try { return try {
operation() operation()
} catch (any: Throwable) { } catch (any: Throwable) {
if (message != null) {
Timber.e(any, message)
}
null null
} }
} }

View file

@ -87,14 +87,13 @@ class EventMatchCondition(
// Very simple glob to regexp converter // Very simple glob to regexp converter
private fun simpleGlobToRegExp(glob: String): String { private fun simpleGlobToRegExp(glob: String): String {
var out = "" // "^" var out = "" // "^"
for (i in 0 until glob.length) { for (element in glob) {
val c = glob[i] when (element) {
when (c) {
'*' -> out += ".*" '*' -> out += ".*"
'?' -> out += '.'.toString() '?' -> out += '.'.toString()
'.' -> out += "\\." '.' -> out += "\\."
'\\' -> out += "\\\\" '\\' -> out += "\\\\"
else -> out += c else -> out += element
} }
} }
out += "" // '$'.toString() out += "" // '$'.toString()

View file

@ -24,6 +24,7 @@ import im.vector.matrix.android.api.pushrules.PushRuleService
import im.vector.matrix.android.api.session.account.AccountService import im.vector.matrix.android.api.session.account.AccountService
import im.vector.matrix.android.api.session.accountdata.AccountDataService import im.vector.matrix.android.api.session.accountdata.AccountDataService
import im.vector.matrix.android.api.session.cache.CacheService import im.vector.matrix.android.api.session.cache.CacheService
import im.vector.matrix.android.api.session.call.CallSignalingService
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.crypto.CryptoService
@ -171,6 +172,11 @@ interface Session :
*/ */
fun integrationManagerService(): IntegrationManagerService fun integrationManagerService(): IntegrationManagerService
/**
* Returns the call signaling service associated with the session
*/
fun callSignalingService(): CallSignalingService
/** /**
* Add a listener to the session. * Add a listener to the session.
* @param listener the listener to add. * @param listener the listener to add.

View file

@ -0,0 +1,36 @@
/*
* 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.call
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
interface CallSignalingService {
fun getTurnServer(callback: MatrixCallback<TurnServerResponse>): Cancelable
/**
* Create an outgoing call
*/
fun createOutgoingCall(roomId: String, otherUserId: String, isVideoCall: Boolean): MxCall
fun addCallListener(listener: CallsListener)
fun removeCallListener(listener: CallsListener)
fun getCallWithId(callId: String) : MxCall?
}

View file

@ -0,0 +1,44 @@
/*
* 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.call
import org.webrtc.PeerConnection
sealed class CallState {
/** Idle, setting up objects */
object Idle : CallState()
/** Dialing. Outgoing call is signaling the remote peer */
object Dialing : CallState()
/** Local ringing. Incoming call offer received */
object LocalRinging : CallState()
/** Answering. Incoming call is responding to remote peer */
object Answering : CallState()
/**
* Connected. Incoming/Outgoing call, ice layer connecting or connected
* Notice that the PeerState failed is not always final, if you switch network, new ice candidtates
* could be exchanged, and the connection could go back to connected
* */
data class Connected(val iceConnectionState: PeerConnection.PeerConnectionState) : CallState()
/** Terminated. Incoming/Outgoing call, the call is terminated */
object Terminated : CallState()
}

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.call
import im.vector.matrix.android.api.session.room.model.call.CallAnswerContent
import im.vector.matrix.android.api.session.room.model.call.CallCandidatesContent
import im.vector.matrix.android.api.session.room.model.call.CallHangupContent
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
interface CallsListener {
/**
* Called when there is an incoming call within the room.
*/
fun onCallInviteReceived(mxCall: MxCall, callInviteContent: CallInviteContent)
fun onCallIceCandidateReceived(mxCall: MxCall, iceCandidatesContent: CallCandidatesContent)
/**
* An outgoing call is started.
*/
fun onCallAnswerReceived(callAnswerContent: CallAnswerContent)
/**
* Called when a called has been hung up
*/
fun onCallHangupReceived(callHangupContent: CallHangupContent)
fun onCallManagedByOtherSession(callId: String)
}

View file

@ -0,0 +1,55 @@
/*
* 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.call
import org.webrtc.EglBase
import timber.log.Timber
/**
* The root [EglBase] instance shared by the entire application for
* the sake of reducing the utilization of system resources (such as EGL
* contexts)
* by performing a runtime check.
*/
object EglUtils {
// TODO how do we release that?
/**
* Lazily creates and returns the one and only [EglBase] which will
* serve as the root for all contexts that are needed.
*/
@get:Synchronized var rootEglBase: EglBase? = null
get() {
if (field == null) {
val configAttributes = EglBase.CONFIG_PLAIN
try {
field = EglBase.createEgl14(configAttributes)
?: EglBase.createEgl10(configAttributes) // Fall back to EglBase10.
} catch (ex: Throwable) {
Timber.e(ex, "Failed to create EglBase")
}
}
return field
}
private set
val rootEglBaseContext: EglBase.Context?
get() {
val eglBase = rootEglBase
return eglBase?.eglBaseContext
}
}

View file

@ -0,0 +1,75 @@
/*
* 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.call
import org.webrtc.IceCandidate
import org.webrtc.SessionDescription
interface MxCallDetail {
val callId: String
val isOutgoing: Boolean
val roomId: String
val otherUserId: String
val isVideoCall: Boolean
}
/**
* Define both an incoming call and on outgoing call
*/
interface MxCall : MxCallDetail {
var state: CallState
/**
* Pick Up the incoming call
* It has no effect on outgoing call
*/
fun accept(sdp: SessionDescription)
/**
* Reject an incoming call
* It's an alias to hangUp
*/
fun reject() = hangUp()
/**
* End the call
*/
fun hangUp()
/**
* Start a call
* Send offer SDP to the other participant.
*/
fun offerSdp(sdp: SessionDescription)
/**
* Send Ice candidate to the other participant.
*/
fun sendLocalIceCandidates(candidates: List<IceCandidate>)
/**
* Send removed ICE candidates to the other participant.
*/
fun sendLocalIceCandidateRemovals(candidates: List<IceCandidate>)
fun addListener(listener: StateListener)
fun removeListener(listener: StateListener)
interface StateListener {
fun onStateUpdate(call: MxCall)
}
}

View file

@ -0,0 +1,47 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.call
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
// TODO Should not be exposed
/**
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-voip-turnserver
*/
@JsonClass(generateAdapter = true)
data class TurnServerResponse(
/**
* Required. The username to use.
*/
@Json(name = "username") val username: String?,
/**
* Required. The password to use.
*/
@Json(name = "password") val password: String?,
/**
* Required. A list of TURN URIs
*/
@Json(name = "uris") val uris: List<String>?,
/**
* Required. The time-to-live in seconds
*/
@Json(name = "ttl") val ttl: Int?
)

View file

@ -138,7 +138,9 @@ interface CryptoService {
fun removeSessionListener(listener: NewSessionListener) fun removeSessionListener(listener: NewSessionListener)
fun getOutgoingRoomKeyRequest(): List<OutgoingRoomKeyRequest> fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
fun getIncomingRoomKeyRequest(): List<IncomingRoomKeyRequest>
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
fun getGossipingEventsTrail(): List<Event> fun getGossipingEventsTrail(): List<Event>
} }

View file

@ -58,7 +58,6 @@ object EventType {
const val STATE_ROOM_ENCRYPTION = "m.room.encryption" const val STATE_ROOM_ENCRYPTION = "m.room.encryption"
// Call Events // Call Events
const val CALL_INVITE = "m.call.invite" const val CALL_INVITE = "m.call.invite"
const val CALL_CANDIDATES = "m.call.candidates" const val CALL_CANDIDATES = "m.call.candidates"
const val CALL_ANSWER = "m.call.answer" const val CALL_ANSWER = "m.call.answer"

View file

@ -26,5 +26,5 @@ object RelationType {
/** Lets you define an event which references an existing event.*/ /** Lets you define an event which references an existing event.*/
const val REFERENCE = "m.reference" const val REFERENCE = "m.reference"
/** Lets you define an event which adds a response to an existing event.*/ /** Lets you define an event which adds a response to an existing event.*/
const val RESPONSE = "m.response" const val RESPONSE = "org.matrix.response"
} }

View file

@ -35,12 +35,19 @@ interface ProfileService {
} }
/** /**
* Return the current dispayname for this user * Return the current display name for this user
* @param userId the userId param to look for * @param userId the userId param to look for
* *
*/ */
fun getDisplayName(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable fun getDisplayName(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable
/**
* Update the display name for this user
* @param userId the userId to update the display name of
* @param newDisplayName the new display name of the user
*/
fun setDisplayName(userId: String, newDisplayName: String, matrixCallback: MatrixCallback<Unit>): Cancelable
/** /**
* Return the current avatarUrl for this user. * Return the current avatarUrl for this user.
* @param userId the userId param to look for * @param userId the userId param to look for

View file

@ -17,6 +17,7 @@
package im.vector.matrix.android.api.session.room package im.vector.matrix.android.api.session.room
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.session.room.call.RoomCallService
import im.vector.matrix.android.api.session.room.crypto.RoomCryptoService import im.vector.matrix.android.api.session.room.crypto.RoomCryptoService
import im.vector.matrix.android.api.session.room.members.MembershipService import im.vector.matrix.android.api.session.room.members.MembershipService
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
@ -47,6 +48,7 @@ interface Room :
StateService, StateService,
UploadsService, UploadsService,
ReportingService, ReportingService,
RoomCallService,
RelationService, RelationService,
RoomCryptoService, RoomCryptoService,
RoomPushRuleService { RoomPushRuleService {

View file

@ -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.android.api.session.room.call
/**
* This interface defines methods to handle calls in a room. It's implemented at the room level.
*/
interface RoomCallService {
/**
* Return true if calls (audio or video) can be performed on this Room
*/
fun canStartCall(): Boolean
}

View file

@ -17,10 +17,12 @@
package im.vector.matrix.android.api.session.room.model package im.vector.matrix.android.api.session.room.model
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/** /**
* Represents the membership of a user on a room * Represents the membership of a user on a room
*/ */
@JsonClass(generateAdapter = false)
enum class Membership(val value: String) { enum class Membership(val value: String) {
NONE("none"), NONE("none"),

View file

@ -17,7 +17,9 @@
package im.vector.matrix.android.api.session.room.model package im.vector.matrix.android.api.session.room.model
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = false)
enum class RoomDirectoryVisibility { enum class RoomDirectoryVisibility {
@Json(name = "private") PRIVATE, @Json(name = "private") PRIVATE,
@Json(name = "public") PUBLIC @Json(name = "public") PUBLIC

View file

@ -29,6 +29,7 @@ data class RoomGuestAccessContent(
@Json(name = "guest_access") val guestAccess: GuestAccess? = null @Json(name = "guest_access") val guestAccess: GuestAccess? = null
) )
@JsonClass(generateAdapter = false)
enum class GuestAccess(val value: String) { enum class GuestAccess(val value: String) {
@Json(name = "can_join") @Json(name = "can_join")
CanJoin("can_join"), CanJoin("can_join"),

View file

@ -17,10 +17,12 @@
package im.vector.matrix.android.api.session.room.model package im.vector.matrix.android.api.session.room.model
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/** /**
* Ref: https://matrix.org/docs/spec/client_server/latest#room-history-visibility * Ref: https://matrix.org/docs/spec/client_server/latest#room-history-visibility
*/ */
@JsonClass(generateAdapter = false)
enum class RoomHistoryVisibility { enum class RoomHistoryVisibility {
/** /**
* All events while this is the m.room.history_visibility value may be shared by any * All events while this is the m.room.history_visibility value may be shared by any

View file

@ -18,10 +18,12 @@
package im.vector.matrix.android.api.session.room.model package im.vector.matrix.android.api.session.room.model
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/** /**
* Enum for [RoomJoinRulesContent] : https://matrix.org/docs/spec/client_server/r0.4.0#m-room-join-rules * Enum for [RoomJoinRulesContent] : https://matrix.org/docs/spec/client_server/r0.4.0#m-room-join-rules
*/ */
@JsonClass(generateAdapter = false)
enum class RoomJoinRules(val value: String) { enum class RoomJoinRules(val value: String) {
@Json(name = "public") @Json(name = "public")

View file

@ -61,6 +61,9 @@ data class RoomSummary constructor(
val isFavorite: Boolean val isFavorite: Boolean
get() = tags.any { it.name == RoomTag.ROOM_TAG_FAVOURITE } get() = tags.any { it.name == RoomTag.ROOM_TAG_FAVOURITE }
val canStartCall: Boolean
get() = isDirect && joinedMembersCount == 2
companion object { companion object {
const val NOT_IN_BREADCRUMBS = -1 const val NOT_IN_BREADCRUMBS = -1
} }

View file

@ -19,16 +19,34 @@ package im.vector.matrix.android.api.session.room.model.call
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
/**
* This event is sent by the callee when they wish to answer the call.
*/
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class CallAnswerContent( data class CallAnswerContent(
/**
* Required. The ID of the call this event relates to.
*/
@Json(name = "call_id") val callId: String, @Json(name = "call_id") val callId: String,
@Json(name = "version") val version: Int, /**
@Json(name = "answer") val answer: Answer * Required. The session description object
*/
@Json(name = "answer") val answer: Answer,
/**
* Required. The version of the VoIP specification this messages adheres to. This specification is version 0.
*/
@Json(name = "version") val version: Int = 0
) { ) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class Answer( data class Answer(
@Json(name = "type") val type: String, /**
* Required. The type of session description. Must be 'answer'.
*/
@Json(name = "type") val type: SdpType = SdpType.ANSWER,
/**
* Required. The SDP text of the session description.
*/
@Json(name = "sdp") val sdp: String @Json(name = "sdp") val sdp: String
) )
} }

View file

@ -19,17 +19,39 @@ package im.vector.matrix.android.api.session.room.model.call
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
/**
* This event is sent by callers after sending an invite and by the callee after answering.
* Its purpose is to give the other party additional ICE candidates to try using to communicate.
*/
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class CallCandidatesContent( data class CallCandidatesContent(
/**
* Required. The ID of the call this event relates to.
*/
@Json(name = "call_id") val callId: String, @Json(name = "call_id") val callId: String,
@Json(name = "version") val version: Int, /**
@Json(name = "candidates") val candidates: List<Candidate> = emptyList() * Required. Array of objects describing the candidates.
*/
@Json(name = "candidates") val candidates: List<Candidate> = emptyList(),
/**
* Required. The version of the VoIP specification this messages adheres to. This specification is version 0.
*/
@Json(name = "version") val version: Int = 0
) { ) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class Candidate( data class Candidate(
/**
* Required. The SDP media type this candidate is intended for.
*/
@Json(name = "sdpMid") val sdpMid: String, @Json(name = "sdpMid") val sdpMid: String,
@Json(name = "sdpMLineIndex") val sdpMLineIndex: String, /**
* Required. The index of the SDP 'm' line this candidate is intended for.
*/
@Json(name = "sdpMLineIndex") val sdpMLineIndex: Int,
/**
* Required. The SDP 'a' line of the candidate.
*/
@Json(name = "candidate") val candidate: String @Json(name = "candidate") val candidate: String
) )
} }

View file

@ -19,8 +19,32 @@ package im.vector.matrix.android.api.session.room.model.call
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
/**
* Sent by either party to signal their termination of the call. This can be sent either once
* the call has been established or before to abort the call.
*/
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class CallHangupContent( data class CallHangupContent(
/**
* Required. The ID of the call this event relates to.
*/
@Json(name = "call_id") val callId: String, @Json(name = "call_id") val callId: String,
@Json(name = "version") val version: Int /**
) * Required. The version of the VoIP specification this message adheres to. This specification is version 0.
*/
@Json(name = "version") val version: Int = 0,
/**
* Optional error reason for the hangup. This should not be provided when the user naturally ends or rejects the call.
* When there was an error in the call negotiation, this should be `ice_failed` for when ICE negotiation fails
* or `invite_timeout` for when the other party did not answer in time. One of: ["ice_failed", "invite_timeout"]
*/
@Json(name = "reason") val reason: Reason? = null
) {
enum class Reason {
@Json(name = "ice_failed")
ICE_FAILED,
@Json(name = "invite_timeout")
INVITE_TIMEOUT
}
}

View file

@ -19,23 +19,45 @@ package im.vector.matrix.android.api.session.room.model.call
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
/**
* This event is sent by the caller when they wish to establish a call.
*/
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class CallInviteContent( data class CallInviteContent(
@Json(name = "call_id") val callId: String, /**
@Json(name = "version") val version: Int, * Required. A unique identifier for the call.
@Json(name = "lifetime") val lifetime: Int, */
@Json(name = "offer") val offer: Offer @Json(name = "call_id") val callId: String?,
/**
* Required. The session description object
*/
@Json(name = "offer") val offer: Offer?,
/**
* Required. The version of the VoIP specification this message adheres to. This specification is version 0.
*/
@Json(name = "version") val version: Int? = 0,
/**
* Required. The time in milliseconds that the invite is valid for.
* Once the invite age exceeds this value, clients should discard it.
* They should also no longer show the call as awaiting an answer in the UI.
*/
@Json(name = "lifetime") val lifetime: Int?
) { ) {
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class Offer( data class Offer(
@Json(name = "type") val type: String, /**
@Json(name = "sdp") val sdp: String * Required. The type of session description. Must be 'offer'.
*/
@Json(name = "type") val type: SdpType? = SdpType.OFFER,
/**
* Required. The SDP text of the session description.
*/
@Json(name = "sdp") val sdp: String?
) { ) {
companion object { companion object {
const val SDP_VIDEO = "m=video" const val SDP_VIDEO = "m=video"
} }
} }
fun isVideo(): Boolean = offer.sdp.contains(Offer.SDP_VIDEO) fun isVideo(): Boolean = offer?.sdp?.contains(Offer.SDP_VIDEO) == true
} }

View file

@ -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.android.api.session.room.model.call
import com.squareup.moshi.Json
enum class SdpType {
@Json(name = "offer")
OFFER,
@Json(name = "answer")
ANSWER
}

View file

@ -17,7 +17,9 @@
package im.vector.matrix.android.api.session.room.model.create package im.vector.matrix.android.api.session.room.model.create
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = false)
enum class CreateRoomPreset { enum class CreateRoomPreset {
@Json(name = "private_chat") @Json(name = "private_chat")
PRESET_PRIVATE_CHAT, PRESET_PRIVATE_CHAT,

View file

@ -90,6 +90,6 @@ interface WidgetPostAPIMediator {
/** /**
* Triggered when a widget is posting * Triggered when a widget is posting
*/ */
fun handleWidgetRequest(eventData: JsonDict): Boolean fun handleWidgetRequest(mediator: WidgetPostAPIMediator, eventData: JsonDict): Boolean
} }
} }

View file

@ -34,7 +34,8 @@ interface WidgetService {
fun getWidgetURLFormatter(): WidgetURLFormatter fun getWidgetURLFormatter(): WidgetURLFormatter
/** /**
* Returns an instance of [WidgetPostAPIMediator]. * Returns a new instance of [WidgetPostAPIMediator].
* Be careful to call clearWebView method and setHandler to null to avoid memory leaks.
* This is to be used for "admin" widgets so you can interact through JS. * This is to be used for "admin" widgets so you can interact through JS.
*/ */
fun getWidgetPostAPIMediator(): WidgetPostAPIMediator fun getWidgetPostAPIMediator(): WidgetPostAPIMediator

View file

@ -21,6 +21,9 @@ import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.util.JsonDict import im.vector.matrix.android.api.util.JsonDict
/**
* Ref: https://github.com/matrix-org/matrix-doc/issues/1236
*/
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class WidgetContent( data class WidgetContent(
@Json(name = "creatorUserId") val creatorUserId: String? = null, @Json(name = "creatorUserId") val creatorUserId: String? = null,

View file

@ -16,6 +16,25 @@
package im.vector.matrix.android.api.session.widgets.model package im.vector.matrix.android.api.session.widgets.model
private val DEFINED_TYPES by lazy {
listOf(
WidgetType.Jitsi,
WidgetType.TradingView,
WidgetType.Spotify,
WidgetType.Video,
WidgetType.GoogleDoc,
WidgetType.GoogleCalendar,
WidgetType.Etherpad,
WidgetType.StickerPicker,
WidgetType.Grafana,
WidgetType.Custom,
WidgetType.IntegrationManager
)
}
/**
* Ref: https://github.com/matrix-org/matrix-doc/issues/1236
*/
sealed class WidgetType(open val preferred: String, open val legacy: String = preferred) { sealed class WidgetType(open val preferred: String, open val legacy: String = preferred) {
object Jitsi : WidgetType("m.jitsi", "jitsi") object Jitsi : WidgetType("m.jitsi", "jitsi")
object TradingView : WidgetType("m.tradingview") object TradingView : WidgetType("m.tradingview")
@ -30,7 +49,7 @@ sealed class WidgetType(open val preferred: String, open val legacy: String = pr
object IntegrationManager : WidgetType("m.integration_manager") object IntegrationManager : WidgetType("m.integration_manager")
data class Fallback(override val preferred: String) : WidgetType(preferred) data class Fallback(override val preferred: String) : WidgetType(preferred)
fun matches(type: String?): Boolean { fun matches(type: String): Boolean {
return type == preferred || type == legacy return type == preferred || type == legacy
} }
@ -40,20 +59,6 @@ sealed class WidgetType(open val preferred: String, open val legacy: String = pr
companion object { companion object {
private val DEFINED_TYPES = listOf(
Jitsi,
TradingView,
Spotify,
Video,
GoogleDoc,
GoogleCalendar,
Etherpad,
StickerPicker,
Grafana,
Custom,
IntegrationManager
)
fun fromString(type: String): WidgetType { fun fromString(type: String): WidgetType {
val matchingType = DEFINED_TYPES.firstOrNull { val matchingType = DEFINED_TYPES.firstOrNull {
it.matches(type) it.matches(type)

View file

@ -67,6 +67,7 @@ import im.vector.matrix.android.internal.crypto.tasks.DefaultEncryptEventTask
import im.vector.matrix.android.internal.crypto.tasks.DefaultGetDeviceInfoTask import im.vector.matrix.android.internal.crypto.tasks.DefaultGetDeviceInfoTask
import im.vector.matrix.android.internal.crypto.tasks.DefaultGetDevicesTask import im.vector.matrix.android.internal.crypto.tasks.DefaultGetDevicesTask
import im.vector.matrix.android.internal.crypto.tasks.DefaultInitializeCrossSigningTask import im.vector.matrix.android.internal.crypto.tasks.DefaultInitializeCrossSigningTask
import im.vector.matrix.android.internal.crypto.tasks.DefaultSendEventTask
import im.vector.matrix.android.internal.crypto.tasks.DefaultSendToDeviceTask import im.vector.matrix.android.internal.crypto.tasks.DefaultSendToDeviceTask
import im.vector.matrix.android.internal.crypto.tasks.DefaultSendVerificationMessageTask import im.vector.matrix.android.internal.crypto.tasks.DefaultSendVerificationMessageTask
import im.vector.matrix.android.internal.crypto.tasks.DefaultSetDeviceNameTask import im.vector.matrix.android.internal.crypto.tasks.DefaultSetDeviceNameTask
@ -80,6 +81,7 @@ import im.vector.matrix.android.internal.crypto.tasks.EncryptEventTask
import im.vector.matrix.android.internal.crypto.tasks.GetDeviceInfoTask import im.vector.matrix.android.internal.crypto.tasks.GetDeviceInfoTask
import im.vector.matrix.android.internal.crypto.tasks.GetDevicesTask import im.vector.matrix.android.internal.crypto.tasks.GetDevicesTask
import im.vector.matrix.android.internal.crypto.tasks.InitializeCrossSigningTask import im.vector.matrix.android.internal.crypto.tasks.InitializeCrossSigningTask
import im.vector.matrix.android.internal.crypto.tasks.SendEventTask
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.crypto.tasks.SendVerificationMessageTask import im.vector.matrix.android.internal.crypto.tasks.SendVerificationMessageTask
import im.vector.matrix.android.internal.crypto.tasks.SetDeviceNameTask import im.vector.matrix.android.internal.crypto.tasks.SetDeviceNameTask
@ -251,4 +253,7 @@ internal abstract class CryptoModule {
@Binds @Binds
abstract fun bindInitializeCrossSigningTask(task: DefaultInitializeCrossSigningTask): InitializeCrossSigningTask abstract fun bindInitializeCrossSigningTask(task: DefaultInitializeCrossSigningTask): InitializeCrossSigningTask
@Binds
abstract fun bindSendEventTask(task: DefaultSendEventTask): SendEventTask
} }

View file

@ -1263,11 +1263,11 @@ internal class DefaultCryptoService @Inject constructor(
return "DefaultCryptoService of $userId ($deviceId)" return "DefaultCryptoService of $userId ($deviceId)"
} }
override fun getOutgoingRoomKeyRequest(): List<OutgoingRoomKeyRequest> { override fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest> {
return cryptoStore.getOutgoingRoomKeyRequests() return cryptoStore.getOutgoingRoomKeyRequests()
} }
override fun getIncomingRoomKeyRequest(): List<IncomingRoomKeyRequest> { override fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> {
return cryptoStore.getIncomingRoomKeyRequests() return cryptoStore.getIncomingRoomKeyRequests()
} }

View file

@ -897,9 +897,9 @@ internal class RealmCryptoStore @Inject constructor(
it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
}.firstOrNull { }.firstOrNull {
it.requestBody?.algorithm == requestBody.algorithm it.requestBody?.algorithm == requestBody.algorithm
it.requestBody?.roomId == requestBody.roomId && it.requestBody?.roomId == requestBody.roomId
it.requestBody?.senderKey == requestBody.senderKey && it.requestBody?.senderKey == requestBody.senderKey
it.requestBody?.sessionId == requestBody.sessionId && it.requestBody?.sessionId == requestBody.sessionId
} }
} }

View file

@ -0,0 +1,79 @@
/*
* 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.crypto.tasks
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.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.SendResponse
import im.vector.matrix.android.internal.task.Task
import org.greenrobot.eventbus.EventBus
import javax.inject.Inject
internal interface SendEventTask : Task<SendEventTask.Params, String> {
data class Params(
val event: Event,
val cryptoService: CryptoService?
)
}
internal class DefaultSendEventTask @Inject constructor(
private val localEchoUpdater: LocalEchoUpdater,
private val encryptEventTask: DefaultEncryptEventTask,
private val roomAPI: RoomAPI,
private val eventBus: EventBus) : SendEventTask {
override suspend fun execute(params: SendEventTask.Params): String {
val event = handleEncryption(params)
val localId = event.eventId!!
try {
localEchoUpdater.updateSendState(localId, SendState.SENDING)
val executeRequest = executeRequest<SendResponse>(eventBus) {
apiCall = roomAPI.send(
localId,
roomId = event.roomId ?: "",
content = event.content,
eventType = event.type
)
}
localEchoUpdater.updateSendState(localId, SendState.SENT)
return executeRequest.eventId
} catch (e: Throwable) {
localEchoUpdater.updateSendState(localId, SendState.UNDELIVERED)
throw e
}
}
private suspend fun handleEncryption(params: SendEventTask.Params): Event {
if (params.cryptoService?.isRoomEncrypted(params.event.roomId ?: "") == true) {
try {
return encryptEventTask.execute(EncryptEventTask.Params(
params.event.roomId ?: "",
params.event,
listOf("m.relates_to"),
params.cryptoService
))
} catch (throwable: Throwable) {
// We said it's ok to send verification request in clear
}
}
return params.event
}
}

View file

@ -28,7 +28,7 @@ import kotlin.math.ceil
*/ */
object HkdfSha256 { object HkdfSha256 {
public fun deriveSecret(inputKeyMaterial: ByteArray, salt: ByteArray?, info: ByteArray, outputLength: Int): ByteArray { fun deriveSecret(inputKeyMaterial: ByteArray, salt: ByteArray?, info: ByteArray, outputLength: Int): ByteArray {
return expand(extract(salt, inputKeyMaterial), info, outputLength) return expand(extract(salt, inputKeyMaterial), info, outputLength)
} }

View file

@ -273,7 +273,7 @@ internal abstract class SASDefaultVerificationTransaction(
if (keyIDNoPrefix == otherCrossSigningMasterKeyPublic) { if (keyIDNoPrefix == otherCrossSigningMasterKeyPublic) {
// Check the signature // Check the signature
val mac = macUsingAgreedMethod(otherCrossSigningMasterKeyPublic, baseInfo + it) val mac = macUsingAgreedMethod(otherCrossSigningMasterKeyPublic, baseInfo + it)
if (mac != theirMacSafe.mac.get(it)) { if (mac != theirMacSafe.mac[it]) {
// WRONG! // WRONG!
Timber.e("## SAS Verification: mac mismatch for MasterKey with id $keyIDNoPrefix") Timber.e("## SAS Verification: mac mismatch for MasterKey with id $keyIDNoPrefix")
cancel(CancelCode.MismatchedKeys) cancel(CancelCode.MismatchedKeys)

View file

@ -85,7 +85,7 @@ internal class SendVerificationMessageWorker(context: Context,
private const val OUTPUT_KEY_FAILED = "failed" private const val OUTPUT_KEY_FAILED = "failed"
fun hasFailed(outputData: Data): Boolean { fun hasFailed(outputData: Data): Boolean {
return outputData.getBoolean(SendVerificationMessageWorker.OUTPUT_KEY_FAILED, false) return outputData.getBoolean(OUTPUT_KEY_FAILED, false)
} }
} }
} }

View file

@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.database
import android.content.Context import android.content.Context
import android.util.Base64 import android.util.Base64
import androidx.core.content.edit
import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.internal.session.securestorage.SecretStoringUtils import im.vector.matrix.android.internal.session.securestorage.SecretStoringUtils
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
@ -67,10 +68,9 @@ internal class RealmKeysUtils @Inject constructor(context: Context,
val key = generateKeyForRealm() val key = generateKeyForRealm()
val encodedKey = Base64.encodeToString(key, Base64.NO_PADDING) val encodedKey = Base64.encodeToString(key, Base64.NO_PADDING)
val toStore = secretStoringUtils.securelyStoreString(encodedKey, alias) val toStore = secretStoringUtils.securelyStoreString(encodedKey, alias)
sharedPreferences sharedPreferences.edit {
.edit() putString("${ENCRYPTED_KEY_PREFIX}_$alias", Base64.encodeToString(toStore!!, Base64.NO_PADDING))
.putString("${ENCRYPTED_KEY_PREFIX}_$alias", Base64.encodeToString(toStore!!, Base64.NO_PADDING)) }
.apply()
return key return key
} }
@ -107,10 +107,9 @@ internal class RealmKeysUtils @Inject constructor(context: Context,
if (hasKeyForDatabase(alias)) { if (hasKeyForDatabase(alias)) {
secretStoringUtils.safeDeleteKey(alias) secretStoringUtils.safeDeleteKey(alias)
sharedPreferences sharedPreferences.edit {
.edit() remove("${ENCRYPTED_KEY_PREFIX}_$alias")
.remove("${ENCRYPTED_KEY_PREFIX}_$alias") }
.apply()
} }
} }

View file

@ -17,6 +17,7 @@
package im.vector.matrix.android.internal.database package im.vector.matrix.android.internal.database
import android.content.Context import android.content.Context
import androidx.core.content.edit
import im.vector.matrix.android.internal.database.model.SessionRealmModule import im.vector.matrix.android.internal.database.model.SessionRealmModule
import im.vector.matrix.android.internal.di.SessionFilesDirectory import im.vector.matrix.android.internal.di.SessionFilesDirectory
import im.vector.matrix.android.internal.di.SessionId import im.vector.matrix.android.internal.di.SessionId
@ -54,10 +55,9 @@ internal class SessionRealmConfigurationFactory @Inject constructor(
Timber.v("************************************************************") Timber.v("************************************************************")
deleteRealmFiles() deleteRealmFiles()
} }
sharedPreferences sharedPreferences.edit {
.edit() putBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", true)
.putBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", true) }
.apply()
val realmConfiguration = RealmConfiguration.Builder() val realmConfiguration = RealmConfiguration.Builder()
.compactOnLaunch() .compactOnLaunch()
@ -73,10 +73,9 @@ internal class SessionRealmConfigurationFactory @Inject constructor(
// Try creating a realm instance and if it succeeds we can clear the flag // Try creating a realm instance and if it succeeds we can clear the flag
Realm.getInstance(realmConfiguration).use { Realm.getInstance(realmConfiguration).use {
Timber.v("Successfully create realm instance") Timber.v("Successfully create realm instance")
sharedPreferences sharedPreferences.edit {
.edit() putBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", false)
.putBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", false) }
.apply()
} }
return realmConfiguration return realmConfiguration
} }

View file

@ -25,7 +25,7 @@ internal object TimelineEventFilter {
*/ */
internal object Content { internal object Content {
internal const val EDIT = """{*"m.relates_to"*"rel_type":*"m.replace"*}""" internal const val EDIT = """{*"m.relates_to"*"rel_type":*"m.replace"*}"""
internal const val RESPONSE = """{*"m.relates_to"*"rel_type":*"m.response"*}""" internal const val RESPONSE = """{*"m.relates_to"*"rel_type":*"org.matrix.response"*}"""
} }
/** /**

View file

@ -1,5 +1,5 @@
/* /*
* Copyright 2019 New Vector Ltd * Copyright 2020 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -22,12 +22,13 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import retrofit2.Call import retrofit2.Call
import retrofit2.awaitResponse
import java.io.IOException import java.io.IOException
internal suspend inline fun <DATA> executeRequest(eventBus: EventBus?, internal suspend inline fun <DATA : Any> executeRequest(eventBus: EventBus?,
block: Request<DATA>.() -> Unit) = Request<DATA>(eventBus).apply(block).execute() block: Request<DATA>.() -> Unit) = Request<DATA>(eventBus).apply(block).execute()
internal class Request<DATA>(private val eventBus: EventBus?) { internal class Request<DATA : Any>(private val eventBus: EventBus?) {
var isRetryable = false var isRetryable = false
var initialDelay: Long = 100L var initialDelay: Long = 100L

View file

@ -1,6 +1,6 @@
/* /*
* *
* * Copyright 2019 New Vector Ltd * * Copyright 2020 New Vector Ltd
* * * *
* * Licensed under the Apache License, Version 2.0 (the "License"); * * Licensed under the Apache License, Version 2.0 (the "License");
* * you may not use this file except in compliance with the License. * * you may not use this file except in compliance with the License.
@ -26,8 +26,6 @@ import im.vector.matrix.android.internal.di.MoshiProvider
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.ResponseBody import okhttp3.ResponseBody
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
import timber.log.Timber import timber.log.Timber
import java.io.IOException import java.io.IOException
@ -35,23 +33,6 @@ import java.net.HttpURLConnection
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
internal suspend fun <T> Call<T>.awaitResponse(): Response<T> {
return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
cancel()
}
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
continuation.resume(response)
}
override fun onFailure(call: Call<T>, t: Throwable) {
continuation.resumeWithException(t)
}
})
}
}
internal suspend fun okhttp3.Call.awaitResponse(): okhttp3.Response { internal suspend fun okhttp3.Call.awaitResponse(): okhttp3.Response {
return suspendCancellableCoroutine { continuation -> return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation { continuation.invokeOnCancellation {

View file

@ -33,7 +33,7 @@ data class Fingerprint(
@Throws(CertificateException::class) @Throws(CertificateException::class)
fun matchesCert(cert: X509Certificate): Boolean { fun matchesCert(cert: X509Certificate): Boolean {
var o: Fingerprint? = when (hashType) { val o: Fingerprint? = when (hashType) {
HashType.SHA256 -> newSha256Fingerprint(cert) HashType.SHA256 -> newSha256Fingerprint(cert)
HashType.SHA1 -> newSha1Fingerprint(cert) HashType.SHA1 -> newSha1Fingerprint(cert)
} }
@ -76,6 +76,7 @@ data class Fingerprint(
} }
} }
@JsonClass(generateAdapter = false)
enum class HashType { enum class HashType {
@Json(name = "sha-1") SHA1, @Json(name = "sha-1") SHA1,
@Json(name = "sha-256")SHA256 @Json(name = "sha-256")SHA256

View file

@ -28,6 +28,7 @@ import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.account.AccountService import im.vector.matrix.android.api.session.account.AccountService
import im.vector.matrix.android.api.session.accountdata.AccountDataService import im.vector.matrix.android.api.session.accountdata.AccountDataService
import im.vector.matrix.android.api.session.cache.CacheService import im.vector.matrix.android.api.session.cache.CacheService
import im.vector.matrix.android.api.session.call.CallSignalingService
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.crypto.CryptoService
@ -108,7 +109,8 @@ internal class DefaultSession @Inject constructor(
private val coroutineDispatchers: MatrixCoroutineDispatchers, private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val defaultIdentityService: DefaultIdentityService, private val defaultIdentityService: DefaultIdentityService,
private val integrationManagerService: IntegrationManagerService, private val integrationManagerService: IntegrationManagerService,
private val taskExecutor: TaskExecutor) private val taskExecutor: TaskExecutor,
private val callSignalingService: Lazy<CallSignalingService>)
: Session, : Session,
RoomService by roomService.get(), RoomService by roomService.get(),
RoomDirectoryService by roomDirectoryService.get(), RoomDirectoryService by roomDirectoryService.get(),
@ -244,6 +246,8 @@ internal class DefaultSession @Inject constructor(
override fun integrationManagerService() = integrationManagerService override fun integrationManagerService() = integrationManagerService
override fun callSignalingService(): CallSignalingService = callSignalingService.get()
override fun addListener(listener: Session.Listener) { override fun addListener(listener: Session.Listener) {
sessionListeners.addListener(listener) sessionListeners.addListener(listener)
} }

View file

@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.di.SessionAssistedInjectModule
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
import im.vector.matrix.android.internal.session.account.AccountModule import im.vector.matrix.android.internal.session.account.AccountModule
import im.vector.matrix.android.internal.session.cache.CacheModule import im.vector.matrix.android.internal.session.cache.CacheModule
import im.vector.matrix.android.internal.session.call.CallModule
import im.vector.matrix.android.internal.session.content.ContentModule import im.vector.matrix.android.internal.session.content.ContentModule
import im.vector.matrix.android.internal.session.content.UploadContentWorker import im.vector.matrix.android.internal.session.content.UploadContentWorker
import im.vector.matrix.android.internal.session.filter.FilterModule import im.vector.matrix.android.internal.session.filter.FilterModule
@ -83,7 +84,8 @@ import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
AccountDataModule::class, AccountDataModule::class,
ProfileModule::class, ProfileModule::class,
SessionAssistedInjectModule::class, SessionAssistedInjectModule::class,
AccountModule::class AccountModule::class,
CallModule::class
] ]
) )
@SessionScope @SessionScope

View file

@ -60,6 +60,7 @@ import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.network.httpclient.addAccessTokenInterceptor import im.vector.matrix.android.internal.network.httpclient.addAccessTokenInterceptor
import im.vector.matrix.android.internal.network.token.AccessTokenProvider import im.vector.matrix.android.internal.network.token.AccessTokenProvider
import im.vector.matrix.android.internal.network.token.HomeserverAccessTokenProvider import im.vector.matrix.android.internal.network.token.HomeserverAccessTokenProvider
import im.vector.matrix.android.internal.session.call.CallEventObserver
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
import im.vector.matrix.android.internal.session.homeserver.DefaultHomeServerCapabilitiesService import im.vector.matrix.android.internal.session.homeserver.DefaultHomeServerCapabilitiesService
import im.vector.matrix.android.internal.session.identity.DefaultIdentityService import im.vector.matrix.android.internal.session.identity.DefaultIdentityService
@ -72,7 +73,6 @@ import im.vector.matrix.android.internal.session.securestorage.DefaultSecureStor
import im.vector.matrix.android.internal.session.typing.DefaultTypingUsersTracker import im.vector.matrix.android.internal.session.typing.DefaultTypingUsersTracker
import im.vector.matrix.android.internal.session.user.accountdata.DefaultAccountDataService import im.vector.matrix.android.internal.session.user.accountdata.DefaultAccountDataService
import im.vector.matrix.android.internal.session.widgets.DefaultWidgetURLFormatter import im.vector.matrix.android.internal.session.widgets.DefaultWidgetURLFormatter
import im.vector.matrix.android.internal.session.widgets.WidgetManager
import im.vector.matrix.android.internal.util.md5 import im.vector.matrix.android.internal.util.md5
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -263,7 +263,7 @@ internal abstract class SessionModule {
@Binds @Binds
@IntoSet @IntoSet
abstract fun bindWidgetManager(observer: WidgetManager): SessionLifecycleObserver abstract fun bindCallEventObserver(observer: CallEventObserver): SessionLifecycleObserver
@Binds @Binds
@IntoSet @IntoSet

View file

@ -0,0 +1,66 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.call
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.model.EventType
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.di.UserId
import io.realm.OrderedCollectionChangeSet
import io.realm.RealmConfiguration
import io.realm.RealmResults
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
internal class CallEventObserver @Inject constructor(
@SessionDatabase realmConfiguration: RealmConfiguration,
@UserId private val userId: String,
private val task: CallEventsObserverTask
) : RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
override val query = Monarchy.Query<EventEntity> {
EventEntity.whereTypes(it, listOf(
EventType.CALL_ANSWER,
EventType.CALL_CANDIDATES,
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
EventType.ENCRYPTED)
)
}
override fun onChange(results: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) {
Timber.v("EventRelationsAggregationUpdater called with ${changeSet.insertions.size} insertions")
val insertedDomains = changeSet.insertions
.asSequence()
.mapNotNull { results[it]?.asDomain() }
.toList()
val params = CallEventsObserverTask.Params(
insertedDomains,
userId
)
observerScope.launch {
task.execute(params)
}
}
}

View file

@ -0,0 +1,92 @@
/*
* 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.session.call
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.api.session.events.model.EventType
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.awaitTransaction
import io.realm.Realm
import timber.log.Timber
import javax.inject.Inject
internal interface CallEventsObserverTask : Task<CallEventsObserverTask.Params, Unit> {
data class Params(
val events: List<Event>,
val userId: String
)
}
internal class DefaultCallEventsObserverTask @Inject constructor(
@SessionDatabase private val monarchy: Monarchy,
private val cryptoService: CryptoService,
private val callService: DefaultCallSignalingService) : CallEventsObserverTask {
override suspend fun execute(params: CallEventsObserverTask.Params) {
val events = params.events
val userId = params.userId
monarchy.awaitTransaction { realm ->
Timber.v(">>> DefaultCallEventsObserverTask[${params.hashCode()}] called with ${events.size} events")
update(realm, events, userId)
Timber.v("<<< DefaultCallEventsObserverTask[${params.hashCode()}] finished")
}
}
private fun update(realm: Realm, events: List<Event>, userId: String) {
val now = System.currentTimeMillis()
// TODO might check if an invite is not closed (hangup/answsered) in the same event batch?
events.forEach { event ->
event.roomId ?: return@forEach Unit.also {
Timber.w("Event with no room id ${event.eventId}")
}
val age = now - (event.ageLocalTs ?: now)
if (age > 40_000) {
// To old to ring?
return@forEach
}
event.ageLocalTs
decryptIfNeeded(event)
if (EventType.isCallEvent(event.getClearType())) {
callService.onCallEvent(event)
}
}
Timber.v("$realm : $userId")
}
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("Call service: Failed to decrypt event")
// TODO -> we should keep track of this and retry, or aggregation will be broken
}
}
}
}

View file

@ -0,0 +1,47 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.call
import dagger.Binds
import dagger.Module
import dagger.Provides
import im.vector.matrix.android.api.session.call.CallSignalingService
import im.vector.matrix.android.internal.session.SessionScope
import retrofit2.Retrofit
@Module
internal abstract class CallModule {
@Module
companion object {
@Provides
@JvmStatic
@SessionScope
fun providesVoipApi(retrofit: Retrofit): VoipApi {
return retrofit.create(VoipApi::class.java)
}
}
@Binds
abstract fun bindCallSignalingService(service: DefaultCallSignalingService): CallSignalingService
@Binds
abstract fun bindGetTurnServerTask(task: DefaultGetTurnServerTask): GetTurnServerTask
@Binds
abstract fun bindCallEventsObserverTask(task: DefaultCallEventsObserverTask): CallEventsObserverTask
}

View file

@ -0,0 +1,236 @@
/*
* 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.session.call
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.extensions.tryThis
import im.vector.matrix.android.api.session.call.CallSignalingService
import im.vector.matrix.android.api.session.call.CallState
import im.vector.matrix.android.api.session.call.CallsListener
import im.vector.matrix.android.api.session.call.MxCall
import im.vector.matrix.android.api.session.call.TurnServerResponse
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.call.CallAnswerContent
import im.vector.matrix.android.api.session.room.model.call.CallCandidatesContent
import im.vector.matrix.android.api.session.room.model.call.CallHangupContent
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.NoOpCancellable
import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.call.model.MxCallImpl
import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory
import im.vector.matrix.android.internal.session.room.send.RoomEventSender
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
import timber.log.Timber
import java.util.UUID
import javax.inject.Inject
@SessionScope
internal class DefaultCallSignalingService @Inject constructor(
@UserId
private val userId: String,
private val localEchoEventFactory: LocalEchoEventFactory,
private val roomEventSender: RoomEventSender,
private val taskExecutor: TaskExecutor,
private val turnServerTask: GetTurnServerTask
) : CallSignalingService {
private val callListeners = mutableSetOf<CallsListener>()
private val activeCalls = mutableListOf<MxCall>()
private var cachedTurnServerResponse: TurnServerResponse? = null
override fun getTurnServer(callback: MatrixCallback<TurnServerResponse>): Cancelable {
if (cachedTurnServerResponse != null) {
cachedTurnServerResponse?.let { callback.onSuccess(it) }
return NoOpCancellable
}
return turnServerTask
.configureWith(GetTurnServerTask.Params) {
this.callback = object : MatrixCallback<TurnServerResponse> {
override fun onSuccess(data: TurnServerResponse) {
cachedTurnServerResponse = data
callback.onSuccess(data)
}
override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
}
}
.executeBy(taskExecutor)
}
override fun createOutgoingCall(roomId: String, otherUserId: String, isVideoCall: Boolean): MxCall {
return MxCallImpl(
callId = UUID.randomUUID().toString(),
isOutgoing = true,
roomId = roomId,
userId = userId,
otherUserId = otherUserId,
isVideoCall = isVideoCall,
localEchoEventFactory = localEchoEventFactory,
roomEventSender = roomEventSender
).also {
activeCalls.add(it)
}
}
override fun addCallListener(listener: CallsListener) {
callListeners.add(listener)
}
override fun removeCallListener(listener: CallsListener) {
callListeners.remove(listener)
}
override fun getCallWithId(callId: String): MxCall? {
Timber.v("## VOIP getCallWithId $callId all calls ${activeCalls.map { it.callId }}")
return activeCalls.find { it.callId == callId }
}
internal fun onCallEvent(event: Event) {
when (event.getClearType()) {
EventType.CALL_ANSWER -> {
event.getClearContent().toModel<CallAnswerContent>()?.let {
if (event.senderId == userId) {
// ok it's an answer from me.. is it remote echo or other session
val knownCall = getCallWithId(it.callId)
if (knownCall == null) {
Timber.d("## VOIP onCallEvent ${event.getClearType()} id ${it.callId} send by me")
} else if (!knownCall.isOutgoing) {
// incoming call
// if it was anwsered by this session, the call state would be in Answering(or connected) state
if (knownCall.state == CallState.LocalRinging) {
// discard current call, it's answered by another of my session
onCallManageByOtherSession(it.callId)
}
}
return
}
onCallAnswer(it)
}
}
EventType.CALL_INVITE -> {
if (event.senderId == userId) {
// Always ignore local echos of invite
return
}
event.getClearContent().toModel<CallInviteContent>()?.let { content ->
val incomingCall = MxCallImpl(
callId = content.callId ?: return@let,
isOutgoing = false,
roomId = event.roomId ?: return@let,
userId = userId,
otherUserId = event.senderId ?: return@let,
isVideoCall = content.isVideo(),
localEchoEventFactory = localEchoEventFactory,
roomEventSender = roomEventSender
)
activeCalls.add(incomingCall)
onCallInvite(incomingCall, content)
}
}
EventType.CALL_HANGUP -> {
event.getClearContent().toModel<CallHangupContent>()?.let { content ->
if (event.senderId == userId) {
// ok it's an answer from me.. is it remote echo or other session
val knownCall = getCallWithId(content.callId)
if (knownCall == null) {
Timber.d("## VOIP onCallEvent ${event.getClearType()} id ${content.callId} send by me")
} else if (!knownCall.isOutgoing) {
// incoming call
if (knownCall.state == CallState.LocalRinging) {
// discard current call, it's answered by another of my session
onCallManageByOtherSession(content.callId)
}
}
return
}
onCallHangup(content)
activeCalls.removeAll { it.callId == content.callId }
}
}
EventType.CALL_CANDIDATES -> {
if (event.senderId == userId) {
// Always ignore local echos of invite
return
}
event.getClearContent().toModel<CallCandidatesContent>()?.let { content ->
activeCalls.firstOrNull { it.callId == content.callId }?.let {
onCallIceCandidate(it, content)
}
}
}
}
}
private fun onCallHangup(hangup: CallHangupContent) {
callListeners.toList().forEach {
tryThis {
it.onCallHangupReceived(hangup)
}
}
}
private fun onCallAnswer(answer: CallAnswerContent) {
callListeners.toList().forEach {
tryThis {
it.onCallAnswerReceived(answer)
}
}
}
private fun onCallManageByOtherSession(callId: String) {
callListeners.toList().forEach {
tryThis {
it.onCallManagedByOtherSession(callId)
}
}
}
private fun onCallInvite(incomingCall: MxCall, invite: CallInviteContent) {
// Ignore the invitation from current user
if (incomingCall.otherUserId == userId) return
callListeners.toList().forEach {
tryThis {
it.onCallInviteReceived(incomingCall, invite)
}
}
}
private fun onCallIceCandidate(incomingCall: MxCall, candidates: CallCandidatesContent) {
callListeners.toList().forEach {
tryThis {
it.onCallIceCandidateReceived(incomingCall, candidates)
}
}
}
companion object {
const val CALL_TIMEOUT_MS = 120_000
}
}

View file

@ -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.session.call
import im.vector.matrix.android.api.session.call.TurnServerResponse
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
import org.greenrobot.eventbus.EventBus
import javax.inject.Inject
internal abstract class GetTurnServerTask : Task<GetTurnServerTask.Params, TurnServerResponse> {
object Params
}
internal class DefaultGetTurnServerTask @Inject constructor(private val voipAPI: VoipApi,
private val eventBus: EventBus) : GetTurnServerTask() {
override suspend fun execute(params: Params): TurnServerResponse {
return executeRequest(eventBus) {
apiCall = voipAPI.getTurnServer()
}
}
}

View file

@ -0,0 +1,28 @@
/*
* 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.session.call
import im.vector.matrix.android.api.session.call.TurnServerResponse
import im.vector.matrix.android.internal.network.NetworkConstants
import retrofit2.Call
import retrofit2.http.GET
internal interface VoipApi {
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "voip/turnServer")
fun getTurnServer(): Call<TurnServerResponse>
}

View file

@ -0,0 +1,150 @@
/*
* 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.session.call.model
import im.vector.matrix.android.api.session.call.CallState
import im.vector.matrix.android.api.session.call.MxCall
import im.vector.matrix.android.api.session.events.model.Content
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.UnsignedData
import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.api.session.room.model.call.CallAnswerContent
import im.vector.matrix.android.api.session.room.model.call.CallCandidatesContent
import im.vector.matrix.android.api.session.room.model.call.CallHangupContent
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
import im.vector.matrix.android.internal.session.call.DefaultCallSignalingService
import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory
import im.vector.matrix.android.internal.session.room.send.RoomEventSender
import org.webrtc.IceCandidate
import org.webrtc.SessionDescription
import timber.log.Timber
internal class MxCallImpl(
override val callId: String,
override val isOutgoing: Boolean,
override val roomId: String,
private val userId: String,
override val otherUserId: String,
override val isVideoCall: Boolean,
private val localEchoEventFactory: LocalEchoEventFactory,
private val roomEventSender: RoomEventSender
) : MxCall {
override var state: CallState = CallState.Idle
set(value) {
field = value
dispatchStateChange()
}
private val listeners = mutableListOf<MxCall.StateListener>()
override fun addListener(listener: MxCall.StateListener) {
listeners.add(listener)
}
override fun removeListener(listener: MxCall.StateListener) {
listeners.remove(listener)
}
private fun dispatchStateChange() {
listeners.forEach {
try {
it.onStateUpdate(this)
} catch (failure: Throwable) {
Timber.d("dispatchStateChange failed for call $callId : ${failure.localizedMessage}")
}
}
}
init {
if (isOutgoing) {
state = CallState.Idle
} else {
// because it's created on reception of an offer
state = CallState.LocalRinging
}
}
override fun offerSdp(sdp: SessionDescription) {
if (!isOutgoing) return
Timber.v("## VOIP offerSdp $callId")
state = CallState.Dialing
CallInviteContent(
callId = callId,
lifetime = DefaultCallSignalingService.CALL_TIMEOUT_MS,
offer = CallInviteContent.Offer(sdp = sdp.description)
)
.let { createEventAndLocalEcho(type = EventType.CALL_INVITE, roomId = roomId, content = it.toContent()) }
.also { roomEventSender.sendEvent(it) }
}
override fun sendLocalIceCandidates(candidates: List<IceCandidate>) {
CallCandidatesContent(
callId = callId,
candidates = candidates.map {
CallCandidatesContent.Candidate(
sdpMid = it.sdpMid,
sdpMLineIndex = it.sdpMLineIndex,
candidate = it.sdp
)
}
)
.let { createEventAndLocalEcho(type = EventType.CALL_CANDIDATES, roomId = roomId, content = it.toContent()) }
.also { roomEventSender.sendEvent(it) }
}
override fun sendLocalIceCandidateRemovals(candidates: List<IceCandidate>) {
// For now we don't support this flow
}
override fun hangUp() {
Timber.v("## VOIP hangup $callId")
CallHangupContent(
callId = callId
)
.let { createEventAndLocalEcho(type = EventType.CALL_HANGUP, roomId = roomId, content = it.toContent()) }
.also { roomEventSender.sendEvent(it) }
state = CallState.Terminated
}
override fun accept(sdp: SessionDescription) {
Timber.v("## VOIP accept $callId")
if (isOutgoing) return
state = CallState.Answering
CallAnswerContent(
callId = callId,
answer = CallAnswerContent.Answer(sdp = sdp.description)
)
.let { createEventAndLocalEcho(type = EventType.CALL_ANSWER, roomId = roomId, content = it.toContent()) }
.also { roomEventSender.sendEvent(it) }
}
private fun createEventAndLocalEcho(localId: String = LocalEcho.createLocalEchoId(), type: String, roomId: String, content: Content): Event {
return Event(
roomId = roomId,
originServerTs = System.currentTimeMillis(),
senderId = userId,
eventId = localId,
type = type,
content = content,
unsignedData = UnsignedData(age = null, transactionId = localId)
)
.also { localEchoEventFactory.createLocalEcho(it) }
}
}

View file

@ -35,7 +35,8 @@ import javax.inject.Inject
internal class DefaultProfileService @Inject constructor(private val taskExecutor: TaskExecutor, internal class DefaultProfileService @Inject constructor(private val taskExecutor: TaskExecutor,
@SessionDatabase private val monarchy: Monarchy, @SessionDatabase private val monarchy: Monarchy,
private val refreshUserThreePidsTask: RefreshUserThreePidsTask, private val refreshUserThreePidsTask: RefreshUserThreePidsTask,
private val getProfileInfoTask: GetProfileInfoTask) : ProfileService { private val getProfileInfoTask: GetProfileInfoTask,
private val setDisplayNameTask: SetDisplayNameTask) : ProfileService {
override fun getDisplayName(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable { override fun getDisplayName(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable {
val params = GetProfileInfoTask.Params(userId) val params = GetProfileInfoTask.Params(userId)
@ -55,6 +56,14 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
override fun setDisplayName(userId: String, newDisplayName: String, matrixCallback: MatrixCallback<Unit>): Cancelable {
return setDisplayNameTask
.configureWith(SetDisplayNameTask.Params(userId = userId, newDisplayName = newDisplayName)) {
callback = matrixCallback
}
.executeBy(taskExecutor)
}
override fun getAvatarUrl(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable { override fun getAvatarUrl(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable {
val params = GetProfileInfoTask.Params(userId) val params = GetProfileInfoTask.Params(userId)
return getProfileInfoTask return getProfileInfoTask

View file

@ -23,6 +23,7 @@ import retrofit2.Call
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.POST import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path import retrofit2.http.Path
internal interface ProfileAPI { internal interface ProfileAPI {
@ -42,6 +43,12 @@ internal interface ProfileAPI {
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid") @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid")
fun getThreePIDs(): Call<AccountThreePidsResponse> fun getThreePIDs(): Call<AccountThreePidsResponse>
/**
* Change user display name
*/
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "profile/{userId}/displayname")
fun setDisplayName(@Path("userId") userId: String, @Body body: SetDisplayNameBody): Call<Unit>
/** /**
* Bind a threePid * Bind a threePid
* Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-account-3pid-bind * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-account-3pid-bind

View file

@ -51,4 +51,7 @@ internal abstract class ProfileModule {
@Binds @Binds
abstract fun bindUnbindThreePidsTask(task: DefaultUnbindThreePidsTask): UnbindThreePidsTask abstract fun bindUnbindThreePidsTask(task: DefaultUnbindThreePidsTask): UnbindThreePidsTask
@Binds
abstract fun bindSetDisplayNameTask(task: DefaultSetDisplayNameTask): SetDisplayNameTask
} }

View file

@ -0,0 +1,28 @@
/*
* 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.session.profile
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class SetDisplayNameBody(
/**
* The new display name for this user.
*/
@Json(name = "displayname")
val displayName: String
)

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.profile
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
import org.greenrobot.eventbus.EventBus
import javax.inject.Inject
internal abstract class SetDisplayNameTask : Task<SetDisplayNameTask.Params, Unit> {
data class Params(
val userId: String,
val newDisplayName: String
)
}
internal class DefaultSetDisplayNameTask @Inject constructor(
private val profileAPI: ProfileAPI,
private val eventBus: EventBus) : SetDisplayNameTask() {
override suspend fun execute(params: Params) {
return executeRequest(eventBus) {
val body = SetDisplayNameBody(
displayName = params.newDisplayName
)
apiCall = profileAPI.setDisplayName(params.userId, body)
}
}
}

View file

@ -21,6 +21,7 @@ import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.crypto.CryptoService 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.EventType
import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.call.RoomCallService
import im.vector.matrix.android.api.session.room.members.MembershipService import im.vector.matrix.android.api.session.room.members.MembershipService
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.relation.RelationService import im.vector.matrix.android.api.session.room.model.relation.RelationService
@ -51,6 +52,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
private val stateService: StateService, private val stateService: StateService,
private val uploadsService: UploadsService, private val uploadsService: UploadsService,
private val reportingService: ReportingService, private val reportingService: ReportingService,
private val roomCallService: RoomCallService,
private val readService: ReadService, private val readService: ReadService,
private val typingService: TypingService, private val typingService: TypingService,
private val tagsService: TagsService, private val tagsService: TagsService,
@ -67,6 +69,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
StateService by stateService, StateService by stateService,
UploadsService by uploadsService, UploadsService by uploadsService,
ReportingService by reportingService, ReportingService by reportingService,
RoomCallService by roomCallService,
ReadService by readService, ReadService by readService,
TypingService by typingService, TypingService by typingService,
TagsService by tagsService, TagsService by tagsService,

View file

@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.session.room
import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.room.call.DefaultRoomCallService
import im.vector.matrix.android.internal.session.room.draft.DefaultDraftService import im.vector.matrix.android.internal.session.room.draft.DefaultDraftService
import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService
import im.vector.matrix.android.internal.session.room.notification.DefaultRoomPushRuleService import im.vector.matrix.android.internal.session.room.notification.DefaultRoomPushRuleService
@ -49,6 +50,7 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService:
private val stateServiceFactory: DefaultStateService.Factory, private val stateServiceFactory: DefaultStateService.Factory,
private val uploadsServiceFactory: DefaultUploadsService.Factory, private val uploadsServiceFactory: DefaultUploadsService.Factory,
private val reportingServiceFactory: DefaultReportingService.Factory, private val reportingServiceFactory: DefaultReportingService.Factory,
private val roomCallServiceFactory: DefaultRoomCallService.Factory,
private val readServiceFactory: DefaultReadService.Factory, private val readServiceFactory: DefaultReadService.Factory,
private val typingServiceFactory: DefaultTypingService.Factory, private val typingServiceFactory: DefaultTypingService.Factory,
private val tagsServiceFactory: DefaultTagsService.Factory, private val tagsServiceFactory: DefaultTagsService.Factory,
@ -69,6 +71,7 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService:
stateService = stateServiceFactory.create(roomId), stateService = stateServiceFactory.create(roomId),
uploadsService = uploadsServiceFactory.create(roomId), uploadsService = uploadsServiceFactory.create(roomId),
reportingService = reportingServiceFactory.create(roomId), reportingService = reportingServiceFactory.create(roomId),
roomCallService = roomCallServiceFactory.create(roomId),
readService = readServiceFactory.create(roomId), readService = readServiceFactory.create(roomId),
typingService = typingServiceFactory.create(roomId), typingService = typingServiceFactory.create(roomId),
tagsService = tagsServiceFactory.create(roomId), tagsService = tagsServiceFactory.create(roomId),

View file

@ -0,0 +1,38 @@
/*
* 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.session.room.call
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.extensions.orFalse
import im.vector.matrix.android.api.session.room.call.RoomCallService
import im.vector.matrix.android.internal.session.room.RoomGetter
internal class DefaultRoomCallService @AssistedInject constructor(
@Assisted private val roomId: String,
private val roomGetter: RoomGetter
) : RoomCallService {
@AssistedInject.Factory
interface Factory {
fun create(roomId: String): RoomCallService
}
override fun canStartCall(): Boolean {
return roomGetter.getRoom(roomId)?.roomSummary()?.canStartCall.orFalse()
}
}

View file

@ -52,7 +52,7 @@ internal class DefaultRoomPushRuleService @AssistedInject constructor(@Assisted
override fun setRoomNotificationState(roomNotificationState: RoomNotificationState, matrixCallback: MatrixCallback<Unit>): Cancelable { override fun setRoomNotificationState(roomNotificationState: RoomNotificationState, matrixCallback: MatrixCallback<Unit>): Cancelable {
return setRoomNotificationStateTask return setRoomNotificationStateTask
.configureWith(SetRoomNotificationStateTask.Params(roomId, roomNotificationState)) { .configureWith(SetRoomNotificationStateTask.Params(roomId, roomNotificationState)) {
this.callback = callback this.callback = matrixCallback
} }
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }

View file

@ -58,7 +58,8 @@ internal class DefaultSendService @AssistedInject constructor(
private val localEchoEventFactory: LocalEchoEventFactory, private val localEchoEventFactory: LocalEchoEventFactory,
private val cryptoService: CryptoService, private val cryptoService: CryptoService,
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val localEchoRepository: LocalEchoRepository private val localEchoRepository: LocalEchoRepository,
private val roomEventSender: RoomEventSender
) : SendService { ) : SendService {
@AssistedInject.Factory @AssistedInject.Factory
@ -111,20 +112,6 @@ internal class DefaultSendService @AssistedInject constructor(
.let { sendEvent(it) } .let { sendEvent(it) }
} }
private fun sendEvent(event: Event): Cancelable {
// Encrypted room handling
return if (cryptoService.isRoomEncrypted(roomId)) {
Timber.v("Send event in encrypted room")
val encryptWork = createEncryptEventWork(event, true)
// Note that event will be replaced by the result of the previous work
val sendWork = createSendEventWork(event, false)
timelineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, sendWork)
} else {
val sendWork = createSendEventWork(event, true)
timelineSendEventWorkCommon.postWork(roomId, sendWork)
}
}
override fun sendMedias(attachments: List<ContentAttachmentData>, override fun sendMedias(attachments: List<ContentAttachmentData>,
compressBeforeSending: Boolean, compressBeforeSending: Boolean,
roomIds: Set<String>): Cancelable { roomIds: Set<String>): Cancelable {
@ -269,6 +256,10 @@ internal class DefaultSendService @AssistedInject constructor(
return cancelableBag return cancelableBag
} }
private fun sendEvent(event: Event): Cancelable {
return roomEventSender.sendEvent(event)
}
private fun createLocalEcho(event: Event) { private fun createLocalEcho(event: Event) {
localEchoEventFactory.createLocalEcho(event) localEchoEventFactory.createLocalEcho(event)
} }

View file

@ -37,7 +37,7 @@ internal class MarkdownParser @Inject constructor(
fun parse(text: String): TextContent { fun parse(text: String): TextContent {
// If no special char are detected, just return plain text // If no special char are detected, just return plain text
if (text.contains(mdSpecialChars).not()) { if (text.contains(mdSpecialChars).not()) {
return TextContent(text.toString()) return TextContent(text)
} }
val document = parser.parse(text) val document = parser.parse(text)
@ -56,7 +56,7 @@ internal class MarkdownParser @Inject constructor(
val plainText = textContentRenderer.render(document) val plainText = textContentRenderer.render(document)
TextContent(plainText, cleanHtmlText.postTreatment()) TextContent(plainText, cleanHtmlText.postTreatment())
} else { } else {
TextContent(text.toString()) TextContent(text)
} }
} }

View file

@ -0,0 +1,72 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.room.send
import androidx.work.BackoffPolicy
import androidx.work.OneTimeWorkRequest
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.util.Cancelable
import im.vector.matrix.android.internal.di.SessionId
import im.vector.matrix.android.internal.di.WorkManagerProvider
import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEventWorkCommon
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
import im.vector.matrix.android.internal.worker.startChain
import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Inject
internal class RoomEventSender @Inject constructor(
private val workManagerProvider: WorkManagerProvider,
private val timelineSendEventWorkCommon: TimelineSendEventWorkCommon,
@SessionId private val sessionId: String,
private val cryptoService: CryptoService
) {
fun sendEvent(event: Event): Cancelable {
// Encrypted room handling
return if (cryptoService.isRoomEncrypted(event.roomId ?: "")) {
Timber.v("Send event in encrypted room")
val encryptWork = createEncryptEventWork(event, true)
// Note that event will be replaced by the result of the previous work
val sendWork = createSendEventWork(event, false)
timelineSendEventWorkCommon.postSequentialWorks(event.roomId ?: "", encryptWork, sendWork)
} else {
val sendWork = createSendEventWork(event, true)
timelineSendEventWorkCommon.postWork(event.roomId ?: "", sendWork)
}
}
private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
// Same parameter
val params = EncryptEventWorker.Params(sessionId, event)
val sendWorkData = WorkerParamsFactory.toData(params)
return workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>()
.setConstraints(WorkManagerProvider.workConstraints)
.setInputData(sendWorkData)
.startChain(startChain)
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS)
.build()
}
private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
val sendContentWorkerParams = SendEventWorker.Params(sessionId, event)
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
return timelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)
}
}

View file

@ -78,7 +78,7 @@ internal class DefaultWidgetPostAPIMediator @Inject constructor(private val mosh
private fun onWidgetMessage(eventData: JsonDict) { private fun onWidgetMessage(eventData: JsonDict) {
try { try {
if (handler?.handleWidgetRequest(eventData) == false) { if (handler?.handleWidgetRequest(this, eventData) == false) {
sendError("", eventData) sendError("", eventData)
} }
} catch (e: Exception) { } catch (e: Exception) {

View file

@ -26,10 +26,11 @@ import im.vector.matrix.android.api.session.widgets.WidgetURLFormatter
import im.vector.matrix.android.api.session.widgets.model.Widget import im.vector.matrix.android.api.session.widgets.model.Widget
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Provider
internal class DefaultWidgetService @Inject constructor(private val widgetManager: WidgetManager, internal class DefaultWidgetService @Inject constructor(private val widgetManager: WidgetManager,
private val widgetURLFormatter: WidgetURLFormatter, private val widgetURLFormatter: WidgetURLFormatter,
private val widgetPostAPIMediator: WidgetPostAPIMediator) private val widgetPostAPIMediator: Provider<WidgetPostAPIMediator>)
: WidgetService { : WidgetService {
override fun getWidgetURLFormatter(): WidgetURLFormatter { override fun getWidgetURLFormatter(): WidgetURLFormatter {
@ -37,7 +38,7 @@ internal class DefaultWidgetService @Inject constructor(private val widgetManage
} }
override fun getWidgetPostAPIMediator(): WidgetPostAPIMediator { override fun getWidgetPostAPIMediator(): WidgetPostAPIMediator {
return widgetPostAPIMediator return widgetPostAPIMediator.get()
} }
override fun getRoomWidgets( override fun getRoomWidgets(

View file

@ -67,8 +67,7 @@ internal class DefaultGetScalarTokenTask @Inject constructor(private val widgets
throw IllegalStateException("Scalar token is null") throw IllegalStateException("Scalar token is null")
} }
scalarTokenStore.setToken(serverUrl, registerWidgetResponse.scalarToken) scalarTokenStore.setToken(serverUrl, registerWidgetResponse.scalarToken)
widgetsAPI.validateToken(registerWidgetResponse.scalarToken, WIDGET_API_VERSION) return validateToken(widgetsAPI, serverUrl, registerWidgetResponse.scalarToken)
return registerWidgetResponse.scalarToken
} }
private suspend fun validateToken(widgetsAPI: WidgetsAPI, serverUrl: String, scalarToken: String): String { private suspend fun validateToken(widgetsAPI: WidgetsAPI, serverUrl: String, scalarToken: String): String {

View file

@ -27,6 +27,7 @@ import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties import android.security.keystore.KeyProperties
import android.util.Base64 import android.util.Base64
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.content.edit
import timber.log.Timber import timber.log.Timber
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
@ -152,9 +153,9 @@ object CompatUtil {
.build()) .build())
key = generator.generateKey() key = generator.generateKey()
sharedPreferences.edit() sharedPreferences.edit {
.putInt(SHARED_KEY_ANDROID_VERSION_WHEN_KEY_HAS_BEEN_GENERATED, Build.VERSION.SDK_INT) putInt(SHARED_KEY_ANDROID_VERSION_WHEN_KEY_HAS_BEEN_GENERATED, Build.VERSION.SDK_INT)
.apply() }
} }
} }
} else { } else {
@ -188,10 +189,10 @@ object CompatUtil {
cipher.init(Cipher.WRAP_MODE, keyPair.public) cipher.init(Cipher.WRAP_MODE, keyPair.public)
val wrappedAesKey = cipher.wrap(key) val wrappedAesKey = cipher.wrap(key)
sharedPreferences.edit() sharedPreferences.edit {
.putString(AES_WRAPPED_PROTECTION_KEY_SHARED_PREFERENCE, Base64.encodeToString(wrappedAesKey, 0)) putString(AES_WRAPPED_PROTECTION_KEY_SHARED_PREFERENCE, Base64.encodeToString(wrappedAesKey, 0))
.putInt(SHARED_KEY_ANDROID_VERSION_WHEN_KEY_HAS_BEEN_GENERATED, Build.VERSION.SDK_INT) putInt(SHARED_KEY_ANDROID_VERSION_WHEN_KEY_HAS_BEEN_GENERATED, Build.VERSION.SDK_INT)
.apply() }
} }
} }

View file

@ -33,8 +33,8 @@
<string name="notice_display_name_set_by_you">You set your display name to %1$s</string> <string name="notice_display_name_set_by_you">You set your display name to %1$s</string>
<string name="notice_display_name_changed_from">%1$s changed their display name from %2$s to %3$s</string> <string name="notice_display_name_changed_from">%1$s changed their display name from %2$s to %3$s</string>
<string name="notice_display_name_changed_from_by_you">You changed your display name from %1$s to %2$s</string> <string name="notice_display_name_changed_from_by_you">You changed your display name from %1$s to %2$s</string>
<string name="notice_display_name_removed">%1$s removed their display name (%2$s)</string> <string name="notice_display_name_removed">%1$s removed their display name (it was %2$s)</string>
<string name="notice_display_name_removed_by_you">You removed your display name (%1$s)</string> <string name="notice_display_name_removed_by_you">You removed your display name (it was %1$s)</string>
<string name="notice_room_topic_changed">%1$s changed the topic to: %2$s</string> <string name="notice_room_topic_changed">%1$s changed the topic to: %2$s</string>
<string name="notice_room_topic_changed_by_you">You changed the topic to: %1$s</string> <string name="notice_room_topic_changed_by_you">You changed the topic to: %1$s</string>
<string name="notice_room_name_changed">%1$s changed the room name to: %2$s</string> <string name="notice_room_name_changed">%1$s changed the room name to: %2$s</string>
@ -43,6 +43,8 @@
<string name="notice_placed_video_call_by_you">You placed a video call.</string> <string name="notice_placed_video_call_by_you">You placed a video call.</string>
<string name="notice_placed_voice_call">%s placed a voice call.</string> <string name="notice_placed_voice_call">%s placed a voice call.</string>
<string name="notice_placed_voice_call_by_you">You placed a voice call.</string> <string name="notice_placed_voice_call_by_you">You placed a voice call.</string>
<string name="notice_call_candidates">%s sent data to setup the call.</string>
<string name="notice_call_candidates_by_you">You sent data to setup the call.</string>
<string name="notice_answered_call">%s answered the call.</string> <string name="notice_answered_call">%s answered the call.</string>
<string name="notice_answered_call_by_you">You answered the call.</string> <string name="notice_answered_call_by_you">You answered the call.</string>
<string name="notice_ended_call">%s ended the call.</string> <string name="notice_ended_call">%s ended the call.</string>
@ -362,4 +364,8 @@
<string name="key_verification_request_fallback_message">%s is requesting to verify your key, but your client does not support in-chat key verification. You will need to use legacy key verification to verify keys.</string> <string name="key_verification_request_fallback_message">%s is requesting to verify your key, but your client does not support in-chat key verification. You will need to use legacy key verification to verify keys.</string>
<string name="call_notification_answer">Accept</string>
<string name="call_notification_reject">Decline</string>
<string name="call_notification_hangup">Hang Up</string>
</resources> </resources>

View file

@ -62,9 +62,9 @@ class ContactPicker(override val requestCode: Int) : Picker<MultiPickerContactTy
val contactId = cursor.getInt(idColumn) val contactId = cursor.getInt(idColumn)
var name = cursor.getString(nameColumn) var name = cursor.getString(nameColumn)
var photoUri = cursor.getString(photoUriColumn) val photoUri = cursor.getString(photoUriColumn)
var phoneNumberList = mutableListOf<String>() val phoneNumberList = mutableListOf<String>()
var emailList = mutableListOf<String>() val emailList = mutableListOf<String>()
getRawContactId(context.contentResolver, contactId)?.let { rawContactId -> getRawContactId(context.contentResolver, contactId)?.let { rawContactId ->
val selection = ContactsContract.Data.RAW_CONTACT_ID + " = ?" val selection = ContactsContract.Data.RAW_CONTACT_ID + " = ?"

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -17,8 +17,9 @@
# #
echo "Configure RiotX Template..." echo "Configure RiotX Template..."
if [ -z ${ANDROID_STUDIO+x} ]; then ANDROID_STUDIO="/Applications/Android Studio.app/Contents"; fi
{ {
ln -s $(pwd)/RiotXFeature /Applications/Android\ Studio.app/Contents/plugins/android/lib/templates/other ln -s $(pwd)/RiotXFeature "${ANDROID_STUDIO%/}/plugins/android/lib/templates/other"
} && { } && {
echo "Please restart Android Studio." echo "Please restart Android Studio."
} }

View file

@ -15,7 +15,7 @@ androidExtensions {
} }
ext.versionMajor = 0 ext.versionMajor = 0
ext.versionMinor = 22 ext.versionMinor = 23
ext.versionPatch = 0 ext.versionPatch = 0
static def getGitTimestamp() { static def getGitTimestamp() {
@ -107,9 +107,8 @@ android {
compileSdkVersion 29 compileSdkVersion 29
defaultConfig { defaultConfig {
applicationId "im.vector.riotx" applicationId "im.vector.riotx"
// Set to API 19 because motionLayout is min API 18. // Set to API 21: see #405
// In the future we may consider using an alternative of MotionLayout to support API 16. But for security reason, maybe not. minSdkVersion 21
minSdkVersion 19
targetSdkVersion 29 targetSdkVersion 29
multiDexEnabled true multiDexEnabled true
@ -192,8 +191,14 @@ android {
resValue "bool", "debug_mode", "false" resValue "bool", "debug_mode", "false"
buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false" buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false"
minifyEnabled false postprocessing {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' removeUnusedCode true
removeUnusedResources true
// We do not activate obfuscation as it makes it hard then to read crash reports, and it's a bit useless on an open source project :)
obfuscate false
optimizeCode true
proguardFiles 'proguard-rules.pro'
}
} }
} }
@ -385,6 +390,9 @@ dependencies {
implementation 'com.github.BillCarsonFr:JsonViewer:0.5' implementation 'com.github.BillCarsonFr:JsonViewer:0.5'
// TODO meant for development purposes only
implementation 'org.webrtc:google-webrtc:1.0.+'
// QR-code // QR-code
// Stick to 3.3.3 because of https://github.com/zxing/zxing/issues/1170 // Stick to 3.3.3 because of https://github.com/zxing/zxing/issues/1170
implementation 'com.google.zxing:core:3.3.3' implementation 'com.google.zxing:core:3.3.3'

View file

@ -19,3 +19,5 @@
# If you keep the line number information, uncomment this to # If you keep the line number information, uncomment this to
# hide the original source file name. # hide the original source file name.
#-renamesourcefileattribute SourceFile #-renamesourcefileattribute SourceFile
-keep class im.vector.riotx.features.** { *; }

View file

@ -21,6 +21,7 @@ import android.app.Activity
import android.content.Context import android.content.Context
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import android.widget.Toast import android.widget.Toast
import androidx.core.content.edit
import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability import com.google.android.gms.common.GoogleApiAvailability
import com.google.firebase.iid.FirebaseInstanceId import com.google.firebase.iid.FirebaseInstanceId
@ -57,10 +58,9 @@ object FcmHelper {
*/ */
fun storeFcmToken(context: Context, fun storeFcmToken(context: Context,
token: String?) { token: String?) {
PreferenceManager.getDefaultSharedPreferences(context) PreferenceManager.getDefaultSharedPreferences(context).edit {
.edit() putString(PREFS_KEY_FCM_TOKEN, token)
.putString(PREFS_KEY_FCM_TOKEN, token) }
.apply()
} }
/** /**

View file

@ -3,13 +3,30 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="im.vector.riotx"> package="im.vector.riotx">
<!-- Needed for VOIP call to detect and switch to headset-->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_CONTACTS" /> <uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- Call feature -->
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
<uses-permission android:name="android.permission.READ_CALL_LOG" />
<!-- Needed for voice call to toggle speaker on or off -->
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<!-- READ_PHONE_STATE is needed only if your calling app reads numbers from the `PHONE_STATE`
intent action. -->
<!-- Needed to show incoming calls activity in lock screen-->
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- Needed for incoming calls -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<!-- Adding CAMERA permission prevents Chromebooks to see the application on the PlayStore --> <!-- Adding CAMERA permission prevents Chromebooks to see the application on the PlayStore -->
<!-- Tell that the Camera is not mandatory to install the application --> <!-- Tell that the Camera is not mandatory to install the application -->
<uses-feature <uses-feature
@ -172,6 +189,7 @@
<activity <activity
android:name=".features.attachments.preview.AttachmentsPreviewActivity" android:name=".features.attachments.preview.AttachmentsPreviewActivity"
android:theme="@style/AppTheme.AttachmentsPreview" /> android:theme="@style/AppTheme.AttachmentsPreview" />
<activity android:name=".features.call.VectorCallActivity" />
<activity android:name=".features.terms.ReviewTermsActivity" /> <activity android:name=".features.terms.ReviewTermsActivity" />
<activity android:name=".features.widgets.WidgetActivity" /> <activity android:name=".features.widgets.WidgetActivity" />
@ -180,20 +198,47 @@
<service <service
android:name=".core.services.CallService" android:name=".core.services.CallService"
android:exported="false" /> android:exported="false" >
<!-- in order to get headset button events -->
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</service>
<service <service
android:name=".core.services.VectorSyncService" android:name=".core.services.VectorSyncService"
android:exported="false" /> android:exported="false" />
<service
android:name=".features.call.telecom.VectorConnectionService"
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE">
<intent-filter>
<action android:name="android.telecom.ConnectionService" />
</intent-filter>
</service>
<!-- Receivers --> <!-- Receivers -->
<receiver
android:name=".features.call.service.CallHeadsUpActionReceiver"
android:exported="false" />
<!-- Exported false, should only be accessible from this app!! --> <!-- Exported false, should only be accessible from this app!! -->
<receiver <receiver
android:name=".features.notifications.NotificationBroadcastReceiver" android:name=".features.notifications.NotificationBroadcastReceiver"
android:enabled="true" android:enabled="true"
android:exported="false" /> android:exported="false" />
<!--
A media button receiver receives and helps translate hardware media playback buttons,
such as those found on wired and wireless headsets, into the appropriate callbacks in your app.
-->
<receiver android:name="androidx.media.session.MediaButtonReceiver" >
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<!-- Providers --> <!-- Providers -->
<provider <provider

View file

@ -43,6 +43,7 @@ import im.vector.riotx.core.di.HasVectorInjector
import im.vector.riotx.core.di.VectorComponent import im.vector.riotx.core.di.VectorComponent
import im.vector.riotx.core.extensions.configureAndStart import im.vector.riotx.core.extensions.configureAndStart
import im.vector.riotx.core.rx.RxConfig import im.vector.riotx.core.rx.RxConfig
import im.vector.riotx.features.call.WebRtcPeerConnectionManager
import im.vector.riotx.features.configuration.VectorConfiguration import im.vector.riotx.features.configuration.VectorConfiguration
import im.vector.riotx.features.lifecycle.VectorActivityLifecycleCallbacks import im.vector.riotx.features.lifecycle.VectorActivityLifecycleCallbacks
import im.vector.riotx.features.notifications.NotificationDrawerManager import im.vector.riotx.features.notifications.NotificationDrawerManager
@ -80,6 +81,8 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
@Inject lateinit var appStateHandler: AppStateHandler @Inject lateinit var appStateHandler: AppStateHandler
@Inject lateinit var rxConfig: RxConfig @Inject lateinit var rxConfig: RxConfig
@Inject lateinit var popupAlertManager: PopupAlertManager @Inject lateinit var popupAlertManager: PopupAlertManager
@Inject lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager
lateinit var vectorComponent: VectorComponent lateinit var vectorComponent: VectorComponent
private var fontThreadHandler: Handler? = null private var fontThreadHandler: Handler? = null
@ -122,6 +125,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
val lastAuthenticatedSession = authenticationService.getLastAuthenticatedSession()!! val lastAuthenticatedSession = authenticationService.getLastAuthenticatedSession()!!
activeSessionHolder.setActiveSession(lastAuthenticatedSession) activeSessionHolder.setActiveSession(lastAuthenticatedSession)
lastAuthenticatedSession.configureAndStart(applicationContext, pushRuleTriggerListener, sessionListener) lastAuthenticatedSession.configureAndStart(applicationContext, pushRuleTriggerListener, sessionListener)
lastAuthenticatedSession.callSignalingService().addCallListener(webRtcPeerConnectionManager)
} }
ProcessLifecycleOwner.get().lifecycle.addObserver(object : LifecycleObserver { ProcessLifecycleOwner.get().lifecycle.addObserver(object : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME) @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)

View file

@ -24,6 +24,8 @@ import dagger.Component
import im.vector.riotx.core.error.ErrorFormatter import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.preference.UserAvatarPreference import im.vector.riotx.core.preference.UserAvatarPreference
import im.vector.riotx.features.MainActivity import im.vector.riotx.features.MainActivity
import im.vector.riotx.features.call.CallControlsBottomSheet
import im.vector.riotx.features.call.VectorCallActivity
import im.vector.riotx.features.createdirect.CreateDirectRoomActivity import im.vector.riotx.features.createdirect.CreateDirectRoomActivity
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
import im.vector.riotx.features.crypto.quads.SharedSecureStorageActivity import im.vector.riotx.features.crypto.quads.SharedSecureStorageActivity
@ -130,6 +132,7 @@ interface ScreenComponent {
fun inject(activity: InviteUsersToRoomActivity) fun inject(activity: InviteUsersToRoomActivity)
fun inject(activity: ReviewTermsActivity) fun inject(activity: ReviewTermsActivity)
fun inject(activity: WidgetActivity) fun inject(activity: WidgetActivity)
fun inject(activity: VectorCallActivity)
/* ========================================================================================== /* ==========================================================================================
* BottomSheets * BottomSheets
@ -146,6 +149,7 @@ interface ScreenComponent {
fun inject(bottomSheet: BootstrapBottomSheet) fun inject(bottomSheet: BootstrapBottomSheet)
fun inject(bottomSheet: RoomWidgetPermissionBottomSheet) fun inject(bottomSheet: RoomWidgetPermissionBottomSheet)
fun inject(bottomSheet: RoomWidgetsBottomSheet) fun inject(bottomSheet: RoomWidgetsBottomSheet)
fun inject(bottomSheet: CallControlsBottomSheet)
/* ========================================================================================== /* ==========================================================================================
* Others * Others

View file

@ -31,6 +31,7 @@ import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.pushers.PushersManager import im.vector.riotx.core.pushers.PushersManager
import im.vector.riotx.core.utils.AssetReader import im.vector.riotx.core.utils.AssetReader
import im.vector.riotx.core.utils.DimensionConverter import im.vector.riotx.core.utils.DimensionConverter
import im.vector.riotx.features.call.WebRtcPeerConnectionManager
import im.vector.riotx.features.configuration.VectorConfiguration import im.vector.riotx.features.configuration.VectorConfiguration
import im.vector.riotx.features.crypto.keysrequest.KeyRequestHandler import im.vector.riotx.features.crypto.keysrequest.KeyRequestHandler
import im.vector.riotx.features.crypto.verification.IncomingVerificationRequestHandler import im.vector.riotx.features.crypto.verification.IncomingVerificationRequestHandler
@ -134,6 +135,8 @@ interface VectorComponent {
fun reAuthHelper(): ReAuthHelper fun reAuthHelper(): ReAuthHelper
fun webRtcPeerConnectionManager(): WebRtcPeerConnectionManager
@Component.Factory @Component.Factory
interface Factory { interface Factory {
fun create(@BindsInstance context: Context): VectorComponent fun create(@BindsInstance context: Context): VectorComponent

View file

@ -22,6 +22,7 @@ import dagger.Binds
import dagger.Module import dagger.Module
import dagger.multibindings.IntoMap import dagger.multibindings.IntoMap
import im.vector.riotx.core.platform.ConfigurationViewModel import im.vector.riotx.core.platform.ConfigurationViewModel
import im.vector.riotx.features.call.SharedActiveCallViewModel
import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromKeyViewModel import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromKeyViewModel
import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromPassphraseViewModel import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromPassphraseViewModel
import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreSharedViewModel import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreSharedViewModel
@ -85,6 +86,11 @@ interface ViewModelModule {
@ViewModelKey(ConfigurationViewModel::class) @ViewModelKey(ConfigurationViewModel::class)
fun bindConfigurationViewModel(viewModel: ConfigurationViewModel): ViewModel fun bindConfigurationViewModel(viewModel: ConfigurationViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(SharedActiveCallViewModel::class)
fun bindSharedActiveCallViewModel(viewModel: SharedActiveCallViewModel): ViewModel
@Binds @Binds
@IntoMap @IntoMap
@ViewModelKey(UserDirectorySharedActionViewModel::class) @ViewModelKey(UserDirectorySharedActionViewModel::class)

View file

@ -40,10 +40,10 @@ class DefaultErrorFormatter @Inject constructor(
null -> null null -> null
is IdentityServiceError -> identityServerError(throwable) is IdentityServiceError -> identityServerError(throwable)
is Failure.NetworkConnection -> { is Failure.NetworkConnection -> {
when { when (throwable.ioException) {
throwable.ioException is SocketTimeoutException -> is SocketTimeoutException ->
stringProvider.getString(R.string.error_network_timeout) stringProvider.getString(R.string.error_network_timeout)
throwable.ioException is UnknownHostException -> is UnknownHostException ->
// Invalid homeserver? // Invalid homeserver?
// TODO Check network state, airplane mode, etc. // TODO Check network state, airplane mode, etc.
stringProvider.getString(R.string.login_error_unknown_host) stringProvider.getString(R.string.login_error_unknown_host)

View file

@ -17,10 +17,15 @@
package im.vector.riotx.core.extensions package im.vector.riotx.core.extensions
import androidx.fragment.app.FragmentTransaction import androidx.fragment.app.FragmentTransaction
import im.vector.matrix.android.api.extensions.tryThis
inline fun androidx.fragment.app.FragmentManager.commitTransactionNow(func: FragmentTransaction.() -> FragmentTransaction) { inline fun androidx.fragment.app.FragmentManager.commitTransactionNow(func: FragmentTransaction.() -> FragmentTransaction) {
// Could throw and make the app crash
// e.g sharedActionViewModel.observe()
tryThis("Failed to commitTransactionNow") {
beginTransaction().func().commitNow() beginTransaction().func().commitNow()
} }
}
inline fun androidx.fragment.app.FragmentManager.commitTransaction(func: FragmentTransaction.() -> FragmentTransaction) { inline fun androidx.fragment.app.FragmentManager.commitTransaction(func: FragmentTransaction.() -> FragmentTransaction) {
beginTransaction().func().commit() beginTransaction().func().commit()

View file

@ -38,10 +38,6 @@ fun Session.configureAndStart(context: Context,
startSyncing(context) startSyncing(context)
refreshPushers() refreshPushers()
pushRuleTriggerListener.startWithSession(this) pushRuleTriggerListener.startWithSession(this)
// TODO P1 From HomeActivity
// @Inject lateinit var incomingVerificationRequestHandler: IncomingVerificationRequestHandler
// @Inject lateinit var keyRequestHandler: KeyRequestHandler
} }
fun Session.startSyncing(context: Context) { fun Session.startSyncing(context: Context) {

View file

@ -165,6 +165,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
Timber.i("onCreate Activity ${this.javaClass.simpleName}")
val vectorComponent = getVectorComponent() val vectorComponent = getVectorComponent()
screenComponent = DaggerScreenComponent.factory().create(vectorComponent, this) screenComponent = DaggerScreenComponent.factory().create(vectorComponent, this)
val timeForInjection = measureTimeMillis { val timeForInjection = measureTimeMillis {
@ -252,6 +253,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
Timber.i("onDestroy Activity ${this.javaClass.simpleName}")
unBinder?.unbind() unBinder?.unbind()
unBinder = null unBinder = null
@ -279,6 +281,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
Timber.i("onPause Activity ${this.javaClass.simpleName}")
rageShake.stop() rageShake.stop()

View file

@ -34,6 +34,7 @@ import com.airbnb.mvrx.MvRxViewId
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.jakewharton.rxbinding3.view.clicks
import im.vector.riotx.core.di.DaggerScreenComponent import im.vector.riotx.core.di.DaggerScreenComponent
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.utils.DimensionConverter import im.vector.riotx.core.utils.DimensionConverter
@ -41,6 +42,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.TimeUnit
/** /**
* Add MvRx capabilities to bottomsheetdialog (like BaseMvRxFragment) * Add MvRx capabilities to bottomsheetdialog (like BaseMvRxFragment)
@ -169,6 +171,18 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
return this return this
} }
/* ==========================================================================================
* Views
* ========================================================================================== */
protected fun View.debouncedClicks(onClicked: () -> Unit) {
clicks()
.throttleFirst(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { onClicked() }
.disposeOnDestroyView()
}
/* ========================================================================================== /* ==========================================================================================
* ViewEvents * ViewEvents
* ========================================================================================== */ * ========================================================================================== */

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