VoIP: hold/resume fix negotiation and start adding UI

This commit is contained in:
ganfra 2020-11-30 19:10:15 +01:00
parent 1a9b0265dc
commit 8f5a11493b
30 changed files with 303 additions and 210 deletions

View File

@ -18,6 +18,7 @@ package org.matrix.android.sdk.api.session.call
import org.matrix.android.sdk.api.session.room.model.call.CallCandidate
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
import org.matrix.android.sdk.api.session.room.model.call.SdpType
import org.matrix.android.sdk.api.util.Optional
interface MxCallDetail {
@ -34,7 +35,7 @@ interface MxCallDetail {
interface MxCall : MxCallDetail {
companion object {
const val VOIP_PROTO_VERSION = 0
const val VOIP_PROTO_VERSION = 1
}
val ourPartyId: String
@ -52,7 +53,7 @@ interface MxCall : MxCallDetail {
/**
* SDP negotiation for media pause, hold/resume, ICE restarts and voice/video call up/downgrading
*/
fun negotiate(sdpString: String)
fun negotiate(sdpString: String, type: SdpType)
/**
* This has to be sent by the caller's client once it has chosen an answer.

View File

@ -37,9 +37,9 @@ data class CallAnswerContent(
*/
@Json(name = "answer") val answer: Answer,
/**
* Required. The version of the VoIP specification this messages adheres to. This specification is version 0.
* Required. The version of the VoIP specification this messages adheres to.
*/
@Json(name = "version") override val version: String? = "0"
@Json(name = "version") override val version: String?
): CallSignallingContent {
@JsonClass(generateAdapter = true)

View File

@ -38,7 +38,7 @@ data class CallCandidatesContent(
*/
@Json(name = "candidates") val candidates: List<CallCandidate> = emptyList(),
/**
* Required. The version of the VoIP specification this messages adheres to. This specification is version 0.
* Required. The version of the VoIP specification this messages adheres to.
*/
@Json(name = "version") override val version: String? = "0"
@Json(name = "version") override val version: String?
): CallSignallingContent

View File

@ -34,9 +34,9 @@ data class CallHangupContent(
*/
@Json(name = "party_id") override val partyId: String? = null,
/**
* Required. The version of the VoIP specification this message adheres to. This specification is version 0.
* Required. The version of the VoIP specification this message adheres to.
*/
@Json(name = "version") override val version: String? = "0",
@Json(name = "version") override val version: String?,
/**
* 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

View File

@ -37,9 +37,9 @@ data class CallInviteContent(
*/
@Json(name = "offer") val offer: Offer?,
/**
* Required. The version of the VoIP specification this message adheres to. This specification is version 0.
* Required. The version of the VoIP specification this message adheres to.
*/
@Json(name = "version") override val version: String? = "0",
@Json(name = "version") override val version: String?,
/**
* Required. The time in milliseconds that the invite is valid for.
* Once the invite age exceeds this value, clients should discard it.

View File

@ -43,9 +43,9 @@ data class CallNegotiateContent(
@Json(name = "description") val description: Description? = null,
/**
* Required. The version of the VoIP specification this message adheres to. This specification is version 0.
* Required. The version of the VoIP specification this message adheres to.
*/
@Json(name = "version") override val version: String? = "0",
@Json(name = "version") override val version: String?,
): CallSignallingContent {
@JsonClass(generateAdapter = true)

View File

@ -34,7 +34,7 @@ data class CallRejectContent(
*/
@Json(name = "party_id") override val partyId: String? = null,
/**
* Required. The version of the VoIP specification this message adheres to. This specification is version 0.
* Required. The version of the VoIP specification this message adheres to.
*/
@Json(name = "version") override val version: String? = "0",
@Json(name = "version") override val version: String?,
):CallSignallingContent

View File

@ -38,7 +38,7 @@ data class CallSelectAnswerContent(
@Json(name = "selected_party_id") val selectedPartyId: String? = null,
/**
* Required. The version of the VoIP specification this message adheres to. This specification is version 0.
* Required. The version of the VoIP specification this message adheres to.
*/
@Json(name = "version") override val version: String? = "0",
@Json(name = "version") override val version: String?,
): CallSignallingContent

View File

@ -30,6 +30,7 @@ import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent
import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent
import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent
import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent
import org.matrix.android.sdk.api.session.room.model.call.CallSignallingContent
@ -163,13 +164,13 @@ internal class DefaultCallSignalingService @Inject constructor(
}
private fun handleCallNegotiateEvent(event: Event) {
val content = event.getClearContent().toModel<CallSelectAnswerContent>() ?: return
val content = event.getClearContent().toModel<CallNegotiateContent>() ?: return
val call = content.getCall() ?: return
if (call.ourPartyId == content.partyId) {
// Ignore remote echo
return
}
callListenersDispatcher.onCallSelectAnswerReceived(content)
callListenersDispatcher.onCallNegotiateReceived(content)
}
private fun handleCallSelectAnswerEvent(event: Event) {

View File

@ -97,7 +97,8 @@ internal class MxCallImpl(
callId = callId,
partyId = ourPartyId,
lifetime = DefaultCallSignalingService.CALL_TIMEOUT_MS,
offer = CallInviteContent.Offer(sdp = sdpString)
offer = CallInviteContent.Offer(sdp = sdpString),
version = MxCall.VOIP_PROTO_VERSION.toString()
)
.let { createEventAndLocalEcho(type = EventType.CALL_INVITE, roomId = roomId, content = it.toContent()) }
.also { eventSenderProcessor.postEvent(it) }
@ -107,7 +108,8 @@ internal class MxCallImpl(
CallCandidatesContent(
callId = callId,
partyId = ourPartyId,
candidates = candidates
candidates = candidates,
version = MxCall.VOIP_PROTO_VERSION.toString()
)
.let { createEventAndLocalEcho(type = EventType.CALL_CANDIDATES, roomId = roomId, content = it.toContent()) }
.also { eventSenderProcessor.postEvent(it) }
@ -139,7 +141,8 @@ internal class MxCallImpl(
CallHangupContent(
callId = callId,
partyId = ourPartyId,
reason = reason ?: CallHangupContent.Reason.USER_HANGUP
reason = reason ?: CallHangupContent.Reason.USER_HANGUP,
version = MxCall.VOIP_PROTO_VERSION.toString()
)
.let { createEventAndLocalEcho(type = EventType.CALL_HANGUP, roomId = roomId, content = it.toContent()) }
.also { eventSenderProcessor.postEvent(it) }
@ -153,19 +156,21 @@ internal class MxCallImpl(
CallAnswerContent(
callId = callId,
partyId = ourPartyId,
answer = CallAnswerContent.Answer(sdp = sdpString)
answer = CallAnswerContent.Answer(sdp = sdpString),
version = MxCall.VOIP_PROTO_VERSION.toString()
)
.let { createEventAndLocalEcho(type = EventType.CALL_ANSWER, roomId = roomId, content = it.toContent()) }
.also { eventSenderProcessor.postEvent(it) }
}
override fun negotiate(sdpString: String) {
override fun negotiate(sdpString: String, type: SdpType) {
Timber.v("## VOIP negotiate $callId")
CallNegotiateContent(
callId = callId,
partyId = ourPartyId,
lifetime = DefaultCallSignalingService.CALL_TIMEOUT_MS,
description = CallNegotiateContent.Description(sdp = sdpString, type = SdpType.OFFER)
description = CallNegotiateContent.Description(sdp = sdpString, type = type),
version = MxCall.VOIP_PROTO_VERSION.toString()
)
.let { createEventAndLocalEcho(type = EventType.CALL_NEGOTIATE, roomId = roomId, content = it.toContent()) }
.also { eventSenderProcessor.postEvent(it) }
@ -178,7 +183,8 @@ internal class MxCallImpl(
CallSelectAnswerContent(
callId = callId,
partyId = ourPartyId,
selectedPartyId = opponentPartyId?.getOrNull()
selectedPartyId = opponentPartyId?.getOrNull(),
version = MxCall.VOIP_PROTO_VERSION.toString()
)
.let { createEventAndLocalEcho(type = EventType.CALL_SELECT_ANSWER, roomId = roomId, content = it.toContent()) }
.also { eventSenderProcessor.postEvent(it) }

View File

@ -53,6 +53,11 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment() {
dismiss()
}
callControlsToggleHoldResume.clickableView.debouncedClicks {
callViewModel.handle(VectorCallViewActions.ToggleHoldResume)
dismiss()
}
callViewModel.observeViewEvents {
when (it) {
is VectorCallViewEvents.ShowSoundDeviceChooser -> {
@ -71,15 +76,15 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment() {
text = getString(R.string.sound_device_wireless_headset)
textStyle = if (current == it) "bold" else "normal"
}
CallAudioManager.SoundDevice.PHONE -> span {
CallAudioManager.SoundDevice.PHONE -> span {
text = getString(R.string.sound_device_phone)
textStyle = if (current == it) "bold" else "normal"
}
CallAudioManager.SoundDevice.SPEAKER -> span {
CallAudioManager.SoundDevice.SPEAKER -> span {
text = getString(R.string.sound_device_speaker)
textStyle = if (current == it) "bold" else "normal"
}
CallAudioManager.SoundDevice.HEADSET -> span {
CallAudioManager.SoundDevice.HEADSET -> span {
text = getString(R.string.sound_device_headset)
textStyle = if (current == it) "bold" else "normal"
}
@ -90,13 +95,13 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment() {
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) -> {
getString(R.string.sound_device_phone) -> {
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.PHONE))
}
getString(R.string.sound_device_speaker) -> {
getString(R.string.sound_device_speaker) -> {
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.SPEAKER))
}
getString(R.string.sound_device_headset) -> {
getString(R.string.sound_device_headset) -> {
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.HEADSET))
}
getString(R.string.sound_device_wireless_headset) -> {
@ -111,9 +116,9 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment() {
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.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)
}
@ -134,5 +139,14 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment() {
} else {
callControlsToggleSDHD.isVisible = false
}
if (state.isRemoteOnHold) {
callControlsToggleHoldResume.title = getString(R.string.call_resume_action)
callControlsToggleHoldResume.subTitle = null
callControlsToggleHoldResume.leftIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_call_resume_action)
} else {
callControlsToggleHoldResume.title = getString(R.string.call_hold_action)
callControlsToggleHoldResume.subTitle = null
callControlsToggleHoldResume.leftIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_call_hold_action)
}
}
}

View File

@ -20,25 +20,27 @@ import android.app.KeyguardManager
import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import android.view.View
import android.view.Window
import android.view.WindowManager
import android.widget.ImageView
import androidx.appcompat.app.AlertDialog
import androidx.core.content.getSystemService
import androidx.core.view.ViewCompat
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
import androidx.core.graphics.drawable.toBitmap
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.lifecycle.lifecycleScope
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.app.R
import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.glide.GlideApp
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.services.CallService
import im.vector.app.core.utils.PERMISSIONS_FOR_AUDIO_IP_CALL
@ -57,11 +59,11 @@ import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.session.call.MxCallDetail
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState
import org.matrix.android.sdk.api.session.call.TurnServerResponse
import org.matrix.android.sdk.api.util.MatrixItem
import org.webrtc.EglBase
import org.webrtc.RendererCommon
import org.webrtc.SurfaceViewRenderer
import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@Parcelize
@ -107,63 +109,16 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis
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?) {
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
window.statusBarColor = Color.TRANSPARENT
window.navigationBarColor = Color.TRANSPARENT
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)) {
@ -179,12 +134,6 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis
turnScreenOnAndKeyguardOff()
}
constraintLayout.clicks()
.throttleFirst(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { toggleUiSystemVisibility() }
.disposeOnDestroy()
configureCallViews()
callViewModel.subscribe(this) {
@ -232,6 +181,9 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis
callControlsView.updateForState(state)
val callState = state.callState.invoke()
callConnectingProgress.isVisible = false
callActionText.setOnClickListener(null)
callActionText.isVisible = false
smallIsHeldIcon.isVisible = false
when (callState) {
is CallState.Idle,
is CallState.Dialing -> {
@ -257,15 +209,33 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis
}
is CallState.Connected -> {
if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) {
if (callArgs.isVideoCall) {
callVideoGroup.isVisible = true
callInfoGroup.isVisible = false
pip_video_view.isVisible = !state.isVideoCaptureInError
} else {
if (state.isLocalOnHold) {
smallIsHeldIcon.isVisible = true
callVideoGroup.isInvisible = true
callInfoGroup.isVisible = true
configureCallInfo(state)
if (state.isRemoteOnHold) {
callActionText.setText(R.string.call_resume_action)
callActionText.isVisible = true
callActionText.setOnClickListener { callViewModel.handle(VectorCallViewActions.ToggleHoldResume) }
callStatusText.setText(R.string.call_held_by_you)
} else {
callActionText.isInvisible = true
state.otherUserMatrixItem.invoke()?.let {
callStatusText.text = getString(R.string.call_held_by_user, it.getBestName())
}
}
} else {
callStatusText.text = null
if (callArgs.isVideoCall) {
callVideoGroup.isVisible = true
callInfoGroup.isVisible = false
pip_video_view.isVisible = !state.isVideoCaptureInError
} else {
callVideoGroup.isInvisible = true
callInfoGroup.isVisible = true
configureCallInfo(state)
}
}
} else {
// This state is not final, if you change network, new candidates will be sent
@ -288,9 +258,8 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis
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)
avatarRenderer.render(it, otherMemberAvatar)
}
}

View File

@ -24,6 +24,7 @@ sealed class VectorCallViewActions : VectorViewModelAction {
object DeclineCall : VectorCallViewActions()
object ToggleMute : VectorCallViewActions()
object ToggleVideo : VectorCallViewActions()
object ToggleHoldResume: VectorCallViewActions()
data class ChangeAudioDevice(val device: CallAudioManager.SoundDevice) : VectorCallViewActions()
object SwitchSoundDevice : VectorCallViewActions()
object HeadSetButtonPressed : VectorCallViewActions()

View File

@ -36,6 +36,7 @@ import org.matrix.android.sdk.api.session.call.MxPeerConnectionState
import org.matrix.android.sdk.api.session.call.TurnServerResponse
import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem
import timber.log.Timber
import java.util.Timer
import java.util.TimerTask
@ -53,6 +54,15 @@ class VectorCallViewModel @AssistedInject constructor(
private val callListener = object : WebRtcCall.Listener {
override fun onHoldUnhold() {
setState {
copy(
isLocalOnHold = call?.isLocalOnHold() ?: false,
isRemoteOnHold = call?.remoteOnHold ?: false
)
}
}
override fun onCaptureStateChanged() {
setState {
copy(
@ -62,7 +72,7 @@ class VectorCallViewModel @AssistedInject constructor(
}
}
override fun onCameraChange() {
override fun onCameraChanged() {
setState {
copy(
canSwitchCamera = call?.canSwitchCamera() ?: false,
@ -107,6 +117,7 @@ class VectorCallViewModel @AssistedInject constructor(
}
private val currentCallListener = object : WebRtcCallManager.CurrentCallListener {
override fun onCurrentCallChange(call: WebRtcCall?) {
// we need to check the state
if (call == null) {
@ -202,6 +213,10 @@ class VectorCallViewModel @AssistedInject constructor(
}
Unit
}
VectorCallViewActions.ToggleHoldResume -> {
val isRemoteOnHold = state.isRemoteOnHold
call?.updateRemoteOnHold(!isRemoteOnHold)
}
is VectorCallViewActions.ChangeAudioDevice -> {
callManager.callAudioManager.setCurrentSoundDevice(action.device)
setState {

View File

@ -26,6 +26,8 @@ data class VectorCallViewState(
val callId: String,
val roomId: String,
val isVideoCall: Boolean,
val isRemoteOnHold: Boolean = false,
val isLocalOnHold: Boolean = false,
val isAudioMuted: Boolean = false,
val isVideoEnabled: Boolean = true,
val isVideoCaptureInError: Boolean = false,

View File

@ -42,7 +42,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.session.call.MxCall
@ -93,7 +92,8 @@ class WebRtcCall(val mxCall: MxCall,
interface Listener : MxCall.StateListener {
fun onCaptureStateChanged() {}
fun onCameraChange() {}
fun onCameraChanged() {}
fun onHoldUnhold() {}
}
private val listeners = ArrayList<Listener>()
@ -113,6 +113,7 @@ class WebRtcCall(val mxCall: MxCall,
private var localAudioTrack: AudioTrack? = null
private var localVideoSource: VideoSource? = null
private var localVideoTrack: VideoTrack? = null
private var remoteAudioTrack: AudioTrack? = null
private var remoteVideoTrack: VideoTrack? = null
// Perfect negotiation state: https://www.w3.org/TR/webrtc/#perfect-negotiation-example
@ -192,7 +193,7 @@ class WebRtcCall(val mxCall: MxCall,
// send offer to peer
mxCall.offerSdp(sessionDescription.description)
} else {
mxCall.negotiate(sessionDescription.description)
mxCall.negotiate(sessionDescription.description, SdpType.OFFER)
}
} catch (failure: Throwable) {
// Need to handle error properly.
@ -463,7 +464,7 @@ class WebRtcCall(val mxCall: MxCall,
?: null.also { cameraInUse = null }
listeners.forEach {
tryOrNull { it.onCameraChange() }
tryOrNull { it.onCameraChanged() }
}
if (camera != null) {
@ -532,8 +533,10 @@ class WebRtcCall(val mxCall: MxCall,
private fun updateMuteStatus() {
val micShouldBeMuted = micMuted || remoteOnHold
localAudioTrack?.setEnabled(!micShouldBeMuted)
remoteAudioTrack?.setEnabled(!remoteOnHold)
val vidShouldBeMuted = videoMuted || remoteOnHold
localVideoTrack?.setEnabled(!vidShouldBeMuted)
remoteVideoTrack?.setEnabled(!remoteOnHold)
}
/**
@ -608,7 +611,7 @@ class WebRtcCall(val mxCall: MxCall,
it.get()?.setMirror(isFrontCamera)
}
listeners.forEach {
tryOrNull { it.onCameraChange() }
tryOrNull { it.onCameraChanged() }
}
}
@ -672,6 +675,11 @@ class WebRtcCall(val mxCall: MxCall,
mxCall.hangUp()
return@launch
}
if (stream.audioTracks.size == 1) {
val remoteAudioTrack = stream.audioTracks.first()
remoteAudioTrack.setEnabled(true)
this@WebRtcCall.remoteAudioTrack = remoteAudioTrack
}
if (stream.videoTracks.size == 1) {
val remoteVideoTrack = stream.videoTracks.first()
remoteVideoTrack.setEnabled(true)
@ -688,11 +696,12 @@ class WebRtcCall(val mxCall: MxCall,
.mapNotNull { it.get() }
.forEach { remoteVideoTrack?.removeSink(it) }
remoteVideoTrack = null
remoteAudioTrack = null
}
}
fun endCall(originatedByMe: Boolean = true, reason: CallHangupContent.Reason? = null) {
if(mxCall.state == CallState.Terminated){
if (mxCall.state == CallState.Terminated) {
return
}
mxCall.state = CallState.Terminated
@ -767,16 +776,24 @@ class WebRtcCall(val mxCall: MxCall,
Timber.i("Ignoring colliding negotiate event because we're impolite")
return@launch
}
val prevOnHold = isLocalOnHold()
try {
val sdp = SessionDescription(type.asWebRTC(), sdpText)
peerConnection.awaitSetRemoteDescription(sdp)
if (type == SdpType.OFFER) {
createAnswer()
mxCall.negotiate(sdpText)
createAnswer()?.also {
mxCall.negotiate(it.description, SdpType.ANSWER)
}
}
} catch (failure: Throwable) {
Timber.e(failure, "Failed to complete negotiation")
}
val nowOnHold = isLocalOnHold()
if (prevOnHold != nowOnHold) {
listeners.forEach {
tryOrNull { it.onHoldUnhold() }
}
}
}
}

View File

@ -67,19 +67,9 @@ class WebRtcCallManager @Inject constructor(
interface CurrentCallListener {
fun onCurrentCallChange(call: WebRtcCall?)
fun onCaptureStateChanged() {}
fun onAudioDevicesChange() {}
fun onCameraChange() {}
}
var capturerIsInError = false
set(value) {
field = value
currentCallsListeners.forEach {
tryOrNull { it.onCaptureStateChanged() }
}
}
private val currentCallsListeners = emptyList<CurrentCallListener>().toMutableList()
fun addCurrentCallListener(listener: CurrentCallListener) {
currentCallsListeners.add(listener)

View File

@ -121,6 +121,7 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
fun getCachedDrawable(glideRequests: GlideRequests, matrixItem: MatrixItem): Drawable {
return buildGlideRequest(glideRequests, matrixItem.avatarUrl)
.onlyRetrieveFromCache(true)
.apply(RequestOptions.circleCropTransform())
.submit()
.get()
}

View 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="M8.027,15.9613C9.168,17.1932 11.9148,19.3263 12.6635,19.7641C12.7078,19.79 12.7585,19.8201 12.8152,19.8538C13.9576,20.5329 17.5373,22.6609 20.1454,20.6694C22.1661,19.1266 21.5091,17.3909 20.8289,16.875C20.3633,16.5128 18.9914,15.5145 17.7006,14.6152C16.4331,13.7322 15.7268,14.4397 15.2492,14.918C15.2404,14.9268 15.2317,14.9355 15.2231,14.9442L14.2621,15.9051C14.0174,16.1498 13.6451,16.0605 13.2886,15.7804C12.0092,14.8061 11.0681,13.8659 10.5972,13.395L10.5933,13.391C10.1225,12.9202 9.1939,11.9908 8.2196,10.7114C7.9395,10.3548 7.8502,9.9826 8.0949,9.7379L9.0559,8.7769C9.0645,8.7683 9.0732,8.7596 9.082,8.7508C9.5603,8.2732 10.2678,7.5668 9.3848,6.2994C8.4855,5.0086 7.4872,3.6367 7.125,3.1711C6.6091,2.4909 4.8734,1.8339 3.3306,3.8546C1.3391,6.4627 3.4671,10.0424 4.1462,11.1848C4.1799,11.2415 4.2101,11.2922 4.2359,11.3365C4.6737,12.0851 6.7951,14.8203 8.027,15.9613Z"
android:fillColor="#ffffff"/>
</vector>

View 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="M12.0084,7.7565C10.3211,7.6916 6.8514,8.1295 6.0078,8.3513C5.9579,8.3645 5.9004,8.3791 5.8362,8.3955C4.541,8.7261 0.4827,9.7618 0.0442,13.0436C-0.2955,15.5862 1.4058,16.3558 2.2562,16.2386C2.8448,16.1648 4.5301,15.8983 6.0872,15.6189C7.6163,15.3446 7.6155,14.3359 7.615,13.6538C7.615,13.6413 7.615,13.6288 7.615,13.6165L7.615,12.2453C7.615,11.8961 7.9432,11.6942 8.3958,11.6396C9.9982,11.422 11.3359,11.4213 12.0055,11.4213L12.0112,11.4213C12.6807,11.4213 14.0018,11.422 15.6042,11.6396C16.0569,11.6942 16.385,11.8961 16.385,12.2453L16.385,13.6165C16.385,13.6289 16.385,13.6413 16.385,13.6538C16.3845,14.3359 16.3837,15.3446 17.9128,15.619C19.4699,15.8983 21.1552,16.1648 21.7438,16.2386C22.5942,16.3558 24.2955,15.5862 23.9558,13.0436C23.5173,9.7618 19.459,8.7261 18.1638,8.3955C18.0996,8.3791 18.0421,8.3645 17.9922,8.3513C17.1487,8.1295 13.6956,7.6916 12.0084,7.7565Z"
android:fillColor="#ffffff"/>
</vector>

View File

@ -0,0 +1,7 @@
<vector android:height="24dp"
android:viewportHeight="20"
android:viewportWidth="20"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#C1C6CD" android:pathData="M10,0C4.48,0 0,4.48 0,10C0,15.52 4.48,20 10,20C15.52,20 20,15.52 20,10C20,4.48 15.52,0 10,0ZM8,14C7.45,14 7,13.55 7,13V7C7,6.45 7.45,6 8,6C8.55,6 9,6.45 9,7V13C9,13.55 8.55,14 8,14ZM12,14C11.45,14 11,13.55 11,13V7C11,6.45 11.45,6 12,6C12.55,6 13,6.45 13,7V13C13,13.55 12.55,14 12,14Z"/>
</vector>

View 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="M11,11H19V17H11V11ZM1,19V4.98C1,3.88 1.9,3 3,3H21C22.1,3 23,3.88 23,4.98V19C23,20.1 22.1,21 21,21H3C1.9,21 1,20.1 1,19ZM3,19.02H21V4.97H3V19.02Z"
android:fillColor="#ffffff"/>
</vector>

View File

@ -0,0 +1,8 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#C1C6CD" android:pathData="M10,0C4.48,0 0,4.48 0,10C0,15.52 4.48,20 10,20C15.52,20 20,15.52 20,10C20,4.48 15.52,0 10,0ZM8,13.5V6.5C8,6.09 8.47,5.85 8.8,6.1L13.47,9.6C13.74,9.8 13.74,10.2 13.47,10.4L8.8,13.9C8.47,14.15 8,13.91 8,13.5Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M10,0C4.48,0 0,4.48 0,10C0,15.52 4.48,20 10,20C15.52,20 20,15.52 20,10C20,4.48 15.52,0 10,0ZM9,14H7V6H9V14ZM13,14H11V6H13V14Z"
android:fillColor="#ffffff"/>
</vector>

View File

@ -1,12 +1,15 @@
<?xml version="1.0" encoding="utf-8"?><!-- tools:ignore is needed because lint thinks this can be replaced with a merge. Replacing this
<?xml version="1.0" encoding="utf-8"?>
<!-- tools:ignore is needed because lint thinks this can be replaced with a merge. Replacing this
with a merge causes the fullscreen SurfaceView not to be centered. -->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/constraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?riotx_background"
android:background="@color/bg_call_screen"
tools:ignore="MergeRootFrame">
<org.webrtc.SurfaceViewRenderer
@ -26,48 +29,44 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/participantNameText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:gravity="center"
android:textColor="?riotx_text_primary"
android:textSize="28sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/callTypeText"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="@sample/matrix.json/data/displayName" />
<TextView
android:id="@+id/callTypeText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:layout_marginBottom="8dp"
android:gravity="center"
android:textColor="?riotx_text_secondary"
android:textSize="22sp"
app:layout_constraintBottom_toTopOf="@id/otherMemberAvatar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="@string/action_video_call" />
<ImageView
android:id="@+id/otherMemberAvatar"
android:layout_width="160dp"
android:layout_height="160dp"
android:layout_width="80dp"
android:layout_height="80dp"
android:contentDescription="@string/avatar"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.3"
tools:src="@tools:sample/avatars" />
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:id="@+id/smallIsHeldIcon"
android:src="@drawable/ic_call_small_pause"
app:layout_constraintTop_toTopOf="@id/otherMemberAvatar"
app:layout_constraintBottom_toBottomOf="@id/otherMemberAvatar"
app:layout_constraintStart_toStartOf="@id/otherMemberAvatar"
app:layout_constraintEnd_toEndOf="@id/otherMemberAvatar" />
<TextView
android:id="@+id/participantNameText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginTop="16dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:gravity="center"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/otherMemberAvatar"
tools:text="@sample/matrix.json/data/displayName" />
<TextView
android:id="@+id/callStatusText"
@ -78,18 +77,31 @@
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:layout_marginBottom="8dp"
android:gravity="center"
android:textColor="?riotx_text_secondary"
android:textSize="22sp"
android:textColor="@color/white"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/otherMemberAvatar"
app:layout_constraintTop_toBottomOf="@id/participantNameText"
tools:text="@string/call_connecting" />
<TextView
android:id="@+id/callActionText"
android:layout_width="0dp"
android:layout_height="48dp"
android:gravity="center"
android:textColor="?attr/colorAccent"
android:textSize="14sp"
android:layout_margin="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/callStatusText"
tools:text="@string/call_resume_action" />
<ProgressBar
android:id="@+id/callConnectingProgress"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_margin="8dp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
@ -102,7 +114,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="visible"
app:constraint_referenced_ids="participantNameText,callTypeText,otherMemberAvatar,callStatusText" />
app:constraint_referenced_ids="participantNameText, otherMemberAvatar,callStatusText" />
<androidx.constraintlayout.widget.Group
android:id="@+id/callVideoGroup"

View File

@ -30,9 +30,18 @@
android:id="@+id/callControlsToggleSDHD"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:actionTitle="@string/call_switch_camera"
app:actionTitle="@string/call_format_turn_hd_on"
app:leftIcon="@drawable/ic_hd"
app:tint="?attr/riotx_text_primary"
tools:actionDescription="Front" />
<im.vector.app.core.ui.views.BottomSheetActionButton
android:id="@+id/callControlsToggleHoldResume"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:actionTitle="Hold/resume"
app:leftIcon="@drawable/ic_call_hold_action"
app:tint="?attr/riotx_text_primary"
tools:actionDescription="" />
</LinearLayout>

View File

@ -24,7 +24,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_share"
tools:src="@drawable/ic_call_resume_action"
tools:visibility="visible"
app:tint="?riotx_text_primary"
tools:ignore="MissingPrefix" />
@ -60,7 +60,7 @@
app:layout_constraintStart_toStartOf="@+id/itemVerificationActionTitle"
app:layout_constraintTop_toBottomOf="@+id/itemVerificationActionTitle"
tools:text="For maximum security, do this in person"
tools:visibility="visible" />
/>
<ImageView
android:id="@+id/itemVerificationActionIcon"

View File

@ -15,29 +15,29 @@
<ImageView
android:id="@+id/iv_icr_accept_call"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_width="56dp"
android:layout_height="56dp"
android:background="@drawable/oval_positive"
android:clickable="true"
android:contentDescription="@string/call_notification_answer"
android:focusable="true"
android:padding="16dp"
android:src="@drawable/ic_call"
tools:ignore="MissingConstraints,MissingPrefix"
app:tint="@color/white" />
android:padding="12dp"
android:src="@drawable/ic_call_answer"
app:tint="@color/white"
tools:ignore="MissingConstraints,MissingPrefix" />
<ImageView
android:id="@+id/iv_icr_end_call"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_width="56dp"
android:layout_height="56dp"
android:background="@drawable/oval_destructive"
android:clickable="true"
android:contentDescription="@string/call_notification_reject"
android:focusable="true"
android:padding="16dp"
android:src="@drawable/ic_call_end"
tools:ignore="MissingConstraints,MissingPrefix"
app:tint="@color/white" />
android:padding="12dp"
android:src="@drawable/ic_call_hangup"
app:tint="@color/white"
tools:ignore="MissingConstraints,MissingPrefix" />
<androidx.constraintlayout.helper.widget.Flow
android:layout_width="match_parent"
@ -53,6 +53,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:paddingStart="32dp"
android:paddingEnd="32dp"
android:visibility="gone"
tools:background="@color/password_strength_bar_low"
tools:layout_marginTop="120dp"
@ -60,73 +62,69 @@
<ImageView
android:id="@+id/iv_leftMiniControl"
android:layout_width="44dp"
android:layout_height="44dp"
android:background="@drawable/oval_positive"
android:layout_width="32dp"
android:layout_height="32dp"
android:clickable="true"
android:contentDescription="@string/a11y_open_chat"
android:focusable="true"
android:padding="10dp"
android:src="@drawable/ic_home_bottom_chat"
app:backgroundTint="?attr/riotx_background"
tools:ignore="MissingConstraints,MissingPrefix"
app:tint="?attr/riotx_text_primary" />
android:padding="4dp"
android:src="@drawable/ic_call_pip"
app:tint="@android:color/white"
tools:ignore="MissingConstraints,MissingPrefix" />
<ImageView
android:id="@+id/iv_mute_toggle"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_width="56dp"
android:layout_height="56dp"
android:background="@drawable/oval_positive"
android:clickable="true"
android:focusable="true"
android:padding="16dp"
android:src="@drawable/ic_microphone_off"
app:backgroundTint="?attr/riotx_background"
app:tint="?attr/riotx_text_primary"
tools:contentDescription="@string/a11y_mute_microphone"
tools:ignore="MissingConstraints,MissingPrefix"
tools:src="@drawable/ic_microphone_on"
app:tint="?attr/riotx_text_primary" />
tools:src="@drawable/ic_microphone_on" />
<ImageView
android:id="@+id/iv_end_call"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_width="56dp"
android:layout_height="56dp"
android:background="@drawable/oval_destructive"
android:clickable="true"
android:contentDescription="@string/call_notification_hangup"
android:focusable="true"
android:padding="16dp"
android:src="@drawable/ic_call_end"
tools:ignore="MissingConstraints,MissingPrefix"
app:tint="@color/white" />
android:padding="12dp"
android:src="@drawable/ic_call_hangup"
app:tint="@color/white"
tools:ignore="MissingConstraints,MissingPrefix" />
<ImageView
android:id="@+id/iv_video_toggle"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_width="56dp"
android:layout_height="56dp"
android:background="@drawable/oval_positive"
android:clickable="true"
android:focusable="true"
android:padding="16dp"
android:src="@drawable/ic_call_videocam_off_default"
app:backgroundTint="?attr/riotx_background"
app:tint="?attr/riotx_text_primary"
tools:contentDescription="@string/a11y_stop_camera"
tools:ignore="MissingConstraints,MissingPrefix"
app:tint="?attr/riotx_text_primary" />
tools:ignore="MissingConstraints,MissingPrefix" />
<ImageView
android:id="@+id/iv_more"
android:layout_width="44dp"
android:layout_height="44dp"
android:background="@drawable/oval_positive"
android:layout_width="32dp"
android:layout_height="32dp"
android:clickable="true"
android:contentDescription="@string/settings"
android:focusable="true"
android:padding="8dp"
android:src="@drawable/ic_more_vertical"
app:backgroundTint="?attr/riotx_background"
tools:ignore="MissingConstraints,MissingPrefix"
app:tint="?attr/riotx_text_primary" />
android:src="@drawable/ic_more_horizontal"
app:tint="@android:color/white"
tools:ignore="MissingConstraints,MissingPrefix" />
<androidx.constraintlayout.helper.widget.Flow
android:layout_width="match_parent"
@ -163,8 +161,8 @@
<!-- <ImageView-->
<!-- android:id="@+id/iv_end_call"-->
<!-- android:layout_width="64dp"-->
<!-- android:layout_height="64dp"-->
<!-- android:layout_width="56dp"-->
<!-- android:layout_height="56dp"-->
<!-- android:layout_marginBottom="32dp"-->
<!-- android:background="@drawable/oval_destructive"-->
<!-- android:clickable="true"-->

View File

@ -18,6 +18,7 @@
<color name="riotx_positive_accent_alpha12">#1E0DBD8B</color>
<color name="riotx_button_disabled_alpha12">#1E61708B</color>
<color name="bg_call_screen">#99000000</color>
<color name="riotx_notice">#FFFF4B55</color>
<color name="riotx_notice_secondary">#FF61708B</color>
@ -46,6 +47,7 @@
'riotx_<name in the palette snake case>_<theme>'
-->
<attr name="riotx_background" format="color" />
<color name="riotx_background_light">#FFFFFFFF</color>
<color name="riotx_background_dark">#FF15191E</color>

View File

@ -402,6 +402,10 @@
<string name="video_call_in_progress">Video Call In Progress…</string>
<string name="active_call_with_duration">Active Call (%s)</string>
<string name="return_to_call">Return to call</string>
<string name="call_resume_action">Resume</string>
<string name="call_hold_action">Hold</string>
<string name="call_held_by_user">%s held the call</string>
<string name="call_held_by_you">You held the call</string>
<string name="call_error_user_not_responding">The remote side failed to pick up.</string>
<string name="call_error_ice_failed">Media Connection Failed</string>