Skip to content

Commit fa80f60

Browse files
authored
NetworkSwitch Fixes (#428)
* 1. Fix Crash when WebRTC Stats is Enabled for NetworkSwitch 2. Implement new CallStates for call recovery * Fix CallTests
1 parent 12643c3 commit fa80f60

File tree

8 files changed

+71
-53
lines changed

8 files changed

+71
-53
lines changed

app/src/main/java/com/telnyx/webrtc/sdk/ui/CallInstanceFragment.kt

+11
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,17 @@ import android.view.LayoutInflater
1313
import android.view.View
1414
import android.view.ViewGroup
1515
import androidx.fragment.app.Fragment
16+
import androidx.lifecycle.Lifecycle
1617
import androidx.lifecycle.ViewModelProvider
18+
import androidx.lifecycle.lifecycleScope
19+
import androidx.lifecycle.repeatOnLifecycle
1720
import com.davidmiguel.numberkeyboard.NumberKeyboard
1821
import com.davidmiguel.numberkeyboard.NumberKeyboardListener
1922
import com.telnyx.webrtc.sdk.R
2023
import com.telnyx.webrtc.sdk.databinding.FragmentCallInstanceBinding
2124
import com.telnyx.webrtc.sdk.model.SocketMethod
2225
import com.telnyx.webrtc.sdk.verto.receive.*
26+
import kotlinx.coroutines.flow.collectLatest
2327
import timber.log.Timber
2428
import java.util.*
2529

@@ -71,6 +75,13 @@ class CallInstanceFragment : Fragment(), NumberKeyboardListener {
7175
mainViewModel.getCallState()?.observe(this.viewLifecycleOwner) { value ->
7276
(requireActivity() as MainActivity).callStateTextValue?.text = value.name
7377
}
78+
79+
this.lifecycleScope.launchWhenStarted {
80+
mainViewModel.getCallStateFlow()?.collectLatest {value ->
81+
// (requireActivity() as MainActivity).callStateTextValue?.text = value.name
82+
}
83+
}
84+
7485
mainViewModel.getIsMuteStatus()?.observe(this.viewLifecycleOwner) { value ->
7586
binding.apply {
7687
if (!value) {

app/src/main/java/com/telnyx/webrtc/sdk/ui/MainViewModel.kt

+7-5
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ class MainViewModel @Inject constructor(
3535
private val holdedCalls = mutableSetOf<Call>()
3636
private var calls: Map<UUID, Call> = mapOf()
3737

38+
fun initTelnyxClient(context: Context) {
39+
telnyxClient = TelnyxClient(context)
40+
}
41+
3842
fun initConnection(
3943
context: Context,
4044
providedServerConfig: TxServerConfiguration?,
@@ -64,10 +68,6 @@ class MainViewModel @Inject constructor(
6468
}
6569
}
6670

67-
fun startDebugStats() {
68-
currentCall?.startDebug()
69-
}
70-
7171
fun saveUserData(
7272
userName: String,
7373
password: String,
@@ -111,6 +111,9 @@ class MainViewModel @Inject constructor(
111111
fun getIsOnLoudSpeakerStatus(): LiveData<Boolean>? = currentCall?.getIsOnLoudSpeakerStatus()
112112

113113

114+
fun getCallStateFlow() = currentCall?.callStateFlow
115+
116+
114117
fun doLoginWithToken(tokenConfig: TokenConfig) {
115118
telnyxClient?.tokenLogin(tokenConfig)
116119
}
@@ -134,7 +137,6 @@ class MainViewModel @Inject constructor(
134137
destinationNumber,
135138
mapOf(Pair("X-testAndroid", "123456"))
136139
)
137-
startDebugStats()
138140
}
139141

140142
fun disablePushNotifications(sipUserName: String, fcmToken: String) {

telnyx_rtc/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -260,4 +260,5 @@ dependencies {
260260

261261
def lifecycle_version = "2.7.0"
262262
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
263+
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
263264
}

telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/Call.kt

+12-22
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import android.content.Context
88
import android.media.AudioManager
99
import androidx.lifecycle.LiveData
1010
import androidx.lifecycle.MutableLiveData
11+
import androidx.lifecycle.asLiveData
1112
import com.google.gson.Gson
1213
import com.google.gson.JsonArray
1314
import com.google.gson.JsonSyntaxException
@@ -41,6 +42,7 @@ data class Call(
4142
val audioManager: AudioManager,
4243
val providedTurn: String = Config.DEFAULT_TURN,
4344
val providedStun: String = Config.DEFAULT_STUN,
45+
internal val mutableCallStateFlow: MutableStateFlow<CallState> = MutableStateFlow(CallState.DONE),
4446
) {
4547

4648
companion object {
@@ -60,12 +62,9 @@ data class Call(
6062
internal var telnyxSessionId: UUID? = null
6163
internal var telnyxLegId: UUID? = null
6264

65+
val callStateFlow: StateFlow<CallState> = mutableCallStateFlow
6366

64-
private val callStateLiveData = MutableLiveData(CallState.NEW)
65-
66-
private val _callStateFlow = MutableStateFlow(CallState.NEW)
67-
68-
val callStateFlow: StateFlow<CallState> = _callStateFlow
67+
private val callStateLiveData = callStateFlow.asLiveData()
6968

7069
// Ongoing call options
7170
// Mute toggle live data
@@ -84,23 +83,14 @@ data class Call(
8483
loudSpeakerLiveData.postValue(audioManager.isSpeakerphoneOn)
8584
}
8685

87-
fun startDebug() {
88-
Timber.d("Peer connection debug started")
89-
90-
// peerConnection?.startTimer()
91-
}
92-
9386
internal fun updateCallState(value: CallState) {
94-
callStateLiveData.postValue(value)
95-
_callStateFlow.value = value
87+
mutableCallStateFlow.value = value
9688
}
9789

98-
fun stopDebug() {
99-
Timber.d("Peer connection debug stopped")
100-
// peerConnection?.stopTimer()
90+
internal fun setCallState(value: CallState) {
91+
mutableCallStateFlow.value = value
10192
}
10293

103-
10494
/**
10595
* Initiates a new call invitation
10696
* @param callerName, the name to appear on the invitation
@@ -161,7 +151,7 @@ data class Call(
161151
val sessionDescriptionString =
162152
peerConnection?.getLocalDescription()?.description
163153
if (sessionDescriptionString == null) {
164-
callStateLiveData.postValue(CallState.ERROR)
154+
mutableCallStateFlow.value = CallState.ERROR
165155
} else {
166156
val answerBodyMessage = SendingMessageBody(
167157
uuid, SocketMethod.ATTACH.methodName,
@@ -176,7 +166,7 @@ data class Call(
176166
)
177167
)
178168
socket.send(answerBodyMessage)
179-
callStateLiveData.postValue(CallState.ACTIVE)
169+
mutableCallStateFlow.value = CallState.ACTIVE
180170
}
181171
}
182172

@@ -231,11 +221,11 @@ data class Call(
231221
fun onHoldUnholdPressed(callId: UUID) {
232222
if (!holdLiveData.value!!) {
233223
holdLiveData.postValue(true)
234-
callStateLiveData.postValue(CallState.HELD)
224+
mutableCallStateFlow.value = CallState.HELD
235225
sendHoldModifier(callId, "hold")
236226
} else {
237227
holdLiveData.postValue(false)
238-
callStateLiveData.postValue(CallState.ACTIVE)
228+
mutableCallStateFlow.value = CallState.ACTIVE
239229
sendHoldModifier(callId, "unhold")
240230
}
241231
}
@@ -357,7 +347,7 @@ data class Call(
357347

358348

359349
fun setCallRecovering() {
360-
callStateLiveData.postValue(CallState.RECOVERING)
350+
mutableCallStateFlow.value = CallState.RECONNECTING
361351
}
362352

363353

telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/TelnyxClient.kt

+32-22
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,9 @@ class TelnyxClient(
430430

431431
Handler(Looper.getMainLooper()).postDelayed(Runnable {
432432
if (!ConnectivityHelper.isNetworkEnabled(context)) {
433+
getActiveCalls().forEach { (_, call) ->
434+
call.updateCallState(CallState.DROPPED)
435+
}
433436
socketResponseLiveData.postValue(SocketResponse.error("No Network Connection"))
434437
} else {
435438
//Network is switched here. Either from Wifi to LTE or vice-versa
@@ -448,10 +451,15 @@ class TelnyxClient(
448451
private suspend fun reconnectToSocket() = withContext(Dispatchers.Default) {
449452

450453
//Disconnect active calls for reconnection
451-
getActiveCalls()?.forEach { (_, call) ->
452-
call?.peerConnection?.disconnect()
454+
getActiveCalls().forEach { (_, call) ->
455+
webRTCReporter?.pauseStats()
456+
call.peerConnection?.disconnect()
457+
call.updateCallState(CallState.RECONNECTING)
453458
}
454459

460+
//Delay for network to be properly established
461+
delay(RECONNECT_DELAY)
462+
455463
// Create new socket connection
456464
socketReconnection = TxSocket(
457465
socket.host_address,
@@ -471,8 +479,6 @@ class TelnyxClient(
471479
if (pushMetaData == null) Config.TELNYX_PROD_HOST_ADDRESS
472480
else
473481
Config.TELNYX_PROD_HOST_ADDRESS
474-
475-
476482
}
477483

478484

@@ -563,8 +569,7 @@ class TelnyxClient(
563569
txPushMetaData: String? = null,
564570
) {
565571

566-
socketResponseLiveData =
567-
MutableLiveData<SocketResponse<ReceivedMessageBody>>(SocketResponse.initialised())
572+
socketResponseLiveData.postValue(SocketResponse.initialised())
568573
waitingForReg = true
569574
invalidateGatewayResponseTimer()
570575
resetGatewayCounters()
@@ -617,8 +622,7 @@ class TelnyxClient(
617622
autoLogin: Boolean = true,
618623
) {
619624

620-
socketResponseLiveData =
621-
MutableLiveData<SocketResponse<ReceivedMessageBody>>(SocketResponse.initialised())
625+
socketResponseLiveData.postValue(SocketResponse.initialised())
622626
waitingForReg = true
623627
invalidateGatewayResponseTimer()
624628
resetGatewayCounters()
@@ -680,8 +684,7 @@ class TelnyxClient(
680684
autoLogin: Boolean = true,
681685
) {
682686

683-
socketResponseLiveData =
684-
MutableLiveData<SocketResponse<ReceivedMessageBody>>(SocketResponse.initialised())
687+
socketResponseLiveData.postValue(SocketResponse.initialised())
685688
waitingForReg = true
686689
invalidateGatewayResponseTimer()
687690
resetGatewayCounters()
@@ -1641,18 +1644,20 @@ class TelnyxClient(
16411644

16421645
override fun onAttachReceived(jsonObject: JsonObject) {
16431646

1644-
val attachCall = call!!.copy(
1647+
val params = jsonObject.getAsJsonObject("params")
1648+
val offerCallId = UUID.fromString(params.get("callID").asString)
1649+
1650+
calls[offerCallId]?.copy(
16451651
context = context,
16461652
client = this,
16471653
socket = socket,
16481654
sessionId = sessid,
16491655
audioManager = audioManager!!,
16501656
providedTurn = providedTurn!!,
1651-
providedStun = providedStun!!
1652-
).apply {
1657+
providedStun = providedStun!!,
1658+
mutableCallStateFlow = calls[offerCallId]!!.mutableCallStateFlow,
1659+
)?.apply {
16531660

1654-
val params = jsonObject.getAsJsonObject("params")
1655-
val offerCallId = UUID.fromString(params.get("callID").asString)
16561661
val remoteSdp = params.get("sdp").asString
16571662
val voiceSdkID = jsonObject.getAsJsonPrimitive("voice_sdk_id")?.asString
16581663
if (voiceSdkID != null) {
@@ -1672,11 +1677,11 @@ class TelnyxClient(
16721677

16731678

16741679
peerConnection = Peer(context, client, providedTurn, providedStun, offerCallId).also {
1675-
if (isDebug) {
1676-
webRTCReporter = WebRTCReporter(socket, callId, telnyxLegId?.toString(), it)
1677-
webRTCReporter?.startStats()
1678-
}
1679-
}
1680+
if (isDebug) {
1681+
webRTCReporter = WebRTCReporter(socket, callId, telnyxLegId?.toString(), it)
1682+
webRTCReporter?.startStats()
1683+
}
1684+
}
16801685

16811686
peerConnection?.startLocalAudioCapture()
16821687

@@ -1696,9 +1701,14 @@ class TelnyxClient(
16961701
},
16971702
Call.ICE_CANDIDATE_DELAY
16981703
)
1704+
calls[this.callId]?.updateCallState(CallState.ACTIVE)
1705+
this.setCallState(calls[this.callId]?.callStateFlow?.value ?: CallState.ACTIVE)
1706+
calls[this.callId] = this.apply {
1707+
updateCallState(CallState.ACTIVE)
1708+
}
1709+
} ?: run {
1710+
Timber.e("Call not found for Attach")
16991711
}
1700-
attachCall.updateCallState(CallState.ACTIVE)
1701-
calls[attachCall.callId] = attachCall
17021712
}
17031713

17041714
override fun setCallRecovering() {

telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/model/CallState.kt

+3-2
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ package com.telnyx.webrtc.sdk.model
1919
enum class CallState {
2020
NEW,
2121
CONNECTING,
22-
RECOVERING,
22+
RECONNECTING,
2323
RINGING,
2424
ACTIVE,
2525
HELD,
2626
DONE,
27-
ERROR
27+
ERROR,
28+
DROPPED
2829
}

telnyx_rtc/src/main/java/com/telnyx/webrtc/sdk/stats/WebRTCReporter.kt

+3
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ internal class WebRTCReporter(val socket: TxSocket, val peerId: UUID, val connec
104104
debugReportStarted = false
105105
}
106106

107+
internal fun pauseStats() {
108+
debugReportJob?.cancel()
109+
}
107110
internal fun onStatsDataEvent(event: StatsData) {
108111
CoroutineScope(Dispatchers.IO).launch {
109112
statsDataFlow.emit(event)

telnyx_rtc/src/test/java/com/telnyx/webrtc/sdk/CallTest.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ class CallTest : BaseTest() {
186186
call.callId = UUID.randomUUID()
187187
client.addToCalls(call)
188188
call.acceptCall(call.callId, "00000")
189-
assertEquals(call.getCallState().value, CallState.ERROR)
189+
assertEquals(call.callStateFlow.value, CallState.ERROR)
190190
}
191191

192192
@Test
@@ -202,7 +202,7 @@ class CallTest : BaseTest() {
202202
call = Mockito.spy(
203203
Call(mockContext, client, client.socket, "123", audioManager)
204204
)
205-
assertEquals(call.getCallState().value, CallState.CONNECTING)
205+
assertEquals(call.callStateFlow.value, CallState.CONNECTING)
206206
}
207207

208208
// NOOP tests, methods that should literally do nothing -- included for test coverage

0 commit comments

Comments
 (0)