mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 18:35:40 +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>coroutine</w>
|
||||
<w>decryptor</w>
|
||||
<w>displayname</w>
|
||||
<w>emoji</w>
|
||||
<w>emojis</w>
|
||||
<w>fdroid</w>
|
||||
|
|
|
@ -2,7 +2,8 @@ Changes in RiotX 0.23.0 (2020-XX-XX)
|
|||
===================================================
|
||||
|
||||
Features ✨:
|
||||
-
|
||||
- Call with WebRTC support (##611)
|
||||
- Add capability to change the display name (#1529)
|
||||
|
||||
Improvements 🙌:
|
||||
- "Add Matrix app" menu is now always visible (#1495)
|
||||
|
@ -10,6 +11,7 @@ Improvements 🙌:
|
|||
Bugfix 🐛:
|
||||
- Fix dark theme issue on login screen (#1097)
|
||||
- Incomplete predicate in RealmCryptoStore#getOutgoingRoomKeyRequest (#1519)
|
||||
- Use vendor prefix for non merged MSC (#1537)
|
||||
|
||||
Translations 🗣:
|
||||
-
|
||||
|
@ -22,7 +24,10 @@ Build 🧱:
|
|||
- SDK is now API level 21 minimum, and so RiotX (#405)
|
||||
|
||||
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)
|
||||
===================================================
|
||||
|
|
|
@ -31,7 +31,7 @@ To create a new screen:
|
|||
- Then right click on the package, and select `New/New Vector/RiotX Feature`.
|
||||
- Follow the Wizard, especially replace `Main` by something more relevant to your feature.
|
||||
- Click on `Finish`.
|
||||
- Remainning steps are described as TODO in the generated files, or will be pointed out by the compilator, or at runtime :)
|
||||
- Remaining steps are described as TODO in the generated files, or will be pointed out by the compilator, or at runtime :)
|
||||
|
||||
Note that if the templates are modified, the only things to do is to restart Android Studio for the change to take effect.
|
||||
|
||||
|
|
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
|
||||
implementation 'com.googlecode.libphonenumber:libphonenumber:8.10.23'
|
||||
|
||||
// Web RTC
|
||||
// TODO meant for development purposes only. See http://webrtc.github.io/webrtc-org/native-code/android/
|
||||
implementation 'org.webrtc:google-webrtc:1.0.+'
|
||||
|
||||
debugImplementation 'com.airbnb.okreplay:okreplay:1.5.0'
|
||||
releaseImplementation 'com.airbnb.okreplay:noop:1.5.0'
|
||||
androidTestImplementation 'com.airbnb.okreplay:espresso:1.5.0'
|
||||
|
|
|
@ -241,14 +241,14 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
|||
val eventWireContent = event.content.toContent()
|
||||
assertNotNull(eventWireContent)
|
||||
|
||||
assertNull(eventWireContent.get("body"))
|
||||
assertEquals(MXCRYPTO_ALGORITHM_MEGOLM, eventWireContent.get("algorithm"))
|
||||
assertNull(eventWireContent["body"])
|
||||
assertEquals(MXCRYPTO_ALGORITHM_MEGOLM, eventWireContent["algorithm"])
|
||||
|
||||
assertNotNull(eventWireContent.get("ciphertext"))
|
||||
assertNotNull(eventWireContent.get("session_id"))
|
||||
assertNotNull(eventWireContent.get("sender_key"))
|
||||
assertNotNull(eventWireContent["ciphertext"])
|
||||
assertNotNull(eventWireContent["session_id"])
|
||||
assertNotNull(eventWireContent["sender_key"])
|
||||
|
||||
assertEquals(senderSession.sessionParams.deviceId, eventWireContent.get("device_id"))
|
||||
assertEquals(senderSession.sessionParams.deviceId, eventWireContent["device_id"])
|
||||
|
||||
assertNotNull(event.eventId)
|
||||
assertEquals(roomId, event.roomId)
|
||||
|
@ -257,7 +257,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
|||
|
||||
val eventContent = event.toContent()
|
||||
assertNotNull(eventContent)
|
||||
assertEquals(clearMessage, eventContent.get("body"))
|
||||
assertEquals(clearMessage, eventContent["body"])
|
||||
assertEquals(senderSession.myUserId, event.senderId)
|
||||
}
|
||||
|
||||
|
|
|
@ -252,7 +252,7 @@ class KeyShareTests : InstrumentedTest {
|
|||
}
|
||||
})
|
||||
|
||||
val txId: String = "m.testVerif12"
|
||||
val txId = "m.testVerif12"
|
||||
aliceVerificationService2.beginKeyVerification(VerificationMethod.SAS, aliceSession1.myUserId, aliceSession1.sessionParams.deviceId
|
||||
?: "", txId)
|
||||
|
||||
|
|
|
@ -144,7 +144,7 @@ class QuadSTests : InstrumentedTest {
|
|||
|
||||
val secretAccountData = assertAccountData(aliceSession, "secret.of.life")
|
||||
|
||||
val encryptedContent = secretAccountData.content.get("encrypted") as? Map<*, *>
|
||||
val encryptedContent = secretAccountData.content["encrypted"] as? Map<*, *>
|
||||
assertNotNull("Element should be encrypted", encryptedContent)
|
||||
assertNotNull("Secret should be encrypted with default key", encryptedContent?.get(keyId))
|
||||
|
||||
|
|
|
@ -69,7 +69,7 @@ data class HomeServerConnectionConfig(
|
|||
*/
|
||||
fun withHomeServerUri(hsUri: Uri): Builder {
|
||||
if (hsUri.scheme != "http" && hsUri.scheme != "https") {
|
||||
throw RuntimeException("Invalid home server URI: " + hsUri)
|
||||
throw RuntimeException("Invalid home server URI: $hsUri")
|
||||
}
|
||||
// ensure trailing /
|
||||
val hsString = hsUri.toString().ensureTrailingSlash()
|
||||
|
|
|
@ -16,10 +16,15 @@
|
|||
|
||||
package im.vector.matrix.android.api.extensions
|
||||
|
||||
inline fun <A> tryThis(operation: () -> A): A? {
|
||||
import timber.log.Timber
|
||||
|
||||
inline fun <A> tryThis(message: String? = null, operation: () -> A): A? {
|
||||
return try {
|
||||
operation()
|
||||
} catch (any: Throwable) {
|
||||
if (message != null) {
|
||||
Timber.e(any, message)
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -87,14 +87,13 @@ class EventMatchCondition(
|
|||
// Very simple glob to regexp converter
|
||||
private fun simpleGlobToRegExp(glob: String): String {
|
||||
var out = "" // "^"
|
||||
for (i in 0 until glob.length) {
|
||||
val c = glob[i]
|
||||
when (c) {
|
||||
for (element in glob) {
|
||||
when (element) {
|
||||
'*' -> out += ".*"
|
||||
'?' -> out += '.'.toString()
|
||||
'.' -> out += "\\."
|
||||
'\\' -> out += "\\\\"
|
||||
else -> out += c
|
||||
else -> out += element
|
||||
}
|
||||
}
|
||||
out += "" // '$'.toString()
|
||||
|
|
|
@ -24,6 +24,7 @@ import im.vector.matrix.android.api.pushrules.PushRuleService
|
|||
import im.vector.matrix.android.api.session.account.AccountService
|
||||
import im.vector.matrix.android.api.session.accountdata.AccountDataService
|
||||
import im.vector.matrix.android.api.session.cache.CacheService
|
||||
import im.vector.matrix.android.api.session.call.CallSignalingService
|
||||
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
|
@ -165,6 +166,11 @@ interface Session :
|
|||
*/
|
||||
fun integrationManagerService(): IntegrationManagerService
|
||||
|
||||
/**
|
||||
* Returns the call signaling service associated with the session
|
||||
*/
|
||||
fun callSignalingService(): CallSignalingService
|
||||
|
||||
/**
|
||||
* Add a listener to the session.
|
||||
* @param listener the listener to add.
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.call
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
|
||||
interface CallSignalingService {
|
||||
|
||||
fun getTurnServer(callback: MatrixCallback<TurnServerResponse>): Cancelable
|
||||
|
||||
/**
|
||||
* Create an outgoing call
|
||||
*/
|
||||
fun createOutgoingCall(roomId: String, otherUserId: String, isVideoCall: Boolean): MxCall
|
||||
|
||||
fun addCallListener(listener: CallsListener)
|
||||
|
||||
fun removeCallListener(listener: CallsListener)
|
||||
|
||||
fun getCallWithId(callId: String) : MxCall?
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.call
|
||||
|
||||
import org.webrtc.PeerConnection
|
||||
|
||||
sealed class CallState {
|
||||
|
||||
/** Idle, setting up objects */
|
||||
object Idle : CallState()
|
||||
|
||||
/** Dialing. Outgoing call is signaling the remote peer */
|
||||
object Dialing : CallState()
|
||||
|
||||
/** Local ringing. Incoming call offer received */
|
||||
object LocalRinging : CallState()
|
||||
|
||||
/** Answering. Incoming call is responding to remote peer */
|
||||
object Answering : CallState()
|
||||
|
||||
/**
|
||||
* Connected. Incoming/Outgoing call, ice layer connecting or connected
|
||||
* Notice that the PeerState failed is not always final, if you switch network, new ice candidtates
|
||||
* could be exchanged, and the connection could go back to connected
|
||||
* */
|
||||
data class Connected(val iceConnectionState: PeerConnection.PeerConnectionState) : CallState()
|
||||
|
||||
/** Terminated. Incoming/Outgoing call, the call is terminated */
|
||||
object Terminated : CallState()
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.call
|
||||
|
||||
import im.vector.matrix.android.api.session.room.model.call.CallAnswerContent
|
||||
import im.vector.matrix.android.api.session.room.model.call.CallCandidatesContent
|
||||
import im.vector.matrix.android.api.session.room.model.call.CallHangupContent
|
||||
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
|
||||
|
||||
interface CallsListener {
|
||||
/**
|
||||
* Called when there is an incoming call within the room.
|
||||
*/
|
||||
fun onCallInviteReceived(mxCall: MxCall, callInviteContent: CallInviteContent)
|
||||
|
||||
fun onCallIceCandidateReceived(mxCall: MxCall, iceCandidatesContent: CallCandidatesContent)
|
||||
|
||||
/**
|
||||
* An outgoing call is started.
|
||||
*/
|
||||
fun onCallAnswerReceived(callAnswerContent: CallAnswerContent)
|
||||
|
||||
/**
|
||||
* Called when a called has been hung up
|
||||
*/
|
||||
fun onCallHangupReceived(callHangupContent: CallHangupContent)
|
||||
|
||||
fun onCallManagedByOtherSession(callId: String)
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.matrix.android.api.session.call
|
||||
|
||||
import org.webrtc.EglBase
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* The root [EglBase] instance shared by the entire application for
|
||||
* the sake of reducing the utilization of system resources (such as EGL
|
||||
* contexts)
|
||||
* by performing a runtime check.
|
||||
*/
|
||||
object EglUtils {
|
||||
|
||||
// TODO how do we release that?
|
||||
|
||||
/**
|
||||
* Lazily creates and returns the one and only [EglBase] which will
|
||||
* serve as the root for all contexts that are needed.
|
||||
*/
|
||||
@get:Synchronized var rootEglBase: EglBase? = null
|
||||
get() {
|
||||
if (field == null) {
|
||||
val configAttributes = EglBase.CONFIG_PLAIN
|
||||
try {
|
||||
field = EglBase.createEgl14(configAttributes)
|
||||
?: EglBase.createEgl10(configAttributes) // Fall back to EglBase10.
|
||||
} catch (ex: Throwable) {
|
||||
Timber.e(ex, "Failed to create EglBase")
|
||||
}
|
||||
}
|
||||
return field
|
||||
}
|
||||
private set
|
||||
|
||||
val rootEglBaseContext: EglBase.Context?
|
||||
get() {
|
||||
val eglBase = rootEglBase
|
||||
return eglBase?.eglBaseContext
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.call
|
||||
|
||||
import org.webrtc.IceCandidate
|
||||
import org.webrtc.SessionDescription
|
||||
|
||||
interface MxCallDetail {
|
||||
val callId: String
|
||||
val isOutgoing: Boolean
|
||||
val roomId: String
|
||||
val otherUserId: String
|
||||
val isVideoCall: Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Define both an incoming call and on outgoing call
|
||||
*/
|
||||
interface MxCall : MxCallDetail {
|
||||
|
||||
var state: CallState
|
||||
/**
|
||||
* Pick Up the incoming call
|
||||
* It has no effect on outgoing call
|
||||
*/
|
||||
fun accept(sdp: SessionDescription)
|
||||
|
||||
/**
|
||||
* Reject an incoming call
|
||||
* It's an alias to hangUp
|
||||
*/
|
||||
fun reject() = hangUp()
|
||||
|
||||
/**
|
||||
* End the call
|
||||
*/
|
||||
fun hangUp()
|
||||
|
||||
/**
|
||||
* Start a call
|
||||
* Send offer SDP to the other participant.
|
||||
*/
|
||||
fun offerSdp(sdp: SessionDescription)
|
||||
|
||||
/**
|
||||
* Send Ice candidate to the other participant.
|
||||
*/
|
||||
fun sendLocalIceCandidates(candidates: List<IceCandidate>)
|
||||
|
||||
/**
|
||||
* Send removed ICE candidates to the other participant.
|
||||
*/
|
||||
fun sendLocalIceCandidateRemovals(candidates: List<IceCandidate>)
|
||||
|
||||
fun addListener(listener: StateListener)
|
||||
fun removeListener(listener: StateListener)
|
||||
|
||||
interface StateListener {
|
||||
fun onStateUpdate(call: MxCall)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.call
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
// TODO Should not be exposed
|
||||
/**
|
||||
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-voip-turnserver
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TurnServerResponse(
|
||||
/**
|
||||
* Required. The username to use.
|
||||
*/
|
||||
@Json(name = "username") val username: String?,
|
||||
|
||||
/**
|
||||
* Required. The password to use.
|
||||
*/
|
||||
@Json(name = "password") val password: String?,
|
||||
|
||||
/**
|
||||
* Required. A list of TURN URIs
|
||||
*/
|
||||
@Json(name = "uris") val uris: List<String>?,
|
||||
|
||||
/**
|
||||
* Required. The time-to-live in seconds
|
||||
*/
|
||||
@Json(name = "ttl") val ttl: Int?
|
||||
)
|
|
@ -58,7 +58,6 @@ object EventType {
|
|||
const val STATE_ROOM_ENCRYPTION = "m.room.encryption"
|
||||
|
||||
// Call Events
|
||||
|
||||
const val CALL_INVITE = "m.call.invite"
|
||||
const val CALL_CANDIDATES = "m.call.candidates"
|
||||
const val CALL_ANSWER = "m.call.answer"
|
||||
|
|
|
@ -26,5 +26,5 @@ object RelationType {
|
|||
/** Lets you define an event which references an existing event.*/
|
||||
const val REFERENCE = "m.reference"
|
||||
/** Lets you define an event which adds a response to an existing event.*/
|
||||
const val RESPONSE = "m.response"
|
||||
const val RESPONSE = "org.matrix.response"
|
||||
}
|
||||
|
|
|
@ -35,12 +35,19 @@ interface ProfileService {
|
|||
}
|
||||
|
||||
/**
|
||||
* Return the current dispayname for this user
|
||||
* Return the current display name for this user
|
||||
* @param userId the userId param to look for
|
||||
*
|
||||
*/
|
||||
fun getDisplayName(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable
|
||||
|
||||
/**
|
||||
* Update the display name for this user
|
||||
* @param userId the userId to update the display name of
|
||||
* @param newDisplayName the new display name of the user
|
||||
*/
|
||||
fun setDisplayName(userId: String, newDisplayName: String, matrixCallback: MatrixCallback<Unit>): Cancelable
|
||||
|
||||
/**
|
||||
* Return the current avatarUrl for this user.
|
||||
* @param userId the userId param to look for
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package im.vector.matrix.android.api.session.room
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import im.vector.matrix.android.api.session.room.call.RoomCallService
|
||||
import im.vector.matrix.android.api.session.room.crypto.RoomCryptoService
|
||||
import im.vector.matrix.android.api.session.room.members.MembershipService
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
|
@ -47,6 +48,7 @@ interface Room :
|
|||
StateService,
|
||||
UploadsService,
|
||||
ReportingService,
|
||||
RoomCallService,
|
||||
RelationService,
|
||||
RoomCryptoService,
|
||||
RoomPushRuleService {
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.room.call
|
||||
|
||||
/**
|
||||
* This interface defines methods to handle calls in a room. It's implemented at the room level.
|
||||
*/
|
||||
interface RoomCallService {
|
||||
/**
|
||||
* Return true if calls (audio or video) can be performed on this Room
|
||||
*/
|
||||
fun canStartCall(): Boolean
|
||||
}
|
|
@ -62,6 +62,9 @@ data class RoomSummary constructor(
|
|||
val isFavorite: Boolean
|
||||
get() = tags.any { it.name == RoomTag.ROOM_TAG_FAVOURITE }
|
||||
|
||||
val canStartCall: Boolean
|
||||
get() = isDirect && joinedMembersCount == 2
|
||||
|
||||
companion object {
|
||||
const val NOT_IN_BREADCRUMBS = -1
|
||||
}
|
||||
|
|
|
@ -19,16 +19,34 @@ package im.vector.matrix.android.api.session.room.model.call
|
|||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* This event is sent by the callee when they wish to answer the call.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CallAnswerContent(
|
||||
/**
|
||||
* Required. The ID of the call this event relates to.
|
||||
*/
|
||||
@Json(name = "call_id") val callId: String,
|
||||
@Json(name = "version") val version: Int,
|
||||
@Json(name = "answer") val answer: Answer
|
||||
/**
|
||||
* Required. The session description object
|
||||
*/
|
||||
@Json(name = "answer") val answer: Answer,
|
||||
/**
|
||||
* Required. The version of the VoIP specification this messages adheres to. This specification is version 0.
|
||||
*/
|
||||
@Json(name = "version") val version: Int = 0
|
||||
) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Answer(
|
||||
@Json(name = "type") val type: String,
|
||||
/**
|
||||
* Required. The type of session description. Must be 'answer'.
|
||||
*/
|
||||
@Json(name = "type") val type: SdpType = SdpType.ANSWER,
|
||||
/**
|
||||
* Required. The SDP text of the session description.
|
||||
*/
|
||||
@Json(name = "sdp") val sdp: String
|
||||
)
|
||||
}
|
||||
|
|
|
@ -19,17 +19,39 @@ package im.vector.matrix.android.api.session.room.model.call
|
|||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* This event is sent by callers after sending an invite and by the callee after answering.
|
||||
* Its purpose is to give the other party additional ICE candidates to try using to communicate.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CallCandidatesContent(
|
||||
/**
|
||||
* Required. The ID of the call this event relates to.
|
||||
*/
|
||||
@Json(name = "call_id") val callId: String,
|
||||
@Json(name = "version") val version: Int,
|
||||
@Json(name = "candidates") val candidates: List<Candidate> = emptyList()
|
||||
/**
|
||||
* Required. Array of objects describing the candidates.
|
||||
*/
|
||||
@Json(name = "candidates") val candidates: List<Candidate> = emptyList(),
|
||||
/**
|
||||
* Required. The version of the VoIP specification this messages adheres to. This specification is version 0.
|
||||
*/
|
||||
@Json(name = "version") val version: Int = 0
|
||||
) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Candidate(
|
||||
/**
|
||||
* Required. The SDP media type this candidate is intended for.
|
||||
*/
|
||||
@Json(name = "sdpMid") val sdpMid: String,
|
||||
@Json(name = "sdpMLineIndex") val sdpMLineIndex: String,
|
||||
/**
|
||||
* Required. The index of the SDP 'm' line this candidate is intended for.
|
||||
*/
|
||||
@Json(name = "sdpMLineIndex") val sdpMLineIndex: Int,
|
||||
/**
|
||||
* Required. The SDP 'a' line of the candidate.
|
||||
*/
|
||||
@Json(name = "candidate") val candidate: String
|
||||
)
|
||||
}
|
||||
|
|
|
@ -19,8 +19,32 @@ package im.vector.matrix.android.api.session.room.model.call
|
|||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Sent by either party to signal their termination of the call. This can be sent either once
|
||||
* the call has been established or before to abort the call.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CallHangupContent(
|
||||
/**
|
||||
* Required. The ID of the call this event relates to.
|
||||
*/
|
||||
@Json(name = "call_id") val callId: String,
|
||||
@Json(name = "version") val version: Int
|
||||
)
|
||||
/**
|
||||
* Required. The version of the VoIP specification this message adheres to. This specification is version 0.
|
||||
*/
|
||||
@Json(name = "version") val version: Int = 0,
|
||||
/**
|
||||
* Optional error reason for the hangup. This should not be provided when the user naturally ends or rejects the call.
|
||||
* When there was an error in the call negotiation, this should be `ice_failed` for when ICE negotiation fails
|
||||
* or `invite_timeout` for when the other party did not answer in time. One of: ["ice_failed", "invite_timeout"]
|
||||
*/
|
||||
@Json(name = "reason") val reason: Reason? = null
|
||||
) {
|
||||
enum class Reason {
|
||||
@Json(name = "ice_failed")
|
||||
ICE_FAILED,
|
||||
|
||||
@Json(name = "invite_timeout")
|
||||
INVITE_TIMEOUT
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,23 +19,45 @@ package im.vector.matrix.android.api.session.room.model.call
|
|||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* This event is sent by the caller when they wish to establish a call.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CallInviteContent(
|
||||
@Json(name = "call_id") val callId: String,
|
||||
@Json(name = "version") val version: Int,
|
||||
@Json(name = "lifetime") val lifetime: Int,
|
||||
@Json(name = "offer") val offer: Offer
|
||||
/**
|
||||
* Required. A unique identifier for the call.
|
||||
*/
|
||||
@Json(name = "call_id") val callId: String?,
|
||||
/**
|
||||
* Required. The session description object
|
||||
*/
|
||||
@Json(name = "offer") val offer: Offer?,
|
||||
/**
|
||||
* Required. The version of the VoIP specification this message adheres to. This specification is version 0.
|
||||
*/
|
||||
@Json(name = "version") val version: Int? = 0,
|
||||
/**
|
||||
* Required. The time in milliseconds that the invite is valid for.
|
||||
* Once the invite age exceeds this value, clients should discard it.
|
||||
* They should also no longer show the call as awaiting an answer in the UI.
|
||||
*/
|
||||
@Json(name = "lifetime") val lifetime: Int?
|
||||
) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Offer(
|
||||
@Json(name = "type") val type: String,
|
||||
@Json(name = "sdp") val sdp: String
|
||||
/**
|
||||
* Required. The type of session description. Must be 'offer'.
|
||||
*/
|
||||
@Json(name = "type") val type: SdpType? = SdpType.OFFER,
|
||||
/**
|
||||
* Required. The SDP text of the session description.
|
||||
*/
|
||||
@Json(name = "sdp") val sdp: String?
|
||||
) {
|
||||
companion object {
|
||||
const val SDP_VIDEO = "m=video"
|
||||
}
|
||||
}
|
||||
|
||||
fun isVideo(): Boolean = offer.sdp.contains(Offer.SDP_VIDEO)
|
||||
fun isVideo(): Boolean = offer?.sdp?.contains(Offer.SDP_VIDEO) == true
|
||||
}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.api.session.room.model.call
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
|
||||
enum class SdpType {
|
||||
@Json(name = "offer")
|
||||
OFFER,
|
||||
|
||||
@Json(name = "answer")
|
||||
ANSWER
|
||||
}
|
|
@ -67,6 +67,7 @@ import im.vector.matrix.android.internal.crypto.tasks.DefaultEncryptEventTask
|
|||
import im.vector.matrix.android.internal.crypto.tasks.DefaultGetDeviceInfoTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultGetDevicesTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultInitializeCrossSigningTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultSendEventTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultSendToDeviceTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultSendVerificationMessageTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.DefaultSetDeviceNameTask
|
||||
|
@ -80,6 +81,7 @@ import im.vector.matrix.android.internal.crypto.tasks.EncryptEventTask
|
|||
import im.vector.matrix.android.internal.crypto.tasks.GetDeviceInfoTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.GetDevicesTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.InitializeCrossSigningTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SendEventTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SendVerificationMessageTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SetDeviceNameTask
|
||||
|
@ -251,4 +253,7 @@ internal abstract class CryptoModule {
|
|||
|
||||
@Binds
|
||||
abstract fun bindInitializeCrossSigningTask(task: DefaultInitializeCrossSigningTask): InitializeCrossSigningTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindSendEventTask(task: DefaultSendEventTask): SendEventTask
|
||||
}
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.matrix.android.internal.crypto.tasks
|
||||
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.session.room.RoomAPI
|
||||
import im.vector.matrix.android.internal.session.room.send.LocalEchoUpdater
|
||||
import im.vector.matrix.android.internal.session.room.send.SendResponse
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface SendEventTask : Task<SendEventTask.Params, String> {
|
||||
data class Params(
|
||||
val event: Event,
|
||||
val cryptoService: CryptoService?
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultSendEventTask @Inject constructor(
|
||||
private val localEchoUpdater: LocalEchoUpdater,
|
||||
private val encryptEventTask: DefaultEncryptEventTask,
|
||||
private val roomAPI: RoomAPI,
|
||||
private val eventBus: EventBus) : SendEventTask {
|
||||
|
||||
override suspend fun execute(params: SendEventTask.Params): String {
|
||||
val event = handleEncryption(params)
|
||||
val localId = event.eventId!!
|
||||
|
||||
try {
|
||||
localEchoUpdater.updateSendState(localId, SendState.SENDING)
|
||||
val executeRequest = executeRequest<SendResponse>(eventBus) {
|
||||
apiCall = roomAPI.send(
|
||||
localId,
|
||||
roomId = event.roomId ?: "",
|
||||
content = event.content,
|
||||
eventType = event.type
|
||||
)
|
||||
}
|
||||
localEchoUpdater.updateSendState(localId, SendState.SENT)
|
||||
return executeRequest.eventId
|
||||
} catch (e: Throwable) {
|
||||
localEchoUpdater.updateSendState(localId, SendState.UNDELIVERED)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleEncryption(params: SendEventTask.Params): Event {
|
||||
if (params.cryptoService?.isRoomEncrypted(params.event.roomId ?: "") == true) {
|
||||
try {
|
||||
return encryptEventTask.execute(EncryptEventTask.Params(
|
||||
params.event.roomId ?: "",
|
||||
params.event,
|
||||
listOf("m.relates_to"),
|
||||
params.cryptoService
|
||||
))
|
||||
} catch (throwable: Throwable) {
|
||||
// We said it's ok to send verification request in clear
|
||||
}
|
||||
}
|
||||
return params.event
|
||||
}
|
||||
}
|
|
@ -28,7 +28,7 @@ import kotlin.math.ceil
|
|||
*/
|
||||
object HkdfSha256 {
|
||||
|
||||
public fun deriveSecret(inputKeyMaterial: ByteArray, salt: ByteArray?, info: ByteArray, outputLength: Int): ByteArray {
|
||||
fun deriveSecret(inputKeyMaterial: ByteArray, salt: ByteArray?, info: ByteArray, outputLength: Int): ByteArray {
|
||||
return expand(extract(salt, inputKeyMaterial), info, outputLength)
|
||||
}
|
||||
|
||||
|
|
|
@ -273,7 +273,7 @@ internal abstract class SASDefaultVerificationTransaction(
|
|||
if (keyIDNoPrefix == otherCrossSigningMasterKeyPublic) {
|
||||
// Check the signature
|
||||
val mac = macUsingAgreedMethod(otherCrossSigningMasterKeyPublic, baseInfo + it)
|
||||
if (mac != theirMacSafe.mac.get(it)) {
|
||||
if (mac != theirMacSafe.mac[it]) {
|
||||
// WRONG!
|
||||
Timber.e("## SAS Verification: mac mismatch for MasterKey with id $keyIDNoPrefix")
|
||||
cancel(CancelCode.MismatchedKeys)
|
||||
|
|
|
@ -85,7 +85,7 @@ internal class SendVerificationMessageWorker(context: Context,
|
|||
private const val OUTPUT_KEY_FAILED = "failed"
|
||||
|
||||
fun hasFailed(outputData: Data): Boolean {
|
||||
return outputData.getBoolean(SendVerificationMessageWorker.OUTPUT_KEY_FAILED, false)
|
||||
return outputData.getBoolean(OUTPUT_KEY_FAILED, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ internal object TimelineEventFilter {
|
|||
*/
|
||||
internal object Content {
|
||||
internal const val EDIT = """{*"m.relates_to"*"rel_type":*"m.replace"*}"""
|
||||
internal const val RESPONSE = """{*"m.relates_to"*"rel_type":*"m.response"*}"""
|
||||
internal const val RESPONSE = """{*"m.relates_to"*"rel_type":*"org.matrix.response"*}"""
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
* Copyright 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -22,12 +22,13 @@ import kotlinx.coroutines.CancellationException
|
|||
import kotlinx.coroutines.delay
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import retrofit2.Call
|
||||
import retrofit2.awaitResponse
|
||||
import java.io.IOException
|
||||
|
||||
internal suspend inline fun <DATA> executeRequest(eventBus: EventBus?,
|
||||
internal suspend inline fun <DATA : Any> executeRequest(eventBus: EventBus?,
|
||||
block: Request<DATA>.() -> Unit) = Request<DATA>(eventBus).apply(block).execute()
|
||||
|
||||
internal class Request<DATA>(private val eventBus: EventBus?) {
|
||||
internal class Request<DATA : Any>(private val eventBus: EventBus?) {
|
||||
|
||||
var isRetryable = false
|
||||
var initialDelay: Long = 100L
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
*
|
||||
* * Copyright 2019 New Vector Ltd
|
||||
* * Copyright 2020 New Vector Ltd
|
||||
* *
|
||||
* * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* * you may not use this file except in compliance with the License.
|
||||
|
@ -26,8 +26,6 @@ import im.vector.matrix.android.internal.di.MoshiProvider
|
|||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import okhttp3.ResponseBody
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
|
@ -35,23 +33,6 @@ import java.net.HttpURLConnection
|
|||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
internal suspend fun <T> Call<T>.awaitResponse(): Response<T> {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
continuation.invokeOnCancellation {
|
||||
cancel()
|
||||
}
|
||||
enqueue(object : Callback<T> {
|
||||
override fun onResponse(call: Call<T>, response: Response<T>) {
|
||||
continuation.resume(response)
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<T>, t: Throwable) {
|
||||
continuation.resumeWithException(t)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun okhttp3.Call.awaitResponse(): okhttp3.Response {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
continuation.invokeOnCancellation {
|
||||
|
|
|
@ -33,7 +33,7 @@ data class Fingerprint(
|
|||
|
||||
@Throws(CertificateException::class)
|
||||
fun matchesCert(cert: X509Certificate): Boolean {
|
||||
var o: Fingerprint? = when (hashType) {
|
||||
val o: Fingerprint? = when (hashType) {
|
||||
HashType.SHA256 -> newSha256Fingerprint(cert)
|
||||
HashType.SHA1 -> newSha1Fingerprint(cert)
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ import im.vector.matrix.android.api.session.Session
|
|||
import im.vector.matrix.android.api.session.account.AccountService
|
||||
import im.vector.matrix.android.api.session.accountdata.AccountDataService
|
||||
import im.vector.matrix.android.api.session.cache.CacheService
|
||||
import im.vector.matrix.android.api.session.call.CallSignalingService
|
||||
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
|
||||
import im.vector.matrix.android.api.session.content.ContentUrlResolver
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
|
@ -110,7 +111,8 @@ internal class DefaultSession @Inject constructor(
|
|||
private val integrationManagerService: IntegrationManagerService,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val widgetDependenciesHolder: WidgetDependenciesHolder,
|
||||
private val shieldTrustUpdater: ShieldTrustUpdater)
|
||||
private val shieldTrustUpdater: ShieldTrustUpdater,
|
||||
private val callSignalingService: Lazy<CallSignalingService>)
|
||||
: Session,
|
||||
RoomService by roomService.get(),
|
||||
RoomDirectoryService by roomDirectoryService.get(),
|
||||
|
@ -245,6 +247,8 @@ internal class DefaultSession @Inject constructor(
|
|||
|
||||
override fun integrationManagerService() = integrationManagerService
|
||||
|
||||
override fun callSignalingService(): CallSignalingService = callSignalingService.get()
|
||||
|
||||
override fun addListener(listener: Session.Listener) {
|
||||
sessionListeners.addListener(listener)
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.di.SessionAssistedInjectModule
|
|||
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
|
||||
import im.vector.matrix.android.internal.session.account.AccountModule
|
||||
import im.vector.matrix.android.internal.session.cache.CacheModule
|
||||
import im.vector.matrix.android.internal.session.call.CallModule
|
||||
import im.vector.matrix.android.internal.session.content.ContentModule
|
||||
import im.vector.matrix.android.internal.session.content.UploadContentWorker
|
||||
import im.vector.matrix.android.internal.session.filter.FilterModule
|
||||
|
@ -83,7 +84,8 @@ import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
|||
AccountDataModule::class,
|
||||
ProfileModule::class,
|
||||
SessionAssistedInjectModule::class,
|
||||
AccountModule::class
|
||||
AccountModule::class,
|
||||
CallModule::class
|
||||
]
|
||||
)
|
||||
@SessionScope
|
||||
|
|
|
@ -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.token.AccessTokenProvider
|
||||
import im.vector.matrix.android.internal.network.token.HomeserverAccessTokenProvider
|
||||
import im.vector.matrix.android.internal.session.call.CallEventObserver
|
||||
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
|
||||
import im.vector.matrix.android.internal.session.homeserver.DefaultHomeServerCapabilitiesService
|
||||
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
|
||||
|
@ -243,6 +244,10 @@ internal abstract class SessionModule {
|
|||
@IntoSet
|
||||
abstract fun bindEventRelationsAggregationUpdater(updater: EventRelationsAggregationUpdater): LiveEntityObserver
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
abstract fun bindCallEventObserver(observer: CallEventObserver): LiveEntityObserver
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
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,
|
||||
private val monarchy: Monarchy,
|
||||
private val refreshUserThreePidsTask: RefreshUserThreePidsTask,
|
||||
private val getProfileInfoTask: GetProfileInfoTask) : ProfileService {
|
||||
private val getProfileInfoTask: GetProfileInfoTask,
|
||||
private val setDisplayNameTask: SetDisplayNameTask) : ProfileService {
|
||||
|
||||
override fun getDisplayName(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable {
|
||||
val params = GetProfileInfoTask.Params(userId)
|
||||
|
@ -54,6 +55,14 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
|
|||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun setDisplayName(userId: String, newDisplayName: String, matrixCallback: MatrixCallback<Unit>): Cancelable {
|
||||
return setDisplayNameTask
|
||||
.configureWith(SetDisplayNameTask.Params(userId = userId, newDisplayName = newDisplayName)) {
|
||||
callback = matrixCallback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override fun getAvatarUrl(userId: String, matrixCallback: MatrixCallback<Optional<String>>): Cancelable {
|
||||
val params = GetProfileInfoTask.Params(userId)
|
||||
return getProfileInfoTask
|
||||
|
|
|
@ -23,6 +23,7 @@ import retrofit2.Call
|
|||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.PUT
|
||||
import retrofit2.http.Path
|
||||
|
||||
internal interface ProfileAPI {
|
||||
|
@ -42,6 +43,12 @@ internal interface ProfileAPI {
|
|||
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/3pid")
|
||||
fun getThreePIDs(): Call<AccountThreePidsResponse>
|
||||
|
||||
/**
|
||||
* Change user display name
|
||||
*/
|
||||
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "profile/{userId}/displayname")
|
||||
fun setDisplayName(@Path("userId") userId: String, @Body body: SetDisplayNameBody): Call<Unit>
|
||||
|
||||
/**
|
||||
* Bind a threePid
|
||||
* Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-account-3pid-bind
|
||||
|
|
|
@ -51,4 +51,7 @@ internal abstract class ProfileModule {
|
|||
|
||||
@Binds
|
||||
abstract fun bindUnbindThreePidsTask(task: DefaultUnbindThreePidsTask): UnbindThreePidsTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindSetDisplayNameTask(task: DefaultSetDisplayNameTask): SetDisplayNameTask
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.matrix.android.internal.session.profile
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class SetDisplayNameBody(
|
||||
/**
|
||||
* The new display name for this user.
|
||||
*/
|
||||
@Json(name = "displayname")
|
||||
val displayName: String
|
||||
)
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.session.profile
|
||||
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import javax.inject.Inject
|
||||
|
||||
internal abstract class SetDisplayNameTask : Task<SetDisplayNameTask.Params, Unit> {
|
||||
data class Params(
|
||||
val userId: String,
|
||||
val newDisplayName: String
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultSetDisplayNameTask @Inject constructor(
|
||||
private val profileAPI: ProfileAPI,
|
||||
private val eventBus: EventBus) : SetDisplayNameTask() {
|
||||
|
||||
override suspend fun execute(params: Params) {
|
||||
return executeRequest(eventBus) {
|
||||
val body = SetDisplayNameBody(
|
||||
displayName = params.newDisplayName
|
||||
)
|
||||
apiCall = profileAPI.setDisplayName(params.userId, body)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.room.Room
|
||||
import im.vector.matrix.android.api.session.room.call.RoomCallService
|
||||
import im.vector.matrix.android.api.session.room.members.MembershipService
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.android.api.session.room.model.relation.RelationService
|
||||
|
@ -58,6 +59,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
|
|||
private val stateService: StateService,
|
||||
private val uploadsService: UploadsService,
|
||||
private val reportingService: ReportingService,
|
||||
private val roomCallService: RoomCallService,
|
||||
private val readService: ReadService,
|
||||
private val typingService: TypingService,
|
||||
private val tagsService: TagsService,
|
||||
|
@ -74,6 +76,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
|
|||
StateService by stateService,
|
||||
UploadsService by uploadsService,
|
||||
ReportingService by reportingService,
|
||||
RoomCallService by roomCallService,
|
||||
ReadService by readService,
|
||||
TypingService by typingService,
|
||||
TagsService by tagsService,
|
||||
|
|
|
@ -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.internal.database.mapper.RoomSummaryMapper
|
||||
import im.vector.matrix.android.internal.session.SessionScope
|
||||
import im.vector.matrix.android.internal.session.room.call.DefaultRoomCallService
|
||||
import im.vector.matrix.android.internal.session.room.draft.DefaultDraftService
|
||||
import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService
|
||||
import im.vector.matrix.android.internal.session.room.notification.DefaultRoomPushRuleService
|
||||
|
@ -51,6 +52,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona
|
|||
private val stateServiceFactory: DefaultStateService.Factory,
|
||||
private val uploadsServiceFactory: DefaultUploadsService.Factory,
|
||||
private val reportingServiceFactory: DefaultReportingService.Factory,
|
||||
private val roomCallServiceFactory: DefaultRoomCallService.Factory,
|
||||
private val readServiceFactory: DefaultReadService.Factory,
|
||||
private val typingServiceFactory: DefaultTypingService.Factory,
|
||||
private val tagsServiceFactory: DefaultTagsService.Factory,
|
||||
|
@ -72,6 +74,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona
|
|||
stateService = stateServiceFactory.create(roomId),
|
||||
uploadsService = uploadsServiceFactory.create(roomId),
|
||||
reportingService = reportingServiceFactory.create(roomId),
|
||||
roomCallService = roomCallServiceFactory.create(roomId),
|
||||
readService = readServiceFactory.create(roomId),
|
||||
typingService = typingServiceFactory.create(roomId),
|
||||
tagsService = tagsServiceFactory.create(roomId),
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.session.room.call
|
||||
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.matrix.android.api.extensions.orFalse
|
||||
import im.vector.matrix.android.api.session.room.call.RoomCallService
|
||||
import im.vector.matrix.android.internal.session.room.RoomGetter
|
||||
|
||||
internal class DefaultRoomCallService @AssistedInject constructor(
|
||||
@Assisted private val roomId: String,
|
||||
private val roomGetter: RoomGetter
|
||||
) : RoomCallService {
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
fun create(roomId: String): RoomCallService
|
||||
}
|
||||
|
||||
override fun canStartCall(): Boolean {
|
||||
return roomGetter.getRoom(roomId)?.roomSummary()?.canStartCall.orFalse()
|
||||
}
|
||||
}
|
|
@ -51,7 +51,7 @@ internal class DefaultRoomPushRuleService @AssistedInject constructor(@Assisted
|
|||
override fun setRoomNotificationState(roomNotificationState: RoomNotificationState, matrixCallback: MatrixCallback<Unit>): Cancelable {
|
||||
return setRoomNotificationStateTask
|
||||
.configureWith(SetRoomNotificationStateTask.Params(roomId, roomNotificationState)) {
|
||||
this.callback = callback
|
||||
this.callback = matrixCallback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
|
|
@ -58,7 +58,8 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
private val localEchoEventFactory: LocalEchoEventFactory,
|
||||
private val cryptoService: CryptoService,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val localEchoRepository: LocalEchoRepository
|
||||
private val localEchoRepository: LocalEchoRepository,
|
||||
private val roomEventSender: RoomEventSender
|
||||
) : SendService {
|
||||
|
||||
@AssistedInject.Factory
|
||||
|
@ -111,20 +112,6 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
.let { sendEvent(it) }
|
||||
}
|
||||
|
||||
private fun sendEvent(event: Event): Cancelable {
|
||||
// Encrypted room handling
|
||||
return if (cryptoService.isRoomEncrypted(roomId)) {
|
||||
Timber.v("Send event in encrypted room")
|
||||
val encryptWork = createEncryptEventWork(event, true)
|
||||
// Note that event will be replaced by the result of the previous work
|
||||
val sendWork = createSendEventWork(event, false)
|
||||
timelineSendEventWorkCommon.postSequentialWorks(roomId, encryptWork, sendWork)
|
||||
} else {
|
||||
val sendWork = createSendEventWork(event, true)
|
||||
timelineSendEventWorkCommon.postWork(roomId, sendWork)
|
||||
}
|
||||
}
|
||||
|
||||
override fun sendMedias(attachments: List<ContentAttachmentData>,
|
||||
compressBeforeSending: Boolean,
|
||||
roomIds: Set<String>): Cancelable {
|
||||
|
@ -269,6 +256,10 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||
return cancelableBag
|
||||
}
|
||||
|
||||
private fun sendEvent(event: Event): Cancelable {
|
||||
return roomEventSender.sendEvent(event)
|
||||
}
|
||||
|
||||
private fun createLocalEcho(event: Event) {
|
||||
localEchoEventFactory.createLocalEcho(event)
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ internal class MarkdownParser @Inject constructor(
|
|||
fun parse(text: String): TextContent {
|
||||
// If no special char are detected, just return plain text
|
||||
if (text.contains(mdSpecialChars).not()) {
|
||||
return TextContent(text.toString())
|
||||
return TextContent(text)
|
||||
}
|
||||
|
||||
val document = parser.parse(text)
|
||||
|
@ -56,7 +56,7 @@ internal class MarkdownParser @Inject constructor(
|
|||
val plainText = textContentRenderer.render(document)
|
||||
TextContent(plainText, cleanHtmlText.postTreatment())
|
||||
} else {
|
||||
TextContent(text.toString())
|
||||
TextContent(text)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.internal.session.room.send
|
||||
|
||||
import androidx.work.BackoffPolicy
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.internal.di.SessionId
|
||||
import im.vector.matrix.android.internal.di.WorkManagerProvider
|
||||
import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEventWorkCommon
|
||||
import im.vector.matrix.android.internal.worker.WorkerParamsFactory
|
||||
import im.vector.matrix.android.internal.worker.startChain
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class RoomEventSender @Inject constructor(
|
||||
private val workManagerProvider: WorkManagerProvider,
|
||||
private val timelineSendEventWorkCommon: TimelineSendEventWorkCommon,
|
||||
@SessionId private val sessionId: String,
|
||||
private val cryptoService: CryptoService
|
||||
) {
|
||||
fun sendEvent(event: Event): Cancelable {
|
||||
// Encrypted room handling
|
||||
return if (cryptoService.isRoomEncrypted(event.roomId ?: "")) {
|
||||
Timber.v("Send event in encrypted room")
|
||||
val encryptWork = createEncryptEventWork(event, true)
|
||||
// Note that event will be replaced by the result of the previous work
|
||||
val sendWork = createSendEventWork(event, false)
|
||||
timelineSendEventWorkCommon.postSequentialWorks(event.roomId ?: "", encryptWork, sendWork)
|
||||
} else {
|
||||
val sendWork = createSendEventWork(event, true)
|
||||
timelineSendEventWorkCommon.postWork(event.roomId ?: "", sendWork)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
|
||||
// Same parameter
|
||||
val params = EncryptEventWorker.Params(sessionId, event)
|
||||
val sendWorkData = WorkerParamsFactory.toData(params)
|
||||
|
||||
return workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>()
|
||||
.setConstraints(WorkManagerProvider.workConstraints)
|
||||
.setInputData(sendWorkData)
|
||||
.startChain(startChain)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
|
||||
val sendContentWorkerParams = SendEventWorker.Params(sessionId, event)
|
||||
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||
|
||||
return timelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)
|
||||
}
|
||||
}
|
|
@ -33,8 +33,8 @@
|
|||
<string name="notice_display_name_set_by_you">You set your display name to %1$s</string>
|
||||
<string name="notice_display_name_changed_from">%1$s changed their display name from %2$s to %3$s</string>
|
||||
<string name="notice_display_name_changed_from_by_you">You changed your display name from %1$s to %2$s</string>
|
||||
<string name="notice_display_name_removed">%1$s removed their display name (%2$s)</string>
|
||||
<string name="notice_display_name_removed_by_you">You removed your display name (%1$s)</string>
|
||||
<string name="notice_display_name_removed">%1$s removed their display name (it was %2$s)</string>
|
||||
<string name="notice_display_name_removed_by_you">You removed your display name (it was %1$s)</string>
|
||||
<string name="notice_room_topic_changed">%1$s changed the topic to: %2$s</string>
|
||||
<string name="notice_room_topic_changed_by_you">You changed the topic to: %1$s</string>
|
||||
<string name="notice_room_name_changed">%1$s changed the room name to: %2$s</string>
|
||||
|
@ -43,6 +43,8 @@
|
|||
<string name="notice_placed_video_call_by_you">You placed a video call.</string>
|
||||
<string name="notice_placed_voice_call">%s placed a voice call.</string>
|
||||
<string name="notice_placed_voice_call_by_you">You placed a voice call.</string>
|
||||
<string name="notice_call_candidates">%s sent data to setup the call.</string>
|
||||
<string name="notice_call_candidates_by_you">You sent data to setup the call.</string>
|
||||
<string name="notice_answered_call">%s answered the call.</string>
|
||||
<string name="notice_answered_call_by_you">You answered the call.</string>
|
||||
<string name="notice_ended_call">%s ended the call.</string>
|
||||
|
@ -362,4 +364,8 @@
|
|||
|
||||
<string name="key_verification_request_fallback_message">%s is requesting to verify your key, but your client does not support in-chat key verification. You will need to use legacy key verification to verify keys.</string>
|
||||
|
||||
<string name="call_notification_answer">Accept</string>
|
||||
<string name="call_notification_reject">Decline</string>
|
||||
<string name="call_notification_hangup">Hang Up</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -62,9 +62,9 @@ class ContactPicker(override val requestCode: Int) : Picker<MultiPickerContactTy
|
|||
|
||||
val contactId = cursor.getInt(idColumn)
|
||||
var name = cursor.getString(nameColumn)
|
||||
var photoUri = cursor.getString(photoUriColumn)
|
||||
var phoneNumberList = mutableListOf<String>()
|
||||
var emailList = mutableListOf<String>()
|
||||
val photoUri = cursor.getString(photoUriColumn)
|
||||
val phoneNumberList = mutableListOf<String>()
|
||||
val emailList = mutableListOf<String>()
|
||||
|
||||
getRawContactId(context.contentResolver, contactId)?.let { rawContactId ->
|
||||
val selection = ContactsContract.Data.RAW_CONTACT_ID + " = ?"
|
||||
|
|
|
@ -390,6 +390,9 @@ dependencies {
|
|||
|
||||
implementation 'com.github.BillCarsonFr:JsonViewer:0.5'
|
||||
|
||||
// TODO meant for development purposes only
|
||||
implementation 'org.webrtc:google-webrtc:1.0.+'
|
||||
|
||||
// QR-code
|
||||
// Stick to 3.3.3 because of https://github.com/zxing/zxing/issues/1170
|
||||
implementation 'com.google.zxing:core:3.3.3'
|
||||
|
|
|
@ -3,13 +3,30 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="im.vector.riotx">
|
||||
|
||||
<!-- Needed for VOIP call to detect and switch to headset-->
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
|
||||
<!-- Call feature -->
|
||||
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
|
||||
<uses-permission android:name="android.permission.READ_CALL_LOG" />
|
||||
<!-- Needed for voice call to toggle speaker on or off -->
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<!-- READ_PHONE_STATE is needed only if your calling app reads numbers from the `PHONE_STATE`
|
||||
intent action. -->
|
||||
|
||||
<!-- Needed to show incoming calls activity in lock screen-->
|
||||
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<!-- Needed for incoming calls -->
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
|
||||
<!-- Adding CAMERA permission prevents Chromebooks to see the application on the PlayStore -->
|
||||
<!-- Tell that the Camera is not mandatory to install the application -->
|
||||
<uses-feature
|
||||
|
@ -172,6 +189,7 @@
|
|||
<activity
|
||||
android:name=".features.attachments.preview.AttachmentsPreviewActivity"
|
||||
android:theme="@style/AppTheme.AttachmentsPreview" />
|
||||
<activity android:name=".features.call.VectorCallActivity" />
|
||||
|
||||
<activity android:name=".features.terms.ReviewTermsActivity" />
|
||||
<activity android:name=".features.widgets.WidgetActivity" />
|
||||
|
@ -180,20 +198,47 @@
|
|||
|
||||
<service
|
||||
android:name=".core.services.CallService"
|
||||
android:exported="false" />
|
||||
android:exported="false" >
|
||||
<!-- in order to get headset button events -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".core.services.VectorSyncService"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".features.call.telecom.VectorConnectionService"
|
||||
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE">
|
||||
<intent-filter>
|
||||
<action android:name="android.telecom.ConnectionService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<!-- Receivers -->
|
||||
|
||||
<receiver
|
||||
android:name=".features.call.service.CallHeadsUpActionReceiver"
|
||||
android:exported="false" />
|
||||
|
||||
<!-- Exported false, should only be accessible from this app!! -->
|
||||
<receiver
|
||||
android:name=".features.notifications.NotificationBroadcastReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
|
||||
<!--
|
||||
A media button receiver receives and helps translate hardware media playback buttons,
|
||||
such as those found on wired and wireless headsets, into the appropriate callbacks in your app.
|
||||
-->
|
||||
<receiver android:name="androidx.media.session.MediaButtonReceiver" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- Providers -->
|
||||
|
||||
<provider
|
||||
|
|
|
@ -43,6 +43,7 @@ import im.vector.riotx.core.di.HasVectorInjector
|
|||
import im.vector.riotx.core.di.VectorComponent
|
||||
import im.vector.riotx.core.extensions.configureAndStart
|
||||
import im.vector.riotx.core.rx.RxConfig
|
||||
import im.vector.riotx.features.call.WebRtcPeerConnectionManager
|
||||
import im.vector.riotx.features.configuration.VectorConfiguration
|
||||
import im.vector.riotx.features.lifecycle.VectorActivityLifecycleCallbacks
|
||||
import im.vector.riotx.features.notifications.NotificationDrawerManager
|
||||
|
@ -80,6 +81,8 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
|
|||
@Inject lateinit var appStateHandler: AppStateHandler
|
||||
@Inject lateinit var rxConfig: RxConfig
|
||||
@Inject lateinit var popupAlertManager: PopupAlertManager
|
||||
@Inject lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager
|
||||
|
||||
lateinit var vectorComponent: VectorComponent
|
||||
private var fontThreadHandler: Handler? = null
|
||||
|
||||
|
@ -122,6 +125,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
|
|||
val lastAuthenticatedSession = authenticationService.getLastAuthenticatedSession()!!
|
||||
activeSessionHolder.setActiveSession(lastAuthenticatedSession)
|
||||
lastAuthenticatedSession.configureAndStart(applicationContext, pushRuleTriggerListener, sessionListener)
|
||||
lastAuthenticatedSession.callSignalingService().addCallListener(webRtcPeerConnectionManager)
|
||||
}
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(object : LifecycleObserver {
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||
|
|
|
@ -24,6 +24,8 @@ import dagger.Component
|
|||
import im.vector.riotx.core.error.ErrorFormatter
|
||||
import im.vector.riotx.core.preference.UserAvatarPreference
|
||||
import im.vector.riotx.features.MainActivity
|
||||
import im.vector.riotx.features.call.CallControlsBottomSheet
|
||||
import im.vector.riotx.features.call.VectorCallActivity
|
||||
import im.vector.riotx.features.createdirect.CreateDirectRoomActivity
|
||||
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
||||
import im.vector.riotx.features.crypto.quads.SharedSecureStorageActivity
|
||||
|
@ -130,6 +132,7 @@ interface ScreenComponent {
|
|||
fun inject(activity: InviteUsersToRoomActivity)
|
||||
fun inject(activity: ReviewTermsActivity)
|
||||
fun inject(activity: WidgetActivity)
|
||||
fun inject(activity: VectorCallActivity)
|
||||
|
||||
/* ==========================================================================================
|
||||
* BottomSheets
|
||||
|
@ -146,6 +149,7 @@ interface ScreenComponent {
|
|||
fun inject(bottomSheet: BootstrapBottomSheet)
|
||||
fun inject(bottomSheet: RoomWidgetPermissionBottomSheet)
|
||||
fun inject(bottomSheet: RoomWidgetsBottomSheet)
|
||||
fun inject(bottomSheet: CallControlsBottomSheet)
|
||||
|
||||
/* ==========================================================================================
|
||||
* Others
|
||||
|
|
|
@ -31,6 +31,7 @@ import im.vector.riotx.core.error.ErrorFormatter
|
|||
import im.vector.riotx.core.pushers.PushersManager
|
||||
import im.vector.riotx.core.utils.AssetReader
|
||||
import im.vector.riotx.core.utils.DimensionConverter
|
||||
import im.vector.riotx.features.call.WebRtcPeerConnectionManager
|
||||
import im.vector.riotx.features.configuration.VectorConfiguration
|
||||
import im.vector.riotx.features.crypto.keysrequest.KeyRequestHandler
|
||||
import im.vector.riotx.features.crypto.verification.IncomingVerificationRequestHandler
|
||||
|
@ -134,6 +135,8 @@ interface VectorComponent {
|
|||
|
||||
fun reAuthHelper(): ReAuthHelper
|
||||
|
||||
fun webRtcPeerConnectionManager(): WebRtcPeerConnectionManager
|
||||
|
||||
@Component.Factory
|
||||
interface Factory {
|
||||
fun create(@BindsInstance context: Context): VectorComponent
|
||||
|
|
|
@ -22,6 +22,7 @@ import dagger.Binds
|
|||
import dagger.Module
|
||||
import dagger.multibindings.IntoMap
|
||||
import im.vector.riotx.core.platform.ConfigurationViewModel
|
||||
import im.vector.riotx.features.call.SharedActiveCallViewModel
|
||||
import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromKeyViewModel
|
||||
import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreFromPassphraseViewModel
|
||||
import im.vector.riotx.features.crypto.keysbackup.restore.KeysBackupRestoreSharedViewModel
|
||||
|
@ -85,6 +86,11 @@ interface ViewModelModule {
|
|||
@ViewModelKey(ConfigurationViewModel::class)
|
||||
fun bindConfigurationViewModel(viewModel: ConfigurationViewModel): ViewModel
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(SharedActiveCallViewModel::class)
|
||||
fun bindSharedActiveCallViewModel(viewModel: SharedActiveCallViewModel): ViewModel
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(UserDirectorySharedActionViewModel::class)
|
||||
|
|
|
@ -40,14 +40,14 @@ class DefaultErrorFormatter @Inject constructor(
|
|||
null -> null
|
||||
is IdentityServiceError -> identityServerError(throwable)
|
||||
is Failure.NetworkConnection -> {
|
||||
when {
|
||||
throwable.ioException is SocketTimeoutException ->
|
||||
when (throwable.ioException) {
|
||||
is SocketTimeoutException ->
|
||||
stringProvider.getString(R.string.error_network_timeout)
|
||||
throwable.ioException is UnknownHostException ->
|
||||
is UnknownHostException ->
|
||||
// Invalid homeserver?
|
||||
// TODO Check network state, airplane mode, etc.
|
||||
stringProvider.getString(R.string.login_error_unknown_host)
|
||||
else ->
|
||||
else ->
|
||||
stringProvider.getString(R.string.error_no_network)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,9 +17,14 @@
|
|||
package im.vector.riotx.core.extensions
|
||||
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
import im.vector.matrix.android.api.extensions.tryThis
|
||||
|
||||
inline fun androidx.fragment.app.FragmentManager.commitTransactionNow(func: FragmentTransaction.() -> FragmentTransaction) {
|
||||
beginTransaction().func().commitNow()
|
||||
// Could throw and make the app crash
|
||||
// e.g sharedActionViewModel.observe()
|
||||
tryThis("Failed to commitTransactionNow") {
|
||||
beginTransaction().func().commitNow()
|
||||
}
|
||||
}
|
||||
|
||||
inline fun androidx.fragment.app.FragmentManager.commitTransaction(func: FragmentTransaction.() -> FragmentTransaction) {
|
||||
|
|
|
@ -38,10 +38,6 @@ fun Session.configureAndStart(context: Context,
|
|||
startSyncing(context)
|
||||
refreshPushers()
|
||||
pushRuleTriggerListener.startWithSession(this)
|
||||
|
||||
// TODO P1 From HomeActivity
|
||||
// @Inject lateinit var incomingVerificationRequestHandler: IncomingVerificationRequestHandler
|
||||
// @Inject lateinit var keyRequestHandler: KeyRequestHandler
|
||||
}
|
||||
|
||||
fun Session.startSyncing(context: Context) {
|
||||
|
|
|
@ -165,6 +165,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
|
|||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
Timber.i("onCreate Activity ${this.javaClass.simpleName}")
|
||||
val vectorComponent = getVectorComponent()
|
||||
screenComponent = DaggerScreenComponent.factory().create(vectorComponent, this)
|
||||
val timeForInjection = measureTimeMillis {
|
||||
|
@ -252,6 +253,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
|
|||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Timber.i("onDestroy Activity ${this.javaClass.simpleName}")
|
||||
unBinder?.unbind()
|
||||
unBinder = null
|
||||
|
||||
|
@ -279,6 +281,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
|
|||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
Timber.i("onPause Activity ${this.javaClass.simpleName}")
|
||||
|
||||
rageShake.stop()
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ import com.airbnb.mvrx.MvRxViewId
|
|||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.jakewharton.rxbinding3.view.clicks
|
||||
import im.vector.riotx.core.di.DaggerScreenComponent
|
||||
import im.vector.riotx.core.di.ScreenComponent
|
||||
import im.vector.riotx.core.utils.DimensionConverter
|
||||
|
@ -41,6 +42,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers
|
|||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.disposables.Disposable
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Add MvRx capabilities to bottomsheetdialog (like BaseMvRxFragment)
|
||||
|
@ -169,6 +171,18 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
|
|||
return this
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* Views
|
||||
* ========================================================================================== */
|
||||
|
||||
protected fun View.debouncedClicks(onClicked: () -> Unit) {
|
||||
clicks()
|
||||
.throttleFirst(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { onClicked() }
|
||||
.disposeOnDestroyView()
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* ViewEvents
|
||||
* ========================================================================================== */
|
||||
|
|
|
@ -45,7 +45,7 @@ class VectorEditTextPreference : EditTextPreference {
|
|||
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
||||
// display the title in multi-line to avoid ellipsis.
|
||||
try {
|
||||
holder.itemView.findViewById<TextView>(android.R.id.title)?.setSingleLine(false)
|
||||
holder.itemView.findViewById<TextView>(android.R.id.title)?.isSingleLine = false
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "onBindView")
|
||||
}
|
||||
|
|
|
@ -87,7 +87,7 @@ open class VectorPreference : Preference {
|
|||
val title = itemView.findViewById<TextView>(android.R.id.title)
|
||||
val summary = itemView.findViewById<TextView>(android.R.id.summary)
|
||||
if (title != null) {
|
||||
title.setSingleLine(false)
|
||||
title.isSingleLine = false
|
||||
title.setTypeface(null, mTypeface)
|
||||
}
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ class VectorSwitchPreference : SwitchPreference {
|
|||
|
||||
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
||||
// 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)
|
||||
}
|
||||
|
|
|
@ -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 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.
|
||||
|
@ -14,53 +15,119 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:Suppress("UNUSED_PARAMETER")
|
||||
|
||||
package im.vector.riotx.core.services
|
||||
|
||||
import android.content.Context
|
||||
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.media.session.MediaButtonReceiver
|
||||
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 timber.log.Timber
|
||||
|
||||
/**
|
||||
* Foreground service to manage calls
|
||||
*/
|
||||
class CallService : VectorService() {
|
||||
class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListener, BluetoothHeadsetReceiver.EventListener {
|
||||
|
||||
/**
|
||||
* call in progress (foreground notification)
|
||||
*/
|
||||
private var mCallIdInProgress: String? = null
|
||||
private val connections = mutableMapOf<String, CallConnection>()
|
||||
|
||||
private lateinit var notificationUtils: NotificationUtils
|
||||
private lateinit var webRtcPeerConnectionManager: WebRtcPeerConnectionManager
|
||||
|
||||
/**
|
||||
* incoming (foreground notification)
|
||||
*/
|
||||
private var mIncomingCallId: String? = null
|
||||
private var callRingPlayer: CallRingPlayer? = null
|
||||
|
||||
private var wiredHeadsetStateReceiver: WiredHeadsetStateReceiver? = 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() {
|
||||
super.onCreate()
|
||||
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 {
|
||||
Timber.v("## VOIP onStartCommand $intent")
|
||||
if (mediaSession == null) {
|
||||
mediaSession = MediaSessionCompat(applicationContext, CallService::class.java.name).apply {
|
||||
setCallback(mediaSessionButtonCallback)
|
||||
}
|
||||
}
|
||||
if (intent == null) {
|
||||
// Service started again by the system.
|
||||
// TODO What do we do here?
|
||||
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) {
|
||||
ACTION_INCOMING_CALL -> displayIncomingCallNotification(intent)
|
||||
ACTION_PENDING_CALL -> displayCallInProgressNotification(intent)
|
||||
ACTION_NO_ACTIVE_CALL -> hideCallNotifications()
|
||||
else ->
|
||||
ACTION_INCOMING_RINGING_CALL -> {
|
||||
mediaSession?.isActive = true
|
||||
callRingPlayer?.start()
|
||||
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
|
||||
callRingPlayer?.stop()
|
||||
myStopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
// We want the system to restore the service if killed
|
||||
|
@ -80,54 +147,65 @@ class CallService : VectorService() {
|
|||
* @param callId the callId
|
||||
*/
|
||||
private fun displayIncomingCallNotification(intent: Intent) {
|
||||
Timber.v("displayIncomingCallNotification")
|
||||
|
||||
// TODO
|
||||
/*
|
||||
Timber.v("## VOIP displayIncomingCallNotification $intent")
|
||||
|
||||
// the incoming call in progress is already displayed
|
||||
if (!TextUtils.isEmpty(mIncomingCallId)) {
|
||||
Timber.v("displayIncomingCallNotification : the incoming call in progress is already displayed")
|
||||
} else if (!TextUtils.isEmpty(mCallIdInProgress)) {
|
||||
Timber.v("displayIncomingCallNotification : a 'call in progress' notification is displayed")
|
||||
} else if (null == CallsManager.getSharedInstance().activeCall) {
|
||||
val callId = intent.getStringExtra(EXTRA_CALL_ID)
|
||||
// if (!TextUtils.isEmpty(mIncomingCallId)) {
|
||||
// Timber.v("displayIncomingCallNotification : the incoming call in progress is already displayed")
|
||||
// } else if (!TextUtils.isEmpty(mCallIdInProgress)) {
|
||||
// Timber.v("displayIncomingCallNotification : a 'call in progress' notification is displayed")
|
||||
// } else
|
||||
// // if (null == webRtcPeerConnectionManager.currentCall)
|
||||
// {
|
||||
val callId = intent.getStringExtra(EXTRA_CALL_ID)
|
||||
|
||||
Timber.v("displayIncomingCallNotification : display the dedicated notification")
|
||||
val notification = NotificationUtils.buildIncomingCallNotification(
|
||||
this,
|
||||
intent.getBooleanExtra(EXTRA_IS_VIDEO, false),
|
||||
intent.getStringExtra(EXTRA_ROOM_NAME),
|
||||
intent.getStringExtra(EXTRA_MATRIX_ID),
|
||||
callId)
|
||||
startForeground(NOTIFICATION_ID, notification)
|
||||
Timber.v("displayIncomingCallNotification : display the dedicated notification")
|
||||
val notification = notificationUtils.buildIncomingCallNotification(
|
||||
intent.getBooleanExtra(EXTRA_IS_VIDEO, false),
|
||||
intent.getStringExtra(EXTRA_ROOM_NAME) ?: "",
|
||||
intent.getStringExtra(EXTRA_ROOM_ID) ?: "",
|
||||
callId ?: "")
|
||||
startForeground(NOTIFICATION_ID, notification)
|
||||
|
||||
mIncomingCallId = callId
|
||||
// mIncomingCallId = callId
|
||||
|
||||
// turn the screen on for 3 seconds
|
||||
if (Matrix.getInstance(VectorApp.getInstance())!!.pushManager.isScreenTurnedOn) {
|
||||
try {
|
||||
val pm = getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
val wl = pm.newWakeLock(
|
||||
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or PowerManager.ACQUIRE_CAUSES_WAKEUP,
|
||||
CallService::class.java.simpleName)
|
||||
wl.acquire(3000)
|
||||
wl.release()
|
||||
} catch (re: RuntimeException) {
|
||||
Timber.e(re, "displayIncomingCallNotification : failed to turn screen on ")
|
||||
}
|
||||
// turn the screen on for 3 seconds
|
||||
// if (Matrix.getInstance(VectorApp.getInstance())!!.pushManager.isScreenTurnedOn) {
|
||||
// try {
|
||||
// val pm = getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
// val wl = pm.newWakeLock(
|
||||
// WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or PowerManager.ACQUIRE_CAUSES_WAKEUP,
|
||||
// CallService::class.java.simpleName)
|
||||
// wl.acquire(3000)
|
||||
// wl.release()
|
||||
// } catch (re: RuntimeException) {
|
||||
// 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")
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
Timber.i("displayIncomingCallNotification : do not display the incoming call notification because there is a pending call")
|
||||
}// test if there is no active call
|
||||
*/
|
||||
private fun displayOutgoingRingingCallNotification(intent: Intent) {
|
||||
val callId = intent.getStringExtra(EXTRA_CALL_ID)
|
||||
|
||||
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.
|
||||
*/
|
||||
private fun displayCallInProgressNotification(intent: Intent) {
|
||||
Timber.v("## VOIP displayCallInProgressNotification")
|
||||
val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: ""
|
||||
|
||||
val notification = notificationUtils.buildPendingCallNotification(
|
||||
|
@ -139,7 +217,27 @@ class CallService : VectorService() {
|
|||
|
||||
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() {
|
||||
val notification = notificationUtils.buildCallEndedNotification()
|
||||
|
||||
mediaSession?.isActive = false
|
||||
// It's mandatory to startForeground to avoid crash
|
||||
startForeground(NOTIFICATION_ID, notification)
|
||||
|
||||
myStopSelf()
|
||||
}
|
||||
|
||||
fun addConnection(callConnection: CallConnection) {
|
||||
connections[callConnection.callId] = callConnection
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val NOTIFICATION_ID = 6480
|
||||
|
||||
private const val ACTION_INCOMING_CALL = "im.vector.riotx.core.services.CallService.INCOMING_CALL"
|
||||
private const val ACTION_PENDING_CALL = "im.vector.riotx.core.services.CallService.PENDING_CALL"
|
||||
private const val ACTION_INCOMING_RINGING_CALL = "im.vector.riotx.core.services.CallService.ACTION_INCOMING_RINGING_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_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_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_CALL_ID = "EXTRA_CALL_ID"
|
||||
|
||||
fun onIncomingCall(context: Context,
|
||||
isVideo: Boolean,
|
||||
roomName: String,
|
||||
roomId: String,
|
||||
matrixId: String,
|
||||
callId: String) {
|
||||
fun onIncomingCallRinging(context: Context,
|
||||
isVideo: Boolean,
|
||||
roomName: String,
|
||||
roomId: String,
|
||||
matrixId: String,
|
||||
callId: String) {
|
||||
val intent = Intent(context, CallService::class.java)
|
||||
.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_ROOM_NAME, roomName)
|
||||
putExtra(EXTRA_ROOM_ID, roomId)
|
||||
|
@ -194,7 +340,7 @@ class CallService : VectorService() {
|
|||
callId: String) {
|
||||
val intent = Intent(context, CallService::class.java)
|
||||
.apply {
|
||||
action = ACTION_PENDING_CALL
|
||||
action = ACTION_ONGOING_CALL
|
||||
putExtra(EXTRA_IS_VIDEO, isVideo)
|
||||
putExtra(EXTRA_ROOM_NAME, roomName)
|
||||
putExtra(EXTRA_ROOM_ID, roomId)
|
||||
|
@ -214,4 +360,20 @@ class CallService : VectorService() {
|
|||
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.VectorBaseActivity
|
||||
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.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.RoomListParams
|
||||
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.workers.signout.SignOutViewModel
|
||||
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 javax.inject.Inject
|
||||
|
||||
|
@ -56,8 +66,9 @@ private const val INDEX_ROOMS = 2
|
|||
class HomeDetailFragment @Inject constructor(
|
||||
val homeDetailViewModelFactory: HomeDetailViewModel.Factory,
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val alertManager: PopupAlertManager
|
||||
) : VectorBaseFragment(), KeysBackupBanner.Delegate {
|
||||
private val alertManager: PopupAlertManager,
|
||||
private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager
|
||||
) : VectorBaseFragment(), KeysBackupBanner.Delegate, ActiveCallView.Callback {
|
||||
|
||||
private val unreadCounterBadgeViews = arrayListOf<UnreadCounterBadgeView>()
|
||||
|
||||
|
@ -65,16 +76,21 @@ class HomeDetailFragment @Inject constructor(
|
|||
private val unknownDeviceDetectorSharedViewModel: UnknownDeviceDetectorSharedViewModel by activityViewModel()
|
||||
|
||||
private lateinit var sharedActionViewModel: HomeSharedActionViewModel
|
||||
private lateinit var sharedCallActionViewModel: SharedActiveCallViewModel
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_home_detail
|
||||
|
||||
private val activeCallViewHolder = ActiveCallViewHolder()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
sharedActionViewModel = activityViewModelProvider.get(HomeSharedActionViewModel::class.java)
|
||||
sharedCallActionViewModel = activityViewModelProvider.get(SharedActiveCallViewModel::class.java)
|
||||
|
||||
setupBottomNavigationView()
|
||||
setupToolbar()
|
||||
setupKeysBackupBanner()
|
||||
setupActiveCallView()
|
||||
|
||||
withState(viewModel) {
|
||||
// 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) {
|
||||
|
@ -203,6 +226,15 @@ class HomeDetailFragment @Inject constructor(
|
|||
homeKeysBackupBanner.delegate = this
|
||||
}
|
||||
|
||||
private fun setupActiveCallView() {
|
||||
activeCallViewHolder.bind(
|
||||
activeCallPiP,
|
||||
activeCallView,
|
||||
activeCallPiPWrap,
|
||||
this
|
||||
)
|
||||
}
|
||||
|
||||
private fun setupToolbar() {
|
||||
val parentActivity = vectorBaseActivity
|
||||
if (parentActivity is ToolbarConfigurable) {
|
||||
|
@ -283,4 +315,20 @@ class HomeDetailFragment @Inject constructor(
|
|||
RoomListDisplayMode.ROOMS -> R.id.bottom_action_rooms
|
||||
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 ResendAll : RoomDetailAction()
|
||||
data class StartCall(val isVideo: Boolean) : RoomDetailAction()
|
||||
object EndCall : RoomDetailAction()
|
||||
|
||||
data class AcceptVerificationRequest(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.forEach
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
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.platform.VectorBaseFragment
|
||||
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.NotificationAreaView
|
||||
import im.vector.riotx.core.utils.Debouncer
|
||||
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.PERMISSION_REQUEST_CODE_DOWNLOAD_FILE
|
||||
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.getColorFromUserId
|
||||
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.saveMedia
|
||||
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.AttachmentsPreviewArgs
|
||||
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.crypto.keysbackup.restore.KeysBackupRestoreActivity
|
||||
import im.vector.riotx.features.crypto.util.toImageRes
|
||||
|
@ -196,17 +206,22 @@ class RoomDetailFragment @Inject constructor(
|
|||
val roomDetailViewModelFactory: RoomDetailViewModel.Factory,
|
||||
private val eventHtmlRenderer: EventHtmlRenderer,
|
||||
private val vectorPreferences: VectorPreferences,
|
||||
private val colorProvider: ColorProvider) :
|
||||
private val colorProvider: ColorProvider,
|
||||
private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager) :
|
||||
VectorBaseFragment(),
|
||||
TimelineEventController.Callback,
|
||||
VectorInviteView.Callback,
|
||||
JumpToReadMarkerView.Callback,
|
||||
AttachmentTypeSelectorView.Callback,
|
||||
AttachmentsHelper.Callback,
|
||||
RoomWidgetsBannerView.Callback {
|
||||
RoomWidgetsBannerView.Callback,
|
||||
ActiveCallView.Callback {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val AUDIO_CALL_PERMISSION_REQUEST_CODE = 1
|
||||
private const val VIDEO_CALL_PERMISSION_REQUEST_CODE = 2
|
||||
|
||||
/**
|
||||
* Sanitize the display name.
|
||||
*
|
||||
|
@ -243,6 +258,8 @@ class RoomDetailFragment @Inject constructor(
|
|||
override fun getMenuRes() = R.menu.menu_timeline
|
||||
|
||||
private lateinit var sharedActionViewModel: MessageSharedActionViewModel
|
||||
private lateinit var sharedCallActionViewModel: SharedActiveCallViewModel
|
||||
|
||||
private lateinit var layoutManager: LinearLayoutManager
|
||||
private lateinit var jumpToBottomViewVisibilityManager: JumpToBottomViewVisibilityManager
|
||||
private var modelBuildListener: OnModelBuildFinishedListener? = null
|
||||
|
@ -255,10 +272,12 @@ class RoomDetailFragment @Inject constructor(
|
|||
private lateinit var attachmentTypeSelector: AttachmentTypeSelectorView
|
||||
|
||||
private var lockSendButton = false
|
||||
private val activeCallViewHolder = ActiveCallViewHolder()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java)
|
||||
sharedCallActionViewModel = activityViewModelProvider.get(SharedActiveCallViewModel::class.java)
|
||||
attachmentsHelper = AttachmentsHelper(requireContext(), this).register()
|
||||
keyboardStateUtils = KeyboardStateUtils(requireActivity())
|
||||
setupToolbar(roomToolbar)
|
||||
|
@ -267,6 +286,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
setupInviteView()
|
||||
setupNotificationView()
|
||||
setupJumpToReadMarkerView()
|
||||
setupActiveCallView()
|
||||
setupJumpToBottomView()
|
||||
setupWidgetsBannerView()
|
||||
|
||||
|
@ -281,6 +301,13 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
.disposeOnDestroyView()
|
||||
|
||||
sharedCallActionViewModel
|
||||
.activeCall
|
||||
.observe(viewLifecycleOwner, Observer {
|
||||
activeCallViewHolder.updateCall(it, webRtcPeerConnectionManager)
|
||||
invalidateOptionsMenu()
|
||||
})
|
||||
|
||||
roomDetailViewModel.selectSubscribe(this, RoomDetailViewState::tombstoneEventHandling, uniqueOnly("tombstoneEventHandling")) {
|
||||
renderTombstoneEventHandling(it)
|
||||
}
|
||||
|
@ -380,6 +407,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
override fun onDestroyView() {
|
||||
timelineEventController.callback = null
|
||||
timelineEventController.removeModelBuildListener(modelBuildListener)
|
||||
activeCallView.callback = null
|
||||
modelBuildListener = null
|
||||
autoCompleter.clear()
|
||||
debouncer.cancelAll()
|
||||
|
@ -389,6 +417,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
activeCallViewHolder.unBind(webRtcPeerConnectionManager)
|
||||
roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
@ -418,6 +447,15 @@ class RoomDetailFragment @Inject constructor(
|
|||
jumpToReadMarkerView.callback = this
|
||||
}
|
||||
|
||||
private fun setupActiveCallView() {
|
||||
activeCallViewHolder.bind(
|
||||
activeCallPiP,
|
||||
activeCallView,
|
||||
activeCallPiPWrap,
|
||||
this
|
||||
)
|
||||
}
|
||||
|
||||
private fun navigateToEvent(action: RoomDetailViewEvents.NavigateToEvent) {
|
||||
val scrollPosition = timelineEventController.searchPositionOfEvent(action.eventId)
|
||||
if (scrollPosition == null) {
|
||||
|
@ -481,6 +519,29 @@ class RoomDetailFragment @Inject constructor(
|
|||
roomDetailViewModel.handle(RoomDetailAction.OpenIntegrationManager)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -496,6 +557,26 @@ class RoomDetailFragment @Inject constructor(
|
|||
.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) {
|
||||
autoCompleter.exitSpecialMode()
|
||||
composerLayout.collapse()
|
||||
|
@ -739,6 +820,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
|
||||
override fun invalidate() = withState(roomDetailViewModel) { state ->
|
||||
renderRoomSummary(state)
|
||||
invalidateOptionsMenu()
|
||||
val summary = state.asyncRoomSummary()
|
||||
val inviter = state.asyncInviter()
|
||||
if (summary?.membership == Membership.JOIN) {
|
||||
|
@ -1090,6 +1172,22 @@ class RoomDetailFragment @Inject constructor(
|
|||
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 {
|
||||
// Reset all pending data
|
||||
|
@ -1473,4 +1571,20 @@ class RoomDetailFragment @Inject constructor(
|
|||
RoomWidgetsBottomSheet.newInstance()
|
||||
.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