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 2e17113..8c098ed 100644 --- a/app/src/main/kotlin/com/github/gotify/service/WebSocketConnection.kt +++ b/app/src/main/kotlin/com/github/gotify/service/WebSocketConnection.kt @@ -12,6 +12,10 @@ import com.github.gotify.client.model.Message import java.util.Calendar import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicLong +import kotlin.math.pow +import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.OkHttpClient import okhttp3.Request @@ -25,8 +29,8 @@ internal class WebSocketConnection( settings: SSLSettings, private val token: String?, private val alarmManager: AlarmManager, - private val reconnectInterval: Int, - private val disableBackoff: Boolean + private val reconnectDelay: Duration, + private val exponentialBackoff: Boolean ) { companion object { private val ID = AtomicLong(0) @@ -130,19 +134,19 @@ internal class WebSocketConnection( state = State.Disconnected } - fun scheduleReconnectNow(seconds: Long) = scheduleReconnect(ID.get(), seconds) + fun scheduleReconnectNow(scheduleIn: Duration) = scheduleReconnect(ID.get(), scheduleIn) @Synchronized - fun scheduleReconnect(id: Long, seconds: Long) { + fun scheduleReconnect(id: Long, scheduleIn: Duration) { if (state == State.Connecting || state == State.Connected) { return } state = State.Scheduled if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - Logger.info("WebSocket: scheduling a restart in $seconds second(s) (via alarm manager)") + Logger.info("WebSocket: scheduling a restart in $scheduleIn (via alarm manager)") val future = Calendar.getInstance() - future.add(Calendar.SECOND, seconds.toInt()) + future.add(Calendar.SECOND, scheduleIn.inWholeSeconds.toInt()) alarmManagerCallback?.run(alarmManager::cancel) val cb = OnAlarmListener { syncExec(id) { start() } } @@ -155,11 +159,11 @@ internal class WebSocketConnection( null ) } else { - Logger.info("WebSocket: scheduling a restart in $seconds second(s)") + Logger.info("WebSocket: scheduling a restart in $scheduleIn") handlerCallback?.run(reconnectHandler::removeCallbacks) val cb = Runnable { syncExec(id) { start() } } handlerCallback = cb - reconnectHandler.postDelayed(cb, TimeUnit.SECONDS.toMillis(seconds)) + reconnectHandler.postDelayed(cb, scheduleIn.inWholeMilliseconds) } } @@ -206,16 +210,15 @@ internal class WebSocketConnection( closed() errorCount++ - val seconds = if (disableBackoff) { - reconnectInterval - } else { - ((errorCount * 2 - 1) * reconnectInterval) - .coerceAtMost(TimeUnit.MINUTES.toSeconds(20).toInt()) - } - val rounded = seconds.coerceAtLeast(5) - onFailure.execute(response?.message ?: "unreachable", rounded) - scheduleReconnect(id, rounded.toLong()) + var scheduleIn = reconnectDelay + if (exponentialBackoff) { + scheduleIn *= 2.0.pow(errorCount - 1) + } + scheduleIn = scheduleIn.coerceIn(5.seconds..20.minutes) + + onFailure.execute(response?.message ?: "unreachable", scheduleIn) + scheduleReconnect(id, scheduleIn) } super.onFailure(webSocket, t, response) } @@ -229,7 +232,7 @@ internal class WebSocketConnection( } internal fun interface OnNetworkFailureRunnable { - fun execute(status: String, seconds: Int) + fun execute(status: String, reconnectIn: Duration) } 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 c86704b..f5e72e2 100644 --- a/app/src/main/kotlin/com/github/gotify/service/WebSocketService.kt +++ b/app/src/main/kotlin/com/github/gotify/service/WebSocketService.kt @@ -40,6 +40,11 @@ import com.github.gotify.messages.MessagesActivity import io.noties.markwon.Markwon import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicLong +import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds +import kotlin.time.DurationUnit +import kotlin.time.toDuration import org.tinylog.kotlin.Logger internal class WebSocketService : Service() { @@ -111,16 +116,15 @@ 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 + val reconnectDelay = + sharedPreferences.getString( + getString(R.string.setting_key_reconnect_delay), + null + )?.toIntOrNull()?.toDuration(DurationUnit.SECONDS) ?: 1.minutes - reconnectInterval = reconnectInterval.coerceIn(5, 60) - - val disableBackoff = sharedPreferences.getBoolean( - getString(R.string.setting_key_backoff), - false + val exponentialBackoff = sharedPreferences.getBoolean( + getString(R.string.setting_key_exponential_backoff), + true ) connection = WebSocketConnection( @@ -128,12 +132,12 @@ internal class WebSocketService : Service() { settings.sslSettings(), settings.token, alarmManager, - reconnectInterval, - disableBackoff + reconnectDelay, + exponentialBackoff ) .onOpen { onOpen() } .onClose { onClose() } - .onFailure { status, seconds -> onFailure(status, seconds) } + .onFailure { status, reconnectIn -> onFailure(status, reconnectIn) } .onMessage { message -> onMessage(message) } .onReconnected { notifyMissedNotifications() } .start() @@ -194,24 +198,14 @@ internal class WebSocketService : Service() { } private fun doReconnect() { - connection?.scheduleReconnectNow(15) + connection?.scheduleReconnectNow(15.seconds) } - private fun onFailure(status: String, seconds: Int) { + private fun onFailure(status: String, reconnectIn: Duration) { val title = getString(R.string.websocket_error, status) - 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" + getString(R.string.websocket_reconnect, reconnectIn.toString()) ) } 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 ac9c406..30985e7 100644 --- a/app/src/main/kotlin/com/github/gotify/settings/SettingsActivity.kt +++ b/app/src/main/kotlin/com/github/gotify/settings/SettingsActivity.kt @@ -21,6 +21,7 @@ import androidx.preference.SwitchPreferenceCompat import com.github.gotify.R import com.github.gotify.Utils import com.github.gotify.databinding.SettingsActivityBinding +import com.github.gotify.service.WebSocketService import com.google.android.material.dialog.MaterialAlertDialogBuilder internal class SettingsActivity : @@ -75,28 +76,23 @@ internal class SettingsActivity : )?.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 + getString(R.string.setting_key_reconnect_delay) + )?.onPreferenceChangeListener = + Preference.OnPreferenceChangeListener { _, newValue -> + val value = (newValue as String).trim().toIntOrNull() ?: 60 + if (value !in 5..1200) { + Utils.showSnackBar( + requireActivity(), + "Please enter a value between 5 and 1200" + ) + return@OnPreferenceChangeListener false } - } + + requestWebSocketRestart() + true + } findPreference( - getString(R.string.setting_key_backoff) + getString(R.string.setting_key_exponential_backoff) )?.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, _ -> requestWebSocketRestart() @@ -105,8 +101,7 @@ internal class SettingsActivity : } private fun requestWebSocketRestart() { - val intent = - Intent(requireContext(), com.github.gotify.service.WebSocketService::class.java) + val intent = Intent(requireContext(), WebSocketService::class.java) requireContext().startService(intent) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9b55ccf..92d715f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -115,22 +115,14 @@ Cancel Error %s (see logs) - Trying to reconnect - - in %d minute - in %d minutes - - - in %d second - in %d seconds - - Reconnect Interval (Seconds) - reconnect_interval + Trying to reconnect in %s + Reconnect Delay (Seconds) + reconnect_delay Connection - Wait time between connection attempts - Constant Retry Interval - Do not increase wait time between retries - reconnect_backoff + Delay between reconnect attempts + Exponential Backoff + Exponentially increase the reconnect delay for each reconnect attempt + reconnect_exponential_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 3ca75c6..f61aa77 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -54,14 +54,15 @@ + android:key="@string/setting_key_exponential_backoff" + android:title="@string/setting_exponential_backoff_title" + android:summary="@string/setting_exponential_backoff_summary" + android:defaultValue="true" />