Skip to content

Commit

Permalink
feat(WEBRTC-2102): implement reconnect functionality with attach flow (
Browse files Browse the repository at this point in the history
…#205)

* chore: fix CauseCode values

* feat: implement reconnect() on failure

* fix: set call within login logic

* feat: convert sessionId to sessid

* feat: implement reattach flow and state changes

* fix: implement correct comment

* feat: strip previous sessionid logic.

* fix: remove old listener method

* fix: add check to see if testing before returning call
  • Loading branch information
Oliver-Zimmerman authored Aug 29, 2022
1 parent 2e6bcc6 commit 78907cb
Show file tree
Hide file tree
Showing 12 changed files with 167 additions and 89 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class UserManager @Inject constructor(private val sharedPreferences: SharedPrefe
}
set(callerNumber) = sharedPreferences.edit().putString(KEY_USER_CALLER_ID_NUMBER, callerNumber).apply()

var sessionId: String
var sessid: String
get() {
val temp = sharedPreferences.getString(KEY_USER_SESSION_ID, "")
return temp ?: ""
Expand Down
85 changes: 76 additions & 9 deletions telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/Call.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import com.telnyx.webrtc.sdk.socket.TxSocketListener
import com.telnyx.webrtc.sdk.utilities.encodeBase64
import com.telnyx.webrtc.sdk.verto.receive.*
import com.telnyx.webrtc.sdk.verto.send.*
import io.ktor.util.*
import org.webrtc.IceCandidate
import org.webrtc.SessionDescription
import timber.log.Timber
Expand Down Expand Up @@ -120,7 +119,7 @@ class Call(
id = uuid,
method = SocketMethod.INVITE.methodName,
params = CallParams(
sessionId = sessionId,
sessid = sessionId,
sdp = peerConnection?.getLocalDescription()?.description.toString(),
dialogParams = CallDialogParams(
callerIdName = callerName,
Expand Down Expand Up @@ -158,7 +157,7 @@ class Call(
val answerBodyMessage = SendingMessageBody(
uuid, SocketMethod.ANSWER.methodName,
CallParams(
sessionId = sessionId,
sessid = sessionId,
sdp = sessionDescriptionString,
dialogParams = CallDialogParams(
callId = callId,
Expand All @@ -173,6 +172,37 @@ class Call(
}
}

/**
* Accepts an attach invitation
* Functions the same as the acceptCall but changes the attach param to true
* @param callId, the callId provided with the invitation
* @param destinationNumber, the number or SIP name that will receive the invitation
* @see [Call]
*/
private fun acceptReattachCall(callId: UUID, destinationNumber: String) {
val uuid: String = UUID.randomUUID().toString()
val sessionDescriptionString =
peerConnection?.getLocalDescription()?.description
if (sessionDescriptionString == null) {
callStateLiveData.postValue(CallState.ERROR)
} else {
val answerBodyMessage = SendingMessageBody(
uuid, SocketMethod.ATTACH.methodName,
CallParams(
sessid = sessionId,
sdp = sessionDescriptionString,
dialogParams = CallDialogParams(
attach = true,
callId = callId,
destinationNumber = destinationNumber
)
)
)
socket.send(answerBodyMessage)
callStateLiveData.postValue(CallState.ACTIVE)
}
}

/**
* Ends an ongoing call with a provided callID, the unique UUID belonging to each call
* @param callId, the callId provided with the invitation
Expand Down Expand Up @@ -256,7 +286,7 @@ class Call(
id = uuid,
method = SocketMethod.MODIFY.methodName,
params = ModifyParams(
sessionId = sessionId,
sessid = sessionId,
action = holdAction,
dialogParams = CallDialogParams(
callId = callId,
Expand All @@ -279,7 +309,7 @@ class Call(
id = uuid,
method = SocketMethod.INFO.methodName,
params = InfoParams(
sessionId = sessionId,
sessid = sessionId,
dtmf = tone,
dialogParams = CallDialogParams(
callId = callId,
Expand Down Expand Up @@ -526,15 +556,52 @@ class Call(
)
}

override fun onClientReady(jsonObject: JsonObject) {
// NOOP
override fun onAttachReceived(jsonObject: JsonObject) {
Timber.d("[%s] :: onAttachReceived [%s]", this@Call.javaClass.simpleName, jsonObject)
val params = jsonObject.getAsJsonObject("params")
val callId = UUID.fromString(params.get("callID").asString)
val remoteSdp = params.get("sdp").asString
val callerNumber = params.get("caller_id_number").asString

peerConnection = Peer(
context, client, providedTurn, providedStun,
object : PeerConnectionObserver() {
override fun onIceCandidate(p0: IceCandidate?) {
super.onIceCandidate(p0)
peerConnection?.addIceCandidate(p0)
}
}
)

peerConnection?.startLocalAudioCapture()

peerConnection?.onRemoteSessionReceived(
SessionDescription(
SessionDescription.Type.OFFER,
remoteSdp
)
)

peerConnection?.answer(AppSdpObserver())

val iceCandidateTimer = Timer()
iceCandidateTimer.schedule(
timerTask {
acceptReattachCall(callId, callerNumber)
},
ICE_CANDIDATE_DELAY
)
}

override fun onSessionIdReceived(jsonObject: JsonObject) {
override fun setCallRecovering() {
callStateLiveData.postValue(CallState.RECOVERING)
}

override fun onClientReady(jsonObject: JsonObject) {
// NOOP
}

override fun onGatewayStateReceived(gatewayState: String, sessionId: String?) {
override fun onGatewayStateReceived(gatewayState: String, receivedSessionId: String?) {
// NOOP
}

Expand Down
71 changes: 46 additions & 25 deletions telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/TelnyxClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class TelnyxClient(
// MediaPlayer for ringtone / ringbacktone
private var mediaPlayer: MediaPlayer? = null

private var sessionId: String? = null
var sessid: String // sessid used to recover calls when reconnecting
val socketResponseLiveData = MutableLiveData<SocketResponse<ReceivedMessageBody>>()
val wsMessagesResponseLiveDate = MutableLiveData<JsonObject>()

Expand All @@ -82,11 +82,22 @@ class TelnyxClient(
* @return [Call]
*/
private fun buildCall(): Call? {
sessionId?.let {
return Call(context, this, socket, sessionId!!, audioManager!!, providedTurn!!, providedStun!!)
if (!BuildConfig.IS_TESTING.get()) {
sessid.let {
return Call(
context,
this,
socket,
sessid,
audioManager!!,
providedTurn!!,
providedStun!!
)
}
} else {
// We are testing, and will instead return a mocked call.
return null
}
socketResponseLiveData.postValue(SocketResponse.error("Session ID is not set, failed to build call"))
return null
}

/**
Expand Down Expand Up @@ -153,17 +164,20 @@ class TelnyxClient(
credentialSessionConfig?.let {
credentialLogin(it)
} ?: tokenLogin(tokenSessionConfig!!)
}

// Change an ongoing call's socket to the new socket.
call?.let { call?.socket = socket }
// Change an ongoing call's socket to the new socket.
call?.let { call?.socket = socket }
}
}

init {
if (!BuildConfig.IS_TESTING.get()) {
Bugsnag.start(context)
}

// Generate random UUID for sessid param, convert it to string and set globally
sessid = UUID.randomUUID().toString()

socket = TxSocket(
host_address = Config.TELNYX_PROD_HOST_ADDRESS,
port = Config.TELNYX_PORT
Expand Down Expand Up @@ -319,7 +333,8 @@ class TelnyxClient(
login = user,
passwd = password,
userVariables = notificationJsonObject,
loginParams = arrayListOf()
loginParams = arrayListOf(),
sessid = sessid
)
)

Expand Down Expand Up @@ -360,7 +375,8 @@ class TelnyxClient(
login = null,
passwd = null,
userVariables = notificationJsonObject,
loginParams = arrayListOf()
loginParams = arrayListOf(),
sessid = sessid
)
)
socket.send(loginMessage)
Expand Down Expand Up @@ -508,7 +524,7 @@ class TelnyxClient(
this@TelnyxClient.javaClass.simpleName,
receivedLoginSessionId
)
sessionId = receivedLoginSessionId
sessid = receivedLoginSessionId
socketResponseLiveData.postValue(
SocketResponse.messageReceived(
ReceivedMessageBody(
Expand Down Expand Up @@ -576,12 +592,6 @@ class TelnyxClient(
}
}

override fun onSessionIdReceived(jsonObject: JsonObject) {
val result = jsonObject.get("result")
val sessId = result.asJsonObject.get("sessid").asString
sessionId = sessId
}

override fun onGatewayStateReceived(gatewayState: String, receivedSessionId: String?) {
when (gatewayState) {
GatewayState.REGED.state -> {
Expand All @@ -591,12 +601,8 @@ class TelnyxClient(
resetGatewayCounters()
onLoginSuccessful(it)
} ?: kotlin.run {
if (sessionId != null) {
resetGatewayCounters()
onLoginSuccessful(sessionId!!)
} else {
socketResponseLiveData.postValue(SocketResponse.error("No session ID received. Please try again"))
}
resetGatewayCounters()
onLoginSuccessful(sessid)
}
}
GatewayState.NOREG.state -> {
Expand All @@ -610,7 +616,10 @@ class TelnyxClient(
GatewayState.FAIL_WAIT.state -> {
if (autoReconnectLogin && connectRetryCounter < RETRY_CONNECT_TIME) {
connectRetryCounter++
Timber.d("[%s] :: Attempting reconnection :: attempt $connectRetryCounter / $RETRY_CONNECT_TIME", this@TelnyxClient.javaClass.simpleName)
Timber.d(
"[%s] :: Attempting reconnection :: attempt $connectRetryCounter / $RETRY_CONNECT_TIME",
this@TelnyxClient.javaClass.simpleName
)
runBlocking { reconnectToSocket() }
} else {
invalidateGatewayResponseTimer()
Expand Down Expand Up @@ -680,14 +689,26 @@ class TelnyxClient(
}

override fun onRingingReceived(jsonObject: JsonObject) {
Timber.d("[%s] :: onRingingReceived [%s]", this@TelnyxClient.javaClass.simpleName, jsonObject)
Timber.d(
"[%s] :: onRingingReceived [%s]",
this@TelnyxClient.javaClass.simpleName,
jsonObject
)
call?.onRingingReceived(jsonObject)
}

override fun onIceCandidateReceived(iceCandidate: IceCandidate) {
call?.onIceCandidateReceived(iceCandidate)
}

override fun onAttachReceived(jsonObject: JsonObject) {
call?.onAttachReceived(jsonObject)
}

override fun setCallRecovering() {
call?.setCallRecovering()
}

internal fun onRemoteSessionErrorReceived(errorMessage: String?) {
stopMediaPlayer()
socketResponseLiveData.postValue(errorMessage?.let { SocketResponse.error(it) })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package com.telnyx.webrtc.sdk.model
enum class CallState {
NEW,
CONNECTING,
RECOVERING,
RINGING,
ACTIVE,
HELD,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ package com.telnyx.webrtc.sdk.model
* Enum class to represent the different Cause Codes that are received when an invitation is refused
* with a given [code]
*
* @param code is the numerical representation of the cause, eg. 1 -> USER_BUSY
* @param code is the numerical representation of the cause, eg. 17 -> USER_BUSY
*
* @property USER_BUSY This cause is used to indicate that the called party is unable to accept another call because the user busy condition has been encountered.
* @property NORMAL_CLEARING This cause indicates that the call is being cleared because one of the users involved in the call has requested that the call be cleared. Under normal situations, the source of this cause is not the network.
* @property INVALID_GATEWAY This cause indicates that there is an issue with the gateway in use, likely due to an invalid configuration
* @property ORIGINATOR_CANCEL This cause indicates that the user initiating the call cancelled it before it was answered
*/
enum class CauseCode(var code: Int) {
USER_BUSY(1),
NORMAL_CLEARING(2),
INVALID_GATEWAY(3),
ORIGINATOR_CANCEL(4)
USER_BUSY(17),
NORMAL_CLEARING(16),
INVALID_GATEWAY(608),
ORIGINATOR_CANCEL(487)
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package com.telnyx.webrtc.sdk.model
*/
enum class SocketMethod(var methodName: String) {
ANSWER("telnyx_rtc.answer"),
ATTACH("telnyx_rtc.attach"),
INVITE("telnyx_rtc.invite"),
BYE("telnyx_rtc.bye"),
MODIFY("telnyx_rtc.modify"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class TxSocket(
) = launch {
client = OkHttpClient.Builder()
.addNetworkInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
.retryOnConnectionFailure(true)
.connectTimeout(25, TimeUnit.SECONDS)
.readTimeout(25, TimeUnit.SECONDS)
.writeTimeout(25, TimeUnit.SECONDS)
Expand Down Expand Up @@ -122,8 +123,6 @@ class TxSocket(
val message = result.get("message").asString
if (message == "logged in" && isLoggedIn) {
listener.onClientReady(jsonObject)
} else {
listener.onSessionIdReceived(jsonObject)
}
}
}
Expand All @@ -144,6 +143,9 @@ class TxSocket(
CLIENT_READY.methodName -> {
listener.onClientReady(jsonObject)
}
ATTACH.methodName -> {
listener.onAttachReceived(jsonObject)
}
INVITE.methodName -> {
listener.onOfferReceived(jsonObject)
}
Expand Down Expand Up @@ -203,7 +205,11 @@ class TxSocket(
}

override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
Timber.tag("TxSocket").i("Socket is closed: $response $t")
Timber.tag("TxSocket")
.i("Socket is closed: $response $t :: Will attempt to reconnect")
if (ongoingCall) {
listener.call?.setCallRecovering()
}
}
}
)
Expand Down
Loading

0 comments on commit 78907cb

Please sign in to comment.