Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import android.view.MotionEvent
import android.view.OrientationEventListener
import android.view.View
import android.view.View.OnTouchListener
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.DrawableRes
import androidx.appcompat.app.AlertDialog
Expand Down Expand Up @@ -311,6 +312,7 @@ class CallActivity : CallBaseActivity() {
private var webSocketClient: WebSocketInstance? = null
private var webSocketConnectionHelper: WebSocketConnectionHelper? = null
private var joinRoomInitiated = false
private var roomJoinRefreshes = 0
private var hasMCU = false
private var hasExternalSignalingServer = false
private var conversationPassword: String? = null
Expand Down Expand Up @@ -1590,6 +1592,31 @@ class CallActivity : CallBaseActivity() {
}
}

/**
* Joining the room for a call was rejected because the cached room session is stale (reaped by the server).
* Drops the cached session so [joinRoomAndCall] fetches a fresh one via the joinRoom API, and retries a few
* times; otherwise the call UI would show "Ringing" forever, as the calling timeout is only armed after a
* successful join.
*/
private fun handleRoomJoinFailed() {
Log.d(TAG, "onMessageEvent 'roomJoinFailed'")
if (!shouldRefreshRoomSession(currentCallStatus, roomJoinRefreshes)) {
if (currentCallStatus !== CallStatus.IN_CONVERSATION) {
Log.e(TAG, "Joining the room for the call failed repeatedly, leaving")
runOnUiThread {
Toast.makeText(context, R.string.nc_call_join_failed, Toast.LENGTH_LONG).show()
finish()
}
}
return
}
roomJoinRefreshes++
Log.d(TAG, "Refreshing the room session and retrying the join ($roomJoinRefreshes/$MAX_ROOM_JOIN_REFRESHES)")
ApplicationWideCurrentRoomHolder.getInstance().session = ""
callSession = null
joinRoomAndCall()
}

private fun callOrJoinRoomViaWebSocket() {
if (hasExternalSignalingServer) {
webSocketClient!!.joinRoomWithRoomTokenAndSession(
Expand Down Expand Up @@ -1900,10 +1927,13 @@ class CallActivity : CallBaseActivity() {
}
startSendingNick()
if (webSocketCommunicationEvent.getHashMap()!!["roomToken"] == roomToken) {
roomJoinRefreshes = 0
performCall()
}
}

"roomJoinFailed" -> handleRoomJoinFailed()

"recordingStatus" -> {
Log.d(TAG, "onMessageEvent 'recordingStatus'")
if (webSocketCommunicationEvent.getHashMap()!!.containsKey(KEY_RECORDING_STATE)) {
Expand Down Expand Up @@ -3225,6 +3255,11 @@ class CallActivity : CallBaseActivity() {
private const val CALLING_TIMEOUT: Long = 45000
private const val PULSE_ANIMATION_DURATION: Int = 310

private const val MAX_ROOM_JOIN_REFRESHES: Int = 2

internal fun shouldRefreshRoomSession(callStatus: CallStatus?, refreshesDone: Int): Boolean =
callStatus !== CallStatus.IN_CONVERSATION && refreshesDone < MAX_ROOM_JOIN_REFRESHES

private const val DELAY_ON_ERROR_STOP_THRESHOLD: Int = 16

private const val SESSION_ID_PREFFIX_END: Int = 4
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,13 @@ class WebSocketInstance internal constructor(conversationUser: User, connectionU
restartWebSocket()
} else if ("hello_expected" == message.code) {
restartWebSocket()
} else if ("no_such_room" == message.code) {
// The room session is stale (e.g. reaped by the server). Clear the cached join state so a retry
// actually sends, and let the call UI fetch a fresh room session via the joinRoom API.
Log.d(TAG, "Joining the room was rejected, the room session needs to be refreshed")
currentRoomToken = ""
currentNormalBackendSession = ""
eventBus!!.post(WebSocketCommunicationEvent("roomJoinFailed", HashMap()))
}
}
}
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,7 @@ How to translate with transifex:
<string name="nc_nick_guest">Guest</string>
<string name="nc_public_call_status">Public conversation</string>
<string name="nc_call_timeout">No response in 45 seconds, tap to try again</string>
<string name="nc_call_join_failed">Could not join the call. Please try again.</string>
<string name="nc_call_reconnecting">Reconnecting …</string>
<string name="nc_offline">Currently offline, please check your connectivity</string>
<string name="nc_leaving_call">Leaving call …</string>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.activities

import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test

/**
* Room session refresh decisions ([CallActivity.shouldRefreshRoomSession]).
*
* Joining a call with a stale room session (reaped by the server) is rejected with "no_such_room". The cached
* session must be dropped and a fresh one fetched via the joinRoom API — but only while the call is still being
* set up (a stray error must never disturb an established call) and only a bounded number of times (otherwise the
* UI would retry forever instead of failing visibly).
*/
class CallActivityRoomJoinRefreshTest {

@Test
fun `stale session is refreshed while the call is being set up`() {
assertTrue(CallActivity.shouldRefreshRoomSession(CallStatus.CONNECTING, 0))
assertTrue(CallActivity.shouldRefreshRoomSession(CallStatus.JOINED, 0))
assertTrue(CallActivity.shouldRefreshRoomSession(CallStatus.RECONNECTING, 0))
}

@Test
fun `session is never refreshed once in conversation`() {
assertFalse(CallActivity.shouldRefreshRoomSession(CallStatus.IN_CONVERSATION, 0))
}

@Test
fun `gives up after the maximum number of refreshes`() {
assertTrue(CallActivity.shouldRefreshRoomSession(CallStatus.CONNECTING, 1))
assertFalse(CallActivity.shouldRefreshRoomSession(CallStatus.CONNECTING, 2))
assertFalse(CallActivity.shouldRefreshRoomSession(CallStatus.CONNECTING, 3))
}
}
Loading