Untested: WS battery improvement, relates to #113 and binwiederhier/ntfy#1662

This commit is contained in:
Philipp Heckel 2026-04-07 10:30:28 -04:00
parent 7bb812662e
commit 3e93a3fb8d
2 changed files with 27 additions and 3 deletions

View file

@ -10,6 +10,7 @@ import io.heckel.ntfy.util.Log
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
class Application : Application() { class Application : Application() {
val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
@ -34,8 +35,19 @@ class Application : Application() {
val connectivityManager = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager val connectivityManager = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
connectivityManager.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() { connectivityManager.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) { override fun onAvailable(network: Network) {
// Force reconnect of all WebSocket/JSON connections so they're rebound to the new
// default network. This catches Wi-Fi <-> cellular handoffs and similar transitions
// where the underlying socket is bound to a network that's no longer the default.
// Without this, broken connections would only be detected via the (potentially
// long) ping/pong timeout.
ioScope.launch {
repository.getSubscriptions()
.map { it.baseUrl }
.distinct()
.forEach { repository.incrementConnectionForceReconnectVersion(it) }
SubscriberServiceManager.refresh(this@Application) SubscriberServiceManager.refresh(this@Application)
} }
}
override fun onLost(network: Network) { override fun onLost(network: Network) {
SubscriberServiceManager.refresh(this@Application) SubscriberServiceManager.refresh(this@Application)
} }

View file

@ -46,12 +46,24 @@ object HttpUtil {
/** /**
* Client for WebSocket connections. * Client for WebSocket connections.
* No read timeout, 1 minute ping interval, 10s connect timeout. * No read timeout, 5 minute ping interval, 10s connect timeout.
*
* Dead connections are normally caught by one of two faster mechanisms:
* 1. Device-side network changes (Wi-Fi <-> cellular, network drop/return) are
* detected instantly by Application.registerNetworkCallback's onAvailable
* handler, which bumps connectionForceReconnectVersion to force a reconnect.
* 2. Server-side failures (crash, restart, server's own pong timeout) surface as
* TCP FIN/RST and are detected instantly via OkHttp's onClosed/onFailure.
*
* The 5-minute client ping is only a fallback for the rare case where neither of
* the above fires: silent server hangs, NAT eviction, asymmetric routing breaks, etc.
* We use a long interval so the modem can fully power down between pings, which is
* the dominant battery factor for the foreground service.
*/ */
suspend fun wsClient(context: Context, baseUrl: String): OkHttpClient { suspend fun wsClient(context: Context, baseUrl: String): OkHttpClient {
return emptyClientBuilder(context, baseUrl) return emptyClientBuilder(context, baseUrl)
.readTimeout(0, TimeUnit.MILLISECONDS) .readTimeout(0, TimeUnit.MILLISECONDS)
.pingInterval(1, TimeUnit.MINUTES) // Technically not necessary, the server also pings us .pingInterval(5, TimeUnit.MINUTES)
.connectTimeout(10, TimeUnit.SECONDS) .connectTimeout(10, TimeUnit.SECONDS)
.build() .build()
} }