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_PRE_OFFER
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_INCOMING_RINGING import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_INCOMING_RINGING
import org.thoughtcrime.securesms.util.CallNotificationBuilder.Companion.TYPE_OUTGOING_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.audio.OutgoingRinger
import org.thoughtcrime.securesms.webrtc.data.Event
import org.thoughtcrime.securesms.webrtc.locks.LockManager import org.thoughtcrime.securesms.webrtc.locks.LockManager
import org.webrtc.* import org.webrtc.DataChannel
import org.webrtc.PeerConnection.IceConnectionState.* import org.webrtc.IceCandidate
import java.util.* 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.ExecutionException
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit 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_WIRED_HEADSET_CHANGE = "WIRED_HEADSET_CHANGE"
const val ACTION_SCREEN_OFF = "SCREEN_OFF" const val ACTION_SCREEN_OFF = "SCREEN_OFF"
const val ACTION_CHECK_TIMEOUT = "CHECK_TIMEOUT" 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_IS_IN_CALL_QUERY = "IS_IN_CALL"
const val ACTION_WANTS_TO_ANSWER = "WANTS_TO_ANSWER" 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 EXTRA_WANTS_TO_ANSWER = "wants_to_answer"
const val INVALID_NOTIFICATION_ID = -1 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) fun cameraEnabled(context: Context, enabled: Boolean) = Intent(context, WebRtcCallService::class.java)
.setAction(ACTION_SET_MUTE_VIDEO) .setAction(ACTION_SET_MUTE_VIDEO)
@@ -165,6 +186,8 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
@Inject lateinit var callManager: CallManager @Inject lateinit var callManager: CallManager
private var wantsToAnswer = false private var wantsToAnswer = false
private var currentTimeouts = 0
private var isNetworkAvailable = true
private val lockManager by lazy { LockManager(this) } private val lockManager by lazy { LockManager(this) }
private val serviceExecutor = Executors.newSingleThreadExecutor() private val serviceExecutor = Executors.newSingleThreadExecutor()
@@ -185,6 +208,9 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
LocalBroadcastManager.getInstance(this).sendBroadcast(Intent(WebRtcCallActivity.ACTION_END)) LocalBroadcastManager.getInstance(this).sendBroadcast(Intent(WebRtcCallActivity.ACTION_END))
lockManager.updatePhoneState(LockManager.PhoneState.IDLE) lockManager.updatePhoneState(LockManager.PhoneState.IDLE)
callManager.stop() callManager.stop()
wantsToAnswer = false
currentTimeouts = 0
isNetworkAvailable = true
stopForeground(true) stopForeground(true)
} }
@@ -239,6 +265,8 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
action == ACTION_ICE_MESSAGE -> handleRemoteIceCandidate(intent) action == ACTION_ICE_MESSAGE -> handleRemoteIceCandidate(intent)
action == ACTION_ICE_CONNECTED -> handleIceConnected(intent) action == ACTION_ICE_CONNECTED -> handleIceConnected(intent)
action == ACTION_CHECK_TIMEOUT -> handleCheckTimeout(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_IS_IN_CALL_QUERY -> handleIsInCallQuery(intent)
action == ACTION_UPDATE_AUDIO -> handleUpdateAudio(intent) action == ACTION_UPDATE_AUDIO -> handleUpdateAudio(intent)
} }
@@ -250,9 +278,10 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
super.onCreate() super.onCreate()
callManager.registerListener(this) callManager.registerListener(this)
wantsToAnswer = false wantsToAnswer = false
isNetworkAvailable = false
registerIncomingPstnCallReceiver() registerIncomingPstnCallReceiver()
registerWiredHeadsetStateReceiver() registerWiredHeadsetStateReceiver()
registerWantsToAnswerReceiver() // TODO unregister registerWantsToAnswerReceiver()
getSystemService(TelephonyManager::class.java) getSystemService(TelephonyManager::class.java)
.listen(hangupOnCallAnswered, PhoneStateListener.LISTEN_CALL_STATE) .listen(hangupOnCallAnswered, PhoneStateListener.LISTEN_CALL_STATE)
registerUncaughtExceptionHandler() registerUncaughtExceptionHandler()
@@ -322,7 +351,6 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
} }
val callId = getCallId(intent) val callId = getCallId(intent)
val recipient = getRemoteRecipient(intent) val recipient = getRemoteRecipient(intent)
val sentTimestamp = intent.getLongExtra(EXTRA_TIMESTAMP, -1)
if (isIncomingMessageExpired(intent)) { if (isIncomingMessageExpired(intent)) {
insertMissedCall(recipient, true) insertMissedCall(recipient, true)
@@ -330,17 +358,16 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
return return
} }
setCallInProgressNotification(TYPE_INCOMING_PRE_OFFER, recipient) callManager.onPreOffer(callId, recipient) {
callManager.onPreOffer(callId, recipient, sentTimestamp) setCallInProgressNotification(TYPE_INCOMING_PRE_OFFER, recipient)
callManager.postViewModelState(CallViewModel.State.CALL_PRE_INIT) callManager.postViewModelState(CallViewModel.State.CALL_PRE_INIT)
callManager.initializeAudioForCall() callManager.initializeAudioForCall()
callManager.startIncomingRinger() callManager.startIncomingRinger()
callManager.setAudioEnabled(true) callManager.setAudioEnabled(true)
}
} }
private fun handleIncomingRing(intent: Intent) { private fun handleIncomingRing(intent: Intent) {
if (!callManager.isPreOffer() && !callManager.isIdle()) throw IllegalStateException("Incoming ring on non-idle")
val callId = getCallId(intent) val callId = getCallId(intent)
val recipient = getRemoteRecipient(intent) val recipient = getRemoteRecipient(intent)
val preOffer = callManager.preOfferCallData val preOffer = callManager.preOfferCallData
@@ -352,119 +379,119 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
val offer = intent.getStringExtra(EXTRA_REMOTE_DESCRIPTION) ?: return val offer = intent.getStringExtra(EXTRA_REMOTE_DESCRIPTION) ?: return
val timestamp = intent.getLongExtra(EXTRA_TIMESTAMP, -1) val timestamp = intent.getLongExtra(EXTRA_TIMESTAMP, -1)
if (wantsToAnswer) {
setCallInProgressNotification(TYPE_INCOMING_CONNECTING, recipient) callManager.onIncomingRing(offer, callId, recipient, timestamp) {
} else { if (wantsToAnswer) {
setCallInProgressNotification(TYPE_INCOMING_RINGING, recipient) 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) { 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) callManager.initializeVideo(this)
val recipient = getRemoteRecipient(intent)
callManager.recipient = recipient
val callId = UUID.randomUUID()
callManager.callId = callId
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) val expectedState = callManager.currentConnectionState
lockManager.updatePhoneState(LockManager.PhoneState.IN_CALL) val expectedCallId = callManager.callId
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 try {
val expectedCallId = callManager.callId val offerFuture = callManager.onOutgoingCall(this)
offerFuture.fail { e ->
try { if (isConsistentState(expectedState, expectedCallId, callManager.currentConnectionState, callManager.callId)) {
val offerFuture = callManager.onOutgoingCall(this) Log.e(TAG,e)
offerFuture.fail { e -> callManager.postViewModelState(CallViewModel.State.NETWORK_FAILURE)
if (isConsistentState(expectedState, expectedCallId, callManager.currentConnectionState, callManager.callId)) { callManager.postConnectionError()
Log.e(TAG,e) terminate()
callManager.postViewModelState(CallViewModel.State.NETWORK_FAILURE) }
terminate()
} }
} catch (e: Exception) {
Log.e(TAG,e)
callManager.postConnectionError()
terminate()
} }
} catch (e: Exception) {
Log.e(TAG,e)
terminate()
} }
} }
private fun handleAnswerCall(intent: Intent) { private fun handleAnswerCall(intent: Intent) {
val recipient = callManager.recipient ?: return 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 pending = callManager.pendingOffer ?: return
val callId = callManager.callId ?: return val callId = callManager.callId ?: return
val timestamp = callManager.pendingOfferTime 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_CALL_ID, callId)
intent.putExtra(EXTRA_RECIPIENT_ADDRESS, recipient.address) intent.putExtra(EXTRA_RECIPIENT_ADDRESS, recipient.address)
intent.putExtra(EXTRA_REMOTE_DESCRIPTION, pending) intent.putExtra(EXTRA_REMOTE_DESCRIPTION, pending)
intent.putExtra(EXTRA_TIMESTAMP, timestamp) intent.putExtra(EXTRA_TIMESTAMP, timestamp)
callManager.silenceIncomingRinger()
callManager.postConnectionEvent(STATE_ANSWERING)
callManager.postViewModelState(CallViewModel.State.CALL_INCOMING)
if (isIncomingMessageExpired(intent)) { if (isIncomingMessageExpired(intent)) {
insertMissedCall(recipient, true) val didHangup = callManager.postConnectionEvent(Event.TimeOut) {
terminate() insertMissedCall(recipient, true)
return 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.silenceIncomingRinger()
callManager.initializeVideo(this) callManager.postViewModelState(CallViewModel.State.CALL_INCOMING)
val expectedState = callManager.currentConnectionState timeoutExecutor.schedule(TimeoutRunnable(callId, this), TIMEOUT_SECONDS, TimeUnit.SECONDS)
val expectedCallId = callManager.callId
try { callManager.initializeAudioForCall()
val answerFuture = callManager.onIncomingCall(this) callManager.initializeVideo(this)
answerFuture.fail { e ->
if (isConsistentState(expectedState,expectedCallId, callManager.currentConnectionState, callManager.callId)) { val expectedState = callManager.currentConnectionState
Log.e(TAG, e) val expectedCallId = callManager.callId
insertMissedCall(recipient, true)
terminate() 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) { private fun handleDenyCall(intent: Intent) {
if (callManager.currentConnectionState !in CallState.CAN_DECLINE_STATES) {
Log.e(TAG,"Can only deny from ringing!")
return
}
callManager.handleDenyCall() callManager.handleDenyCall()
terminate() terminate()
} }
@@ -509,7 +536,7 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
private fun handleResponseMessage(intent: Intent) { private fun handleResponseMessage(intent: Intent) {
try { try {
val recipient = getRemoteRecipient(intent) 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) handleLocalHangup(intent)
return return
} }
@@ -542,22 +569,20 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
private fun handleIceConnected(intent: Intent) { private fun handleIceConnected(intent: Intent) {
val recipient = callManager.recipient ?: return val recipient = callManager.recipient ?: return
if (callManager.currentConnectionState in arrayOf(STATE_ANSWERING)) { val connected = callManager.postConnectionEvent(Event.Connect) {
callManager.postConnectionEvent(STATE_CONNECTED)
callManager.postViewModelState(CallViewModel.State.CALL_CONNECTED) callManager.postViewModelState(CallViewModel.State.CALL_CONNECTED)
} else { setCallInProgressNotification(TYPE_ESTABLISHED, recipient)
Log.w(TAG, "Got ice connected out of state") callManager.startCommunication(lockManager)
}
if (!connected) {
terminate()
} }
setCallInProgressNotification(TYPE_ESTABLISHED, recipient)
callManager.startCommunication(lockManager)
} }
private fun handleIsInCallQuery(intent: Intent) { private fun handleIsInCallQuery(intent: Intent) {
val listener = intent.getParcelableExtra<ResultReceiver>(EXTRA_RESULT_RECEIVER) ?: return val listener = intent.getParcelableExtra<ResultReceiver>(EXTRA_RESULT_RECEIVER) ?: return
val currentState = callManager.currentConnectionState 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()) 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) { private fun handleCheckTimeout(intent: Intent) {
val callId = callManager.callId ?: return val callId = callManager.callId ?: return
val callState = callManager.currentConnectionState 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") Log.w(TAG, "Timing out call: $callId")
handleLocalHangup(intent) handleLocalHangup(intent)
} }
@@ -622,9 +668,7 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
callReceiver?.let { receiver -> callReceiver?.let { receiver ->
unregisterReceiver(receiver) unregisterReceiver(receiver)
} }
networkChangedReceiver?.let { receiver -> networkChangedReceiver?.unregister(this)
receiver.unregister(this)
}
wantsToAnswerReceiver?.let { receiver -> wantsToAnswerReceiver?.let { receiver ->
LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver) LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver)
} }
@@ -632,12 +676,33 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
callReceiver = null callReceiver = null
uncaughtExceptionHandlerManager?.unregister() uncaughtExceptionHandlerManager?.unregister()
wantsToAnswer = false wantsToAnswer = false
currentTimeouts = 0
isNetworkAvailable = true
super.onDestroy() super.onDestroy()
} }
fun networkChange(networkAvailable: Boolean) { fun networkChange(networkAvailable: Boolean) {
if (networkAvailable && !callManager.isReestablishing && callManager.currentConnectionState in arrayOf(STATE_CONNECTED)) { isNetworkAvailable = networkAvailable
callManager.networkReestablished() 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>( private abstract class FailureListener<V>(
expectedState: CallManager.CallState, expectedState: CallState,
expectedCallId: UUID?, 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) {} override fun onSuccessContinue(result: V) {}
} }
private abstract class SuccessOnlyListener<V>( private abstract class SuccessOnlyListener<V>(
expectedState: CallManager.CallState, expectedState: CallState,
expectedCallId: UUID?, 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?) { override fun onFailureContinue(throwable: Throwable?) {
Log.e(TAG, throwable) Log.e(TAG, throwable)
throw AssertionError(throwable) throw AssertionError(throwable)
@@ -668,9 +733,9 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
} }
private abstract class StateAwareListener<V>( private abstract class StateAwareListener<V>(
private val expectedState: CallManager.CallState, private val expectedState: CallState,
private val expectedCallId: UUID?, private val expectedCallId: UUID?,
private val getState: ()->Pair<CallManager.CallState, UUID?>): FutureTaskListener<V> { private val getState: ()->Pair<CallState, UUID?>): FutureTaskListener<V> {
companion object { companion object {
private val TAG = Log.tag(StateAwareListener::class.java) private val TAG = Log.tag(StateAwareListener::class.java)
@@ -706,9 +771,9 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
} }
private fun isConsistentState( private fun isConsistentState(
expectedState: CallManager.CallState, expectedState: CallState,
expectedCallId: UUID?, expectedCallId: UUID?,
currentState: CallManager.CallState, currentState: CallState,
currentCallId: UUID? currentCallId: UUID?
): Boolean { ): Boolean {
return expectedState == currentState && expectedCallId == currentCallId return expectedState == currentState && expectedCallId == currentCallId
@@ -717,16 +782,22 @@ class WebRtcCallService: Service(), CallManager.WebRtcListener {
override fun onSignalingChange(p0: PeerConnection.SignalingState?) {} override fun onSignalingChange(p0: PeerConnection.SignalingState?) {}
override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState?) { override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState?) {
if (newState in arrayOf(CONNECTED, COMPLETED)) { if (newState == CONNECTED) {
val intent = Intent(this, WebRtcCallService::class.java) val intent = Intent(this, WebRtcCallService::class.java)
.setAction(ACTION_ICE_CONNECTED) .setAction(ACTION_ICE_CONNECTED)
startService(intent) startService(intent)
} else if (newState == FAILED) { } else if (newState == FAILED) {
val intent = Intent(this, WebRtcCallService::class.java) val intent = hangupIntent(this)
.setAction(ACTION_LOCAL_HANGUP)
.putExtra(EXTRA_CALL_ID, callManager.callId)
startService(intent) 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") 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.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.webrtc.CallManager.StateEvent.AudioDeviceUpdate import org.thoughtcrime.securesms.webrtc.CallManager.StateEvent.AudioDeviceUpdate
import org.thoughtcrime.securesms.webrtc.CallManager.StateEvent.AudioEnabled 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.RecipientUpdate
import org.thoughtcrime.securesms.webrtc.CallManager.StateEvent.VideoEnabled import org.thoughtcrime.securesms.webrtc.CallManager.StateEvent.VideoEnabled
import org.thoughtcrime.securesms.webrtc.audio.AudioManagerCompat import org.thoughtcrime.securesms.webrtc.audio.AudioManagerCompat
import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger import org.thoughtcrime.securesms.webrtc.audio.OutgoingRinger
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager.AudioDevice 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.data.StateProcessor
import org.thoughtcrime.securesms.webrtc.locks.LockManager import org.thoughtcrime.securesms.webrtc.locks.LockManager
import org.thoughtcrime.securesms.webrtc.video.CameraEventListener import org.thoughtcrime.securesms.webrtc.video.CameraEventListener
@@ -163,8 +163,12 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
signalAudioManager.handleCommand(AudioManagerCommand.SilenceIncomingRinger) signalAudioManager.handleCommand(AudioManagerCommand.SilenceIncomingRinger)
} }
fun postConnectionEvent(newState: CallState) { fun postConnectionEvent(transition: Event, onSuccess: ()->Unit): Boolean {
_connectionEvents.value = CallStateUpdate(newState) return stateProcessor.processEvent(transition, onSuccess)
}
fun postConnectionError(): Boolean {
return stateProcessor.processEvent(Event.Error)
} }
fun postViewModelState(newState: CallViewModel.State) { fun postViewModelState(newState: CallViewModel.State) {
@@ -221,7 +225,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
} }
fun setAudioEnabled(isEnabled: Boolean) { fun setAudioEnabled(isEnabled: Boolean) {
currentConnectionState.withState(*PENDING_CONNECTION_STATES)) { currentConnectionState.withState(*CallState.CAN_HANGUP_STATES) {
peerConnection?.setAudioEnabled(isEnabled) peerConnection?.setAudioEnabled(isEnabled)
_audioEvents.value = AudioEnabled(true) _audioEvents.value = AudioEnabled(true)
} }
@@ -345,51 +349,50 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
_audioDeviceEvents.value = AudioDeviceUpdate(activeDevice, devices) _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() { fun stop() {
signalAudioManager.handleCommand(AudioManagerCommand.Stop(currentConnectionState in OUTGOING_STATES)) val isOutgoing = currentConnectionState in CallState.OUTGOING_STATES
peerConnection?.dispose() stateProcessor.processEvent(Event.Cleanup) {
peerConnection = null signalAudioManager.handleCommand(AudioManagerCommand.Stop(isOutgoing))
peerConnection?.dispose()
peerConnection = null
localRenderer?.release() localRenderer?.release()
remoteRotationSink?.release() remoteRotationSink?.release()
remoteRenderer?.release() remoteRenderer?.release()
eglBase?.release() eglBase?.release()
localRenderer = null localRenderer = null
remoteRenderer = null remoteRenderer = null
eglBase = null eglBase = null
_connectionEvents.value = CallStateUpdate(CallState.STATE_IDLE) localCameraState = CameraState.UNKNOWN
localCameraState = CameraState.UNKNOWN recipient = null
recipient = null callId = null
callId = null pendingOfferTime = -1
pendingOfferTime = -1 pendingOffer = null
pendingOffer = null callStartTime = -1
callStartTime = -1 _audioEvents.value = AudioEnabled(false)
_audioEvents.value = AudioEnabled(false) _videoEvents.value = VideoEnabled(false)
_videoEvents.value = VideoEnabled(false) _remoteVideoEvents.value = VideoEnabled(false)
_remoteVideoEvents.value = VideoEnabled(false) pendingOutgoingIceUpdates.clear()
pendingOutgoingIceUpdates.clear() pendingIncomingIceUpdates.clear()
pendingIncomingIceUpdates.clear() }
} }
override fun onCameraSwitchCompleted(newCameraState: CameraState) { override fun onCameraSwitchCompleted(newCameraState: CameraState) {
localCameraState = newCameraState localCameraState = newCameraState
} }
fun onPreOffer(callId: UUID, recipient: Recipient, sentTimestamp: Long) { fun onPreOffer(callId: UUID, recipient: Recipient, onSuccess: () -> Unit) {
if (preOfferCallData != null) { stateProcessor.processEvent(Event.ReceivePreOffer) {
Log.d(TAG, "Received new pre-offer when we are already expecting one") 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> { 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) return MessageSender.sendNonDurably(answerMessage, recipient.address)
} }
fun onIncomingRing(offer: String, callId: UUID, recipient: Recipient, callTime: Long) { fun onIncomingRing(offer: String, callId: UUID, recipient: Recipient, callTime: Long, onSuccess: () -> Unit) {
if (currentConnectionState !in arrayOf(CallState.STATE_IDLE, CallState.STATE_PRE_OFFER)) return postConnectionEvent(Event.ReceiveOffer) {
this.callId = callId
this.callId = callId this.recipient = recipient
this.recipient = recipient this.pendingOffer = offer
this.pendingOffer = offer this.pendingOfferTime = callTime
this.pendingOfferTime = callTime initializeAudioForCall()
initializeAudioForCall() startIncomingRinger()
startIncomingRinger() onSuccess()
}
} }
fun onIncomingCall(context: Context, isAlwaysTurn: Boolean = false): Promise<Unit, Exception> { 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")) ?: return Promise.ofFail(NullPointerException("localRenderer is null"))
val base = eglBase ?: return Promise.ofFail(NullPointerException("eglBase 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, context,
factory, factory,
this, this,
@@ -480,23 +489,24 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
this, this,
base, base,
isAlwaysTurn isAlwaysTurn
) )
peerConnection = connection peerConnection = connection
localCameraState = connection.getCameraState() localCameraState = connection.getCameraState()
val dataChannel = connection.createDataChannel(DATA_CHANNEL_NAME) val dataChannel = connection.createDataChannel(DATA_CHANNEL_NAME)
dataChannel.registerObserver(this) dataChannel.registerObserver(this)
this.dataChannel = dataChannel this.dataChannel = dataChannel
val offer = connection.createOffer(MediaConstraints()) val offer = connection.createOffer(MediaConstraints())
connection.setLocalDescription(offer) connection.setLocalDescription(offer)
return MessageSender.sendNonDurably(CallMessage.preOffer( return MessageSender.sendNonDurably(CallMessage.preOffer(
callId callId
), recipient.address).bind { ), recipient.address).bind {
MessageSender.sendNonDurably(CallMessage.offer( MessageSender.sendNonDurably(CallMessage.offer(
offer.description, offer.description,
callId callId
), recipient.address) ), recipient.address)
}
} }
} }
@@ -507,9 +517,11 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
val callId = callId ?: return val callId = callId ?: return
val recipient = recipient ?: return val recipient = recipient ?: return
val userAddress = storage.getUserPublicKey() ?: return val userAddress = storage.getUserPublicKey() ?: return
MessageSender.sendNonDurably(CallMessage.endCall(callId), Address.fromSerialized(userAddress)) stateProcessor.processEvent(Event.DeclineCall) {
MessageSender.sendNonDurably(CallMessage.endCall(callId), recipient.address) MessageSender.sendNonDurably(CallMessage.endCall(callId), Address.fromSerialized(userAddress))
insertCallMessage(recipient.address.serialize(), CallMessageType.CALL_MISSED) MessageSender.sendNonDurably(CallMessage.endCall(callId), recipient.address)
insertCallMessage(recipient.address.serialize(), CallMessageType.CALL_MISSED)
}
} }
fun handleLocalHangup(intentRecipient: Recipient?) { 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) val sendHangup = intentRecipient == null || (intentRecipient == recipient && recipient.address.serialize() != currentUserPublicKey)
postViewModelState(CallViewModel.State.CALL_DISCONNECTED) postViewModelState(CallViewModel.State.CALL_DISCONNECTED)
stateProcessor.processEvent(Event.Hangup)
if (sendHangup) { if (sendHangup) {
dataChannel?.let { channel -> dataChannel?.let { channel ->
val buffer = DataChannel.Buffer(ByteBuffer.wrap(HANGUP_JSON.toString().encodeToByteArray()), false) 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() { fun handleRemoteHangup() {
when (currentConnectionState) { when (currentConnectionState) {
CallState.STATE_DIALING, CallState.LocalRing,
CallState.STATE_REMOTE_RINGING -> postViewModelState(CallViewModel.State.RECIPIENT_UNAVAILABLE) CallState.RemoteRing -> postViewModelState(CallViewModel.State.RECIPIENT_UNAVAILABLE)
else -> postViewModelState(CallViewModel.State.CALL_DISCONNECTED) else -> postViewModelState(CallViewModel.State.CALL_DISCONNECTED)
} }
} }
@@ -556,7 +569,7 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
channel.send(buffer) channel.send(buffer)
} }
if (currentConnectionState == CallState.STATE_CONNECTED) { if (currentConnectionState == CallState.Connected) {
if (connection.isVideoEnabled()) lockManager.updatePhoneState(LockManager.PhoneState.IN_VIDEO) if (connection.isVideoEnabled()) lockManager.updatePhoneState(LockManager.PhoneState.IN_VIDEO)
else lockManager.updatePhoneState(LockManager.PhoneState.IN_CALL) else lockManager.updatePhoneState(LockManager.PhoneState.IN_CALL)
} }
@@ -585,9 +598,9 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
} }
fun handleWiredHeadsetChanged(present: Boolean) { fun handleWiredHeadsetChanged(present: Boolean) {
if (currentConnectionState in arrayOf(CallState.STATE_CONNECTED, if (currentConnectionState in arrayOf(CallState.Connected,
CallState.STATE_DIALING, CallState.LocalRing,
CallState.STATE_REMOTE_RINGING)) { CallState.RemoteRing)) {
if (present && signalAudioManager.isSpeakerphoneOn()) { if (present && signalAudioManager.isSpeakerphoneOn()) {
signalAudioManager.handleCommand(AudioManagerCommand.SetUserDevice(AudioDevice.WIRED_HEADSET)) signalAudioManager.handleCommand(AudioManagerCommand.SetUserDevice(AudioDevice.WIRED_HEADSET))
} else if (!present && !signalAudioManager.isSpeakerphoneOn() && !signalAudioManager.isBluetoothScoOn() && localCameraState.enabled) { } else if (!present && !signalAudioManager.isSpeakerphoneOn() && !signalAudioManager.isBluetoothScoOn() && localCameraState.enabled) {
@@ -597,24 +610,26 @@ class CallManager(context: Context, audioManager: AudioManagerCompat, private va
} }
fun handleScreenOffChange() { 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) signalAudioManager.handleCommand(AudioManagerCommand.SilenceIncomingRinger)
} }
} }
fun handleResponseMessage(recipient: Recipient, callId: UUID, answer: SessionDescription) { 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") Log.w(TAG,"Got answer for recipient and call ID we're not currently dialing")
return return
} }
val connection = peerConnection ?: throw AssertionError("assert") stateProcessor.processEvent(Event.ReceiveAnswer) {
val connection = peerConnection ?: throw AssertionError("assert")
connection.setRemoteDescription(answer) connection.setRemoteDescription(answer)
while (pendingIncomingIceUpdates.isNotEmpty()) { while (pendingIncomingIceUpdates.isNotEmpty()) {
connection.addIceCandidate(pendingIncomingIceUpdates.pop()) connection.addIceCandidate(pendingIncomingIceUpdates.pop())
}
queueOutgoingIce(callId, recipient)
} }
queueOutgoingIce(callId, recipient)
} }
fun handleRemoteIceCandidate(iceCandidates: List<IceCandidate>, callId: UUID) { 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_RINGING,
CALL_BUSY, CALL_BUSY,
CALL_DISCONNECTED, CALL_DISCONNECTED,
CALL_RECONNECTING,
NETWORK_FAILURE, NETWORK_FAILURE,
RECIPIENT_UNAVAILABLE, RECIPIENT_UNAVAILABLE,

View File

@@ -1,5 +1,7 @@
package org.thoughtcrime.securesms.webrtc.data 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 import org.thoughtcrime.securesms.webrtc.data.State.Companion.CAN_HANGUP_STATES
sealed class State { sealed class State {
@@ -13,6 +15,10 @@ sealed class State {
object Reconnecting : State() object Reconnecting : State()
object Disconnected : State() object Disconnected : State()
companion object { companion object {
val ALL_STATES = arrayOf(Idle, RemotePreOffer, RemoteRing, LocalPreOffer, LocalRing,
Connecting, Connected, Reconnecting, Disconnected)
val CAN_DECLINE_STATES = arrayOf(RemotePreOffer, RemoteRing) val CAN_DECLINE_STATES = arrayOf(RemotePreOffer, RemoteRing)
val PENDING_CONNECTION_STATES = arrayOf( val PENDING_CONNECTION_STATES = arrayOf(
LocalPreOffer, LocalPreOffer,
@@ -26,10 +32,17 @@ sealed class State {
LocalRing, LocalRing,
) )
val CAN_HANGUP_STATES = val CAN_HANGUP_STATES =
arrayOf(LocalPreOffer, LocalRing, Connecting, Connected, Reconnecting) arrayOf(RemotePreOffer, RemoteRing, LocalPreOffer, LocalRing, Connecting, Connected, Reconnecting)
val CAN_RECEIVE_ICE_STATES = val CAN_RECEIVE_ICE_STATES =
arrayOf(RemoteRing, LocalRing, Connecting, Connected, Reconnecting) 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) { 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 NetworkReconnect : Event(State.Reconnecting, outputState = State.Connecting)
object TimeOut : object TimeOut :
Event(State.Connecting, State.LocalRing, State.RemoteRing, outputState = State.Disconnected) 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 Hangup : Event(*CAN_HANGUP_STATES, outputState = State.Disconnected)
object Cleanup : Event(State.Disconnected, outputState = State.Idle) object Cleanup : Event(State.Disconnected, outputState = State.Idle)
} }
@@ -62,6 +76,7 @@ open class StateProcessor(initialState: State) {
sideEffect() sideEffect()
return true return true
} }
Log.e("Loki-Call", "error transitioning from $currentState with ${event::class.simpleName}")
return false return false
} }
} }

View File

@@ -1,8 +1,13 @@
package org.thoughtcrime.securesms.calls package org.thoughtcrime.securesms.calls
import org.junit.After
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Before import org.junit.Before
import org.junit.Test 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.Event
import org.thoughtcrime.securesms.webrtc.data.State import org.thoughtcrime.securesms.webrtc.data.State
@@ -10,9 +15,19 @@ class CallStateMachineTests {
private lateinit var stateProcessor: TestStateProcessor private lateinit var stateProcessor: TestStateProcessor
lateinit var mock: MockedStatic<Log>
@Before @Before
fun setup() { fun setup() {
stateProcessor = TestStateProcessor(State.Idle) 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 @Test
@@ -119,6 +134,10 @@ class CallStateMachineTests {
Event.ReceiveOffer, Event.ReceiveOffer,
Event.SendAnswer, Event.SendAnswer,
Event.IceFailed, Event.IceFailed,
Event.Cleanup,
Event.ReceivePreOffer,
Event.ReceiveOffer,
Event.DeclineCall,
Event.Cleanup Event.Cleanup
) )

View File

@@ -1,20 +1,22 @@
package org.thoughtcrime.securesms.database; package org.thoughtcrime.securesms.database;
import android.content.Context; import static org.junit.Assert.assertEquals;
import android.database.Cursor; import static org.junit.Assert.assertNotEquals;
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.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; 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 { public class CursorRecyclerViewAdapterTest {
private CursorRecyclerViewAdapter adapter; private CursorRecyclerViewAdapter adapter;
private Context context; private Context context;