diff --git a/app/src/main/java/io/heckel/ntfy/db/Database.kt b/app/src/main/java/io/heckel/ntfy/db/Database.kt index 5e9079da..c3212a6a 100644 --- a/app/src/main/java/io/heckel/ntfy/db/Database.kt +++ b/app/src/main/java/io/heckel/ntfy/db/Database.kt @@ -88,6 +88,19 @@ data class ConnectionDetails( fun hasError(): Boolean { return error != null } + + fun isConnectionRefused(): Boolean { + return hasCauseOfType() + } + + private inline fun hasCauseOfType(): Boolean { + var current: Throwable? = error + while (current != null) { + if (current is T) return true + current = current.cause + } + return false + } } data class SubscriptionWithMetadata( diff --git a/app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt b/app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt index 697b03d9..d9e8f0b3 100644 --- a/app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt +++ b/app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt @@ -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() diff --git a/app/src/main/java/io/heckel/ntfy/ui/ConnectionErrorFragment.kt b/app/src/main/java/io/heckel/ntfy/ui/ConnectionErrorFragment.kt index 8bb1952d..6924d092 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/ConnectionErrorFragment.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/ConnectionErrorFragment.kt @@ -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 diff --git a/app/src/main/res/drawable/ic_refresh_white_24dp.xml b/app/src/main/res/drawable/ic_refresh_white_24dp.xml new file mode 100644 index 00000000..fd33bf07 --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh_white_24dp.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/layout/fragment_connection_error_dialog.xml b/app/src/main/res/layout/fragment_connection_error_dialog.xml index 5ca44f6e..af12bc55 100644 --- a/app/src/main/res/layout/fragment_connection_error_dialog.xml +++ b/app/src/main/res/layout/fragment_connection_error_dialog.xml @@ -41,19 +41,6 @@ android:layout_height="wrap_content" android:paddingBottom="16dp"> - - - + app:layout_constraintTop_toTopOf="parent"> - - - - + @@ -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" /> - - + - - + @@ -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"> + Details No additional details available No error + Connection refused. The server may be down or the address may be incorrect. Copy Retry now Retrying in %1$ds…