From a5736efc7504ff83bca9ee4e742f6af074610a2b Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 22 Dec 2020 12:05:36 +0100 Subject: [PATCH] VoIP: add info on other call when switching --- .../im/vector/app/core/di/ViewModelModule.kt | 6 +-- .../app/core/ui/views/ActiveCallViewHolder.kt | 4 +- ...{ActiveCallView.kt => CurrentCallsView.kt} | 33 +++++++----- ...wModel.kt => SharedKnownCallsViewModel.kt} | 36 ++++++++----- .../app/features/call/VectorCallActivity.kt | 26 ++++++++-- .../app/features/call/VectorCallViewModel.kt | 23 ++++++++- .../app/features/call/VectorCallViewState.kt | 10 +++- .../app/features/call/webrtc/WebRtcCall.kt | 28 +++++----- .../features/call/webrtc/WebRtcCallManager.kt | 51 +++++++++++-------- .../app/features/home/HomeDetailFragment.kt | 16 +++--- .../home/room/detail/RoomDetailFragment.kt | 31 +++++------ .../app/features/navigation/Navigator.kt | 1 + .../app/features/popup/IncomingCallAlert.kt | 3 +- .../features/popup/VerificationVectorAlert.kt | 3 +- vector/src/main/res/layout/activity_call.xml | 43 +++++++++++++--- .../main/res/layout/fragment_home_detail.xml | 2 +- .../main/res/layout/fragment_room_detail.xml | 2 +- ...e_call_view.xml => view_current_calls.xml} | 9 ++-- vector/src/main/res/values/strings.xml | 10 ++-- 19 files changed, 220 insertions(+), 117 deletions(-) rename vector/src/main/java/im/vector/app/core/ui/views/{ActiveCallView.kt => CurrentCallsView.kt} (59%) rename vector/src/main/java/im/vector/app/features/call/{SharedCurrentCallViewModel.kt => SharedKnownCallsViewModel.kt} (56%) rename vector/src/main/res/layout/{view_active_call_view.xml => view_current_calls.xml} (85%) 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 9d5c0d5491..8409021845 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.SharedCurrentCallViewModel +import im.vector.app.features.call.SharedKnownCallsViewModel 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(SharedCurrentCallViewModel::class) - fun bindSharedActiveCallViewModel(viewModel: SharedCurrentCallViewModel): ViewModel + @ViewModelKey(SharedKnownCallsViewModel::class) + fun bindSharedActiveCallViewModel(viewModel: SharedKnownCallsViewModel): ViewModel @Binds @IntoMap 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 fdf3b99986..adee15afe3 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 @@ -28,7 +28,7 @@ import org.webrtc.SurfaceViewRenderer class ActiveCallViewHolder { private var activeCallPiP: SurfaceViewRenderer? = null - private var activeCallView: ActiveCallView? = null + private var activeCallView: CurrentCallsView? = null private var pipWrapper: CardView? = null private var activeCall: WebRtcCall? = null @@ -70,7 +70,7 @@ class ActiveCallViewHolder { } } - fun bind(activeCallPiP: SurfaceViewRenderer, activeCallView: ActiveCallView, pipWrapper: CardView, interactionListener: ActiveCallView.Callback) { + fun bind(activeCallPiP: SurfaceViewRenderer, activeCallView: CurrentCallsView, pipWrapper: CardView, interactionListener: CurrentCallsView.Callback) { this.activeCallPiP = activeCallPiP this.activeCallView = activeCallView this.pipWrapper = pipWrapper 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/CurrentCallsView.kt similarity index 59% rename from vector/src/main/java/im/vector/app/core/ui/views/ActiveCallView.kt rename to vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsView.kt index fd3159c2e0..8fd7bb4c90 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/CurrentCallsView.kt @@ -22,10 +22,10 @@ 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 kotlinx.android.synthetic.main.view_current_calls.view.* import org.matrix.android.sdk.api.session.call.CallState -class ActiveCallView @JvmOverloads constructor( +class CurrentCallsView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 @@ -42,25 +42,32 @@ class ActiveCallView @JvmOverloads constructor( } private fun setupView() { - inflate(context, R.layout.view_active_call_view, this) + inflate(context, R.layout.view_current_calls, this) 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) + val connectedCalls = calls.filter { + it.mxCall.state is CallState.Connected + } + val heldCalls = connectedCalls.filter { + it.isLocalOnHold() || it.remoteOnHold + } + if (connectedCalls.size == 1) { + if (heldCalls.size == 1) { + currentCallsInfo.setText(R.string.call_only_paused) } else { - activeCallInfo.setText(R.string.call_one_active_one_paused_call) + currentCallsInfo.setText(R.string.call_only_active) } } else { - visibility = GONE + if (heldCalls.size > 1) { + currentCallsInfo.text = resources.getString(R.string.call_only_multiple_paused , heldCalls.size) + } else if (heldCalls.size == 1) { + currentCallsInfo.setText(R.string.call_active_and_single_paused) + } else { + currentCallsInfo.text = resources.getString(R.string.call_active_and_multiple_paused, "00:00", heldCalls.size) + } } } } diff --git a/vector/src/main/java/im/vector/app/features/call/SharedCurrentCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/SharedKnownCallsViewModel.kt similarity index 56% rename from vector/src/main/java/im/vector/app/features/call/SharedCurrentCallViewModel.kt rename to vector/src/main/java/im/vector/app/features/call/SharedKnownCallsViewModel.kt index d823fa6d5b..685b23f332 100644 --- a/vector/src/main/java/im/vector/app/features/call/SharedCurrentCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/SharedKnownCallsViewModel.kt @@ -23,43 +23,51 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager import org.matrix.android.sdk.api.session.call.MxCall import javax.inject.Inject -class SharedCurrentCallViewModel @Inject constructor( +class SharedKnownCallsViewModel @Inject constructor( private val callManager: WebRtcCallManager ) : ViewModel() { - val currentCall: MutableLiveData = MutableLiveData() + val liveKnownCalls: MutableLiveData> = MutableLiveData() - val callStateListener = object : WebRtcCall.Listener { + val callListener = object : WebRtcCall.Listener { override fun onStateUpdate(call: MxCall) { //post it-self - currentCall.postValue(currentCall.value) + liveKnownCalls.postValue(liveKnownCalls.value) } override fun onHoldUnhold() { super.onHoldUnhold() //post it-self - currentCall.postValue(currentCall.value) + liveKnownCalls.postValue(liveKnownCalls.value) } - } - private val listener = object : WebRtcCallManager.CurrentCallListener { + private val currentCallListener = object : WebRtcCallManager.CurrentCallListener { override fun onCurrentCallChange(call: WebRtcCall?) { - currentCall.value?.mxCall?.removeListener(callStateListener) - currentCall.postValue(call) - call?.addListener(callStateListener) + val knownCalls = callManager.getCalls() + liveKnownCalls.postValue(knownCalls) + knownCalls.forEach { + it.removeListener(callListener) + it.addListener(callListener) + } } } init { - currentCall.postValue(callManager.currentCall) - callManager.addCurrentCallListener(listener) + val knownCalls = callManager.getCalls() + liveKnownCalls.postValue(knownCalls) + callManager.addCurrentCallListener(currentCallListener) + knownCalls.forEach { + it.addListener(callListener) + } } override fun onCleared() { - currentCall.value?.removeListener(callStateListener) - callManager.removeCurrentCallListener(listener) + callManager.getCalls().forEach { + it.removeListener(callListener) + } + callManager.removeCurrentCallListener(currentCallListener) 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 2e5b54ae9a..5f7bf1802a 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 @@ -34,10 +34,10 @@ import androidx.core.view.isVisible import com.airbnb.mvrx.Fail import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.viewModel +import com.airbnb.mvrx.withState import im.vector.app.R import im.vector.app.core.di.ScreenComponent 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 import im.vector.app.core.utils.PERMISSIONS_FOR_VIDEO_IP_CALL import im.vector.app.core.utils.allGranted @@ -50,6 +50,7 @@ import im.vector.app.features.home.room.detail.RoomDetailActivity import im.vector.app.features.home.room.detail.RoomDetailArgs import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.parcelize.Parcelize +import org.matrix.android.sdk.api.extensions.orFalse 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 @@ -199,7 +200,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro views.callStatusText.setText(R.string.call_held_by_you) } else { views.callActionText.isInvisible = true - state.otherUserMatrixItem.invoke()?.let { + state.callInfo.otherUserItem?.let { views.callStatusText.text = getString(R.string.call_held_by_user, it.getBestName()) } } @@ -208,7 +209,8 @@ class VectorCallActivity : VectorBaseActivity(), CallContro if (callArgs.isVideoCall) { views.callVideoGroup.isVisible = true views.callInfoGroup.isVisible = false - //views.pip_video_view.isVisible = !state.isVideoCaptureInError + views.pipRenderer.isVisible = !state.isVideoCaptureInError && state.otherKnownCallInfo == null + configureCallInfo(state) } else { views.callVideoGroup.isInvisible = true views.callInfoGroup.isVisible = true @@ -235,7 +237,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro } private fun configureCallInfo(state: VectorCallViewState, blurAvatar: Boolean = false) { - state.otherUserMatrixItem.invoke()?.let { + state.callInfo.otherUserItem?.let { val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen) avatarRenderer.renderBlur(it, views.bgCallView, sampling = 20, rounded = false, colorFilter = colorFilter) views.participantNameText.text = it.getBestName() @@ -245,10 +247,26 @@ class VectorCallActivity : VectorBaseActivity(), CallContro avatarRenderer.render(it, views.otherMemberAvatar) } } + if (state.otherKnownCallInfo?.otherUserItem == null) { + views.otherKnownCallLayout.isVisible = false + } else { + val otherCall = callManager.getCallById(state.otherKnownCallInfo.callId) + val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen) + avatarRenderer.renderBlur(state.otherKnownCallInfo.otherUserItem, views.otherKnownCallAvatarView, sampling = 20, rounded = false, colorFilter = colorFilter) + views.otherKnownCallLayout.isVisible = true + views.otherSmallIsHeldIcon.isVisible = otherCall?.let { it.isLocalOnHold() || it.remoteOnHold }.orFalse() + } } private fun configureCallViews() { views.callControlsView.interactionListener = this + views.otherKnownCallAvatarView.setOnClickListener { + withState(callViewModel) { + val otherCall = callManager.getCallById(it.otherKnownCallInfo?.callId ?: "") ?: return@withState + startActivity(newIntent(this, otherCall.mxCall, null)) + finish() + } + } } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { 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 ecea4f1cb1..7e3977bc99 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 @@ -20,7 +20,6 @@ import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success -import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject @@ -117,6 +116,10 @@ class VectorCallViewModel @AssistedInject constructor( private val currentCallListener = object : WebRtcCallManager.CurrentCallListener { + override fun onCurrentCallChange(call: WebRtcCall?) { + updateOtherKnownCall(call) + } + override fun onAudioDevicesChange() { val currentSoundDevice = callManager.callAudioManager.getCurrentSoundDevice() if (currentSoundDevice == CallAudioManager.SoundDevice.PHONE) { @@ -134,6 +137,21 @@ class VectorCallViewModel @AssistedInject constructor( } } + private fun updateOtherKnownCall(currentCall: WebRtcCall?) { + if (currentCall == null) return + val otherCall = callManager.getCalls().firstOrNull { + it.callId != currentCall.callId && it.mxCall.state is CallState.Connected + } + setState { + if (otherCall == null) { + copy(otherKnownCallInfo = null) + } else { + val otherUserItem: MatrixItem? = session.getUser(otherCall.mxCall.opponentUserId)?.toMatrixItem() + copy(otherKnownCallInfo = VectorCallViewState.CallInfo(otherCall.callId, otherUserItem)) + } + } + } + init { val webRtcCall = callManager.getCallById(initialState.callId) if (webRtcCall == null) { @@ -153,7 +171,7 @@ class VectorCallViewModel @AssistedInject constructor( copy( isVideoCall = webRtcCall.mxCall.isVideoCall, callState = Success(webRtcCall.mxCall.state), - otherUserMatrixItem = item?.let { Success(it) } ?: Uninitialized, + callInfo = VectorCallViewState.CallInfo(callId, item), soundDevice = currentSoundDevice, isLocalOnHold = webRtcCall.isLocalOnHold(), isRemoteOnHold = webRtcCall.remoteOnHold, @@ -163,6 +181,7 @@ class VectorCallViewModel @AssistedInject constructor( isHD = webRtcCall.mxCall.isVideoCall && webRtcCall.currentCaptureFormat() is CaptureFormat.HD ) } + updateOtherKnownCall(webRtcCall) } } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt index 8742e2cc7c..aba109091a 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt @@ -36,10 +36,16 @@ data class VectorCallViewState( val canSwitchCamera: Boolean = true, val soundDevice: CallAudioManager.SoundDevice = CallAudioManager.SoundDevice.PHONE, val availableSoundDevices: List = emptyList(), - val otherUserMatrixItem: Async = Uninitialized, - val callState: Async = Uninitialized + val callState: Async = Uninitialized, + val otherKnownCallInfo: CallInfo? = null, + val callInfo: CallInfo = CallInfo(callId) ) : MvRxState { + data class CallInfo( + val callId: String, + val otherUserItem: MatrixItem? = null + ) + constructor(callArgs: CallArgs): this( callId = callArgs.callId, roomId = callArgs.roomId, 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 157b97252d..1b7775a5d7 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 @@ -72,6 +72,7 @@ import org.webrtc.VideoSource import org.webrtc.VideoTrack import timber.log.Timber import java.lang.ref.WeakReference +import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.TimeUnit import javax.inject.Provider import kotlin.coroutines.CoroutineContext @@ -97,7 +98,7 @@ class WebRtcCall(val mxCall: MxCall, fun onHoldUnhold() {} } - private val listeners = ArrayList() + private val listeners = CopyOnWriteArrayList() fun addListener(listener: Listener) { listeners.add(listener) @@ -277,7 +278,7 @@ class WebRtcCall(val mxCall: MxCall, } } - fun detachRenderers(renderers: List?) { + fun detachRenderers(renderers: List?) = synchronized(this) { Timber.v("## VOIP detachRenderers") // currentCall?.localMediaStream?.let { currentCall?.peerConnection?.removeStream(it) } if (renderers.isNullOrEmpty()) { @@ -533,7 +534,7 @@ class WebRtcCall(val mxCall: MxCall, * rather than 'sendonly') * @returns true if the other party has put us on hold */ - fun isLocalOnHold(): Boolean { + fun isLocalOnHold(): Boolean = synchronized(this) { if (mxCall.state !is CallState.Connected) return false var callOnHold = true // We consider a call to be on hold only if *all* the tracks are on hold @@ -546,7 +547,7 @@ class WebRtcCall(val mxCall: MxCall, return callOnHold } - fun updateRemoteOnHold(onHold: Boolean) { + fun updateRemoteOnHold(onHold: Boolean) = synchronized(this){ if (remoteOnHold == onHold) return remoteOnHold = onHold if (!onHold) { @@ -563,21 +564,21 @@ class WebRtcCall(val mxCall: MxCall, updateMuteStatus() } - fun muteCall(muted: Boolean) { + fun muteCall(muted: Boolean) = synchronized(this) { micMuted = muted updateMuteStatus() } - fun enableVideo(enabled: Boolean) { + fun enableVideo(enabled: Boolean) = synchronized(this) { videoMuted = !enabled updateMuteStatus() } - fun canSwitchCamera(): Boolean { + fun canSwitchCamera(): Boolean = synchronized(this){ return availableCamera.size > 1 } - private fun getOppositeCameraIfAny(): CameraProxy? { + private fun getOppositeCameraIfAny(): CameraProxy? = synchronized(this){ val currentCamera = cameraInUse ?: return null return if (currentCamera.type == CameraType.FRONT) { availableCamera.firstOrNull { it.type == CameraType.BACK } @@ -586,7 +587,7 @@ class WebRtcCall(val mxCall: MxCall, } } - fun switchCamera() { + fun switchCamera() = synchronized(this){ Timber.v("## VOIP switchCamera") if (mxCall.state is CallState.Connected && mxCall.isVideoCall) { val oppositeCamera = getOppositeCameraIfAny() ?: return @@ -629,15 +630,15 @@ class WebRtcCall(val mxCall: MxCall, } } - fun currentCameraType(): CameraType? { + fun currentCameraType(): CameraType? = synchronized(this){ return cameraInUse?.type } - fun currentCaptureFormat(): CaptureFormat { + fun currentCaptureFormat(): CaptureFormat = synchronized(this) { return currentCaptureFormat } - private fun release() { + private fun release() { mxCall.removeListener(this) videoCapturer?.stopCapture() videoCapturer?.dispose() @@ -689,7 +690,7 @@ class WebRtcCall(val mxCall: MxCall, } } - fun endCall(originatedByMe: Boolean = true, reason: CallHangupContent.Reason? = null) { + fun endCall(originatedByMe: Boolean = true, reason: CallHangupContent.Reason? = null) = synchronized(this) { if (mxCall.state == CallState.Terminated) { return } @@ -702,6 +703,7 @@ class WebRtcCall(val mxCall: MxCall, cameraManager.unregisterAvailabilityCallback(cameraAvailabilityCallback) } release() + listeners.clear() onCallEnded(this) if (originatedByMe) { // send hang up event 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 a484acbc5f..883b966701 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 @@ -46,7 +46,9 @@ import org.webrtc.DefaultVideoEncoderFactory import org.webrtc.PeerConnectionFactory import timber.log.Timber import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicReference import javax.inject.Inject import javax.inject.Singleton @@ -68,7 +70,7 @@ class WebRtcCallManager @Inject constructor( fun onAudioDevicesChange() {} } - private val currentCallsListeners = emptyList().toMutableList() + private val currentCallsListeners = CopyOnWriteArrayList() fun addCurrentCallListener(listener: CurrentCallListener) { currentCallsListeners.add(listener) } @@ -101,13 +103,17 @@ class WebRtcCallManager @Inject constructor( isInBackground = true } - var currentCall: WebRtcCall? = null - private set(value) { - field = value - currentCallsListeners.forEach { - tryOrNull { it.onCurrentCallChange(value) } - } + /** + * The current call is the call we interacted with whatever his state (connected,resumed, held...) + * As soon as we interact with an other call, it replaces this one and put it on held if not already. + */ + var currentCall: AtomicReference = AtomicReference(null) + private fun AtomicReference.setAndNotify(newValue: WebRtcCall?) { + set(newValue) + currentCallsListeners.forEach { + tryOrNull { it.onCurrentCallChange(newValue) } } + } private val callsByCallId = ConcurrentHashMap() private val callsByRoomId = ConcurrentHashMap>() @@ -120,13 +126,17 @@ class WebRtcCallManager @Inject constructor( return callsByRoomId[roomId] ?: emptyList() } + fun getCurrentCall(): WebRtcCall? { + return currentCall.get() + } + fun getCalls(): List { return callsByCallId.values.toList() } fun headSetButtonTapped() { Timber.v("## VOIP headSetButtonTapped") - val call = currentCall ?: return + val call = currentCall.get() ?: return if (call.mxCall.state is CallState.LocalRinging) { // accept call call.acceptIncomingCall() @@ -168,10 +178,9 @@ class WebRtcCallManager @Inject constructor( private fun onCallActive(call: WebRtcCall) { Timber.v("## VOIP WebRtcPeerConnectionManager onCall active: ${call.mxCall.callId}") - if (currentCall != call) { - currentCall?.updateRemoteOnHold(onHold = true) - currentCall = call - } + val currentCall = currentCall.get().takeIf { it != call } + currentCall?.updateRemoteOnHold(onHold = true) + this.currentCall.setAndNotify(call) } private fun onCallEnded(call: WebRtcCall) { @@ -180,12 +189,13 @@ class WebRtcCallManager @Inject constructor( callAudioManager.stop() callsByCallId.remove(call.mxCall.callId) callsByRoomId[call.mxCall.roomId]?.remove(call) - if (currentCall == call) { - currentCall = getCalls().lastOrNull() + if (currentCall.get() == call) { + val otherCall = getCalls().lastOrNull() + currentCall.setAndNotify(otherCall) } // This must be done in this thread executor.execute { - if (currentCall == null) { + if (currentCall.get() == null) { Timber.v("## VOIP Dispose peerConnectionFactory as there is no need to keep one") peerConnectionFactory?.dispose() peerConnectionFactory = null @@ -196,7 +206,7 @@ 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) { + if (currentCall.get() != null && currentCall.get()?.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 @@ -204,9 +214,10 @@ class WebRtcCallManager @Inject constructor( executor.execute { createPeerConnectionFactoryIfNeeded() } - currentCall?.updateRemoteOnHold(onHold = true) + currentCall.get()?.updateRemoteOnHold(onHold = true) val mxCall = currentSession?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return - currentCall = createWebRtcCall(mxCall) + val webRtcCall = createWebRtcCall(mxCall) + currentCall.setAndNotify(webRtcCall) callAudioManager.startForCall(mxCall) CallService.onOutgoingCallRinging( @@ -253,7 +264,7 @@ class WebRtcCallManager @Inject constructor( fun onWiredDeviceEvent(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) { Timber.v("## VOIP onWiredDeviceEvent $event") - currentCall ?: return + currentCall.get() ?: return // sometimes we received un-wanted unplugged... callAudioManager.wiredStateChange(event) } @@ -265,7 +276,7 @@ class WebRtcCallManager @Inject constructor( override fun onCallInviteReceived(mxCall: MxCall, callInviteContent: CallInviteContent) { Timber.v("## VOIP onCallInviteReceived callId ${mxCall.callId}") - if (currentCall != null && currentCall?.mxCall?.state !is CallState.Connected || getCalls().size >= 2) { + if (currentCall.get() != null && currentCall.get()?.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 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 8281f055e7..87b561ff93 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 @@ -32,11 +32,11 @@ import im.vector.app.core.glide.GlideApp import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseFragment -import im.vector.app.core.ui.views.ActiveCallView +import im.vector.app.core.ui.views.CurrentCallsView 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.SharedCurrentCallViewModel +import im.vector.app.features.call.SharedKnownCallsViewModel import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.home.room.list.RoomListFragment @@ -69,7 +69,7 @@ class HomeDetailFragment @Inject constructor( private val vectorPreferences: VectorPreferences ) : VectorBaseFragment(), KeysBackupBanner.Delegate, - ActiveCallView.Callback, + CurrentCallsView.Callback, ServerBackupStatusViewModel.Factory { private val viewModel: HomeDetailViewModel by fragmentViewModel() @@ -77,7 +77,7 @@ class HomeDetailFragment @Inject constructor( private val serverBackupStatusViewModel: ServerBackupStatusViewModel by activityViewModel() private lateinit var sharedActionViewModel: HomeSharedActionViewModel - private lateinit var sharedCallActionViewModel: SharedCurrentCallViewModel + private lateinit var sharedCallActionViewModel: SharedKnownCallsViewModel override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentHomeDetailBinding { return FragmentHomeDetailBinding.inflate(inflater, container, false) @@ -88,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(SharedCurrentCallViewModel::class.java) + sharedCallActionViewModel = activityViewModelProvider.get(SharedKnownCallsViewModel::class.java) setupBottomNavigationView() setupToolbar() @@ -126,9 +126,9 @@ class HomeDetailFragment @Inject constructor( } sharedCallActionViewModel - .currentCall + .liveKnownCalls .observe(viewLifecycleOwner, { - activeCallViewHolder.updateCall(it, callManager.getCalls()) + activeCallViewHolder.updateCall(callManager.getCurrentCall(), callManager.getCalls()) invalidateOptionsMenu() }) } @@ -335,7 +335,7 @@ class HomeDetailFragment @Inject constructor( } override fun onTapToReturnToCall() { - sharedCallActionViewModel.currentCall.value?.let { call -> + callManager.getCurrentCall()?.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 62e00f03be..958e956fee 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 @@ -89,7 +89,7 @@ import im.vector.app.core.intent.getFilenameFromUri import im.vector.app.core.intent.getMimeTypeFromUri import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.resources.ColorProvider -import im.vector.app.core.ui.views.ActiveCallView +import im.vector.app.core.ui.views.CurrentCallsView import im.vector.app.core.ui.views.ActiveCallViewHolder import im.vector.app.core.ui.views.ActiveConferenceView import im.vector.app.core.ui.views.JumpToReadMarkerView @@ -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.SharedCurrentCallViewModel +import im.vector.app.features.call.SharedKnownCallsViewModel import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.conference.JitsiCallViewModel import im.vector.app.features.call.webrtc.WebRtcCallManager @@ -237,7 +237,7 @@ class RoomDetailFragment @Inject constructor( AttachmentTypeSelectorView.Callback, AttachmentsHelper.Callback, GalleryOrCameraDialogHelper.Listener, - ActiveCallView.Callback { + CurrentCallsView.Callback { companion object { /** @@ -283,7 +283,7 @@ class RoomDetailFragment @Inject constructor( override fun getMenuRes() = R.menu.menu_timeline private lateinit var sharedActionViewModel: MessageSharedActionViewModel - private lateinit var sharedCurrentCallViewModel: SharedCurrentCallViewModel + private lateinit var knownCallsViewModel: SharedKnownCallsViewModel private lateinit var layoutManager: LinearLayoutManager private lateinit var jumpToBottomViewVisibilityManager: JumpToBottomViewVisibilityManager @@ -300,7 +300,7 @@ class RoomDetailFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java) - sharedCurrentCallViewModel = activityViewModelProvider.get(SharedCurrentCallViewModel::class.java) + knownCallsViewModel = activityViewModelProvider.get(SharedKnownCallsViewModel::class.java) attachmentsHelper = AttachmentsHelper(requireContext(), this).register() keyboardStateUtils = KeyboardStateUtils(requireActivity()) setupToolbar(views.roomToolbar) @@ -325,10 +325,10 @@ class RoomDetailFragment @Inject constructor( } .disposeOnDestroyView() - sharedCurrentCallViewModel - .currentCall + knownCallsViewModel + .liveKnownCalls .observe(viewLifecycleOwner, { - activeCallViewHolder.updateCall(it, callManager.getCalls()) + activeCallViewHolder.updateCall(callManager.getCurrentCall(), it) invalidateOptionsMenu() }) @@ -801,17 +801,14 @@ class RoomDetailFragment @Inject constructor( } } 2 -> { - val activeCall = sharedCurrentCallViewModel.currentCall.value - if (activeCall != null) { + val currentCall = callManager.getCurrentCall() + if (currentCall != null) { // resume existing if same room, if not prompt to kill and then restart new call? - if (activeCall.roomId == roomDetailArgs.roomId) { + if (currentCall.mxCall.roomId == roomDetailArgs.roomId) { onTapToReturnToCall() + }else { + safeStartCall(isVideoCall) } - // else { - // TODO might not work well, and should prompt - // webRtcPeerConnectionManager.endCall() - // safeStartCall(it, isVideoCall) - // } } else if (!state.isAllowedToStartWebRTCCall) { showDialogWithMessage(getString( if (state.isDm()) { @@ -2016,7 +2013,7 @@ class RoomDetailFragment @Inject constructor( } override fun onTapToReturnToCall() { - sharedCurrentCallViewModel.currentCall.value?.let { call -> + callManager.getCurrentCall()?.let { call -> VectorCallActivity.newIntent( context = requireContext(), callId = call.callId, diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt index 504fccb63a..21a8d42848 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt @@ -113,4 +113,5 @@ interface Navigator { options: ((MutableList>) -> Unit)?) fun openSearch(context: Context, roomId: String) + } diff --git a/vector/src/main/java/im/vector/app/features/popup/IncomingCallAlert.kt b/vector/src/main/java/im/vector/app/features/popup/IncomingCallAlert.kt index 64e729e54d..b3882945b4 100644 --- a/vector/src/main/java/im/vector/app/features/popup/IncomingCallAlert.kt +++ b/vector/src/main/java/im/vector/app/features/popup/IncomingCallAlert.kt @@ -21,6 +21,7 @@ import android.view.View import android.widget.ImageView import android.widget.TextView import im.vector.app.R +import im.vector.app.core.glide.GlideApp import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.util.MatrixItem @@ -50,7 +51,7 @@ class IncomingCallAlert(uid: String, view.findViewById(R.id.incomingCallKindView).setText(callKind) view.findViewById(R.id.incomingCallNameView).text = matrixItem?.getBestName() view.findViewById(R.id.incomingCallAvatar)?.let { imageView -> - matrixItem?.let { avatarRenderer.render(it, imageView) } + matrixItem?.let { avatarRenderer.render(it, imageView, GlideApp.with(view.context.applicationContext)) } } view.findViewById(R.id.incomingCallAcceptView).setOnClickListener { onAccept() diff --git a/vector/src/main/java/im/vector/app/features/popup/VerificationVectorAlert.kt b/vector/src/main/java/im/vector/app/features/popup/VerificationVectorAlert.kt index 2cd9ab59ac..ee6728f969 100644 --- a/vector/src/main/java/im/vector/app/features/popup/VerificationVectorAlert.kt +++ b/vector/src/main/java/im/vector/app/features/popup/VerificationVectorAlert.kt @@ -21,6 +21,7 @@ import android.view.View import android.widget.ImageView import androidx.annotation.DrawableRes import im.vector.app.R +import im.vector.app.core.glide.GlideApp import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.util.MatrixItem @@ -43,7 +44,7 @@ class VerificationVectorAlert(uid: String, override fun bind(view: View) { view.findViewById(R.id.ivUserAvatar)?.let { imageView -> - matrixItem?.let { avatarRenderer.render(it, imageView) } + matrixItem?.let { avatarRenderer.render(it, imageView, GlideApp.with(view.context.applicationContext)) } } } } diff --git a/vector/src/main/res/layout/activity_call.xml b/vector/src/main/res/layout/activity_call.xml index b82915d383..45be67ae8e 100644 --- a/vector/src/main/res/layout/activity_call.xml +++ b/vector/src/main/res/layout/activity_call.xml @@ -14,10 +14,10 @@ + android:layout_height="match_parent" + android:scaleType="centerCrop" + tools:src="@tools:sample/avatars" /> + + + + + + + + + app:layout_constraintTop_toTopOf="@id/otherMemberAvatar" /> - - @@ -31,8 +30,8 @@ style="@style/Widget.MaterialComponents.Button.TextButton" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignTop="@+id/activeCallInfo" - android:layout_alignBottom="@+id/activeCallInfo" + android:layout_alignTop="@+id/currentCallsInfo" + android:layout_alignBottom="@+id/currentCallsInfo" android:layout_alignParentEnd="true" android:clickable="false" android:focusable="false" diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 508c8a2c31..190eae97bc 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -2775,8 +2775,12 @@ This call has ended Call back - Active call (%1$s) - 1 active call (%1$s) · 1 paused call - 2 paused calls + + Active call (%1$s) + Paused call + 1 active call (%1$s) · 1 paused call + 1 active call (%1$s) · %2$d paused calls + %1$d paused calls +