From 3e93a3fb8d06f57c4ebab4f9d93eabe744003205 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Tue, 7 Apr 2026 10:30:28 -0400 Subject: [PATCH] Untested: WS battery improvement, relates to #113 and binwiederhier/ntfy#1662 --- .../main/java/io/heckel/ntfy/app/Application.kt | 14 +++++++++++++- .../main/java/io/heckel/ntfy/util/HttpUtil.kt | 16 ++++++++++++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/io/heckel/ntfy/app/Application.kt b/app/src/main/java/io/heckel/ntfy/app/Application.kt index da9219ea..02b7fbd9 100644 --- a/app/src/main/java/io/heckel/ntfy/app/Application.kt +++ b/app/src/main/java/io/heckel/ntfy/app/Application.kt @@ -10,6 +10,7 @@ import io.heckel.ntfy.util.Log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch class Application : Application() { val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) @@ -34,7 +35,18 @@ class Application : Application() { val connectivityManager = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager connectivityManager.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { - SubscriberServiceManager.refresh(this@Application) + // 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) + } } override fun onLost(network: Network) { SubscriberServiceManager.refresh(this@Application) diff --git a/app/src/main/java/io/heckel/ntfy/util/HttpUtil.kt b/app/src/main/java/io/heckel/ntfy/util/HttpUtil.kt index c4ac9c7f..a20a64f9 100644 --- a/app/src/main/java/io/heckel/ntfy/util/HttpUtil.kt +++ b/app/src/main/java/io/heckel/ntfy/util/HttpUtil.kt @@ -46,12 +46,24 @@ object HttpUtil { /** * 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 { return emptyClientBuilder(context, baseUrl) .readTimeout(0, TimeUnit.MILLISECONDS) - .pingInterval(1, TimeUnit.MINUTES) // Technically not necessary, the server also pings us + .pingInterval(5, TimeUnit.MINUTES) .connectTimeout(10, TimeUnit.SECONDS) .build() }