diff --git a/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt index 5230069f1e..69c0fab9a8 100644 --- a/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt @@ -153,7 +153,7 @@ interface VectorComponent { fun pinLocker(): PinLocker - fun webRtcPeerConnectionManager(): WebRtcCallManager + fun webRtcCallManager(): WebRtcCallManager @Component.Factory interface Factory { diff --git a/vector/src/main/java/im/vector/app/core/services/CallService.kt b/vector/src/main/java/im/vector/app/core/services/CallService.kt index 4fcc9cee28..397394e4fe 100644 --- a/vector/src/main/java/im/vector/app/core/services/CallService.kt +++ b/vector/src/main/java/im/vector/app/core/services/CallService.kt @@ -63,7 +63,7 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe override fun onCreate() { super.onCreate() notificationUtils = vectorComponent().notificationUtils() - callManager = vectorComponent().webRtcPeerConnectionManager() + callManager = vectorComponent().webRtcCallManager() callRingPlayerIncoming = CallRingPlayerIncoming(applicationContext) callRingPlayerOutgoing = CallRingPlayerOutgoing(applicationContext) wiredHeadsetStateReceiver = WiredHeadsetStateReceiver.createAndRegister(this, this) diff --git a/vector/src/main/java/im/vector/app/core/ui/views/ActiveCallViewHolder.kt b/vector/src/main/java/im/vector/app/core/ui/views/ActiveCallViewHolder.kt index 55ce5cb0d7..b74d13e232 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/ActiveCallViewHolder.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/ActiveCallViewHolder.kt @@ -23,7 +23,7 @@ import im.vector.app.core.utils.DebouncedClickListener import im.vector.app.features.call.webrtc.WebRtcCallManager import org.matrix.android.sdk.api.session.call.CallState import im.vector.app.features.call.utils.EglUtils -import org.matrix.android.sdk.api.session.call.MxCall +import im.vector.app.features.call.webrtc.WebRtcCall import org.webrtc.RendererCommon import org.webrtc.SurfaceViewRenderer @@ -32,26 +32,28 @@ class ActiveCallViewHolder { private var activeCallPiP: SurfaceViewRenderer? = null private var activeCallView: ActiveCallView? = null private var pipWrapper: CardView? = null + private var activeCall: WebRtcCall? = null private var activeCallPipInitialized = false - fun updateCall(activeCall: MxCall?, callManager: WebRtcCallManager) { - val hasActiveCall = activeCall?.state is CallState.Connected + fun updateCall(activeCall: WebRtcCall?) { + this.activeCall = activeCall + val hasActiveCall = activeCall?.mxCall?.state is CallState.Connected if (hasActiveCall) { - val isVideoCall = activeCall?.isVideoCall == true + val isVideoCall = activeCall?.mxCall?.isVideoCall == true if (isVideoCall) initIfNeeded() activeCallView?.isVisible = !isVideoCall pipWrapper?.isVisible = isVideoCall activeCallPiP?.isVisible = isVideoCall activeCallPiP?.let { - callManager.attachViewRenderers(null, it, null) + activeCall?.attachViewRenderers(null, it, null) } } else { activeCallView?.isVisible = false activeCallPiP?.isVisible = false pipWrapper?.isVisible = false activeCallPiP?.let { - callManager.detachRenderers(listOf(it)) + activeCall?.detachRenderers(listOf(it)) } } } @@ -82,9 +84,9 @@ class ActiveCallViewHolder { ) } - fun unBind(callManager: WebRtcCallManager) { + fun unBind() { activeCallPiP?.let { - callManager.detachRenderers(listOf(it)) + activeCall?.detachRenderers(listOf(it)) } if (activeCallPipInitialized) { activeCallPiP?.release() diff --git a/vector/src/main/java/im/vector/app/features/call/SharedActiveCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/SharedActiveCallViewModel.kt index bff4a164ca..e35ed3e87a 100644 --- a/vector/src/main/java/im/vector/app/features/call/SharedActiveCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/SharedActiveCallViewModel.kt @@ -18,6 +18,7 @@ package im.vector.app.features.call import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import im.vector.app.features.call.webrtc.WebRtcCall import im.vector.app.features.call.webrtc.WebRtcCallManager import org.matrix.android.sdk.api.session.call.MxCall import javax.inject.Inject @@ -26,27 +27,27 @@ class SharedActiveCallViewModel @Inject constructor( private val callManager: WebRtcCallManager ) : ViewModel() { - val activeCall: MutableLiveData = MutableLiveData() + val activeCall: MutableLiveData = MutableLiveData() - val callStateListener = object : MxCall.StateListener { + val callStateListener = object : WebRtcCall.Listener { override fun onStateUpdate(call: MxCall) { if (activeCall.value?.callId == call.callId) { - activeCall.postValue(call) + activeCall.postValue(callManager.getCallById(call.callId)) } } } private val listener = object : WebRtcCallManager.CurrentCallListener { - override fun onCurrentCallChange(call: MxCall?) { - activeCall.value?.removeListener(callStateListener) + override fun onCurrentCallChange(call: WebRtcCall?) { + activeCall.value?.mxCall?.removeListener(callStateListener) activeCall.postValue(call) call?.addListener(callStateListener) } } init { - activeCall.postValue(callManager.currentCall?.mxCall) + activeCall.postValue(callManager.currentCall) callManager.addCurrentCallListener(listener) } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt index 188932c43d..56e877a619 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt @@ -45,6 +45,8 @@ import im.vector.app.core.utils.PERMISSIONS_FOR_AUDIO_IP_CALL import im.vector.app.core.utils.PERMISSIONS_FOR_VIDEO_IP_CALL import im.vector.app.core.utils.allGranted import im.vector.app.core.utils.checkPermissions +import im.vector.app.features.call.utils.EglUtils +import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.RoomDetailActivity import im.vector.app.features.home.room.detail.RoomDetailArgs @@ -52,8 +54,6 @@ import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.activity_call.* import org.matrix.android.sdk.api.session.call.CallState -import im.vector.app.features.call.utils.EglUtils -import im.vector.app.features.call.webrtc.WebRtcCallManager 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 @@ -211,7 +211,7 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis } override fun onDestroy() { - callManager.detachRenderers(listOf(pipRenderer, fullscreenRenderer)) + callManager.getCallById(callArgs.callId)?.detachRenderers(listOf(pipRenderer, fullscreenRenderer)) if (surfaceRenderersAreInitialized) { pipRenderer.release() fullscreenRenderer.release() @@ -234,7 +234,7 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis callConnectingProgress.isVisible = false when (callState) { is CallState.Idle, - is CallState.Dialing -> { + is CallState.Dialing -> { callVideoGroup.isInvisible = true callInfoGroup.isVisible = true callStatusText.setText(R.string.call_ring) @@ -248,14 +248,14 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis configureCallInfo(state) } - is CallState.Answering -> { + is CallState.Answering -> { callVideoGroup.isInvisible = true callInfoGroup.isVisible = true callStatusText.setText(R.string.call_connecting) callConnectingProgress.isVisible = true configureCallInfo(state) } - is CallState.Connected -> { + is CallState.Connected -> { if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { if (callArgs.isVideoCall) { callVideoGroup.isVisible = true @@ -276,12 +276,12 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis callConnectingProgress.isVisible = true } // ensure all attached? - callManager.attachViewRenderers(pipRenderer, fullscreenRenderer, null) + callManager.getCallById(callArgs.callId)?.attachViewRenderers(pipRenderer, fullscreenRenderer, null) } - is CallState.Terminated -> { + is CallState.Terminated -> { finish() } - null -> { + null -> { } } } @@ -326,7 +326,7 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis pipRenderer.setEnableHardwareScaler(true /* enabled */) fullscreenRenderer.setEnableHardwareScaler(true /* enabled */) - callManager.attachViewRenderers(pipRenderer, fullscreenRenderer, + callManager.getCallById(callArgs.callId)?.attachViewRenderers(pipRenderer, fullscreenRenderer, intent.getStringExtra(EXTRA_MODE)?.takeIf { isFirstCreation() }) pipRenderer.setOnClickListener { @@ -338,14 +338,14 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis private fun handleViewEvents(event: VectorCallViewEvents?) { Timber.v("## VOIP handleViewEvents $event") when (event) { - VectorCallViewEvents.DismissNoCall -> { + VectorCallViewEvents.DismissNoCall -> { CallService.onNoActiveCall(this) finish() } is VectorCallViewEvents.ConnectionTimeout -> { onErrorTimoutConnect(event.turn) } - null -> { + null -> { } } } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt index 1fe8e3a0f1..1990d6fa9c 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt @@ -107,7 +107,7 @@ class VectorCallViewModel @AssistedInject constructor( } private val currentCallListener = object : WebRtcCallManager.CurrentCallListener { - override fun onCurrentCallChange(call: MxCall?) { + override fun onCurrentCallChange(call: WebRtcCall?) { // we need to check the state if (call == null) { // we should dismiss, e.g handled by other session? @@ -155,9 +155,9 @@ class VectorCallViewModel @AssistedInject constructor( otherUserMatrixItem = item?.let { Success(it) } ?: Uninitialized, soundDevice = currentSoundDevice, availableSoundDevices = callManager.callAudioManager.getAvailableSoundDevices(), - isFrontCamera = callManager.currentCameraType() == CameraType.FRONT, - canSwitchCamera = callManager.canSwitchCamera(), - isHD = webRtcCall.mxCall.isVideoCall && callManager.currentCaptureFormat() is CaptureFormat.HD + isFrontCamera = call?.currentCameraType() == CameraType.FRONT, + canSwitchCamera = call?.canSwitchCamera() ?: false, + isHD = webRtcCall.mxCall.isVideoCall && webRtcCall.currentCaptureFormat() is CaptureFormat.HD ) } } diff --git a/vector/src/main/java/im/vector/app/features/call/service/CallHeadsUpActionReceiver.kt b/vector/src/main/java/im/vector/app/features/call/service/CallHeadsUpActionReceiver.kt index b2b24a8e24..9991b4f753 100644 --- a/vector/src/main/java/im/vector/app/features/call/service/CallHeadsUpActionReceiver.kt +++ b/vector/src/main/java/im/vector/app/features/call/service/CallHeadsUpActionReceiver.kt @@ -27,17 +27,22 @@ class CallHeadsUpActionReceiver : BroadcastReceiver() { companion object { const val EXTRA_CALL_ACTION_KEY = "EXTRA_CALL_ACTION_KEY" + const val EXTRA_CALL_ID = "EXTRA_CALL_ID" const val CALL_ACTION_REJECT = 0 } override fun onReceive(context: Context, intent: Intent?) { - val peerConnectionManager = (context.applicationContext as? HasVectorInjector) + val webRtcCallManager = (context.applicationContext as? HasVectorInjector) ?.injector() - ?.webRtcPeerConnectionManager() + ?.webRtcCallManager() ?: return + when (intent?.getIntExtra(EXTRA_CALL_ACTION_KEY, 0)) { - CALL_ACTION_REJECT -> onCallRejectClicked(peerConnectionManager) + CALL_ACTION_REJECT -> { + val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: return + onCallRejectClicked(webRtcCallManager, callId) + } } // Not sure why this should be needed @@ -48,9 +53,9 @@ class CallHeadsUpActionReceiver : BroadcastReceiver() { // context.stopService(Intent(context, CallHeadsUpService::class.java)) } - private fun onCallRejectClicked(callManager: WebRtcCallManager) { + private fun onCallRejectClicked(callManager: WebRtcCallManager, callId: String) { Timber.d("onCallRejectClicked") - callManager.endCall() + callManager.getCallById(callId)?.endCall() } // private fun onCallAnswerClicked(context: Context) { diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/PeerConnectionObserver.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/PeerConnectionObserver.kt index 40670412c9..681a0caeac 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/PeerConnectionObserver.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/PeerConnectionObserver.kt @@ -19,6 +19,7 @@ package im.vector.app.features.call.webrtc import im.vector.app.features.call.CallAudioManager import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.MxPeerConnectionState +import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent import org.webrtc.DataChannel import org.webrtc.IceCandidate import org.webrtc.MediaStream @@ -132,9 +133,7 @@ class PeerConnectionObserver(private val webRtcCall: WebRtcCall, * It is, however, possible that the ICE agent did find compatible connections for some components. */ PeerConnection.IceConnectionState.FAILED -> { - // I should not hangup here.. - // because new candidates could arrive - // webRtcCall.mxCall.hangUp() + webRtcCall.endCall(true, CallHangupContent.Reason.ICE_FAILED) } /** * The ICE agent has finished gathering candidates, has checked all pairs against one another, and has found a connection for all components. diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt index cb8dcf7820..5f400bb5fe 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt @@ -42,6 +42,7 @@ 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 @@ -88,9 +89,9 @@ class WebRtcCall(val mxCall: MxCall, private val dispatcher: CoroutineContext, private val sessionProvider: Provider, private val peerConnectionFactoryProvider: Provider, - private val onCallEnded: (WebRtcCall) -> Unit): MxCall.StateListener { + private val onCallEnded: (WebRtcCall) -> Unit) : MxCall.StateListener { - interface Listener: MxCall.StateListener { + interface Listener : MxCall.StateListener { fun onCaptureStateChanged() {} fun onCameraChange() {} } @@ -245,7 +246,7 @@ class WebRtcCall(val mxCall: MxCall, isVideo = mxCall.isVideoCall, roomName = name, roomId = mxCall.roomId, - matrixId = session?.myUserId ?:"", + matrixId = session?.myUserId ?: "", callId = mxCall.callId) } @@ -461,8 +462,13 @@ class WebRtcCall(val mxCall: MxCall, ?: backCamera?.also { cameraInUse = backCamera } ?: null.also { cameraInUse = null } + listeners.forEach { + tryOrNull { it.onCameraChange() } + } + if (camera != null) { val videoCapturer = cameraIterator.createCapturer(camera.name, object : CameraEventsHandlerAdapter() { + override fun onFirstFrameAvailable() { super.onFirstFrameAvailable() videoCapturerIsInError = false @@ -470,12 +476,25 @@ class WebRtcCall(val mxCall: MxCall, override fun onCameraClosed() { super.onCameraClosed() + Timber.v("onCameraClosed") // This could happen if you open the camera app in chat // We then register in order to restart capture as soon as the camera is available again videoCapturerIsInError = true val cameraManager = context.getSystemService() cameraAvailabilityCallback = object : CameraManager.AvailabilityCallback() { + + override fun onCameraUnavailable(cameraId: String) { + super.onCameraUnavailable(cameraId) + Timber.v("On camera unavailable: $cameraId") + } + + override fun onCameraAccessPrioritiesChanged() { + super.onCameraAccessPrioritiesChanged() + Timber.v("onCameraAccessPrioritiesChanged") + } + override fun onCameraAvailable(cameraId: String) { + Timber.v("On camera available: $cameraId") if (cameraId == camera.name) { videoCapturer?.startCapture(currentCaptureFormat.width, currentCaptureFormat.height, currentCaptureFormat.fps) cameraManager?.unregisterAvailabilityCallback(this) @@ -505,11 +524,9 @@ class WebRtcCall(val mxCall: MxCall, } fun setCaptureFormat(format: CaptureFormat) { - GlobalScope.launch(dispatcher) { - Timber.v("## VOIP setCaptureFormat $format") - videoCapturer?.changeCaptureFormat(format.width, format.height, format.fps) - currentCaptureFormat = format - } + Timber.v("## VOIP setCaptureFormat $format") + videoCapturer?.changeCaptureFormat(format.width, format.height, format.fps) + currentCaptureFormat = format } private fun updateMuteStatus() { @@ -565,31 +582,41 @@ class WebRtcCall(val mxCall: MxCall, } fun canSwitchCamera(): Boolean { - return availableCamera.size > 0 + return availableCamera.size > 1 + } + + private fun getOppositeCameraIfAny(): CameraProxy? { + val currentCamera = cameraInUse ?: return null + return if (currentCamera.type == CameraType.FRONT) { + availableCamera.firstOrNull { it.type == CameraType.BACK } + } else { + availableCamera.firstOrNull { it.type == CameraType.FRONT } + } } fun switchCamera() { Timber.v("## VOIP switchCamera") - if (!canSwitchCamera()) return if (mxCall.state is CallState.Connected && mxCall.isVideoCall) { - videoCapturer?.switchCamera(object : CameraVideoCapturer.CameraSwitchHandler { - // Invoked on success. |isFrontCamera| is true if the new camera is front facing. - override fun onCameraSwitchDone(isFrontCamera: Boolean) { - Timber.v("## VOIP onCameraSwitchDone isFront $isFrontCamera") - cameraInUse = availableCamera.first { if (isFrontCamera) it.type == CameraType.FRONT else it.type == CameraType.BACK } - localSurfaceRenderers.forEach { - it.get()?.setMirror(isFrontCamera) - } - listeners.forEach { - tryOrNull { it.onCameraChange() } - } + val oppositeCamera = getOppositeCameraIfAny() ?: return + videoCapturer?.switchCamera( + object : CameraVideoCapturer.CameraSwitchHandler { + // Invoked on success. |isFrontCamera| is true if the new camera is front facing. + override fun onCameraSwitchDone(isFrontCamera: Boolean) { + Timber.v("## VOIP onCameraSwitchDone isFront $isFrontCamera") + cameraInUse = oppositeCamera + localSurfaceRenderers.forEach { + it.get()?.setMirror(isFrontCamera) + } + listeners.forEach { + tryOrNull { it.onCameraChange() } + } + } - } - - override fun onCameraSwitchError(errorDescription: String?) { - Timber.v("## VOIP onCameraSwitchError isFront $errorDescription") - } - }) + override fun onCameraSwitchError(errorDescription: String?) { + Timber.v("## VOIP onCameraSwitchError isFront $errorDescription") + } + }, oppositeCamera.name + ) } } @@ -665,6 +692,9 @@ class WebRtcCall(val mxCall: MxCall, } fun endCall(originatedByMe: Boolean = true, reason: CallHangupContent.Reason? = null) { + if(mxCall.state == CallState.Terminated){ + return + } mxCall.state = CallState.Terminated //Close tracks ASAP localVideoTrack?.setEnabled(false) @@ -704,6 +734,7 @@ class WebRtcCall(val mxCall: MxCall, try { peerConnection?.awaitSetRemoteDescription(sdp) } catch (failure: Throwable) { + endCall(true, CallHangupContent.Reason.UNKWOWN_ERROR) return@launch } if (mxCall.opponentPartyId?.hasValue().orFalse()) { diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt index 913379752b..180b7f2f92 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt @@ -31,7 +31,6 @@ import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.utils.EglUtils import im.vector.app.push.fcm.FcmHelper import kotlinx.coroutines.asCoroutineDispatcher -import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.call.CallListener @@ -67,7 +66,7 @@ class WebRtcCallManager @Inject constructor( get() = activeSessionDataSource.currentValue?.orNull() interface CurrentCallListener { - fun onCurrentCallChange(call: MxCall?) + fun onCurrentCallChange(call: WebRtcCall?) fun onCaptureStateChanged() {} fun onAudioDevicesChange() {} fun onCameraChange() {} @@ -118,31 +117,32 @@ class WebRtcCallManager @Inject constructor( set(value) { field = value currentCallsListeners.forEach { - tryOrNull { it.onCurrentCallChange(value?.mxCall) } + tryOrNull { it.onCurrentCallChange(value) } } } private val callsByCallId = HashMap() + private val callsByRoomId = HashMap>() fun getCallById(callId: String): WebRtcCall? { return callsByCallId[callId] } - fun headSetButtonTapped() { - Timber.v("## VOIP headSetButtonTapped") - val call = currentCall?.mxCall ?: return - if (call.state is CallState.LocalRinging) { - // accept call - acceptIncomingCall() - } - if (call.state is CallState.Connected) { - // end call? - endCall() - } + fun getCallsByRoomId(roomId: String): List { + return callsByRoomId[roomId] ?: emptyList() } - fun attachViewRenderers(localViewRenderer: SurfaceViewRenderer?, remoteViewRenderer: SurfaceViewRenderer, mode: String?) { - currentCall?.attachViewRenderers(localViewRenderer, remoteViewRenderer, mode) + fun headSetButtonTapped() { + Timber.v("## VOIP headSetButtonTapped") + val call = currentCall ?: return + if (call.mxCall.state is CallState.LocalRinging) { + // accept call + call.acceptIncomingCall() + } + if (call.mxCall.state is CallState.Connected) { + // end call? + call.endCall() + } } private fun createPeerConnectionFactoryIfNeeded() { @@ -174,20 +174,13 @@ class WebRtcCallManager @Inject constructor( .createPeerConnectionFactory() } - fun acceptIncomingCall() { - currentCall?.acceptIncomingCall() - } - - fun detachRenderers(renderers: List?) { - currentCall?.detachRenderers(renderers) - } - private fun onCallEnded(call: WebRtcCall) { Timber.v("## VOIP WebRtcPeerConnectionManager onCall ended: ${call.mxCall.callId}") CallService.onNoActiveCall(context) callAudioManager.stop() currentCall = null callsByCallId.remove(call.mxCall.callId) + callsByRoomId[call.mxCall.roomId]?.remove(call) // This must be done in this thread executor.execute { if (currentCall == null) { @@ -231,9 +224,48 @@ class WebRtcCallManager @Inject constructor( currentCall?.onCallIceCandidateReceived(iceCandidatesContent) } + private fun createWebRtcCall(mxCall: MxCall): WebRtcCall { + val webRtcCall = WebRtcCall( + mxCall = mxCall, + callAudioManager = callAudioManager, + rootEglBase = rootEglBase, + context = context, + dispatcher = dispatcher, + peerConnectionFactoryProvider = { + createPeerConnectionFactoryIfNeeded() + peerConnectionFactory + }, + sessionProvider = { currentSession }, + onCallEnded = this::onCallEnded + ) + currentCall = webRtcCall + callsByCallId[mxCall.callId] = webRtcCall + callsByRoomId.getOrPut(mxCall.roomId, { ArrayList() }).add(webRtcCall) + return webRtcCall + } + + fun acceptIncomingCall() { + currentCall?.acceptIncomingCall() + } + + fun endCall(originatedByMe: Boolean = true) { + currentCall?.endCall(originatedByMe) + } + + fun onWiredDeviceEvent(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) { + Timber.v("## VOIP onWiredDeviceEvent $event") + currentCall ?: return + // sometimes we received un-wanted unplugged... + callAudioManager.wiredStateChange(event) + } + + fun onWirelessDeviceEvent(event: BluetoothHeadsetReceiver.BTHeadsetPlugEvent) { + Timber.v("## VOIP onWirelessDeviceEvent $event") + callAudioManager.bluetoothStateChange(event.plugged) + } + override fun onCallInviteReceived(mxCall: MxCall, callInviteContent: CallInviteContent) { Timber.v("## VOIP onCallInviteReceived callId ${mxCall.callId}") - // to simplify we only treat one call at a time, and ignore others if (currentCall != null) { Timber.w("## VOIP receiving incoming call while already in call?") // Just ignore, maybe we could answer from other session? @@ -267,74 +299,11 @@ class WebRtcCallManager @Inject constructor( } } - private fun createWebRtcCall(mxCall: MxCall): WebRtcCall { - val webRtcCall = WebRtcCall( - mxCall = mxCall, - callAudioManager = callAudioManager, - rootEglBase = rootEglBase, - context = context, - dispatcher = dispatcher, - peerConnectionFactoryProvider = { - createPeerConnectionFactoryIfNeeded() - peerConnectionFactory - }, - sessionProvider = { currentSession }, - onCallEnded = this::onCallEnded - ) - currentCall = webRtcCall - callsByCallId[mxCall.callId] = webRtcCall - return webRtcCall - } - - fun muteCall(muted: Boolean) { - currentCall?.muteCall(muted) - } - - fun enableVideo(enabled: Boolean) { - currentCall?.enableVideo(enabled) - } - - fun switchCamera() { - currentCall?.switchCamera() - } - - fun canSwitchCamera(): Boolean { - return currentCall?.canSwitchCamera() ?: false - } - - fun currentCameraType(): CameraType? { - return currentCall?.currentCameraType() - } - - fun setCaptureFormat(format: CaptureFormat) { - currentCall?.setCaptureFormat(format) - } - - fun currentCaptureFormat(): CaptureFormat { - return currentCall?.currentCaptureFormat() ?: CaptureFormat.HD - } - - fun endCall(originatedByMe: Boolean = true) { - currentCall?.endCall(originatedByMe) - } - - fun onWiredDeviceEvent(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) { - Timber.v("## VOIP onWiredDeviceEvent $event") - currentCall ?: return - // sometimes we received un-wanted unplugged... - callAudioManager.wiredStateChange(event) - } - - fun onWirelessDeviceEvent(event: BluetoothHeadsetReceiver.BTHeadsetPlugEvent) { - Timber.v("## VOIP onWirelessDeviceEvent $event") - callAudioManager.bluetoothStateChange(event.plugged) - } - override fun onCallAnswerReceived(callAnswerContent: CallAnswerContent) { - val call = currentCall ?: return - if (call.mxCall.callId != callAnswerContent.callId) return Unit.also { - Timber.w("onCallAnswerReceived for non active call? ${callAnswerContent.callId}") - } + val call = callsByCallId[callAnswerContent.callId] + ?: return Unit.also { + Timber.w("onCallAnswerReceived for non active call? ${callAnswerContent.callId}") + } val mxCall = call.mxCall // Update service state val name = currentSession?.getUser(mxCall.opponentUserId)?.getBestName() @@ -351,48 +320,49 @@ class WebRtcCallManager @Inject constructor( } override fun onCallHangupReceived(callHangupContent: CallHangupContent) { - val call = currentCall ?: return - // Remote echos are filtered, so it's only remote hangups that i will get here - if (call.mxCall.callId != callHangupContent.callId) return Unit.also { - Timber.w("onCallHangupReceived for non active call? ${callHangupContent.callId}") - } - endCall(false) + val call = callsByCallId[callHangupContent.callId] + ?: return Unit.also { + Timber.w("onCallHangupReceived for non active call? ${callHangupContent.callId}") + } + call.endCall(false) } override fun onCallRejectReceived(callRejectContent: CallRejectContent) { - val call = currentCall ?: return - // Remote echos are filtered, so it's only remote hangups that i will get here - if (call.mxCall.callId != callRejectContent.callId) return Unit.also { - Timber.w("onCallRejected for non active call? ${callRejectContent.callId}") - } - endCall(false) + val call = callsByCallId[callRejectContent.callId] + ?: return Unit.also { + Timber.w("onCallRejectReceived for non active call? ${callRejectContent.callId}") + } + call.endCall(false) } override fun onCallSelectAnswerReceived(callSelectAnswerContent: CallSelectAnswerContent) { - val call = currentCall ?: return - if (call.mxCall.callId != callSelectAnswerContent.callId) return Unit.also { - Timber.w("onCallSelectAnswerReceived for non active call? ${callSelectAnswerContent.callId}") - } + val call = callsByCallId[callSelectAnswerContent.callId] + ?: return Unit.also { + Timber.w("onCallSelectAnswerReceived for non active call? ${callSelectAnswerContent.callId}") + } val selectedPartyId = callSelectAnswerContent.selectedPartyId if (selectedPartyId != call.mxCall.ourPartyId) { Timber.i("Got select_answer for party ID ${selectedPartyId}: we are party ID ${call.mxCall.ourPartyId}."); // The other party has picked somebody else's answer - endCall(false) + call.endCall(false) } } override fun onCallNegotiateReceived(callNegotiateContent: CallNegotiateContent) { - val call = currentCall ?: return - if (call.mxCall.callId != callNegotiateContent.callId) return Unit.also { - Timber.w("onCallNegotiateReceived for non active call? ${callNegotiateContent.callId}") - } + val call = callsByCallId[callNegotiateContent.callId] + ?: return Unit.also { + Timber.w("onCallNegotiateReceived for non active call? ${callNegotiateContent.callId}") + } call.onCallNegotiateReceived(callNegotiateContent) } override fun onCallManagedByOtherSession(callId: String) { Timber.v("## VOIP onCallManagedByOtherSession: $callId") currentCall = null - callsByCallId.remove(callId) + val webRtcCall = callsByCallId.remove(callId) + if (webRtcCall != null) { + callsByRoomId[webRtcCall.mxCall.roomId]?.remove(webRtcCall) + } CallService.onNoActiveCall(context) // did we start background sync? so we should stop it @@ -405,24 +375,4 @@ class WebRtcCallManager @Inject constructor( } } } - - /** - * Indicates whether we are 'on hold' to the remote party (ie. if true, - * they cannot hear us). Note that this will return true when we put the - * remote on hold too due to the way hold is implemented (since we don't - * wish to play hold music when we put a call on hold, we use 'inactive' - * rather than 'sendonly') - * @returns true if the other party has put us on hold - */ - fun isLocalOnHold(): Boolean { - return currentCall?.isLocalOnHold().orFalse() - } - - fun isRemoteOnHold(): Boolean { - return currentCall?.remoteOnHold.orFalse() - } - - fun setRemoteOnHold(onHold: Boolean) { - currentCall?.updateRemoteOnHold(onHold) - } } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt index 2178bfccd6..853eb31274 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt @@ -120,7 +120,7 @@ class HomeDetailFragment @Inject constructor( sharedCallActionViewModel .activeCall .observe(viewLifecycleOwner, Observer { - activeCallViewHolder.updateCall(it, callManager) + activeCallViewHolder.updateCall(it) invalidateOptionsMenu() }) } @@ -331,10 +331,10 @@ class HomeDetailFragment @Inject constructor( VectorCallActivity.newIntent( context = requireContext(), callId = call.callId, - roomId = call.roomId, - otherUserId = call.opponentUserId, - isIncomingCall = !call.isOutgoing, - isVideoCall = call.isVideoCall, + roomId = call.mxCall.roomId, + otherUserId = call.mxCall.opponentUserId, + isIncomingCall = !call.mxCall.isOutgoing, + isVideoCall = call.mxCall.isVideoCall, mode = null ).let { startActivity(it) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index ac577a0e68..a1dbc5f014 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -315,7 +315,7 @@ class RoomDetailFragment @Inject constructor( sharedCallActionViewModel .activeCall .observe(viewLifecycleOwner, Observer { - activeCallViewHolder.updateCall(it, callManager) + activeCallViewHolder.updateCall(it) invalidateOptionsMenu() }) @@ -514,7 +514,7 @@ class RoomDetailFragment @Inject constructor( } override fun onDestroy() { - activeCallViewHolder.unBind(callManager) + activeCallViewHolder.unBind() roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState) super.onDestroy() } @@ -712,7 +712,7 @@ class RoomDetailFragment @Inject constructor( val activeCall = sharedCallActionViewModel.activeCall.value if (activeCall != null) { // resume existing if same room, if not prompt to kill and then restart new call? - if (activeCall.roomId == roomDetailArgs.roomId) { + if (activeCall.mxCall.roomId == roomDetailArgs.roomId) { onTapToReturnToCall() } // else { @@ -1961,10 +1961,10 @@ class RoomDetailFragment @Inject constructor( VectorCallActivity.newIntent( context = requireContext(), callId = call.callId, - roomId = call.roomId, - otherUserId = call.opponentUserId, - isIncomingCall = !call.isOutgoing, - isVideoCall = call.isVideoCall, + roomId = call.mxCall.roomId, + otherUserId = call.mxCall.opponentUserId, + isIncomingCall = !call.mxCall.isOutgoing, + isVideoCall = call.mxCall.isVideoCall, mode = null ).let { startActivity(it) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index b6472291b2..a317177bc4 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -565,8 +565,8 @@ class RoomDetailViewModel @AssistedInject constructor( R.id.clear_all -> state.asyncRoomSummary()?.hasFailedSending == true R.id.open_matrix_apps -> true R.id.voice_call, - R.id.video_call -> true // always show for discoverability - R.id.hangup_call -> callManager.currentCall != null + R.id.video_call -> callManager.getCallsByRoomId(state.roomId).isEmpty() + R.id.hangup_call -> callManager.getCallsByRoomId(state.roomId).isNotEmpty() R.id.search -> true else -> false } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt index 44eb278c64..fbf0ed9085 100755 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt @@ -296,7 +296,6 @@ class NotificationUtils @Inject constructor(private val context: Context, builder.priority = NotificationCompat.PRIORITY_HIGH // - val requestId = Random.nextInt(1000) // val pendingIntent = stackBuilder.getPendingIntent(requestId, PendingIntent.FLAG_UPDATE_CURRENT) val contentIntent = VectorCallActivity.newIntent( @@ -326,16 +325,7 @@ class NotificationUtils @Inject constructor(private val context: Context, ) .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 - ) + val rejectCallPendingIntent = buildRejectCallPendingIntent(callId) builder.addAction( NotificationCompat.Action( @@ -375,8 +365,6 @@ class NotificationUtils @Inject constructor(private val context: Context, .setLights(accentColor, 500, 500) .setOngoing(true) - val requestId = Random.nextInt(1000) - val contentIntent = VectorCallActivity.newIntent( context = context, callId = callId, @@ -390,16 +378,7 @@ class NotificationUtils @Inject constructor(private val context: Context, } 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 - ) + val rejectCallPendingIntent = buildRejectCallPendingIntent(callId) builder.addAction( NotificationCompat.Action( @@ -446,17 +425,7 @@ class NotificationUtils @Inject constructor(private val context: Context, builder.setOngoing(true) } - val rejectCallActionReceiver = Intent(context, CallHeadsUpActionReceiver::class.java).apply { - data = Uri.parse("mxcall://end?$callId") - putExtra(CallHeadsUpActionReceiver.EXTRA_CALL_ACTION_KEY, CallHeadsUpActionReceiver.CALL_ACTION_REJECT) - } - - val rejectCallPendingIntent = PendingIntent.getBroadcast( - context, - System.currentTimeMillis().toInt(), - rejectCallActionReceiver, - PendingIntent.FLAG_UPDATE_CURRENT - ) + val rejectCallPendingIntent = buildRejectCallPendingIntent(callId) builder.addAction( NotificationCompat.Action( @@ -476,6 +445,19 @@ class NotificationUtils @Inject constructor(private val context: Context, return builder.build() } + private fun buildRejectCallPendingIntent(callId: String): PendingIntent { + val rejectCallActionReceiver = Intent(context, CallHeadsUpActionReceiver::class.java).apply { + putExtra(CallHeadsUpActionReceiver.EXTRA_CALL_ID, callId) + putExtra(CallHeadsUpActionReceiver.EXTRA_CALL_ACTION_KEY, CallHeadsUpActionReceiver.CALL_ACTION_REJECT) + } + return PendingIntent.getBroadcast( + context, + System.currentTimeMillis().toInt(), + rejectCallActionReceiver, + PendingIntent.FLAG_UPDATE_CURRENT + ) + } + /** * Build a temporary (because service will be stopped just after) notification for the CallService, when a call is ended */