webrtc: show connection information, mark call as missed if it ends while pending (#672)

This commit is contained in:
Evgeny Poberezkin
2022-05-20 07:43:44 +01:00
committed by GitHub
parent d50ebbd061
commit 9c9f6d8443
10 changed files with 101 additions and 50 deletions
@@ -513,7 +513,7 @@ open class ChatController(private val ctrl: ChatCtrl, private val ntfManager: Nt
onConfirm = {
if (chatModel.activeCallInvitation.value == null) {
AlertManager.shared.hideAlert()
AlertManager.shared.showAlertMsg(generalGetString(R.string.call_already_ended))
withApi { AlertManager.shared.showAlertMsg(generalGetString(R.string.call_already_ended)) }
} else {
chatModel.activeCallInvitation.value = null
chatModel.activeCall.value = Call(
@@ -546,18 +546,20 @@ open class ChatController(private val ctrl: ChatCtrl, private val ntfManager: Nt
}
}
is CR.CallExtraInfo -> {
withCall(r, r.contact) { call ->
withCall(r, r.contact) { _ ->
chatModel.callCommand.value = WCallCommand.Ice(iceCandidates = r.extraInfo.rtcIceCandidates)
}
}
is CR.CallEnded -> {
withCall(r, r.contact) { call ->
withCall(r, r.contact) { _ ->
chatModel.callCommand.value = WCallCommand.End
chatModel.activeCall.value = null
chatModel.activeCallInvitation.value = null
chatModel.callCommand.value = null
chatModel.showCallView.value = false
withApi {
chatModel.activeCall.value = null
chatModel.callCommand.value = null
}
}
chatModel.activeCallInvitation.value = null
chatModel.showCallView.value = false
}
else ->
Log.d(TAG , "unsupported event: ${r.responseType}")
@@ -836,7 +838,7 @@ sealed class CC {
is ApiSendCallAnswer -> "/_call answer @${contact.apiId} ${json.encodeToString(answer)}"
is ApiSendCallExtraInfo -> "/_call extra @${contact.apiId} ${json.encodeToString(extraInfo)}"
is ApiEndCall -> "/_call end @${contact.apiId}"
is ApiCallStatus -> "/_call status @${contact.apiId} ${callStatus}"
is ApiCallStatus -> "/_call status @${contact.apiId} ${callStatus.value}"
is ApiChatRead -> "/_read chat ${chatRef(type, id)} from=${range.from} to=${range.to}"
is ReceiveFile -> "/freceive $fileId"
}
@@ -3,6 +3,7 @@ package chat.simplex.app.views.call
import android.Manifest
import android.content.ClipData
import android.content.ClipboardManager
import android.graphics.fonts.FontStyle
import android.os.Build
import android.service.controls.templates.ControlButton
import android.util.Log
@@ -26,6 +27,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.capitalize
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -189,7 +191,7 @@ private fun ActiveCallOverlayLayout(
private fun ControlButton(call: Call, icon: ImageVector, @StringRes iconText: Int, action: () -> Unit) {
if (call.hasMedia) {
IconButton(onClick = action) {
Icon(icon, stringResource(iconText), tint = Color.White, modifier = Modifier.size(40.dp))
Icon(icon, stringResource(iconText), tint = Color(0xFFFFFFD8), modifier = Modifier.size(40.dp))
}
} else {
Spacer(Modifier.size(40.dp))
@@ -207,10 +209,16 @@ private fun ToggleAudioButton(call: Call, toggleAudio: () -> Unit) {
@Composable
private fun CallInfoView(call: Call, alignment: Alignment.Horizontal) {
@Composable fun InfoText(text: String, style: TextStyle = MaterialTheme.typography.body2) =
Text(text, color = Color(0xFFFFFFD8), style = style)
Column(horizontalAlignment = alignment) {
Text(call.contact.chatViewName, color = Color.White, style = MaterialTheme.typography.body2)
Text(call.callState.text, color = Color.White, style = MaterialTheme.typography.body2)
Text(call.encryptionStatus, color = Color.White, style = MaterialTheme.typography.body2)
InfoText(call.contact.chatViewName, style = MaterialTheme.typography.h2)
InfoText(call.callState.text)
val connInfo =
if (call.connectionInfo == null) ""
else " (${call.connectionInfo.text})"
InfoText(call.encryptionStatus + connInfo)
}
}
@@ -390,7 +398,8 @@ fun PreviewActiveCallOverlayVideo() {
contact = Contact.sampleData,
callState = CallState.Negotiated,
localMedia = CallMediaType.Video,
peerMedia = CallMediaType.Video
peerMedia = CallMediaType.Video,
connectionInfo = ConnectionInfo(RTCIceCandidate(RTCIceCandidateType.Host), RTCIceCandidate(RTCIceCandidateType.Host))
),
dismiss = {},
toggleAudio = {},
@@ -409,7 +418,8 @@ fun PreviewActiveCallOverlayAudio() {
contact = Contact.sampleData,
callState = CallState.Negotiated,
localMedia = CallMediaType.Audio,
peerMedia = CallMediaType.Audio
peerMedia = CallMediaType.Audio,
connectionInfo = ConnectionInfo(RTCIceCandidate(RTCIceCandidateType.Host), RTCIceCandidate(RTCIceCandidateType.Host))
),
dismiss = {},
toggleAudio = {},
@@ -96,7 +96,16 @@ sealed class WCallResponse {
})
}
@Serializable class CallCapabilities(val encryption: Boolean)
@Serializable class ConnectionInfo(val localCandidate: RTCIceCandidate?, val remoteCandidate: RTCIceCandidate)
@Serializable class ConnectionInfo(val localCandidate: RTCIceCandidate?, val remoteCandidate: RTCIceCandidate) {
val text: String @Composable get() = when {
localCandidate?.candidateType == RTCIceCandidateType.Host && remoteCandidate?.candidateType == RTCIceCandidateType.Host ->
stringResource(R.string.call_connection_peer_to_peer)
localCandidate?.candidateType == RTCIceCandidateType.Relay && remoteCandidate?.candidateType == RTCIceCandidateType.Relay ->
stringResource(R.string.call_connection_via_relay)
else ->
"${localCandidate?.candidateType?.value ?: "unknown"} / ${remoteCandidate?.candidateType?.value ?: "unknown"}"
}
}
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate
@Serializable class RTCIceCandidate(val candidateType: RTCIceCandidateType?)
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer
@@ -104,19 +113,19 @@ sealed class WCallResponse {
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate/type
@Serializable
enum class RTCIceCandidateType {
@SerialName("host") Host,
@SerialName("srflx") ServerReflexive,
@SerialName("prflx") PeerReflexive,
@SerialName("relay") Relay
enum class RTCIceCandidateType(val value: String) {
@SerialName("host") Host("host"),
@SerialName("srflx") ServerReflexive("srflx"),
@SerialName("prflx") PeerReflexive("prflx"),
@SerialName("relay") Relay("relay")
}
@Serializable
enum class WebRTCCallStatus {
@SerialName("connected") Connected,
@SerialName("connecting") Connecting,
@SerialName("disconnected") Disconnected,
@SerialName("failed") Failed
enum class WebRTCCallStatus(val value: String) {
@SerialName("connected") Connected("connected"),
@SerialName("connecting") Connecting("connecting"),
@SerialName("disconnected") Disconnected("disconnected"),
@SerialName("failed") Failed("failed")
}
@Serializable
@@ -3,6 +3,7 @@ package chat.simplex.app.views.chat
import android.content.res.Configuration
import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.annotation.StringRes
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.*
@@ -18,12 +19,14 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import chat.simplex.app.R
import chat.simplex.app.TAG
@@ -174,42 +177,42 @@ fun ChatLayout(
@Composable
fun ChatInfoToolbar(chat: Chat, back: () -> Unit, info: () -> Unit, startCall: (CallMediaType) -> Unit) {
@Composable fun toolbarButton(icon: ImageVector, @StringRes textId: Int, modifier: Modifier = Modifier.padding(0.dp), onClick: () -> Unit) {
IconButton(onClick, modifier = modifier) {
Icon(icon, stringResource(textId), tint = MaterialTheme.colors.primary)
}
}
Column {
Box(
Modifier
.fillMaxWidth()
.height(52.dp)
.background(if (isSystemInDarkTheme()) ToolbarDark else ToolbarLight)
.padding(horizontal = 8.dp),
.padding(horizontal = 4.dp),
contentAlignment = Alignment.CenterStart,
) {
IconButton(onClick = back) {
Icon(
Icons.Outlined.ArrowBackIos,
stringResource(R.string.back),
tint = MaterialTheme.colors.primary,
modifier = Modifier.padding(10.dp)
)
}
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) {
IconButton(onClick = { startCall(CallMediaType.Video) }) {
Icon(
Icons.Outlined.Videocam,
"video call",
tint = MaterialTheme.colors.primary,
modifier = Modifier.padding(10.dp)
)
val cInfo = chat.chatInfo
toolbarButton(Icons.Outlined.ArrowBackIos, R.string.back, onClick = back)
if (cInfo is ChatInfo.Direct) {
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) {
Box(Modifier.width(85.dp), contentAlignment = Alignment.CenterStart) {
toolbarButton(Icons.Outlined.Phone, R.string.icon_descr_audio_call) {
startCall(CallMediaType.Audio)
}
}
toolbarButton(Icons.Outlined.Videocam, R.string.icon_descr_video_call) {
startCall(CallMediaType.Video)
}
}
}
Row(
Modifier
.padding(horizontal = 68.dp)
.padding(horizontal = 80.dp)
.fillMaxWidth()
.clickable(onClick = info),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
val cInfo = chat.chatInfo
ChatInfoImage(cInfo, size = 40.dp)
Column(
Modifier.padding(start = 8.dp),
@@ -295,7 +295,7 @@
<string name="callstatus_accepted">принятый звонок</string>
<string name="callstatus_connecting">соединяется…</string>
<string name="callstatus_in_progress">активный звонок</string>
<string name="callstatus_ended">звонок завершён <xliff:g id="duration" example="01:15">%1$s!</xliff:g></string>
<string name="callstatus_ended">звонок завершён <xliff:g id="duration" example="01:15">%1$s</xliff:g></string>
<string name="callstatus_error">ошибка соединения</string>
<!-- CallState -->
@@ -347,15 +347,19 @@
<string name="encrypted_video_call">e2e encrypted video call</string>
<string name="audio_call_no_encryption">audio call (not e2e encrypted)</string>
<string name="encrypted_audio_call">e2e encrypted audio call</string>
<string name="if_you_accept_this_call_your_ip_address_visible">If you accept this call, your IP address might be visible to your contact, unless you connect via relay.</string>
<string name="if_you_accept_this_call_your_ip_address_visible">If you accept this call and you don\'t use relay, your IP address might be visible to your contact.</string>
<string name="answer">Answer</string>
<string name="call_already_ended">Call already ended!</string>
<string name="icon_descr_video_call">video call</string>
<string name="icon_descr_audio_call">audio call</string>
<!-- Call overlay -->
<string name="status_e2e_encrypted">e2e encrypted</string>
<string name="status_no_e2e_encryption">no e2e encryption</string>
<string name="status_contact_has_e2e_encryption">contact has e2e encryption</string>
<string name="status_contact_has_no_e2e_encryption">contact has no e2e encryption</string>
<string name="call_connection_peer_to_peer">peer-to-peer</string>
<string name="call_connection_via_relay">via relay</string>
<string name="icon_descr_hang_up">Hang up</string>
<string name="icon_descr_video_off">Video off</string>
<string name="icon_descr_video_on">Video on</string>
@@ -296,7 +296,7 @@
<string name="callstatus_accepted">accepted</string>
<string name="callstatus_connecting">connecting…</string>
<string name="callstatus_in_progress">in progress</string>
<string name="callstatus_ended">ended <xliff:g id="duration" example="01:15">%1$s!</xliff:g></string>
<string name="callstatus_ended">ended <xliff:g id="duration" example="01:15">%1$s</xliff:g></string>
<string name="callstatus_error">error</string>
<!-- CallState -->
@@ -348,15 +348,19 @@
<string name="encrypted_video_call">e2e encrypted video call</string>
<string name="audio_call_no_encryption">audio call (not e2e encrypted)</string>
<string name="encrypted_audio_call">e2e encrypted audio call</string>
<string name="if_you_accept_this_call_your_ip_address_visible">If you accept this call, your IP address might be visible to your contact, unless you connect via relay.</string>
<string name="if_you_accept_this_call_your_ip_address_visible">If you accept this call and you don\'t use relay, your IP address might be visible to your contact.</string>
<string name="answer">Answer</string>
<string name="call_already_ended">Call already ended!</string>
<string name="icon_descr_video_call">video call</string>
<string name="icon_descr_audio_call">audio call</string>
<!-- Call overlay -->
<string name="status_e2e_encrypted">e2e encrypted</string>
<string name="status_no_e2e_encryption">no e2e encryption</string>
<string name="status_contact_has_e2e_encryption">contact has e2e encryption</string>
<string name="status_contact_has_no_e2e_encryption">contact has no e2e encryption</string>
<string name="call_connection_peer_to_peer">peer-to-peer</string>
<string name="call_connection_via_relay">via relay</string>
<string name="icon_descr_hang_up">Hang up</string>
<string name="icon_descr_video_off">Video off</string>
<string name="icon_descr_video_on">Video on</string>
@@ -174,7 +174,12 @@ struct ActiveCallOverlay: View {
.frame(maxWidth: .infinity, alignment: alignment)
Group {
Text(call.callState.text)
Text(call.encryptionStatus)
HStack {
Text(call.encryptionStatus)
if let connInfo = call.connectionInfo?.text {
Text("(") + Text(connInfo) + Text(")")
}
}
}
.font(.subheadline)
.frame(maxWidth: .infinity, alignment: alignment)
+13
View File
@@ -379,6 +379,19 @@ struct ConnectionState: Codable, Equatable {
struct ConnectionInfo: Codable, Equatable {
var localCandidate: RTCIceCandidate?
var remoteCandidate: RTCIceCandidate?
var text: LocalizedStringKey {
get {
if localCandidate?.candidateType == .host && remoteCandidate?.candidateType == .host {
return "peer-to-peer"
} else if localCandidate?.candidateType == .relay && remoteCandidate?.candidateType == .relay {
return "via relay"
} else {
let unknown = NSLocalizedString("unknown", comment: "connection info")
return "\(localCandidate?.candidateType?.rawValue ?? unknown)) / \(remoteCandidate?.candidateType?.rawValue ?? unknown)"
}
}
}
}
// https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate
@@ -103,7 +103,7 @@ struct ChatListView: View {
message: Text(contact.profile.displayName).bold() +
Text(" wants to connect with you via ") +
Text(invitation.callTypeText) +
Text("\nIf you accept this call, your IP address might be visible to your contact, unless you connect via relay."),
Text("\nIf you accept this call and you don't use relay, your IP address might be visible to your contact."),
primaryButton: .default(Text("Answer")) {
if chatModel.activeCallInvitation == nil {
DispatchQueue.main.async {
+1
View File
@@ -848,6 +848,7 @@ callStatusItemContent userId Contact {contactId} chatItemId receivedStatus = do
(Just CISCallProgress, WCSConnected) -> Nothing -- if call in-progress received connected -> no change
(Just CISCallProgress, WCSDisconnected) -> Just (CISCallEnded, callDuration) -- calculate in-progress duration
(Just CISCallProgress, WCSFailed) -> Just (CISCallEnded, callDuration) -- whether call disconnected or failed
(Just CISCallPending, WCSDisconnected) -> Just (CISCallMissed, 0)
(Just CISCallEnded, _) -> Nothing -- if call already ended or failed -> no change
(Just CISCallError, _) -> Nothing
(Just _, WCSConnected) -> Just (CISCallProgress, 0) -- if call ended that was never connected, duration = 0