Skip to content

Commit bd7e226

Browse files
Merge pull request #6550 from nextcloud/fix/stale-room-session-rejoin
fix(call): recover from stale room session instead of ringing forever
2 parents 2893de4 + fc217e3 commit bd7e226

4 files changed

Lines changed: 84 additions & 0 deletions

File tree

app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import android.view.MotionEvent
4141
import android.view.OrientationEventListener
4242
import android.view.View
4343
import android.view.View.OnTouchListener
44+
import android.widget.Toast
4445
import androidx.activity.result.contract.ActivityResultContracts
4546
import androidx.annotation.DrawableRes
4647
import androidx.appcompat.app.AlertDialog
@@ -311,6 +312,7 @@ class CallActivity : CallBaseActivity() {
311312
private var webSocketClient: WebSocketInstance? = null
312313
private var webSocketConnectionHelper: WebSocketConnectionHelper? = null
313314
private var joinRoomInitiated = false
315+
private var roomJoinRefreshes = 0
314316
private var hasMCU = false
315317
private var hasExternalSignalingServer = false
316318
private var conversationPassword: String? = null
@@ -1590,6 +1592,31 @@ class CallActivity : CallBaseActivity() {
15901592
}
15911593
}
15921594

1595+
/**
1596+
* Joining the room for a call was rejected because the cached room session is stale (reaped by the server).
1597+
* Drops the cached session so [joinRoomAndCall] fetches a fresh one via the joinRoom API, and retries a few
1598+
* times; otherwise the call UI would show "Ringing" forever, as the calling timeout is only armed after a
1599+
* successful join.
1600+
*/
1601+
private fun handleRoomJoinFailed() {
1602+
Log.d(TAG, "onMessageEvent 'roomJoinFailed'")
1603+
if (!shouldRefreshRoomSession(currentCallStatus, roomJoinRefreshes)) {
1604+
if (currentCallStatus !== CallStatus.IN_CONVERSATION) {
1605+
Log.e(TAG, "Joining the room for the call failed repeatedly, leaving")
1606+
runOnUiThread {
1607+
Toast.makeText(context, R.string.nc_call_join_failed, Toast.LENGTH_LONG).show()
1608+
finish()
1609+
}
1610+
}
1611+
return
1612+
}
1613+
roomJoinRefreshes++
1614+
Log.d(TAG, "Refreshing the room session and retrying the join ($roomJoinRefreshes/$MAX_ROOM_JOIN_REFRESHES)")
1615+
ApplicationWideCurrentRoomHolder.getInstance().session = ""
1616+
callSession = null
1617+
joinRoomAndCall()
1618+
}
1619+
15931620
private fun callOrJoinRoomViaWebSocket() {
15941621
if (hasExternalSignalingServer) {
15951622
webSocketClient!!.joinRoomWithRoomTokenAndSession(
@@ -1900,10 +1927,13 @@ class CallActivity : CallBaseActivity() {
19001927
}
19011928
startSendingNick()
19021929
if (webSocketCommunicationEvent.getHashMap()!!["roomToken"] == roomToken) {
1930+
roomJoinRefreshes = 0
19031931
performCall()
19041932
}
19051933
}
19061934

1935+
"roomJoinFailed" -> handleRoomJoinFailed()
1936+
19071937
"recordingStatus" -> {
19081938
Log.d(TAG, "onMessageEvent 'recordingStatus'")
19091939
if (webSocketCommunicationEvent.getHashMap()!!.containsKey(KEY_RECORDING_STATE)) {
@@ -3237,6 +3267,11 @@ class CallActivity : CallBaseActivity() {
32373267
internal fun isPushToTalkRelease(action: Int): Boolean =
32383268
action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL
32393269

3270+
private const val MAX_ROOM_JOIN_REFRESHES: Int = 2
3271+
3272+
internal fun shouldRefreshRoomSession(callStatus: CallStatus?, refreshesDone: Int): Boolean =
3273+
callStatus !== CallStatus.IN_CONVERSATION && refreshesDone < MAX_ROOM_JOIN_REFRESHES
3274+
32403275
private const val DELAY_ON_ERROR_STOP_THRESHOLD: Int = 16
32413276

32423277
private const val SESSION_ID_PREFFIX_END: Int = 4

app/src/main/java/com/nextcloud/talk/webrtc/WebSocketInstance.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,13 @@ class WebSocketInstance internal constructor(conversationUser: User, connectionU
313313
restartWebSocket()
314314
} else if ("hello_expected" == message.code) {
315315
restartWebSocket()
316+
} else if ("no_such_room" == message.code) {
317+
// The room session is stale (e.g. reaped by the server). Clear the cached join state so a retry
318+
// actually sends, and let the call UI fetch a fresh room session via the joinRoom API.
319+
Log.d(TAG, "Joining the room was rejected, the room session needs to be refreshed")
320+
currentRoomToken = ""
321+
currentNormalBackendSession = ""
322+
eventBus!!.post(WebSocketCommunicationEvent("roomJoinFailed", HashMap()))
316323
}
317324
}
318325
}

app/src/main/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,7 @@ How to translate with transifex:
398398
<string name="nc_nick_guest">Guest</string>
399399
<string name="nc_public_call_status">Public conversation</string>
400400
<string name="nc_call_timeout">No response in 45 seconds, tap to try again</string>
401+
<string name="nc_call_join_failed">Could not join the call. Please try again.</string>
401402
<string name="nc_call_reconnecting">Reconnecting …</string>
402403
<string name="nc_offline">Currently offline, please check your connectivity</string>
403404
<string name="nc_leaving_call">Leaving call …</string>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Nextcloud Talk - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: GPL-3.0-or-later
6+
*/
7+
package com.nextcloud.talk.activities
8+
9+
import org.junit.Assert.assertFalse
10+
import org.junit.Assert.assertTrue
11+
import org.junit.Test
12+
13+
/**
14+
* Room session refresh decisions ([CallActivity.shouldRefreshRoomSession]).
15+
*
16+
* Joining a call with a stale room session (reaped by the server) is rejected with "no_such_room". The cached
17+
* session must be dropped and a fresh one fetched via the joinRoom API — but only while the call is still being
18+
* set up (a stray error must never disturb an established call) and only a bounded number of times (otherwise the
19+
* UI would retry forever instead of failing visibly).
20+
*/
21+
class CallActivityRoomJoinRefreshTest {
22+
23+
@Test
24+
fun `stale session is refreshed while the call is being set up`() {
25+
assertTrue(CallActivity.shouldRefreshRoomSession(CallStatus.CONNECTING, 0))
26+
assertTrue(CallActivity.shouldRefreshRoomSession(CallStatus.JOINED, 0))
27+
assertTrue(CallActivity.shouldRefreshRoomSession(CallStatus.RECONNECTING, 0))
28+
}
29+
30+
@Test
31+
fun `session is never refreshed once in conversation`() {
32+
assertFalse(CallActivity.shouldRefreshRoomSession(CallStatus.IN_CONVERSATION, 0))
33+
}
34+
35+
@Test
36+
fun `gives up after the maximum number of refreshes`() {
37+
assertTrue(CallActivity.shouldRefreshRoomSession(CallStatus.CONNECTING, 1))
38+
assertFalse(CallActivity.shouldRefreshRoomSession(CallStatus.CONNECTING, 2))
39+
assertFalse(CallActivity.shouldRefreshRoomSession(CallStatus.CONNECTING, 3))
40+
}
41+
}

0 commit comments

Comments
 (0)