Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -24,7 +28,9 @@ internal class WebSocketConnection(
private val baseUrl: String,
settings: SSLSettings,
private val token: String?,
private val alarmManager: AlarmManager
private val alarmManager: AlarmManager,
private val reconnectDelay: Duration,
private val exponentialBackoff: Boolean
Comment on lines +32 to +33
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've made some changes

  • used Durations everwhere
  • renamed interval to delay
  • added exponential backoff
  • inversed the constant retry interval setting

) {
companion object {
private val ID = AtomicLong(0)
Expand Down Expand Up @@ -128,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() } }
Expand All @@ -153,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)
}
}

Expand Down Expand Up @@ -204,10 +210,15 @@ internal class WebSocketConnection(
closed()

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

onFailure.execute(response?.message ?: "unreachable", minutes)
scheduleReconnect(id, TimeUnit.MINUTES.toSeconds(minutes.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)
}
Expand All @@ -221,7 +232,7 @@ internal class WebSocketConnection(
}

internal fun interface OnNetworkFailureRunnable {
fun execute(status: String, minutes: Int)
fun execute(status: String, reconnectIn: Duration)
}

internal enum class State {
Expand Down
30 changes: 23 additions & 7 deletions app/src/main/kotlin/com/github/gotify/service/WebSocketService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -110,16 +115,29 @@ internal class WebSocketService : Service() {

val cm = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
val alarmManager = getSystemService(ALARM_SERVICE) as AlarmManager
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
val reconnectDelay =
sharedPreferences.getString(
getString(R.string.setting_key_reconnect_delay),
null
)?.toIntOrNull()?.toDuration(DurationUnit.SECONDS) ?: 1.minutes

val exponentialBackoff = sharedPreferences.getBoolean(
getString(R.string.setting_key_exponential_backoff),
true
)

connection = WebSocketConnection(
settings.url,
settings.sslSettings(),
settings.token,
alarmManager
alarmManager,
reconnectDelay,
exponentialBackoff
)
.onOpen { onOpen() }
.onClose { onClose() }
.onFailure { status, minutes -> onFailure(status, minutes) }
.onFailure { status, reconnectIn -> onFailure(status, reconnectIn) }
.onMessage { message -> onMessage(message) }
.onReconnected { notifyMissedNotifications() }
.start()
Expand Down Expand Up @@ -180,16 +198,14 @@ internal class WebSocketService : Service() {
}

private fun doReconnect() {
connection?.scheduleReconnectNow(15)
connection?.scheduleReconnectNow(15.seconds)
}

private fun onFailure(status: String, minutes: Int) {
private fun onFailure(status: String, reconnectIn: Duration) {
val title = getString(R.string.websocket_error, status)
val intervalUnit = resources
.getQuantityString(R.plurals.websocket_retry_interval, minutes, minutes)
showForegroundNotification(
title,
"${getString(R.string.websocket_reconnect)} $intervalUnit"
getString(R.string.websocket_reconnect, reconnectIn.toString())
)
}

Expand Down
29 changes: 29 additions & 0 deletions app/src/main/kotlin/com/github/gotify/settings/SettingsActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 :
Expand Down Expand Up @@ -74,6 +75,34 @@ internal class SettingsActivity :
getString(R.string.setting_key_notification_channels)
)?.isEnabled = true
}
findPreference<androidx.preference.EditTextPreference>(
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_exponential_backoff)
)?.onPreferenceChangeListener =
Preference.OnPreferenceChangeListener { _, _ ->
requestWebSocketRestart()
true
}
}

private fun requestWebSocketRestart() {
val intent = Intent(requireContext(), WebSocketService::class.java)
requireContext().startService(intent)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/arrays.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
</string-array>
<string name="time_format_value_absolute">time_format_absolute</string>
<string name="time_format_value_relative">time_format_relative</string>

<bool name="notification_channels">false</bool>
<bool name="exclude_from_recent">false</bool>
<bool name="prompt_onreceive_intent">true</bool>
Expand Down
13 changes: 8 additions & 5 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +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>
<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">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 (&lt;1)</string>
Expand Down
14 changes: 14 additions & 0 deletions app/src/main/res/xml/root_preferences.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,18 @@
android:summary="@string/setting_summary_prompt_onreceive_intent" />
</PreferenceCategory>

<PreferenceCategory app:title="@string/setting_connection">
<EditTextPreference
android:defaultValue="60"
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_exponential_backoff"
android:title="@string/setting_exponential_backoff_title"
android:summary="@string/setting_exponential_backoff_summary"
android:defaultValue="true" />
</PreferenceCategory>

</PreferenceScreen>