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
12 changes: 11 additions & 1 deletion app/src/main/java/com/nextcloud/talk/webrtc/WebSocketInstance.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import java.io.IOException
import java.lang.Thread.sleep
import java.util.concurrent.TimeUnit
import javax.inject.Inject

@AutoInjector(NextcloudTalkApplication::class)
Expand Down Expand Up @@ -80,6 +81,7 @@ class WebSocketInstance internal constructor(conversationUser: User, connectionU
private var messagesQueue: MutableList<String> = ArrayList()
private val signalingMessageReceiver = ExternalSignalingMessageReceiver()
val signalingMessageSender = ExternalSignalingMessageSender()
private val signalingHttpClient: OkHttpClient by lazy { createSignalingHttpClient(okHttpClient!!) }

init {
sharedApplication!!.componentApplication.inject(this)
Expand Down Expand Up @@ -140,7 +142,7 @@ class WebSocketInstance internal constructor(conversationUser: User, connectionU
reconnecting = true
Log.d(TAG, "restartWebSocket: $connectionUrl")
val request = Request.Builder().url(connectionUrl).build()
okHttpClient!!.newWebSocket(request, this)
signalingHttpClient.newWebSocket(request, this)
}

override fun onMessage(webSocket: WebSocket, text: String) {
Expand Down Expand Up @@ -524,5 +526,13 @@ class WebSocketInstance internal constructor(conversationUser: User, connectionU
private const val TAG = "WebSocketInstance"
private const val NORMAL_CLOSURE = 1000
private const val ONE_SECOND: Long = 1000
private const val PING_INTERVAL_SECONDS: Long = 10

// Dedicated client with pings, so half-open WebSocket connections
// (e.g. after a WiFi to cellular switch without TCP reset) fail and trigger the reconnect path.
internal fun createSignalingHttpClient(baseClient: OkHttpClient): OkHttpClient =
baseClient.newBuilder()
.pingInterval(PING_INTERVAL_SECONDS, TimeUnit.SECONDS)
.build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.webrtc

import okhttp3.OkHttpClient
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotSame
import org.junit.Test
import java.util.concurrent.TimeUnit

/**
* Tests for the signaling WebSocket client configuration ([WebSocketInstance.createSignalingHttpClient]).
*
* Without a ping interval OkHttp sends no protocol-level pings on WebSockets and uses an infinite read timeout,
* so a half-open connection (e.g. after a WiFi to cellular switch without a TCP reset) is never detected: the
* reconnect path via "onFailure" never runs and the call goes silently mute/deaf. These tests pin the ping
* configuration that makes dead connections fail fast.
*/
class WebSocketInstanceSignalingClientTest {

@Test
fun signalingClientHasPingIntervalConfigured() {
val signalingClient = WebSocketInstance.createSignalingHttpClient(OkHttpClient())

// hardcoded on purpose: fails if the ping interval in WebSocketInstance changes or is removed
assertEquals(
"signaling WebSocket client must send pings to detect half-open connections",
10_000,
signalingClient.pingIntervalMillis
)
}

@Test
fun signalingClientIsDerivedFromBaseClientWithoutMutatingIt() {
val baseClient = OkHttpClient.Builder()
.connectTimeout(45, TimeUnit.SECONDS)
.readTimeout(45, TimeUnit.SECONDS)
.build()

val signalingClient = WebSocketInstance.createSignalingHttpClient(baseClient)

assertNotSame("a dedicated client instance must be used", baseClient, signalingClient)
assertEquals(
"base client (shared with regular HTTP calls) must stay unchanged",
0,
baseClient.pingIntervalMillis
)
assertEquals(baseClient.connectTimeoutMillis, signalingClient.connectTimeoutMillis)
assertEquals(baseClient.readTimeoutMillis, signalingClient.readTimeoutMillis)
}
}
Loading