Special case for connection refused

This commit is contained in:
Philipp Heckel 2026-01-11 17:42:51 -05:00
parent ed9ce05282
commit 1dcd77287c
7 changed files with 81 additions and 83 deletions

View file

@ -88,6 +88,19 @@ data class ConnectionDetails(
fun hasError(): Boolean {
return error != null
}
fun isConnectionRefused(): Boolean {
return hasCauseOfType<java.net.ConnectException>()
}
private inline fun <reified T : Throwable> hasCauseOfType(): Boolean {
var current: Throwable? = error
while (current != null) {
if (current is T) return true
current = current.cause
}
return false
}
}
data class SubscriptionWithMetadata(

View file

@ -202,7 +202,7 @@ class SubscriberService : Service() {
* It is guaranteed that only one of function is run at a time (see mutex above).
*/
private suspend fun reallyRefreshConnections(scope: CoroutineScope) {
// Group INSTANT subscriptions by base URL, there is only one connection per base URL
// Group instant subscriptions by base URL, there is only one connection per base URL
val instantSubscriptions = repository.getSubscriptions().filter { s -> s.instant }
val activeConnectionIds = connections.keys().toList().toSet()
val connectionProtocol = repository.getConnectionProtocol()

View file

@ -13,7 +13,6 @@ import android.widget.HorizontalScrollView
import android.widget.TextView
import androidx.fragment.app.DialogFragment
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.chip.Chip
import com.google.android.material.color.MaterialColors
import com.google.android.material.textfield.TextInputLayout
import io.heckel.ntfy.R
@ -31,10 +30,9 @@ class ConnectionErrorFragment : DialogFragment() {
private lateinit var toolbar: MaterialToolbar
private lateinit var serverLayout: TextInputLayout
private lateinit var serverDropdown: AutoCompleteTextView
private lateinit var descriptionTextView: TextView
private lateinit var errorTextView: TextView
private lateinit var countdownTextView: TextView
private lateinit var retryChip: Chip
private lateinit var detailsChip: Chip
private lateinit var detailsScrollView: HorizontalScrollView
private lateinit var stackTraceTextView: TextView
@ -73,6 +71,11 @@ class ConnectionErrorFragment : DialogFragment() {
toolbar.setNavigationOnClickListener { dismiss() }
toolbar.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.connection_error_dialog_action_retry -> {
SubscriberServiceManager.refresh(requireContext())
dismiss()
true
}
R.id.connection_error_dialog_action_copy -> {
copyErrorToClipboard()
true
@ -83,16 +86,15 @@ class ConnectionErrorFragment : DialogFragment() {
// Tint menu icons to match toolbar text color
val iconColor = MaterialColors.getColor(requireContext(), R.attr.colorOnSurface, Color.BLACK)
val copyMenuItem = toolbar.menu.findItem(R.id.connection_error_dialog_action_copy)
copyMenuItem?.icon?.setTint(iconColor)
toolbar.menu.findItem(R.id.connection_error_dialog_action_retry)?.icon?.setTint(iconColor)
toolbar.menu.findItem(R.id.connection_error_dialog_action_copy)?.icon?.setTint(iconColor)
// Get view references
serverLayout = view.findViewById(R.id.connection_error_dialog_server_layout)
serverDropdown = view.findViewById(R.id.connection_error_dialog_server_dropdown)
descriptionTextView = view.findViewById(R.id.connection_error_dialog_description)
errorTextView = view.findViewById(R.id.connection_error_dialog_error_text)
countdownTextView = view.findViewById(R.id.connection_error_dialog_countdown)
retryChip = view.findViewById(R.id.connection_error_dialog_retry_chip)
detailsChip = view.findViewById(R.id.connection_error_dialog_details_chip)
detailsScrollView = view.findViewById(R.id.connection_error_dialog_details_scroll)
stackTraceTextView = view.findViewById(R.id.connection_error_dialog_stack_trace)
@ -115,17 +117,6 @@ class ConnectionErrorFragment : DialogFragment() {
selectedBaseUrl = baseUrls.firstOrNull()
updateErrorDisplay()
// Toggle details visibility using chip checked state
detailsChip.setOnCheckedChangeListener { _, isChecked ->
updateDetailsVisibility(isChecked)
}
// Retry now button
retryChip.setOnClickListener {
SubscriberServiceManager.refresh(requireContext())
dismiss()
}
// Observe connection details to update countdown when it changes
repository.getConnectionDetailsLiveData().observe(this) { details ->
connectionDetails = if (filterBaseUrl != null) {
@ -162,17 +153,26 @@ class ConnectionErrorFragment : DialogFragment() {
}
private fun updateErrorDisplay() {
val details = selectedBaseUrl?.let { connectionDetails[it] }
val baseUrl = selectedBaseUrl ?: return
descriptionTextView.text = getString(R.string.connection_error_dialog_message)
val details = connectionDetails[baseUrl]
if (details != null && details.hasError()) {
errorTextView.text = details.error?.message ?: getString(R.string.connection_error_dialog_no_error)
stackTraceTextView.text = details.getStackTraceString().ifEmpty {
getString(R.string.connection_error_dialog_no_stack_trace)
errorTextView.text = when {
details.isConnectionRefused() -> getString(R.string.connection_error_dialog_connection_refused)
else -> details.error?.message ?: getString(R.string.connection_error_dialog_no_error)
}
val stackTrace = details.getStackTraceString()
if (stackTrace.isNotEmpty()) {
stackTraceTextView.text = stackTrace
detailsScrollView.visibility = View.VISIBLE
} else {
detailsScrollView.visibility = View.GONE
}
} else {
errorTextView.text = getString(R.string.connection_error_dialog_no_error)
stackTraceTextView.text = ""
detailsScrollView.visibility = View.GONE
}
updateDetailsVisibility(detailsChip.isChecked)
updateCountdown()
}
@ -193,10 +193,6 @@ class ConnectionErrorFragment : DialogFragment() {
}
}
private fun updateDetailsVisibility(visible: Boolean) {
detailsScrollView.visibility = if (visible) View.VISIBLE else View.GONE
}
private fun copyErrorToClipboard() {
val baseUrl = selectedBaseUrl ?: return
val details = connectionDetails[baseUrl] ?: return

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="#FFFFFF"
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z"/>
</vector>

View file

@ -41,19 +41,6 @@
android:layout_height="wrap_content"
android:paddingBottom="16dp">
<!-- Description text (left aligned like UserFragment) -->
<TextView
android:id="@+id/connection_error_dialog_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="4dp"
android:paddingEnd="4dp"
android:paddingTop="16dp"
android:text="@string/connection_error_dialog_message"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- Server dropdown (Material 3 style, only visible when multiple servers have errors) -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/connection_error_dialog_server_layout"
@ -65,7 +52,7 @@
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/connection_error_dialog_description">
app:layout_constraintTop_toTopOf="parent">
<AutoCompleteTextView
android:id="@+id/connection_error_dialog_server_dropdown"
@ -76,27 +63,16 @@
</com.google.android.material.textfield.TextInputLayout>
<!-- Error icon -->
<ImageView
android:id="@+id/connection_error_dialog_error_icon"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginTop="16dp"
app:srcCompat="@drawable/ic_error_red_24dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/connection_error_dialog_server_layout" />
<!-- Error message text (red, like AddFragment) -->
<!-- Description text (left aligned like UserFragment) -->
<TextView
android:id="@+id/connection_error_dialog_error_text"
android:layout_width="0dp"
android:id="@+id/connection_error_dialog_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:paddingStart="4dp"
android:paddingEnd="4dp"
android:textAppearance="@style/DangerText"
android:paddingTop="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/connection_error_dialog_error_icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/connection_error_dialog_server_layout" />
<!-- Countdown text -->
@ -104,40 +80,36 @@
android:id="@+id/connection_error_dialog_countdown"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginTop="16dp"
android:paddingStart="4dp"
android:paddingEnd="4dp"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/connection_error_dialog_error_text" />
app:layout_constraintTop_toBottomOf="@id/connection_error_dialog_description" />
<!-- Retry now chip (left aligned) -->
<com.google.android.material.chip.Chip
android:id="@+id/connection_error_dialog_retry_chip"
style="@style/Widget.Material3.Chip.Assist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/connection_error_dialog_retry_now"
app:chipBackgroundColor="@color/chip_background_state"
app:chipStrokeWidth="0dp"
<!-- Error icon -->
<ImageView
android:id="@+id/connection_error_dialog_error_icon"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginTop="8dp"
app:srcCompat="@drawable/ic_error_red_24dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/connection_error_dialog_countdown" />
<!-- Details chip (right aligned) -->
<com.google.android.material.chip.Chip
android:id="@+id/connection_error_dialog_details_chip"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
<!-- Error message text (red, like AddFragment) -->
<TextView
android:id="@+id/connection_error_dialog_error_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/connection_error_dialog_details"
app:chipBackgroundColor="@color/chip_background_state"
app:chipStrokeWidth="0dp"
app:checkedIconVisible="false"
android:layout_marginTop="8dp"
android:paddingStart="4dp"
android:paddingEnd="4dp"
android:textAppearance="@style/DangerText"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/connection_error_dialog_error_icon"
app:layout_constraintTop_toBottomOf="@id/connection_error_dialog_countdown" />
<!-- Stack trace (scrollable horizontally, no word wrap) -->
@ -145,12 +117,12 @@
android:id="@+id/connection_error_dialog_details_scroll"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginTop="16dp"
android:fillViewport="true"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/connection_error_dialog_retry_chip">
app:layout_constraintTop_toBottomOf="@id/connection_error_dialog_error_text">
<ScrollView
android:layout_width="wrap_content"

View file

@ -1,6 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/connection_error_dialog_action_retry"
android:icon="@drawable/ic_refresh_white_24dp"
android:title="@string/connection_error_dialog_retry_now"
app:showAsAction="always" />
<item
android:id="@+id/connection_error_dialog_action_copy"
android:icon="@drawable/ic_content_copy_white_24dp"

View file

@ -297,6 +297,7 @@
<string name="connection_error_dialog_details">Details</string>
<string name="connection_error_dialog_no_stack_trace">No additional details available</string>
<string name="connection_error_dialog_no_error">No error</string>
<string name="connection_error_dialog_connection_refused">Connection refused. The server may be down or the address may be incorrect.</string>
<string name="connection_error_dialog_copy">Copy</string>
<string name="connection_error_dialog_retry_now">Retry now</string>
<string name="connection_error_dialog_retry_countdown">Retrying in %1$ds…</string>