From 629488bbe69ce617f5528120f2ed929c371362d5 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 22 Dec 2020 11:58:38 +0100 Subject: [PATCH] VoIP: start introducing switch call --- .../im/vector/app/core/di/ViewModelModule.kt | 6 +- .../vector/app/core/services/CallService.kt | 83 +++++++++++++------ .../app/core/ui/views/ActiveCallView.kt | 20 +++++ .../app/core/ui/views/ActiveCallViewHolder.kt | 7 +- ...Model.kt => SharedCurrentCallViewModel.kt} | 24 ++++-- .../app/features/call/VectorCallActivity.kt | 4 - .../app/features/call/VectorCallViewModel.kt | 10 +-- .../app/features/call/webrtc/WebRtcCall.kt | 10 +++ .../features/call/webrtc/WebRtcCallManager.kt | 65 +++++++++------ .../app/features/home/HomeDetailFragment.kt | 15 ++-- .../home/room/detail/RoomDetailFragment.kt | 21 ++--- .../home/room/detail/RoomDetailViewModel.kt | 2 +- .../notifications/NotificationUtils.kt | 1 + .../main/res/layout/view_active_call_view.xml | 9 +- vector/src/main/res/values/strings.xml | 6 ++ 15 files changed, 183 insertions(+), 100 deletions(-) rename vector/src/main/java/im/vector/app/features/call/{SharedActiveCallViewModel.kt => SharedCurrentCallViewModel.kt} (71%) diff --git a/vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt b/vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt index bed2e0b850..9d5c0d5491 100644 --- a/vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/ViewModelModule.kt @@ -22,7 +22,7 @@ import dagger.Binds import dagger.Module import dagger.multibindings.IntoMap import im.vector.app.core.platform.ConfigurationViewModel -import im.vector.app.features.call.SharedActiveCallViewModel +import im.vector.app.features.call.SharedCurrentCallViewModel import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreFromKeyViewModel import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreFromPassphraseViewModel import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreSharedViewModel @@ -85,8 +85,8 @@ interface ViewModelModule { @Binds @IntoMap - @ViewModelKey(SharedActiveCallViewModel::class) - fun bindSharedActiveCallViewModel(viewModel: SharedActiveCallViewModel): ViewModel + @ViewModelKey(SharedCurrentCallViewModel::class) + fun bindSharedActiveCallViewModel(viewModel: SharedCurrentCallViewModel): ViewModel @Binds @IntoMap 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 a5a59dc0ba..efdec1c251 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 @@ -22,6 +22,7 @@ import android.content.Intent import android.os.Binder import android.support.v4.media.session.MediaSessionCompat import android.view.KeyEvent +import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import androidx.media.session.MediaButtonReceiver import com.airbnb.mvrx.MvRx @@ -46,7 +47,9 @@ import timber.log.Timber class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListener, BluetoothHeadsetReceiver.EventListener { private val connections = mutableMapOf() + private val knownCalls = mutableSetOf() + private lateinit var notificationManager: NotificationManagerCompat private lateinit var notificationUtils: NotificationUtils private lateinit var callManager: WebRtcCallManager private lateinit var avatarRenderer: AvatarRenderer @@ -74,6 +77,7 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe override fun onCreate() { super.onCreate() + notificationManager = NotificationManagerCompat.from(this) notificationUtils = vectorComponent().notificationUtils() callManager = vectorComponent().webRtcCallManager() avatarRenderer = vectorComponent().avatarRenderer() @@ -130,7 +134,6 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe callRingPlayerOutgoing?.stop() displayCallInProgressNotification(intent) } - ACTION_NO_ACTIVE_CALL -> hideCallNotifications() ACTION_CALL_CONNECTING -> { // lower notification priority displayCallInProgressNotification(intent) @@ -138,6 +141,9 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe callRingPlayerIncoming?.stop() callRingPlayerOutgoing?.stop() } + ACTION_CALL_TERMINATED -> { + handleCallTerminated(intent) + } ACTION_ONGOING_CALL_BG -> { // there is an ongoing call but call activity is in background displayCallOnGoingInBackground(intent) @@ -166,11 +172,15 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe Timber.v("## VOIP displayIncomingCallNotification $intent") val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: "" val call = callManager.getCallById(callId) ?: return + if (knownCalls.contains(callId)) { + Timber.v("Call already notified $callId$") + return + } val isVideoCall = call.mxCall.isVideoCall val fromBg = intent.getBooleanExtra(EXTRA_IS_IN_BG, false) val opponentMatrixItem = getOpponentMatrixItem(call) Timber.v("displayIncomingCallNotification : display the dedicated notification") - val incomingCallAlert = IncomingCallAlert(INCOMING_CALL_ALERT_UID, + val incomingCallAlert = IncomingCallAlert(callId, shouldBeDisplayedIn = { activity -> if (activity is RoomDetailActivity) { call.roomId != activity.currentRoomId @@ -195,7 +205,27 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe title = opponentMatrixItem?.getBestName() ?: call.mxCall.opponentUserId, fromBg = fromBg ) - startForeground(NOTIFICATION_ID, notification) + if (knownCalls.isEmpty()) { + startForeground(callId.hashCode(), notification) + } else { + notificationManager.notify(callId.hashCode(), notification) + } + knownCalls.add(callId) + } + + private fun handleCallTerminated(intent: Intent) { + val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: "" + if (!knownCalls.remove(callId)) { + Timber.v("Call terminated for unknown call $callId$") + return + } + val notification = notificationUtils.buildCallEndedNotification() + notificationManager.notify(callId.hashCode(), notification) + alertManager.cancelAlert(callId) + if (knownCalls.isEmpty()) { + mediaSession?.isActive = false + myStopSelf() + } } private fun showCallScreen(call: WebRtcCall, mode: String) { @@ -210,13 +240,22 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe private fun displayOutgoingRingingCallNotification(intent: Intent) { val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: return val call = callManager.getCallById(callId) ?: return + if (knownCalls.contains(callId)) { + Timber.v("Call already notified $callId$") + return + } val opponentMatrixItem = getOpponentMatrixItem(call) Timber.v("displayOutgoingCallNotification : display the dedicated notification") val notification = notificationUtils.buildOutgoingRingingCallNotification( mxCall = call.mxCall, title = opponentMatrixItem?.getBestName() ?: call.mxCall.opponentUserId ) - startForeground(NOTIFICATION_ID, notification) + if (knownCalls.isEmpty()) { + startForeground(callId.hashCode(), notification) + } else { + notificationManager.notify(callId.hashCode(), notification) + } + knownCalls.add(callId) } /** @@ -226,14 +265,17 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe Timber.v("## VOIP displayCallInProgressNotification") val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: "" val call = callManager.getCallById(callId) ?: return + if (!knownCalls.contains(callId)) { + Timber.v("Call in progress for unknown call $callId$") + return + } val opponentMatrixItem = getOpponentMatrixItem(call) - alertManager.cancelAlert(INCOMING_CALL_ALERT_UID) + alertManager.cancelAlert(callId) val notification = notificationUtils.buildPendingCallNotification( mxCall = call.mxCall, title = opponentMatrixItem?.getBestName() ?: call.mxCall.opponentUserId ) - startForeground(NOTIFICATION_ID, notification) - // mCallIdInProgress = callId + notificationManager.notify(callId.hashCode(), notification) } /** @@ -243,27 +285,17 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe Timber.v("## VOIP displayCallInProgressNotification") val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: return val call = callManager.getCallById(callId) ?: return + if (!knownCalls.contains(callId)) { + Timber.v("Call in in background for unknown call $callId$") + return + } val opponentMatrixItem = getOpponentMatrixItem(call) val notification = notificationUtils.buildPendingCallNotification( mxCall = call.mxCall, title = opponentMatrixItem?.getBestName() ?: call.mxCall.opponentUserId, fromBg = true) - startForeground(NOTIFICATION_ID, notification) - // mCallIdInProgress = callId - } - - /** - * Hide the permanent call notifications - */ - private fun hideCallNotifications() { - val notification = notificationUtils.buildCallEndedNotification() - alertManager.cancelAlert(INCOMING_CALL_ALERT_UID) - mediaSession?.isActive = false - // It's mandatory to startForeground to avoid crash - startForeground(NOTIFICATION_ID, notification) - - myStopSelf() + notificationManager.notify(callId.hashCode(), notification) } fun addConnection(callConnection: CallConnection) { @@ -277,12 +309,12 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe companion object { private const val NOTIFICATION_ID = 6480 - private const val INCOMING_CALL_ALERT_UID = "INCOMING_CALL_ALERT_UID" private const val ACTION_INCOMING_RINGING_CALL = "im.vector.app.core.services.CallService.ACTION_INCOMING_RINGING_CALL" private const val ACTION_OUTGOING_RINGING_CALL = "im.vector.app.core.services.CallService.ACTION_OUTGOING_RINGING_CALL" private const val ACTION_CALL_CONNECTING = "im.vector.app.core.services.CallService.ACTION_CALL_CONNECTING" private const val ACTION_ONGOING_CALL = "im.vector.app.core.services.CallService.ACTION_ONGOING_CALL" private const val ACTION_ONGOING_CALL_BG = "im.vector.app.core.services.CallService.ACTION_ONGOING_CALL_BG" + private const val ACTION_CALL_TERMINATED = "im.vector.app.core.services.CallService.ACTION_CALL_TERMINATED" private const val ACTION_NO_ACTIVE_CALL = "im.vector.app.core.services.CallService.NO_ACTIVE_CALL" // private const val ACTION_ACTIVITY_VISIBLE = "im.vector.app.core.services.CallService.ACTION_ACTIVITY_VISIBLE" // private const val ACTION_STOP_RINGING = "im.vector.app.core.services.CallService.ACTION_STOP_RINGING" @@ -335,10 +367,11 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe ContextCompat.startForegroundService(context, intent) } - fun onNoActiveCall(context: Context) { + fun onCallTerminated(context: Context, callId: String) { val intent = Intent(context, CallService::class.java) .apply { - action = ACTION_NO_ACTIVE_CALL + action = ACTION_CALL_TERMINATED + putExtra(EXTRA_CALL_ID, callId) } ContextCompat.startForegroundService(context, intent) } diff --git a/vector/src/main/java/im/vector/app/core/ui/views/ActiveCallView.kt b/vector/src/main/java/im/vector/app/core/ui/views/ActiveCallView.kt index 19d1fbb6f6..fd3159c2e0 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/ActiveCallView.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/ActiveCallView.kt @@ -20,7 +20,10 @@ import android.content.Context import android.util.AttributeSet import android.widget.RelativeLayout import im.vector.app.R +import im.vector.app.features.call.webrtc.WebRtcCall import im.vector.app.features.themes.ThemeUtils +import kotlinx.android.synthetic.main.view_active_call_view.view.* +import org.matrix.android.sdk.api.session.call.CallState class ActiveCallView @JvmOverloads constructor( context: Context, @@ -43,4 +46,21 @@ class ActiveCallView @JvmOverloads constructor( setBackgroundColor(ThemeUtils.getColor(context, R.attr.colorPrimary)) setOnClickListener { callback?.onTapToReturnToCall() } } + + fun render(calls: List) { + if (calls.size == 1) { + activeCallInfo.setText(R.string.call_active_call) + } else if (calls.size == 2) { + val activeCall = calls.firstOrNull { + it.mxCall.state is CallState.Connected && !it.isLocalOnHold() + } + if (activeCall == null) { + activeCallInfo.setText(R.string.call_two_paused_calls) + } else { + activeCallInfo.setText(R.string.call_one_active_one_paused_call) + } + } else { + visibility = GONE + } + } } 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 193a4b2387..fdf3b99986 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 @@ -16,7 +16,6 @@ package im.vector.app.core.ui.views -import android.view.View import androidx.cardview.widget.CardView import androidx.core.view.isVisible import im.vector.app.core.utils.DebouncedClickListener @@ -35,13 +34,14 @@ class ActiveCallViewHolder { private var activeCallPipInitialized = false - fun updateCall(activeCall: WebRtcCall?) { + fun updateCall(activeCall: WebRtcCall?, calls: List) { this.activeCall = activeCall val hasActiveCall = activeCall?.mxCall?.state is CallState.Connected if (hasActiveCall) { val isVideoCall = activeCall?.mxCall?.isVideoCall == true if (isVideoCall) initIfNeeded() activeCallView?.isVisible = !isVideoCall + activeCallView?.render(calls) pipWrapper?.isVisible = isVideoCall activeCallPiP?.isVisible = isVideoCall activeCallPiP?.let { @@ -74,10 +74,9 @@ class ActiveCallViewHolder { this.activeCallPiP = activeCallPiP this.activeCallView = activeCallView this.pipWrapper = pipWrapper - this.activeCallView?.callback = interactionListener pipWrapper.setOnClickListener( - DebouncedClickListener(View.OnClickListener { _ -> + DebouncedClickListener({ _ -> interactionListener.onTapToReturnToCall() }) ) diff --git a/vector/src/main/java/im/vector/app/features/call/SharedActiveCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/SharedCurrentCallViewModel.kt similarity index 71% rename from vector/src/main/java/im/vector/app/features/call/SharedActiveCallViewModel.kt rename to vector/src/main/java/im/vector/app/features/call/SharedCurrentCallViewModel.kt index e35ed3e87a..d823fa6d5b 100644 --- a/vector/src/main/java/im/vector/app/features/call/SharedActiveCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/SharedCurrentCallViewModel.kt @@ -23,36 +23,42 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager import org.matrix.android.sdk.api.session.call.MxCall import javax.inject.Inject -class SharedActiveCallViewModel @Inject constructor( +class SharedCurrentCallViewModel @Inject constructor( private val callManager: WebRtcCallManager ) : ViewModel() { - val activeCall: MutableLiveData = MutableLiveData() + val currentCall: MutableLiveData = MutableLiveData() val callStateListener = object : WebRtcCall.Listener { override fun onStateUpdate(call: MxCall) { - if (activeCall.value?.callId == call.callId) { - activeCall.postValue(callManager.getCallById(call.callId)) - } + //post it-self + currentCall.postValue(currentCall.value) } + + override fun onHoldUnhold() { + super.onHoldUnhold() + //post it-self + currentCall.postValue(currentCall.value) + } + } private val listener = object : WebRtcCallManager.CurrentCallListener { override fun onCurrentCallChange(call: WebRtcCall?) { - activeCall.value?.mxCall?.removeListener(callStateListener) - activeCall.postValue(call) + currentCall.value?.mxCall?.removeListener(callStateListener) + currentCall.postValue(call) call?.addListener(callStateListener) } } init { - activeCall.postValue(callManager.currentCall) + currentCall.postValue(callManager.currentCall) callManager.addCurrentCallListener(listener) } override fun onCleared() { - activeCall.value?.removeListener(callStateListener) + currentCall.value?.removeListener(callStateListener) callManager.removeCurrentCallListener(listener) super.onCleared() } 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 c9a078e260..2e5b54ae9a 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 @@ -106,7 +106,6 @@ class VectorCallActivity : VectorBaseActivity(), CallContro callArgs = intent.getParcelableExtra(MvRx.KEY_ARG)!! } else { Timber.e("## VOIP missing callArgs for VectorCall Activity") - CallService.onNoActiveCall(this) finish() } @@ -153,8 +152,6 @@ class VectorCallActivity : VectorBaseActivity(), CallContro private fun renderState(state: VectorCallViewState) { Timber.v("## VOIP renderState call $state") if (state.callState is Fail) { - // be sure to clear notification - CallService.onNoActiveCall(this) finish() return } @@ -295,7 +292,6 @@ class VectorCallActivity : VectorBaseActivity(), CallContro Timber.v("## VOIP handleViewEvents $event") when (event) { VectorCallViewEvents.DismissNoCall -> { - CallService.onNoActiveCall(this) finish() } is VectorCallViewEvents.ConnectionTimeout -> { 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 e1a01bbaa3..ecea4f1cb1 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 @@ -117,14 +117,6 @@ class VectorCallViewModel @AssistedInject constructor( private val currentCallListener = object : WebRtcCallManager.CurrentCallListener { - override fun onCurrentCallChange(call: WebRtcCall?) { - // we need to check the state - if (call == null) { - // we should dismiss, e.g handled by other session? - _viewEvents.post(VectorCallViewEvents.DismissNoCall) - } - } - override fun onAudioDevicesChange() { val currentSoundDevice = callManager.callAudioManager.getCurrentSoundDevice() if (currentSoundDevice == CallAudioManager.SoundDevice.PHONE) { @@ -163,6 +155,8 @@ class VectorCallViewModel @AssistedInject constructor( callState = Success(webRtcCall.mxCall.state), otherUserMatrixItem = item?.let { Success(it) } ?: Uninitialized, soundDevice = currentSoundDevice, + isLocalOnHold = webRtcCall.isLocalOnHold(), + isRemoteOnHold = webRtcCall.remoteOnHold, availableSoundDevices = callManager.callAudioManager.getAvailableSoundDevices(), isFrontCamera = call?.currentCameraType() == CameraType.FRONT, canSwitchCamera = call?.canSwitchCamera() ?: false, 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 10c7cb2e24..157b97252d 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 @@ -88,6 +88,7 @@ class WebRtcCall(val mxCall: MxCall, private val dispatcher: CoroutineContext, private val sessionProvider: Provider, private val peerConnectionFactoryProvider: Provider, + private val onCallBecomeActive: (WebRtcCall) -> Unit, private val onCallEnded: (WebRtcCall) -> Unit) : MxCall.StateListener { interface Listener : MxCall.StateListener { @@ -130,8 +131,11 @@ class WebRtcCall(val mxCall: MxCall, // Mute status var micMuted = false + private set var videoMuted = false + private set var remoteOnHold = false + private set var offerSdp: CallInviteContent.Offer? = null @@ -328,6 +332,9 @@ class WebRtcCall(val mxCall: MxCall, } private suspend fun internalAcceptIncomingCall() = withContext(dispatcher) { + tryOrNull { + onCallBecomeActive(this@WebRtcCall) + } val turnServerResponse = getTurnServer() // Update service state withContext(Dispatchers.Main) { @@ -542,6 +549,9 @@ class WebRtcCall(val mxCall: MxCall, fun updateRemoteOnHold(onHold: Boolean) { if (remoteOnHold == onHold) return remoteOnHold = onHold + if (!onHold) { + onCallBecomeActive(this) + } val direction = if (onHold) { RtpTransceiver.RtpTransceiverDirection.INACTIVE } else { 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 f6fcfd446e..a484acbc5f 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 @@ -45,6 +45,7 @@ import org.webrtc.DefaultVideoDecoderFactory import org.webrtc.DefaultVideoEncoderFactory import org.webrtc.PeerConnectionFactory import timber.log.Timber +import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Executors import javax.inject.Inject import javax.inject.Singleton @@ -63,7 +64,7 @@ class WebRtcCallManager @Inject constructor( get() = activeSessionDataSource.currentValue?.orNull() interface CurrentCallListener { - fun onCurrentCallChange(call: WebRtcCall?) + fun onCurrentCallChange(call: WebRtcCall?) {} fun onAudioDevicesChange() {} } @@ -101,15 +102,15 @@ class WebRtcCallManager @Inject constructor( } var currentCall: WebRtcCall? = null - set(value) { + private set(value) { field = value currentCallsListeners.forEach { tryOrNull { it.onCurrentCallChange(value) } } } - private val callsByCallId = HashMap() - private val callsByRoomId = HashMap>() + private val callsByCallId = ConcurrentHashMap() + private val callsByRoomId = ConcurrentHashMap>() fun getCallById(callId: String): WebRtcCall? { return callsByCallId[callId] @@ -119,6 +120,10 @@ class WebRtcCallManager @Inject constructor( return callsByRoomId[roomId] ?: emptyList() } + fun getCalls(): List { + return callsByCallId.values.toList() + } + fun headSetButtonTapped() { Timber.v("## VOIP headSetButtonTapped") val call = currentCall ?: return @@ -161,13 +166,23 @@ class WebRtcCallManager @Inject constructor( .createPeerConnectionFactory() } + private fun onCallActive(call: WebRtcCall) { + Timber.v("## VOIP WebRtcPeerConnectionManager onCall active: ${call.mxCall.callId}") + if (currentCall != call) { + currentCall?.updateRemoteOnHold(onHold = true) + currentCall = call + } + } + private fun onCallEnded(call: WebRtcCall) { Timber.v("## VOIP WebRtcPeerConnectionManager onCall ended: ${call.mxCall.callId}") - CallService.onNoActiveCall(context) + CallService.onCallTerminated(context, call.callId) callAudioManager.stop() - currentCall = null callsByCallId.remove(call.mxCall.callId) callsByRoomId[call.mxCall.roomId]?.remove(call) + if (currentCall == call) { + currentCall = getCalls().lastOrNull() + } // This must be done in this thread executor.execute { if (currentCall == null) { @@ -181,12 +196,17 @@ class WebRtcCallManager @Inject constructor( fun startOutgoingCall(signalingRoomId: String, otherUserId: String, isVideoCall: Boolean) { Timber.v("## VOIP startOutgoingCall in room $signalingRoomId to $otherUserId isVideo $isVideoCall") + if (currentCall != null && currentCall?.mxCall?.state !is CallState.Connected || getCalls().size >= 2) { + Timber.w("## VOIP cannot start outgoing call") + // Just ignore, maybe we could answer from other session? + return + } executor.execute { createPeerConnectionFactoryIfNeeded() } - + currentCall?.updateRemoteOnHold(onHold = true) val mxCall = currentSession?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return - createWebRtcCall(mxCall) + currentCall = createWebRtcCall(mxCall) callAudioManager.startForCall(mxCall) CallService.onOutgoingCallRinging( @@ -199,10 +219,11 @@ class WebRtcCallManager @Inject constructor( override fun onCallIceCandidateReceived(mxCall: MxCall, iceCandidatesContent: CallCandidatesContent) { Timber.v("## VOIP onCallIceCandidateReceived for call ${mxCall.callId}") - if (currentCall?.mxCall?.callId != mxCall.callId) return Unit.also { - Timber.w("## VOIP ignore ice candidates from other call") - } - currentCall?.onCallIceCandidateReceived(iceCandidatesContent) + val call = callsByCallId[iceCandidatesContent.callId] + ?: return Unit.also { + Timber.w("onCallIceCandidateReceived for non active call? ${iceCandidatesContent.callId}") + } + call.onCallIceCandidateReceived(iceCandidatesContent) } private fun createWebRtcCall(mxCall: MxCall): WebRtcCall { @@ -217,21 +238,17 @@ class WebRtcCallManager @Inject constructor( peerConnectionFactory }, sessionProvider = { currentSession }, + onCallBecomeActive = this::onCallActive, onCallEnded = this::onCallEnded ) - currentCall = webRtcCall callsByCallId[mxCall.callId] = webRtcCall - callsByRoomId.getOrPut(mxCall.roomId) { ArrayList() } + callsByRoomId.getOrPut(mxCall.roomId) { ArrayList(1) } .add(webRtcCall) return webRtcCall } - fun acceptIncomingCall() { - currentCall?.acceptIncomingCall() - } - - fun endCall(originatedByMe: Boolean = true) { - currentCall?.endCall(originatedByMe) + fun endCallForRoom(roomId: String, originatedByMe: Boolean = true) { + callsByRoomId[roomId]?.forEach { it.endCall(originatedByMe) } } fun onWiredDeviceEvent(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) { @@ -248,8 +265,8 @@ class WebRtcCallManager @Inject constructor( override fun onCallInviteReceived(mxCall: MxCall, callInviteContent: CallInviteContent) { Timber.v("## VOIP onCallInviteReceived callId ${mxCall.callId}") - if (currentCall != null) { - Timber.w("## VOIP receiving incoming call while already in call?") + if (currentCall != null && currentCall?.mxCall?.state !is CallState.Connected || getCalls().size >= 2) { + Timber.w("## VOIP receiving incoming call but cannot handle it") // Just ignore, maybe we could answer from other session? return } @@ -329,12 +346,12 @@ class WebRtcCallManager @Inject constructor( override fun onCallManagedByOtherSession(callId: String) { Timber.v("## VOIP onCallManagedByOtherSession: $callId") - currentCall = null val webRtcCall = callsByCallId.remove(callId) if (webRtcCall != null) { callsByRoomId[webRtcCall.mxCall.roomId]?.remove(webRtcCall) } - CallService.onNoActiveCall(context) + // TODO: handle this properly + CallService.onCallTerminated(context, callId) // did we start background sync? so we should stop it if (isInBackground) { 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 d344f1bd86..8281f055e7 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 @@ -21,7 +21,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat -import androidx.lifecycle.Observer import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState @@ -37,7 +36,7 @@ import im.vector.app.core.ui.views.ActiveCallView import im.vector.app.core.ui.views.ActiveCallViewHolder import im.vector.app.core.ui.views.KeysBackupBanner import im.vector.app.databinding.FragmentHomeDetailBinding -import im.vector.app.features.call.SharedActiveCallViewModel +import im.vector.app.features.call.SharedCurrentCallViewModel import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.home.room.list.RoomListFragment @@ -78,7 +77,7 @@ class HomeDetailFragment @Inject constructor( private val serverBackupStatusViewModel: ServerBackupStatusViewModel by activityViewModel() private lateinit var sharedActionViewModel: HomeSharedActionViewModel - private lateinit var sharedCallActionViewModel: SharedActiveCallViewModel + private lateinit var sharedCallActionViewModel: SharedCurrentCallViewModel override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentHomeDetailBinding { return FragmentHomeDetailBinding.inflate(inflater, container, false) @@ -89,7 +88,7 @@ class HomeDetailFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) sharedActionViewModel = activityViewModelProvider.get(HomeSharedActionViewModel::class.java) - sharedCallActionViewModel = activityViewModelProvider.get(SharedActiveCallViewModel::class.java) + sharedCallActionViewModel = activityViewModelProvider.get(SharedCurrentCallViewModel::class.java) setupBottomNavigationView() setupToolbar() @@ -127,9 +126,9 @@ class HomeDetailFragment @Inject constructor( } sharedCallActionViewModel - .activeCall - .observe(viewLifecycleOwner, Observer { - activeCallViewHolder.updateCall(it) + .currentCall + .observe(viewLifecycleOwner, { + activeCallViewHolder.updateCall(it, callManager.getCalls()) invalidateOptionsMenu() }) } @@ -336,7 +335,7 @@ class HomeDetailFragment @Inject constructor( } override fun onTapToReturnToCall() { - sharedCallActionViewModel.activeCall.value?.let { call -> + sharedCallActionViewModel.currentCall.value?.let { call -> VectorCallActivity.newIntent( context = requireContext(), callId = call.callId, 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 988cf2fd30..62e00f03be 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 @@ -120,7 +120,7 @@ import im.vector.app.features.attachments.ContactAttachment import im.vector.app.features.attachments.preview.AttachmentsPreviewActivity import im.vector.app.features.attachments.preview.AttachmentsPreviewArgs import im.vector.app.features.attachments.toGroupedContentAttachmentData -import im.vector.app.features.call.SharedActiveCallViewModel +import im.vector.app.features.call.SharedCurrentCallViewModel import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.conference.JitsiCallViewModel import im.vector.app.features.call.webrtc.WebRtcCallManager @@ -227,7 +227,8 @@ class RoomDetailFragment @Inject constructor( private val matrixItemColorProvider: MatrixItemColorProvider, private val imageContentRenderer: ImageContentRenderer, private val roomDetailPendingActionStore: RoomDetailPendingActionStore, - private val pillsPostProcessorFactory: PillsPostProcessor.Factory + private val pillsPostProcessorFactory: PillsPostProcessor.Factory, + private val callManager: WebRtcCallManager ) : VectorBaseFragment(), TimelineEventController.Callback, @@ -282,7 +283,7 @@ class RoomDetailFragment @Inject constructor( override fun getMenuRes() = R.menu.menu_timeline private lateinit var sharedActionViewModel: MessageSharedActionViewModel - private lateinit var sharedCallActionViewModel: SharedActiveCallViewModel + private lateinit var sharedCurrentCallViewModel: SharedCurrentCallViewModel private lateinit var layoutManager: LinearLayoutManager private lateinit var jumpToBottomViewVisibilityManager: JumpToBottomViewVisibilityManager @@ -299,7 +300,7 @@ class RoomDetailFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java) - sharedCallActionViewModel = activityViewModelProvider.get(SharedActiveCallViewModel::class.java) + sharedCurrentCallViewModel = activityViewModelProvider.get(SharedCurrentCallViewModel::class.java) attachmentsHelper = AttachmentsHelper(requireContext(), this).register() keyboardStateUtils = KeyboardStateUtils(requireActivity()) setupToolbar(views.roomToolbar) @@ -324,10 +325,10 @@ class RoomDetailFragment @Inject constructor( } .disposeOnDestroyView() - sharedCallActionViewModel - .activeCall + sharedCurrentCallViewModel + .currentCall .observe(viewLifecycleOwner, { - activeCallViewHolder.updateCall(it) + activeCallViewHolder.updateCall(it, callManager.getCalls()) invalidateOptionsMenu() }) @@ -799,8 +800,8 @@ class RoomDetailFragment @Inject constructor( showDialogWithMessage(getString(R.string.cannot_call_yourself)) } } - 2 -> { - val activeCall = sharedCallActionViewModel.activeCall.value + 2 -> { + val activeCall = sharedCurrentCallViewModel.currentCall.value if (activeCall != null) { // resume existing if same room, if not prompt to kill and then restart new call? if (activeCall.roomId == roomDetailArgs.roomId) { @@ -2015,7 +2016,7 @@ class RoomDetailFragment @Inject constructor( } override fun onTapToReturnToCall() { - sharedCallActionViewModel.activeCall.value?.let { call -> + sharedCurrentCallViewModel.currentCall.value?.let { call -> VectorCallActivity.newIntent( context = requireContext(), callId = call.callId, 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 a8cd200814..ab4236489b 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 @@ -349,7 +349,7 @@ class RoomDetailViewModel @AssistedInject constructor( } private fun handleEndCall() { - callManager.endCall() + callManager.endCallForRoom(initialState.roomId) } private fun handleSelectStickerAttachment() { 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 74a93fceda..2ec917c739 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 @@ -450,6 +450,7 @@ class NotificationUtils @Inject constructor(private val context: Context, fun buildCallEndedNotification(): Notification { return NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID) .setContentTitle(stringProvider.getString(R.string.call_ended)) + .setTimeoutAfter(2000) .setSmallIcon(R.drawable.ic_material_call_end_grey) .setCategory(NotificationCompat.CATEGORY_CALL) .build() diff --git a/vector/src/main/res/layout/view_active_call_view.xml b/vector/src/main/res/layout/view_active_call_view.xml index e8b21de7e8..6fb585865e 100644 --- a/vector/src/main/res/layout/view_active_call_view.xml +++ b/vector/src/main/res/layout/view_active_call_view.xml @@ -20,10 +20,11 @@ android:paddingTop="12dp" android:paddingEnd="16dp" android:paddingBottom="12dp" - android:text="@string/active_call" + android:textSize="14sp" + android:text="@string/call_one_active_one_paused_call" android:textColor="@color/white" app:drawableTint="@color/white" - app:drawableStartCompat="@drawable/ic_call" /> + app:drawableStartCompat="@drawable/ic_call_answer" /> diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 61f79444c7..508c8a2c31 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -141,6 +141,7 @@ Unpublish Copied to clipboard Disable + Return Confirmation @@ -2773,4 +2774,9 @@ %1$s declined this call This call has ended Call back + + Active call (%1$s) + 1 active call (%1$s) ยท 1 paused call + 2 paused calls +