mirror of
https://github.com/element-hq/element-android
synced 2024-11-28 13:38:49 +03:00
Merge branch 'develop' into feature/fix_widget
This commit is contained in:
commit
554c37febe
150 changed files with 6857 additions and 292 deletions
|
@ -7,6 +7,7 @@
|
||||||
<w>ciphertext</w>
|
<w>ciphertext</w>
|
||||||
<w>coroutine</w>
|
<w>coroutine</w>
|
||||||
<w>decryptor</w>
|
<w>decryptor</w>
|
||||||
|
<w>displayname</w>
|
||||||
<w>emoji</w>
|
<w>emoji</w>
|
||||||
<w>emojis</w>
|
<w>emojis</w>
|
||||||
<w>fdroid</w>
|
<w>fdroid</w>
|
||||||
|
|
|
@ -2,7 +2,8 @@ Changes in RiotX 0.23.0 (2020-XX-XX)
|
||||||
===================================================
|
===================================================
|
||||||
|
|
||||||
Features ✨:
|
Features ✨:
|
||||||
-
|
- Call with WebRTC support (##611)
|
||||||
|
- Add capability to change the display name (#1529)
|
||||||
|
|
||||||
Improvements 🙌:
|
Improvements 🙌:
|
||||||
- "Add Matrix app" menu is now always visible (#1495)
|
- "Add Matrix app" menu is now always visible (#1495)
|
||||||
|
@ -10,6 +11,7 @@ Improvements 🙌:
|
||||||
Bugfix 🐛:
|
Bugfix 🐛:
|
||||||
- Fix dark theme issue on login screen (#1097)
|
- Fix dark theme issue on login screen (#1097)
|
||||||
- Incomplete predicate in RealmCryptoStore#getOutgoingRoomKeyRequest (#1519)
|
- Incomplete predicate in RealmCryptoStore#getOutgoingRoomKeyRequest (#1519)
|
||||||
|
- Use vendor prefix for non merged MSC (#1537)
|
||||||
|
|
||||||
Translations 🗣:
|
Translations 🗣:
|
||||||
-
|
-
|
||||||
|
@ -22,7 +24,10 @@ Build 🧱:
|
||||||
- SDK is now API level 21 minimum, and so RiotX (#405)
|
- SDK is now API level 21 minimum, and so RiotX (#405)
|
||||||
|
|
||||||
Other changes:
|
Other changes:
|
||||||
-
|
- 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)
|
Changes in RiotX 0.22.0 (2020-06-15)
|
||||||
===================================================
|
===================================================
|
||||||
|
|
|
@ -31,7 +31,7 @@ To create a new screen:
|
||||||
- Then right click on the package, and select `New/New Vector/RiotX Feature`.
|
- Then right click on the package, and select `New/New Vector/RiotX Feature`.
|
||||||
- Follow the Wizard, especially replace `Main` by something more relevant to your feature.
|
- Follow the Wizard, especially replace `Main` by something more relevant to your feature.
|
||||||
- Click on `Finish`.
|
- Click on `Finish`.
|
||||||
- Remainning steps are described as TODO in the generated files, or will be pointed out by the compilator, or at runtime :)
|
- Remaining steps are described as TODO in the generated files, or will be pointed out by the compilator, or at runtime :)
|
||||||
|
|
||||||
Note that if the templates are modified, the only things to do is to restart Android Studio for the change to take effect.
|
Note that if the templates are modified, the only things to do is to restart Android Studio for the change to take effect.
|
||||||
|
|
||||||
|
|
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 │
|
||||||
|
└────────────────────┘ │ └────────────────────┘
|
||||||
|
│
|
||||||
|
│
|
||||||
|
│
|
||||||
|
│
|
||||||
|
│
|
||||||
|
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
|
@ -162,6 +162,10 @@ dependencies {
|
||||||
// Phone number https://github.com/google/libphonenumber
|
// Phone number https://github.com/google/libphonenumber
|
||||||
implementation 'com.googlecode.libphonenumber:libphonenumber:8.10.23'
|
implementation 'com.googlecode.libphonenumber:libphonenumber:8.10.23'
|
||||||
|
|
||||||
|
// Web RTC
|
||||||
|
// TODO meant for development purposes only. See http://webrtc.github.io/webrtc-org/native-code/android/
|
||||||
|
implementation 'org.webrtc:google-webrtc:1.0.+'
|
||||||
|
|
||||||
debugImplementation 'com.airbnb.okreplay:okreplay:1.5.0'
|
debugImplementation 'com.airbnb.okreplay:okreplay:1.5.0'
|
||||||
releaseImplementation 'com.airbnb.okreplay:noop:1.5.0'
|
releaseImplementation 'com.airbnb.okreplay:noop:1.5.0'
|
||||||
androidTestImplementation 'com.airbnb.okreplay:espresso:1.5.0'
|
androidTestImplementation 'com.airbnb.okreplay:espresso:1.5.0'
|
||||||
|
|
|
@ -241,14 +241,14 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
||||||
val eventWireContent = event.content.toContent()
|
val eventWireContent = event.content.toContent()
|
||||||
assertNotNull(eventWireContent)
|
assertNotNull(eventWireContent)
|
||||||
|
|
||||||
assertNull(eventWireContent.get("body"))
|
assertNull(eventWireContent["body"])
|
||||||
assertEquals(MXCRYPTO_ALGORITHM_MEGOLM, eventWireContent.get("algorithm"))
|
assertEquals(MXCRYPTO_ALGORITHM_MEGOLM, eventWireContent["algorithm"])
|
||||||
|
|
||||||
assertNotNull(eventWireContent.get("ciphertext"))
|
assertNotNull(eventWireContent["ciphertext"])
|
||||||
assertNotNull(eventWireContent.get("session_id"))
|
assertNotNull(eventWireContent["session_id"])
|
||||||
assertNotNull(eventWireContent.get("sender_key"))
|
assertNotNull(eventWireContent["sender_key"])
|
||||||
|
|
||||||
assertEquals(senderSession.sessionParams.deviceId, eventWireContent.get("device_id"))
|
assertEquals(senderSession.sessionParams.deviceId, eventWireContent["device_id"])
|
||||||
|
|
||||||
assertNotNull(event.eventId)
|
assertNotNull(event.eventId)
|
||||||
assertEquals(roomId, event.roomId)
|
assertEquals(roomId, event.roomId)
|
||||||
|
@ -257,7 +257,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
||||||
|
|
||||||
val eventContent = event.toContent()
|
val eventContent = event.toContent()
|
||||||
assertNotNull(eventContent)
|
assertNotNull(eventContent)
|
||||||
assertEquals(clearMessage, eventContent.get("body"))
|
assertEquals(clearMessage, eventContent["body"])
|
||||||
assertEquals(senderSession.myUserId, event.senderId)
|
assertEquals(senderSession.myUserId, event.senderId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -252,7 +252,7 @@ class KeyShareTests : InstrumentedTest {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
val txId: String = "m.testVerif12"
|
val txId = "m.testVerif12"
|
||||||
aliceVerificationService2.beginKeyVerification(VerificationMethod.SAS, aliceSession1.myUserId, aliceSession1.sessionParams.deviceId
|
aliceVerificationService2.beginKeyVerification(VerificationMethod.SAS, aliceSession1.myUserId, aliceSession1.sessionParams.deviceId
|
||||||
?: "", txId)
|
?: "", txId)
|
||||||
|
|
||||||
|
|
|
@ -144,7 +144,7 @@ class QuadSTests : InstrumentedTest {
|
||||||
|
|
||||||
val secretAccountData = assertAccountData(aliceSession, "secret.of.life")
|
val secretAccountData = assertAccountData(aliceSession, "secret.of.life")
|
||||||
|
|
||||||
val encryptedContent = secretAccountData.content.get("encrypted") as? Map<*, *>
|
val encryptedContent = secretAccountData.content["encrypted"] as? Map<*, *>
|
||||||
assertNotNull("Element should be encrypted", encryptedContent)
|
assertNotNull("Element should be encrypted", encryptedContent)
|
||||||
assertNotNull("Secret should be encrypted with default key", encryptedContent?.get(keyId))
|
assertNotNull("Secret should be encrypted with default key", encryptedContent?.get(keyId))
|
||||||
|
|
||||||
|
|
|
@ -69,7 +69,7 @@ data class HomeServerConnectionConfig(
|
||||||
*/
|
*/
|
||||||
fun withHomeServerUri(hsUri: Uri): Builder {
|
fun withHomeServerUri(hsUri: Uri): Builder {
|
||||||
if (hsUri.scheme != "http" && hsUri.scheme != "https") {
|
if (hsUri.scheme != "http" && hsUri.scheme != "https") {
|
||||||
throw RuntimeException("Invalid home server URI: " + hsUri)
|
throw RuntimeException("Invalid home server URI: $hsUri")
|
||||||
}
|
}
|
||||||
// ensure trailing /
|
// ensure trailing /
|
||||||
val hsString = hsUri.toString().ensureTrailingSlash()
|
val hsString = hsUri.toString().ensureTrailingSlash()
|
||||||
|
|
|
@ -16,10 +16,15 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.api.extensions
|
package im.vector.matrix.android.api.extensions
|
||||||
|
|
||||||
inline fun <A> tryThis(operation: () -> A): A? {
|
import timber.log.Timber
|
||||||
|
|
||||||
|
inline fun <A> tryThis(message: String? = null, operation: () -> A): A? {
|
||||||
return try {
|
return try {
|
||||||
operation()
|
operation()
|
||||||
} catch (any: Throwable) {
|
} catch (any: Throwable) {
|
||||||
|
if (message != null) {
|
||||||
|
Timber.e(any, message)
|
||||||
|
}
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,14 +87,13 @@ class EventMatchCondition(
|
||||||
// Very simple glob to regexp converter
|
// Very simple glob to regexp converter
|
||||||
private fun simpleGlobToRegExp(glob: String): String {
|
private fun simpleGlobToRegExp(glob: String): String {
|
||||||
var out = "" // "^"
|
var out = "" // "^"
|
||||||
for (i in 0 until glob.length) {
|
for (element in glob) {
|
||||||
val c = glob[i]
|
when (element) {
|
||||||
when (c) {
|
|
||||||
'*' -> out += ".*"
|
'*' -> out += ".*"
|
||||||
'?' -> out += '.'.toString()
|
'?' -> out += '.'.toString()
|
||||||
'.' -> out += "\\."
|
'.' -> out += "\\."
|
||||||
'\\' -> out += "\\\\"
|
'\\' -> out += "\\\\"
|
||||||
else -> out += c
|
else -> out += element
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out += "" // '$'.toString()
|
out += "" // '$'.toString()
|
||||||
|
|
|
@ -24,6 +24,7 @@ import im.vector.matrix.android.api.pushrules.PushRuleService
|
||||||
import im.vector.matrix.android.api.session.account.AccountService
|
import im.vector.matrix.android.api.session.account.AccountService
|
||||||
import im.vector.matrix.android.api.session.accountdata.AccountDataService
|
import im.vector.matrix.android.api.session.accountdata.AccountDataService
|
||||||
import im.vector.matrix.android.api.session.cache.CacheService
|
import im.vector.matrix.android.api.session.cache.CacheService
|
||||||
|
import im.vector.matrix.android.api.session.call.CallSignalingService
|
||||||
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
||||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||||
|
@ -165,6 +166,11 @@ interface Session :
|
||||||
*/
|
*/
|
||||||
fun integrationManagerService(): IntegrationManagerService
|
fun integrationManagerService(): IntegrationManagerService
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the call signaling service associated with the session
|
||||||
|
*/
|
||||||
|
fun callSignalingService(): CallSignalingService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a listener to the session.
|
* Add a listener to the session.
|
||||||
* @param listener the listener to add.
|
* @param listener the listener to add.
|
||||||
|
|
|
@ -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?
|
||||||
|
)
|
|
@ -58,7 +58,6 @@ object EventType {
|
||||||
const val STATE_ROOM_ENCRYPTION = "m.room.encryption"
|
const val STATE_ROOM_ENCRYPTION = "m.room.encryption"
|
||||||
|
|
||||||
// Call Events
|
// Call Events
|
||||||
|
|
||||||
const val CALL_INVITE = "m.call.invite"
|
const val CALL_INVITE = "m.call.invite"
|
||||||
const val CALL_CANDIDATES = "m.call.candidates"
|
const val CALL_CANDIDATES = "m.call.candidates"
|
||||||
const val CALL_ANSWER = "m.call.answer"
|
const val CALL_ANSWER = "m.call.answer"
|
||||||
|
|
|
@ -26,5 +26,5 @@ object RelationType {
|
||||||
/** Lets you define an event which references an existing event.*/
|
/** Lets you define an event which references an existing event.*/
|
||||||
const val REFERENCE = "m.reference"
|
const val REFERENCE = "m.reference"
|
||||||
/** Lets you define an event which adds a response to an existing event.*/
|
/** Lets you define an event which adds a response to an existing event.*/
|
||||||
const val RESPONSE = "m.response"
|
const val RESPONSE = "org.matrix.response"
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,12 +35,19 @@ interface ProfileService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the current dispayname for this user
|
* Return the current display name for this user
|
||||||
* @param userId the userId param to look for
|
* @param userId the userId param to look for
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
fun getDisplayName(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable
|
fun getDisplayName(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the display name for this user
|
||||||
|
* @param userId the userId to update the display name of
|
||||||
|
* @param newDisplayName the new display name of the user
|
||||||
|
*/
|
||||||
|
fun setDisplayName(userId: String, newDisplayName: String, matrixCallback: MatrixCallback<Unit>): Cancelable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the current avatarUrl for this user.
|
* Return the current avatarUrl for this user.
|
||||||
* @param userId the userId param to look for
|
* @param userId the userId param to look for
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
package im.vector.matrix.android.api.session.room
|
package im.vector.matrix.android.api.session.room
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
|
import im.vector.matrix.android.api.session.room.call.RoomCallService
|
||||||
import im.vector.matrix.android.api.session.room.crypto.RoomCryptoService
|
import im.vector.matrix.android.api.session.room.crypto.RoomCryptoService
|
||||||
import im.vector.matrix.android.api.session.room.members.MembershipService
|
import im.vector.matrix.android.api.session.room.members.MembershipService
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||||
|
@ -47,6 +48,7 @@ interface Room :
|
||||||
StateService,
|
StateService,
|
||||||
UploadsService,
|
UploadsService,
|
||||||
ReportingService,
|
ReportingService,
|
||||||
|
RoomCallService,
|
||||||
RelationService,
|
RelationService,
|
||||||
RoomCryptoService,
|
RoomCryptoService,
|
||||||
RoomPushRuleService {
|
RoomPushRuleService {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -62,6 +62,9 @@ data class RoomSummary constructor(
|
||||||
val isFavorite: Boolean
|
val isFavorite: Boolean
|
||||||
get() = tags.any { it.name == RoomTag.ROOM_TAG_FAVOURITE }
|
get() = tags.any { it.name == RoomTag.ROOM_TAG_FAVOURITE }
|
||||||
|
|
||||||
|
val canStartCall: Boolean
|
||||||
|
get() = isDirect && joinedMembersCount == 2
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val NOT_IN_BREADCRUMBS = -1
|
const val NOT_IN_BREADCRUMBS = -1
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,16 +19,34 @@ package im.vector.matrix.android.api.session.room.model.call
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This event is sent by the callee when they wish to answer the call.
|
||||||
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class CallAnswerContent(
|
data class CallAnswerContent(
|
||||||
|
/**
|
||||||
|
* Required. The ID of the call this event relates to.
|
||||||
|
*/
|
||||||
@Json(name = "call_id") val callId: String,
|
@Json(name = "call_id") val callId: String,
|
||||||
@Json(name = "version") val version: Int,
|
/**
|
||||||
@Json(name = "answer") val answer: Answer
|
* Required. The session description object
|
||||||
|
*/
|
||||||
|
@Json(name = "answer") val answer: Answer,
|
||||||
|
/**
|
||||||
|
* Required. The version of the VoIP specification this messages adheres to. This specification is version 0.
|
||||||
|
*/
|
||||||
|
@Json(name = "version") val version: Int = 0
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class Answer(
|
data class Answer(
|
||||||
@Json(name = "type") val type: String,
|
/**
|
||||||
|
* Required. The type of session description. Must be 'answer'.
|
||||||
|
*/
|
||||||
|
@Json(name = "type") val type: SdpType = SdpType.ANSWER,
|
||||||
|
/**
|
||||||
|
* Required. The SDP text of the session description.
|
||||||
|
*/
|
||||||
@Json(name = "sdp") val sdp: String
|
@Json(name = "sdp") val sdp: String
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,17 +19,39 @@ package im.vector.matrix.android.api.session.room.model.call
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This event is sent by callers after sending an invite and by the callee after answering.
|
||||||
|
* Its purpose is to give the other party additional ICE candidates to try using to communicate.
|
||||||
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class CallCandidatesContent(
|
data class CallCandidatesContent(
|
||||||
|
/**
|
||||||
|
* Required. The ID of the call this event relates to.
|
||||||
|
*/
|
||||||
@Json(name = "call_id") val callId: String,
|
@Json(name = "call_id") val callId: String,
|
||||||
@Json(name = "version") val version: Int,
|
/**
|
||||||
@Json(name = "candidates") val candidates: List<Candidate> = emptyList()
|
* Required. Array of objects describing the candidates.
|
||||||
|
*/
|
||||||
|
@Json(name = "candidates") val candidates: List<Candidate> = emptyList(),
|
||||||
|
/**
|
||||||
|
* Required. The version of the VoIP specification this messages adheres to. This specification is version 0.
|
||||||
|
*/
|
||||||
|
@Json(name = "version") val version: Int = 0
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class Candidate(
|
data class Candidate(
|
||||||
|
/**
|
||||||
|
* Required. The SDP media type this candidate is intended for.
|
||||||
|
*/
|
||||||
@Json(name = "sdpMid") val sdpMid: String,
|
@Json(name = "sdpMid") val sdpMid: String,
|
||||||
@Json(name = "sdpMLineIndex") val sdpMLineIndex: String,
|
/**
|
||||||
|
* Required. The index of the SDP 'm' line this candidate is intended for.
|
||||||
|
*/
|
||||||
|
@Json(name = "sdpMLineIndex") val sdpMLineIndex: Int,
|
||||||
|
/**
|
||||||
|
* Required. The SDP 'a' line of the candidate.
|
||||||
|
*/
|
||||||
@Json(name = "candidate") val candidate: String
|
@Json(name = "candidate") val candidate: String
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,8 +19,32 @@ package im.vector.matrix.android.api.session.room.model.call
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sent by either party to signal their termination of the call. This can be sent either once
|
||||||
|
* the call has been established or before to abort the call.
|
||||||
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class CallHangupContent(
|
data class CallHangupContent(
|
||||||
|
/**
|
||||||
|
* Required. The ID of the call this event relates to.
|
||||||
|
*/
|
||||||
@Json(name = "call_id") val callId: String,
|
@Json(name = "call_id") val callId: String,
|
||||||
@Json(name = "version") val version: Int
|
/**
|
||||||
)
|
* Required. The version of the VoIP specification this message adheres to. This specification is version 0.
|
||||||
|
*/
|
||||||
|
@Json(name = "version") val version: Int = 0,
|
||||||
|
/**
|
||||||
|
* Optional error reason for the hangup. This should not be provided when the user naturally ends or rejects the call.
|
||||||
|
* When there was an error in the call negotiation, this should be `ice_failed` for when ICE negotiation fails
|
||||||
|
* or `invite_timeout` for when the other party did not answer in time. One of: ["ice_failed", "invite_timeout"]
|
||||||
|
*/
|
||||||
|
@Json(name = "reason") val reason: Reason? = null
|
||||||
|
) {
|
||||||
|
enum class Reason {
|
||||||
|
@Json(name = "ice_failed")
|
||||||
|
ICE_FAILED,
|
||||||
|
|
||||||
|
@Json(name = "invite_timeout")
|
||||||
|
INVITE_TIMEOUT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -19,23 +19,45 @@ package im.vector.matrix.android.api.session.room.model.call
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This event is sent by the caller when they wish to establish a call.
|
||||||
|
*/
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class CallInviteContent(
|
data class CallInviteContent(
|
||||||
@Json(name = "call_id") val callId: String,
|
/**
|
||||||
@Json(name = "version") val version: Int,
|
* Required. A unique identifier for the call.
|
||||||
@Json(name = "lifetime") val lifetime: Int,
|
*/
|
||||||
@Json(name = "offer") val offer: Offer
|
@Json(name = "call_id") val callId: String?,
|
||||||
|
/**
|
||||||
|
* Required. The session description object
|
||||||
|
*/
|
||||||
|
@Json(name = "offer") val offer: Offer?,
|
||||||
|
/**
|
||||||
|
* Required. The version of the VoIP specification this message adheres to. This specification is version 0.
|
||||||
|
*/
|
||||||
|
@Json(name = "version") val version: Int? = 0,
|
||||||
|
/**
|
||||||
|
* Required. The time in milliseconds that the invite is valid for.
|
||||||
|
* Once the invite age exceeds this value, clients should discard it.
|
||||||
|
* They should also no longer show the call as awaiting an answer in the UI.
|
||||||
|
*/
|
||||||
|
@Json(name = "lifetime") val lifetime: Int?
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class Offer(
|
data class Offer(
|
||||||
@Json(name = "type") val type: String,
|
/**
|
||||||
@Json(name = "sdp") val sdp: String
|
* Required. The type of session description. Must be 'offer'.
|
||||||
|
*/
|
||||||
|
@Json(name = "type") val type: SdpType? = SdpType.OFFER,
|
||||||
|
/**
|
||||||
|
* Required. The SDP text of the session description.
|
||||||
|
*/
|
||||||
|
@Json(name = "sdp") val sdp: String?
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
const val SDP_VIDEO = "m=video"
|
const val SDP_VIDEO = "m=video"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isVideo(): Boolean = offer.sdp.contains(Offer.SDP_VIDEO)
|
fun isVideo(): Boolean = offer?.sdp?.contains(Offer.SDP_VIDEO) == true
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -67,6 +67,7 @@ import im.vector.matrix.android.internal.crypto.tasks.DefaultEncryptEventTask
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultGetDeviceInfoTask
|
import im.vector.matrix.android.internal.crypto.tasks.DefaultGetDeviceInfoTask
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultGetDevicesTask
|
import im.vector.matrix.android.internal.crypto.tasks.DefaultGetDevicesTask
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultInitializeCrossSigningTask
|
import im.vector.matrix.android.internal.crypto.tasks.DefaultInitializeCrossSigningTask
|
||||||
|
import im.vector.matrix.android.internal.crypto.tasks.DefaultSendEventTask
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultSendToDeviceTask
|
import im.vector.matrix.android.internal.crypto.tasks.DefaultSendToDeviceTask
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultSendVerificationMessageTask
|
import im.vector.matrix.android.internal.crypto.tasks.DefaultSendVerificationMessageTask
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultSetDeviceNameTask
|
import im.vector.matrix.android.internal.crypto.tasks.DefaultSetDeviceNameTask
|
||||||
|
@ -80,6 +81,7 @@ import im.vector.matrix.android.internal.crypto.tasks.EncryptEventTask
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.GetDeviceInfoTask
|
import im.vector.matrix.android.internal.crypto.tasks.GetDeviceInfoTask
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.GetDevicesTask
|
import im.vector.matrix.android.internal.crypto.tasks.GetDevicesTask
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.InitializeCrossSigningTask
|
import im.vector.matrix.android.internal.crypto.tasks.InitializeCrossSigningTask
|
||||||
|
import im.vector.matrix.android.internal.crypto.tasks.SendEventTask
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.SendVerificationMessageTask
|
import im.vector.matrix.android.internal.crypto.tasks.SendVerificationMessageTask
|
||||||
import im.vector.matrix.android.internal.crypto.tasks.SetDeviceNameTask
|
import im.vector.matrix.android.internal.crypto.tasks.SetDeviceNameTask
|
||||||
|
@ -251,4 +253,7 @@ internal abstract class CryptoModule {
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindInitializeCrossSigningTask(task: DefaultInitializeCrossSigningTask): InitializeCrossSigningTask
|
abstract fun bindInitializeCrossSigningTask(task: DefaultInitializeCrossSigningTask): InitializeCrossSigningTask
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindSendEventTask(task: DefaultSendEventTask): SendEventTask
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
object HkdfSha256 {
|
||||||
|
|
||||||
public fun deriveSecret(inputKeyMaterial: ByteArray, salt: ByteArray?, info: ByteArray, outputLength: Int): ByteArray {
|
fun deriveSecret(inputKeyMaterial: ByteArray, salt: ByteArray?, info: ByteArray, outputLength: Int): ByteArray {
|
||||||
return expand(extract(salt, inputKeyMaterial), info, outputLength)
|
return expand(extract(salt, inputKeyMaterial), info, outputLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -273,7 +273,7 @@ internal abstract class SASDefaultVerificationTransaction(
|
||||||
if (keyIDNoPrefix == otherCrossSigningMasterKeyPublic) {
|
if (keyIDNoPrefix == otherCrossSigningMasterKeyPublic) {
|
||||||
// Check the signature
|
// Check the signature
|
||||||
val mac = macUsingAgreedMethod(otherCrossSigningMasterKeyPublic, baseInfo + it)
|
val mac = macUsingAgreedMethod(otherCrossSigningMasterKeyPublic, baseInfo + it)
|
||||||
if (mac != theirMacSafe.mac.get(it)) {
|
if (mac != theirMacSafe.mac[it]) {
|
||||||
// WRONG!
|
// WRONG!
|
||||||
Timber.e("## SAS Verification: mac mismatch for MasterKey with id $keyIDNoPrefix")
|
Timber.e("## SAS Verification: mac mismatch for MasterKey with id $keyIDNoPrefix")
|
||||||
cancel(CancelCode.MismatchedKeys)
|
cancel(CancelCode.MismatchedKeys)
|
||||||
|
|
|
@ -85,7 +85,7 @@ internal class SendVerificationMessageWorker(context: Context,
|
||||||
private const val OUTPUT_KEY_FAILED = "failed"
|
private const val OUTPUT_KEY_FAILED = "failed"
|
||||||
|
|
||||||
fun hasFailed(outputData: Data): Boolean {
|
fun hasFailed(outputData: Data): Boolean {
|
||||||
return outputData.getBoolean(SendVerificationMessageWorker.OUTPUT_KEY_FAILED, false)
|
return outputData.getBoolean(OUTPUT_KEY_FAILED, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ internal object TimelineEventFilter {
|
||||||
*/
|
*/
|
||||||
internal object Content {
|
internal object Content {
|
||||||
internal const val EDIT = """{*"m.relates_to"*"rel_type":*"m.replace"*}"""
|
internal const val EDIT = """{*"m.relates_to"*"rel_type":*"m.replace"*}"""
|
||||||
internal const val RESPONSE = """{*"m.relates_to"*"rel_type":*"m.response"*}"""
|
internal const val RESPONSE = """{*"m.relates_to"*"rel_type":*"org.matrix.response"*}"""
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2019 New Vector Ltd
|
* Copyright 2020 New Vector Ltd
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -22,12 +22,13 @@ import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
|
import retrofit2.awaitResponse
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
internal suspend inline fun <DATA> executeRequest(eventBus: EventBus?,
|
internal suspend inline fun <DATA : Any> executeRequest(eventBus: EventBus?,
|
||||||
block: Request<DATA>.() -> Unit) = Request<DATA>(eventBus).apply(block).execute()
|
block: Request<DATA>.() -> Unit) = Request<DATA>(eventBus).apply(block).execute()
|
||||||
|
|
||||||
internal class Request<DATA>(private val eventBus: EventBus?) {
|
internal class Request<DATA : Any>(private val eventBus: EventBus?) {
|
||||||
|
|
||||||
var isRetryable = false
|
var isRetryable = false
|
||||||
var initialDelay: Long = 100L
|
var initialDelay: Long = 100L
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
*
|
*
|
||||||
* * Copyright 2019 New Vector Ltd
|
* * Copyright 2020 New Vector Ltd
|
||||||
* *
|
* *
|
||||||
* * Licensed under the Apache License, Version 2.0 (the "License");
|
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* * you may not use this file except in compliance with the License.
|
* * you may not use this file except in compliance with the License.
|
||||||
|
@ -26,8 +26,6 @@ import im.vector.matrix.android.internal.di.MoshiProvider
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import okhttp3.ResponseBody
|
import okhttp3.ResponseBody
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
import retrofit2.Call
|
|
||||||
import retrofit2.Callback
|
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
@ -35,23 +33,6 @@ import java.net.HttpURLConnection
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.resumeWithException
|
import kotlin.coroutines.resumeWithException
|
||||||
|
|
||||||
internal suspend fun <T> Call<T>.awaitResponse(): Response<T> {
|
|
||||||
return suspendCancellableCoroutine { continuation ->
|
|
||||||
continuation.invokeOnCancellation {
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
enqueue(object : Callback<T> {
|
|
||||||
override fun onResponse(call: Call<T>, response: Response<T>) {
|
|
||||||
continuation.resume(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<T>, t: Throwable) {
|
|
||||||
continuation.resumeWithException(t)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal suspend fun okhttp3.Call.awaitResponse(): okhttp3.Response {
|
internal suspend fun okhttp3.Call.awaitResponse(): okhttp3.Response {
|
||||||
return suspendCancellableCoroutine { continuation ->
|
return suspendCancellableCoroutine { continuation ->
|
||||||
continuation.invokeOnCancellation {
|
continuation.invokeOnCancellation {
|
||||||
|
|
|
@ -33,7 +33,7 @@ data class Fingerprint(
|
||||||
|
|
||||||
@Throws(CertificateException::class)
|
@Throws(CertificateException::class)
|
||||||
fun matchesCert(cert: X509Certificate): Boolean {
|
fun matchesCert(cert: X509Certificate): Boolean {
|
||||||
var o: Fingerprint? = when (hashType) {
|
val o: Fingerprint? = when (hashType) {
|
||||||
HashType.SHA256 -> newSha256Fingerprint(cert)
|
HashType.SHA256 -> newSha256Fingerprint(cert)
|
||||||
HashType.SHA1 -> newSha1Fingerprint(cert)
|
HashType.SHA1 -> newSha1Fingerprint(cert)
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ import im.vector.matrix.android.api.session.Session
|
||||||
import im.vector.matrix.android.api.session.account.AccountService
|
import im.vector.matrix.android.api.session.account.AccountService
|
||||||
import im.vector.matrix.android.api.session.accountdata.AccountDataService
|
import im.vector.matrix.android.api.session.accountdata.AccountDataService
|
||||||
import im.vector.matrix.android.api.session.cache.CacheService
|
import im.vector.matrix.android.api.session.cache.CacheService
|
||||||
|
import im.vector.matrix.android.api.session.call.CallSignalingService
|
||||||
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
||||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||||
|
@ -110,7 +111,8 @@ internal class DefaultSession @Inject constructor(
|
||||||
private val integrationManagerService: IntegrationManagerService,
|
private val integrationManagerService: IntegrationManagerService,
|
||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
private val widgetDependenciesHolder: WidgetDependenciesHolder,
|
private val widgetDependenciesHolder: WidgetDependenciesHolder,
|
||||||
private val shieldTrustUpdater: ShieldTrustUpdater)
|
private val shieldTrustUpdater: ShieldTrustUpdater,
|
||||||
|
private val callSignalingService: Lazy<CallSignalingService>)
|
||||||
: Session,
|
: Session,
|
||||||
RoomService by roomService.get(),
|
RoomService by roomService.get(),
|
||||||
RoomDirectoryService by roomDirectoryService.get(),
|
RoomDirectoryService by roomDirectoryService.get(),
|
||||||
|
@ -245,6 +247,8 @@ internal class DefaultSession @Inject constructor(
|
||||||
|
|
||||||
override fun integrationManagerService() = integrationManagerService
|
override fun integrationManagerService() = integrationManagerService
|
||||||
|
|
||||||
|
override fun callSignalingService(): CallSignalingService = callSignalingService.get()
|
||||||
|
|
||||||
override fun addListener(listener: Session.Listener) {
|
override fun addListener(listener: Session.Listener) {
|
||||||
sessionListeners.addListener(listener)
|
sessionListeners.addListener(listener)
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.di.SessionAssistedInjectModule
|
||||||
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
|
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
|
||||||
import im.vector.matrix.android.internal.session.account.AccountModule
|
import im.vector.matrix.android.internal.session.account.AccountModule
|
||||||
import im.vector.matrix.android.internal.session.cache.CacheModule
|
import im.vector.matrix.android.internal.session.cache.CacheModule
|
||||||
|
import im.vector.matrix.android.internal.session.call.CallModule
|
||||||
import im.vector.matrix.android.internal.session.content.ContentModule
|
import im.vector.matrix.android.internal.session.content.ContentModule
|
||||||
import im.vector.matrix.android.internal.session.content.UploadContentWorker
|
import im.vector.matrix.android.internal.session.content.UploadContentWorker
|
||||||
import im.vector.matrix.android.internal.session.filter.FilterModule
|
import im.vector.matrix.android.internal.session.filter.FilterModule
|
||||||
|
@ -83,7 +84,8 @@ import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||||
AccountDataModule::class,
|
AccountDataModule::class,
|
||||||
ProfileModule::class,
|
ProfileModule::class,
|
||||||
SessionAssistedInjectModule::class,
|
SessionAssistedInjectModule::class,
|
||||||
AccountModule::class
|
AccountModule::class,
|
||||||
|
CallModule::class
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@SessionScope
|
@SessionScope
|
||||||
|
|
|
@ -59,6 +59,7 @@ import im.vector.matrix.android.internal.network.RetrofitFactory
|
||||||
import im.vector.matrix.android.internal.network.httpclient.addAccessTokenInterceptor
|
import im.vector.matrix.android.internal.network.httpclient.addAccessTokenInterceptor
|
||||||
import im.vector.matrix.android.internal.network.token.AccessTokenProvider
|
import im.vector.matrix.android.internal.network.token.AccessTokenProvider
|
||||||
import im.vector.matrix.android.internal.network.token.HomeserverAccessTokenProvider
|
import im.vector.matrix.android.internal.network.token.HomeserverAccessTokenProvider
|
||||||
|
import im.vector.matrix.android.internal.session.call.CallEventObserver
|
||||||
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
|
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
|
||||||
import im.vector.matrix.android.internal.session.homeserver.DefaultHomeServerCapabilitiesService
|
import im.vector.matrix.android.internal.session.homeserver.DefaultHomeServerCapabilitiesService
|
||||||
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
|
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
|
||||||
|
@ -243,6 +244,10 @@ internal abstract class SessionModule {
|
||||||
@IntoSet
|
@IntoSet
|
||||||
abstract fun bindEventRelationsAggregationUpdater(updater: EventRelationsAggregationUpdater): LiveEntityObserver
|
abstract fun bindEventRelationsAggregationUpdater(updater: EventRelationsAggregationUpdater): LiveEntityObserver
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoSet
|
||||||
|
abstract fun bindCallEventObserver(observer: CallEventObserver): LiveEntityObserver
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoSet
|
@IntoSet
|
||||||
abstract fun bindRoomTombstoneEventLiveObserver(observer: RoomTombstoneEventLiveObserver): LiveEntityObserver
|
abstract fun bindRoomTombstoneEventLiveObserver(observer: RoomTombstoneEventLiveObserver): LiveEntityObserver
|
||||||
|
|
|
@ -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,91 @@
|
||||||
|
/*
|
||||||
|
* 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.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(
|
||||||
|
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) }
|
||||||
|
}
|
||||||
|
}
|
|
@ -34,7 +34,8 @@ import javax.inject.Inject
|
||||||
internal class DefaultProfileService @Inject constructor(private val taskExecutor: TaskExecutor,
|
internal class DefaultProfileService @Inject constructor(private val taskExecutor: TaskExecutor,
|
||||||
private val monarchy: Monarchy,
|
private val monarchy: Monarchy,
|
||||||
private val refreshUserThreePidsTask: RefreshUserThreePidsTask,
|
private val refreshUserThreePidsTask: RefreshUserThreePidsTask,
|
||||||
private val getProfileInfoTask: GetProfileInfoTask) : ProfileService {
|
private val getProfileInfoTask: GetProfileInfoTask,
|
||||||
|
private val setDisplayNameTask: SetDisplayNameTask) : ProfileService {
|
||||||
|
|
||||||
override fun getDisplayName(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable {
|
override fun getDisplayName(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable {
|
||||||
val params = GetProfileInfoTask.Params(userId)
|
val params = GetProfileInfoTask.Params(userId)
|
||||||
|
@ -54,6 +55,14 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
|
||||||
.executeBy(taskExecutor)
|
.executeBy(taskExecutor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setDisplayName(userId: String, newDisplayName: String, matrixCallback: MatrixCallback<Unit>): Cancelable {
|
||||||
|
return setDisplayNameTask
|
||||||
|
.configureWith(SetDisplayNameTask.Params(userId = userId, newDisplayName = newDisplayName)) {
|
||||||
|
callback = matrixCallback
|
||||||
|
}
|
||||||
|
.executeBy(taskExecutor)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getAvatarUrl(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable {
|
override fun getAvatarUrl(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable {
|
||||||
val params = GetProfileInfoTask.Params(userId)
|
val params = GetProfileInfoTask.Params(userId)
|
||||||
return getProfileInfoTask
|
return getProfileInfoTask
|
||||||
|
|
|
@ -23,6 +23,7 @@ import retrofit2.Call
|
||||||
import retrofit2.http.Body
|
import retrofit2.http.Body
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
import retrofit2.http.POST
|
import retrofit2.http.POST
|
||||||
|
import retrofit2.http.PUT
|
||||||
import retrofit2.http.Path
|
import retrofit2.http.Path
|
||||||
|
|
||||||
internal interface ProfileAPI {
|
internal interface ProfileAPI {
|
||||||
|
@ -42,6 +43,12 @@ internal interface ProfileAPI {
|
||||||
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid")
|
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid")
|
||||||
fun getThreePIDs(): Call<AccountThreePidsResponse>
|
fun getThreePIDs(): Call<AccountThreePidsResponse>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change user display name
|
||||||
|
*/
|
||||||
|
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "profile/{userId}/displayname")
|
||||||
|
fun setDisplayName(@Path("userId") userId: String, @Body body: SetDisplayNameBody): Call<Unit>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bind a threePid
|
* Bind a threePid
|
||||||
* Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-account-3pid-bind
|
* Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-account-3pid-bind
|
||||||
|
|
|
@ -51,4 +51,7 @@ internal abstract class ProfileModule {
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindUnbindThreePidsTask(task: DefaultUnbindThreePidsTask): UnbindThreePidsTask
|
abstract fun bindUnbindThreePidsTask(task: DefaultUnbindThreePidsTask): UnbindThreePidsTask
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindSetDisplayNameTask(task: DefaultSetDisplayNameTask): SetDisplayNameTask
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,6 +23,7 @@ import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
import im.vector.matrix.android.api.session.room.Room
|
import im.vector.matrix.android.api.session.room.Room
|
||||||
|
import im.vector.matrix.android.api.session.room.call.RoomCallService
|
||||||
import im.vector.matrix.android.api.session.room.members.MembershipService
|
import im.vector.matrix.android.api.session.room.members.MembershipService
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||||
import im.vector.matrix.android.api.session.room.model.relation.RelationService
|
import im.vector.matrix.android.api.session.room.model.relation.RelationService
|
||||||
|
@ -58,6 +59,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
|
||||||
private val stateService: StateService,
|
private val stateService: StateService,
|
||||||
private val uploadsService: UploadsService,
|
private val uploadsService: UploadsService,
|
||||||
private val reportingService: ReportingService,
|
private val reportingService: ReportingService,
|
||||||
|
private val roomCallService: RoomCallService,
|
||||||
private val readService: ReadService,
|
private val readService: ReadService,
|
||||||
private val typingService: TypingService,
|
private val typingService: TypingService,
|
||||||
private val tagsService: TagsService,
|
private val tagsService: TagsService,
|
||||||
|
@ -74,6 +76,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
|
||||||
StateService by stateService,
|
StateService by stateService,
|
||||||
UploadsService by uploadsService,
|
UploadsService by uploadsService,
|
||||||
ReportingService by reportingService,
|
ReportingService by reportingService,
|
||||||
|
RoomCallService by roomCallService,
|
||||||
ReadService by readService,
|
ReadService by readService,
|
||||||
TypingService by typingService,
|
TypingService by typingService,
|
||||||
TagsService by tagsService,
|
TagsService by tagsService,
|
||||||
|
|
|
@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||||
import im.vector.matrix.android.api.session.room.Room
|
import im.vector.matrix.android.api.session.room.Room
|
||||||
import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper
|
import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper
|
||||||
import im.vector.matrix.android.internal.session.SessionScope
|
import im.vector.matrix.android.internal.session.SessionScope
|
||||||
|
import im.vector.matrix.android.internal.session.room.call.DefaultRoomCallService
|
||||||
import im.vector.matrix.android.internal.session.room.draft.DefaultDraftService
|
import im.vector.matrix.android.internal.session.room.draft.DefaultDraftService
|
||||||
import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService
|
import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService
|
||||||
import im.vector.matrix.android.internal.session.room.notification.DefaultRoomPushRuleService
|
import im.vector.matrix.android.internal.session.room.notification.DefaultRoomPushRuleService
|
||||||
|
@ -51,6 +52,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona
|
||||||
private val stateServiceFactory: DefaultStateService.Factory,
|
private val stateServiceFactory: DefaultStateService.Factory,
|
||||||
private val uploadsServiceFactory: DefaultUploadsService.Factory,
|
private val uploadsServiceFactory: DefaultUploadsService.Factory,
|
||||||
private val reportingServiceFactory: DefaultReportingService.Factory,
|
private val reportingServiceFactory: DefaultReportingService.Factory,
|
||||||
|
private val roomCallServiceFactory: DefaultRoomCallService.Factory,
|
||||||
private val readServiceFactory: DefaultReadService.Factory,
|
private val readServiceFactory: DefaultReadService.Factory,
|
||||||
private val typingServiceFactory: DefaultTypingService.Factory,
|
private val typingServiceFactory: DefaultTypingService.Factory,
|
||||||
private val tagsServiceFactory: DefaultTagsService.Factory,
|
private val tagsServiceFactory: DefaultTagsService.Factory,
|
||||||
|
@ -72,6 +74,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona
|
||||||
stateService = stateServiceFactory.create(roomId),
|
stateService = stateServiceFactory.create(roomId),
|
||||||
uploadsService = uploadsServiceFactory.create(roomId),
|
uploadsService = uploadsServiceFactory.create(roomId),
|
||||||
reportingService = reportingServiceFactory.create(roomId),
|
reportingService = reportingServiceFactory.create(roomId),
|
||||||
|
roomCallService = roomCallServiceFactory.create(roomId),
|
||||||
readService = readServiceFactory.create(roomId),
|
readService = readServiceFactory.create(roomId),
|
||||||
typingService = typingServiceFactory.create(roomId),
|
typingService = typingServiceFactory.create(roomId),
|
||||||
tagsService = tagsServiceFactory.create(roomId),
|
tagsService = tagsServiceFactory.create(roomId),
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -51,7 +51,7 @@ internal class DefaultRoomPushRuleService @AssistedInject constructor(@Assisted
|
||||||
override fun setRoomNotificationState(roomNotificationState: RoomNotificationState, matrixCallback: MatrixCallback<Unit>): Cancelable {
|
override fun setRoomNotificationState(roomNotificationState: RoomNotificationState, matrixCallback: MatrixCallback<Unit>): Cancelable {
|
||||||
return setRoomNotificationStateTask
|
return setRoomNotificationStateTask
|
||||||
.configureWith(SetRoomNotificationStateTask.Params(roomId, roomNotificationState)) {
|
.configureWith(SetRoomNotificationStateTask.Params(roomId, roomNotificationState)) {
|
||||||
this.callback = callback
|
this.callback = matrixCallback
|
||||||
}
|
}
|
||||||
.executeBy(taskExecutor)
|
.executeBy(taskExecutor)
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,7 +58,8 @@ internal class DefaultSendService @AssistedInject constructor(
|
||||||
private val localEchoEventFactory: LocalEchoEventFactory,
|
private val localEchoEventFactory: LocalEchoEventFactory,
|
||||||
private val cryptoService: CryptoService,
|
private val cryptoService: CryptoService,
|
||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
private val localEchoRepository: LocalEchoRepository
|
private val localEchoRepository: LocalEchoRepository,
|
||||||
|
private val roomEventSender: RoomEventSender
|
||||||
) : SendService {
|
) : SendService {
|
||||||
|
|
||||||
@AssistedInject.Factory
|
@AssistedInject.Factory
|
||||||
|
@ -111,20 +112,6 @@ internal class DefaultSendService @AssistedInject constructor(
|
||||||
.let { sendEvent(it) }
|
.let { sendEvent(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendEvent(event: Event): Cancelable {
|
|
||||||
// Encrypted room handling
|
|
||||||
return if (cryptoService.isRoomEncrypted(roomId)) {
|
|
||||||
Timber.v("Send event in encrypted room")
|
|
||||||
val encryptWork = createEncryptEventWork(event, true)
|
|
||||||
// Note that event will be replaced by the result of the previous work
|
|
||||||
val sendWork = createSendEventWork(event, false)
|
|
||||||
timelineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, sendWork)
|
|
||||||
} else {
|
|
||||||
val sendWork = createSendEventWork(event, true)
|
|
||||||
timelineSendEventWorkCommon.postWork(roomId, sendWork)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun sendMedias(attachments: List<ContentAttachmentData>,
|
override fun sendMedias(attachments: List<ContentAttachmentData>,
|
||||||
compressBeforeSending: Boolean,
|
compressBeforeSending: Boolean,
|
||||||
roomIds: Set<String>): Cancelable {
|
roomIds: Set<String>): Cancelable {
|
||||||
|
@ -269,6 +256,10 @@ internal class DefaultSendService @AssistedInject constructor(
|
||||||
return cancelableBag
|
return cancelableBag
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun sendEvent(event: Event): Cancelable {
|
||||||
|
return roomEventSender.sendEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
private fun createLocalEcho(event: Event) {
|
private fun createLocalEcho(event: Event) {
|
||||||
localEchoEventFactory.createLocalEcho(event)
|
localEchoEventFactory.createLocalEcho(event)
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ internal class MarkdownParser @Inject constructor(
|
||||||
fun parse(text: String): TextContent {
|
fun parse(text: String): TextContent {
|
||||||
// If no special char are detected, just return plain text
|
// If no special char are detected, just return plain text
|
||||||
if (text.contains(mdSpecialChars).not()) {
|
if (text.contains(mdSpecialChars).not()) {
|
||||||
return TextContent(text.toString())
|
return TextContent(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
val document = parser.parse(text)
|
val document = parser.parse(text)
|
||||||
|
@ -56,7 +56,7 @@ internal class MarkdownParser @Inject constructor(
|
||||||
val plainText = textContentRenderer.render(document)
|
val plainText = textContentRenderer.render(document)
|
||||||
TextContent(plainText, cleanHtmlText.postTreatment())
|
TextContent(plainText, cleanHtmlText.postTreatment())
|
||||||
} else {
|
} else {
|
||||||
TextContent(text.toString())
|
TextContent(text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,8 +33,8 @@
|
||||||
<string name="notice_display_name_set_by_you">You set your display name to %1$s</string>
|
<string name="notice_display_name_set_by_you">You set your display name to %1$s</string>
|
||||||
<string name="notice_display_name_changed_from">%1$s changed their display name from %2$s to %3$s</string>
|
<string name="notice_display_name_changed_from">%1$s changed their display name from %2$s to %3$s</string>
|
||||||
<string name="notice_display_name_changed_from_by_you">You changed your display name from %1$s to %2$s</string>
|
<string name="notice_display_name_changed_from_by_you">You changed your display name from %1$s to %2$s</string>
|
||||||
<string name="notice_display_name_removed">%1$s removed their display name (%2$s)</string>
|
<string name="notice_display_name_removed">%1$s removed their display name (it was %2$s)</string>
|
||||||
<string name="notice_display_name_removed_by_you">You removed your display name (%1$s)</string>
|
<string name="notice_display_name_removed_by_you">You removed your display name (it was %1$s)</string>
|
||||||
<string name="notice_room_topic_changed">%1$s changed the topic to: %2$s</string>
|
<string name="notice_room_topic_changed">%1$s changed the topic to: %2$s</string>
|
||||||
<string name="notice_room_topic_changed_by_you">You changed the topic to: %1$s</string>
|
<string name="notice_room_topic_changed_by_you">You changed the topic to: %1$s</string>
|
||||||
<string name="notice_room_name_changed">%1$s changed the room name to: %2$s</string>
|
<string name="notice_room_name_changed">%1$s changed the room name to: %2$s</string>
|
||||||
|
@ -43,6 +43,8 @@
|
||||||
<string name="notice_placed_video_call_by_you">You placed a video call.</string>
|
<string name="notice_placed_video_call_by_you">You placed a video call.</string>
|
||||||
<string name="notice_placed_voice_call">%s placed a voice call.</string>
|
<string name="notice_placed_voice_call">%s placed a voice call.</string>
|
||||||
<string name="notice_placed_voice_call_by_you">You placed a voice call.</string>
|
<string name="notice_placed_voice_call_by_you">You placed a voice call.</string>
|
||||||
|
<string name="notice_call_candidates">%s sent data to setup the call.</string>
|
||||||
|
<string name="notice_call_candidates_by_you">You sent data to setup the call.</string>
|
||||||
<string name="notice_answered_call">%s answered the call.</string>
|
<string name="notice_answered_call">%s answered the call.</string>
|
||||||
<string name="notice_answered_call_by_you">You answered the call.</string>
|
<string name="notice_answered_call_by_you">You answered the call.</string>
|
||||||
<string name="notice_ended_call">%s ended the call.</string>
|
<string name="notice_ended_call">%s ended the call.</string>
|
||||||
|
@ -362,4 +364,8 @@
|
||||||
|
|
||||||
<string name="key_verification_request_fallback_message">%s is requesting to verify your key, but your client does not support in-chat key verification. You will need to use legacy key verification to verify keys.</string>
|
<string name="key_verification_request_fallback_message">%s is requesting to verify your key, but your client does not support in-chat key verification. You will need to use legacy key verification to verify keys.</string>
|
||||||
|
|
||||||
|
<string name="call_notification_answer">Accept</string>
|
||||||
|
<string name="call_notification_reject">Decline</string>
|
||||||
|
<string name="call_notification_hangup">Hang Up</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -62,9 +62,9 @@ class ContactPicker(override val requestCode: Int) : Picker<MultiPickerContactTy
|
||||||
|
|
||||||
val contactId = cursor.getInt(idColumn)
|
val contactId = cursor.getInt(idColumn)
|
||||||
var name = cursor.getString(nameColumn)
|
var name = cursor.getString(nameColumn)
|
||||||
var photoUri = cursor.getString(photoUriColumn)
|
val photoUri = cursor.getString(photoUriColumn)
|
||||||
var phoneNumberList = mutableListOf<String>()
|
val phoneNumberList = mutableListOf<String>()
|
||||||
var emailList = mutableListOf<String>()
|
val emailList = mutableListOf<String>()
|
||||||
|
|
||||||
getRawContactId(context.contentResolver, contactId)?.let { rawContactId ->
|
getRawContactId(context.contentResolver, contactId)?.let { rawContactId ->
|
||||||
val selection = ContactsContract.Data.RAW_CONTACT_ID + " = ?"
|
val selection = ContactsContract.Data.RAW_CONTACT_ID + " = ?"
|
||||||
|
|
|
@ -390,6 +390,9 @@ dependencies {
|
||||||
|
|
||||||
implementation 'com.github.BillCarsonFr:JsonViewer:0.5'
|
implementation 'com.github.BillCarsonFr:JsonViewer:0.5'
|
||||||
|
|
||||||
|
// TODO meant for development purposes only
|
||||||
|
implementation 'org.webrtc:google-webrtc:1.0.+'
|
||||||
|
|
||||||
// QR-code
|
// QR-code
|
||||||
// Stick to 3.3.3 because of https://github.com/zxing/zxing/issues/1170
|
// Stick to 3.3.3 because of https://github.com/zxing/zxing/issues/1170
|
||||||
implementation 'com.google.zxing:core:3.3.3'
|
implementation 'com.google.zxing:core:3.3.3'
|
||||||
|
|
|
@ -3,13 +3,30 @@
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="im.vector.riotx">
|
package="im.vector.riotx">
|
||||||
|
|
||||||
|
<!-- Needed for VOIP call to detect and switch to headset-->
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
|
<!-- Call feature -->
|
||||||
|
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
|
||||||
|
<uses-permission android:name="android.permission.READ_CALL_LOG" />
|
||||||
|
<!-- Needed for voice call to toggle speaker on or off -->
|
||||||
|
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||||
|
<!-- READ_PHONE_STATE is needed only if your calling app reads numbers from the `PHONE_STATE`
|
||||||
|
intent action. -->
|
||||||
|
|
||||||
|
<!-- Needed to show incoming calls activity in lock screen-->
|
||||||
|
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
<!-- Needed for incoming calls -->
|
||||||
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||||
|
|
||||||
<!-- Adding CAMERA permission prevents Chromebooks to see the application on the PlayStore -->
|
<!-- Adding CAMERA permission prevents Chromebooks to see the application on the PlayStore -->
|
||||||
<!-- Tell that the Camera is not mandatory to install the application -->
|
<!-- Tell that the Camera is not mandatory to install the application -->
|
||||||
<uses-feature
|
<uses-feature
|
||||||
|
@ -172,6 +189,7 @@
|
||||||
<activity
|
<activity
|
||||||
android:name=".features.attachments.preview.AttachmentsPreviewActivity"
|
android:name=".features.attachments.preview.AttachmentsPreviewActivity"
|
||||||
android:theme="@style/AppTheme.AttachmentsPreview" />
|
android:theme="@style/AppTheme.AttachmentsPreview" />
|
||||||
|
<activity android:name=".features.call.VectorCallActivity" />
|
||||||
|
|
||||||
<activity android:name=".features.terms.ReviewTermsActivity" />
|
<activity android:name=".features.terms.ReviewTermsActivity" />
|
||||||
<activity android:name=".features.widgets.WidgetActivity" />
|
<activity android:name=".features.widgets.WidgetActivity" />
|
||||||
|
@ -180,20 +198,47 @@
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".core.services.CallService"
|
android:name=".core.services.CallService"
|
||||||
android:exported="false" />
|
android:exported="false" >
|
||||||
|
<!-- in order to get headset button events -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".core.services.VectorSyncService"
|
android:name=".core.services.VectorSyncService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".features.call.telecom.VectorConnectionService"
|
||||||
|
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.telecom.ConnectionService" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
<!-- Receivers -->
|
<!-- Receivers -->
|
||||||
|
|
||||||
|
<receiver
|
||||||
|
android:name=".features.call.service.CallHeadsUpActionReceiver"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<!-- Exported false, should only be accessible from this app!! -->
|
<!-- Exported false, should only be accessible from this app!! -->
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".features.notifications.NotificationBroadcastReceiver"
|
android:name=".features.notifications.NotificationBroadcastReceiver"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
|
<!--
|
||||||
|
A media button receiver receives and helps translate hardware media playback buttons,
|
||||||
|
such as those found on wired and wireless headsets, into the appropriate callbacks in your app.
|
||||||
|
-->
|
||||||
|
<receiver android:name="androidx.media.session.MediaButtonReceiver" >
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<!-- Providers -->
|
<!-- Providers -->
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
|
|
|
@ -43,6 +43,7 @@ import im.vector.riotx.core.di.HasVectorInjector
|
||||||
import im.vector.riotx.core.di.VectorComponent
|
import im.vector.riotx.core.di.VectorComponent
|
||||||
import im.vector.riotx.core.extensions.configureAndStart
|
import im.vector.riotx.core.extensions.configureAndStart
|
||||||
import im.vector.riotx.core.rx.RxConfig
|
import im.vector.riotx.core.rx.RxConfig
|
||||||
|
import im.vector.riotx.features.call.WebRtcPeerConnectionManager
|
||||||
import im.vector.riotx.features.configuration.VectorConfiguration
|
import im.vector.riotx.features.configuration.VectorConfiguration
|
||||||
import im.vector.riotx.features.lifecycle.VectorActivityLifecycleCallbacks
|
import im.vector.riotx.features.lifecycle.VectorActivityLifecycleCallbacks
|
||||||
import im.vector.riotx.features.notifications.NotificationDrawerManager
|
import im.vector.riotx.features.notifications.NotificationDrawerManager
|
||||||
|
@ -80,6 +81,8 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
|
||||||
@Inject lateinit var appStateHandler: AppStateHandler
|
@Inject lateinit var appStateHandler: AppStateHandler
|
||||||
@Inject lateinit var rxConfig: RxConfig
|
@Inject lateinit var rxConfig: RxConfig
|
||||||
@Inject lateinit var popupAlertManager: PopupAlertManager
|
@Inject lateinit var popupAlertManager: PopupAlertManager
|
||||||
|
@Inject lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager
|
||||||
|
|
||||||
lateinit var vectorComponent: VectorComponent
|
lateinit var vectorComponent: VectorComponent
|
||||||
private var fontThreadHandler: Handler? = null
|
private var fontThreadHandler: Handler? = null
|
||||||
|
|
||||||
|
@ -122,6 +125,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
|
||||||
val lastAuthenticatedSession = authenticationService.getLastAuthenticatedSession()!!
|
val lastAuthenticatedSession = authenticationService.getLastAuthenticatedSession()!!
|
||||||
activeSessionHolder.setActiveSession(lastAuthenticatedSession)
|
activeSessionHolder.setActiveSession(lastAuthenticatedSession)
|
||||||
lastAuthenticatedSession.configureAndStart(applicationContext, pushRuleTriggerListener, sessionListener)
|
lastAuthenticatedSession.configureAndStart(applicationContext, pushRuleTriggerListener, sessionListener)
|
||||||
|
lastAuthenticatedSession.callSignalingService().addCallListener(webRtcPeerConnectionManager)
|
||||||
}
|
}
|
||||||
ProcessLifecycleOwner.get().lifecycle.addObserver(object : LifecycleObserver {
|
ProcessLifecycleOwner.get().lifecycle.addObserver(object : LifecycleObserver {
|
||||||
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||||
|
|
|
@ -24,6 +24,8 @@ import dagger.Component
|
||||||
import im.vector.riotx.core.error.ErrorFormatter
|
import im.vector.riotx.core.error.ErrorFormatter
|
||||||
import im.vector.riotx.core.preference.UserAvatarPreference
|
import im.vector.riotx.core.preference.UserAvatarPreference
|
||||||
import im.vector.riotx.features.MainActivity
|
import im.vector.riotx.features.MainActivity
|
||||||
|
import im.vector.riotx.features.call.CallControlsBottomSheet
|
||||||
|
import im.vector.riotx.features.call.VectorCallActivity
|
||||||
import im.vector.riotx.features.createdirect.CreateDirectRoomActivity
|
import im.vector.riotx.features.createdirect.CreateDirectRoomActivity
|
||||||
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
||||||
import im.vector.riotx.features.crypto.quads.SharedSecureStorageActivity
|
import im.vector.riotx.features.crypto.quads.SharedSecureStorageActivity
|
||||||
|
@ -130,6 +132,7 @@ interface ScreenComponent {
|
||||||
fun inject(activity: InviteUsersToRoomActivity)
|
fun inject(activity: InviteUsersToRoomActivity)
|
||||||
fun inject(activity: ReviewTermsActivity)
|
fun inject(activity: ReviewTermsActivity)
|
||||||
fun inject(activity: WidgetActivity)
|
fun inject(activity: WidgetActivity)
|
||||||
|
fun inject(activity: VectorCallActivity)
|
||||||
|
|
||||||
/* ==========================================================================================
|
/* ==========================================================================================
|
||||||
* BottomSheets
|
* BottomSheets
|
||||||
|
@ -146,6 +149,7 @@ interface ScreenComponent {
|
||||||
fun inject(bottomSheet: BootstrapBottomSheet)
|
fun inject(bottomSheet: BootstrapBottomSheet)
|
||||||
fun inject(bottomSheet: RoomWidgetPermissionBottomSheet)
|
fun inject(bottomSheet: RoomWidgetPermissionBottomSheet)
|
||||||
fun inject(bottomSheet: RoomWidgetsBottomSheet)
|
fun inject(bottomSheet: RoomWidgetsBottomSheet)
|
||||||
|
fun inject(bottomSheet: CallControlsBottomSheet)
|
||||||
|
|
||||||
/* ==========================================================================================
|
/* ==========================================================================================
|
||||||
* Others
|
* Others
|
||||||
|
|
|
@ -31,6 +31,7 @@ import im.vector.riotx.core.error.ErrorFormatter
|
||||||
import im.vector.riotx.core.pushers.PushersManager
|
import im.vector.riotx.core.pushers.PushersManager
|
||||||
import im.vector.riotx.core.utils.AssetReader
|
import im.vector.riotx.core.utils.AssetReader
|
||||||
import im.vector.riotx.core.utils.DimensionConverter
|
import im.vector.riotx.core.utils.DimensionConverter
|
||||||
|
import im.vector.riotx.features.call.WebRtcPeerConnectionManager
|
||||||
import im.vector.riotx.features.configuration.VectorConfiguration
|
import im.vector.riotx.features.configuration.VectorConfiguration
|
||||||
import im.vector.riotx.features.crypto.keysrequest.KeyRequestHandler
|
import im.vector.riotx.features.crypto.keysrequest.KeyRequestHandler
|
||||||
import im.vector.riotx.features.crypto.verification.IncomingVerificationRequestHandler
|
import im.vector.riotx.features.crypto.verification.IncomingVerificationRequestHandler
|
||||||
|
@ -134,6 +135,8 @@ interface VectorComponent {
|
||||||
|
|
||||||
fun reAuthHelper(): ReAuthHelper
|
fun reAuthHelper(): ReAuthHelper
|
||||||
|
|
||||||
|
fun webRtcPeerConnectionManager(): WebRtcPeerConnectionManager
|
||||||
|
|
||||||
@Component.Factory
|
@Component.Factory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(@BindsInstance context: Context): VectorComponent
|
fun create(@BindsInstance context: Context): VectorComponent
|
||||||
|
|
|
@ -22,6 +22,7 @@ import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.multibindings.IntoMap
|
import dagger.multibindings.IntoMap
|
||||||
import im.vector.riotx.core.platform.ConfigurationViewModel
|
import im.vector.riotx.core.platform.ConfigurationViewModel
|
||||||
|
import im.vector.riotx.features.call.SharedActiveCallViewModel
|
||||||
import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromKeyViewModel
|
import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromKeyViewModel
|
||||||
import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromPassphraseViewModel
|
import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromPassphraseViewModel
|
||||||
import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreSharedViewModel
|
import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreSharedViewModel
|
||||||
|
@ -85,6 +86,11 @@ interface ViewModelModule {
|
||||||
@ViewModelKey(ConfigurationViewModel::class)
|
@ViewModelKey(ConfigurationViewModel::class)
|
||||||
fun bindConfigurationViewModel(viewModel: ConfigurationViewModel): ViewModel
|
fun bindConfigurationViewModel(viewModel: ConfigurationViewModel): ViewModel
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@ViewModelKey(SharedActiveCallViewModel::class)
|
||||||
|
fun bindSharedActiveCallViewModel(viewModel: SharedActiveCallViewModel): ViewModel
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@ViewModelKey(UserDirectorySharedActionViewModel::class)
|
@ViewModelKey(UserDirectorySharedActionViewModel::class)
|
||||||
|
|
|
@ -40,14 +40,14 @@ class DefaultErrorFormatter @Inject constructor(
|
||||||
null -> null
|
null -> null
|
||||||
is IdentityServiceError -> identityServerError(throwable)
|
is IdentityServiceError -> identityServerError(throwable)
|
||||||
is Failure.NetworkConnection -> {
|
is Failure.NetworkConnection -> {
|
||||||
when {
|
when (throwable.ioException) {
|
||||||
throwable.ioException is SocketTimeoutException ->
|
is SocketTimeoutException ->
|
||||||
stringProvider.getString(R.string.error_network_timeout)
|
stringProvider.getString(R.string.error_network_timeout)
|
||||||
throwable.ioException is UnknownHostException ->
|
is UnknownHostException ->
|
||||||
// Invalid homeserver?
|
// Invalid homeserver?
|
||||||
// TODO Check network state, airplane mode, etc.
|
// TODO Check network state, airplane mode, etc.
|
||||||
stringProvider.getString(R.string.login_error_unknown_host)
|
stringProvider.getString(R.string.login_error_unknown_host)
|
||||||
else ->
|
else ->
|
||||||
stringProvider.getString(R.string.error_no_network)
|
stringProvider.getString(R.string.error_no_network)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,9 +17,14 @@
|
||||||
package im.vector.riotx.core.extensions
|
package im.vector.riotx.core.extensions
|
||||||
|
|
||||||
import androidx.fragment.app.FragmentTransaction
|
import androidx.fragment.app.FragmentTransaction
|
||||||
|
import im.vector.matrix.android.api.extensions.tryThis
|
||||||
|
|
||||||
inline fun androidx.fragment.app.FragmentManager.commitTransactionNow(func: FragmentTransaction.() -> FragmentTransaction) {
|
inline fun androidx.fragment.app.FragmentManager.commitTransactionNow(func: FragmentTransaction.() -> FragmentTransaction) {
|
||||||
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) {
|
inline fun androidx.fragment.app.FragmentManager.commitTransaction(func: FragmentTransaction.() -> FragmentTransaction) {
|
||||||
|
|
|
@ -38,10 +38,6 @@ fun Session.configureAndStart(context: Context,
|
||||||
startSyncing(context)
|
startSyncing(context)
|
||||||
refreshPushers()
|
refreshPushers()
|
||||||
pushRuleTriggerListener.startWithSession(this)
|
pushRuleTriggerListener.startWithSession(this)
|
||||||
|
|
||||||
// TODO P1 From HomeActivity
|
|
||||||
// @Inject lateinit var incomingVerificationRequestHandler: IncomingVerificationRequestHandler
|
|
||||||
// @Inject lateinit var keyRequestHandler: KeyRequestHandler
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Session.startSyncing(context: Context) {
|
fun Session.startSyncing(context: Context) {
|
||||||
|
|
|
@ -165,6 +165,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
Timber.i("onCreate Activity ${this.javaClass.simpleName}")
|
||||||
val vectorComponent = getVectorComponent()
|
val vectorComponent = getVectorComponent()
|
||||||
screenComponent = DaggerScreenComponent.factory().create(vectorComponent, this)
|
screenComponent = DaggerScreenComponent.factory().create(vectorComponent, this)
|
||||||
val timeForInjection = measureTimeMillis {
|
val timeForInjection = measureTimeMillis {
|
||||||
|
@ -252,6 +253,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
Timber.i("onDestroy Activity ${this.javaClass.simpleName}")
|
||||||
unBinder?.unbind()
|
unBinder?.unbind()
|
||||||
unBinder = null
|
unBinder = null
|
||||||
|
|
||||||
|
@ -279,6 +281,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
|
Timber.i("onPause Activity ${this.javaClass.simpleName}")
|
||||||
|
|
||||||
rageShake.stop()
|
rageShake.stop()
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,7 @@ import com.airbnb.mvrx.MvRxViewId
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
|
import com.jakewharton.rxbinding3.view.clicks
|
||||||
import im.vector.riotx.core.di.DaggerScreenComponent
|
import im.vector.riotx.core.di.DaggerScreenComponent
|
||||||
import im.vector.riotx.core.di.ScreenComponent
|
import im.vector.riotx.core.di.ScreenComponent
|
||||||
import im.vector.riotx.core.utils.DimensionConverter
|
import im.vector.riotx.core.utils.DimensionConverter
|
||||||
|
@ -41,6 +42,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.disposables.CompositeDisposable
|
import io.reactivex.disposables.CompositeDisposable
|
||||||
import io.reactivex.disposables.Disposable
|
import io.reactivex.disposables.Disposable
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add MvRx capabilities to bottomsheetdialog (like BaseMvRxFragment)
|
* Add MvRx capabilities to bottomsheetdialog (like BaseMvRxFragment)
|
||||||
|
@ -169,6 +171,18 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================================================================
|
||||||
|
* Views
|
||||||
|
* ========================================================================================== */
|
||||||
|
|
||||||
|
protected fun View.debouncedClicks(onClicked: () -> Unit) {
|
||||||
|
clicks()
|
||||||
|
.throttleFirst(300, TimeUnit.MILLISECONDS)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe { onClicked() }
|
||||||
|
.disposeOnDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
/* ==========================================================================================
|
/* ==========================================================================================
|
||||||
* ViewEvents
|
* ViewEvents
|
||||||
* ========================================================================================== */
|
* ========================================================================================== */
|
||||||
|
|
|
@ -45,7 +45,7 @@ class VectorEditTextPreference : EditTextPreference {
|
||||||
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
||||||
// display the title in multi-line to avoid ellipsis.
|
// display the title in multi-line to avoid ellipsis.
|
||||||
try {
|
try {
|
||||||
holder.itemView.findViewById<TextView>(android.R.id.title)?.setSingleLine(false)
|
holder.itemView.findViewById<TextView>(android.R.id.title)?.isSingleLine = false
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "onBindView")
|
Timber.e(e, "onBindView")
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,7 +87,7 @@ open class VectorPreference : Preference {
|
||||||
val title = itemView.findViewById<TextView>(android.R.id.title)
|
val title = itemView.findViewById<TextView>(android.R.id.title)
|
||||||
val summary = itemView.findViewById<TextView>(android.R.id.summary)
|
val summary = itemView.findViewById<TextView>(android.R.id.summary)
|
||||||
if (title != null) {
|
if (title != null) {
|
||||||
title.setSingleLine(false)
|
title.isSingleLine = false
|
||||||
title.setTypeface(null, mTypeface)
|
title.setTypeface(null, mTypeface)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,7 @@ class VectorSwitchPreference : SwitchPreference {
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
||||||
// display the title in multi-line to avoid ellipsis.
|
// display the title in multi-line to avoid ellipsis.
|
||||||
holder.itemView.findViewById<TextView>(android.R.id.title)?.setSingleLine(false)
|
holder.itemView.findViewById<TextView>(android.R.id.title)?.isSingleLine = false
|
||||||
|
|
||||||
super.onBindViewHolder(holder)
|
super.onBindViewHolder(holder)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.riotx.core.services
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothAdapter
|
||||||
|
import android.bluetooth.BluetoothClass
|
||||||
|
import android.bluetooth.BluetoothDevice
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
|
class BluetoothHeadsetReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
|
interface EventListener {
|
||||||
|
fun onBTHeadsetEvent(event: BTHeadsetPlugEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
var delegate: WeakReference<EventListener>? = null
|
||||||
|
|
||||||
|
data class BTHeadsetPlugEvent(
|
||||||
|
val plugged: Boolean,
|
||||||
|
val headsetName: String?,
|
||||||
|
/**
|
||||||
|
* BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE
|
||||||
|
* BluetoothClass.Device.AUDIO_VIDEO_CAR_AUDIO
|
||||||
|
* AUDIO_VIDEO_WEARABLE_HEADSET
|
||||||
|
*/
|
||||||
|
val deviceClass: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
// This intent will have 3 extras:
|
||||||
|
// EXTRA_CONNECTION_STATE - The current connection state
|
||||||
|
// EXTRA_PREVIOUS_CONNECTION_STATE}- The previous connection state.
|
||||||
|
// BluetoothDevice#EXTRA_DEVICE - The remote device.
|
||||||
|
// EXTRA_CONNECTION_STATE or EXTRA_PREVIOUS_CONNECTION_STATE can be any of
|
||||||
|
// STATE_DISCONNECTED}, STATE_CONNECTING, STATE_CONNECTED, STATE_DISCONNECTING
|
||||||
|
|
||||||
|
val headsetConnected = when (intent?.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE, -1)) {
|
||||||
|
BluetoothAdapter.STATE_CONNECTED -> true
|
||||||
|
BluetoothAdapter.STATE_DISCONNECTED -> false
|
||||||
|
else -> return // ignore intermediate states
|
||||||
|
}
|
||||||
|
|
||||||
|
val device = intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE)
|
||||||
|
val deviceName = device?.name
|
||||||
|
when (device?.bluetoothClass?.deviceClass) {
|
||||||
|
BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE,
|
||||||
|
BluetoothClass.Device.AUDIO_VIDEO_CAR_AUDIO,
|
||||||
|
BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET -> {
|
||||||
|
// filter only device that we care about for
|
||||||
|
delegate?.get()?.onBTHeadsetEvent(
|
||||||
|
BTHeadsetPlugEvent(
|
||||||
|
plugged = headsetConnected,
|
||||||
|
headsetName = deviceName,
|
||||||
|
deviceClass = device.bluetoothClass.deviceClass
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun createAndRegister(context: Context, listener: EventListener): BluetoothHeadsetReceiver {
|
||||||
|
val receiver = BluetoothHeadsetReceiver()
|
||||||
|
receiver.delegate = WeakReference(listener)
|
||||||
|
context.registerReceiver(receiver, IntentFilter(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED))
|
||||||
|
return receiver
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unRegister(context: Context, receiver: BluetoothHeadsetReceiver) {
|
||||||
|
context.unregisterReceiver(receiver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.core.services
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.media.AudioAttributes
|
||||||
|
import android.media.AudioManager
|
||||||
|
import android.media.MediaPlayer
|
||||||
|
import android.os.Build
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
class CallRingPlayer(
|
||||||
|
context: Context
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val applicationContext = context.applicationContext
|
||||||
|
|
||||||
|
private var player: MediaPlayer? = null
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
val audioManager: AudioManager = applicationContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||||
|
player?.release()
|
||||||
|
player = createPlayer()
|
||||||
|
|
||||||
|
// Check if sound is enabled
|
||||||
|
val ringerMode = audioManager.ringerMode
|
||||||
|
if (player != null && ringerMode == AudioManager.RINGER_MODE_NORMAL) {
|
||||||
|
try {
|
||||||
|
if (player?.isPlaying == false) {
|
||||||
|
player?.start()
|
||||||
|
Timber.v("## VOIP Starting ringing")
|
||||||
|
} else {
|
||||||
|
Timber.v("## VOIP already playing")
|
||||||
|
}
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
Timber.e(failure, "## VOIP Failed to start ringing")
|
||||||
|
player = null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Timber.v("## VOIP Can't play $player ode $ringerMode")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
player?.release()
|
||||||
|
player = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createPlayer(): MediaPlayer? {
|
||||||
|
try {
|
||||||
|
val mediaPlayer = MediaPlayer.create(applicationContext, R.raw.ring)
|
||||||
|
|
||||||
|
mediaPlayer.setOnErrorListener(MediaPlayerErrorListener())
|
||||||
|
mediaPlayer.isLooping = true
|
||||||
|
if (Build.VERSION.SDK_INT <= 21) {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
mediaPlayer.setAudioStreamType(AudioManager.STREAM_RING)
|
||||||
|
} else {
|
||||||
|
mediaPlayer.setAudioAttributes(AudioAttributes.Builder()
|
||||||
|
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
||||||
|
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING)
|
||||||
|
.build())
|
||||||
|
}
|
||||||
|
return mediaPlayer
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
Timber.e(failure, "Failed to create Call ring player")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class MediaPlayerErrorListener : MediaPlayer.OnErrorListener {
|
||||||
|
override fun onError(mp: MediaPlayer, what: Int, extra: Int): Boolean {
|
||||||
|
Timber.w("onError($mp, $what, $extra")
|
||||||
|
player = null
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2019 New Vector Ltd
|
* Copyright 2019 New Vector Ltd
|
||||||
|
* Copyright 2020 New Vector Ltd
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -14,53 +15,119 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:Suppress("UNUSED_PARAMETER")
|
|
||||||
|
|
||||||
package im.vector.riotx.core.services
|
package im.vector.riotx.core.services
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.os.Binder
|
||||||
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
|
import android.view.KeyEvent
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.media.session.MediaButtonReceiver
|
||||||
import im.vector.riotx.core.extensions.vectorComponent
|
import im.vector.riotx.core.extensions.vectorComponent
|
||||||
|
import im.vector.riotx.features.call.WebRtcPeerConnectionManager
|
||||||
|
import im.vector.riotx.features.call.telecom.CallConnection
|
||||||
import im.vector.riotx.features.notifications.NotificationUtils
|
import im.vector.riotx.features.notifications.NotificationUtils
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Foreground service to manage calls
|
* Foreground service to manage calls
|
||||||
*/
|
*/
|
||||||
class CallService : VectorService() {
|
class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListener, BluetoothHeadsetReceiver.EventListener {
|
||||||
|
|
||||||
/**
|
private val connections = mutableMapOf<String, CallConnection>()
|
||||||
* call in progress (foreground notification)
|
|
||||||
*/
|
|
||||||
private var mCallIdInProgress: String? = null
|
|
||||||
|
|
||||||
private lateinit var notificationUtils: NotificationUtils
|
private lateinit var notificationUtils: NotificationUtils
|
||||||
|
private lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager
|
||||||
|
|
||||||
/**
|
private var callRingPlayer: CallRingPlayer? = null
|
||||||
* incoming (foreground notification)
|
|
||||||
*/
|
private var wiredHeadsetStateReceiver: WiredHeadsetStateReceiver? = null
|
||||||
private var mIncomingCallId: String? = null
|
private var bluetoothHeadsetStateReceiver: BluetoothHeadsetReceiver? = null
|
||||||
|
|
||||||
|
// 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
|
||||||
|
private var mediaSession: MediaSessionCompat? = null
|
||||||
|
private val mediaSessionButtonCallback = object : MediaSessionCompat.Callback() {
|
||||||
|
override fun onMediaButtonEvent(mediaButtonEvent: Intent?): Boolean {
|
||||||
|
val keyEvent = mediaButtonEvent?.getParcelableExtra<KeyEvent>(Intent.EXTRA_KEY_EVENT) ?: return false
|
||||||
|
if (keyEvent.keyCode == KeyEvent.KEYCODE_HEADSETHOOK) {
|
||||||
|
webRtcPeerConnectionManager.headSetButtonTapped()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
notificationUtils = vectorComponent().notificationUtils()
|
notificationUtils = vectorComponent().notificationUtils()
|
||||||
|
webRtcPeerConnectionManager = vectorComponent().webRtcPeerConnectionManager()
|
||||||
|
callRingPlayer = CallRingPlayer(applicationContext)
|
||||||
|
wiredHeadsetStateReceiver = WiredHeadsetStateReceiver.createAndRegister(this, this)
|
||||||
|
bluetoothHeadsetStateReceiver = BluetoothHeadsetReceiver.createAndRegister(this, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
callRingPlayer?.stop()
|
||||||
|
wiredHeadsetStateReceiver?.let { WiredHeadsetStateReceiver.unRegister(this, it) }
|
||||||
|
wiredHeadsetStateReceiver = null
|
||||||
|
bluetoothHeadsetStateReceiver?.let { BluetoothHeadsetReceiver.unRegister(this, it) }
|
||||||
|
bluetoothHeadsetStateReceiver = null
|
||||||
|
mediaSession?.release()
|
||||||
|
mediaSession = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
Timber.v("## VOIP onStartCommand $intent")
|
||||||
|
if (mediaSession == null) {
|
||||||
|
mediaSession = MediaSessionCompat(applicationContext, CallService::class.java.name).apply {
|
||||||
|
setCallback(mediaSessionButtonCallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
if (intent == null) {
|
if (intent == null) {
|
||||||
// Service started again by the system.
|
// Service started again by the system.
|
||||||
// TODO What do we do here?
|
// TODO What do we do here?
|
||||||
return START_STICKY
|
return START_STICKY
|
||||||
}
|
}
|
||||||
|
mediaSession?.let {
|
||||||
|
// This ensures that the correct callbacks to MediaSessionCompat.Callback
|
||||||
|
// will be triggered based on the incoming KeyEvent.
|
||||||
|
MediaButtonReceiver.handleIntent(it, intent)
|
||||||
|
}
|
||||||
|
|
||||||
when (intent.action) {
|
when (intent.action) {
|
||||||
ACTION_INCOMING_CALL -> displayIncomingCallNotification(intent)
|
ACTION_INCOMING_RINGING_CALL -> {
|
||||||
ACTION_PENDING_CALL -> displayCallInProgressNotification(intent)
|
mediaSession?.isActive = true
|
||||||
ACTION_NO_ACTIVE_CALL -> hideCallNotifications()
|
callRingPlayer?.start()
|
||||||
else ->
|
displayIncomingCallNotification(intent)
|
||||||
|
}
|
||||||
|
ACTION_OUTGOING_RINGING_CALL -> {
|
||||||
|
mediaSession?.isActive = true
|
||||||
|
callRingPlayer?.start()
|
||||||
|
displayOutgoingRingingCallNotification(intent)
|
||||||
|
}
|
||||||
|
ACTION_ONGOING_CALL -> {
|
||||||
|
callRingPlayer?.stop()
|
||||||
|
displayCallInProgressNotification(intent)
|
||||||
|
}
|
||||||
|
ACTION_NO_ACTIVE_CALL -> hideCallNotifications()
|
||||||
|
ACTION_CALL_CONNECTING -> {
|
||||||
|
// lower notification priority
|
||||||
|
displayCallInProgressNotification(intent)
|
||||||
|
// stop ringing
|
||||||
|
callRingPlayer?.stop()
|
||||||
|
}
|
||||||
|
ACTION_ONGOING_CALL_BG -> {
|
||||||
|
// there is an ongoing call but call activity is in background
|
||||||
|
displayCallOnGoingInBackground(intent)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
// Should not happen
|
// Should not happen
|
||||||
|
callRingPlayer?.stop()
|
||||||
myStopSelf()
|
myStopSelf()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We want the system to restore the service if killed
|
// We want the system to restore the service if killed
|
||||||
|
@ -80,54 +147,65 @@ class CallService : VectorService() {
|
||||||
* @param callId the callId
|
* @param callId the callId
|
||||||
*/
|
*/
|
||||||
private fun displayIncomingCallNotification(intent: Intent) {
|
private fun displayIncomingCallNotification(intent: Intent) {
|
||||||
Timber.v("displayIncomingCallNotification")
|
Timber.v("## VOIP displayIncomingCallNotification $intent")
|
||||||
|
|
||||||
// TODO
|
|
||||||
/*
|
|
||||||
|
|
||||||
// the incoming call in progress is already displayed
|
// the incoming call in progress is already displayed
|
||||||
if (!TextUtils.isEmpty(mIncomingCallId)) {
|
// if (!TextUtils.isEmpty(mIncomingCallId)) {
|
||||||
Timber.v("displayIncomingCallNotification : the incoming call in progress is already displayed")
|
// Timber.v("displayIncomingCallNotification : the incoming call in progress is already displayed")
|
||||||
} else if (!TextUtils.isEmpty(mCallIdInProgress)) {
|
// } else if (!TextUtils.isEmpty(mCallIdInProgress)) {
|
||||||
Timber.v("displayIncomingCallNotification : a 'call in progress' notification is displayed")
|
// Timber.v("displayIncomingCallNotification : a 'call in progress' notification is displayed")
|
||||||
} else if (null == CallsManager.getSharedInstance().activeCall) {
|
// } else
|
||||||
val callId = intent.getStringExtra(EXTRA_CALL_ID)
|
// // if (null == webRtcPeerConnectionManager.currentCall)
|
||||||
|
// {
|
||||||
|
val callId = intent.getStringExtra(EXTRA_CALL_ID)
|
||||||
|
|
||||||
Timber.v("displayIncomingCallNotification : display the dedicated notification")
|
Timber.v("displayIncomingCallNotification : display the dedicated notification")
|
||||||
val notification = NotificationUtils.buildIncomingCallNotification(
|
val notification = notificationUtils.buildIncomingCallNotification(
|
||||||
this,
|
intent.getBooleanExtra(EXTRA_IS_VIDEO, false),
|
||||||
intent.getBooleanExtra(EXTRA_IS_VIDEO, false),
|
intent.getStringExtra(EXTRA_ROOM_NAME) ?: "",
|
||||||
intent.getStringExtra(EXTRA_ROOM_NAME),
|
intent.getStringExtra(EXTRA_ROOM_ID) ?: "",
|
||||||
intent.getStringExtra(EXTRA_MATRIX_ID),
|
callId ?: "")
|
||||||
callId)
|
startForeground(NOTIFICATION_ID, notification)
|
||||||
startForeground(NOTIFICATION_ID, notification)
|
|
||||||
|
|
||||||
mIncomingCallId = callId
|
// mIncomingCallId = callId
|
||||||
|
|
||||||
// turn the screen on for 3 seconds
|
// turn the screen on for 3 seconds
|
||||||
if (Matrix.getInstance(VectorApp.getInstance())!!.pushManager.isScreenTurnedOn) {
|
// if (Matrix.getInstance(VectorApp.getInstance())!!.pushManager.isScreenTurnedOn) {
|
||||||
try {
|
// try {
|
||||||
val pm = getSystemService(Context.POWER_SERVICE) as PowerManager
|
// val pm = getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||||
val wl = pm.newWakeLock(
|
// val wl = pm.newWakeLock(
|
||||||
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or PowerManager.ACQUIRE_CAUSES_WAKEUP,
|
// WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or PowerManager.ACQUIRE_CAUSES_WAKEUP,
|
||||||
CallService::class.java.simpleName)
|
// CallService::class.java.simpleName)
|
||||||
wl.acquire(3000)
|
// wl.acquire(3000)
|
||||||
wl.release()
|
// wl.release()
|
||||||
} catch (re: RuntimeException) {
|
// } catch (re: RuntimeException) {
|
||||||
Timber.e(re, "displayIncomingCallNotification : failed to turn screen on ")
|
// Timber.e(re, "displayIncomingCallNotification : failed to turn screen on ")
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// else {
|
||||||
|
// Timber.i("displayIncomingCallNotification : do not display the incoming call notification because there is a pending call")
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
}
|
private fun displayOutgoingRingingCallNotification(intent: Intent) {
|
||||||
} else {
|
val callId = intent.getStringExtra(EXTRA_CALL_ID)
|
||||||
Timber.i("displayIncomingCallNotification : do not display the incoming call notification because there is a pending call")
|
|
||||||
}// test if there is no active call
|
Timber.v("displayOutgoingCallNotification : display the dedicated notification")
|
||||||
*/
|
val notification = notificationUtils.buildOutgoingRingingCallNotification(
|
||||||
|
intent.getBooleanExtra(EXTRA_IS_VIDEO, false),
|
||||||
|
intent.getStringExtra(EXTRA_ROOM_NAME) ?: "",
|
||||||
|
intent.getStringExtra(EXTRA_ROOM_ID) ?: "",
|
||||||
|
callId ?: "")
|
||||||
|
startForeground(NOTIFICATION_ID, notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display a call in progress notification.
|
* Display a call in progress notification.
|
||||||
*/
|
*/
|
||||||
private fun displayCallInProgressNotification(intent: Intent) {
|
private fun displayCallInProgressNotification(intent: Intent) {
|
||||||
|
Timber.v("## VOIP displayCallInProgressNotification")
|
||||||
val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: ""
|
val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: ""
|
||||||
|
|
||||||
val notification = notificationUtils.buildPendingCallNotification(
|
val notification = notificationUtils.buildPendingCallNotification(
|
||||||
|
@ -139,7 +217,27 @@ class CallService : VectorService() {
|
||||||
|
|
||||||
startForeground(NOTIFICATION_ID, notification)
|
startForeground(NOTIFICATION_ID, notification)
|
||||||
|
|
||||||
mCallIdInProgress = callId
|
// mCallIdInProgress = callId
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a call in progress notification.
|
||||||
|
*/
|
||||||
|
private fun displayCallOnGoingInBackground(intent: Intent) {
|
||||||
|
Timber.v("## VOIP displayCallInProgressNotification")
|
||||||
|
val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: ""
|
||||||
|
|
||||||
|
val notification = notificationUtils.buildPendingCallNotification(
|
||||||
|
isVideo = intent.getBooleanExtra(EXTRA_IS_VIDEO, false),
|
||||||
|
roomName = intent.getStringExtra(EXTRA_ROOM_NAME) ?: "",
|
||||||
|
roomId = intent.getStringExtra(EXTRA_ROOM_ID) ?: "",
|
||||||
|
matrixId = intent.getStringExtra(EXTRA_MATRIX_ID) ?: "",
|
||||||
|
callId = callId,
|
||||||
|
fromBg = true)
|
||||||
|
|
||||||
|
startForeground(NOTIFICATION_ID, notification)
|
||||||
|
|
||||||
|
// mCallIdInProgress = callId
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -148,18 +246,28 @@ class CallService : VectorService() {
|
||||||
private fun hideCallNotifications() {
|
private fun hideCallNotifications() {
|
||||||
val notification = notificationUtils.buildCallEndedNotification()
|
val notification = notificationUtils.buildCallEndedNotification()
|
||||||
|
|
||||||
|
mediaSession?.isActive = false
|
||||||
// It's mandatory to startForeground to avoid crash
|
// It's mandatory to startForeground to avoid crash
|
||||||
startForeground(NOTIFICATION_ID, notification)
|
startForeground(NOTIFICATION_ID, notification)
|
||||||
|
|
||||||
myStopSelf()
|
myStopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addConnection(callConnection: CallConnection) {
|
||||||
|
connections[callConnection.callId] = callConnection
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val NOTIFICATION_ID = 6480
|
private const val NOTIFICATION_ID = 6480
|
||||||
|
|
||||||
private const val ACTION_INCOMING_CALL = "im.vector.riotx.core.services.CallService.INCOMING_CALL"
|
private const val ACTION_INCOMING_RINGING_CALL = "im.vector.riotx.core.services.CallService.ACTION_INCOMING_RINGING_CALL"
|
||||||
private const val ACTION_PENDING_CALL = "im.vector.riotx.core.services.CallService.PENDING_CALL"
|
private const val ACTION_OUTGOING_RINGING_CALL = "im.vector.riotx.core.services.CallService.ACTION_OUTGOING_RINGING_CALL"
|
||||||
|
private const val ACTION_CALL_CONNECTING = "im.vector.riotx.core.services.CallService.ACTION_CALL_CONNECTING"
|
||||||
|
private const val ACTION_ONGOING_CALL = "im.vector.riotx.core.services.CallService.ACTION_ONGOING_CALL"
|
||||||
|
private const val ACTION_ONGOING_CALL_BG = "im.vector.riotx.core.services.CallService.ACTION_ONGOING_CALL_BG"
|
||||||
private const val ACTION_NO_ACTIVE_CALL = "im.vector.riotx.core.services.CallService.NO_ACTIVE_CALL"
|
private const val ACTION_NO_ACTIVE_CALL = "im.vector.riotx.core.services.CallService.NO_ACTIVE_CALL"
|
||||||
|
// private const val ACTION_ACTIVITY_VISIBLE = "im.vector.riotx.core.services.CallService.ACTION_ACTIVITY_VISIBLE"
|
||||||
|
// private const val ACTION_STOP_RINGING = "im.vector.riotx.core.services.CallService.ACTION_STOP_RINGING"
|
||||||
|
|
||||||
private const val EXTRA_IS_VIDEO = "EXTRA_IS_VIDEO"
|
private const val EXTRA_IS_VIDEO = "EXTRA_IS_VIDEO"
|
||||||
private const val EXTRA_ROOM_NAME = "EXTRA_ROOM_NAME"
|
private const val EXTRA_ROOM_NAME = "EXTRA_ROOM_NAME"
|
||||||
|
@ -167,15 +275,53 @@ class CallService : VectorService() {
|
||||||
private const val EXTRA_MATRIX_ID = "EXTRA_MATRIX_ID"
|
private const val EXTRA_MATRIX_ID = "EXTRA_MATRIX_ID"
|
||||||
private const val EXTRA_CALL_ID = "EXTRA_CALL_ID"
|
private const val EXTRA_CALL_ID = "EXTRA_CALL_ID"
|
||||||
|
|
||||||
fun onIncomingCall(context: Context,
|
fun onIncomingCallRinging(context: Context,
|
||||||
isVideo: Boolean,
|
isVideo: Boolean,
|
||||||
roomName: String,
|
roomName: String,
|
||||||
roomId: String,
|
roomId: String,
|
||||||
matrixId: String,
|
matrixId: String,
|
||||||
callId: String) {
|
callId: String) {
|
||||||
val intent = Intent(context, CallService::class.java)
|
val intent = Intent(context, CallService::class.java)
|
||||||
.apply {
|
.apply {
|
||||||
action = ACTION_INCOMING_CALL
|
action = ACTION_INCOMING_RINGING_CALL
|
||||||
|
putExtra(EXTRA_IS_VIDEO, isVideo)
|
||||||
|
putExtra(EXTRA_ROOM_NAME, roomName)
|
||||||
|
putExtra(EXTRA_ROOM_ID, roomId)
|
||||||
|
putExtra(EXTRA_MATRIX_ID, matrixId)
|
||||||
|
putExtra(EXTRA_CALL_ID, callId)
|
||||||
|
}
|
||||||
|
|
||||||
|
ContextCompat.startForegroundService(context, intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onOnGoingCallBackground(context: Context,
|
||||||
|
isVideo: Boolean,
|
||||||
|
roomName: String,
|
||||||
|
roomId: String,
|
||||||
|
matrixId: String,
|
||||||
|
callId: String) {
|
||||||
|
val intent = Intent(context, CallService::class.java)
|
||||||
|
.apply {
|
||||||
|
action = ACTION_ONGOING_CALL_BG
|
||||||
|
putExtra(EXTRA_IS_VIDEO, isVideo)
|
||||||
|
putExtra(EXTRA_ROOM_NAME, roomName)
|
||||||
|
putExtra(EXTRA_ROOM_ID, roomId)
|
||||||
|
putExtra(EXTRA_MATRIX_ID, matrixId)
|
||||||
|
putExtra(EXTRA_CALL_ID, callId)
|
||||||
|
}
|
||||||
|
|
||||||
|
ContextCompat.startForegroundService(context, intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onOutgoingCallRinging(context: Context,
|
||||||
|
isVideo: Boolean,
|
||||||
|
roomName: String,
|
||||||
|
roomId: String,
|
||||||
|
matrixId: String,
|
||||||
|
callId: String) {
|
||||||
|
val intent = Intent(context, CallService::class.java)
|
||||||
|
.apply {
|
||||||
|
action = ACTION_OUTGOING_RINGING_CALL
|
||||||
putExtra(EXTRA_IS_VIDEO, isVideo)
|
putExtra(EXTRA_IS_VIDEO, isVideo)
|
||||||
putExtra(EXTRA_ROOM_NAME, roomName)
|
putExtra(EXTRA_ROOM_NAME, roomName)
|
||||||
putExtra(EXTRA_ROOM_ID, roomId)
|
putExtra(EXTRA_ROOM_ID, roomId)
|
||||||
|
@ -194,7 +340,7 @@ class CallService : VectorService() {
|
||||||
callId: String) {
|
callId: String) {
|
||||||
val intent = Intent(context, CallService::class.java)
|
val intent = Intent(context, CallService::class.java)
|
||||||
.apply {
|
.apply {
|
||||||
action = ACTION_PENDING_CALL
|
action = ACTION_ONGOING_CALL
|
||||||
putExtra(EXTRA_IS_VIDEO, isVideo)
|
putExtra(EXTRA_IS_VIDEO, isVideo)
|
||||||
putExtra(EXTRA_ROOM_NAME, roomName)
|
putExtra(EXTRA_ROOM_NAME, roomName)
|
||||||
putExtra(EXTRA_ROOM_ID, roomId)
|
putExtra(EXTRA_ROOM_ID, roomId)
|
||||||
|
@ -214,4 +360,20 @@ class CallService : VectorService() {
|
||||||
ContextCompat.startForegroundService(context, intent)
|
ContextCompat.startForegroundService(context, intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inner class CallServiceBinder : Binder() {
|
||||||
|
fun getCallService(): CallService {
|
||||||
|
return this@CallService
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onHeadsetEvent(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) {
|
||||||
|
Timber.v("## VOIP: onHeadsetEvent $event")
|
||||||
|
webRtcPeerConnectionManager.onWiredDeviceEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBTHeadsetEvent(event: BluetoothHeadsetReceiver.BTHeadsetPlugEvent) {
|
||||||
|
Timber.v("## VOIP: onBTHeadsetEvent $event")
|
||||||
|
webRtcPeerConnectionManager.onWirelessDeviceEvent(event)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.core.services
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.media.AudioManager
|
||||||
|
import android.os.Build
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamic broadcast receiver to detect headset plug/unplug
|
||||||
|
*/
|
||||||
|
class WiredHeadsetStateReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
|
interface HeadsetEventListener {
|
||||||
|
fun onHeadsetEvent(event: HeadsetPlugEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
var delegate: WeakReference<HeadsetEventListener>? = null
|
||||||
|
|
||||||
|
data class HeadsetPlugEvent(
|
||||||
|
val plugged: Boolean,
|
||||||
|
val headsetName: String?,
|
||||||
|
val hasMicrophone: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
// The intent will have the following extra values:
|
||||||
|
// state 0 for unplugged, 1 for plugged
|
||||||
|
// name Headset type, human readable string
|
||||||
|
// microphone 1 if headset has a microphone, 0 otherwise
|
||||||
|
|
||||||
|
val isPlugged = when (intent?.getIntExtra("state", -1)) {
|
||||||
|
0 -> false
|
||||||
|
1 -> true
|
||||||
|
else -> return Unit.also {
|
||||||
|
Timber.v("## VOIP WiredHeadsetStateReceiver invalid state")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val hasMicrophone = when (intent.getIntExtra("microphone", -1)) {
|
||||||
|
1 -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate?.get()?.onHeadsetEvent(
|
||||||
|
HeadsetPlugEvent(plugged = isPlugged, headsetName = intent.getStringExtra("name"), hasMicrophone = hasMicrophone)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun createAndRegister(context: Context, listener: HeadsetEventListener): WiredHeadsetStateReceiver {
|
||||||
|
val receiver = WiredHeadsetStateReceiver()
|
||||||
|
receiver.delegate = WeakReference(listener)
|
||||||
|
val action = if (Build.VERSION.SDK_INT >= 21) {
|
||||||
|
AudioManager.ACTION_HEADSET_PLUG
|
||||||
|
} else {
|
||||||
|
Intent.ACTION_HEADSET_PLUG
|
||||||
|
}
|
||||||
|
context.registerReceiver(receiver, IntentFilter(action))
|
||||||
|
return receiver
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unRegister(context: Context, receiver: WiredHeadsetStateReceiver) {
|
||||||
|
context.unregisterReceiver(receiver)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.core.ui.views
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.widget.RelativeLayout
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.features.themes.ThemeUtils
|
||||||
|
|
||||||
|
class ActiveCallView @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0
|
||||||
|
) : RelativeLayout(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
|
interface Callback {
|
||||||
|
fun onTapToReturnToCall()
|
||||||
|
}
|
||||||
|
|
||||||
|
var callback: Callback? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
setupView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupView() {
|
||||||
|
inflate(context, R.layout.view_active_call_view, this)
|
||||||
|
setBackgroundColor(ThemeUtils.getColor(context, R.attr.colorPrimary))
|
||||||
|
setOnClickListener { callback?.onTapToReturnToCall() }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.core.ui.views
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import androidx.cardview.widget.CardView
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import im.vector.matrix.android.api.session.call.CallState
|
||||||
|
import im.vector.matrix.android.api.session.call.EglUtils
|
||||||
|
import im.vector.matrix.android.api.session.call.MxCall
|
||||||
|
import im.vector.riotx.core.utils.DebouncedClickListener
|
||||||
|
import im.vector.riotx.features.call.WebRtcPeerConnectionManager
|
||||||
|
import org.webrtc.RendererCommon
|
||||||
|
import org.webrtc.SurfaceViewRenderer
|
||||||
|
|
||||||
|
class ActiveCallViewHolder {
|
||||||
|
|
||||||
|
private var activeCallPiP: SurfaceViewRenderer? = null
|
||||||
|
private var activeCallView: ActiveCallView? = null
|
||||||
|
private var pipWrapper: CardView? = null
|
||||||
|
|
||||||
|
private var activeCallPipInitialized = false
|
||||||
|
|
||||||
|
fun updateCall(activeCall: MxCall?, webRtcPeerConnectionManager: WebRtcPeerConnectionManager) {
|
||||||
|
val hasActiveCall = activeCall?.state is CallState.Connected
|
||||||
|
if (hasActiveCall) {
|
||||||
|
val isVideoCall = activeCall?.isVideoCall == true
|
||||||
|
if (isVideoCall) initIfNeeded()
|
||||||
|
activeCallView?.isVisible = !isVideoCall
|
||||||
|
pipWrapper?.isVisible = isVideoCall
|
||||||
|
activeCallPiP?.isVisible = isVideoCall
|
||||||
|
activeCallPiP?.let {
|
||||||
|
webRtcPeerConnectionManager.attachViewRenderers(null, it, null)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
activeCallView?.isVisible = false
|
||||||
|
activeCallPiP?.isVisible = false
|
||||||
|
pipWrapper?.isVisible = false
|
||||||
|
activeCallPiP?.let {
|
||||||
|
webRtcPeerConnectionManager.detachRenderers(listOf(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initIfNeeded() {
|
||||||
|
if (!activeCallPipInitialized && activeCallPiP != null) {
|
||||||
|
activeCallPiP?.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL)
|
||||||
|
EglUtils.rootEglBase?.let { eglBase ->
|
||||||
|
activeCallPiP?.init(eglBase.eglBaseContext, null)
|
||||||
|
activeCallPiP?.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_BALANCED)
|
||||||
|
activeCallPiP?.setEnableHardwareScaler(true /* enabled */)
|
||||||
|
activeCallPiP?.setZOrderMediaOverlay(true)
|
||||||
|
activeCallPipInitialized = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bind(activeCallPiP: SurfaceViewRenderer, activeCallView: ActiveCallView, pipWrapper: CardView, interactionListener: ActiveCallView.Callback) {
|
||||||
|
this.activeCallPiP = activeCallPiP
|
||||||
|
this.activeCallView = activeCallView
|
||||||
|
this.pipWrapper = pipWrapper
|
||||||
|
|
||||||
|
this.activeCallView?.callback = interactionListener
|
||||||
|
pipWrapper.setOnClickListener(
|
||||||
|
DebouncedClickListener(View.OnClickListener { _ ->
|
||||||
|
interactionListener.onTapToReturnToCall()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unBind(webRtcPeerConnectionManager: WebRtcPeerConnectionManager) {
|
||||||
|
activeCallPiP?.let {
|
||||||
|
webRtcPeerConnectionManager.detachRenderers(listOf(it))
|
||||||
|
}
|
||||||
|
if (activeCallPipInitialized) {
|
||||||
|
activeCallPiP?.release()
|
||||||
|
}
|
||||||
|
this.activeCallView?.callback = null
|
||||||
|
pipWrapper?.setOnClickListener(null)
|
||||||
|
activeCallPiP = null
|
||||||
|
activeCallView = null
|
||||||
|
pipWrapper = null
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,298 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.features.call
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothAdapter
|
||||||
|
import android.bluetooth.BluetoothManager
|
||||||
|
import android.bluetooth.BluetoothProfile
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.media.AudioManager
|
||||||
|
import im.vector.matrix.android.api.session.call.MxCall
|
||||||
|
import im.vector.riotx.core.services.WiredHeadsetStateReceiver
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
|
class CallAudioManager(
|
||||||
|
val applicationContext: Context,
|
||||||
|
val configChange: (() -> Unit)?
|
||||||
|
) {
|
||||||
|
|
||||||
|
enum class SoundDevice {
|
||||||
|
PHONE,
|
||||||
|
SPEAKER,
|
||||||
|
HEADSET,
|
||||||
|
WIRELESS_HEADSET
|
||||||
|
}
|
||||||
|
|
||||||
|
// if all calls to audio manager not in the same thread it's not working well.
|
||||||
|
private val executor = Executors.newSingleThreadExecutor()
|
||||||
|
|
||||||
|
private var audioManager: AudioManager? = null
|
||||||
|
|
||||||
|
private var savedIsSpeakerPhoneOn = false
|
||||||
|
private var savedIsMicrophoneMute = false
|
||||||
|
private var savedAudioMode = AudioManager.MODE_INVALID
|
||||||
|
|
||||||
|
private var connectedBlueToothHeadset: BluetoothProfile? = null
|
||||||
|
private var wantsBluetoothConnection = false
|
||||||
|
|
||||||
|
private var bluetoothAdapter: BluetoothAdapter? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
executor.execute {
|
||||||
|
audioManager = applicationContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||||
|
}
|
||||||
|
val bm = applicationContext.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
|
||||||
|
val adapter = bm?.adapter
|
||||||
|
Timber.d("## VOIP Bluetooth adapter $adapter")
|
||||||
|
bluetoothAdapter = adapter
|
||||||
|
adapter?.getProfileProxy(applicationContext, object : BluetoothProfile.ServiceListener {
|
||||||
|
override fun onServiceDisconnected(profile: Int) {
|
||||||
|
Timber.d("## VOIP onServiceDisconnected $profile")
|
||||||
|
if (profile == BluetoothProfile.HEADSET) {
|
||||||
|
connectedBlueToothHeadset = null
|
||||||
|
configChange?.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile?) {
|
||||||
|
Timber.d("## VOIP onServiceConnected $profile , proxy:$proxy")
|
||||||
|
if (profile == BluetoothProfile.HEADSET) {
|
||||||
|
connectedBlueToothHeadset = proxy
|
||||||
|
configChange?.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, BluetoothProfile.HEADSET)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
|
||||||
|
|
||||||
|
// Called on the listener to notify if the audio focus for this listener has been changed.
|
||||||
|
// The |focusChange| value indicates whether the focus was gained, whether the focus was lost,
|
||||||
|
// and whether that loss is transient, or whether the new focus holder will hold it for an
|
||||||
|
// unknown amount of time.
|
||||||
|
Timber.v("## VOIP: Audio focus change $focusChange")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startForCall(mxCall: MxCall) {
|
||||||
|
Timber.v("## VOIP: AudioManager startForCall ${mxCall.callId}")
|
||||||
|
val audioManager = audioManager ?: return
|
||||||
|
savedIsSpeakerPhoneOn = audioManager.isSpeakerphoneOn
|
||||||
|
savedIsMicrophoneMute = audioManager.isMicrophoneMute
|
||||||
|
savedAudioMode = audioManager.mode
|
||||||
|
|
||||||
|
// Request audio playout focus (without ducking) and install listener for changes in focus.
|
||||||
|
|
||||||
|
// Remove the deprecation forces us to use 2 different method depending on API level
|
||||||
|
@Suppress("DEPRECATION") val result = audioManager.requestAudioFocus(audioFocusChangeListener,
|
||||||
|
AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
|
||||||
|
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
|
||||||
|
Timber.d("## VOIP Audio focus request granted for VOICE_CALL streams")
|
||||||
|
} else {
|
||||||
|
Timber.d("## VOIP Audio focus request failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start by setting MODE_IN_COMMUNICATION as default audio mode. It is
|
||||||
|
// required to be in this mode when playout and/or recording starts for
|
||||||
|
// best possible VoIP performance.
|
||||||
|
audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
|
||||||
|
|
||||||
|
// Always disable microphone mute during a WebRTC call.
|
||||||
|
setMicrophoneMute(false)
|
||||||
|
|
||||||
|
executor.execute {
|
||||||
|
// If there are no headset, start video output in speaker
|
||||||
|
// (you can't watch the video and have the phone close to your ear)
|
||||||
|
if (mxCall.isVideoCall && !isHeadsetOn()) {
|
||||||
|
Timber.v("##VOIP: AudioManager default to speaker ")
|
||||||
|
setCurrentSoundDevice(SoundDevice.SPEAKER)
|
||||||
|
} else {
|
||||||
|
// if a wired headset is plugged, sound will be directed to it
|
||||||
|
// (can't really force earpiece when headset is plugged)
|
||||||
|
if (isBluetoothHeadsetOn()) {
|
||||||
|
Timber.v("##VOIP: AudioManager default to WIRELESS_HEADSET ")
|
||||||
|
setCurrentSoundDevice(SoundDevice.WIRELESS_HEADSET)
|
||||||
|
// try now in case already connected?
|
||||||
|
audioManager.isBluetoothScoOn = true
|
||||||
|
} else {
|
||||||
|
Timber.v("##VOIP: AudioManager default to PHONE/HEADSET ")
|
||||||
|
setCurrentSoundDevice(if (isWiredHeadsetOn()) SoundDevice.HEADSET else SoundDevice.PHONE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAvailableSoundDevices(): List<SoundDevice> {
|
||||||
|
return ArrayList<SoundDevice>().apply {
|
||||||
|
if (isBluetoothHeadsetOn()) add(SoundDevice.WIRELESS_HEADSET)
|
||||||
|
add(if (isWiredHeadsetOn()) SoundDevice.HEADSET else SoundDevice.PHONE)
|
||||||
|
add(SoundDevice.SPEAKER)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
Timber.v("## VOIP: AudioManager stopCall")
|
||||||
|
executor.execute {
|
||||||
|
// Restore previously stored audio states.
|
||||||
|
setSpeakerphoneOn(savedIsSpeakerPhoneOn)
|
||||||
|
setMicrophoneMute(savedIsMicrophoneMute)
|
||||||
|
audioManager?.mode = savedAudioMode
|
||||||
|
|
||||||
|
connectedBlueToothHeadset?.let {
|
||||||
|
if (audioManager != null && isBluetoothHeadsetConnected(audioManager!!)) {
|
||||||
|
audioManager?.stopBluetoothSco()
|
||||||
|
audioManager?.isBluetoothScoOn = false
|
||||||
|
audioManager?.isSpeakerphoneOn = false
|
||||||
|
}
|
||||||
|
bluetoothAdapter?.closeProfileProxy(BluetoothProfile.HEADSET, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
audioManager?.mode = AudioManager.MODE_NORMAL
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
audioManager?.abandonAudioFocus(audioFocusChangeListener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCurrentSoundDevice(): SoundDevice {
|
||||||
|
val audioManager = audioManager ?: return SoundDevice.PHONE
|
||||||
|
if (audioManager.isSpeakerphoneOn) {
|
||||||
|
return SoundDevice.SPEAKER
|
||||||
|
} else {
|
||||||
|
if (isBluetoothHeadsetConnected(audioManager)) return SoundDevice.WIRELESS_HEADSET
|
||||||
|
return if (isHeadsetOn()) SoundDevice.HEADSET else SoundDevice.PHONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isBluetoothHeadsetConnected(audioManager: AudioManager) =
|
||||||
|
isBluetoothHeadsetOn()
|
||||||
|
&& !connectedBlueToothHeadset?.connectedDevices.isNullOrEmpty()
|
||||||
|
&& (wantsBluetoothConnection || audioManager.isBluetoothScoOn)
|
||||||
|
|
||||||
|
fun setCurrentSoundDevice(device: SoundDevice) {
|
||||||
|
executor.execute {
|
||||||
|
Timber.v("## VOIP setCurrentSoundDevice $device")
|
||||||
|
when (device) {
|
||||||
|
SoundDevice.HEADSET,
|
||||||
|
SoundDevice.PHONE -> {
|
||||||
|
wantsBluetoothConnection = false
|
||||||
|
if (isBluetoothHeadsetOn()) {
|
||||||
|
audioManager?.stopBluetoothSco()
|
||||||
|
audioManager?.isBluetoothScoOn = false
|
||||||
|
}
|
||||||
|
setSpeakerphoneOn(false)
|
||||||
|
}
|
||||||
|
SoundDevice.SPEAKER -> {
|
||||||
|
setSpeakerphoneOn(true)
|
||||||
|
wantsBluetoothConnection = false
|
||||||
|
audioManager?.stopBluetoothSco()
|
||||||
|
audioManager?.isBluetoothScoOn = false
|
||||||
|
}
|
||||||
|
SoundDevice.WIRELESS_HEADSET -> {
|
||||||
|
setSpeakerphoneOn(false)
|
||||||
|
// I cannot directly do it, i have to start then wait that it's connected
|
||||||
|
// to route to bt
|
||||||
|
audioManager?.startBluetoothSco()
|
||||||
|
wantsBluetoothConnection = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configChange?.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bluetoothStateChange(plugged: Boolean) {
|
||||||
|
executor.execute {
|
||||||
|
if (plugged && wantsBluetoothConnection) {
|
||||||
|
audioManager?.isBluetoothScoOn = true
|
||||||
|
} else if (!plugged && !wantsBluetoothConnection) {
|
||||||
|
audioManager?.stopBluetoothSco()
|
||||||
|
}
|
||||||
|
|
||||||
|
configChange?.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun wiredStateChange(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) {
|
||||||
|
executor.execute {
|
||||||
|
// if it's plugged and speaker is on we should route to headset
|
||||||
|
if (event.plugged && getCurrentSoundDevice() == SoundDevice.SPEAKER) {
|
||||||
|
setCurrentSoundDevice(CallAudioManager.SoundDevice.HEADSET)
|
||||||
|
} else if (!event.plugged) {
|
||||||
|
// if it's unplugged ? always route to speaker?
|
||||||
|
// this is questionable?
|
||||||
|
if (!wantsBluetoothConnection) {
|
||||||
|
setCurrentSoundDevice(SoundDevice.SPEAKER)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
configChange?.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isHeadsetOn(): Boolean {
|
||||||
|
return isWiredHeadsetOn() || isBluetoothHeadsetOn()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isWiredHeadsetOn(): Boolean {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
return audioManager?.isWiredHeadsetOn ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isBluetoothHeadsetOn(): Boolean {
|
||||||
|
Timber.v("## VOIP: AudioManager isBluetoothHeadsetOn")
|
||||||
|
try {
|
||||||
|
if (connectedBlueToothHeadset == null) return false.also {
|
||||||
|
Timber.v("## VOIP: AudioManager no connected bluetooth headset")
|
||||||
|
}
|
||||||
|
if (audioManager?.isBluetoothScoAvailableOffCall == false) return false.also {
|
||||||
|
Timber.v("## VOIP: AudioManager isBluetoothScoAvailableOffCall false")
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
Timber.e("## VOIP: AudioManager isBluetoothHeadsetOn failure ${failure.localizedMessage}")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets the speaker phone mode. */
|
||||||
|
private fun setSpeakerphoneOn(on: Boolean) {
|
||||||
|
Timber.v("## VOIP: AudioManager setSpeakerphoneOn $on")
|
||||||
|
val wasOn = audioManager?.isSpeakerphoneOn ?: false
|
||||||
|
if (wasOn == on) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
audioManager?.isSpeakerphoneOn = on
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets the microphone mute state. */
|
||||||
|
private fun setMicrophoneMute(on: Boolean) {
|
||||||
|
Timber.v("## VOIP: AudioManager setMicrophoneMute $on")
|
||||||
|
val wasMuted = audioManager?.isMicrophoneMute ?: false
|
||||||
|
if (wasMuted == on) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
audioManager?.isMicrophoneMute = on
|
||||||
|
}
|
||||||
|
|
||||||
|
/** true if the device has a telephony radio with data
|
||||||
|
* communication support. */
|
||||||
|
private fun isThisPhone(): Boolean {
|
||||||
|
return applicationContext.packageManager.hasSystemFeature(
|
||||||
|
PackageManager.FEATURE_TELEPHONY)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,138 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.features.call
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import com.airbnb.mvrx.activityViewModel
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
|
||||||
|
import kotlinx.android.synthetic.main.bottom_sheet_call_controls.*
|
||||||
|
import me.gujun.android.span.span
|
||||||
|
|
||||||
|
class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||||
|
override fun getLayoutResId() = R.layout.bottom_sheet_call_controls
|
||||||
|
|
||||||
|
private val callViewModel: VectorCallViewModel by activityViewModel()
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
callViewModel.subscribe(this) {
|
||||||
|
renderState(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
callControlsSoundDevice.clickableView.debouncedClicks {
|
||||||
|
callViewModel.handle(VectorCallViewActions.SwitchSoundDevice)
|
||||||
|
}
|
||||||
|
|
||||||
|
callControlsSwitchCamera.clickableView.debouncedClicks {
|
||||||
|
callViewModel.handle(VectorCallViewActions.ToggleCamera)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
callControlsToggleSDHD.clickableView.debouncedClicks {
|
||||||
|
callViewModel.handle(VectorCallViewActions.ToggleHDSD)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
callViewModel.observeViewEvents {
|
||||||
|
when (it) {
|
||||||
|
is VectorCallViewEvents.ShowSoundDeviceChooser -> {
|
||||||
|
showSoundDeviceChooser(it.available, it.current)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showSoundDeviceChooser(available: List<CallAudioManager.SoundDevice>, current: CallAudioManager.SoundDevice) {
|
||||||
|
val soundDevices = available.map {
|
||||||
|
when (it) {
|
||||||
|
CallAudioManager.SoundDevice.WIRELESS_HEADSET -> span {
|
||||||
|
text = getString(R.string.sound_device_wireless_headset)
|
||||||
|
textStyle = if (current == it) "bold" else "normal"
|
||||||
|
}
|
||||||
|
CallAudioManager.SoundDevice.PHONE -> span {
|
||||||
|
text = getString(R.string.sound_device_phone)
|
||||||
|
textStyle = if (current == it) "bold" else "normal"
|
||||||
|
}
|
||||||
|
CallAudioManager.SoundDevice.SPEAKER -> span {
|
||||||
|
text = getString(R.string.sound_device_speaker)
|
||||||
|
textStyle = if (current == it) "bold" else "normal"
|
||||||
|
}
|
||||||
|
CallAudioManager.SoundDevice.HEADSET -> span {
|
||||||
|
text = getString(R.string.sound_device_headset)
|
||||||
|
textStyle = if (current == it) "bold" else "normal"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AlertDialog.Builder(requireContext())
|
||||||
|
.setItems(soundDevices.toTypedArray()) { d, n ->
|
||||||
|
d.cancel()
|
||||||
|
when (soundDevices[n].toString()) {
|
||||||
|
// TODO Make an adapter and handle multiple Bluetooth headsets. Also do not use translations.
|
||||||
|
getString(R.string.sound_device_phone) -> {
|
||||||
|
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.PHONE))
|
||||||
|
}
|
||||||
|
getString(R.string.sound_device_speaker) -> {
|
||||||
|
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.SPEAKER))
|
||||||
|
}
|
||||||
|
getString(R.string.sound_device_headset) -> {
|
||||||
|
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.HEADSET))
|
||||||
|
}
|
||||||
|
getString(R.string.sound_device_wireless_headset) -> {
|
||||||
|
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.WIRELESS_HEADSET))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setNegativeButton(R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderState(state: VectorCallViewState) {
|
||||||
|
callControlsSoundDevice.title = getString(R.string.call_select_sound_device)
|
||||||
|
callControlsSoundDevice.subTitle = when (state.soundDevice) {
|
||||||
|
CallAudioManager.SoundDevice.PHONE -> getString(R.string.sound_device_phone)
|
||||||
|
CallAudioManager.SoundDevice.SPEAKER -> getString(R.string.sound_device_speaker)
|
||||||
|
CallAudioManager.SoundDevice.HEADSET -> getString(R.string.sound_device_headset)
|
||||||
|
CallAudioManager.SoundDevice.WIRELESS_HEADSET -> getString(R.string.sound_device_wireless_headset)
|
||||||
|
}
|
||||||
|
|
||||||
|
callControlsSwitchCamera.isVisible = state.isVideoCall && state.canSwitchCamera
|
||||||
|
callControlsSwitchCamera.subTitle = getString(if (state.isFrontCamera) R.string.call_camera_front else R.string.call_camera_back)
|
||||||
|
|
||||||
|
if (state.isVideoCall) {
|
||||||
|
callControlsToggleSDHD.isVisible = true
|
||||||
|
if (state.isHD) {
|
||||||
|
callControlsToggleSDHD.title = getString(R.string.call_format_turn_hd_off)
|
||||||
|
callControlsToggleSDHD.subTitle = null
|
||||||
|
callControlsToggleSDHD.leftIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_hd_disabled)
|
||||||
|
} else {
|
||||||
|
callControlsToggleSDHD.title = getString(R.string.call_format_turn_hd_on)
|
||||||
|
callControlsToggleSDHD.subTitle = null
|
||||||
|
callControlsToggleSDHD.leftIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_hd)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
callControlsToggleSDHD.isVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,160 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.features.call
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import butterknife.BindView
|
||||||
|
import butterknife.ButterKnife
|
||||||
|
import butterknife.OnClick
|
||||||
|
import im.vector.matrix.android.api.session.call.CallState
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import kotlinx.android.synthetic.main.view_call_controls.view.*
|
||||||
|
import org.webrtc.PeerConnection
|
||||||
|
|
||||||
|
class CallControlsView @JvmOverloads constructor(
|
||||||
|
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
||||||
|
) : LinearLayout(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
|
var interactionListener: InteractionListener? = null
|
||||||
|
|
||||||
|
@BindView(R.id.ringingControls)
|
||||||
|
lateinit var ringingControls: ViewGroup
|
||||||
|
|
||||||
|
@BindView(R.id.iv_icr_accept_call)
|
||||||
|
lateinit var ringingControlAccept: ImageView
|
||||||
|
|
||||||
|
@BindView(R.id.iv_icr_end_call)
|
||||||
|
lateinit var ringingControlDecline: ImageView
|
||||||
|
|
||||||
|
@BindView(R.id.connectedControls)
|
||||||
|
lateinit var connectedControls: ViewGroup
|
||||||
|
|
||||||
|
@BindView(R.id.iv_mute_toggle)
|
||||||
|
lateinit var muteIcon: ImageView
|
||||||
|
|
||||||
|
@BindView(R.id.iv_video_toggle)
|
||||||
|
lateinit var videoToggleIcon: ImageView
|
||||||
|
|
||||||
|
init {
|
||||||
|
ConstraintLayout.inflate(context, R.layout.view_call_controls, this)
|
||||||
|
// layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||||
|
ButterKnife.bind(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnClick(R.id.iv_icr_accept_call)
|
||||||
|
fun acceptIncomingCall() {
|
||||||
|
interactionListener?.didAcceptIncomingCall()
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnClick(R.id.iv_icr_end_call)
|
||||||
|
fun declineIncomingCall() {
|
||||||
|
interactionListener?.didDeclineIncomingCall()
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnClick(R.id.iv_end_call)
|
||||||
|
fun endOngoingCall() {
|
||||||
|
interactionListener?.didEndCall()
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnClick(R.id.iv_mute_toggle)
|
||||||
|
fun toggleMute() {
|
||||||
|
interactionListener?.didTapToggleMute()
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnClick(R.id.iv_video_toggle)
|
||||||
|
fun toggleVideo() {
|
||||||
|
interactionListener?.didTapToggleVideo()
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnClick(R.id.iv_leftMiniControl)
|
||||||
|
fun returnToChat() {
|
||||||
|
interactionListener?.returnToChat()
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnClick(R.id.iv_more)
|
||||||
|
fun moreControlOption() {
|
||||||
|
interactionListener?.didTapMore()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateForState(state: VectorCallViewState) {
|
||||||
|
val callState = state.callState.invoke()
|
||||||
|
if (state.isAudioMuted) {
|
||||||
|
muteIcon.setImageResource(R.drawable.ic_microphone_off)
|
||||||
|
muteIcon.contentDescription = resources.getString(R.string.a11y_unmute_microphone)
|
||||||
|
} else {
|
||||||
|
muteIcon.setImageResource(R.drawable.ic_microphone_on)
|
||||||
|
muteIcon.contentDescription = resources.getString(R.string.a11y_mute_microphone)
|
||||||
|
}
|
||||||
|
if (state.isVideoEnabled) {
|
||||||
|
videoToggleIcon.setImageResource(R.drawable.ic_video)
|
||||||
|
videoToggleIcon.contentDescription = resources.getString(R.string.a11y_stop_camera)
|
||||||
|
} else {
|
||||||
|
videoToggleIcon.setImageResource(R.drawable.ic_video_off)
|
||||||
|
videoToggleIcon.contentDescription = resources.getString(R.string.a11y_start_camera)
|
||||||
|
}
|
||||||
|
|
||||||
|
when (callState) {
|
||||||
|
is CallState.Idle,
|
||||||
|
is CallState.Dialing,
|
||||||
|
is CallState.Answering -> {
|
||||||
|
ringingControls.isVisible = true
|
||||||
|
ringingControlAccept.isVisible = false
|
||||||
|
ringingControlDecline.isVisible = true
|
||||||
|
connectedControls.isVisible = false
|
||||||
|
}
|
||||||
|
is CallState.LocalRinging -> {
|
||||||
|
ringingControls.isVisible = true
|
||||||
|
ringingControlAccept.isVisible = true
|
||||||
|
ringingControlDecline.isVisible = true
|
||||||
|
connectedControls.isVisible = false
|
||||||
|
}
|
||||||
|
is CallState.Connected -> {
|
||||||
|
if (callState.iceConnectionState == PeerConnection.PeerConnectionState.CONNECTED) {
|
||||||
|
ringingControls.isVisible = false
|
||||||
|
connectedControls.isVisible = true
|
||||||
|
iv_video_toggle.isVisible = state.isVideoCall
|
||||||
|
} else {
|
||||||
|
ringingControls.isVisible = true
|
||||||
|
ringingControlAccept.isVisible = false
|
||||||
|
ringingControlDecline.isVisible = true
|
||||||
|
connectedControls.isVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is CallState.Terminated,
|
||||||
|
null -> {
|
||||||
|
ringingControls.isVisible = false
|
||||||
|
connectedControls.isVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InteractionListener {
|
||||||
|
fun didAcceptIncomingCall()
|
||||||
|
fun didDeclineIncomingCall()
|
||||||
|
fun didEndCall()
|
||||||
|
fun didTapToggleMute()
|
||||||
|
fun didTapToggleVideo()
|
||||||
|
fun returnToChat()
|
||||||
|
fun didTapMore()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.features.call
|
||||||
|
|
||||||
|
import org.webrtc.CameraVideoCapturer
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
open class CameraEventsHandlerAdapter : CameraVideoCapturer.CameraEventsHandler {
|
||||||
|
override fun onCameraError(p0: String?) {
|
||||||
|
Timber.v("## VOIP onCameraError $p0")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCameraOpening(p0: String?) {
|
||||||
|
Timber.v("## VOIP onCameraOpening $p0")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCameraDisconnected() {
|
||||||
|
Timber.v("## VOIP onCameraOpening")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCameraFreezed(p0: String?) {
|
||||||
|
Timber.v("## VOIP onCameraFreezed $p0")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFirstFrameAvailable() {
|
||||||
|
Timber.v("## VOIP onFirstFrameAvailable")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCameraClosed() {
|
||||||
|
Timber.v("## VOIP onCameraClosed")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.features.call
|
||||||
|
|
||||||
|
enum class CameraType {
|
||||||
|
FRONT,
|
||||||
|
BACK
|
||||||
|
}
|
||||||
|
|
||||||
|
data class CameraProxy(
|
||||||
|
val name: String,
|
||||||
|
val type: CameraType
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed class CaptureFormat(val width: Int, val height: Int, val fps: Int) {
|
||||||
|
object HD : CaptureFormat(1280, 720, 30)
|
||||||
|
object SD : CaptureFormat(640, 480, 30)
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.features.call
|
||||||
|
|
||||||
|
import org.webrtc.DataChannel
|
||||||
|
import org.webrtc.IceCandidate
|
||||||
|
import org.webrtc.MediaStream
|
||||||
|
import org.webrtc.PeerConnection
|
||||||
|
import org.webrtc.RtpReceiver
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
abstract class PeerConnectionObserverAdapter : PeerConnection.Observer {
|
||||||
|
override fun onIceCandidate(p0: IceCandidate?) {
|
||||||
|
Timber.v("## VOIP onIceCandidate $p0")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDataChannel(p0: DataChannel?) {
|
||||||
|
Timber.v("## VOIP onDataChannel $p0")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onIceConnectionReceivingChange(p0: Boolean) {
|
||||||
|
Timber.v("## VOIP onIceConnectionReceivingChange $p0")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onIceConnectionChange(p0: PeerConnection.IceConnectionState?) {
|
||||||
|
Timber.v("## VOIP onIceConnectionChange $p0")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onIceGatheringChange(p0: PeerConnection.IceGatheringState?) {
|
||||||
|
Timber.v("## VOIP onIceConnectionChange $p0")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAddStream(mediaStream: MediaStream?) {
|
||||||
|
Timber.v("## VOIP onAddStream $mediaStream")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSignalingChange(p0: PeerConnection.SignalingState?) {
|
||||||
|
Timber.v("## VOIP onSignalingChange $p0")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onIceCandidatesRemoved(p0: Array<out IceCandidate>?) {
|
||||||
|
Timber.v("## VOIP onIceCandidatesRemoved $p0")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRemoveStream(mediaStream: MediaStream?) {
|
||||||
|
Timber.v("## VOIP onRemoveStream $mediaStream")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRenegotiationNeeded() {
|
||||||
|
Timber.v("## VOIP onRenegotiationNeeded")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAddTrack(p0: RtpReceiver?, p1: Array<out MediaStream>?) {
|
||||||
|
Timber.v("## VOIP onAddTrack $p0 / out: $p1")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.features.call
|
||||||
|
|
||||||
|
import org.webrtc.SdpObserver
|
||||||
|
import org.webrtc.SessionDescription
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
open class SdpObserverAdapter : SdpObserver {
|
||||||
|
override fun onSetFailure(p0: String?) {
|
||||||
|
Timber.e("## SdpObserver: onSetFailure $p0")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSetSuccess() {
|
||||||
|
Timber.v("## SdpObserver: onSetSuccess")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateSuccess(p0: SessionDescription?) {
|
||||||
|
Timber.e("## SdpObserver: onSetFailure $p0")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateFailure(p0: String?) {
|
||||||
|
Timber.e("## SdpObserver: onSetFailure $p0")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.features.call
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import im.vector.matrix.android.api.session.call.MxCall
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class SharedActiveCallViewModel @Inject constructor(
|
||||||
|
private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
val activeCall: MutableLiveData<MxCall?> = MutableLiveData()
|
||||||
|
|
||||||
|
val callStateListener = object : MxCall.StateListener {
|
||||||
|
|
||||||
|
override fun onStateUpdate(call: MxCall) {
|
||||||
|
if (activeCall.value?.callId == call.callId) {
|
||||||
|
activeCall.postValue(call)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val listener = object : WebRtcPeerConnectionManager.CurrentCallListener {
|
||||||
|
override fun onCurrentCallChange(call: MxCall?) {
|
||||||
|
activeCall.value?.removeListener(callStateListener)
|
||||||
|
activeCall.postValue(call)
|
||||||
|
call?.addListener(callStateListener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
activeCall.postValue(webRtcPeerConnectionManager.currentCall?.mxCall)
|
||||||
|
webRtcPeerConnectionManager.addCurrentCallListener(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
activeCall.value?.removeListener(callStateListener)
|
||||||
|
webRtcPeerConnectionManager.removeCurrentCallListener(listener)
|
||||||
|
super.onCleared()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,481 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.features.call
|
||||||
|
|
||||||
|
import android.app.KeyguardManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.view.Window
|
||||||
|
import android.view.WindowManager
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
|
import androidx.core.view.isInvisible
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.core.view.updatePadding
|
||||||
|
import butterknife.BindView
|
||||||
|
import com.airbnb.mvrx.Fail
|
||||||
|
import com.airbnb.mvrx.MvRx
|
||||||
|
import com.airbnb.mvrx.viewModel
|
||||||
|
import com.jakewharton.rxbinding3.view.clicks
|
||||||
|
import im.vector.matrix.android.api.session.call.CallState
|
||||||
|
import im.vector.matrix.android.api.session.call.EglUtils
|
||||||
|
import im.vector.matrix.android.api.session.call.MxCallDetail
|
||||||
|
import im.vector.matrix.android.api.session.call.TurnServerResponse
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.di.ScreenComponent
|
||||||
|
import im.vector.riotx.core.platform.VectorBaseActivity
|
||||||
|
import im.vector.riotx.core.services.CallService
|
||||||
|
import im.vector.riotx.core.utils.PERMISSIONS_FOR_AUDIO_IP_CALL
|
||||||
|
import im.vector.riotx.core.utils.PERMISSIONS_FOR_VIDEO_IP_CALL
|
||||||
|
import im.vector.riotx.core.utils.allGranted
|
||||||
|
import im.vector.riotx.core.utils.checkPermissions
|
||||||
|
import im.vector.riotx.features.home.AvatarRenderer
|
||||||
|
import im.vector.riotx.features.home.room.detail.RoomDetailActivity
|
||||||
|
import im.vector.riotx.features.home.room.detail.RoomDetailArgs
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
import kotlinx.android.synthetic.main.activity_call.*
|
||||||
|
import org.webrtc.EglBase
|
||||||
|
import org.webrtc.PeerConnection
|
||||||
|
import org.webrtc.RendererCommon
|
||||||
|
import org.webrtc.SurfaceViewRenderer
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class CallArgs(
|
||||||
|
val roomId: String,
|
||||||
|
val callId: String?,
|
||||||
|
val participantUserId: String,
|
||||||
|
val isIncomingCall: Boolean,
|
||||||
|
val isVideoCall: Boolean
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionListener {
|
||||||
|
|
||||||
|
override fun getLayoutRes() = R.layout.activity_call
|
||||||
|
|
||||||
|
@Inject lateinit var avatarRenderer: AvatarRenderer
|
||||||
|
|
||||||
|
override fun injectWith(injector: ScreenComponent) {
|
||||||
|
super.injectWith(injector)
|
||||||
|
injector.inject(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val callViewModel: VectorCallViewModel by viewModel()
|
||||||
|
private lateinit var callArgs: CallArgs
|
||||||
|
|
||||||
|
@Inject lateinit var peerConnectionManager: WebRtcPeerConnectionManager
|
||||||
|
|
||||||
|
@Inject lateinit var viewModelFactory: VectorCallViewModel.Factory
|
||||||
|
|
||||||
|
@BindView(R.id.pip_video_view)
|
||||||
|
lateinit var pipRenderer: SurfaceViewRenderer
|
||||||
|
|
||||||
|
@BindView(R.id.fullscreen_video_view)
|
||||||
|
lateinit var fullscreenRenderer: SurfaceViewRenderer
|
||||||
|
|
||||||
|
@BindView(R.id.callControls)
|
||||||
|
lateinit var callControlsView: CallControlsView
|
||||||
|
|
||||||
|
private var rootEglBase: EglBase? = null
|
||||||
|
|
||||||
|
var systemUiVisibility = false
|
||||||
|
|
||||||
|
var surfaceRenderersAreInitialized = false
|
||||||
|
|
||||||
|
override fun doBeforeSetContentView() {
|
||||||
|
// Set window styles for fullscreen-window size. Needs to be done before adding content.
|
||||||
|
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||||
|
|
||||||
|
hideSystemUI()
|
||||||
|
setContentView(R.layout.activity_call)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hideSystemUI() {
|
||||||
|
systemUiVisibility = false
|
||||||
|
// Enables regular immersive mode.
|
||||||
|
// For "lean back" mode, remove SYSTEM_UI_FLAG_IMMERSIVE.
|
||||||
|
// Or for "sticky immersive," replace it with SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||||
|
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_IMMERSIVE
|
||||||
|
// Set the content to appear under the system bars so that the
|
||||||
|
// content doesn't resize when the system bars hide and show.
|
||||||
|
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
|
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
|
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
// Hide the nav bar and status bar
|
||||||
|
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
|
or View.SYSTEM_UI_FLAG_FULLSCREEN)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shows the system bars by removing all the flags
|
||||||
|
// except for the ones that make the content appear under the system bars.
|
||||||
|
private fun showSystemUI() {
|
||||||
|
systemUiVisibility = true
|
||||||
|
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||||
|
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
|
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun toggleUiSystemVisibility() {
|
||||||
|
if (systemUiVisibility) {
|
||||||
|
hideSystemUI()
|
||||||
|
} else {
|
||||||
|
showSystemUI()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||||
|
super.onWindowFocusChanged(hasFocus)
|
||||||
|
// Rehide when bottom sheet is dismissed
|
||||||
|
if (hasFocus) {
|
||||||
|
hideSystemUI()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// This will need to be refined
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(constraintLayout) { v, insets ->
|
||||||
|
v.updatePadding(bottom = if (systemUiVisibility) insets.systemWindowInsetBottom else 0)
|
||||||
|
insets
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
|
|
||||||
|
if (intent.hasExtra(MvRx.KEY_ARG)) {
|
||||||
|
callArgs = intent.getParcelableExtra(MvRx.KEY_ARG)!!
|
||||||
|
} else {
|
||||||
|
Timber.e("## VOIP missing callArgs for VectorCall Activity")
|
||||||
|
CallService.onNoActiveCall(this)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.v("## VOIP EXTRA_MODE is ${intent.getStringExtra(EXTRA_MODE)}")
|
||||||
|
if (intent.getStringExtra(EXTRA_MODE) == INCOMING_RINGING) {
|
||||||
|
turnScreenOnAndKeyguardOff()
|
||||||
|
}
|
||||||
|
|
||||||
|
constraintLayout.clicks()
|
||||||
|
.throttleFirst(300, TimeUnit.MILLISECONDS)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe { toggleUiSystemVisibility() }
|
||||||
|
.disposeOnDestroy()
|
||||||
|
|
||||||
|
configureCallViews()
|
||||||
|
|
||||||
|
callViewModel.subscribe(this) {
|
||||||
|
renderState(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
callViewModel.viewEvents
|
||||||
|
.observe()
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe {
|
||||||
|
handleViewEvents(it)
|
||||||
|
}
|
||||||
|
.disposeOnDestroy()
|
||||||
|
|
||||||
|
if (callArgs.isVideoCall) {
|
||||||
|
if (checkPermissions(PERMISSIONS_FOR_VIDEO_IP_CALL, this, CAPTURE_PERMISSION_REQUEST_CODE, R.string.permissions_rationale_msg_camera_and_audio)) {
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (checkPermissions(PERMISSIONS_FOR_AUDIO_IP_CALL, this, CAPTURE_PERMISSION_REQUEST_CODE, R.string.permissions_rationale_msg_record_audio)) {
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
peerConnectionManager.detachRenderers(listOf(pipRenderer, fullscreenRenderer))
|
||||||
|
if (surfaceRenderersAreInitialized) {
|
||||||
|
pipRenderer.release()
|
||||||
|
fullscreenRenderer.release()
|
||||||
|
}
|
||||||
|
turnScreenOffAndKeyguardOn()
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun renderState(state: VectorCallViewState) {
|
||||||
|
Timber.v("## VOIP renderState call $state")
|
||||||
|
if (state.callState is Fail) {
|
||||||
|
// be sure to clear notification
|
||||||
|
CallService.onNoActiveCall(this)
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
callControlsView.updateForState(state)
|
||||||
|
val callState = state.callState.invoke()
|
||||||
|
callConnectingProgress.isVisible = false
|
||||||
|
when (callState) {
|
||||||
|
is CallState.Idle,
|
||||||
|
is CallState.Dialing -> {
|
||||||
|
callVideoGroup.isInvisible = true
|
||||||
|
callInfoGroup.isVisible = true
|
||||||
|
callStatusText.setText(R.string.call_ring)
|
||||||
|
configureCallInfo(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
is CallState.LocalRinging -> {
|
||||||
|
callVideoGroup.isInvisible = true
|
||||||
|
callInfoGroup.isVisible = true
|
||||||
|
callStatusText.text = null
|
||||||
|
configureCallInfo(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
is CallState.Answering -> {
|
||||||
|
callVideoGroup.isInvisible = true
|
||||||
|
callInfoGroup.isVisible = true
|
||||||
|
callStatusText.setText(R.string.call_connecting)
|
||||||
|
callConnectingProgress.isVisible = true
|
||||||
|
configureCallInfo(state)
|
||||||
|
}
|
||||||
|
is CallState.Connected -> {
|
||||||
|
if (callState.iceConnectionState == PeerConnection.PeerConnectionState.CONNECTED) {
|
||||||
|
if (callArgs.isVideoCall) {
|
||||||
|
callVideoGroup.isVisible = true
|
||||||
|
callInfoGroup.isVisible = false
|
||||||
|
pip_video_view.isVisible = !state.isVideoCaptureInError
|
||||||
|
} else {
|
||||||
|
callVideoGroup.isInvisible = true
|
||||||
|
callInfoGroup.isVisible = true
|
||||||
|
configureCallInfo(state)
|
||||||
|
callStatusText.text = null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// This state is not final, if you change network, new candidates will be sent
|
||||||
|
callVideoGroup.isInvisible = true
|
||||||
|
callInfoGroup.isVisible = true
|
||||||
|
configureCallInfo(state)
|
||||||
|
callStatusText.setText(R.string.call_connecting)
|
||||||
|
callConnectingProgress.isVisible = true
|
||||||
|
}
|
||||||
|
// ensure all attached?
|
||||||
|
peerConnectionManager.attachViewRenderers(pipRenderer, fullscreenRenderer, null)
|
||||||
|
}
|
||||||
|
is CallState.Terminated -> {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
null -> {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun configureCallInfo(state: VectorCallViewState) {
|
||||||
|
state.otherUserMatrixItem.invoke()?.let {
|
||||||
|
avatarRenderer.render(it, otherMemberAvatar)
|
||||||
|
participantNameText.text = it.getBestName()
|
||||||
|
callTypeText.setText(if (state.isVideoCall) R.string.action_video_call else R.string.action_voice_call)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun configureCallViews() {
|
||||||
|
callControlsView.interactionListener = this
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||||
|
if (requestCode == CAPTURE_PERMISSION_REQUEST_CODE && allGranted(grantResults)) {
|
||||||
|
start()
|
||||||
|
} else {
|
||||||
|
// TODO display something
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun start() {
|
||||||
|
rootEglBase = EglUtils.rootEglBase ?: return Unit.also {
|
||||||
|
Timber.v("## VOIP rootEglBase is null")
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init Picture in Picture renderer
|
||||||
|
pipRenderer.init(rootEglBase!!.eglBaseContext, null)
|
||||||
|
pipRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
|
||||||
|
|
||||||
|
// Init Full Screen renderer
|
||||||
|
fullscreenRenderer.init(rootEglBase!!.eglBaseContext, null)
|
||||||
|
fullscreenRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
|
||||||
|
|
||||||
|
pipRenderer.setZOrderMediaOverlay(true)
|
||||||
|
pipRenderer.setEnableHardwareScaler(true /* enabled */)
|
||||||
|
fullscreenRenderer.setEnableHardwareScaler(true /* enabled */)
|
||||||
|
|
||||||
|
peerConnectionManager.attachViewRenderers(pipRenderer, fullscreenRenderer,
|
||||||
|
intent.getStringExtra(EXTRA_MODE)?.takeIf { isFirstCreation() })
|
||||||
|
|
||||||
|
pipRenderer.setOnClickListener {
|
||||||
|
callViewModel.handle(VectorCallViewActions.ToggleCamera)
|
||||||
|
}
|
||||||
|
surfaceRenderersAreInitialized = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
// for newer version, it will be passed automatically to active media session
|
||||||
|
// in call service
|
||||||
|
when (keyCode) {
|
||||||
|
KeyEvent.KEYCODE_HEADSETHOOK -> {
|
||||||
|
callViewModel.handle(VectorCallViewActions.HeadSetButtonPressed)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.onKeyDown(keyCode, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleViewEvents(event: VectorCallViewEvents?) {
|
||||||
|
Timber.v("## VOIP handleViewEvents $event")
|
||||||
|
when (event) {
|
||||||
|
VectorCallViewEvents.DismissNoCall -> {
|
||||||
|
CallService.onNoActiveCall(this)
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
is VectorCallViewEvents.ConnectionTimeout -> {
|
||||||
|
onErrorTimoutConnect(event.turn)
|
||||||
|
}
|
||||||
|
null -> {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onErrorTimoutConnect(turn: TurnServerResponse?) {
|
||||||
|
Timber.d("## VOIP onErrorTimoutConnect $turn")
|
||||||
|
// TODO ask to use default stun, etc...
|
||||||
|
AlertDialog
|
||||||
|
.Builder(this)
|
||||||
|
.setTitle(R.string.call_failed_no_connection)
|
||||||
|
.setMessage(getString(R.string.call_failed_no_connection_description))
|
||||||
|
.setNegativeButton(R.string.ok) { _, _ ->
|
||||||
|
callViewModel.handle(VectorCallViewActions.EndCall)
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val CAPTURE_PERMISSION_REQUEST_CODE = 1
|
||||||
|
private const val EXTRA_MODE = "EXTRA_MODE"
|
||||||
|
|
||||||
|
const val OUTGOING_CREATED = "OUTGOING_CREATED"
|
||||||
|
const val INCOMING_RINGING = "INCOMING_RINGING"
|
||||||
|
const val INCOMING_ACCEPT = "INCOMING_ACCEPT"
|
||||||
|
|
||||||
|
fun newIntent(context: Context, mxCall: MxCallDetail): Intent {
|
||||||
|
return Intent(context, VectorCallActivity::class.java).apply {
|
||||||
|
// what could be the best flags?
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
putExtra(MvRx.KEY_ARG, CallArgs(mxCall.roomId, mxCall.callId, mxCall.otherUserId, !mxCall.isOutgoing, mxCall.isVideoCall))
|
||||||
|
putExtra(EXTRA_MODE, OUTGOING_CREATED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun newIntent(context: Context,
|
||||||
|
callId: String?,
|
||||||
|
roomId: String,
|
||||||
|
otherUserId: String,
|
||||||
|
isIncomingCall: Boolean,
|
||||||
|
isVideoCall: Boolean,
|
||||||
|
mode: String?): Intent {
|
||||||
|
return Intent(context, VectorCallActivity::class.java).apply {
|
||||||
|
// what could be the best flags?
|
||||||
|
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||||
|
putExtra(MvRx.KEY_ARG, CallArgs(roomId, callId, otherUserId, isIncomingCall, isVideoCall))
|
||||||
|
putExtra(EXTRA_MODE, mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun didAcceptIncomingCall() {
|
||||||
|
callViewModel.handle(VectorCallViewActions.AcceptCall)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun didDeclineIncomingCall() {
|
||||||
|
callViewModel.handle(VectorCallViewActions.DeclineCall)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun didEndCall() {
|
||||||
|
callViewModel.handle(VectorCallViewActions.EndCall)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun didTapToggleMute() {
|
||||||
|
callViewModel.handle(VectorCallViewActions.ToggleMute)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun didTapToggleVideo() {
|
||||||
|
callViewModel.handle(VectorCallViewActions.ToggleVideo)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun returnToChat() {
|
||||||
|
val args = RoomDetailArgs(callArgs.roomId)
|
||||||
|
val intent = RoomDetailActivity.newIntent(this, args).apply {
|
||||||
|
flags = FLAG_ACTIVITY_CLEAR_TOP
|
||||||
|
}
|
||||||
|
startActivity(intent)
|
||||||
|
// is it needed?
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun didTapMore() {
|
||||||
|
CallControlsBottomSheet().show(supportFragmentManager, "Controls")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Needed to let you answer call when phone is locked
|
||||||
|
private fun turnScreenOnAndKeyguardOff() {
|
||||||
|
Timber.v("## VOIP turnScreenOnAndKeyguardOff")
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||||
|
setShowWhenLocked(true)
|
||||||
|
setTurnScreenOn(true)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
window.addFlags(
|
||||||
|
WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
|
||||||
|
or WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
|
||||||
|
or WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
with(getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
requestDismissKeyguard(this@VectorCallActivity, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun turnScreenOffAndKeyguardOn() {
|
||||||
|
Timber.v("## VOIP turnScreenOnAndKeyguardOn")
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||||
|
setShowWhenLocked(false)
|
||||||
|
setTurnScreenOn(false)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
window.clearFlags(
|
||||||
|
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
|
||||||
|
or WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,296 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.features.call
|
||||||
|
|
||||||
|
import com.airbnb.mvrx.Async
|
||||||
|
import com.airbnb.mvrx.Fail
|
||||||
|
import com.airbnb.mvrx.Loading
|
||||||
|
import com.airbnb.mvrx.MvRxState
|
||||||
|
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||||
|
import com.airbnb.mvrx.Success
|
||||||
|
import com.airbnb.mvrx.Uninitialized
|
||||||
|
import com.airbnb.mvrx.ViewModelContext
|
||||||
|
import com.squareup.inject.assisted.Assisted
|
||||||
|
import com.squareup.inject.assisted.AssistedInject
|
||||||
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
|
import im.vector.matrix.android.api.session.Session
|
||||||
|
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.call.TurnServerResponse
|
||||||
|
import im.vector.matrix.android.api.util.MatrixItem
|
||||||
|
import im.vector.matrix.android.api.util.toMatrixItem
|
||||||
|
import im.vector.riotx.core.extensions.exhaustive
|
||||||
|
import im.vector.riotx.core.platform.VectorViewEvents
|
||||||
|
import im.vector.riotx.core.platform.VectorViewModel
|
||||||
|
import im.vector.riotx.core.platform.VectorViewModelAction
|
||||||
|
import org.webrtc.PeerConnection
|
||||||
|
import java.util.Timer
|
||||||
|
import java.util.TimerTask
|
||||||
|
|
||||||
|
data class VectorCallViewState(
|
||||||
|
val callId: String? = null,
|
||||||
|
val roomId: String = "",
|
||||||
|
val isVideoCall: Boolean,
|
||||||
|
val isAudioMuted: Boolean = false,
|
||||||
|
val isVideoEnabled: Boolean = true,
|
||||||
|
val isVideoCaptureInError: Boolean = false,
|
||||||
|
val isHD: Boolean = false,
|
||||||
|
val isFrontCamera: Boolean = true,
|
||||||
|
val canSwitchCamera: Boolean = true,
|
||||||
|
val soundDevice: CallAudioManager.SoundDevice = CallAudioManager.SoundDevice.PHONE,
|
||||||
|
val availableSoundDevices: List<CallAudioManager.SoundDevice> = emptyList(),
|
||||||
|
val otherUserMatrixItem: Async<MatrixItem> = Uninitialized,
|
||||||
|
val callState: Async<CallState> = Uninitialized
|
||||||
|
) : MvRxState
|
||||||
|
|
||||||
|
sealed class VectorCallViewActions : VectorViewModelAction {
|
||||||
|
object EndCall : VectorCallViewActions()
|
||||||
|
object AcceptCall : VectorCallViewActions()
|
||||||
|
object DeclineCall : VectorCallViewActions()
|
||||||
|
object ToggleMute : VectorCallViewActions()
|
||||||
|
object ToggleVideo : VectorCallViewActions()
|
||||||
|
data class ChangeAudioDevice(val device: CallAudioManager.SoundDevice) : VectorCallViewActions()
|
||||||
|
object SwitchSoundDevice : VectorCallViewActions()
|
||||||
|
object HeadSetButtonPressed : VectorCallViewActions()
|
||||||
|
object ToggleCamera : VectorCallViewActions()
|
||||||
|
object ToggleHDSD : VectorCallViewActions()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class VectorCallViewEvents : VectorViewEvents {
|
||||||
|
|
||||||
|
object DismissNoCall : VectorCallViewEvents()
|
||||||
|
data class ConnectionTimeout(val turn: TurnServerResponse?) : VectorCallViewEvents()
|
||||||
|
data class ShowSoundDeviceChooser(
|
||||||
|
val available: List<CallAudioManager.SoundDevice>,
|
||||||
|
val current: CallAudioManager.SoundDevice
|
||||||
|
) : VectorCallViewEvents()
|
||||||
|
// data class CallAnswered(val content: CallAnswerContent) : VectorCallViewEvents()
|
||||||
|
// data class CallHangup(val content: CallHangupContent) : VectorCallViewEvents()
|
||||||
|
// object CallAccepted : VectorCallViewEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
class VectorCallViewModel @AssistedInject constructor(
|
||||||
|
@Assisted initialState: VectorCallViewState,
|
||||||
|
@Assisted val args: CallArgs,
|
||||||
|
val session: Session,
|
||||||
|
val webRtcPeerConnectionManager: WebRtcPeerConnectionManager
|
||||||
|
) : VectorViewModel<VectorCallViewState, VectorCallViewActions, VectorCallViewEvents>(initialState) {
|
||||||
|
|
||||||
|
var call: MxCall? = null
|
||||||
|
|
||||||
|
var connectionTimoutTimer: Timer? = null
|
||||||
|
var hasBeenConnectedOnce = false
|
||||||
|
|
||||||
|
private val callStateListener = object : MxCall.StateListener {
|
||||||
|
override fun onStateUpdate(call: MxCall) {
|
||||||
|
val callState = call.state
|
||||||
|
if (callState is CallState.Connected && callState.iceConnectionState == PeerConnection.PeerConnectionState.CONNECTED) {
|
||||||
|
hasBeenConnectedOnce = true
|
||||||
|
connectionTimoutTimer?.cancel()
|
||||||
|
connectionTimoutTimer = null
|
||||||
|
} else {
|
||||||
|
// do we reset as long as it's moving?
|
||||||
|
connectionTimoutTimer?.cancel()
|
||||||
|
if (hasBeenConnectedOnce) {
|
||||||
|
connectionTimoutTimer = Timer().apply {
|
||||||
|
schedule(object : TimerTask() {
|
||||||
|
override fun run() {
|
||||||
|
session.callSignalingService().getTurnServer(object : MatrixCallback<TurnServerResponse> {
|
||||||
|
override fun onFailure(failure: Throwable) {
|
||||||
|
_viewEvents.post(VectorCallViewEvents.ConnectionTimeout(null))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSuccess(data: TurnServerResponse) {
|
||||||
|
_viewEvents.post(VectorCallViewEvents.ConnectionTimeout(data))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, 30_000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
callState = Success(callState)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val currentCallListener = object : WebRtcPeerConnectionManager.CurrentCallListener {
|
||||||
|
override fun onCurrentCallChange(call: MxCall?) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCaptureStateChanged(mgr: WebRtcPeerConnectionManager) {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
isVideoCaptureInError = mgr.capturerIsInError,
|
||||||
|
isHD = mgr.currentCaptureFormat() is CaptureFormat.HD
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAudioDevicesChange(mgr: WebRtcPeerConnectionManager) {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
availableSoundDevices = mgr.audioManager.getAvailableSoundDevices(),
|
||||||
|
soundDevice = mgr.audioManager.getCurrentSoundDevice()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCameraChange(mgr: WebRtcPeerConnectionManager) {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
canSwitchCamera = mgr.canSwitchCamera(),
|
||||||
|
isFrontCamera = mgr.currentCameraType() == CameraType.FRONT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
|
||||||
|
initialState.callId?.let {
|
||||||
|
|
||||||
|
webRtcPeerConnectionManager.addCurrentCallListener(currentCallListener)
|
||||||
|
|
||||||
|
session.callSignalingService().getCallWithId(it)?.let { mxCall ->
|
||||||
|
this.call = mxCall
|
||||||
|
mxCall.otherUserId
|
||||||
|
val item: MatrixItem? = session.getUser(mxCall.otherUserId)?.toMatrixItem()
|
||||||
|
|
||||||
|
mxCall.addListener(callStateListener)
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
isVideoCall = mxCall.isVideoCall,
|
||||||
|
callState = Success(mxCall.state),
|
||||||
|
otherUserMatrixItem = item?.let { Success(it) } ?: Uninitialized,
|
||||||
|
soundDevice = webRtcPeerConnectionManager.audioManager.getCurrentSoundDevice(),
|
||||||
|
availableSoundDevices = webRtcPeerConnectionManager.audioManager.getAvailableSoundDevices(),
|
||||||
|
isFrontCamera = webRtcPeerConnectionManager.currentCameraType() == CameraType.FRONT,
|
||||||
|
canSwitchCamera = webRtcPeerConnectionManager.canSwitchCamera(),
|
||||||
|
isHD = mxCall.isVideoCall && webRtcPeerConnectionManager.currentCaptureFormat() is CaptureFormat.HD
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} ?: run {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
callState = Fail(IllegalArgumentException("No call"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
// session.callService().removeCallListener(callServiceListener)
|
||||||
|
webRtcPeerConnectionManager.removeCurrentCallListener(currentCallListener)
|
||||||
|
this.call?.removeListener(callStateListener)
|
||||||
|
super.onCleared()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handle(action: VectorCallViewActions) = withState { state ->
|
||||||
|
when (action) {
|
||||||
|
VectorCallViewActions.EndCall -> webRtcPeerConnectionManager.endCall()
|
||||||
|
VectorCallViewActions.AcceptCall -> {
|
||||||
|
setState {
|
||||||
|
copy(callState = Loading())
|
||||||
|
}
|
||||||
|
webRtcPeerConnectionManager.acceptIncomingCall()
|
||||||
|
}
|
||||||
|
VectorCallViewActions.DeclineCall -> {
|
||||||
|
setState {
|
||||||
|
copy(callState = Loading())
|
||||||
|
}
|
||||||
|
webRtcPeerConnectionManager.endCall()
|
||||||
|
}
|
||||||
|
VectorCallViewActions.ToggleMute -> {
|
||||||
|
val muted = state.isAudioMuted
|
||||||
|
webRtcPeerConnectionManager.muteCall(!muted)
|
||||||
|
setState {
|
||||||
|
copy(isAudioMuted = !muted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VectorCallViewActions.ToggleVideo -> {
|
||||||
|
if (state.isVideoCall) {
|
||||||
|
val videoEnabled = state.isVideoEnabled
|
||||||
|
webRtcPeerConnectionManager.enableVideo(!videoEnabled)
|
||||||
|
setState {
|
||||||
|
copy(isVideoEnabled = !videoEnabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Unit
|
||||||
|
}
|
||||||
|
is VectorCallViewActions.ChangeAudioDevice -> {
|
||||||
|
webRtcPeerConnectionManager.audioManager.setCurrentSoundDevice(action.device)
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
soundDevice = webRtcPeerConnectionManager.audioManager.getCurrentSoundDevice()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VectorCallViewActions.SwitchSoundDevice -> {
|
||||||
|
_viewEvents.post(
|
||||||
|
VectorCallViewEvents.ShowSoundDeviceChooser(state.availableSoundDevices, state.soundDevice)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
VectorCallViewActions.HeadSetButtonPressed -> {
|
||||||
|
if (state.callState.invoke() is CallState.LocalRinging) {
|
||||||
|
// accept call
|
||||||
|
webRtcPeerConnectionManager.acceptIncomingCall()
|
||||||
|
}
|
||||||
|
if (state.callState.invoke() is CallState.Connected) {
|
||||||
|
// end call?
|
||||||
|
webRtcPeerConnectionManager.endCall()
|
||||||
|
}
|
||||||
|
Unit
|
||||||
|
}
|
||||||
|
VectorCallViewActions.ToggleCamera -> {
|
||||||
|
webRtcPeerConnectionManager.switchCamera()
|
||||||
|
}
|
||||||
|
VectorCallViewActions.ToggleHDSD -> {
|
||||||
|
if (!state.isVideoCall) return@withState
|
||||||
|
webRtcPeerConnectionManager.setCaptureFormat(if (state.isHD) CaptureFormat.SD else CaptureFormat.HD)
|
||||||
|
}
|
||||||
|
}.exhaustive
|
||||||
|
}
|
||||||
|
|
||||||
|
@AssistedInject.Factory
|
||||||
|
interface Factory {
|
||||||
|
fun create(initialState: VectorCallViewState, args: CallArgs): VectorCallViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object : MvRxViewModelFactory<VectorCallViewModel, VectorCallViewState> {
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
override fun create(viewModelContext: ViewModelContext, state: VectorCallViewState): VectorCallViewModel? {
|
||||||
|
val callActivity: VectorCallActivity = viewModelContext.activity()
|
||||||
|
val callArgs: CallArgs = viewModelContext.args()
|
||||||
|
return callActivity.viewModelFactory.create(state, callArgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun initialState(viewModelContext: ViewModelContext): VectorCallViewState? {
|
||||||
|
val args: CallArgs = viewModelContext.args()
|
||||||
|
return VectorCallViewState(
|
||||||
|
callId = args.callId,
|
||||||
|
roomId = args.roomId,
|
||||||
|
isVideoCall = args.isVideoCall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.features.call.service
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import im.vector.riotx.core.di.HasVectorInjector
|
||||||
|
import im.vector.riotx.features.call.WebRtcPeerConnectionManager
|
||||||
|
import im.vector.riotx.features.notifications.NotificationUtils
|
||||||
|
import im.vector.riotx.features.settings.VectorLocale.context
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
class CallHeadsUpActionReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val EXTRA_CALL_ACTION_KEY = "EXTRA_CALL_ACTION_KEY"
|
||||||
|
const val CALL_ACTION_REJECT = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private lateinit var peerConnectionManager: WebRtcPeerConnectionManager
|
||||||
|
private lateinit var notificationUtils: NotificationUtils
|
||||||
|
|
||||||
|
init {
|
||||||
|
val appContext = context.applicationContext
|
||||||
|
if (appContext is HasVectorInjector) {
|
||||||
|
peerConnectionManager = appContext.injector().webRtcPeerConnectionManager()
|
||||||
|
notificationUtils = appContext.injector().notificationUtils()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
|
when (intent?.getIntExtra(EXTRA_CALL_ACTION_KEY, 0)) {
|
||||||
|
CALL_ACTION_REJECT -> onCallRejectClicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not sure why this should be needed
|
||||||
|
// val it = Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)
|
||||||
|
// context.sendBroadcast(it)
|
||||||
|
|
||||||
|
// Close the notification after the click action is performed.
|
||||||
|
// context.stopService(Intent(context, CallHeadsUpService::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onCallRejectClicked() {
|
||||||
|
Timber.d("onCallRejectClicked")
|
||||||
|
peerConnectionManager.endCall()
|
||||||
|
}
|
||||||
|
|
||||||
|
// private fun onCallAnswerClicked(context: Context) {
|
||||||
|
// Timber.d("onCallAnswerClicked")
|
||||||
|
// peerConnectionManager.answerCall(context)
|
||||||
|
// }
|
||||||
|
}
|
|
@ -0,0 +1,112 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.features.call.telecom
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.telecom.Connection
|
||||||
|
import android.telecom.DisconnectCause
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import im.vector.riotx.features.call.VectorCallViewModel
|
||||||
|
import im.vector.riotx.features.call.WebRtcPeerConnectionManager
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M) class CallConnection(
|
||||||
|
private val context: Context,
|
||||||
|
private val roomId: String,
|
||||||
|
val callId: String
|
||||||
|
) : Connection() {
|
||||||
|
|
||||||
|
@Inject lateinit var peerConnectionManager: WebRtcPeerConnectionManager
|
||||||
|
@Inject lateinit var callViewModel: VectorCallViewModel
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
connectionProperties = PROPERTY_SELF_MANAGED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The telecom subsystem calls this method when you add a new incoming call and your app should show its incoming call UI.
|
||||||
|
*/
|
||||||
|
override fun onShowIncomingCallUi() {
|
||||||
|
super.onShowIncomingCallUi()
|
||||||
|
Timber.i("onShowIncomingCallUi")
|
||||||
|
/*
|
||||||
|
VectorCallActivity.newIntent(context, roomId).let {
|
||||||
|
context.startActivity(it)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnswer() {
|
||||||
|
super.onAnswer()
|
||||||
|
// startCall()
|
||||||
|
Timber.i("onShowIncomingCallUi")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStateChanged(state: Int) {
|
||||||
|
super.onStateChanged(state)
|
||||||
|
Timber.i("onStateChanged${stateToString(state)}")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReject() {
|
||||||
|
super.onReject()
|
||||||
|
Timber.i("onReject")
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDisconnect() {
|
||||||
|
onDisconnect()
|
||||||
|
Timber.i("onDisconnect")
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun close() {
|
||||||
|
setDisconnected(DisconnectCause(DisconnectCause.CANCELED))
|
||||||
|
destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startCall() {
|
||||||
|
/*
|
||||||
|
//peerConnectionManager.createPeerConnectionFactory()
|
||||||
|
peerConnectionManager.listener = this
|
||||||
|
|
||||||
|
val cameraIterator = if (Camera2Enumerator.isSupported(context)) Camera2Enumerator(context) else Camera1Enumerator(false)
|
||||||
|
val frontCamera = cameraIterator.deviceNames
|
||||||
|
?.firstOrNull { cameraIterator.isFrontFacing(it) }
|
||||||
|
?: cameraIterator.deviceNames?.first()
|
||||||
|
?: return
|
||||||
|
val videoCapturer = cameraIterator.createCapturer(frontCamera, null)
|
||||||
|
|
||||||
|
val iceServers = ArrayList<PeerConnection.IceServer>().apply {
|
||||||
|
listOf("turn:turn.matrix.org:3478?transport=udp", "turn:turn.matrix.org:3478?transport=tcp", "turns:turn.matrix.org:443?transport=tcp").forEach {
|
||||||
|
add(
|
||||||
|
PeerConnection.IceServer.builder(it)
|
||||||
|
.setUsername("xxxxx")
|
||||||
|
.setPassword("xxxxx")
|
||||||
|
.createIceServer()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
peerConnectionManager.createPeerConnection(videoCapturer, iceServers)
|
||||||
|
//peerConnectionManager.startCall()
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.features.call.telecom
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.telephony.TelephonyManager
|
||||||
|
|
||||||
|
object TelecomUtils {
|
||||||
|
|
||||||
|
fun isLineBusy(context: Context): Boolean {
|
||||||
|
val telephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager
|
||||||
|
?: return false
|
||||||
|
return telephonyManager.callState != TelephonyManager.CALL_STATE_IDLE
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.riotx.features.call.telecom
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.ServiceConnection
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.telecom.Connection
|
||||||
|
import android.telecom.ConnectionRequest
|
||||||
|
import android.telecom.ConnectionService
|
||||||
|
import android.telecom.PhoneAccountHandle
|
||||||
|
import android.telecom.StatusHints
|
||||||
|
import android.telecom.TelecomManager
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import im.vector.riotx.core.services.CallService
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No active calls in other apps
|
||||||
|
*
|
||||||
|
*To answer incoming calls when there are no active calls in other apps, follow these steps:
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* * Your app receives a new incoming call using its usual mechanisms.
|
||||||
|
* - Use the addNewIncomingCall(PhoneAccountHandle, Bundle) method to inform the telecom subsystem about the new incoming call.
|
||||||
|
* - The telecom subsystem binds to your app's ConnectionService implementation and requests a new instance of the
|
||||||
|
* Connection class representing the new incoming call using the onCreateIncomingConnection(PhoneAccountHandle, ConnectionRequest) method.
|
||||||
|
* - The telecom subsystem informs your app that it should show its incoming call user interface using the onShowIncomingCallUi() method.
|
||||||
|
* - Your app shows its incoming UI using a notification with an associated full-screen intent. For more information, see onShowIncomingCallUi().
|
||||||
|
* - Call the setActive() method if the user accepts the incoming call, or setDisconnected(DisconnectCause) specifying REJECTED as
|
||||||
|
* the parameter followed by a call to the destroy() method if the user rejects the incoming call.
|
||||||
|
*</pre>
|
||||||
|
*/
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M) class VectorConnectionService : ConnectionService() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The telecom subsystem calls this method in response to your app calling placeCall(Uri, Bundle) to create a new outgoing call
|
||||||
|
*/
|
||||||
|
override fun onCreateOutgoingConnection(connectionManagerPhoneAccount: PhoneAccountHandle?, request: ConnectionRequest?): Connection? {
|
||||||
|
val callId = request?.address?.encodedQuery ?: return null
|
||||||
|
val roomId = request.extras.getString("MX_CALL_ROOM_ID") ?: return null
|
||||||
|
return CallConnection(applicationContext, roomId, callId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateIncomingConnection(connectionManagerPhoneAccount: PhoneAccountHandle?, request: ConnectionRequest?): Connection {
|
||||||
|
val roomId = request?.extras?.getString("MX_CALL_ROOM_ID") ?: return super.onCreateIncomingConnection(connectionManagerPhoneAccount, request)
|
||||||
|
val callId = request.extras.getString("MX_CALL_CALL_ID") ?: return super.onCreateIncomingConnection(connectionManagerPhoneAccount, request)
|
||||||
|
|
||||||
|
val connection = CallConnection(applicationContext, roomId, callId)
|
||||||
|
connection.connectionCapabilities = Connection.CAPABILITY_MUTE
|
||||||
|
connection.audioModeIsVoip = true
|
||||||
|
connection.setAddress(Uri.fromParts("tel", "+905000000000", null), TelecomManager.PRESENTATION_ALLOWED)
|
||||||
|
connection.setCallerDisplayName("RiotX Caller", TelecomManager.PRESENTATION_ALLOWED)
|
||||||
|
connection.statusHints = StatusHints("Testing Hint...", null, null)
|
||||||
|
|
||||||
|
bindService(Intent(applicationContext, CallService::class.java), CallServiceConnection(connection), 0)
|
||||||
|
connection.setInitializing()
|
||||||
|
return CallConnection(applicationContext, roomId, callId)
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class CallServiceConnection(private val callConnection: CallConnection) : ServiceConnection {
|
||||||
|
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
|
||||||
|
val callSrvBinder = binder as CallService.CallServiceBinder
|
||||||
|
callSrvBinder.getCallService().addConnection(callConnection)
|
||||||
|
unbindService(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "TComService"
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,7 +37,12 @@ import im.vector.riotx.core.glide.GlideApp
|
||||||
import im.vector.riotx.core.platform.ToolbarConfigurable
|
import im.vector.riotx.core.platform.ToolbarConfigurable
|
||||||
import im.vector.riotx.core.platform.VectorBaseActivity
|
import im.vector.riotx.core.platform.VectorBaseActivity
|
||||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||||
|
import im.vector.riotx.core.ui.views.ActiveCallView
|
||||||
|
import im.vector.riotx.core.ui.views.ActiveCallViewHolder
|
||||||
import im.vector.riotx.core.ui.views.KeysBackupBanner
|
import im.vector.riotx.core.ui.views.KeysBackupBanner
|
||||||
|
import im.vector.riotx.features.call.SharedActiveCallViewModel
|
||||||
|
import im.vector.riotx.features.call.VectorCallActivity
|
||||||
|
import im.vector.riotx.features.call.WebRtcPeerConnectionManager
|
||||||
import im.vector.riotx.features.home.room.list.RoomListFragment
|
import im.vector.riotx.features.home.room.list.RoomListFragment
|
||||||
import im.vector.riotx.features.home.room.list.RoomListParams
|
import im.vector.riotx.features.home.room.list.RoomListParams
|
||||||
import im.vector.riotx.features.home.room.list.UnreadCounterBadgeView
|
import im.vector.riotx.features.home.room.list.UnreadCounterBadgeView
|
||||||
|
@ -46,6 +51,11 @@ import im.vector.riotx.features.popup.VerificationVectorAlert
|
||||||
import im.vector.riotx.features.settings.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS
|
import im.vector.riotx.features.settings.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS
|
||||||
import im.vector.riotx.features.workers.signout.SignOutViewModel
|
import im.vector.riotx.features.workers.signout.SignOutViewModel
|
||||||
import kotlinx.android.synthetic.main.fragment_home_detail.*
|
import kotlinx.android.synthetic.main.fragment_home_detail.*
|
||||||
|
import kotlinx.android.synthetic.main.fragment_home_detail.activeCallPiP
|
||||||
|
import kotlinx.android.synthetic.main.fragment_home_detail.activeCallPiPWrap
|
||||||
|
import kotlinx.android.synthetic.main.fragment_home_detail.activeCallView
|
||||||
|
import kotlinx.android.synthetic.main.fragment_home_detail.syncStateView
|
||||||
|
import kotlinx.android.synthetic.main.fragment_room_detail.*
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -56,8 +66,9 @@ private const val INDEX_ROOMS = 2
|
||||||
class HomeDetailFragment @Inject constructor(
|
class HomeDetailFragment @Inject constructor(
|
||||||
val homeDetailViewModelFactory: HomeDetailViewModel.Factory,
|
val homeDetailViewModelFactory: HomeDetailViewModel.Factory,
|
||||||
private val avatarRenderer: AvatarRenderer,
|
private val avatarRenderer: AvatarRenderer,
|
||||||
private val alertManager: PopupAlertManager
|
private val alertManager: PopupAlertManager,
|
||||||
) : VectorBaseFragment(), KeysBackupBanner.Delegate {
|
private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager
|
||||||
|
) : VectorBaseFragment(), KeysBackupBanner.Delegate, ActiveCallView.Callback {
|
||||||
|
|
||||||
private val unreadCounterBadgeViews = arrayListOf<UnreadCounterBadgeView>()
|
private val unreadCounterBadgeViews = arrayListOf<UnreadCounterBadgeView>()
|
||||||
|
|
||||||
|
@ -65,16 +76,21 @@ class HomeDetailFragment @Inject constructor(
|
||||||
private val unknownDeviceDetectorSharedViewModel: UnknownDeviceDetectorSharedViewModel by activityViewModel()
|
private val unknownDeviceDetectorSharedViewModel: UnknownDeviceDetectorSharedViewModel by activityViewModel()
|
||||||
|
|
||||||
private lateinit var sharedActionViewModel: HomeSharedActionViewModel
|
private lateinit var sharedActionViewModel: HomeSharedActionViewModel
|
||||||
|
private lateinit var sharedCallActionViewModel: SharedActiveCallViewModel
|
||||||
|
|
||||||
override fun getLayoutResId() = R.layout.fragment_home_detail
|
override fun getLayoutResId() = R.layout.fragment_home_detail
|
||||||
|
|
||||||
|
private val activeCallViewHolder = ActiveCallViewHolder()
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
sharedActionViewModel = activityViewModelProvider.get(HomeSharedActionViewModel::class.java)
|
sharedActionViewModel = activityViewModelProvider.get(HomeSharedActionViewModel::class.java)
|
||||||
|
sharedCallActionViewModel = activityViewModelProvider.get(SharedActiveCallViewModel::class.java)
|
||||||
|
|
||||||
setupBottomNavigationView()
|
setupBottomNavigationView()
|
||||||
setupToolbar()
|
setupToolbar()
|
||||||
setupKeysBackupBanner()
|
setupKeysBackupBanner()
|
||||||
|
setupActiveCallView()
|
||||||
|
|
||||||
withState(viewModel) {
|
withState(viewModel) {
|
||||||
// Update the navigation view if needed (for when we restore the tabs)
|
// Update the navigation view if needed (for when we restore the tabs)
|
||||||
|
@ -105,6 +121,13 @@ class HomeDetailFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sharedCallActionViewModel
|
||||||
|
.activeCall
|
||||||
|
.observe(viewLifecycleOwner, Observer {
|
||||||
|
activeCallViewHolder.updateCall(it, webRtcPeerConnectionManager)
|
||||||
|
invalidateOptionsMenu()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun promptForNewUnknownDevices(uid: String, state: UnknownDevicesState, newest: DeviceInfo) {
|
private fun promptForNewUnknownDevices(uid: String, state: UnknownDevicesState, newest: DeviceInfo) {
|
||||||
|
@ -203,6 +226,15 @@ class HomeDetailFragment @Inject constructor(
|
||||||
homeKeysBackupBanner.delegate = this
|
homeKeysBackupBanner.delegate = this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setupActiveCallView() {
|
||||||
|
activeCallViewHolder.bind(
|
||||||
|
activeCallPiP,
|
||||||
|
activeCallView,
|
||||||
|
activeCallPiPWrap,
|
||||||
|
this
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun setupToolbar() {
|
private fun setupToolbar() {
|
||||||
val parentActivity = vectorBaseActivity
|
val parentActivity = vectorBaseActivity
|
||||||
if (parentActivity is ToolbarConfigurable) {
|
if (parentActivity is ToolbarConfigurable) {
|
||||||
|
@ -283,4 +315,20 @@ class HomeDetailFragment @Inject constructor(
|
||||||
RoomListDisplayMode.ROOMS -> R.id.bottom_action_rooms
|
RoomListDisplayMode.ROOMS -> R.id.bottom_action_rooms
|
||||||
else -> R.id.bottom_action_home
|
else -> R.id.bottom_action_home
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onTapToReturnToCall() {
|
||||||
|
sharedCallActionViewModel.activeCall.value?.let { call ->
|
||||||
|
VectorCallActivity.newIntent(
|
||||||
|
context = requireContext(),
|
||||||
|
callId = call.callId,
|
||||||
|
roomId = call.roomId,
|
||||||
|
otherUserId = call.otherUserId,
|
||||||
|
isIncomingCall = !call.isOutgoing,
|
||||||
|
isVideoCall = call.isVideoCall,
|
||||||
|
mode = null
|
||||||
|
).let {
|
||||||
|
startActivity(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,6 +68,8 @@ sealed class RoomDetailAction : VectorViewModelAction {
|
||||||
|
|
||||||
object ClearSendQueue : RoomDetailAction()
|
object ClearSendQueue : RoomDetailAction()
|
||||||
object ResendAll : RoomDetailAction()
|
object ResendAll : RoomDetailAction()
|
||||||
|
data class StartCall(val isVideo: Boolean) : RoomDetailAction()
|
||||||
|
object EndCall : RoomDetailAction()
|
||||||
|
|
||||||
data class AcceptVerificationRequest(val transactionId: String, val otherUserId: String) : RoomDetailAction()
|
data class AcceptVerificationRequest(val transactionId: String, val otherUserId: String) : RoomDetailAction()
|
||||||
data class DeclineVerificationRequest(val transactionId: String, val otherUserId: String) : RoomDetailAction()
|
data class DeclineVerificationRequest(val transactionId: String, val otherUserId: String) : RoomDetailAction()
|
||||||
|
|
|
@ -42,6 +42,7 @@ import androidx.core.util.Pair
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.forEach
|
import androidx.core.view.forEach
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
@ -100,10 +101,14 @@ import im.vector.riotx.core.glide.GlideApp
|
||||||
import im.vector.riotx.core.intent.getMimeTypeFromUri
|
import im.vector.riotx.core.intent.getMimeTypeFromUri
|
||||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||||
import im.vector.riotx.core.resources.ColorProvider
|
import im.vector.riotx.core.resources.ColorProvider
|
||||||
|
import im.vector.riotx.core.ui.views.ActiveCallView
|
||||||
|
import im.vector.riotx.core.ui.views.ActiveCallViewHolder
|
||||||
import im.vector.riotx.core.ui.views.JumpToReadMarkerView
|
import im.vector.riotx.core.ui.views.JumpToReadMarkerView
|
||||||
import im.vector.riotx.core.ui.views.NotificationAreaView
|
import im.vector.riotx.core.ui.views.NotificationAreaView
|
||||||
import im.vector.riotx.core.utils.Debouncer
|
import im.vector.riotx.core.utils.Debouncer
|
||||||
import im.vector.riotx.core.utils.KeyboardStateUtils
|
import im.vector.riotx.core.utils.KeyboardStateUtils
|
||||||
|
import im.vector.riotx.core.utils.PERMISSIONS_FOR_AUDIO_IP_CALL
|
||||||
|
import im.vector.riotx.core.utils.PERMISSIONS_FOR_VIDEO_IP_CALL
|
||||||
import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES
|
import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES
|
||||||
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_DOWNLOAD_FILE
|
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_DOWNLOAD_FILE
|
||||||
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_INCOMING_URI
|
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_INCOMING_URI
|
||||||
|
@ -117,6 +122,8 @@ import im.vector.riotx.core.utils.createJSonViewerStyleProvider
|
||||||
import im.vector.riotx.core.utils.createUIHandler
|
import im.vector.riotx.core.utils.createUIHandler
|
||||||
import im.vector.riotx.core.utils.getColorFromUserId
|
import im.vector.riotx.core.utils.getColorFromUserId
|
||||||
import im.vector.riotx.core.utils.isValidUrl
|
import im.vector.riotx.core.utils.isValidUrl
|
||||||
|
import im.vector.riotx.core.utils.onPermissionResultAudioIpCall
|
||||||
|
import im.vector.riotx.core.utils.onPermissionResultVideoIpCall
|
||||||
import im.vector.riotx.core.utils.openUrlInExternalBrowser
|
import im.vector.riotx.core.utils.openUrlInExternalBrowser
|
||||||
import im.vector.riotx.core.utils.saveMedia
|
import im.vector.riotx.core.utils.saveMedia
|
||||||
import im.vector.riotx.core.utils.shareMedia
|
import im.vector.riotx.core.utils.shareMedia
|
||||||
|
@ -127,6 +134,9 @@ import im.vector.riotx.features.attachments.ContactAttachment
|
||||||
import im.vector.riotx.features.attachments.preview.AttachmentsPreviewActivity
|
import im.vector.riotx.features.attachments.preview.AttachmentsPreviewActivity
|
||||||
import im.vector.riotx.features.attachments.preview.AttachmentsPreviewArgs
|
import im.vector.riotx.features.attachments.preview.AttachmentsPreviewArgs
|
||||||
import im.vector.riotx.features.attachments.toGroupedContentAttachmentData
|
import im.vector.riotx.features.attachments.toGroupedContentAttachmentData
|
||||||
|
import im.vector.riotx.features.call.SharedActiveCallViewModel
|
||||||
|
import im.vector.riotx.features.call.VectorCallActivity
|
||||||
|
import im.vector.riotx.features.call.WebRtcPeerConnectionManager
|
||||||
import im.vector.riotx.features.command.Command
|
import im.vector.riotx.features.command.Command
|
||||||
import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreActivity
|
import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreActivity
|
||||||
import im.vector.riotx.features.crypto.util.toImageRes
|
import im.vector.riotx.features.crypto.util.toImageRes
|
||||||
|
@ -196,17 +206,22 @@ class RoomDetailFragment @Inject constructor(
|
||||||
val roomDetailViewModelFactory: RoomDetailViewModel.Factory,
|
val roomDetailViewModelFactory: RoomDetailViewModel.Factory,
|
||||||
private val eventHtmlRenderer: EventHtmlRenderer,
|
private val eventHtmlRenderer: EventHtmlRenderer,
|
||||||
private val vectorPreferences: VectorPreferences,
|
private val vectorPreferences: VectorPreferences,
|
||||||
private val colorProvider: ColorProvider) :
|
private val colorProvider: ColorProvider,
|
||||||
|
private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager) :
|
||||||
VectorBaseFragment(),
|
VectorBaseFragment(),
|
||||||
TimelineEventController.Callback,
|
TimelineEventController.Callback,
|
||||||
VectorInviteView.Callback,
|
VectorInviteView.Callback,
|
||||||
JumpToReadMarkerView.Callback,
|
JumpToReadMarkerView.Callback,
|
||||||
AttachmentTypeSelectorView.Callback,
|
AttachmentTypeSelectorView.Callback,
|
||||||
AttachmentsHelper.Callback,
|
AttachmentsHelper.Callback,
|
||||||
RoomWidgetsBannerView.Callback {
|
RoomWidgetsBannerView.Callback,
|
||||||
|
ActiveCallView.Callback {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
private const val AUDIO_CALL_PERMISSION_REQUEST_CODE = 1
|
||||||
|
private const val VIDEO_CALL_PERMISSION_REQUEST_CODE = 2
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sanitize the display name.
|
* Sanitize the display name.
|
||||||
*
|
*
|
||||||
|
@ -243,6 +258,8 @@ class RoomDetailFragment @Inject constructor(
|
||||||
override fun getMenuRes() = R.menu.menu_timeline
|
override fun getMenuRes() = R.menu.menu_timeline
|
||||||
|
|
||||||
private lateinit var sharedActionViewModel: MessageSharedActionViewModel
|
private lateinit var sharedActionViewModel: MessageSharedActionViewModel
|
||||||
|
private lateinit var sharedCallActionViewModel: SharedActiveCallViewModel
|
||||||
|
|
||||||
private lateinit var layoutManager: LinearLayoutManager
|
private lateinit var layoutManager: LinearLayoutManager
|
||||||
private lateinit var jumpToBottomViewVisibilityManager: JumpToBottomViewVisibilityManager
|
private lateinit var jumpToBottomViewVisibilityManager: JumpToBottomViewVisibilityManager
|
||||||
private var modelBuildListener: OnModelBuildFinishedListener? = null
|
private var modelBuildListener: OnModelBuildFinishedListener? = null
|
||||||
|
@ -255,10 +272,12 @@ class RoomDetailFragment @Inject constructor(
|
||||||
private lateinit var attachmentTypeSelector: AttachmentTypeSelectorView
|
private lateinit var attachmentTypeSelector: AttachmentTypeSelectorView
|
||||||
|
|
||||||
private var lockSendButton = false
|
private var lockSendButton = false
|
||||||
|
private val activeCallViewHolder = ActiveCallViewHolder()
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java)
|
sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java)
|
||||||
|
sharedCallActionViewModel = activityViewModelProvider.get(SharedActiveCallViewModel::class.java)
|
||||||
attachmentsHelper = AttachmentsHelper(requireContext(), this).register()
|
attachmentsHelper = AttachmentsHelper(requireContext(), this).register()
|
||||||
keyboardStateUtils = KeyboardStateUtils(requireActivity())
|
keyboardStateUtils = KeyboardStateUtils(requireActivity())
|
||||||
setupToolbar(roomToolbar)
|
setupToolbar(roomToolbar)
|
||||||
|
@ -267,6 +286,7 @@ class RoomDetailFragment @Inject constructor(
|
||||||
setupInviteView()
|
setupInviteView()
|
||||||
setupNotificationView()
|
setupNotificationView()
|
||||||
setupJumpToReadMarkerView()
|
setupJumpToReadMarkerView()
|
||||||
|
setupActiveCallView()
|
||||||
setupJumpToBottomView()
|
setupJumpToBottomView()
|
||||||
setupWidgetsBannerView()
|
setupWidgetsBannerView()
|
||||||
|
|
||||||
|
@ -281,6 +301,13 @@ class RoomDetailFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
.disposeOnDestroyView()
|
.disposeOnDestroyView()
|
||||||
|
|
||||||
|
sharedCallActionViewModel
|
||||||
|
.activeCall
|
||||||
|
.observe(viewLifecycleOwner, Observer {
|
||||||
|
activeCallViewHolder.updateCall(it, webRtcPeerConnectionManager)
|
||||||
|
invalidateOptionsMenu()
|
||||||
|
})
|
||||||
|
|
||||||
roomDetailViewModel.selectSubscribe(this, RoomDetailViewState::tombstoneEventHandling, uniqueOnly("tombstoneEventHandling")) {
|
roomDetailViewModel.selectSubscribe(this, RoomDetailViewState::tombstoneEventHandling, uniqueOnly("tombstoneEventHandling")) {
|
||||||
renderTombstoneEventHandling(it)
|
renderTombstoneEventHandling(it)
|
||||||
}
|
}
|
||||||
|
@ -380,6 +407,7 @@ class RoomDetailFragment @Inject constructor(
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
timelineEventController.callback = null
|
timelineEventController.callback = null
|
||||||
timelineEventController.removeModelBuildListener(modelBuildListener)
|
timelineEventController.removeModelBuildListener(modelBuildListener)
|
||||||
|
activeCallView.callback = null
|
||||||
modelBuildListener = null
|
modelBuildListener = null
|
||||||
autoCompleter.clear()
|
autoCompleter.clear()
|
||||||
debouncer.cancelAll()
|
debouncer.cancelAll()
|
||||||
|
@ -389,6 +417,7 @@ class RoomDetailFragment @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
activeCallViewHolder.unBind(webRtcPeerConnectionManager)
|
||||||
roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState)
|
roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState)
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
@ -418,6 +447,15 @@ class RoomDetailFragment @Inject constructor(
|
||||||
jumpToReadMarkerView.callback = this
|
jumpToReadMarkerView.callback = this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setupActiveCallView() {
|
||||||
|
activeCallViewHolder.bind(
|
||||||
|
activeCallPiP,
|
||||||
|
activeCallView,
|
||||||
|
activeCallPiPWrap,
|
||||||
|
this
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun navigateToEvent(action: RoomDetailViewEvents.NavigateToEvent) {
|
private fun navigateToEvent(action: RoomDetailViewEvents.NavigateToEvent) {
|
||||||
val scrollPosition = timelineEventController.searchPositionOfEvent(action.eventId)
|
val scrollPosition = timelineEventController.searchPositionOfEvent(action.eventId)
|
||||||
if (scrollPosition == null) {
|
if (scrollPosition == null) {
|
||||||
|
@ -481,6 +519,29 @@ class RoomDetailFragment @Inject constructor(
|
||||||
roomDetailViewModel.handle(RoomDetailAction.OpenIntegrationManager)
|
roomDetailViewModel.handle(RoomDetailAction.OpenIntegrationManager)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
R.id.voice_call,
|
||||||
|
R.id.video_call -> {
|
||||||
|
val activeCall = sharedCallActionViewModel.activeCall.value
|
||||||
|
val isVideoCall = item.itemId == R.id.video_call
|
||||||
|
if (activeCall != null) {
|
||||||
|
// resume existing if same room, if not prompt to kill and then restart new call?
|
||||||
|
if (activeCall.roomId == roomDetailArgs.roomId) {
|
||||||
|
onTapToReturnToCall()
|
||||||
|
}
|
||||||
|
// else {
|
||||||
|
// TODO might not work well, and should prompt
|
||||||
|
// webRtcPeerConnectionManager.endCall()
|
||||||
|
// safeStartCall(it, isVideoCall)
|
||||||
|
// }
|
||||||
|
} else {
|
||||||
|
safeStartCall(isVideoCall)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
R.id.hangup_call -> {
|
||||||
|
roomDetailViewModel.handle(RoomDetailAction.EndCall)
|
||||||
|
true
|
||||||
|
}
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -496,6 +557,26 @@ class RoomDetailFragment @Inject constructor(
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun safeStartCall(isVideoCall: Boolean) {
|
||||||
|
val startCallAction = RoomDetailAction.StartCall(isVideoCall)
|
||||||
|
roomDetailViewModel.pendingAction = startCallAction
|
||||||
|
if (isVideoCall) {
|
||||||
|
if (checkPermissions(PERMISSIONS_FOR_VIDEO_IP_CALL,
|
||||||
|
this, VIDEO_CALL_PERMISSION_REQUEST_CODE,
|
||||||
|
R.string.permissions_rationale_msg_camera_and_audio)) {
|
||||||
|
roomDetailViewModel.pendingAction = null
|
||||||
|
roomDetailViewModel.handle(startCallAction)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (checkPermissions(PERMISSIONS_FOR_AUDIO_IP_CALL,
|
||||||
|
this, AUDIO_CALL_PERMISSION_REQUEST_CODE,
|
||||||
|
R.string.permissions_rationale_msg_record_audio)) {
|
||||||
|
roomDetailViewModel.pendingAction = null
|
||||||
|
roomDetailViewModel.handle(startCallAction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun renderRegularMode(text: String) {
|
private fun renderRegularMode(text: String) {
|
||||||
autoCompleter.exitSpecialMode()
|
autoCompleter.exitSpecialMode()
|
||||||
composerLayout.collapse()
|
composerLayout.collapse()
|
||||||
|
@ -739,6 +820,7 @@ class RoomDetailFragment @Inject constructor(
|
||||||
|
|
||||||
override fun invalidate() = withState(roomDetailViewModel) { state ->
|
override fun invalidate() = withState(roomDetailViewModel) { state ->
|
||||||
renderRoomSummary(state)
|
renderRoomSummary(state)
|
||||||
|
invalidateOptionsMenu()
|
||||||
val summary = state.asyncRoomSummary()
|
val summary = state.asyncRoomSummary()
|
||||||
val inviter = state.asyncInviter()
|
val inviter = state.asyncInviter()
|
||||||
if (summary?.membership == Membership.JOIN) {
|
if (summary?.membership == Membership.JOIN) {
|
||||||
|
@ -1090,6 +1172,22 @@ class RoomDetailFragment @Inject constructor(
|
||||||
launchAttachmentProcess(pendingType)
|
launchAttachmentProcess(pendingType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
AUDIO_CALL_PERMISSION_REQUEST_CODE -> {
|
||||||
|
if (onPermissionResultAudioIpCall(requireContext(), grantResults)) {
|
||||||
|
(roomDetailViewModel.pendingAction as? RoomDetailAction.StartCall)?.let {
|
||||||
|
roomDetailViewModel.pendingAction = null
|
||||||
|
roomDetailViewModel.handle(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VIDEO_CALL_PERMISSION_REQUEST_CODE -> {
|
||||||
|
if (onPermissionResultVideoIpCall(requireContext(), grantResults)) {
|
||||||
|
(roomDetailViewModel.pendingAction as? RoomDetailAction.StartCall)?.let {
|
||||||
|
roomDetailViewModel.pendingAction = null
|
||||||
|
roomDetailViewModel.handle(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Reset all pending data
|
// Reset all pending data
|
||||||
|
@ -1473,4 +1571,20 @@ class RoomDetailFragment @Inject constructor(
|
||||||
RoomWidgetsBottomSheet.newInstance()
|
RoomWidgetsBottomSheet.newInstance()
|
||||||
.show(childFragmentManager, "ROOM_WIDGETS_BOTTOM_SHEET")
|
.show(childFragmentManager, "ROOM_WIDGETS_BOTTOM_SHEET")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onTapToReturnToCall() {
|
||||||
|
sharedCallActionViewModel.activeCall.value?.let { call ->
|
||||||
|
VectorCallActivity.newIntent(
|
||||||
|
context = requireContext(),
|
||||||
|
callId = call.callId,
|
||||||
|
roomId = call.roomId,
|
||||||
|
otherUserId = call.otherUserId,
|
||||||
|
isIncomingCall = !call.isOutgoing,
|
||||||
|
isVideoCall = call.isVideoCall,
|
||||||
|
mode = null
|
||||||
|
).let {
|
||||||
|
startActivity(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue