mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-02-16 20:10:04 +03:00
Merge pull request #1416 from vector-im/feature/voip
Call support with WebRTC
This commit is contained in:
commit
55993aff04
110 changed files with 6669 additions and 171 deletions
|
@ -2,7 +2,7 @@ Changes in RiotX 0.23.0 (2020-XX-XX)
|
|||
===================================================
|
||||
|
||||
Features ✨:
|
||||
-
|
||||
- Call with WebRTC support (##611)
|
||||
|
||||
Improvements 🙌:
|
||||
- "Add Matrix app" menu is now always visible (#1495)
|
||||
|
|
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'
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,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) }
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
* ========================================================================================== */
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
@ -372,6 +399,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
override fun onDestroyView() {
|
||||
timelineEventController.callback = null
|
||||
timelineEventController.removeModelBuildListener(modelBuildListener)
|
||||
activeCallView.callback = null
|
||||
modelBuildListener = null
|
||||
autoCompleter.clear()
|
||||
debouncer.cancelAll()
|
||||
|
@ -381,6 +409,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
activeCallViewHolder.unBind(webRtcPeerConnectionManager)
|
||||
roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
@ -410,6 +439,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) {
|
||||
|
@ -477,6 +515,29 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -492,6 +553,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()
|
||||
|
@ -735,6 +816,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) {
|
||||
|
@ -1086,6 +1168,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
|
||||
|
@ -1469,4 +1567,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ import com.squareup.inject.assisted.AssistedInject
|
|||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.MatrixPatterns
|
||||
import im.vector.matrix.android.api.NoOpMatrixCallback
|
||||
import im.vector.matrix.android.api.extensions.orFalse
|
||||
import im.vector.matrix.android.api.query.QueryStringValue
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
|
@ -66,6 +67,7 @@ import im.vector.riotx.core.platform.VectorViewModel
|
|||
import im.vector.riotx.core.resources.StringProvider
|
||||
import im.vector.riotx.core.resources.UserPreferencesProvider
|
||||
import im.vector.riotx.core.utils.subscribeLogError
|
||||
import im.vector.riotx.features.call.WebRtcPeerConnectionManager
|
||||
import im.vector.riotx.features.command.CommandParser
|
||||
import im.vector.riotx.features.command.ParsedCommand
|
||||
import im.vector.riotx.features.crypto.verification.SupportedVerificationMethodsProvider
|
||||
|
@ -96,7 +98,8 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
private val rainbowGenerator: RainbowGenerator,
|
||||
private val session: Session,
|
||||
private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider,
|
||||
private val stickerPickerActionHandler: StickerPickerActionHandler
|
||||
private val stickerPickerActionHandler: StickerPickerActionHandler,
|
||||
private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager
|
||||
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState), Timeline.Listener {
|
||||
|
||||
private val room = session.getRoom(initialState.roomId)!!
|
||||
|
@ -121,8 +124,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private var timelineEvents = PublishRelay.create<List<TimelineEvent>>()
|
||||
var timeline = room.createTimeline(eventId, timelineSettings)
|
||||
private set
|
||||
val timeline = room.createTimeline(eventId, timelineSettings)
|
||||
|
||||
// Slot to keep a pending action during permission request
|
||||
var pendingAction: RoomDetailAction? = null
|
||||
|
@ -135,6 +137,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
|
||||
private var trackUnreadMessages = AtomicBoolean(false)
|
||||
private var mostRecentDisplayedEvent: TimelineEvent? = null
|
||||
private var canDoCall = false
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
|
@ -213,6 +216,8 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun getOtherUserIds() = room.roomSummary()?.otherMemberIds
|
||||
|
||||
override fun handle(action: RoomDetailAction) {
|
||||
when (action) {
|
||||
is RoomDetailAction.UserIsTyping -> handleUserIsTyping(action)
|
||||
|
@ -252,13 +257,25 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action)
|
||||
is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action)
|
||||
is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment()
|
||||
}
|
||||
is RoomDetailAction.StartCall -> handleStartCall(action)
|
||||
is RoomDetailAction.EndCall -> handleEndCall()
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
private fun handleSendSticker(action: RoomDetailAction.SendSticker) {
|
||||
room.sendEvent(EventType.STICKER, action.stickerContent.toContent())
|
||||
}
|
||||
|
||||
private fun handleStartCall(action: RoomDetailAction.StartCall) {
|
||||
room.roomSummary()?.otherMemberIds?.firstOrNull()?.let {
|
||||
webRtcPeerConnectionManager.startOutgoingCall(room.roomId, it, action.isVideo)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleEndCall() {
|
||||
webRtcPeerConnectionManager.endCall()
|
||||
}
|
||||
|
||||
private fun handleSelectStickerAttachment() {
|
||||
viewModelScope.launch {
|
||||
val viewEvent = stickerPickerActionHandler.handle()
|
||||
|
@ -367,11 +384,14 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
|
||||
fun isMenuItemVisible(@IdRes itemId: Int) = when (itemId) {
|
||||
R.id.clear_message_queue ->
|
||||
/* For now always disable on production, worker cancellation is not working properly */
|
||||
// For now always disable when not in developer mode, worker cancellation is not working properly
|
||||
timeline.pendingEventCount() > 0 && vectorPreferences.developerMode()
|
||||
R.id.resend_all -> timeline.failedToDeliverEventCount() > 0
|
||||
R.id.clear_all -> timeline.failedToDeliverEventCount() > 0
|
||||
R.id.open_matrix_apps -> true
|
||||
R.id.voice_call,
|
||||
R.id.video_call -> room.canStartCall() && webRtcPeerConnectionManager.currentCall == null
|
||||
R.id.hangup_call -> webRtcPeerConnectionManager.currentCall != null
|
||||
else -> false
|
||||
}
|
||||
|
||||
|
@ -1000,6 +1020,8 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
val typingRoomMembers =
|
||||
typingHelper.toTypingRoomMembers(async.invoke()?.typingRoomMemberIds.orEmpty(), room)
|
||||
|
||||
canDoCall = async.invoke()?.canStartCall.orFalse()
|
||||
|
||||
copy(
|
||||
asyncRoomSummary = async,
|
||||
typingRoomMembers = typingRoomMembers,
|
||||
|
|
|
@ -184,6 +184,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
|||
EventType.STATE_ROOM_CANONICAL_ALIAS,
|
||||
EventType.STATE_ROOM_HISTORY_VISIBILITY,
|
||||
EventType.CALL_INVITE,
|
||||
EventType.CALL_CANDIDATES,
|
||||
EventType.CALL_HANGUP,
|
||||
EventType.CALL_ANSWER -> {
|
||||
noticeEventFormatter.format(timelineEvent)
|
||||
|
|
|
@ -80,12 +80,15 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||
EventType.KEY_VERIFICATION_START,
|
||||
EventType.KEY_VERIFICATION_KEY,
|
||||
EventType.KEY_VERIFICATION_READY,
|
||||
EventType.KEY_VERIFICATION_MAC -> {
|
||||
EventType.KEY_VERIFICATION_MAC,
|
||||
EventType.CALL_CANDIDATES -> {
|
||||
// TODO These are not filtered out by timeline when encrypted
|
||||
// For now manually ignore
|
||||
if (userPreferencesProvider.shouldShowHiddenEvents()) {
|
||||
noticeItemFactory.create(event, highlight, callback)
|
||||
} else null
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
EventType.KEY_VERIFICATION_CANCEL,
|
||||
EventType.KEY_VERIFICATION_DONE -> {
|
||||
|
|
|
@ -29,7 +29,6 @@ import me.gujun.android.span.span
|
|||
import javax.inject.Inject
|
||||
|
||||
class DisplayableEventFormatter @Inject constructor(
|
||||
// private val sessionHolder: ActiveSessionHolder,
|
||||
private val stringProvider: StringProvider,
|
||||
private val colorProvider: ColorProvider,
|
||||
private val noticeEventFormatter: NoticeEventFormatter
|
||||
|
|
|
@ -68,6 +68,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
|
|||
EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||
EventType.STATE_ROOM_POWER_LEVELS -> formatRoomPowerLevels(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||
EventType.CALL_INVITE,
|
||||
EventType.CALL_CANDIDATES,
|
||||
EventType.CALL_HANGUP,
|
||||
EventType.CALL_ANSWER -> formatCallEvent(type, timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
|
||||
EventType.MESSAGE,
|
||||
|
@ -237,9 +238,9 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
|
|||
|
||||
private fun formatCallEvent(type: String, event: Event, senderName: String?): CharSequence? {
|
||||
return when (type) {
|
||||
EventType.CALL_INVITE -> {
|
||||
EventType.CALL_INVITE -> {
|
||||
val content = event.getClearContent().toModel<CallInviteContent>() ?: return null
|
||||
val isVideoCall = content.offer.sdp == CallInviteContent.Offer.SDP_VIDEO
|
||||
val isVideoCall = content.offer?.sdp == CallInviteContent.Offer.SDP_VIDEO
|
||||
return if (isVideoCall) {
|
||||
if (event.isSentByCurrentUser()) {
|
||||
sp.getString(R.string.notice_placed_video_call_by_you)
|
||||
|
@ -254,19 +255,25 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
|
|||
}
|
||||
}
|
||||
}
|
||||
EventType.CALL_ANSWER ->
|
||||
EventType.CALL_ANSWER ->
|
||||
if (event.isSentByCurrentUser()) {
|
||||
sp.getString(R.string.notice_answered_call_by_you)
|
||||
} else {
|
||||
sp.getString(R.string.notice_answered_call, senderName)
|
||||
}
|
||||
EventType.CALL_HANGUP ->
|
||||
EventType.CALL_HANGUP ->
|
||||
if (event.isSentByCurrentUser()) {
|
||||
sp.getString(R.string.notice_ended_call_by_you)
|
||||
} else {
|
||||
sp.getString(R.string.notice_ended_call, senderName)
|
||||
}
|
||||
else -> null
|
||||
EventType.CALL_CANDIDATES ->
|
||||
if (event.isSentByCurrentUser()) {
|
||||
sp.getString(R.string.notice_call_candidates_by_you)
|
||||
} else {
|
||||
sp.getString(R.string.notice_call_candidates, senderName)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -48,6 +48,7 @@ import im.vector.riotx.core.extensions.configureAndStart
|
|||
import im.vector.riotx.core.extensions.exhaustive
|
||||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
import im.vector.riotx.features.call.WebRtcPeerConnectionManager
|
||||
import im.vector.riotx.features.notifications.PushRuleTriggerListener
|
||||
import im.vector.riotx.features.session.SessionListener
|
||||
import im.vector.riotx.features.signout.soft.SoftLogoutActivity
|
||||
|
@ -66,7 +67,8 @@ class LoginViewModel @AssistedInject constructor(
|
|||
private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory,
|
||||
private val sessionListener: SessionListener,
|
||||
private val reAuthHelper: ReAuthHelper,
|
||||
private val stringProvider: StringProvider)
|
||||
private val stringProvider: StringProvider,
|
||||
private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager)
|
||||
: VectorViewModel<LoginViewState, LoginAction, LoginViewEvents>(initialState) {
|
||||
|
||||
@AssistedInject.Factory
|
||||
|
@ -613,6 +615,7 @@ class LoginViewModel @AssistedInject constructor(
|
|||
private fun onSessionCreated(session: Session) {
|
||||
activeSessionHolder.setActiveSession(session)
|
||||
session.configureAndStart(applicationContext, pushRuleTriggerListener, sessionListener)
|
||||
session.callSignalingService().addCallListener(webRtcPeerConnectionManager)
|
||||
setState {
|
||||
copy(
|
||||
asyncLoginAction = Success(Unit)
|
||||
|
|
|
@ -35,11 +35,14 @@ import androidx.core.app.NotificationManagerCompat
|
|||
import androidx.core.app.RemoteInput
|
||||
import androidx.core.app.TaskStackBuilder
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import im.vector.riotx.BuildConfig
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
import im.vector.riotx.core.utils.startNotificationChannelSettingsIntent
|
||||
import im.vector.riotx.features.call.VectorCallActivity
|
||||
import im.vector.riotx.features.call.service.CallHeadsUpActionReceiver
|
||||
import im.vector.riotx.features.home.HomeActivity
|
||||
import im.vector.riotx.features.home.room.detail.RoomDetailActivity
|
||||
import im.vector.riotx.features.home.room.detail.RoomDetailArgs
|
||||
|
@ -263,13 +266,13 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
|||
*/
|
||||
@SuppressLint("NewApi")
|
||||
fun buildIncomingCallNotification(isVideo: Boolean,
|
||||
roomName: String,
|
||||
matrixId: String,
|
||||
otherUserId: String,
|
||||
roomId: String,
|
||||
callId: String): Notification {
|
||||
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
|
||||
|
||||
val builder = NotificationCompat.Builder(context, CALL_NOTIFICATION_CHANNEL_ID)
|
||||
.setContentTitle(ensureTitleNotEmpty(roomName))
|
||||
.setContentTitle(ensureTitleNotEmpty(otherUserId))
|
||||
.apply {
|
||||
if (isVideo) {
|
||||
setContentText(stringProvider.getString(R.string.incoming_video_call))
|
||||
|
@ -280,28 +283,124 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
|||
.setSmallIcon(R.drawable.incoming_call_notification_transparent)
|
||||
.setCategory(NotificationCompat.CATEGORY_CALL)
|
||||
.setLights(accentColor, 500, 500)
|
||||
.setOngoing(true)
|
||||
|
||||
// Compat: Display the incoming call notification on the lock screen
|
||||
builder.priority = NotificationCompat.PRIORITY_MAX
|
||||
builder.priority = NotificationCompat.PRIORITY_HIGH
|
||||
|
||||
// clear the activity stack to home activity
|
||||
val intent = Intent(context, HomeActivity::class.java)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
// TODO .putExtra(VectorHomeActivity.EXTRA_CALL_SESSION_ID, matrixId)
|
||||
// TODO .putExtra(VectorHomeActivity.EXTRA_CALL_ID, callId)
|
||||
|
||||
// Recreate the back stack
|
||||
val stackBuilder = TaskStackBuilder.create(context)
|
||||
.addParentStack(HomeActivity::class.java)
|
||||
.addNextIntent(intent)
|
||||
|
||||
// android 4.3 issue
|
||||
// use a generator for the private requestCode.
|
||||
// When using 0, the intent is not created/launched when the user taps on the notification.
|
||||
//
|
||||
val pendingIntent = stackBuilder.getPendingIntent(Random.nextInt(1000), PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
val requestId = Random.nextInt(1000)
|
||||
// val pendingIntent = stackBuilder.getPendingIntent(requestId, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
|
||||
builder.setContentIntent(pendingIntent)
|
||||
val contentIntent = VectorCallActivity.newIntent(
|
||||
context = context,
|
||||
callId = callId,
|
||||
roomId = roomId,
|
||||
otherUserId = otherUserId,
|
||||
isIncomingCall = true,
|
||||
isVideoCall = isVideo,
|
||||
mode = VectorCallActivity.INCOMING_RINGING
|
||||
).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
data = Uri.parse("foobar://$callId")
|
||||
}
|
||||
val contentPendingIntent = PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), contentIntent, 0)
|
||||
|
||||
val answerCallPendingIntent = TaskStackBuilder.create(context)
|
||||
.addNextIntentWithParentStack(Intent(context, HomeActivity::class.java))
|
||||
.addNextIntent(VectorCallActivity.newIntent(
|
||||
context = context,
|
||||
callId = callId,
|
||||
roomId = roomId,
|
||||
otherUserId = otherUserId,
|
||||
isIncomingCall = true,
|
||||
isVideoCall = isVideo,
|
||||
mode = VectorCallActivity.INCOMING_ACCEPT)
|
||||
)
|
||||
.getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
|
||||
val rejectCallActionReceiver = Intent(context, CallHeadsUpActionReceiver::class.java).apply {
|
||||
putExtra(CallHeadsUpActionReceiver.EXTRA_CALL_ACTION_KEY, CallHeadsUpActionReceiver.CALL_ACTION_REJECT)
|
||||
}
|
||||
// val answerCallPendingIntent = PendingIntent.getBroadcast(context, requestId, answerCallActionReceiver, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
val rejectCallPendingIntent = PendingIntent.getBroadcast(
|
||||
context,
|
||||
requestId + 1,
|
||||
rejectCallActionReceiver,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
builder.addAction(
|
||||
NotificationCompat.Action(
|
||||
R.drawable.ic_call,
|
||||
// IconCompat.createWithResource(applicationContext, R.drawable.ic_call)
|
||||
// .setTint(ContextCompat.getColor(applicationContext, R.color.riotx_positive_accent)),
|
||||
context.getString(R.string.call_notification_answer),
|
||||
answerCallPendingIntent
|
||||
)
|
||||
)
|
||||
|
||||
builder.addAction(
|
||||
NotificationCompat.Action(
|
||||
IconCompat.createWithResource(context, R.drawable.ic_call_end).setTint(ContextCompat.getColor(context, R.color.riotx_notice)),
|
||||
context.getString(R.string.call_notification_reject),
|
||||
rejectCallPendingIntent)
|
||||
)
|
||||
|
||||
builder.setFullScreenIntent(contentPendingIntent, true)
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
fun buildOutgoingRingingCallNotification(isVideo: Boolean,
|
||||
otherUserId: String,
|
||||
roomId: String,
|
||||
callId: String): Notification {
|
||||
val accentColor = ContextCompat.getColor(context, R.color.notification_accent_color)
|
||||
|
||||
val builder = NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID)
|
||||
.setContentTitle(ensureTitleNotEmpty(otherUserId))
|
||||
.apply {
|
||||
setContentText(stringProvider.getString(R.string.call_ring))
|
||||
}
|
||||
.setSmallIcon(R.drawable.incoming_call_notification_transparent)
|
||||
.setCategory(NotificationCompat.CATEGORY_CALL)
|
||||
.setLights(accentColor, 500, 500)
|
||||
.setOngoing(true)
|
||||
|
||||
val requestId = Random.nextInt(1000)
|
||||
|
||||
val contentIntent = VectorCallActivity.newIntent(
|
||||
context = context,
|
||||
callId = callId,
|
||||
roomId = roomId,
|
||||
otherUserId = otherUserId,
|
||||
isIncomingCall = true,
|
||||
isVideoCall = isVideo,
|
||||
mode = null).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
data = Uri.parse("foobar://$callId")
|
||||
}
|
||||
val contentPendingIntent = PendingIntent.getActivity(context, System.currentTimeMillis().toInt(), contentIntent, 0)
|
||||
|
||||
val rejectCallActionReceiver = Intent(context, CallHeadsUpActionReceiver::class.java).apply {
|
||||
putExtra(CallHeadsUpActionReceiver.EXTRA_CALL_ACTION_KEY, CallHeadsUpActionReceiver.CALL_ACTION_REJECT)
|
||||
}
|
||||
|
||||
val rejectCallPendingIntent = PendingIntent.getBroadcast(
|
||||
context,
|
||||
requestId + 1,
|
||||
rejectCallActionReceiver,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
builder.addAction(
|
||||
NotificationCompat.Action(
|
||||
IconCompat.createWithResource(context, R.drawable.ic_call_end).setTint(ContextCompat.getColor(context, R.color.riotx_notice)),
|
||||
context.getString(R.string.call_notification_hangup),
|
||||
rejectCallPendingIntent)
|
||||
)
|
||||
builder.setContentIntent(contentPendingIntent)
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
@ -321,8 +420,9 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
|||
roomName: String,
|
||||
roomId: String,
|
||||
matrixId: String,
|
||||
callId: String): Notification {
|
||||
val builder = NotificationCompat.Builder(context, CALL_NOTIFICATION_CHANNEL_ID)
|
||||
callId: String,
|
||||
fromBg: Boolean = false): Notification {
|
||||
val builder = NotificationCompat.Builder(context, if (fromBg) CALL_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID)
|
||||
.setContentTitle(ensureTitleNotEmpty(roomName))
|
||||
.apply {
|
||||
if (isVideo) {
|
||||
|
@ -334,31 +434,37 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
|||
.setSmallIcon(R.drawable.incoming_call_notification_transparent)
|
||||
.setCategory(NotificationCompat.CATEGORY_CALL)
|
||||
|
||||
// Display the pending call notification on the lock screen
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
builder.priority = NotificationCompat.PRIORITY_MAX
|
||||
if (fromBg) {
|
||||
builder.priority = NotificationCompat.PRIORITY_LOW
|
||||
builder.setOngoing(true)
|
||||
}
|
||||
|
||||
/* TODO
|
||||
// Build the pending intent for when the notification is clicked
|
||||
val roomIntent = Intent(context, VectorRoomActivity::class.java)
|
||||
.putExtra(VectorRoomActivity.EXTRA_ROOM_ID, roomId)
|
||||
.putExtra(VectorRoomActivity.EXTRA_MATRIX_ID, matrixId)
|
||||
.putExtra(VectorRoomActivity.EXTRA_START_CALL_ID, callId)
|
||||
val rejectCallActionReceiver = Intent(context, CallHeadsUpActionReceiver::class.java).apply {
|
||||
data = Uri.parse("mxcall://end?$callId")
|
||||
putExtra(CallHeadsUpActionReceiver.EXTRA_CALL_ACTION_KEY, CallHeadsUpActionReceiver.CALL_ACTION_REJECT)
|
||||
}
|
||||
|
||||
// Recreate the back stack
|
||||
val stackBuilder = TaskStackBuilder.create(context)
|
||||
.addParentStack(VectorRoomActivity::class.java)
|
||||
.addNextIntent(roomIntent)
|
||||
val rejectCallPendingIntent = PendingIntent.getBroadcast(
|
||||
context,
|
||||
System.currentTimeMillis().toInt(),
|
||||
rejectCallActionReceiver,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
// android 4.3 issue
|
||||
// use a generator for the private requestCode.
|
||||
// When using 0, the intent is not created/launched when the user taps on the notification.
|
||||
//
|
||||
val pendingIntent = stackBuilder.getPendingIntent(Random().nextInt(1000), PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
builder.addAction(
|
||||
NotificationCompat.Action(
|
||||
IconCompat.createWithResource(context, R.drawable.ic_call_end).setTint(ContextCompat.getColor(context, R.color.riotx_notice)),
|
||||
context.getString(R.string.call_notification_hangup),
|
||||
rejectCallPendingIntent)
|
||||
)
|
||||
|
||||
builder.setContentIntent(pendingIntent)
|
||||
*/
|
||||
val contentPendingIntent = TaskStackBuilder.create(context)
|
||||
.addNextIntentWithParentStack(Intent(context, HomeActivity::class.java))
|
||||
// TODO other userId
|
||||
.addNextIntent(VectorCallActivity.newIntent(context, callId, roomId, "otherUserId", true, isVideo, null))
|
||||
.getPendingIntent(System.currentTimeMillis().toInt(), PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
|
||||
builder.setContentIntent(contentPendingIntent)
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
|
14
vector/src/main/res/drawable/ic_call.xml
Normal file
14
vector/src/main/res/drawable/ic_call.xml
Normal file
|
@ -0,0 +1,14 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M21.9994,16.9738V19.9846C22.0017,20.5498 21.7651,21.0898 21.3478,21.4718C20.9305,21.8539 20.3712,22.0427 19.8072,21.9918C16.7128,21.6563 13.7404,20.601 11.1289,18.9108C8.6992,17.3699 6.6393,15.3141 5.0953,12.8892C3.3959,10.271 2.3382,7.2901 2.0082,4.188C1.9574,3.6268 2.1452,3.0702 2.5258,2.6541C2.9064,2.2379 3.4448,2.0006 4.0093,2.0001H7.0261C8.0356,1.9902 8.896,2.7287 9.0373,3.7263C9.1646,4.6898 9.4007,5.6359 9.7412,6.5464C10.0175,7.2799 9.8407,8.1068 9.2887,8.664L8.0116,9.9386C9.4431,12.4512 11.5276,14.5315 14.0451,15.9602L15.3222,14.6856C15.8805,14.1346 16.7091,13.9583 17.444,14.234C18.3564,14.5738 19.3043,14.8094 20.2697,14.9365C21.2809,15.0789 22.0247,15.955 21.9994,16.9738Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
14
vector/src/main/res/drawable/ic_call_end.xml
Normal file
14
vector/src/main/res/drawable/ic_call_end.xml
Normal file
|
@ -0,0 +1,14 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M5.2415,18.995L3.2268,16.7576C2.8469,16.3391 2.6614,15.7796 2.7158,15.2164C2.7702,14.6532 3.0596,14.1387 3.5127,13.799C6.0368,11.9778 8.9518,10.773 12.0235,10.2815C14.8601,9.8008 17.7666,9.9501 20.5365,10.719C23.5514,11.5274 26.332,13.0348 28.6531,15.1192C29.0664,15.5022 29.2993,16.0416 29.2949,16.6055C29.2905,17.1694 29.0492,17.706 28.6301,18.0841L26.3882,20.1028C25.6447,20.7857 24.5111,20.8127 23.7386,20.1659C22.9992,19.535 22.1907,18.99 21.3284,18.5412C20.6322,18.181 20.2102,17.4482 20.2477,16.6648L20.3438,14.863C17.5987,13.9538 14.6576,13.8027 11.8307,14.4256L11.7346,16.2273C11.6884,17.0104 11.1907,17.6959 10.46,17.9828C9.5547,18.3408 8.6926,18.8 7.8901,19.3516C7.0434,19.9224 5.9044,19.7691 5.2415,18.995Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
11
vector/src/main/res/drawable/ic_call_flip_camera_active.xml
Normal file
11
vector/src/main/res/drawable/ic_call_flip_camera_active.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<vector android:autoMirrored="true" android:height="40dp"
|
||||
android:viewportHeight="40" android:viewportWidth="40"
|
||||
android:width="40dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M20,20m-20,0a20,20 0,1 1,40 0a20,20 0,1 1,-40 0"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
<path android:fillColor="#FFFFFF" android:fillType="nonZero"
|
||||
android:pathData="M26,14.75L23.6225,14.75L22.25,13.25L17.75,13.25L16.3775,14.75L14,14.75C13.175,14.75 12.5,15.425 12.5,16.25L12.5,25.25C12.5,26.075 13.175,26.75 14,26.75L26,26.75C26.825,26.75 27.5,26.075 27.5,25.25L27.5,16.25C27.5,15.425 26.825,14.75 26,14.75ZM20,24.5C17.93,24.5 16.25,22.82 16.25,20.75L14.75,20.75L16.625,18.875L18.5,20.75L17,20.75C17,22.4075 18.3425,23.75 20,23.75C20.435,23.75 20.8475,23.6525 21.215,23.4875L21.77,24.0425C21.2375,24.32 20.645,24.5 20,24.5ZM23.375,22.625L21.5,20.75L23,20.75C23,19.0925 21.6575,17.75 20,17.75C19.565,17.75 19.1525,17.8475 18.785,18.0125L18.23,17.465C18.7625,17.18 19.355,17 20,17C22.07,17 23.75,18.68 23.75,20.75L25.25,20.75L23.375,22.625Z"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
</vector>
|
|
@ -0,0 +1,7 @@
|
|||
<vector android:autoMirrored="true" android:height="18dp"
|
||||
android:viewportHeight="18" android:viewportWidth="18"
|
||||
android:width="18dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FFFFFF" android:fillType="nonZero"
|
||||
android:pathData="M15,3.75L12.6225,3.75L11.25,2.25L6.75,2.25L5.3775,3.75L3,3.75C2.175,3.75 1.5,4.425 1.5,5.25L1.5,14.25C1.5,15.075 2.175,15.75 3,15.75L15,15.75C15.825,15.75 16.5,15.075 16.5,14.25L16.5,5.25C16.5,4.425 15.825,3.75 15,3.75ZM9,13.5C6.93,13.5 5.25,11.82 5.25,9.75L3.75,9.75L5.625,7.875L7.5,9.75L6,9.75C6,11.4075 7.3425,12.75 9,12.75C9.435,12.75 9.8475,12.6525 10.215,12.4875L10.77,13.0425C10.2375,13.32 9.645,13.5 9,13.5ZM12.375,11.625L10.5,9.75L12,9.75C12,8.0925 10.6575,6.75 9,6.75C8.565,6.75 8.1525,6.8475 7.785,7.0125L7.23,6.465C7.7625,6.18 8.355,6 9,6C11.07,6 12.75,7.68 12.75,9.75L14.25,9.75L12.375,11.625Z"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
</vector>
|
11
vector/src/main/res/drawable/ic_call_mute_active.xml
Normal file
11
vector/src/main/res/drawable/ic_call_mute_active.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<vector android:autoMirrored="true" android:height="40dp"
|
||||
android:viewportHeight="40" android:viewportWidth="40"
|
||||
android:width="40dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M20,20m-20,0a20,20 0,1 1,40 0a20,20 0,1 1,-40 0"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
<path android:fillColor="#FFFFFF" android:fillType="nonZero"
|
||||
android:pathData="M25.25,19.25L23.975,19.25C23.975,19.805 23.855,20.3225 23.6525,20.7875L24.575,21.71C24.995,20.975 25.25,20.1425 25.25,19.25L25.25,19.25ZM22.235,19.3775C22.235,19.3325 22.25,19.295 22.25,19.25L22.25,14.75C22.25,13.505 21.245,12.5 20,12.5C18.755,12.5 17.75,13.505 17.75,14.75L17.75,14.885L22.235,19.3775ZM14.2025,13.25L13.25,14.2025L17.7575,18.71L17.7575,19.25C17.7575,20.495 18.755,21.5 20,21.5C20.165,21.5 20.33,21.4775 20.4875,21.44L21.7325,22.685C21.2,22.9325 20.6075,23.075 20,23.075C17.93,23.075 16.025,21.5 16.025,19.25L14.75,19.25C14.75,21.8075 16.79,23.9225 19.25,24.29L19.25,26.75L20.75,26.75L20.75,24.29C21.4325,24.1925 22.0775,23.9525 22.655,23.615L25.7975,26.75L26.75,25.7975L14.2025,13.25Z"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
</vector>
|
7
vector/src/main/res/drawable/ic_call_mute_default.xml
Normal file
7
vector/src/main/res/drawable/ic_call_mute_default.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<vector android:autoMirrored="true" android:height="18dp"
|
||||
android:viewportHeight="18" android:viewportWidth="18"
|
||||
android:width="18dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FFFFFF" android:fillType="nonZero"
|
||||
android:pathData="M14.25,8.25L12.975,8.25C12.975,8.805 12.855,9.3225 12.6525,9.7875L13.575,10.71C13.995,9.975 14.25,9.1425 14.25,8.25L14.25,8.25ZM11.235,8.3775C11.235,8.3325 11.25,8.295 11.25,8.25L11.25,3.75C11.25,2.505 10.245,1.5 9,1.5C7.755,1.5 6.75,2.505 6.75,3.75L6.75,3.885L11.235,8.3775ZM3.2025,2.25L2.25,3.2025L6.7575,7.71L6.7575,8.25C6.7575,9.495 7.755,10.5 9,10.5C9.165,10.5 9.33,10.4775 9.4875,10.44L10.7325,11.685C10.2,11.9325 9.6075,12.075 9,12.075C6.93,12.075 5.025,10.5 5.025,8.25L3.75,8.25C3.75,10.8075 5.79,12.9225 8.25,13.29L8.25,15.75L9.75,15.75L9.75,13.29C10.4325,13.1925 11.0775,12.9525 11.655,12.615L14.7975,15.75L15.75,14.7975L3.2025,2.25Z"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
</vector>
|
11
vector/src/main/res/drawable/ic_call_speaker_active.xml
Normal file
11
vector/src/main/res/drawable/ic_call_speaker_active.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<vector android:autoMirrored="true" android:height="40dp"
|
||||
android:viewportHeight="40" android:viewportWidth="40"
|
||||
android:width="40dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M20,20m-20,0a20,20 0,1 1,40 0a20,20 0,1 1,-40 0"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
<path android:fillColor="#FFFFFF" android:fillType="nonZero"
|
||||
android:pathData="M13.25,17.75L13.25,22.25L16.25,22.25L20,26L20,14L16.25,17.75L13.25,17.75ZM23.375,20C23.375,18.6725 22.61,17.5325 21.5,16.9775L21.5,23.015C22.61,22.4675 23.375,21.3275 23.375,20ZM21.5,13.4225L21.5,14.9675C23.6675,15.6125 25.25,17.6225 25.25,20C25.25,22.3775 23.6675,24.3875 21.5,25.0325L21.5,26.5775C24.5075,25.895 26.75,23.21 26.75,20C26.75,16.79 24.5075,14.105 21.5,13.4225Z"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
</vector>
|
7
vector/src/main/res/drawable/ic_call_speaker_default.xml
Normal file
7
vector/src/main/res/drawable/ic_call_speaker_default.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<vector android:autoMirrored="true" android:height="18dp"
|
||||
android:viewportHeight="18" android:viewportWidth="18"
|
||||
android:width="18dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FFFFFF" android:fillType="nonZero"
|
||||
android:pathData="M2.25,6.75L2.25,11.25L5.25,11.25L9,15L9,3L5.25,6.75L2.25,6.75ZM12.375,9C12.375,7.6725 11.61,6.5325 10.5,5.9775L10.5,12.015C11.61,11.4675 12.375,10.3275 12.375,9ZM10.5,2.4225L10.5,3.9675C12.6675,4.6125 14.25,6.6225 14.25,9C14.25,11.3775 12.6675,13.3875 10.5,14.0325L10.5,15.5775C13.5075,14.895 15.75,12.21 15.75,9C15.75,5.79 13.5075,3.105 10.5,2.4225Z"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
</vector>
|
11
vector/src/main/res/drawable/ic_call_videocam_off_active.xml
Normal file
11
vector/src/main/res/drawable/ic_call_videocam_off_active.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<vector android:autoMirrored="true" android:height="40dp"
|
||||
android:viewportHeight="40" android:viewportWidth="40"
|
||||
android:width="40dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillAlpha="0.2" android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M20,20m-20,0a20,20 0,1 1,40 0a20,20 0,1 1,-40 0"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
<path android:fillColor="#FFFFFF" android:fillType="nonZero"
|
||||
android:pathData="M26.75,15.875L23.75,18.875L23.75,16.25C23.75,15.8375 23.4125,15.5 23,15.5L18.365,15.5L26.75,23.885L26.75,15.875ZM13.4525,12.5L12.5,13.4525L14.5475,15.5L14,15.5C13.5875,15.5 13.25,15.8375 13.25,16.25L13.25,23.75C13.25,24.1625 13.5875,24.5 14,24.5L23,24.5C23.1575,24.5 23.2925,24.44 23.405,24.365L25.7975,26.75L26.75,25.7975L13.4525,12.5Z"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
</vector>
|
|
@ -0,0 +1,7 @@
|
|||
<vector android:autoMirrored="true" android:height="18dp"
|
||||
android:viewportHeight="18" android:viewportWidth="18"
|
||||
android:width="18dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FFFFFF" android:fillType="nonZero"
|
||||
android:pathData="M15.75,4.875L12.75,7.875L12.75,5.25C12.75,4.8375 12.4125,4.5 12,4.5L7.365,4.5L15.75,12.885L15.75,4.875ZM2.4525,1.5L1.5,2.4525L3.5475,4.5L3,4.5C2.5875,4.5 2.25,4.8375 2.25,5.25L2.25,12.75C2.25,13.1625 2.5875,13.5 3,13.5L12,13.5C12.1575,13.5 12.2925,13.44 12.405,13.365L14.7975,15.75L15.75,14.7975L2.4525,1.5Z"
|
||||
android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
</vector>
|
9
vector/src/main/res/drawable/ic_hd.xml
Normal file
9
vector/src/main/res/drawable/ic_hd.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M19,3H5C3.89,3 3,3.9 3,5V19C3,20.1 3.89,21 5,21H19C20.1,21 21,20.1 21,19V5C21,3.9 20.1,3 19,3ZM11,15H9.5V13H7.5V15H6V9H7.5V11.5H9.5V9H11V15ZM13,9H17C17.55,9 18,9.45 18,10V14C18,14.55 17.55,15 17,15H13V9ZM14.5,13.5H16.5V10.5H14.5V13.5Z"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
9
vector/src/main/res/drawable/ic_hd_disabled.xml
Normal file
9
vector/src/main/res/drawable/ic_hd_disabled.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M17.5,15V13H18.6L19.5,15H21L20.1,12.9C20.6,12.7 21,12.1 21,11.5V10.5C21,9.7 20.3,9 19.5,9H16V13.9L17.1,15H17.5ZM17.5,10.5H19.5V11.5H17.5V10.5ZM13,10.5V10.9L14.5,12.4V10.5C14.5,9.7 13.8,9 13,9H11.1L12.6,10.5H13ZM9.5,9.5L2.5,2.5L1.4,3.5L6.9,9H6.5V11H4.5V9H3V15H4.5V12.5H6.5V15H8V10.1L9.5,11.6V15H12.9L20.5,22.6L21.6,21.5L9.5,9.5Z"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
41
vector/src/main/res/drawable/ic_microphone_off.xml
Normal file
41
vector/src/main/res/drawable/ic_microphone_off.xml
Normal file
|
@ -0,0 +1,41 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="25dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="25">
|
||||
<path
|
||||
android:pathData="M1,2L23,24"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M15,10.34V5C15.0007,4.256 14.725,3.5383 14.2264,2.9862C13.7277,2.4341 13.0417,2.0869 12.3015,2.0122C11.5613,1.9374 10.8197,2.1403 10.2207,2.5816C9.6217,3.0228 9.208,3.6709 9.06,4.4M9,10V13C9.0005,13.593 9.1768,14.1725 9.5064,14.6653C9.8361,15.1582 10.3045,15.5423 10.8523,15.7691C11.4002,15.996 12.0029,16.0554 12.5845,15.9399C13.1661,15.8243 13.7005,15.539 14.12,15.12L9,10Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M16.9999,17.95C16.0237,18.9464 14.7721,19.6285 13.4056,19.9086C12.039,20.1887 10.62,20.0542 9.3304,19.5223C8.0409,18.9903 6.9397,18.0853 6.1681,16.9232C5.3965,15.761 4.9897,14.3949 4.9999,13V11M18.9999,11V13C18.9996,13.4124 18.9628,13.824 18.8899,14.23"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M12,20V24"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M8,24H16"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
10
vector/src/main/res/drawable/ic_microphone_on.xml
Normal file
10
vector/src/main/res/drawable/ic_microphone_on.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M12,0C10.9391,0 9.9217,0.4214 9.1716,1.1716C8.4214,1.9217 8,2.9391 8,4V12C8,13.0609 8.4214,14.0783 9.1716,14.8284C9.9217,15.5786 10.9391,16 12,16C13.0609,16 14.0783,15.5786 14.8284,14.8284C15.5786,14.0783 16,13.0609 16,12V4C16,2.9391 15.5786,1.9217 14.8284,1.1716C14.0783,0.4214 13.0609,0 12,0ZM10.5858,2.5858C10.9609,2.2107 11.4696,2 12,2C12.5304,2 13.0391,2.2107 13.4142,2.5858C13.7893,2.9609 14,3.4696 14,4V12C14,12.5304 13.7893,13.0391 13.4142,13.4142C13.0391,13.7893 12.5304,14 12,14C11.4696,14 10.9609,13.7893 10.5858,13.4142C10.2107,13.0391 10,12.5304 10,12V4C10,3.4696 10.2107,2.9609 10.5858,2.5858ZM6,10C6,9.4477 5.5523,9 5,9C4.4477,9 4,9.4477 4,10V12C4,14.1217 4.8429,16.1566 6.3432,17.6569C7.6058,18.9195 9.247,19.7165 11,19.9373V22H8C7.4477,22 7,22.4477 7,23C7,23.5523 7.4477,24 8,24H12H16C16.5523,24 17,23.5523 17,23C17,22.4477 16.5523,22 16,22H13V19.9373C14.753,19.7165 16.3942,18.9195 17.6569,17.6569C19.1571,16.1566 20,14.1217 20,12V10C20,9.4477 19.5523,9 19,9C18.4477,9 18,9.4477 18,10V12C18,13.5913 17.3679,15.1174 16.2426,16.2426C15.1174,17.3679 13.5913,18 12,18C10.4087,18 8.8826,17.3679 7.7574,16.2426C6.6321,15.1174 6,13.5913 6,12V10Z"
|
||||
android:fillColor="#2E2F32"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
18
vector/src/main/res/drawable/ic_more_vertical.xml
Normal file
18
vector/src/main/res/drawable/ic_more_vertical.xml
Normal file
|
@ -0,0 +1,18 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M12,14C13.1046,14 14,13.1046 14,12C14,10.8954 13.1046,10 12,10C10.8954,10 10,10.8954 10,12C10,13.1046 10.8954,14 12,14Z"
|
||||
android:fillColor="#2E2F32"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M12,6C13.1046,6 14,5.1046 14,4C14,2.8954 13.1046,2 12,2C10.8954,2 10,2.8954 10,4C10,5.1046 10.8954,6 12,6Z"
|
||||
android:fillColor="#2E2F32"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M12,22C13.1046,22 14,21.1046 14,20C14,18.8954 13.1046,18 12,18C10.8954,18 10,18.8954 10,20C10,21.1046 10.8954,22 12,22Z"
|
||||
android:fillColor="#2E2F32"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
14
vector/src/main/res/drawable/ic_phone.xml
Normal file
14
vector/src/main/res/drawable/ic_phone.xml
Normal file
|
@ -0,0 +1,14 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M21.9994,16.9738V19.9846C22.0017,20.5498 21.7651,21.0898 21.3478,21.4718C20.9305,21.8539 20.3712,22.0427 19.8072,21.9918C16.7128,21.6563 13.7404,20.601 11.1289,18.9108C8.6992,17.3699 6.6393,15.3141 5.0953,12.8892C3.3959,10.271 2.3382,7.2901 2.0082,4.188C1.9574,3.6268 2.1452,3.0702 2.5258,2.6541C2.9064,2.2379 3.4448,2.0006 4.0093,2.0001H7.0261C8.0356,1.9902 8.896,2.7287 9.0373,3.7263C9.1646,4.6898 9.4007,5.6359 9.7412,6.5464C10.0175,7.2799 9.8407,8.1068 9.2887,8.664L8.0116,9.9386C9.4431,12.4512 11.5276,14.5315 14.0451,15.9602L15.3222,14.6856C15.8805,14.1346 16.7091,13.9583 17.444,14.234C18.3564,14.5738 19.3043,14.8094 20.2697,14.9365C21.2809,15.0789 22.0247,15.955 21.9994,16.9738Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
22
vector/src/main/res/drawable/ic_video.xml
Normal file
22
vector/src/main/res/drawable/ic_video.xml
Normal file
|
@ -0,0 +1,22 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M23,7L16,12L23,17V7Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M1,7C1,5.8954 1.8954,5 3,5H14C15.1046,5 16,5.8954 16,7V17C16,18.1046 15.1046,19 14,19H3C1.8954,19 1,18.1046 1,17V7Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
42
vector/src/main/res/drawable/ic_video_flip.xml
Normal file
42
vector/src/main/res/drawable/ic_video_flip.xml
Normal file
|
@ -0,0 +1,42 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M4,9L1,12L4,15"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M10,15L13,12L10,9"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M13,12H2"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M23,7L16,12L23,17V7Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M1,7.5V7C1,5.8954 1.8954,5 3,5H14C15.1046,5 16,5.8954 16,7V17C16,18.1046 15.1046,19 14,19H3C1.8954,19 1,18.1046 1,17V16.5"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
20
vector/src/main/res/drawable/ic_video_off.xml
Normal file
20
vector/src/main/res/drawable/ic_video_off.xml
Normal file
|
@ -0,0 +1,20 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M10.66,5H14C14.5304,5 15.0391,5.2107 15.4142,5.5858C15.7893,5.9609 16,6.4696 16,7V10.34L17,11.34L23,7V17M16,16V17C16,17.5304 15.7893,18.0391 15.4142,18.4142C15.0391,18.7893 14.5304,19 14,19H3C2.4696,19 1.9609,18.7893 1.5858,18.4142C1.2107,18.0391 1,17.5304 1,17V7C1,6.4696 1.2107,5.9609 1.5858,5.5858C1.9609,5.2107 2.4696,5 3,5H5L16,16Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M1,1L23,23"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
5
vector/src/main/res/drawable/ic_videocam.xml
Normal file
5
vector/src/main/res/drawable/ic_videocam.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M17,10.5V7c0,-0.55 -0.45,-1 -1,-1H4c-0.55,0 -1,0.45 -1,1v10c0,0.55 0.45,1 1,1h12c0.55,0 1,-0.45 1,-1v-3.5l4,4v-11l-4,4z"/>
|
||||
</vector>
|
11
vector/src/main/res/drawable/oval_destructive.xml
Normal file
11
vector/src/main/res/drawable/oval_destructive.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
|
||||
<size
|
||||
android:width="40dp"
|
||||
android:height="40dp" />
|
||||
|
||||
<solid android:color="@color/riotx_destructive_accent" />
|
||||
|
||||
</shape>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue