Missed call notif: make some cleanup and minor changes

This commit is contained in:
ganfra 2021-07-21 12:28:14 +02:00
parent 88cc7471a8
commit 5dda5a107a
14 changed files with 159 additions and 93 deletions

1
changelog.d/3710.feature Normal file
View file

@ -0,0 +1 @@
Show missed call notification.

View file

@ -16,6 +16,8 @@
package org.matrix.android.sdk.api.session.call
import org.matrix.android.sdk.api.session.room.model.call.EndCallReason
sealed class CallState {
/** Idle, setting up objects */
@ -42,6 +44,6 @@ sealed class CallState {
* */
data class Connected(val iceConnectionState: MxPeerConnectionState) : CallState()
/** Terminated. Incoming/Outgoing call, the call is terminated */
object Terminated : CallState()
/** Ended. Incoming/Outgoing call, the call is terminated */
data class Ended(val reason: EndCallReason? = null) : CallState()
}

View file

@ -18,7 +18,7 @@ package org.matrix.android.sdk.api.session.call
import org.matrix.android.sdk.api.session.room.model.call.CallCandidate
import org.matrix.android.sdk.api.session.room.model.call.CallCapabilities
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
import org.matrix.android.sdk.api.session.room.model.call.EndCallReason
import org.matrix.android.sdk.api.session.room.model.call.SdpType
import org.matrix.android.sdk.api.util.Optional
@ -69,7 +69,7 @@ interface MxCall : MxCallDetail {
/**
* End the call
*/
fun hangUp(reason: CallHangupContent.Reason? = null)
fun hangUp(reason: EndCallReason? = null)
/**
* Start a call

View file

@ -43,29 +43,5 @@ data class CallHangupContent(
* or `invite_timeout` for when the other party did not answer in time.
* One of: ["ice_failed", "invite_timeout"]
*/
@Json(name = "reason") val reason: Reason? = null
) : CallSignalingContent {
@JsonClass(generateAdapter = false)
enum class Reason {
@Json(name = "ice_failed")
ICE_FAILED,
@Json(name = "ice_timeout")
ICE_TIMEOUT,
@Json(name = "user_hangup")
USER_HANGUP,
@Json(name = "replaced")
REPLACED,
@Json(name = "user_media_failed")
USER_MEDIA_FAILED,
@Json(name = "invite_timeout")
INVITE_TIMEOUT,
@Json(name = "unknown_error")
UNKWOWN_ERROR
}
}
@Json(name = "reason") val reason: EndCallReason? = null
) : CallSignalingContent

View file

@ -36,5 +36,10 @@ data class CallRejectContent(
/**
* Required. The version of the VoIP specification this message adheres to.
*/
@Json(name = "version") override val version: String?
@Json(name = "version") override val version: String?,
/**
* Optional error reason for the reject.
*/
@Json(name = "reason") val reason: EndCallReason? = null
) : CallSignalingContent

View file

@ -0,0 +1,50 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.room.model.call
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = false)
enum class EndCallReason {
@Json(name = "ice_failed")
ICE_FAILED,
@Json(name = "ice_timeout")
ICE_TIMEOUT,
@Json(name = "user_hangup")
USER_HANGUP,
@Json(name = "replaced")
REPLACED,
@Json(name = "user_media_failed")
USER_MEDIA_FAILED,
@Json(name = "invite_timeout")
INVITE_TIMEOUT,
@Json(name = "unknown_error")
UNKWOWN_ERROR,
@Json(name = "user_busy")
USER_BUSY,
@Json(name = "answered_elsewhere")
ANSWERED_ELSEWHERE
}

View file

