mirror of
https://github.com/gotify/android.git
synced 2026-05-06 22:26:15 +08:00
fix: use duration and exponential backoff
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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<androidx.preference.EditTextPreference>(
|
||||
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<SwitchPreferenceCompat>(
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -115,22 +115,14 @@
|
||||
<string name="action_dialog_button_cancel">Cancel</string>
|
||||
|
||||
<string name="websocket_error">Error %s (see logs)</string>
|
||||
<string name="websocket_reconnect">Trying to reconnect</string>
|
||||
<plurals name="websocket_retry_interval">
|
||||
<item quantity="one">in %d minute</item>
|
||||
<item quantity="other">in %d minutes</item>
|
||||
</plurals>
|
||||
<plurals name="websocket_retry_interval_seconds">
|
||||
<item quantity="one">in %d second</item>
|
||||
<item quantity="other">in %d seconds</item>
|
||||
</plurals>
|
||||
<string name="setting_reconnect_interval">Reconnect Interval (Seconds)</string>
|
||||
<string name="setting_key_reconnect_interval">reconnect_interval</string>
|
||||
<string name="websocket_reconnect">Trying to reconnect in %s</string>
|
||||
<string name="setting_reconnect_delay">Reconnect Delay (Seconds)</string>
|
||||
<string name="setting_key_reconnect_delay">reconnect_delay</string>
|
||||
<string name="setting_connection">Connection</string>
|
||||
<string name="setting_reconnect_interval_summary">Wait time between connection attempts</string>
|
||||
<string name="setting_backoff_title">Constant Retry Interval</string>
|
||||
<string name="setting_backoff_summary">Do not increase wait time between retries</string>
|
||||
<string name="setting_key_backoff">reconnect_backoff</string>
|
||||
<string name="setting_reconnect_interval_summary">Delay between reconnect attempts</string>
|
||||
<string name="setting_exponential_backoff_title">Exponential Backoff</string>
|
||||
<string name="setting_exponential_backoff_summary">Exponentially increase the reconnect delay for each reconnect attempt</string>
|
||||
<string name="setting_key_exponential_backoff">reconnect_exponential_backoff</string>
|
||||
|
||||
<string name="notification_channel_title_foreground">Gotify foreground notification</string>
|
||||
<string name="notification_channel_title_min">Min priority messages (<1)</string>
|
||||
|
||||
@@ -54,14 +54,15 @@
|
||||
<PreferenceCategory app:title="@string/setting_connection">
|
||||
<EditTextPreference
|
||||
android:defaultValue="60"
|
||||
android:key="@string/setting_key_reconnect_interval"
|
||||
android:title="@string/setting_reconnect_interval"
|
||||
android:inputType="numberSigned"
|
||||
android:key="@string/setting_key_reconnect_delay"
|
||||
android:title="@string/setting_reconnect_delay"
|
||||
android:summary="@string/setting_reconnect_interval_summary" />
|
||||
<SwitchPreferenceCompat
|
||||
android:key="@string/setting_key_backoff"
|
||||
android:title="@string/setting_backoff_title"
|
||||
android:summary="@string/setting_backoff_summary"
|
||||
android:defaultValue="false" />
|
||||
android:key="@string/setting_key_exponential_backoff"
|
||||
android:title="@string/setting_exponential_backoff_title"
|
||||
android:summary="@string/setting_exponential_backoff_summary"
|
||||
android:defaultValue="true" />
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
||||
|
||||
Reference in New Issue
Block a user