feat: new call state processing

This commit is contained in:
jubb 2022-03-03 15:18:19 +11:00
parent 5dba223c2e
commit 573f0930df
6 changed files with 333 additions and 210 deletions

View File

@ -25,12 +25,30 @@ import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_IN
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_INCOMING_PRE_OFFER
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_INCOMING_RINGING
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_OUTGOING_RINGING
import org.thoughtcrime.securesms.webrtc.*
import org.thoughtcrime.securesms.webrtc.AudioManagerCommand
import org.thoughtcrime.securesms.webrtc.CallManager
import org.thoughtcrime.securesms.webrtc.CallViewModel
import org.thoughtcrime.securesms.webrtc.HangUpRtcOnPstnCallAnsweredListener
import org.thoughtcrime.securesms.webrtc.IncomingPstnCallReceiver
import org.thoughtcrime.securesms.webrtc.NetworkChangeReceiver
import org.thoughtcrime.securesms.webrtc.PeerConnectionException
import org.thoughtcrime.securesms.webrtc.PowerButtonReceiver
import org.thoughtcrime.securesms.webrtc.ProximityLockRelease
import org.thoughtcrime.securesms.webrtc.UncaughtExceptionHandlerManager
import org.thoughtcrime.securesms.webrtc.WiredHeadsetStateReceiver
import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger
import org.thoughtcrime.securesms.webrtc.data.Event
import org.thoughtcrime.securesms.webrtc.locks.LockManager
import org.webrtc.*
import org.webrtc.PeerConnection.IceConnectionState.*
import java.util.*
import org.webrtc.DataChannel
import org.webrtc.IceCandidate
import org.webrtc.MediaStream
import org.webrtc.PeerConnection
import org.webrtc.PeerConnection.IceConnectionState.CONNECTED
import org.webrtc.PeerConnection.IceConnectionState.DISCONNECTED
import org.webrtc.PeerConnection.IceConnectionState.FAILED
import org.webrtc.RtpReceiver
import org.webrtc.SessionDescription
import java.util.UUID
import java.util.concurrent.ExecutionException
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
@ -56,6 +74,8 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
const val ACTION_WIRED_HEADSET_CHANGE = "WIRED_HEADSET_CHANGE"
const val ACTION_SCREEN_OFF = "SCREEN_OFF"
const val ACTION_CHECK_TIMEOUT = "CHECK_TIMEOUT"
const val ACTION_CHECK_RECONNECT = "CHECK_RECONNECT"
const val ACTION_CHECK_RECONNECT_TIMEOUT = "CHECK_RECONNECT_TIMEOUT"
const val ACTION_IS_IN_CALL_QUERY = "IS_IN_CALL"
const val ACTION_WANTS_TO_ANSWER = "WANTS_TO_ANSWER"
@ -80,7 +100,8 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
const val EXTRA_WANTS_TO_ANSWER = "wants_to_answer"
const val INVALID_NOTIFICATION_ID = -1
private const val TIMEOUT_SECONDS = 30L
private const val TIMEOUT_SECONDS = 90L
private const val MAX_TIMEOUTS = 3
fun cameraEnabled(context: Context, enabled: Boolean) = Intent(context, WebRtcCallService::class.java)
.setAction(ACTION_SET_MUTE_VIDEO)
@ -165,6 +186,8 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
@Inject lateinit var callManager: CallManager
private var wantsToAnswer = false
private var currentTimeouts = 0
private var isNetworkAvailable = true
private val lockManager by lazy { LockManager(this) }
private val serviceExecutor = Executors.newSingleThreadExecutor()
@ -185,6 +208,9 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
LocalBroadcastManager.getInstance(this).sendBroadcast(Intent(WebRtcCallActivity.ACTION_END))
lockManager.updatePhoneState(LockManager.PhoneState.IDLE)
callManager.stop()
wantsToAnswer = false
currentTimeouts = 0
isNetworkAvailable = true
stopForeground(true)
}
@ -239,6 +265,8 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
action == ACTION_ICE_MESSAGE -> handleRemoteIceCandidate(intent)
action == ACTION_ICE_CONNECTED -> handleIceConnected(intent)
action == ACTION_CHECK_TIMEOUT -> handleCheckTimeout(intent)
action == ACTION_CHECK_RECONNECT -> handleCheckReconnect(intent)
action == ACTION_CHECK_RECONNECT_TIMEOUT -> handleCheckReconnectTimeout(intent)
action == ACTION_IS_IN_CALL_QUERY -> handleIsInCallQuery(intent)
action == ACTION_UPDATE_AUDIO -> handleUpdateAudio(intent)
}
@ -250,9 +278,10 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
super.onCreate()
callManager.registerListener(this)
wantsToAnswer = false
isNetworkAvailable = false
registerIncomingPstnCallReceiver()
registerWiredHeadsetStateReceiver()
registerWantsToAnswerReceiver() // TODO unregister
registerWantsToAnswerReceiver()
getSystemService(TelephonyManager::class.java)
.listen(hangupOnCallAnswered, PhoneStateListener.LISTEN_CALL_STATE)
registerUncaughtExceptionHandler()
@ -322,7 +351,6 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
}
val callId = getCallId(intent)
val recipient = getRemoteRecipient(intent)
val sentTimestamp = intent.getLongExtra(EXTRA_TIMESTAMP, -1)
if (isIncomingMessageExpired(intent)) {
insertMissedCall(recipient, true)
@ -330,17 +358,16 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
return
}
setCallInProgressNotification(TYPE_INCOMING_PRE_OFFER, recipient)
callManager.onPreOffer(callId, recipient, sentTimestamp)
callManager.postViewModelState(CallViewModel.State.CALL_PRE_INIT)
callManager.initializeAudioForCall()
callManager.startIncomingRinger()
callManager.setAudioEnabled(true)
callManager.onPreOffer(callId, recipient) {
setCallInProgressNotification(TYPE_INCOMING_PRE_OFFER, recipient)
callManager.postViewModelState(CallViewModel.State.CALL_PRE_INIT)
callManager.initializeAudioForCall()
callManager.startIncomingRinger()
callManager.setAudioEnabled(true)
}
}
private fun handleIncomingRing(intent: Intent) {
if (!callManager.isPreOffer() && !callManager.isIdle()) throw IllegalStateException("Incoming ring on non-idle")
val callId = getCallId(intent)
val recipient = getRemoteRecipient(intent)
val preOffer = callManager.preOfferCallData
@ -352,119 +379,119 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
val offer = intent.getStringExtra(EXTRA_REMOTE_DESCRIPTION) ?: return
val timestamp = intent.getLongExtra(EXTRA_TIMESTAMP, -1)
if (wantsToAnswer) {
setCallInProgressNotification(TYPE_INCOMING_CONNECTING, recipient)
} else {
setCallInProgressNotification(TYPE_INCOMING_RINGING, recipient)
callManager.onIncomingRing(offer, callId, recipient, timestamp) {
if (wantsToAnswer) {
setCallInProgressNotification(TYPE_INCOMING_CONNECTING, recipient)
} else {
setCallInProgressNotification(TYPE_INCOMING_RINGING, recipient)
}
callManager.clearPendingIceUpdates()
callManager.postViewModelState(CallViewModel.State.CALL_RINGING)
registerPowerButtonReceiver()
}
callManager.clearPendingIceUpdates()
callManager.onIncomingRing(offer, callId, recipient, timestamp)
callManager.postConnectionEvent(STATE_LOCAL_RINGING)
callManager.postViewModelState(CallViewModel.State.CALL_RINGING)
registerPowerButtonReceiver()
}
private fun handleOutgoingCall(intent: Intent) {
if (callManager.currentConnectionState != STATE_IDLE) throw IllegalStateException("Dialing from non-idle")
callManager.postConnectionEvent(Event.SendPreOffer) {
val recipient = getRemoteRecipient(intent)
callManager.recipient = recipient
val callId = UUID.randomUUID()
callManager.callId = callId
callManager.postConnectionEvent(STATE_DIALING)
val recipient = getRemoteRecipient(intent)
callManager.recipient = recipient
val callId = UUID.randomUUID()
callManager.callId = callId
callManager.initializeVideo(this)
callManager.initializeVideo(this)
callManager.postViewModelState(CallViewModel.State.CALL_OUTGOING)
lockManager.updatePhoneState(LockManager.PhoneState.IN_CALL)
callManager.initializeAudioForCall()
callManager.startOutgoingRinger(OutgoingRinger.Type.RINGING)
setCallInProgressNotification(TYPE_OUTGOING_RINGING, callManager.recipient)
callManager.insertCallMessage(recipient.address.serialize(), CallMessageType.CALL_OUTGOING)
timeoutExecutor.schedule(TimeoutRunnable(callId, this), TIMEOUT_SECONDS, TimeUnit.SECONDS)
callManager.setAudioEnabled(true)
callManager.postViewModelState(CallViewModel.State.CALL_OUTGOING)
lockManager.updatePhoneState(LockManager.PhoneState.IN_CALL)
callManager.initializeAudioForCall()
callManager.startOutgoingRinger(OutgoingRinger.Type.RINGING)
setCallInProgressNotification(TYPE_OUTGOING_RINGING, callManager.recipient)
callManager.insertCallMessage(recipient.address.serialize(), CallMessageType.CALL_OUTGOING)
timeoutExecutor.schedule(TimeoutRunnable(callId, this), TIMEOUT_SECONDS, TimeUnit.SECONDS)
callManager.setAudioEnabled(true)
val expectedState = callManager.currentConnectionState
val expectedCallId = callManager.callId
val expectedState = callManager.currentConnectionState
val expectedCallId = callManager.callId
try {
val offerFuture = callManager.onOutgoingCall(this)
offerFuture.fail { e ->
if (isConsistentState(expectedState, expectedCallId, callManager.currentConnectionState, callManager.callId)) {
Log.e(TAG,e)
callManager.postViewModelState(CallViewModel.State.NETWORK_FAILURE)
terminate()
try {
val offerFuture = callManager.onOutgoingCall(this)
offerFuture.fail { e ->
if (isConsistentState(expectedState, expectedCallId, callManager.currentConnectionState, callManager.callId)) {
Log.e(TAG,e)
callManager.postViewModelState(CallViewModel.State.NETWORK_FAILURE)
callManager.postConnectionError()
terminate()
}
}
} catch (e: Exception) {
Log.e(TAG,e)
callManager.postConnectionError()
terminate()
}
} catch (e: Exception) {
Log.e(TAG,e)
terminate()
}
}
private fun handleAnswerCall(intent: Intent) {
val recipient = callManager.recipient ?: return
if (callManager.currentConnectionState != STATE_LOCAL_RINGING) {
if (callManager.currentConnectionState == STATE_PRE_OFFER) {
// show answer state from pre-offer
setCallInProgressNotification(TYPE_INCOMING_CONNECTING, recipient)
}
Log.e(TAG, "Can only answer from ringing!")
return
}
val pending = callManager.pendingOffer ?: return
val callId = callManager.callId ?: return
val timestamp = callManager.pendingOfferTime
setCallInProgressNotification(TYPE_INCOMING_CONNECTING, recipient)
if (callManager.currentConnectionState != CallState.RemoteRing) {
Log.e(TAG, "Can only answer from ringing!")
return
}
intent.putExtra(EXTRA_CALL_ID, callId)
intent.putExtra(EXTRA_RECIPIENT_ADDRESS, recipient.address)
intent.putExtra(EXTRA_REMOTE_DESCRIPTION, pending)
intent.putExtra(EXTRA_TIMESTAMP, timestamp)
callManager.silenceIncomingRinger()
callManager.postConnectionEvent(STATE_ANSWERING)
callManager.postViewModelState(CallViewModel.State.CALL_INCOMING)
if (isIncomingMessageExpired(intent)) {
insertMissedCall(recipient, true)
terminate()
return
val didHangup = callManager.postConnectionEvent(Event.TimeOut) {
insertMissedCall(recipient, true)
terminate()
}
if (didHangup) {
return
}
}
timeoutExecutor.schedule(TimeoutRunnable(callId, this), TIMEOUT_SECONDS, TimeUnit.SECONDS)
callManager.postConnectionEvent(Event.SendAnswer) {
setCallInProgressNotification(TYPE_INCOMING_CONNECTING, recipient)
callManager.initializeAudioForCall()
callManager.initializeVideo(this)
callManager.silenceIncomingRinger()
callManager.postViewModelState(CallViewModel.State.CALL_INCOMING)
val expectedState = callManager.currentConnectionState
val expectedCallId = callManager.callId
timeoutExecutor.schedule(TimeoutRunnable(callId, this), TIMEOUT_SECONDS, TimeUnit.SECONDS)
try {
val answerFuture = callManager.onIncomingCall(this)
answerFuture.fail { e ->
if (isConsistentState(expectedState,expectedCallId, callManager.currentConnectionState, callManager.callId)) {
Log.e(TAG, e)
insertMissedCall(recipient, true)
terminate()
callManager.initializeAudioForCall()
callManager.initializeVideo(this)
val expectedState = callManager.currentConnectionState
val expectedCallId = callManager.callId
try {
val answerFuture = callManager.onIncomingCall(this)
answerFuture.fail { e ->
if (isConsistentState(expectedState,expectedCallId, callManager.currentConnectionState, callManager.callId)) {
Log.e(TAG, e)
insertMissedCall(recipient, true)
callManager.postConnectionError()
terminate()
}
}
lockManager.updatePhoneState(LockManager.PhoneState.PROCESSING)
callManager.setAudioEnabled(true)
} catch (e: Exception) {
Log.e(TAG,e)
callManager.postConnectionError()
terminate()
}
lockManager.updatePhoneState(LockManager.PhoneState.PROCESSING)
callManager.setAudioEnabled(true)
} catch (e: Exception) {
Log.e(TAG,e)
terminate()
}
}
private fun handleDenyCall(intent: Intent) {
if (callManager.currentConnectionState !in CallState.CAN_DECLINE_STATES) {
Log.e(TAG,"Can only deny from ringing!")
return
}
callManager.handleDenyCall()
terminate()
}
@ -509,7 +536,7 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
private fun handleResponseMessage(intent: Intent) {
try {
val recipient = getRemoteRecipient(intent)
if (callManager.isCurrentUser(recipient) && callManager.currentConnectionState == STATE_LOCAL_RINGING) {
if (callManager.isCurrentUser(recipient) && callManager.currentConnectionState in CallState.OUTGOING_STATES) {
handleLocalHangup(intent)
return
}
@ -542,22 +569,20 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
private fun handleIceConnected(intent: Intent) {
val recipient = callManager.recipient ?: return
if (callManager.currentConnectionState in arrayOf(STATE_ANSWERING)) {
callManager.postConnectionEvent(STATE_CONNECTED)
val connected = callManager.postConnectionEvent(Event.Connect) {
callManager.postViewModelState(CallViewModel.State.CALL_CONNECTED)
} else {
Log.w(TAG, "Got ice connected out of state")
setCallInProgressNotification(TYPE_ESTABLISHED, recipient)
callManager.startCommunication(lockManager)
}
if (!connected) {
terminate()
}
setCallInProgressNotification(TYPE_ESTABLISHED, recipient)
callManager.startCommunication(lockManager)
}
private fun handleIsInCallQuery(intent: Intent) {
val listener = intent.getParcelableExtra<ResultReceiver>(EXTRA_RESULT_RECEIVER) ?: return
val currentState = callManager.currentConnectionState
val isInCall = if (currentState in CONNECTED_STATES || currentState in PENDING_CONNECTION_STATES) 1 else 0
val isInCall = if (currentState in arrayOf(*CallState.PENDING_CONNECTION_STATES, CallState.Connected)) 1 else 0
listener.send(isInCall, bundleOf())
}
@ -569,11 +594,32 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
}
}
private fun handleCheckReconnect(intent: Intent) {
val callId = callManager.callId ?: return
val numTimeouts = ++currentTimeouts
if (callId == getCallId(intent) && isNetworkAvailable && numTimeouts <= 5) {
callManager.networkReestablished()
}
}
private fun handleCheckReconnectTimeout(intent: Intent) {
val callId = callManager.callId ?: return
val callState = callManager.currentConnectionState
if (callId == getCallId(intent) && (callState !in arrayOf(CallState.Connected, CallState.Connecting))) {
Log.w(TAG, "Timing out reconnect: $callId")
handleLocalHangup(intent)
}
}
private fun handleCheckTimeout(intent: Intent) {
val callId = callManager.callId ?: return
val callState = callManager.currentConnectionState
if (callId == getCallId(intent) && (callState !in arrayOf(STATE_CONNECTED) || callManager.iceState == CHECKING)) {
if (callId == getCallId(intent) && (callState !in arrayOf(CallState.Connected, CallState.Connecting))) {
Log.w(TAG, "Timing out call: $callId")
handleLocalHangup(intent)
}
@ -622,9 +668,7 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
callReceiver?.let { receiver ->
unregisterReceiver(receiver)
}
networkChangedReceiver?.let { receiver ->
receiver.unregister(this)
}
networkChangedReceiver?.unregister(this)
wantsToAnswerReceiver?.let { receiver ->
LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver)
}
@ -632,12 +676,33 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
callReceiver = null
uncaughtExceptionHandlerManager?.unregister()
wantsToAnswer = false
currentTimeouts = 0
isNetworkAvailable = true
super.onDestroy()
}
fun networkChange(networkAvailable: Boolean) {
if (networkAvailable && !callManager.isReestablishing && callManager.currentConnectionState in arrayOf(STATE_CONNECTED)) {
callManager.networkReestablished()
isNetworkAvailable = networkAvailable
if (networkAvailable && !callManager.isReestablishing && callManager.currentConnectionState == CallState.Connected) {
Log.d("Loki", "Should reconnected")
}
}
private class CheckReconnectedRunnable(private val callId: UUID, private val context: Context): Runnable {
override fun run() {
val intent = Intent(context, WebRtcCallService::class.java)
.setAction(ACTION_CHECK_RECONNECT)
.putExtra(EXTRA_CALL_ID, callId)
context.startService(intent)
}
}
private class ReconnectTimeoutRunnable(private val callId: UUID, private val context: Context): Runnable {
override fun run() {
val intent = Intent(context, WebRtcCallService::class.java)
.setAction(ACTION_CHECK_RECONNECT_TIMEOUT)
.putExtra(EXTRA_CALL_ID, callId)
context.startService(intent)
}
}
@ -651,16 +716,16 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
}
private abstract class FailureListener<V>(
expectedState: CallManager.CallState,
expectedState: CallState,
expectedCallId: UUID?,
getState: () -> Pair<CallManager.CallState, UUID?>): StateAwareListener<V>(expectedState, expectedCallId, getState) {
getState: () -> Pair<CallState, UUID?>): StateAwareListener<V>(expectedState, expectedCallId, getState) {
override fun onSuccessContinue(result: V) {}
}
private abstract class SuccessOnlyListener<V>(
expectedState: CallManager.CallState,
expectedState: CallState,
expectedCallId: UUID?,
getState: () -> Pair<CallManager.CallState, UUID>): StateAwareListener<V>(expectedState, expectedCallId, getState) {
getState: () -> Pair<CallState, UUID>): StateAwareListener<V>(expectedState, expectedCallId, getState) {
override fun onFailureContinue(throwable: Throwable?) {
Log.e(TAG, throwable)
throw AssertionError(throwable)
@ -668,9 +733,9 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
}
private abstract class StateAwareListener<V>(
private val expectedState: CallManager.CallState,
private val expectedState: CallState,
private val expectedCallId: UUID?,
private val getState: ()->Pair<CallManager.CallState, UUID?>): FutureTaskListener<V> {
private val getState: ()->Pair<CallState, UUID?>): FutureTaskListener<V> {
companion object {
private val TAG = Log.tag(StateAwareListener::class.java)
@ -706,9 +771,9 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
}
private fun isConsistentState(
expectedState: CallManager.CallState,
expectedState: CallState,
expectedCallId: UUID?,
currentState: CallManager.CallState,
currentState: CallState,
currentCallId: UUID?
): Boolean {
return expectedState == currentState && expectedCallId == currentCallId
@ -717,16 +782,22 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
override fun onSignalingChange(p0: PeerConnection.SignalingState?) {}
override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState?) {
if (newState in arrayOf(CONNECTED, COMPLETED)) {
if (newState == CONNECTED) {
val intent = Intent(this, WebRtcCallService::class.java)
.setAction(ACTION_ICE_CONNECTED)
startService(intent)
} else if (newState == FAILED) {
val intent = Intent(this, WebRtcCallService::class.java)
.setAction(ACTION_LOCAL_HANGUP)
.putExtra(EXTRA_CALL_ID, callManager.callId)
val intent = hangupIntent(this)
startService(intent)
} else if (newState == DISCONNECTED) {
callManager.callId?.let { callId ->
callManager.postViewModelState(CallViewModel.State.CALL_RECONNECTING)
timeoutExecutor.schedule(CheckReconnectedRunnable(callId, this), 5, TimeUnit.SECONDS)
timeoutExecutor.schedule(ReconnectTimeoutRunnable(callId, this), TIMEOUT_SECONDS, TimeUnit.SECONDS)
} ?: run {
val intent = hangupIntent(this)
startService(intent)
}
}
Log.d(TAG, "onIceConnectionChange: $newState")
}

View File

@ -24,13 +24,13 @@ import org.session.libsignal.protos.SignalServiceProtos.CallMessage.Type.ICE_CAN
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.webrtc.CallManager.StateEvent.AudioDeviceUpdate
import org.thoughtcrime.securesms.webrtc.CallManager.StateEvent.AudioEnabled
import org.thoughtcrime.securesms.webrtc.CallManager.StateEvent.CallStateUpdate
import org.thoughtcrime.securesms.webrtc.CallManager.StateEvent.RecipientUpdate
import org.thoughtcrime.securesms.webrtc.CallManager.StateEvent.VideoEnabled
import org.thoughtcrime.securesms.webrtc.audio.AudioManagerCompat
import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager.AudioDevice
import org.thoughtcrime.securesms.webrtc.data.Event
import org.thoughtcrime.securesms.webrtc.data.StateProcessor
import org.thoughtcrime.securesms.webrtc.locks.LockManager
import org.thoughtcrime.securesms.webrtc.video.CameraEventListener
@ -163,8 +163,12 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
signalAudioManager.handleCommand(AudioManagerCommand.SilenceIncomingRinger)
}
fun postConnectionEvent(newState: CallState) {
_connectionEvents.value = CallStateUpdate(newState)
fun postConnectionEvent(transition: Event, onSuccess: ()->Unit): Boolean {
return stateProcessor.processEvent(transition, onSuccess)
}
fun postConnectionError(): Boolean {
return stateProcessor.processEvent(Event.Error)
}
fun postViewModelState(newState: CallViewModel.State) {
@ -221,7 +225,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
}
fun setAudioEnabled(isEnabled: Boolean) {
currentConnectionState.withState(*PENDING_CONNECTION_STATES)) {
currentConnectionState.withState(*CallState.CAN_HANGUP_STATES) {
peerConnection?.setAudioEnabled(isEnabled)
_audioEvents.value = AudioEnabled(true)
}
@ -345,51 +349,50 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
_audioDeviceEvents.value = AudioDeviceUpdate(activeDevice, devices)
}
private fun CallState.withState(vararg expected: CallState, transition: () -> Unit) {
if (this in expected) transition()
else Log.w(TAG,"Tried to transition state $this but expected $expected")
}
fun stop() {
signalAudioManager.handleCommand(AudioManagerCommand.Stop(currentConnectionState in OUTGOING_STATES))
peerConnection?.dispose()
peerConnection = null
val isOutgoing = currentConnectionState in CallState.OUTGOING_STATES
stateProcessor.processEvent(Event.Cleanup) {
signalAudioManager.handleCommand(AudioManagerCommand.Stop(isOutgoing))
peerConnection?.dispose()
peerConnection = null
localRenderer?.release()
remoteRotationSink?.release()
remoteRenderer?.release()
eglBase?.release()
localRenderer?.release()
remoteRotationSink?.release()
remoteRenderer?.release()
eglBase?.release()
localRenderer = null
remoteRenderer = null
eglBase = null
localRenderer = null
remoteRenderer = null
eglBase = null
_connectionEvents.value = CallStateUpdate(CallState.STATE_IDLE)
localCameraState = CameraState.UNKNOWN
recipient = null
callId = null
pendingOfferTime = -1
pendingOffer = null
callStartTime = -1
_audioEvents.value = AudioEnabled(false)
_videoEvents.value = VideoEnabled(false)
_remoteVideoEvents.value = VideoEnabled(false)
pendingOutgoingIceUpdates.clear()
pendingIncomingIceUpdates.clear()
localCameraState = CameraState.UNKNOWN
recipient = null
callId = null
pendingOfferTime = -1
pendingOffer = null
callStartTime = -1
_audioEvents.value = AudioEnabled(false)
_videoEvents.value = VideoEnabled(false)
_remoteVideoEvents.value = VideoEnabled(false)
pendingOutgoingIceUpdates.clear()
pendingIncomingIceUpdates.clear()
}
}
override fun onCameraSwitchCompleted(newCameraState: CameraState) {
localCameraState = newCameraState
}
fun onPreOffer(callId: UUID, recipient: Recipient, sentTimestamp: Long) {
if (preOfferCallData != null) {
Log.d(TAG, "Received new pre-offer when we are already expecting one")
fun onPreOffer(callId: UUID, recipient: Recipient, onSuccess: () -> Unit) {
stateProcessor.processEvent(Event.ReceivePreOffer) {
if (preOfferCallData != null) {
Log.d(TAG, "Received new pre-offer when we are already expecting one")
}
this.recipient = recipient
this.callId = callId
preOfferCallData = PreOffer(callId, recipient)
onSuccess()
}
this.recipient = recipient
this.callId = callId
preOfferCallData = PreOffer(callId, recipient)
postConnectionEvent(CallState.STATE_PRE_OFFER)
}
fun onNewOffer(offer: String, callId: UUID, recipient: Recipient): Promise<Unit, Exception> {
@ -407,15 +410,16 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
return MessageSender.sendNonDurably(answerMessage, recipient.address)
}
fun onIncomingRing(offer: String, callId: UUID, recipient: Recipient, callTime: Long) {
if (currentConnectionState !in arrayOf(CallState.STATE_IDLE, CallState.STATE_PRE_OFFER)) return
this.callId = callId
this.recipient = recipient
this.pendingOffer = offer
this.pendingOfferTime = callTime
initializeAudioForCall()
startIncomingRinger()
fun onIncomingRing(offer: String, callId: UUID, recipient: Recipient, callTime: Long, onSuccess: () -> Unit) {
postConnectionEvent(Event.ReceiveOffer) {
this.callId = callId
this.recipient = recipient
this.pendingOffer = offer
this.pendingOfferTime = callTime
initializeAudioForCall()
startIncomingRinger()
onSuccess()
}
}
fun onIncomingCall(context: Context, isAlwaysTurn: Boolean = false): Promise<Unit, Exception> {
@ -472,7 +476,12 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
?: return Promise.ofFail(NullPointerException("localRenderer is null"))
val base = eglBase ?: return Promise.ofFail(NullPointerException("eglBase is null"))
val connection = PeerConnectionWrapper(
val sentOffer = stateProcessor.processEvent(Event.SendOffer)
if (!sentOffer) {
return Promise.ofFail(Exception("Couldn't transition to sent offer state"))
} else {
val connection = PeerConnectionWrapper(
context,
factory,
this,
@ -480,23 +489,24 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
this,
base,
isAlwaysTurn
)
)
peerConnection = connection
localCameraState = connection.getCameraState()
val dataChannel = connection.createDataChannel(DATA_CHANNEL_NAME)
dataChannel.registerObserver(this)
this.dataChannel = dataChannel
val offer = connection.createOffer(MediaConstraints())
connection.setLocalDescription(offer)
peerConnection = connection
localCameraState = connection.getCameraState()
val dataChannel = connection.createDataChannel(DATA_CHANNEL_NAME)
dataChannel.registerObserver(this)
this.dataChannel = dataChannel
val offer = connection.createOffer(MediaConstraints())
connection.setLocalDescription(offer)
return MessageSender.sendNonDurably(CallMessage.preOffer(
return MessageSender.sendNonDurably(CallMessage.preOffer(
callId
), recipient.address).bind {
MessageSender.sendNonDurably(CallMessage.offer(
), recipient.address).bind {
MessageSender.sendNonDurably(CallMessage.offer(
offer.description,
callId
), recipient.address)
), recipient.address)
}
}
}
@ -507,9 +517,11 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
val callId = callId ?: return
val recipient = recipient ?: return
val userAddress = storage.getUserPublicKey() ?: return
MessageSender.sendNonDurably(CallMessage.endCall(callId), Address.fromSerialized(userAddress))
MessageSender.sendNonDurably(CallMessage.endCall(callId), recipient.address)
insertCallMessage(recipient.address.serialize(), CallMessageType.CALL_MISSED)
stateProcessor.processEvent(Event.DeclineCall) {
MessageSender.sendNonDurably(CallMessage.endCall(callId), Address.fromSerialized(userAddress))
MessageSender.sendNonDurably(CallMessage.endCall(callId), recipient.address)
insertCallMessage(recipient.address.serialize(), CallMessageType.CALL_MISSED)
}
}
fun handleLocalHangup(intentRecipient: Recipient?) {
@ -520,6 +532,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
val sendHangup = intentRecipient == null || (intentRecipient == recipient && recipient.address.serialize() != currentUserPublicKey)
postViewModelState(CallViewModel.State.CALL_DISCONNECTED)
stateProcessor.processEvent(Event.Hangup)
if (sendHangup) {
dataChannel?.let { channel ->
val buffer = DataChannel.Buffer(ByteBuffer.wrap(HANGUP_JSON.toString().encodeToByteArray()), false)
@ -535,8 +548,8 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
fun handleRemoteHangup() {
when (currentConnectionState) {
CallState.STATE_DIALING,
CallState.STATE_REMOTE_RINGING -> postViewModelState(CallViewModel.State.RECIPIENT_UNAVAILABLE)
CallState.LocalRing,
CallState.RemoteRing -> postViewModelState(CallViewModel.State.RECIPIENT_UNAVAILABLE)
else -> postViewModelState(CallViewModel.State.CALL_DISCONNECTED)
}
}
@ -556,7 +569,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
channel.send(buffer)
}
if (currentConnectionState == CallState.STATE_CONNECTED) {
if (currentConnectionState == CallState.Connected) {
if (connection.isVideoEnabled()) lockManager.updatePhoneState(LockManager.PhoneState.IN_VIDEO)
else lockManager.updatePhoneState(LockManager.PhoneState.IN_CALL)
}
@ -585,9 +598,9 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
}
fun handleWiredHeadsetChanged(present: Boolean) {
if (currentConnectionState in arrayOf(CallState.STATE_CONNECTED,
CallState.STATE_DIALING,
CallState.STATE_REMOTE_RINGING)) {
if (currentConnectionState in arrayOf(CallState.Connected,
CallState.LocalRing,
CallState.RemoteRing)) {
if (present && signalAudioManager.isSpeakerphoneOn()) {
signalAudioManager.handleCommand(AudioManagerCommand.SetUserDevice(AudioDevice.WIRED_HEADSET))
} else if (!present && !signalAudioManager.isSpeakerphoneOn() && !signalAudioManager.isBluetoothScoOn() && localCameraState.enabled) {
@ -597,24 +610,26 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
}
fun handleScreenOffChange() {
if (currentConnectionState in arrayOf(CallState.STATE_ANSWERING, CallState.STATE_LOCAL_RINGING)) {
if (currentConnectionState in arrayOf(CallState.Connecting, CallState.LocalRing)) {
signalAudioManager.handleCommand(AudioManagerCommand.SilenceIncomingRinger)
}
}
fun handleResponseMessage(recipient: Recipient, callId: UUID, answer: SessionDescription) {
if (currentConnectionState !in arrayOf(CallState.STATE_DIALING, CallState.STATE_CONNECTED) || recipient != this.recipient || callId != this.callId) {
if (recipient != this.recipient || callId != this.callId) {
Log.w(TAG,"Got answer for recipient and call ID we're not currently dialing")
return
}
val connection = peerConnection ?: throw AssertionError("assert")
stateProcessor.processEvent(Event.ReceiveAnswer) {
val connection = peerConnection ?: throw AssertionError("assert")
connection.setRemoteDescription(answer)
while (pendingIncomingIceUpdates.isNotEmpty()) {
connection.addIceCandidate(pendingIncomingIceUpdates.pop())
connection.setRemoteDescription(answer)
while (pendingIncomingIceUpdates.isNotEmpty()) {
connection.addIceCandidate(pendingIncomingIceUpdates.pop())
}
queueOutgoingIce(callId, recipient)
}
queueOutgoingIce(callId, recipient)
}
fun handleRemoteIceCandidate(iceCandidates: List<IceCandidate>, callId: UUID) {

View File

@ -21,6 +21,7 @@ class CallViewModel @Inject constructor(private val callManager: CallManager): V
CALL_RINGING,
CALL_BUSY,
CALL_DISCONNECTED,
CALL_RECONNECTING,
NETWORK_FAILURE,
RECIPIENT_UNAVAILABLE,

View File

@ -1,5 +1,7 @@
package org.thoughtcrime.securesms.webrtc.data
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.webrtc.data.State.Companion.CAN_DECLINE_STATES
import org.thoughtcrime.securesms.webrtc.data.State.Companion.CAN_HANGUP_STATES
sealed class State {
@ -13,6 +15,10 @@ sealed class State {
object Reconnecting : State()
object Disconnected : State()
companion object {
val ALL_STATES = arrayOf(Idle, RemotePreOffer, RemoteRing, LocalPreOffer, LocalRing,
Connecting, Connected, Reconnecting, Disconnected)
val CAN_DECLINE_STATES = arrayOf(RemotePreOffer, RemoteRing)
val PENDING_CONNECTION_STATES = arrayOf(
LocalPreOffer,
@ -26,10 +32,17 @@ sealed class State {
LocalRing,
)
val CAN_HANGUP_STATES =
arrayOf(LocalPreOffer, LocalRing, Connecting, Connected, Reconnecting)
arrayOf(RemotePreOffer, RemoteRing, LocalPreOffer, LocalRing, Connecting, Connected, Reconnecting)
val CAN_RECEIVE_ICE_STATES =
arrayOf(RemoteRing, LocalRing, Connecting, Connected, Reconnecting)
}
fun withState(vararg expectedState: State, body: ()->Unit) {
if (this in expectedState) {
body()
}
}
}
sealed class Event(vararg val expectedStates: State, val outputState: State) {
@ -47,7 +60,8 @@ sealed class Event(vararg val expectedStates: State, val outputState: State) {
object NetworkReconnect : Event(State.Reconnecting, outputState = State.Connecting)
object TimeOut :
Event(State.Connecting, State.LocalRing, State.RemoteRing, outputState = State.Disconnected)
object Error : Event(*State.ALL_STATES, outputState = State.Disconnected)
object DeclineCall : Event(*CAN_DECLINE_STATES, outputState = State.Disconnected)
object Hangup : Event(*CAN_HANGUP_STATES, outputState = State.Disconnected)
object Cleanup : Event(State.Disconnected, outputState = State.Idle)
}
@ -62,6 +76,7 @@ open class StateProcessor(initialState: State) {
sideEffect()
return true
}
Log.e("Loki-Call", "error transitioning from $currentState with ${event::class.simpleName}")
return false
}
}

View File

@ -1,8 +1,13 @@
package org.thoughtcrime.securesms.calls
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.mockito.MockedStatic
import org.mockito.Mockito.any
import org.mockito.Mockito.mockStatic
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.webrtc.data.Event
import org.thoughtcrime.securesms.webrtc.data.State
@ -10,9 +15,19 @@ class CallStateMachineTests {
private lateinit var stateProcessor: TestStateProcessor
lateinit var mock: MockedStatic<Log>
@Before
fun setup() {
stateProcessor = TestStateProcessor(State.Idle)
mock = mockStatic(Log::class.java).apply {
`when`<Unit> { Log.e(any(), any(), any()) }.then { /* do nothing */ }
}
}
@After
fun teardown() {
mock.close()
}
@Test
@ -119,6 +134,10 @@ class CallStateMachineTests {
Event.ReceiveOffer,
Event.SendAnswer,
Event.IceFailed,
Event.Cleanup,
Event.ReceivePreOffer,
Event.ReceiveOffer,
Event.DeclineCall,
Event.Cleanup
)

View File

@ -1,20 +1,22 @@
package org.thoughtcrime.securesms.database;
import android.content.Context;
import android.database.Cursor;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import android.view.View;
import android.view.ViewGroup;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.database.Cursor;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import org.junit.Before;
import org.junit.Test;
public class CursorRecyclerViewAdapterTest {
private CursorRecyclerViewAdapter adapter;
private Context context;