Skip to content

Commit 0689d2d

Browse files
authored
Merge pull request #434 from sratabix/configurable-reconnect
add a configurable reconnect option for websocket
2 parents 547d9fd + 9a43db0 commit 0689d2d

6 files changed

Lines changed: 97 additions & 23 deletions

File tree

app/src/main/kotlin/com/github/gotify/service/WebSocketConnection.kt

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import com.github.gotify.client.model.Message
1212
import java.util.Calendar
1313
import java.util.concurrent.TimeUnit
1414
import java.util.concurrent.atomic.AtomicLong
15+
import kotlin.math.pow
16+
import kotlin.time.Duration
17+
import kotlin.time.Duration.Companion.minutes
18+
import kotlin.time.Duration.Companion.seconds
1519
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
1620
import okhttp3.OkHttpClient
1721
import okhttp3.Request
@@ -24,7 +28,9 @@ internal class WebSocketConnection(
2428
private val baseUrl: String,
2529
settings: SSLSettings,
2630
private val token: String?,
27-
private val alarmManager: AlarmManager
31+
private val alarmManager: AlarmManager,
32+
private val reconnectDelay: Duration,
33+
private val exponentialBackoff: Boolean
2834
) {
2935
companion object {
3036
private val ID = AtomicLong(0)
@@ -128,19 +134,19 @@ internal class WebSocketConnection(
128134
state = State.Disconnected
129135
}
130136

131-
fun scheduleReconnectNow(seconds: Long) = scheduleReconnect(ID.get(), seconds)
137+
fun scheduleReconnectNow(scheduleIn: Duration) = scheduleReconnect(ID.get(), scheduleIn)
132138

133139
@Synchronized
134-
fun scheduleReconnect(id: Long, seconds: Long) {
140+
fun scheduleReconnect(id: Long, scheduleIn: Duration) {
135141
if (state == State.Connecting || state == State.Connected) {
136142
return
137143
}
138144
state = State.Scheduled
139145

140146
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
141-
Logger.info("WebSocket: scheduling a restart in $seconds second(s) (via alarm manager)")
147+
Logger.info("WebSocket: scheduling a restart in $scheduleIn (via alarm manager)")
142148
val future = Calendar.getInstance()
143-
future.add(Calendar.SECOND, seconds.toInt())
149+
future.add(Calendar.SECOND, scheduleIn.inWholeSeconds.toInt())
144150

145151
alarmManagerCallback?.run(alarmManager::cancel)
146152
val cb = OnAlarmListener { syncExec(id) { start() } }
@@ -153,11 +159,11 @@ internal class WebSocketConnection(
153159
null
154160
)
155161
} else {
156-
Logger.info("WebSocket: scheduling a restart in $seconds second(s)")
162+
Logger.info("WebSocket: scheduling a restart in $scheduleIn")
157163
handlerCallback?.run(reconnectHandler::removeCallbacks)
158164
val cb = Runnable { syncExec(id) { start() } }
159165
handlerCallback = cb
160-
reconnectHandler.postDelayed(cb, TimeUnit.SECONDS.toMillis(seconds))
166+
reconnectHandler.postDelayed(cb, scheduleIn.inWholeMilliseconds)
161167
}
162168
}
163169

@@ -204,10 +210,15 @@ internal class WebSocketConnection(
204210
closed()
205211

206212
errorCount++
207-
val minutes = (errorCount * 2 - 1).coerceAtMost(20)
208213

209-
onFailure.execute(response?.message ?: "unreachable", minutes)
210-
scheduleReconnect(id, TimeUnit.MINUTES.toSeconds(minutes.toLong()))
214+
var scheduleIn = reconnectDelay
215+
if (exponentialBackoff) {
216+
scheduleIn *= 2.0.pow(errorCount - 1)
217+
}
218+
scheduleIn = scheduleIn.coerceIn(5.seconds..20.minutes)
219+
220+
onFailure.execute(response?.message ?: "unreachable", scheduleIn)
221+
scheduleReconnect(id, scheduleIn)
211222
}
212223
super.onFailure(webSocket, t, response)
213224
}
@@ -221,7 +232,7 @@ internal class WebSocketConnection(
221232
}
222233

223234
internal fun interface OnNetworkFailureRunnable {
224-
fun execute(status: String, minutes: Int)
235+
fun execute(status: String, reconnectIn: Duration)
225236
}
226237

227238
internal enum class State {

app/src/main/kotlin/com/github/gotify/service/WebSocketService.kt

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ import com.github.gotify.messages.MessagesActivity
4040
import io.noties.markwon.Markwon
4141
import java.util.concurrent.ConcurrentHashMap
4242
import java.util.concurrent.atomic.AtomicLong
43+
import kotlin.time.Duration
44+
import kotlin.time.Duration.Companion.minutes
45+
import kotlin.time.Duration.Companion.seconds
46+
import kotlin.time.DurationUnit
47+
import kotlin.time.toDuration
4348
import org.tinylog.kotlin.Logger
4449

4550
internal class WebSocketService : Service() {
@@ -110,16 +115,29 @@ internal class WebSocketService : Service() {
110115

111116
val cm = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
112117
val alarmManager = getSystemService(ALARM_SERVICE) as AlarmManager
118+
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
119+
val reconnectDelay =
120+
sharedPreferences.getString(
121+
getString(R.string.setting_key_reconnect_delay),
122+
null
123+
)?.toIntOrNull()?.toDuration(DurationUnit.SECONDS) ?: 1.minutes
124+
125+
val exponentialBackoff = sharedPreferences.getBoolean(
126+
getString(R.string.setting_key_exponential_backoff),
127+
true
128+
)
113129

114130
connection = WebSocketConnection(
115131
settings.url,
116132
settings.sslSettings(),
117133
settings.token,
118-
alarmManager
134+
alarmManager,
135+
reconnectDelay,
136+
exponentialBackoff
119137
)
120138
.onOpen { onOpen() }
121139
.onClose { onClose() }
122-
.onFailure { status, minutes -> onFailure(status, minutes) }
140+
.onFailure { status, reconnectIn -> onFailure(status, reconnectIn) }
123141
.onMessage { message -> onMessage(message) }
124142
.onReconnected { notifyMissedNotifications() }
125143
.start()
@@ -180,16 +198,14 @@ internal class WebSocketService : Service() {
180198
}
181199

182200
private fun doReconnect() {
183-
connection?.scheduleReconnectNow(15)
201+
connection?.scheduleReconnectNow(15.seconds)
184202
}
185203

186-
private fun onFailure(status: String, minutes: Int) {
204+
private fun onFailure(status: String, reconnectIn: Duration) {
187205
val title = getString(R.string.websocket_error, status)
188-
val intervalUnit = resources
189-
.getQuantityString(R.plurals.websocket_retry_interval, minutes, minutes)
190206
showForegroundNotification(
191207
title,
192-
"${getString(R.string.websocket_reconnect)} $intervalUnit"
208+
getString(R.string.websocket_reconnect, reconnectIn.toString())
193209
)
194210
}
195211

app/src/main/kotlin/com/github/gotify/settings/SettingsActivity.kt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import androidx.preference.SwitchPreferenceCompat
2121
import com.github.gotify.R
2222
import com.github.gotify.Utils
2323
import com.github.gotify.databinding.SettingsActivityBinding
24+
import com.github.gotify.service.WebSocketService
2425
import com.google.android.material.dialog.MaterialAlertDialogBuilder
2526

2627
internal class SettingsActivity :
@@ -74,6 +75,34 @@ internal class SettingsActivity :
7475
getString(R.string.setting_key_notification_channels)
7576
)?.isEnabled = true
7677
}
78+
findPreference<androidx.preference.EditTextPreference>(
79+
getString(R.string.setting_key_reconnect_delay)
80+
)?.onPreferenceChangeListener =
81+
Preference.OnPreferenceChangeListener { _, newValue ->
82+
val value = (newValue as String).trim().toIntOrNull() ?: 60
83+
if (value !in 5..1200) {
84+
Utils.showSnackBar(
85+
requireActivity(),
86+
"Please enter a value between 5 and 1200"
87+
)
88+
return@OnPreferenceChangeListener false
89+
}
90+
91+
requestWebSocketRestart()
92+
true
93+
}
94+
findPreference<SwitchPreferenceCompat>(
95+
getString(R.string.setting_key_exponential_backoff)
96+
)?.onPreferenceChangeListener =
97+
Preference.OnPreferenceChangeListener { _, _ ->
98+
requestWebSocketRestart()
99+
true
100+
}
101+
}
102+
103+
private fun requestWebSocketRestart() {
104+
val intent = Intent(requireContext(), WebSocketService::class.java)
105+
requireContext().startService(intent)
77106
}
78107

79108
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

app/src/main/res/values/arrays.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
</string-array>
3535
<string name="time_format_value_absolute">time_format_absolute</string>
3636
<string name="time_format_value_relative">time_format_relative</string>
37+
3738
<bool name="notification_channels">false</bool>
3839
<bool name="exclude_from_recent">false</bool>
3940
<bool name="prompt_onreceive_intent">true</bool>

app/src/main/res/values/strings.xml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,14 @@
115115
<string name="action_dialog_button_cancel">Cancel</string>
116116

117117
<string name="websocket_error">Error %s (see logs)</string>
118-
<string name="websocket_reconnect">Trying to reconnect</string>
119-
<plurals name="websocket_retry_interval">
120-
<item quantity="one">in %d minute</item>
121-
<item quantity="other">in %d minutes</item>
122-
</plurals>
118+
<string name="websocket_reconnect">Trying to reconnect in %s</string>
119+
<string name="setting_reconnect_delay">Reconnect Delay (Seconds)</string>
120+
<string name="setting_key_reconnect_delay">reconnect_delay</string>
121+
<string name="setting_connection">Connection</string>
122+
<string name="setting_reconnect_interval_summary">Delay between reconnect attempts</string>
123+
<string name="setting_exponential_backoff_title">Exponential Backoff</string>
124+
<string name="setting_exponential_backoff_summary">Exponentially increase the reconnect delay for each reconnect attempt</string>
125+
<string name="setting_key_exponential_backoff">reconnect_exponential_backoff</string>
123126

124127
<string name="notification_channel_title_foreground">Gotify foreground notification</string>
125128
<string name="notification_channel_title_min">Min priority messages (&lt;1)</string>

app/src/main/res/xml/root_preferences.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,18 @@
5151
android:summary="@string/setting_summary_prompt_onreceive_intent" />
5252
</PreferenceCategory>
5353

54+
<PreferenceCategory app:title="@string/setting_connection">
55+
<EditTextPreference
56+
android:defaultValue="60"
57+
android:inputType="numberSigned"
58+
android:key="@string/setting_key_reconnect_delay"
59+
android:title="@string/setting_reconnect_delay"
60+
android:summary="@string/setting_reconnect_interval_summary" />
61+
<SwitchPreferenceCompat
62+
android:key="@string/setting_key_exponential_backoff"
63+
android:title="@string/setting_exponential_backoff_title"
64+
android:summary="@string/setting_exponential_backoff_summary"
65+
android:defaultValue="true" />
66+
</PreferenceCategory>
67+
5468
</PreferenceScreen>

0 commit comments

Comments
 (0)