From cf71da474c7753439cc5babd46e7dd8ae736619f Mon Sep 17 00:00:00 2001 From: Sacha Date: Tue, 27 Jan 2026 19:21:52 +0100 Subject: [PATCH] add a configurable reconnect option for websocket --- .../gotify/service/WebSocketConnection.kt | 18 +++++++---- .../github/gotify/service/WebSocketService.kt | 28 +++++++++++++---- .../gotify/settings/SettingsActivity.kt | 30 +++++++++++++++++++ app/src/main/res/values/arrays.xml | 1 + app/src/main/res/values/strings.xml | 15 ++++++++++ app/src/main/res/xml/root_preferences.xml | 13 ++++++++ 6 files changed, 95 insertions(+), 10 deletions(-) diff --git a/app/src/main/kotlin/com/github/gotify/service/WebSocketConnection.kt b/app/src/main/kotlin/com/github/gotify/service/WebSocketConnection.kt index 0199d565..2e171132 100644 --- a/app/src/main/kotlin/com/github/gotify/service/WebSocketConnection.kt +++ b/app/src/main/kotlin/com/github/gotify/service/WebSocketConnection.kt @@ -24,7 +24,9 @@ internal class WebSocketConnection( private val baseUrl: String, settings: SSLSettings, private val token: String?, - private val alarmManager: AlarmManager + private val alarmManager: AlarmManager, + private val reconnectInterval: Int, + private val disableBackoff: Boolean ) { companion object { private val ID = AtomicLong(0) @@ -204,10 +206,16 @@ internal class WebSocketConnection( closed() errorCount++ - val minutes = (errorCount * 2 - 1).coerceAtMost(20) + val seconds = if (disableBackoff) { + reconnectInterval + } else { + ((errorCount * 2 - 1) * reconnectInterval) + .coerceAtMost(TimeUnit.MINUTES.toSeconds(20).toInt()) + } - onFailure.execute(response?.message ?: "unreachable", minutes) - scheduleReconnect(id, TimeUnit.MINUTES.toSeconds(minutes.toLong())) + val rounded = seconds.coerceAtLeast(5) + onFailure.execute(response?.message ?: "unreachable", rounded) + scheduleReconnect(id, rounded.toLong()) } super.onFailure(webSocket, t, response) } @@ -221,7 +229,7 @@ internal class WebSocketConnection( } internal fun interface OnNetworkFailureRunnable { - fun execute(status: String, minutes: Int) + fun execute(status: String, seconds: Int) } internal enum class State { diff --git a/app/src/main/kotlin/com/github/gotify/service/WebSocketService.kt b/app/src/main/kotlin/com/github/gotify/service/WebSocketService.kt index 30df1e08..92be5592 100644 --- a/app/src/main/kotlin/com/github/gotify/service/WebSocketService.kt +++ b/app/src/main/kotlin/com/github/gotify/service/WebSocketService.kt @@ -110,16 +110,30 @@ internal class WebSocketService : Service() { val cm = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager val alarmManager = getSystemService(ALARM_SERVICE) as AlarmManager + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) + var reconnectInterval = sharedPreferences + .getString(getString(R.string.setting_key_reconnect_interval), "60") + ?.trim() + ?.toIntOrNull() ?: 60 + + reconnectInterval = reconnectInterval.coerceIn(5, 60) + + val disableBackoff = sharedPreferences.getBoolean( + getString(R.string.setting_key_backoff), + false + ) connection = WebSocketConnection( settings.url, settings.sslSettings(), settings.token, - alarmManager + alarmManager, + reconnectInterval, + disableBackoff ) .onOpen { onOpen() } .onClose { onClose() } - .onFailure { status, minutes -> onFailure(status, minutes) } + .onFailure { status, seconds -> onFailure(status, seconds) } .onMessage { message -> onMessage(message) } .onReconnected { notifyMissedNotifications() } .start() @@ -183,10 +197,14 @@ internal class WebSocketService : Service() { connection?.scheduleReconnectNow(15) } - private fun onFailure(status: String, minutes: Int) { + private fun onFailure(status: String, seconds: Int) { val title = getString(R.string.websocket_error, status) - val intervalUnit = resources - .getQuantityString(R.plurals.websocket_retry_interval, minutes, minutes) + val intervalUnit = if (seconds >= 60) { + val minutes = seconds / 60 + resources.getQuantityString(R.plurals.websocket_retry_interval, minutes, minutes) + } else { + resources.getQuantityString(R.plurals.websocket_retry_interval_seconds, seconds, seconds) + } showForegroundNotification( title, "${getString(R.string.websocket_reconnect)} $intervalUnit" diff --git a/app/src/main/kotlin/com/github/gotify/settings/SettingsActivity.kt b/app/src/main/kotlin/com/github/gotify/settings/SettingsActivity.kt index f341270b..78b679bb 100644 --- a/app/src/main/kotlin/com/github/gotify/settings/SettingsActivity.kt +++ b/app/src/main/kotlin/com/github/gotify/settings/SettingsActivity.kt @@ -74,6 +74,36 @@ internal class SettingsActivity : getString(R.string.setting_key_notification_channels) )?.isEnabled = true } + findPreference( + getString(R.string.setting_key_reconnect_interval) + )?.let { + it.setOnBindEditTextListener { editText -> + editText.inputType = android.text.InputType.TYPE_CLASS_NUMBER + } + it.onPreferenceChangeListener = + Preference.OnPreferenceChangeListener { _, newValue -> + val value = (newValue as String).trim().toIntOrNull() ?: 60 + if (value < 5 || value > 60) { + Utils.showSnackBar(requireActivity(), "Please enter a value between 5 and 60") + return@OnPreferenceChangeListener false + } + + requestWebSocketRestart() + true + } + } + findPreference( + getString(R.string.setting_key_backoff) + )?.onPreferenceChangeListener = + Preference.OnPreferenceChangeListener { _, _ -> + requestWebSocketRestart() + true + } + } + + private fun requestWebSocketRestart() { + val intent = Intent(requireContext(), com.github.gotify.service.WebSocketService::class.java) + requireContext().startService(intent) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 113e78fc..7e9159bc 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -34,6 +34,7 @@ time_format_absolute time_format_relative + false false true diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d96bb587..3cddb9f0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -120,6 +120,21 @@ in %d minute in %d minutes + + in %d second + in %d seconds + + Reconnect Interval (Seconds) + reconnect_interval + Connection + 10 seconds + 30 seconds + 1 minute (Default) + 5 minutes + Wait time between connection attempts + Constant Retry Interval + Do not increase wait time between retries + reconnect_backoff Gotify foreground notification Min priority messages (<1) diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index 89cac847..3ca75c62 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -51,4 +51,17 @@ android:summary="@string/setting_summary_prompt_onreceive_intent" /> + + + + +