@ -166,7 +166,7 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa
Timber.v("Ignoring hangup from party ID ${content.partyId} we have chosen party ID ${call.opponentPartyId}")
return
}
if (call.state != CallState.Terminated) {
if (call.state !is CallState.Ended) {
activeCallHandler.removeCall(content.callId)
callListenersDispatcher.onCallHangupReceived(content)
}

View file

@ -38,6 +38,7 @@ import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent
import org.matrix.android.sdk.api.session.room.model.call.CallReplacesContent
import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent
import org.matrix.android.sdk.api.session.room.model.call.CallSignalingContent
import org.matrix.android.sdk.api.session.room.model.call.EndCallReason
import org.matrix.android.sdk.api.session.room.model.call.SdpType
import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.session.call.DefaultCallSignalingService
@ -153,20 +154,20 @@ internal class MxCallImpl(
)
.let { createEventAndLocalEcho(type = EventType.CALL_REJECT, roomId = roomId, content = it.toContent()) }
.also { eventSenderProcessor.postEvent(it) }
state = CallState.Terminated
state = CallState.Ended(reason = EndCallReason.USER_HANGUP)
}
override fun hangUp(reason: CallHangupContent.Reason?) {
override fun hangUp(reason: EndCallReason?) {
Timber.v("## VOIP hangup $callId")
CallHangupContent(
callId = callId,
partyId = ourPartyId,
reason = reason ?: CallHangupContent.Reason.USER_HANGUP,
reason = reason ?: EndCallReason.USER_HANGUP,
version = MxCall.VOIP_PROTO_VERSION.toString()
)
.let { createEventAndLocalEcho(type = EventType.CALL_HANGUP, roomId = roomId, content = it.toContent()) }
.also { eventSenderProcessor.postEvent(it) }
state = CallState.Terminated
state = CallState.Ended(reason)
}
override fun accept(sdpString: String) {

View file

@ -37,7 +37,7 @@ import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.notifications.NotificationUtils
import im.vector.app.features.popup.IncomingCallAlert
import im.vector.app.features.popup.PopupAlertManager
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
import org.matrix.android.sdk.api.session.room.model.call.EndCallReason
import org.matrix.android.sdk.api.util.MatrixItem
import timber.log.Timber
@ -192,7 +192,8 @@ class CallService : VectorService() {
private fun handleCallTerminated(intent: Intent) {
val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: ""
val isRejected = intent.getBooleanExtra(EXTRA_IS_REJECTED, false)
val endCallReason = intent.getSerializableExtra(EXTRA_END_CALL_REASON) as EndCallReason
val rejected = intent.getBooleanExtra(EXTRA_END_CALL_REJECTED, false)
alertManager.cancelAlert(callId)
val terminatedCall = knownCalls.firstOrNull { it.callId == callId }
if (terminatedCall == null) {
@ -206,13 +207,13 @@ class CallService : VectorService() {
myStopSelf()
}
val wasConnected = connectedCallIds.remove(callId)
if (wasConnected || terminatedCall.isOutgoing || isRejected) {
val notification = notificationUtils.buildCallEndedNotification(terminatedCall.isVideoCall)
notificationManager.notify(callId.hashCode(), notification)
} else {
if (!wasConnected && !terminatedCall.isOutgoing && !rejected && endCallReason != EndCallReason.ANSWERED_ELSEWHERE) {
val notification = notificationUtils.buildCallMissedNotification(terminatedCall)
notificationManager.cancel(callId.hashCode())
notificationManager.notify(MISSED_CALL_TAG, terminatedCall.nativeRoomId.hashCode(), notification)
} else {
val notification = notificationUtils.buildCallEndedNotification(terminatedCall.isVideoCall)
notificationManager.notify(callId.hashCode(), notification)
}
}
@ -287,7 +288,7 @@ class CallService : VectorService() {
connections[callConnection.callId] = callConnection
}
private fun WebRtcCall.toCallInformation(): CallInformation{
private fun WebRtcCall.toCallInformation(): CallInformation {
return CallInformation(
callId = this.callId,
nativeRoomId = this.nativeRoomId,
@ -306,7 +307,7 @@ class CallService : VectorService() {
val opponentUserId: String,
val matrixItem: MatrixItem?,
val isVideoCall: Boolean,
val isOutgoing: Boolean,
val isOutgoing: Boolean
)
companion object {
@ -324,7 +325,8 @@ class CallService : VectorService() {
private const val EXTRA_CALL_ID = "EXTRA_CALL_ID"
private const val EXTRA_IS_IN_BG = "EXTRA_IS_IN_BG"
private const val EXTRA_IS_REJECTED = "EXTRA_IS_REJECTED"
private const val EXTRA_END_CALL_REJECTED = "EXTRA_END_CALL_REJECTED"
private const val EXTRA_END_CALL_REASON = "EXTRA_END_CALL_REASON"
fun onIncomingCallRinging(context: Context,
callId: String,
@ -360,12 +362,13 @@ class CallService : VectorService() {
ContextCompat.startForegroundService(context, intent)
}
fun onCallTerminated(context: Context, callId: String, isRejected: Boolean) {
fun onCallTerminated(context: Context, callId: String, endCallReason: EndCallReason, rejected: Boolean) {
val intent = Intent(context, CallService::class.java)
.apply {
action = ACTION_CALL_TERMINATED
putExtra(EXTRA_CALL_ID, callId)
putExtra(EXTRA_IS_REJECTED, isRejected)
putExtra(EXTRA_END_CALL_REASON, endCallReason)
putExtra(EXTRA_END_CALL_REJECTED, rejected)
}
ContextCompat.startForegroundService(context, intent)
}

View file

@ -118,7 +118,7 @@ class CallControlsView @JvmOverloads constructor(
views.connectedControls.isVisible = false
}
}
is CallState.Terminated,
is CallState.Ended,
null -> {
views.ringingControls.isVisible = false
views.connectedControls.isVisible = false

View file

@ -196,7 +196,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
views.callConnectingProgress.isVisible = true
configureCallInfo(state)
}
is CallState.Connected -> {
is CallState.Connected -> {
if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) {
if (state.isLocalOnHold || state.isRemoteOnHold) {
views.smallIsHeldIcon.isVisible = true
@ -246,10 +246,10 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
views.callConnectingProgress.isVisible = true
}
}
is CallState.Terminated -> {
is CallState.Ended -> {
finish()
}
null -> {
null -> {
}
}
}

View file

@ -57,7 +57,7 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState:
private val call = callManager.getCallById(initialState.callId)
private val callListener = object : WebRtcCall.Listener {
override fun onStateUpdate(call: MxCall) {
if (call.state == CallState.Terminated) {
if (call.state is CallState.Ended) {
_viewEvents.post(CallTransferViewEvents.Dismiss)
}
}

View file

@ -57,6 +57,9 @@ import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent
import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent
import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent
import org.matrix.android.sdk.api.session.room.model.call.EndCallReason
import org.matrix.android.sdk.api.session.room.model.call.SdpType
import org.threeten.bp.Duration
import org.webrtc.AudioSource
@ -99,7 +102,7 @@ class WebRtcCall(
private val sessionProvider: Provider<Session?>,
private val peerConnectionFactoryProvider: Provider<PeerConnectionFactory?>,
private val onCallBecomeActive: (WebRtcCall) -> Unit,
private val onCallEnded: (String, Boolean) -> Unit
private val onCallEnded: (String, EndCallReason, Boolean) -> Unit
) : MxCall.StateListener {
interface Listener : MxCall.StateListener {
@ -227,7 +230,7 @@ class WebRtcCall(
// Allow a short time for initial candidates to be gathered
delay(200)
}
if (mxCall.state == CallState.Terminated) {
if (mxCall.state is CallState.Ended) {
return@launch
}
if (mxCall.state == CallState.CreateOffer) {
@ -285,7 +288,7 @@ class WebRtcCall(
createCallId = CallIdGenerator.generate(),
awaitCallId = null
)
endCall(sendEndSignaling = false)
terminate(EndCallReason.REPLACED)
}
}
@ -307,8 +310,8 @@ class WebRtcCall(
createCallId = newCallId,
awaitCallId = null
)
endCall(sendEndSignaling = false)
transferTargetCall.endCall(sendEndSignaling = false)
terminate(EndCallReason.REPLACED)
transferTargetCall.terminate(EndCallReason.REPLACED)
}
}
@ -461,7 +464,7 @@ class WebRtcCall(
peerConnection?.awaitSetRemoteDescription(offerSdp)
} catch (failure: Throwable) {
Timber.v("Failure putting remote description")
endCall(true, CallHangupContent.Reason.UNKWOWN_ERROR)
endCall(reason = EndCallReason.UNKWOWN_ERROR)
return@withContext
}
// 2) Access camera + microphone, create local stream
@ -767,7 +770,7 @@ class WebRtcCall(
if (stream.audioTracks.size > 1 || stream.videoTracks.size > 1) {
Timber.e("## VOIP StreamObserver weird looking stream: $stream")
// TODO maybe do something more??
endCall(true)
endCall(EndCallReason.UNKWOWN_ERROR)
return@launch
}
if (stream.audioTracks.size == 1) {
@ -795,33 +798,34 @@ class WebRtcCall(
}
}
fun endCall(sendEndSignaling: Boolean = true, reason: CallHangupContent.Reason? = null) {
fun endCall(reason: EndCallReason? = null) {
sessionScope?.launch(dispatcher) {
if (mxCall.state == CallState.Terminated) {
if (mxCall.state is CallState.Ended) {
return@launch
}
// Close tracks ASAP
localVideoTrack?.setEnabled(false)
localVideoTrack?.setEnabled(false)
cameraAvailabilityCallback?.let { cameraAvailabilityCallback ->
val cameraManager = context.getSystemService<CameraManager>()!!
cameraManager.unregisterAvailabilityCallback(cameraAvailabilityCallback)
}
val wasRinging = mxCall.state is CallState.LocalRinging
mxCall.state = CallState.Terminated
release()
val isRejected = wasRinging && sendEndSignaling
onCallEnded(callId, isRejected)
if (sendEndSignaling) {
if (wasRinging) {
mxCall.reject()
} else {
mxCall.hangUp(reason)
}
val reject = mxCall.state is CallState.LocalRinging
terminate(EndCallReason.USER_HANGUP, reject)
if (reject) {
mxCall.reject()
} else {
mxCall.hangUp(reason)
}
}
}
private suspend fun terminate(reason: EndCallReason? = null, rejected: Boolean = false) = withContext(dispatcher) {
// Close tracks ASAP
localVideoTrack?.setEnabled(false)
localVideoTrack?.setEnabled(false)
cameraAvailabilityCallback?.let { cameraAvailabilityCallback ->
val cameraManager = context.getSystemService<CameraManager>()!!
cameraManager.unregisterAvailabilityCallback(cameraAvailabilityCallback)
}
mxCall.state = CallState.Ended(reason ?: EndCallReason.USER_HANGUP)
release()
onCallEnded(callId, reason ?: EndCallReason.USER_HANGUP, rejected)
}
// Call listener
fun onCallIceCandidateReceived(iceCandidatesContent: CallCandidatesContent) {
@ -844,7 +848,7 @@ class WebRtcCall(
try {
peerConnection?.awaitSetRemoteDescription(sdp)
} catch (failure: Throwable) {
endCall(true, CallHangupContent.Reason.UNKWOWN_ERROR)
endCall(EndCallReason.UNKWOWN_ERROR)
return@launch
}
if (mxCall.opponentPartyId?.hasValue().orFalse()) {
@ -905,6 +909,29 @@ class WebRtcCall(
}
}
fun onCallHangupReceived(callHangupContent: CallHangupContent) {
sessionScope?.launch(dispatcher) {
terminate(callHangupContent.reason)
}
}
fun onCallRejectReceived(callRejectContent: CallRejectContent) {
sessionScope?.launch(dispatcher) {
terminate(callRejectContent.reason, true)
}
}
fun onCallSelectedAnswerReceived(callSelectAnswerContent: CallSelectAnswerContent) {
sessionScope?.launch(dispatcher) {
val selectedPartyId = callSelectAnswerContent.selectedPartyId
if (selectedPartyId != mxCall.ourPartyId) {
Timber.i("Got select_answer for party ID $selectedPartyId: we are party ID ${mxCall.ourPartyId}.")
// The other party has picked somebody else's answer
terminate()
}
}
}
fun onCallAssertedIdentityReceived(callAssertedIdentityContent: CallAssertedIdentityContent) {
sessionScope?.launch(dispatcher) {
val session = sessionProvider.get() ?: return@launch

View file

@ -29,7 +29,9 @@ import im.vector.app.features.call.lookup.CallProtocolsChecker
import im.vector.app.features.call.lookup.CallUserMapper
import im.vector.app.features.call.utils.EglUtils
import im.vector.app.features.call.vectorCallService
import im.vector.app.features.session.coroutineScope
import im.vector.app.push.fcm.FcmHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.asCoroutineDispatcher
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull
@ -45,6 +47,7 @@ import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent
import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent
import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent
import org.matrix.android.sdk.api.session.room.model.call.EndCallReason
import org.webrtc.DefaultVideoDecoderFactory
import org.webrtc.DefaultVideoEncoderFactory
import org.webrtc.PeerConnectionFactory
@ -75,6 +78,9 @@ class WebRtcCallManager @Inject constructor(
private val callUserMapper: CallUserMapper?
get() = currentSession?.vectorCallService?.userMapper
private val sessionScope: CoroutineScope?
get() = currentSession?.coroutineScope
interface CurrentCallListener {
fun onCurrentCallChange(call: WebRtcCall?) {}
fun onAudioDevicesChange() {}
@ -232,12 +238,12 @@ class WebRtcCallManager @Inject constructor(
this.currentCall.setAndNotify(call)
}
private fun onCallEnded(callId: String, isRejected: Boolean) {
private fun onCallEnded(callId: String, endCallReason: EndCallReason, rejected: Boolean) {
Timber.v("## VOIP WebRtcPeerConnectionManager onCall ended: $callId")
val webRtcCall = callsByCallId.remove(callId) ?: return Unit.also {
Timber.v("On call ended for unknown call $callId")
}
CallService.onCallTerminated(context, callId, isRejected)
CallService.onCallTerminated(context, callId, endCallReason, rejected)
callsByRoomId[webRtcCall.signalingRoomId]?.remove(webRtcCall)
callsByRoomId[webRtcCall.nativeRoomId]?.remove(webRtcCall)
transferees.remove(callId)
@ -329,8 +335,8 @@ class WebRtcCallManager @Inject constructor(
return webRtcCall
}
fun endCallForRoom(roomId: String, originatedByMe: Boolean = true) {
callsByRoomId[roomId]?.firstOrNull()?.endCall(originatedByMe)
fun endCallForRoom(roomId: String) {
callsByRoomId[roomId]?.firstOrNull()?.endCall()
}
override fun onCallInviteReceived(mxCall: MxCall, callInviteContent: CallInviteContent) {
@ -386,7 +392,7 @@ class WebRtcCallManager @Inject constructor(
?: return Unit.also {
Timber.w("onCallHangupReceived for non active call? ${callHangupContent.callId}")
}
call.endCall(false)
call.onCallHangupReceived(callHangupContent)
}
override fun onCallRejectReceived(callRejectContent: CallRejectContent) {
@ -394,7 +400,7 @@ class WebRtcCallManager @Inject constructor(
?: return Unit.also {
Timber.w("onCallRejectReceived for non active call? ${callRejectContent.callId}")
}
call.endCall(false)
call.onCallRejectReceived(callRejectContent)
}
override fun onCallSelectAnswerReceived(callSelectAnswerContent: CallSelectAnswerContent) {
@ -402,12 +408,7 @@ class WebRtcCallManager @Inject constructor(
?: return Unit.also {
Timber.w("onCallSelectAnswerReceived for non active call? ${callSelectAnswerContent.callId}")
}
val selectedPartyId = callSelectAnswerContent.selectedPartyId
if (selectedPartyId != call.mxCall.ourPartyId) {
Timber.i("Got select_answer for party ID $selectedPartyId: we are party ID ${call.mxCall.ourPartyId}.")
// The other party has picked somebody else's answer
call.endCall(false)
}
call.onCallSelectedAnswerReceived(callSelectAnswerContent)
}
override fun onCallNegotiateReceived(callNegotiateContent: CallNegotiateContent) {
@ -420,7 +421,7 @@ class WebRtcCallManager @Inject constructor(
override fun onCallManagedByOtherSession(callId: String) {
Timber.v("## VOIP onCallManagedByOtherSession: $callId")
onCallEnded(callId, false)
onCallEnded(callId, EndCallReason.ANSWERED_ELSEWHERE, false)
}
override fun onCallAssertedIdentityReceived(callAssertedIdentityContent: CallAssertedIdentityContent) {