VoIP: add info on other call when switching
This commit is contained in:
parent
b1f492de58
commit
a5736efc75
@ -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
|
||||
|
@ -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
|
||||
|
@ -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<WebRtcCall>) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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<WebRtcCall?> = MutableLiveData()
|
||||
val liveKnownCalls: MutableLiveData<List<WebRtcCall>> = 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()
|
||||
}
|
||||
}
|
@ -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<ActivityCallBinding>(), 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<ActivityCallBinding>(), 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<ActivityCallBinding>(), 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<ActivityCallBinding>(), 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<out String>, grantResults: IntArray) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -36,10 +36,16 @@ data class VectorCallViewState(
|
||||
val canSwitchCamera: Boolean = true,
|
||||
val soundDevice: CallAudioManager.SoundDevice = CallAudioManager.SoundDevice.PHONE,
|
||||
val availableSoundDevices: List<CallAudioManager.SoundDevice> = emptyList(),
|
||||
val otherUserMatrixItem: Async<MatrixItem> = Uninitialized,
|
||||
val callState: Async<CallState> = Uninitialized
|
||||
val callState: Async<CallState> = 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,
|
||||
|
@ -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<Listener>()
|
||||
private val listeners = CopyOnWriteArrayList<Listener>()
|
||||
|
||||
fun addListener(listener: Listener) {
|
||||
listeners.add(listener)
|
||||
@ -277,7 +278,7 @@ class WebRtcCall(val mxCall: MxCall,
|
||||
}
|
||||
}
|
||||
|
||||
fun detachRenderers(renderers: List<SurfaceViewRenderer>?) {
|
||||
fun detachRenderers(renderers: List<SurfaceViewRenderer>?) = 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
|
||||
|
@ -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<CurrentCallListener>().toMutableList()
|
||||
private val currentCallsListeners = CopyOnWriteArrayList<CurrentCallListener>()
|
||||
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<WebRtcCall?> = AtomicReference(null)
|
||||
private fun AtomicReference<WebRtcCall?>.setAndNotify(newValue: WebRtcCall?) {
|
||||
set(newValue)
|
||||
currentCallsListeners.forEach {
|
||||
tryOrNull { it.onCurrentCallChange(newValue) }
|
||||
}
|
||||
}
|
||||
|
||||
private val callsByCallId = ConcurrentHashMap<String, WebRtcCall>()
|
||||
private val callsByRoomId = ConcurrentHashMap<String, MutableList<WebRtcCall>>()
|
||||
@ -120,13 +126,17 @@ class WebRtcCallManager @Inject constructor(
|
||||
return callsByRoomId[roomId] ?: emptyList()
|
||||
}
|
||||
|
||||
fun getCurrentCall(): WebRtcCall? {
|
||||
return currentCall.get()
|
||||
}
|
||||
|
||||
fun getCalls(): List<WebRtcCall> {
|
||||
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
|
||||
|
@ -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<FragmentHomeDetailBinding>(),
|
||||
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,
|
||||
|
@ -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,
|
||||
|
@ -113,4 +113,5 @@ interface Navigator {
|
||||
options: ((MutableList<Pair<View, String>>) -> Unit)?)
|
||||
|
||||
fun openSearch(context: Context, roomId: String)
|
||||
|
||||
}
|
||||
|
@ -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<TextView>(R.id.incomingCallKindView).setText(callKind)
|
||||
view.findViewById<TextView>(R.id.incomingCallNameView).text = matrixItem?.getBestName()
|
||||
view.findViewById<ImageView>(R.id.incomingCallAvatar)?.let { imageView ->
|
||||
matrixItem?.let { avatarRenderer.render(it, imageView) }
|
||||
matrixItem?.let { avatarRenderer.render(it, imageView, GlideApp.with(view.context.applicationContext)) }
|
||||
}
|
||||
view.findViewById<ImageView>(R.id.incomingCallAcceptView).setOnClickListener {
|
||||
onAccept()
|
||||
|
@ -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<ImageView>(R.id.ivUserAvatar)?.let { imageView ->
|
||||
matrixItem?.let { avatarRenderer.render(it, imageView) }
|
||||
matrixItem?.let { avatarRenderer.render(it, imageView, GlideApp.with(view.context.applicationContext)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,10 +14,10 @@
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/bgCallView"
|
||||
tools:src="@tools:sample/avatars"
|
||||
android:scaleType="centerCrop"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<org.webrtc.SurfaceViewRenderer
|
||||
android:id="@+id/fullscreenRenderer"
|
||||
@ -36,6 +36,35 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/otherKnownCallLayout"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="144dp"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
android:background="@color/riotx_background_light"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/otherKnownCallAvatarView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/otherSmallIsHeldIcon"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/ic_call_small_pause" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/otherMemberAvatar"
|
||||
android:layout_width="80dp"
|
||||
@ -49,14 +78,14 @@
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/smallIsHeldIcon"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:id="@+id/smallIsHeldIcon"
|
||||
android:src="@drawable/ic_call_small_pause"
|
||||
app:layout_constraintTop_toTopOf="@id/otherMemberAvatar"
|
||||
app:layout_constraintBottom_toBottomOf="@id/otherMemberAvatar"
|
||||
app:layout_constraintEnd_toEndOf="@id/otherMemberAvatar"
|
||||
app:layout_constraintStart_toStartOf="@id/otherMemberAvatar"
|
||||
app:layout_constraintEnd_toEndOf="@id/otherMemberAvatar" />
|
||||
app:layout_constraintTop_toTopOf="@id/otherMemberAvatar" />
|
||||
|
||||
|
||||
<TextView
|
||||
@ -95,10 +124,10 @@
|
||||
android:id="@+id/callActionText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_margin="8dp"
|
||||
android:gravity="center"
|
||||
android:textColor="?attr/colorAccent"
|
||||
android:textSize="14sp"
|
||||
android:layout_margin="8dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/callStatusText"
|
||||
|
@ -65,7 +65,7 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/syncStateView" />
|
||||
|
||||
<im.vector.app.core.ui.views.ActiveCallView
|
||||
<im.vector.app.core.ui.views.CurrentCallsView
|
||||
android:id="@+id/activeCallView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -96,7 +96,7 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/roomToolbar" />
|
||||
|
||||
<im.vector.app.core.ui.views.ActiveCallView
|
||||
<im.vector.app.core.ui.views.CurrentCallsView
|
||||
android:id="@+id/activeCallView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -9,11 +9,10 @@
|
||||
tools:parentTag="android.widget.RelativeLayout">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/activeCallInfo"
|
||||
android:id="@+id/currentCallsInfo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toStartOf="@id/returnToCallButton"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:drawablePadding="10dp"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="16dp"
|
||||
@ -21,7 +20,7 @@
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:textSize="14sp"
|
||||
android:text="@string/call_one_active_one_paused_call"
|
||||
android:text="@string/call_active_and_single_paused"
|
||||
android:textColor="@color/white"
|
||||
app:drawableTint="@color/white"
|
||||
app:drawableStartCompat="@drawable/ic_call_answer" />
|
||||
@ -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"
|
@ -2775,8 +2775,12 @@
|
||||
<string name="call_tile_ended">This call has ended</string>
|
||||
<string name="call_tile_call_back">Call back</string>
|
||||
|
||||
<string name="call_active_call">Active call (%1$s)</string>
|
||||
<string name="call_one_active_one_paused_call">1 active call (%1$s) · 1 paused call</string>
|
||||
<string name="call_two_paused_calls">2 paused calls</string>
|
||||
|
||||
<string name="call_only_active">Active call (%1$s)</string>
|
||||
<string name="call_only_paused">Paused call</string>
|
||||
<string name="call_active_and_single_paused">1 active call (%1$s) · 1 paused call</string>
|
||||
<string name="call_active_and_multiple_paused">1 active call (%1$s) · %2$d paused calls</string>
|
||||
<string name="call_only_multiple_paused">%1$d paused calls</string>
|
||||
|
||||
|
||||
</resources>
|
||||
|
Loading…
x
Reference in New Issue
Block a user