mirror of
https://github.com/element-hq/element-android
synced 2024-11-27 11:59:12 +03:00
Merge branch 'develop' into feature/improve_perf
This commit is contained in:
commit
f28e3ca504
206 changed files with 7331 additions and 556 deletions
|
@ -7,6 +7,7 @@
|
|||
<w>ciphertext</w>
|
||||
<w>coroutine</w>
|
||||
<w>decryptor</w>
|
||||
<w>displayname</w>
|
||||
<w>emoji</w>
|
||||
<w>emojis</w>
|
||||
<w>fdroid</w>
|
||||
|
|
45
CHANGES.md
45
CHANGES.md
|
@ -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 ✨:
|
||||
|
@ -23,15 +56,7 @@ Bugfix 🐛:
|
|||
- Fix status bar icon contrast on API in [21,23[
|
||||
- Wrong /query request (#1444)
|
||||
- Make Credentials.homeServer optional because it is deprecated (#1443)
|
||||
|
||||
Translations 🗣:
|
||||
-
|
||||
|
||||
SDK API changes ⚠️:
|
||||
-
|
||||
|
||||
Build 🧱:
|
||||
-
|
||||
- Fix issue on dark themes, after alert popup dismiss
|
||||
|
||||
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
|
||||
|
|
|
@ -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):
|
||||
- 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.
|
||||
|
||||
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`.
|
||||
- Follow the Wizard, especially replace `Main` by something more relevant to your feature.
|
||||
- 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.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
[<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="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/google-play-badge.png" alt="Get it on Google Play" height="60">](https://play.google.com/store/apps/details?id=im.vector.riotx)
|
||||
[<img src="resources/img/f-droid-badge.png" alt="Get it on F-Droid" height="60">](https://f-droid.org/app/im.vector.riotx)
|
||||
|
||||
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
420
docs/voip_signaling.md
Normal 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 │
|
||||
└────────────────────┘ │ └────────────────────┘
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
│
|
||||
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
|
@ -6,7 +6,7 @@ android {
|
|||
compileSdkVersion 29
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
|
|
@ -23,7 +23,7 @@ android {
|
|||
testOptions.unitTests.includeAndroidResources = true
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode 1
|
||||
versionName "0.0.1"
|
||||
|
@ -35,6 +35,10 @@ android {
|
|||
resValue "string", "git_sdk_revision", "\"${gitRevision()}\""
|
||||
resValue "string", "git_sdk_revision_unix_date", "\"${gitRevisionUnixDate()}\""
|
||||
resValue "string", "git_sdk_revision_date", "\"${gitRevisionDate()}\""
|
||||
|
||||
defaultConfig {
|
||||
consumerProguardFiles 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
@ -49,9 +53,6 @@ android {
|
|||
release {
|
||||
buildConfigField "boolean", "LOG_PRIVATE_DATA", "false"
|
||||
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 "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-common-java8:$lifecycle_version"
|
||||
|
@ -161,6 +163,10 @@ dependencies {
|
|||
// Phone number https://github.com/google/libphonenumber
|
||||
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'
|
||||
releaseImplementation 'com.airbnb.okreplay:noop:1.5.0'
|
||||
androidTestImplementation 'com.airbnb.okreplay:espresso:1.5.0'
|
||||
|
|
42
matrix-sdk-android/proguard-rules.pro
vendored
42
matrix-sdk-android/proguard-rules.pro
vendored
|
@ -19,3 +19,45 @@
|
|||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-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.** { *; }
|
|
@ -241,14 +241,14 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
|||
val eventWireContent = event.content.toContent()
|
||||
assertNotNull(eventWireContent)
|
||||
|
||||
assertNull(eventWireContent.get("body"))
|
||||
assertEquals(MXCRYPTO_ALGORITHM_MEGOLM, eventWireContent.get("algorithm"))
|
||||
assertNull(eventWireContent["body"])
|
||||
assertEquals(MXCRYPTO_ALGORITHM_MEGOLM, eventWireContent["algorithm"])
|
||||
|
||||
assertNotNull(eventWireContent.get("ciphertext"))
|
||||
assertNotNull(eventWireContent.get("session_id"))
|
||||
assertNotNull(eventWireContent.get("sender_key"))
|
||||
assertNotNull(eventWireContent["ciphertext"])
|
||||
assertNotNull(eventWireContent["session_id"])
|
||||
assertNotNull(eventWireContent["sender_key"])
|
||||
|
||||
assertEquals(senderSession.sessionParams.deviceId, eventWireContent.get("device_id"))
|
||||
assertEquals(senderSession.sessionParams.deviceId, eventWireContent["device_id"])
|
||||
|
||||
assertNotNull(event.eventId)
|
||||
assertEquals(roomId, event.roomId)
|
||||
|
@ -257,7 +257,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
|||
|
||||
val eventContent = event.toContent()
|
||||
assertNotNull(eventContent)
|
||||
assertEquals(clearMessage, eventContent.get("body"))
|
||||
assertEquals(clearMessage, eventContent["body"])
|
||||
assertEquals(senderSession.myUserId, event.senderId)
|
||||
}
|
||||
|
||||
|
|
|
@ -90,7 +90,7 @@ class KeyShareTests : InstrumentedTest {
|
|||
} catch (failure: Throwable) {
|
||||
}
|
||||
|
||||
val outgoingRequestBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequest()
|
||||
val outgoingRequestsBefore = aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
|
||||
// Try to request
|
||||
aliceSession2.cryptoService().requestRoomKeyForEvent(receivedEvent.root)
|
||||
|
||||
|
@ -100,10 +100,10 @@ class KeyShareTests : InstrumentedTest {
|
|||
var outGoingRequestId: String? = null
|
||||
|
||||
mTestHelper.retryPeriodicallyWithLatch(waitLatch) {
|
||||
aliceSession2.cryptoService().getOutgoingRoomKeyRequest()
|
||||
aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
|
||||
.filter { req ->
|
||||
// filter out request that was known before
|
||||
!outgoingRequestBefore.any { req.requestId == it.requestId }
|
||||
!outgoingRequestsBefore.any { req.requestId == it.requestId }
|
||||
}
|
||||
.let {
|
||||
val outgoing = it.firstOrNull { it.sessionId == eventMegolmSessionId }
|
||||
|
@ -115,10 +115,10 @@ class KeyShareTests : InstrumentedTest {
|
|||
|
||||
Log.v("TEST", "=======> Outgoing requet Id is $outGoingRequestId")
|
||||
|
||||
val outgoingRequestAfter = aliceSession2.cryptoService().getOutgoingRoomKeyRequest()
|
||||
val outgoingRequestAfter = aliceSession2.cryptoService().getOutgoingRoomKeyRequests()
|
||||
|
||||
// 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 })
|
||||
|
||||
// The first session should see an incoming request
|
||||
|
@ -126,7 +126,7 @@ class KeyShareTests : InstrumentedTest {
|
|||
mTestHelper.waitWithLatch { latch ->
|
||||
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
// DEBUG LOGS
|
||||
aliceSession.cryptoService().getIncomingRoomKeyRequest().let {
|
||||
aliceSession.cryptoService().getIncomingRoomKeyRequests().let {
|
||||
Log.v("TEST", "Incoming request Session 1 (looking for $outGoingRequestId)")
|
||||
Log.v("TEST", "=========================")
|
||||
it.forEach { keyRequest ->
|
||||
|
@ -135,7 +135,7 @@ class KeyShareTests : InstrumentedTest {
|
|||
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
|
||||
}
|
||||
}
|
||||
|
@ -155,7 +155,7 @@ class KeyShareTests : InstrumentedTest {
|
|||
|
||||
mTestHelper.waitWithLatch { latch ->
|
||||
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
aliceSession.cryptoService().getIncomingRoomKeyRequest().let {
|
||||
aliceSession.cryptoService().getIncomingRoomKeyRequests().let {
|
||||
Log.v("TEST", "Incoming request Session 1")
|
||||
Log.v("TEST", "=========================")
|
||||
it.forEach {
|
||||
|
@ -171,7 +171,7 @@ class KeyShareTests : InstrumentedTest {
|
|||
Thread.sleep(6_000)
|
||||
mTestHelper.waitWithLatch { latch ->
|
||||
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||
aliceSession2.cryptoService().getOutgoingRoomKeyRequest().let {
|
||||
aliceSession2.cryptoService().getOutgoingRoomKeyRequests().let {
|
||||
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
|
||||
?: "", txId)
|
||||
|
||||
|
|
|
@ -144,7 +144,7 @@ class QuadSTests : InstrumentedTest {
|
|||
|
||||
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("Secret should be encrypted with default key", encryptedContent?.get(keyId))
|
||||
|
||||
|
|
|
@ -69,7 +69,7 @@ data class HomeServerConnectionConfig(
|
|||
*/
|
||||
fun withHomeServerUri(hsUri: Uri): Builder {
|
||||
if (hsUri.scheme != "http" && hsUri.scheme != "https") {
|
||||
throw RuntimeException("Invalid home server URI: " + hsUri)
|
||||
throw RuntimeException("Invalid home server URI: $hsUri")
|
||||
}
|
||||
// ensure trailing /
|
||||
val hsString = hsUri.toString().ensureTrailingSlash()
|
||||
|
|
|
@ -16,10 +16,15 @@
|
|||
|
||||
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 {
|
||||
operation()
|
||||
} catch (any: Throwable) {
|
||||
if (message != null) {
|
||||
Timber.e(any, message)
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -87,14 +87,13 @@ class EventMatchCondition(
|
|||
// Very simple glob to regexp converter
|
||||
private fun simpleGlobToRegExp(glob: String): String {
|
||||
var out = "" // "^"
|
||||
for (i in 0 until glob.length) {
|
||||
val c = glob[i]
|
||||
when (c) {
|
||||
for (element in glob) {
|
||||
when (element) {
|
||||
'*' -> out += ".*"
|
||||
'?' -> out += '.'.toString()
|
||||
'.' -> out += "\\."
|
||||
'\\' -> out += "\\\\"
|
||||
else -> out += c
|
||||
else -> out += element
|
||||
}
|
||||
}
|
||||
out += "" // '$'.toString()
|
||||
|
|
|
@ -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.accountdata.AccountDataService
|
||||
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.ContentUrlResolver
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
|
@ -171,6 +172,11 @@ interface Session :
|
|||
*/
|
||||
fun integrationManagerService(): IntegrationManagerService
|
||||
|
||||
/**
|
||||
* Returns the call signaling service associated with the session
|
||||
*/
|
||||
fun callSignalingService(): CallSignalingService
|
||||
|
||||
/**
|
||||
* Add a listener to the session.
|
||||
* @param listener the listener to add.
|
||||
|
|
|
@ -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?
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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?
|
||||
)
|
|
@ -138,7 +138,9 @@ interface CryptoService {
|
|||
|
||||
fun removeSessionListener(listener: NewSessionListener)
|
||||
|
||||
fun getOutgoingRoomKeyRequest(): List<OutgoingRoomKeyRequest>
|
||||
fun getIncomingRoomKeyRequest(): List<IncomingRoomKeyRequest>
|
||||
fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest>
|
||||
|
||||
fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest>
|
||||
|
||||
fun getGossipingEventsTrail(): List<Event>
|
||||
}
|
||||
|
|
|
@ -58,7 +58,6 @@ object EventType {
|
|||
const val STATE_ROOM_ENCRYPTION = "m.room.encryption"
|
||||
|
||||
// Call Events
|
||||
|
||||
const val CALL_INVITE = "m.call.invite"
|
||||
const val CALL_CANDIDATES = "m.call.candidates"
|
||||
const val CALL_ANSWER = "m.call.answer"
|
||||
|
|
|
@ -26,5 +26,5 @@ object RelationType {
|
|||
/** Lets you define an event which references an existing event.*/
|
||||
const val REFERENCE = "m.reference"
|
||||
/** Lets you define an event which adds a response to an existing event.*/
|
||||
const val RESPONSE = "m.response"
|
||||
const val RESPONSE = "org.matrix.response"
|
||||
}
|
||||
|
|
|
@ -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
|
||||
*
|
||||
*/
|
||||
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.
|
||||
* @param userId the userId param to look for
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package im.vector.matrix.android.api.session.room
|
||||
|
||||
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.members.MembershipService
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
|
@ -47,6 +48,7 @@ interface Room :
|
|||
StateService,
|
||||
UploadsService,
|
||||
ReportingService,
|
||||
RoomCallService,
|
||||
RelationService,
|
||||
RoomCryptoService,
|
||||
RoomPushRuleService {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -17,10 +17,12 @@
|
|||
package im.vector.matrix.android.api.session.room.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Represents the membership of a user on a room
|
||||
*/
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class Membership(val value: String) {
|
||||
|
||||
NONE("none"),
|
||||
|
|
|
@ -17,7 +17,9 @@
|
|||
package im.vector.matrix.android.api.session.room.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class RoomDirectoryVisibility {
|
||||
@Json(name = "private") PRIVATE,
|
||||
@Json(name = "public") PUBLIC
|
||||
|
|
|
@ -29,6 +29,7 @@ data class RoomGuestAccessContent(
|
|||
@Json(name = "guest_access") val guestAccess: GuestAccess? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class GuestAccess(val value: String) {
|
||||
@Json(name = "can_join")
|
||||
CanJoin("can_join"),
|
||||
|
|
|
@ -17,10 +17,12 @@
|
|||
package im.vector.matrix.android.api.session.room.model
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Ref: https://matrix.org/docs/spec/client_server/latest#room-history-visibility
|
||||
*/
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class RoomHistoryVisibility {
|
||||
/**
|
||||
* All events while this is the m.room.history_visibility value may be shared by any
|
||||
|
|
|
@ -18,10 +18,12 @@
|
|||
package im.vector.matrix.android.api.session.room.model
|
||||
|
||||
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
|
||||
*/
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class RoomJoinRules(val value: String) {
|
||||
|
||||
@Json(name = "public")
|
||||
|
|
|
@ -61,6 +61,9 @@ data class RoomSummary constructor(
|
|||
val isFavorite: Boolean
|
||||
get() = tags.any { it.name == RoomTag.ROOM_TAG_FAVOURITE }
|
||||
|
||||
val canStartCall: Boolean
|
||||
get() = isDirect && joinedMembersCount == 2
|
||||
|
||||
companion object {
|
||||
const val NOT_IN_BREADCRUMBS = -1
|
||||
}
|
||||
|
|
|
@ -19,16 +19,34 @@ package im.vector.matrix.android.api.session.room.model.call
|
|||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* This event is sent by the callee when they wish to answer the call.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CallAnswerContent(
|
||||
/**
|
||||
* Required. The ID of the call this event relates to.
|
||||
*/
|
||||
@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)
|
||||
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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -19,17 +19,39 @@ package im.vector.matrix.android.api.session.room.model.call
|
|||
import com.squareup.moshi.Json
|
||||
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)
|
||||
data class CallCandidatesContent(
|
||||
/**
|
||||
* Required. The ID of the call this event relates to.
|
||||
*/
|
||||
@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)
|
||||
data class Candidate(
|
||||
/**
|
||||
* Required. The SDP media type this candidate is intended for.
|
||||
*/
|
||||
@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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -19,8 +19,32 @@ package im.vector.matrix.android.api.session.room.model.call
|
|||
import com.squareup.moshi.Json
|
||||
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)
|
||||
data class CallHangupContent(
|
||||
/**
|
||||
* Required. The ID of the call this event relates to.
|
||||
*/
|
||||
@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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,23 +19,45 @@ package im.vector.matrix.android.api.session.room.model.call
|
|||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* This event is sent by the caller when they wish to establish a call.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CallInviteContent(
|
||||
@Json(name = "call_id") val callId: String,
|
||||
@Json(name = "version") val version: Int,
|
||||
@Json(name = "lifetime") val lifetime: Int,
|
||||
@Json(name = "offer") val offer: Offer
|
||||
/**
|
||||
* Required. A unique identifier for the call.
|
||||
*/
|
||||
@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)
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -17,7 +17,9 @@
|
|||
package im.vector.matrix.android.api.session.room.model.create
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class CreateRoomPreset {
|
||||
@Json(name = "private_chat")
|
||||
PRESET_PRIVATE_CHAT,
|
||||
|
|
|
@ -90,6 +90,6 @@ interface WidgetPostAPIMediator {
|
|||
/**
|
||||
* Triggered when a widget is posting
|
||||
*/
|
||||
fun handleWidgetRequest(eventData: JsonDict): Boolean
|
||||
fun handleWidgetRequest(mediator: WidgetPostAPIMediator, eventData: JsonDict): Boolean
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,8 @@ interface WidgetService {
|
|||
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.
|
||||
*/
|
||||
fun getWidgetPostAPIMediator(): WidgetPostAPIMediator
|
||||
|
|
|
@ -21,6 +21,9 @@ import com.squareup.moshi.Json
|
|||
import com.squareup.moshi.JsonClass
|
||||
import im.vector.matrix.android.api.util.JsonDict
|
||||
|
||||
/**
|
||||
* Ref: https://github.com/matrix-org/matrix-doc/issues/1236
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class WidgetContent(
|
||||
@Json(name = "creatorUserId") val creatorUserId: String? = null,
|
||||
|
|
|
@ -16,6 +16,25 @@
|
|||
|
||||
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) {
|
||||
object Jitsi : WidgetType("m.jitsi", "jitsi")
|
||||
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")
|
||||
data class Fallback(override val preferred: String) : WidgetType(preferred)
|
||||
|
||||
fun matches(type: String?): Boolean {
|
||||
fun matches(type: String): Boolean {
|
||||
return type == preferred || type == legacy
|
||||
}
|
||||
|
||||
|
@ -40,20 +59,6 @@ sealed class WidgetType(open val preferred: String, open val legacy: String = pr
|
|||
|
||||
companion object {
|
||||
|
||||
private val DEFINED_TYPES = listOf(
|
||||
Jitsi,
|
||||
TradingView,
|
||||
Spotify,
|
||||
Video,
|
||||
GoogleDoc,
|
||||
GoogleCalendar,
|
||||
Etherpad,
|
||||
StickerPicker,
|
||||
Grafana,
|
||||
Custom,
|
||||
IntegrationManager
|
||||
)
|
||||
|
||||
fun fromString(type: String): WidgetType {
|
||||
val matchingType = DEFINED_TYPES.firstOrNull {
|
||||
it.matches(type)
|
||||
|
|
|
@ -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.DefaultGetDevicesTask
|
||||
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.DefaultSendVerificationMessageTask
|
||||
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.GetDevicesTask
|
||||
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.SendVerificationMessageTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SetDeviceNameTask
|
||||
|
@ -251,4 +253,7 @@ internal abstract class CryptoModule {
|
|||
|
||||
@Binds
|
||||
abstract fun bindInitializeCrossSigningTask(task: DefaultInitializeCrossSigningTask): InitializeCrossSigningTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindSendEventTask(task: DefaultSendEventTask): SendEventTask
|
||||
}
|
||||
|
|
|
@ -1263,11 +1263,11 @@ internal class DefaultCryptoService @Inject constructor(
|
|||
return "DefaultCryptoService of $userId ($deviceId)"
|
||||
}
|
||||
|
||||
override fun getOutgoingRoomKeyRequest(): List<OutgoingRoomKeyRequest> {
|
||||
override fun getOutgoingRoomKeyRequests(): List<OutgoingRoomKeyRequest> {
|
||||
return cryptoStore.getOutgoingRoomKeyRequests()
|
||||
}
|
||||
|
||||
override fun getIncomingRoomKeyRequest(): List<IncomingRoomKeyRequest> {
|
||||
override fun getIncomingRoomKeyRequests(): List<IncomingRoomKeyRequest> {
|
||||
return cryptoStore.getIncomingRoomKeyRequests()
|
||||
}
|
||||
|
||||
|
|
|
@ -897,9 +897,9 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
it.toOutgoingGossipingRequest() as? OutgoingRoomKeyRequest
|
||||
}.firstOrNull {
|
||||
it.requestBody?.algorithm == requestBody.algorithm
|
||||
it.requestBody?.roomId == requestBody.roomId
|
||||
it.requestBody?.senderKey == requestBody.senderKey
|
||||
it.requestBody?.sessionId == requestBody.sessionId
|
||||
&& it.requestBody?.roomId == requestBody.roomId
|
||||
&& it.requestBody?.senderKey == requestBody.senderKey
|
||||
&& it.requestBody?.sessionId == requestBody.sessionId
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1266,7 +1266,7 @@ internal class RealmCryptoStore @Inject constructor(
|
|||
deviceInfoEntity.trustLevelEntity = it
|
||||
}
|
||||
} else {
|
||||
locallyVerified?.let { trustEntity.locallyVerified = it }
|
||||
locallyVerified?.let { trustEntity.locallyVerified = it }
|
||||
trustEntity.crossSignedVerified = crossSignedVerified
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -28,7 +28,7 @@ import kotlin.math.ceil
|
|||
*/
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -273,7 +273,7 @@ internal abstract class SASDefaultVerificationTransaction(
|
|||
if (keyIDNoPrefix == otherCrossSigningMasterKeyPublic) {
|
||||
// Check the signature
|
||||
val mac = macUsingAgreedMethod(otherCrossSigningMasterKeyPublic, baseInfo + it)
|
||||
if (mac != theirMacSafe.mac.get(it)) {
|
||||
if (mac != theirMacSafe.mac[it]) {
|
||||
// WRONG!
|
||||
Timber.e("## SAS Verification: mac mismatch for MasterKey with id $keyIDNoPrefix")
|
||||
cancel(CancelCode.MismatchedKeys)
|
||||
|
|
|
@ -85,7 +85,7 @@ internal class SendVerificationMessageWorker(context: Context,
|
|||
private const val OUTPUT_KEY_FAILED = "failed"
|
||||
|
||||
fun hasFailed(outputData: Data): Boolean {
|
||||
return outputData.getBoolean(SendVerificationMessageWorker.OUTPUT_KEY_FAILED, false)
|
||||
return outputData.getBoolean(OUTPUT_KEY_FAILED, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.database
|
|||
|
||||
import android.content.Context
|
||||
import android.util.Base64
|
||||
import androidx.core.content.edit
|
||||
import im.vector.matrix.android.BuildConfig
|
||||
import im.vector.matrix.android.internal.session.securestorage.SecretStoringUtils
|
||||
import io.realm.RealmConfiguration
|
||||
|
@ -67,10 +68,9 @@ internal class RealmKeysUtils @Inject constructor(context: Context,
|
|||
val key = generateKeyForRealm()
|
||||
val encodedKey = Base64.encodeToString(key, Base64.NO_PADDING)
|
||||
val toStore = secretStoringUtils.securelyStoreString(encodedKey, alias)
|
||||
sharedPreferences
|
||||
.edit()
|
||||
.putString("${ENCRYPTED_KEY_PREFIX}_$alias", Base64.encodeToString(toStore!!, Base64.NO_PADDING))
|
||||
.apply()
|
||||
sharedPreferences.edit {
|
||||
putString("${ENCRYPTED_KEY_PREFIX}_$alias", Base64.encodeToString(toStore!!, Base64.NO_PADDING))
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
|
@ -107,10 +107,9 @@ internal class RealmKeysUtils @Inject constructor(context: Context,
|
|||
if (hasKeyForDatabase(alias)) {
|
||||
secretStoringUtils.safeDeleteKey(alias)
|
||||
|
||||
sharedPreferences
|
||||
.edit()
|
||||
.remove("${ENCRYPTED_KEY_PREFIX}_$alias")
|
||||
.apply()
|
||||
sharedPreferences.edit {
|
||||
remove("${ENCRYPTED_KEY_PREFIX}_$alias")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package im.vector.matrix.android.internal.database
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
import im.vector.matrix.android.internal.database.model.SessionRealmModule
|
||||
import im.vector.matrix.android.internal.di.SessionFilesDirectory
|
||||
import im.vector.matrix.android.internal.di.SessionId
|
||||
|
@ -54,10 +55,9 @@ internal class SessionRealmConfigurationFactory @Inject constructor(
|
|||
Timber.v("************************************************************")
|
||||
deleteRealmFiles()
|
||||
}
|
||||
sharedPreferences
|
||||
.edit()
|
||||
.putBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", true)
|
||||
.apply()
|
||||
sharedPreferences.edit {
|
||||
putBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", true)
|
||||
}
|
||||
|
||||
val realmConfiguration = RealmConfiguration.Builder()
|
||||
.compactOnLaunch()
|
||||
|
@ -73,10 +73,9 @@ internal class SessionRealmConfigurationFactory @Inject constructor(
|
|||
// Try creating a realm instance and if it succeeds we can clear the flag
|
||||
Realm.getInstance(realmConfiguration).use {
|
||||
Timber.v("Successfully create realm instance")
|
||||
sharedPreferences
|
||||
.edit()
|
||||
.putBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", false)
|
||||
.apply()
|
||||
sharedPreferences.edit {
|
||||
putBoolean("$REALM_SHOULD_CLEAR_FLAG_$sessionId", false)
|
||||
}
|
||||
}
|
||||
return realmConfiguration
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ internal object TimelineEventFilter {
|
|||
*/
|
||||
internal object Content {
|
||||
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"*}"""
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
* Copyright 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.
|
||||
|
@ -22,12 +22,13 @@ import kotlinx.coroutines.CancellationException
|
|||
import kotlinx.coroutines.delay
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import retrofit2.Call
|
||||
import retrofit2.awaitResponse
|
||||
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()
|
||||
|
||||
internal class Request<DATA>(private val eventBus: EventBus?) {
|
||||
internal class Request<DATA : Any>(private val eventBus: EventBus?) {
|
||||
|
||||
var isRetryable = false
|
||||
var initialDelay: Long = 100L
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
*
|
||||
* * Copyright 2019 New Vector Ltd
|
||||
* * Copyright 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.
|
||||
|
@ -26,8 +26,6 @@ import im.vector.matrix.android.internal.di.MoshiProvider
|
|||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import okhttp3.ResponseBody
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
|
@ -35,23 +33,6 @@ import java.net.HttpURLConnection
|
|||
import kotlin.coroutines.resume
|
||||
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 {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
continuation.invokeOnCancellation {
|
||||
|
|
|
@ -33,7 +33,7 @@ data class Fingerprint(
|
|||
|
||||
@Throws(CertificateException::class)
|
||||
fun matchesCert(cert: X509Certificate): Boolean {
|
||||
var o: Fingerprint? = when (hashType) {
|
||||
val o: Fingerprint? = when (hashType) {
|
||||
HashType.SHA256 -> newSha256Fingerprint(cert)
|
||||
HashType.SHA1 -> newSha1Fingerprint(cert)
|
||||
}
|
||||
|
@ -76,6 +76,7 @@ data class Fingerprint(
|
|||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class HashType {
|
||||
@Json(name = "sha-1") SHA1,
|
||||
@Json(name = "sha-256")SHA256
|
||||
|
|
|
@ -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.accountdata.AccountDataService
|
||||
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.ContentUrlResolver
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
|
@ -108,7 +109,8 @@ internal class DefaultSession @Inject constructor(
|
|||
private val coroutineDispatchers: MatrixCoroutineDispatchers,
|
||||
private val defaultIdentityService: DefaultIdentityService,
|
||||
private val integrationManagerService: IntegrationManagerService,
|
||||
private val taskExecutor: TaskExecutor)
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val callSignalingService: Lazy<CallSignalingService>)
|
||||
: Session,
|
||||
RoomService by roomService.get(),
|
||||
RoomDirectoryService by roomDirectoryService.get(),
|
||||
|
@ -244,6 +246,8 @@ internal class DefaultSession @Inject constructor(
|
|||
|
||||
override fun integrationManagerService() = integrationManagerService
|
||||
|
||||
override fun callSignalingService(): CallSignalingService = callSignalingService.get()
|
||||
|
||||
override fun addListener(listener: Session.Listener) {
|
||||
sessionListeners.addListener(listener)
|
||||
}
|
||||
|
|
|
@ -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.session.account.AccountModule
|
||||
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.UploadContentWorker
|
||||
import im.vector.matrix.android.internal.session.filter.FilterModule
|
||||
|
@ -83,7 +84,8 @@ import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
|||
AccountDataModule::class,
|
||||
ProfileModule::class,
|
||||
SessionAssistedInjectModule::class,
|
||||
AccountModule::class
|
||||
AccountModule::class,
|
||||
CallModule::class
|
||||
]
|
||||
)
|
||||
@SessionScope
|
||||
|
|
|
@ -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.token.AccessTokenProvider
|
||||
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.homeserver.DefaultHomeServerCapabilitiesService
|
||||
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.user.accountdata.DefaultAccountDataService
|
||||
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 io.realm.RealmConfiguration
|
||||
import okhttp3.OkHttpClient
|
||||
|
@ -263,7 +263,7 @@ internal abstract class SessionModule {
|
|||
|
||||
@Binds
|
||||
@IntoSet
|
||||
abstract fun bindWidgetManager(observer: WidgetManager): SessionLifecycleObserver
|
||||
abstract fun bindCallEventObserver(observer: CallEventObserver): SessionLifecycleObserver
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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) }
|
||||
}
|
||||
}
|
|
@ -35,7 +35,8 @@ import javax.inject.Inject
|
|||
internal class DefaultProfileService @Inject constructor(private val taskExecutor: TaskExecutor,
|
||||
@SessionDatabase private val monarchy: Monarchy,
|
||||
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 {
|
||||
val params = GetProfileInfoTask.Params(userId)
|
||||
|
@ -55,6 +56,14 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
|
|||
.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 {
|
||||
val params = GetProfileInfoTask.Params(userId)
|
||||
return getProfileInfoTask
|
||||
|
|
|
@ -23,6 +23,7 @@ import retrofit2.Call
|
|||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.PUT
|
||||
import retrofit2.http.Path
|
||||
|
||||
internal interface ProfileAPI {
|
||||
|
@ -42,6 +43,12 @@ internal interface ProfileAPI {
|
|||
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid")
|
||||
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
|
||||
* Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-account-3pid-bind
|
||||
|
|
|
@ -51,4 +51,7 @@ internal abstract class ProfileModule {
|
|||
|
||||
@Binds
|
||||
abstract fun bindUnbindThreePidsTask(task: DefaultUnbindThreePidsTask): UnbindThreePidsTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindSetDisplayNameTask(task: DefaultSetDisplayNameTask): SetDisplayNameTask
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.events.model.EventType
|
||||
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.model.RoomSummary
|
||||
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 uploadsService: UploadsService,
|
||||
private val reportingService: ReportingService,
|
||||
private val roomCallService: RoomCallService,
|
||||
private val readService: ReadService,
|
||||
private val typingService: TypingService,
|
||||
private val tagsService: TagsService,
|
||||
|
@ -67,6 +69,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
|
|||
StateService by stateService,
|
||||
UploadsService by uploadsService,
|
||||
ReportingService by reportingService,
|
||||
RoomCallService by roomCallService,
|
||||
ReadService by readService,
|
||||
TypingService by typingService,
|
||||
TagsService by tagsService,
|
||||
|
|
|
@ -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.room.Room
|
||||
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.membership.DefaultMembershipService
|
||||
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 uploadsServiceFactory: DefaultUploadsService.Factory,
|
||||
private val reportingServiceFactory: DefaultReportingService.Factory,
|
||||
private val roomCallServiceFactory: DefaultRoomCallService.Factory,
|
||||
private val readServiceFactory: DefaultReadService.Factory,
|
||||
private val typingServiceFactory: DefaultTypingService.Factory,
|
||||
private val tagsServiceFactory: DefaultTagsService.Factory,
|
||||
|
@ -69,6 +71,7 @@ internal class DefaultRoomFactory @Inject constructor(private val cryptoService:
|
|||
stateService = stateServiceFactory.create(roomId),
|
||||
uploadsService = uploadsServiceFactory.create(roomId),
|
||||
reportingService = reportingServiceFactory.create(roomId),
|
||||
roomCallService = roomCallServiceFactory.create(roomId),
|
||||
readService = readServiceFactory.create(roomId),
|
||||
typingService = typingServiceFactory.create(roomId),
|
||||
tagsService = tagsServiceFactory.create(roomId),
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -52,7 +52,7 @@ internal class DefaultRoomPushRuleService @AssistedInject constructor(@Assisted
|
|||
override fun setRoomNotificationState(roomNotificationState: RoomNotificationState, matrixCallback: MatrixCallback<Unit>): Cancelable {
|
||||
return setRoomNotificationStateTask
|
||||
.configureWith(SetRoomNotificationStateTask.Params(roomId, roomNotificationState)) {
|
||||
this.callback = callback
|
||||
this.callback = matrixCallback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
|
|
@ -58,7 +58,8 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
private val localEchoEventFactory: LocalEchoEventFactory,
|
||||
private val cryptoService: CryptoService,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val localEchoRepository: LocalEchoRepository
|
||||
private val localEchoRepository: LocalEchoRepository,
|
||||
private val roomEventSender: RoomEventSender
|
||||
) : SendService {
|
||||
|
||||
@AssistedInject.Factory
|
||||
|
@ -111,20 +112,6 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
.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>,
|
||||
compressBeforeSending: Boolean,
|
||||
roomIds: Set<String>): Cancelable {
|
||||
|
@ -269,6 +256,10 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
return cancelableBag
|
||||
}
|
||||
|
||||
private fun sendEvent(event: Event): Cancelable {
|
||||
return roomEventSender.sendEvent(event)
|
||||
}
|
||||
|
||||
private fun createLocalEcho(event: Event) {
|
||||
localEchoEventFactory.createLocalEcho(event)
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ internal class MarkdownParser @Inject constructor(
|
|||
fun parse(text: String): TextContent {
|
||||
// If no special char are detected, just return plain text
|
||||
if (text.contains(mdSpecialChars).not()) {
|
||||
return TextContent(text.toString())
|
||||
return TextContent(text)
|
||||
}
|
||||
|
||||
val document = parser.parse(text)
|
||||
|
@ -56,7 +56,7 @@ internal class MarkdownParser @Inject constructor(
|
|||
val plainText = textContentRenderer.render(document)
|
||||
TextContent(plainText, cleanHtmlText.postTreatment())
|
||||
} else {
|
||||
TextContent(text.toString())
|
||||
TextContent(text)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -78,7 +78,7 @@ internal class DefaultWidgetPostAPIMediator @Inject constructor(private val mosh
|
|||
|
||||
private fun onWidgetMessage(eventData: JsonDict) {
|
||||
try {
|
||||
if (handler?.handleWidgetRequest(eventData) == false) {
|
||||
if (handler?.handleWidgetRequest(this, eventData) == false) {
|
||||
sendError("", eventData)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
|
|
@ -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.util.Cancelable
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
|
||||
internal class DefaultWidgetService @Inject constructor(private val widgetManager: WidgetManager,
|
||||
private val widgetURLFormatter: WidgetURLFormatter,
|
||||
private val widgetPostAPIMediator: WidgetPostAPIMediator)
|
||||
private val widgetPostAPIMediator: Provider<WidgetPostAPIMediator>)
|
||||
: WidgetService {
|
||||
|
||||
override fun getWidgetURLFormatter(): WidgetURLFormatter {
|
||||
|
@ -37,7 +38,7 @@ internal class DefaultWidgetService @Inject constructor(private val widgetManage
|
|||
}
|
||||
|
||||
override fun getWidgetPostAPIMediator(): WidgetPostAPIMediator {
|
||||
return widgetPostAPIMediator
|
||||
return widgetPostAPIMediator.get()
|
||||
}
|
||||
|
||||
override fun getRoomWidgets(
|
||||
|
|
|
@ -67,8 +67,7 @@ internal class DefaultGetScalarTokenTask @Inject constructor(private val widgets
|
|||
throw IllegalStateException("Scalar token is null")
|
||||
}
|
||||
scalarTokenStore.setToken(serverUrl, registerWidgetResponse.scalarToken)
|
||||
widgetsAPI.validateToken(registerWidgetResponse.scalarToken, WIDGET_API_VERSION)
|
||||
return registerWidgetResponse.scalarToken
|
||||
return validateToken(widgetsAPI, serverUrl, registerWidgetResponse.scalarToken)
|
||||
}
|
||||
|
||||
private suspend fun validateToken(widgetsAPI: WidgetsAPI, serverUrl: String, scalarToken: String): String {
|
||||
|
|
|
@ -27,6 +27,7 @@ import android.security.keystore.KeyGenParameterSpec
|
|||
import android.security.keystore.KeyProperties
|
||||
import android.util.Base64
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.content.edit
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
@ -152,9 +153,9 @@ object CompatUtil {
|
|||
.build())
|
||||
key = generator.generateKey()
|
||||
|
||||
sharedPreferences.edit()
|
||||
.putInt(SHARED_KEY_ANDROID_VERSION_WHEN_KEY_HAS_BEEN_GENERATED, Build.VERSION.SDK_INT)
|
||||
.apply()
|
||||
sharedPreferences.edit {
|
||||
putInt(SHARED_KEY_ANDROID_VERSION_WHEN_KEY_HAS_BEEN_GENERATED, Build.VERSION.SDK_INT)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -188,10 +189,10 @@ object CompatUtil {
|
|||
cipher.init(Cipher.WRAP_MODE, keyPair.public)
|
||||
val wrappedAesKey = cipher.wrap(key)
|
||||
|
||||
sharedPreferences.edit()
|
||||
.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)
|
||||
.apply()
|
||||
sharedPreferences.edit {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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_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_removed">%1$s removed their display name (%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">%1$s removed their display name (it was %2$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_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>
|
||||
|
@ -43,6 +43,8 @@
|
|||
<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_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_by_you">You answered 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="call_notification_answer">Accept</string>
|
||||
<string name="call_notification_reject">Decline</string>
|
||||
<string name="call_notification_hangup">Hang Up</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -62,9 +62,9 @@ class ContactPicker(override val requestCode: Int) : Picker<MultiPickerContactTy
|
|||
|
||||
val contactId = cursor.getInt(idColumn)
|
||||
var name = cursor.getString(nameColumn)
|
||||
var photoUri = cursor.getString(photoUriColumn)
|
||||
var phoneNumberList = mutableListOf<String>()
|
||||
var emailList = mutableListOf<String>()
|
||||
val photoUri = cursor.getString(photoUriColumn)
|
||||
val phoneNumberList = mutableListOf<String>()
|
||||
val emailList = mutableListOf<String>()
|
||||
|
||||
getRawContactId(context.contentResolver, contactId)?.let { rawContactId ->
|
||||
val selection = ContactsContract.Data.RAW_CONTACT_ID + " = ?"
|
||||
|
|
BIN
resources/img/f-droid-badge.png
Normal file
BIN
resources/img/f-droid-badge.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
BIN
resources/img/google-play-badge.png
Normal file
BIN
resources/img/google-play-badge.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
|
@ -17,8 +17,9 @@
|
|||
#
|
||||
|
||||
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."
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ androidExtensions {
|
|||
}
|
||||
|
||||
ext.versionMajor = 0
|
||||
ext.versionMinor = 22
|
||||
ext.versionMinor = 23
|
||||
ext.versionPatch = 0
|
||||
|
||||
static def getGitTimestamp() {
|
||||
|
@ -107,9 +107,8 @@ android {
|
|||
compileSdkVersion 29
|
||||
defaultConfig {
|
||||
applicationId "im.vector.riotx"
|
||||
// Set to API 19 because motionLayout is min API 18.
|
||||
// In the future we may consider using an alternative of MotionLayout to support API 16. But for security reason, maybe not.
|
||||
minSdkVersion 19
|
||||
// Set to API 21: see #405
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
multiDexEnabled true
|
||||
|
||||
|
@ -192,8 +191,14 @@ android {
|
|||
resValue "bool", "debug_mode", "false"
|
||||
buildConfigField "boolean", "LOW_PRIVACY_LOG_ENABLE", "false"
|
||||
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
postprocessing {
|
||||
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'
|
||||
|
||||
// TODO meant for development purposes only
|
||||
implementation 'org.webrtc:google-webrtc:1.0.+'
|
||||
|
||||
// QR-code
|
||||
// Stick to 3.3.3 because of https://github.com/zxing/zxing/issues/1170
|
||||
implementation 'com.google.zxing:core:3.3.3'
|
||||
|
|
2
vector/proguard-rules.pro
vendored
2
vector/proguard-rules.pro
vendored
|
@ -19,3 +19,5 @@
|
|||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
-keep class im.vector.riotx.features.** { *; }
|
|
@ -21,6 +21,7 @@ import android.app.Activity
|
|||
import android.content.Context
|
||||
import androidx.preference.PreferenceManager
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.edit
|
||||
import com.google.android.gms.common.ConnectionResult
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
import com.google.firebase.iid.FirebaseInstanceId
|
||||
|
@ -57,10 +58,9 @@ object FcmHelper {
|
|||
*/
|
||||
fun storeFcmToken(context: Context,
|
||||
token: String?) {
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.edit()
|
||||
.putString(PREFS_KEY_FCM_TOKEN, token)
|
||||
.apply()
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit {
|
||||
putString(PREFS_KEY_FCM_TOKEN, token)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,13 +3,30 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
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.READ_CONTACTS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<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 -->
|
||||
<!-- Tell that the Camera is not mandatory to install the application -->
|
||||
<uses-feature
|
||||
|
@ -172,6 +189,7 @@
|
|||
<activity
|
||||
android:name=".features.attachments.preview.AttachmentsPreviewActivity"
|
||||
android:theme="@style/AppTheme.AttachmentsPreview" />
|
||||
<activity android:name=".features.call.VectorCallActivity" />
|
||||
|
||||
<activity android:name=".features.terms.ReviewTermsActivity" />
|
||||
<activity android:name=".features.widgets.WidgetActivity" />
|
||||
|
@ -180,20 +198,47 @@
|
|||
|
||||
<service
|
||||
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
|
||||
android:name=".core.services.VectorSyncService"
|
||||
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 -->
|
||||
|
||||
<receiver
|
||||
android:name=".features.call.service.CallHeadsUpActionReceiver"
|
||||
android:exported="false" />
|
||||
|
||||
<!-- Exported false, should only be accessible from this app!! -->
|
||||
<receiver
|
||||
android:name=".features.notifications.NotificationBroadcastReceiver"
|
||||
android:enabled="true"
|
||||
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 -->
|
||||
|
||||
<provider
|
||||
|
|
|
@ -43,6 +43,7 @@ import im.vector.riotx.core.di.HasVectorInjector
|
|||
import im.vector.riotx.core.di.VectorComponent
|
||||
import im.vector.riotx.core.extensions.configureAndStart
|
||||
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.lifecycle.VectorActivityLifecycleCallbacks
|
||||
import im.vector.riotx.features.notifications.NotificationDrawerManager
|
||||
|
@ -80,6 +81,8 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
|
|||
@Inject lateinit var appStateHandler: AppStateHandler
|
||||
@Inject lateinit var rxConfig: RxConfig
|
||||
@Inject lateinit var popupAlertManager: PopupAlertManager
|
||||
@Inject lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager
|
||||
|
||||
lateinit var vectorComponent: VectorComponent
|
||||
private var fontThreadHandler: Handler? = null
|
||||
|
||||
|
@ -122,6 +125,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
|
|||
val lastAuthenticatedSession = authenticationService.getLastAuthenticatedSession()!!
|
||||
activeSessionHolder.setActiveSession(lastAuthenticatedSession)
|
||||
lastAuthenticatedSession.configureAndStart(applicationContext, pushRuleTriggerListener, sessionListener)
|
||||
lastAuthenticatedSession.callSignalingService().addCallListener(webRtcPeerConnectionManager)
|
||||
}
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(object : LifecycleObserver {
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||
|
|
|
@ -24,6 +24,8 @@ import dagger.Component
|
|||
import im.vector.riotx.core.error.ErrorFormatter
|
||||
import im.vector.riotx.core.preference.UserAvatarPreference
|
||||
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.crypto.keysbackup.settings.KeysBackupManageActivity
|
||||
import im.vector.riotx.features.crypto.quads.SharedSecureStorageActivity
|
||||
|
@ -130,6 +132,7 @@ interface ScreenComponent {
|
|||
fun inject(activity: InviteUsersToRoomActivity)
|
||||
fun inject(activity: ReviewTermsActivity)
|
||||
fun inject(activity: WidgetActivity)
|
||||
fun inject(activity: VectorCallActivity)
|
||||
|
||||
/* ==========================================================================================
|
||||
* BottomSheets
|
||||
|
@ -146,6 +149,7 @@ interface ScreenComponent {
|
|||
fun inject(bottomSheet: BootstrapBottomSheet)
|
||||
fun inject(bottomSheet: RoomWidgetPermissionBottomSheet)
|
||||
fun inject(bottomSheet: RoomWidgetsBottomSheet)
|
||||
fun inject(bottomSheet: CallControlsBottomSheet)
|
||||
|
||||
/* ==========================================================================================
|
||||
* Others
|
||||
|
|
|
@ -31,6 +31,7 @@ import im.vector.riotx.core.error.ErrorFormatter
|
|||
import im.vector.riotx.core.pushers.PushersManager
|
||||
import im.vector.riotx.core.utils.AssetReader
|
||||
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.crypto.keysrequest.KeyRequestHandler
|
||||
import im.vector.riotx.features.crypto.verification.IncomingVerificationRequestHandler
|
||||
|
@ -134,6 +135,8 @@ interface VectorComponent {
|
|||
|
||||
fun reAuthHelper(): ReAuthHelper
|
||||
|
||||
fun webRtcPeerConnectionManager(): WebRtcPeerConnectionManager
|
||||
|
||||
@Component.Factory
|
||||
interface Factory {
|
||||
fun create(@BindsInstance context: Context): VectorComponent
|
||||
|
|
|
@ -22,6 +22,7 @@ import dagger.Binds
|
|||
import dagger.Module
|
||||
import dagger.multibindings.IntoMap
|
||||
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.KeysBackupRestoreFromPassphraseViewModel
|
||||
import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreSharedViewModel
|
||||
|
@ -85,6 +86,11 @@ interface ViewModelModule {
|
|||
@ViewModelKey(ConfigurationViewModel::class)
|
||||
fun bindConfigurationViewModel(viewModel: ConfigurationViewModel): ViewModel
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(SharedActiveCallViewModel::class)
|
||||
fun bindSharedActiveCallViewModel(viewModel: SharedActiveCallViewModel): ViewModel
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(UserDirectorySharedActionViewModel::class)
|
||||
|
|
|
@ -40,14 +40,14 @@ class DefaultErrorFormatter @Inject constructor(
|
|||
null -> null
|
||||
is IdentityServiceError -> identityServerError(throwable)
|
||||
is Failure.NetworkConnection -> {
|
||||
when {
|
||||
throwable.ioException is SocketTimeoutException ->
|
||||
when (throwable.ioException) {
|
||||
is SocketTimeoutException ->
|
||||
stringProvider.getString(R.string.error_network_timeout)
|
||||
throwable.ioException is UnknownHostException ->
|
||||
is UnknownHostException ->
|
||||
// Invalid homeserver?
|
||||
// TODO Check network state, airplane mode, etc.
|
||||
stringProvider.getString(R.string.login_error_unknown_host)
|
||||
else ->
|
||||
else ->
|
||||
stringProvider.getString(R.string.error_no_network)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,9 +17,14 @@
|
|||
package im.vector.riotx.core.extensions
|
||||
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
import im.vector.matrix.android.api.extensions.tryThis
|
||||
|
||||
inline fun androidx.fragment.app.FragmentManager.commitTransactionNow(func: FragmentTransaction.() -> FragmentTransaction) {
|
||||
beginTransaction().func().commitNow()
|
||||
// Could throw and make the app crash
|
||||
// e.g sharedActionViewModel.observe()
|
||||
tryThis("Failed to commitTransactionNow") {
|
||||
beginTransaction().func().commitNow()
|
||||
}
|
||||
}
|
||||
|
||||
inline fun androidx.fragment.app.FragmentManager.commitTransaction(func: FragmentTransaction.() -> FragmentTransaction) {
|
||||
|
|
|
@ -38,10 +38,6 @@ fun Session.configureAndStart(context: Context,
|
|||
startSyncing(context)
|
||||
refreshPushers()
|
||||
pushRuleTriggerListener.startWithSession(this)
|
||||
|
||||
// TODO P1 From HomeActivity
|
||||
// @Inject lateinit var incomingVerificationRequestHandler: IncomingVerificationRequestHandler
|
||||
// @Inject lateinit var keyRequestHandler: KeyRequestHandler
|
||||
}
|
||||
|
||||
fun Session.startSyncing(context: Context) {
|
||||
|
|
|
@ -165,6 +165,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
|
|||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
Timber.i("onCreate Activity ${this.javaClass.simpleName}")
|
||||
val vectorComponent = getVectorComponent()
|
||||
screenComponent = DaggerScreenComponent.factory().create(vectorComponent, this)
|
||||
val timeForInjection = measureTimeMillis {
|
||||
|
@ -252,6 +253,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
|
|||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Timber.i("onDestroy Activity ${this.javaClass.simpleName}")
|
||||
unBinder?.unbind()
|
||||
unBinder = null
|
||||
|
||||
|
@ -279,6 +281,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
|
|||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
Timber.i("onPause Activity ${this.javaClass.simpleName}")
|
||||
|
||||
rageShake.stop()
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ import com.airbnb.mvrx.MvRxViewId
|
|||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
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.ScreenComponent
|
||||
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.Disposable
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Add MvRx capabilities to bottomsheetdialog (like BaseMvRxFragment)
|
||||
|
@ -169,6 +171,18 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
|
|||
return this
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* Views
|
||||
* ========================================================================================== */
|
||||
|
||||
protected fun View.debouncedClicks(onClicked: () -> Unit) {
|
||||
clicks()
|
||||
.throttleFirst(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { onClicked() }
|
||||
.disposeOnDestroyView()
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* ViewEvents
|
||||
* ========================================================================================== */
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue