From feb2907cd4d1a38b57a2c5d63482ab2a28faa998 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Sun, 11 Jan 2026 15:08:41 -0500 Subject: [PATCH] Error dialog refinements --- .../heckel/ntfy/ui/ConnectionErrorFragment.kt | 93 ++++++- .../java/io/heckel/ntfy/ui/DetailActivity.kt | 28 +++ .../java/io/heckel/ntfy/ui/MainActivity.kt | 4 +- .../fragment_connection_error_dialog.xml | 233 +++++++++++------- .../res/menu/menu_connection_error_dialog.xml | 9 + .../main/res/menu/menu_detail_action_bar.xml | 6 + app/src/main/res/values/strings.xml | 5 +- 7 files changed, 272 insertions(+), 106 deletions(-) create mode 100644 app/src/main/res/menu/menu_connection_error_dialog.xml 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 e0b00a2c..14096044 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/ConnectionErrorFragment.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/ConnectionErrorFragment.kt @@ -1,30 +1,37 @@ package io.heckel.ntfy.ui import android.app.Dialog +import android.graphics.Color import android.os.Bundle import android.view.View +import android.view.ViewGroup import android.widget.AdapterView import android.widget.ArrayAdapter -import android.widget.ImageView -import android.widget.ScrollView +import android.widget.HorizontalScrollView import android.widget.Spinner import android.widget.TextView +import android.widget.Toast import androidx.fragment.app.DialogFragment -import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.appbar.MaterialToolbar +import com.google.android.material.color.MaterialColors import io.heckel.ntfy.R import io.heckel.ntfy.db.ConnectionError import io.heckel.ntfy.db.Repository +import io.heckel.ntfy.util.copyToClipboard class ConnectionErrorFragment : DialogFragment() { private lateinit var repository: Repository private var connectionErrors: Map = emptyMap() private var selectedBaseUrl: String? = null private var detailsVisible = false + private var filterBaseUrl: String? = null + private lateinit var toolbar: MaterialToolbar + private lateinit var serverLabel: TextView private lateinit var serverSpinner: Spinner private lateinit var errorTextView: TextView private lateinit var showDetailsTextView: TextView - private lateinit var detailsScrollView: ScrollView + private lateinit var detailsScrollView: HorizontalScrollView private lateinit var stackTraceTextView: TextView override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { @@ -32,14 +39,43 @@ class ConnectionErrorFragment : DialogFragment() { throw IllegalStateException("Activity cannot be null") } + // Get optional baseUrl filter from arguments + filterBaseUrl = arguments?.getString(ARG_BASE_URL) + // Dependencies repository = Repository.getInstance(requireContext()) - connectionErrors = repository.getConnectionErrors() + + // Get connection errors, optionally filtered by baseUrl + val allErrors = repository.getConnectionErrors() + connectionErrors = if (filterBaseUrl != null) { + allErrors.filterKeys { it == filterBaseUrl } + } else { + allErrors + } // Build root view val view = requireActivity().layoutInflater.inflate(R.layout.fragment_connection_error_dialog, null) + // Setup toolbar + toolbar = view.findViewById(R.id.connection_error_dialog_toolbar) + toolbar.setNavigationOnClickListener { dismiss() } + toolbar.setOnMenuItemClickListener { menuItem -> + when (menuItem.itemId) { + R.id.connection_error_dialog_action_copy -> { + copyErrorToClipboard() + true + } + else -> false + } + } + + // 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) + // Get view references + serverLabel = view.findViewById(R.id.connection_error_dialog_server_label) serverSpinner = view.findViewById(R.id.connection_error_dialog_server_spinner) errorTextView = view.findViewById(R.id.connection_error_dialog_error_text) showDetailsTextView = view.findViewById(R.id.connection_error_dialog_show_details) @@ -49,6 +85,7 @@ class ConnectionErrorFragment : DialogFragment() { // Setup server spinner if multiple errors val baseUrls = connectionErrors.keys.toList() if (baseUrls.size > 1) { + serverLabel.visibility = View.VISIBLE serverSpinner.visibility = View.VISIBLE val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, baseUrls) adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) @@ -61,6 +98,7 @@ class ConnectionErrorFragment : DialogFragment() { override fun onNothingSelected(parent: AdapterView<*>?) {} } } else { + serverLabel.visibility = View.GONE serverSpinner.visibility = View.GONE } @@ -74,12 +112,21 @@ class ConnectionErrorFragment : DialogFragment() { updateDetailsVisibility() } - return MaterialAlertDialogBuilder(requireContext()) - .setView(view) - .setPositiveButton(R.string.connection_error_dialog_dismiss) { dialog, _ -> - dialog.dismiss() - } - .create() + // Build dialog + val dialog = Dialog(requireContext(), R.style.Theme_App_FullScreenDialog) + dialog.setContentView(view) + + return dialog + } + + override fun onStart() { + super.onStart() + dialog?.window?.apply { + setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + } } private fun updateErrorDisplay() { @@ -106,7 +153,31 @@ class ConnectionErrorFragment : DialogFragment() { } } + private fun copyErrorToClipboard() { + val error = selectedBaseUrl?.let { connectionErrors[it] } ?: return + val text = buildString { + appendLine("Server: ${error.baseUrl}") + appendLine("Error: ${error.message}") + appendLine() + appendLine("Stack trace:") + append(error.getStackTraceString().ifEmpty { "No stack trace available" }) + } + copyToClipboard(requireContext(), "connection error", text) + Toast.makeText(context, R.string.connection_error_dialog_copied, Toast.LENGTH_SHORT).show() + } + companion object { const val TAG = "NtfyConnectionErrorFragment" + private const val ARG_BASE_URL = "base_url" + + fun newInstance(baseUrl: String? = null): ConnectionErrorFragment { + val fragment = ConnectionErrorFragment() + if (baseUrl != null) { + val args = Bundle() + args.putString(ARG_BASE_URL, baseUrl) + fragment.arguments = args + } + return fragment + } } } diff --git a/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt index 4664f62c..02c1afd7 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt @@ -361,6 +361,11 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet SubscriberServiceManager.refresh(this) } + // Observe connection errors and update menu item visibility + repository.getConnectionErrorsLiveData().observe(this) { errors -> + showHideConnectionErrorMenuItem(errors) + } + // Mark this subscription as "open" so we don't receive notifications for it repository.detailViewSubscriptionId.set(subscriptionId) @@ -508,6 +513,7 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet showHideInstantMenuItems(subscriptionInstant) showHideMutedUntilMenuItems(subscriptionMutedUntil) showHideCopyMenuItems(subscription.baseUrl) + showHideConnectionErrorMenuItem(repository.getConnectionErrors()) updateTitle(subscriptionDisplayName) } } @@ -550,6 +556,7 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet showHideInstantMenuItems(subscriptionInstant) showHideMutedUntilMenuItems(subscriptionMutedUntil) showHideCopyMenuItems(subscriptionBaseUrl) + showHideConnectionErrorMenuItem(repository.getConnectionErrors()) // Regularly check if "notification muted" time has passed // NOTE: This is done here, because then we know that we've initialized the menu items. @@ -603,6 +610,10 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet onInstantEnableClick(enable = false) true } + R.id.detail_menu_connection_error -> { + onConnectionErrorClick() + true + } R.id.detail_menu_copy_url -> { onCopyUrlClick() true @@ -668,6 +679,12 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet } } + private fun onConnectionErrorClick() { + Log.d(TAG, "Showing connection error dialog for ${subscriptionBaseUrl}") + val connectionErrorFragment = ConnectionErrorFragment.newInstance(subscriptionBaseUrl) + connectionErrorFragment.show(supportFragmentManager, ConnectionErrorFragment.TAG) + } + override fun onNotificationMutedUntilChanged(mutedUntilTimestamp: Long) { lifecycleScope.launch(Dispatchers.IO) { Log.d(TAG, "Setting subscription 'muted until' to $mutedUntilTimestamp") @@ -800,6 +817,17 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet } } + private fun showHideConnectionErrorMenuItem(errors: Map) { + if (!this::menu.isInitialized) { + return + } + runOnUiThread { + val connectionErrorItem = menu.findItem(R.id.detail_menu_connection_error) + // Only show if there's an error for this subscription's base URL + connectionErrorItem?.isVisible = errors.containsKey(subscriptionBaseUrl) + } + } + private fun updateTitle(subscriptionDisplayName: String) { runOnUiThread { title = subscriptionDisplayName diff --git a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt index badeafb3..312289de 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt @@ -375,6 +375,7 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific override fun onResume() { super.onResume() showHideNotificationMenuItems() + showHideConnectionErrorMenuItem(repository.getConnectionErrors()) redrawList() } @@ -492,6 +493,7 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific } showHideNotificationMenuItems() + showHideConnectionErrorMenuItem(repository.getConnectionErrors()) checkSubscriptionsMuted() // This is done here, because then we know that we've initialized the menu return true } @@ -628,7 +630,7 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific private fun onConnectionErrorClick() { Log.d(TAG, "Showing connection error dialog") - val connectionErrorFragment = ConnectionErrorFragment() + val connectionErrorFragment = ConnectionErrorFragment.newInstance() connectionErrorFragment.show(supportFragmentManager, ConnectionErrorFragment.TAG) } 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 e6857ae8..bf93ce92 100644 --- a/app/src/main/res/layout/fragment_connection_error_dialog.xml +++ b/app/src/main/res/layout/fragment_connection_error_dialog.xml @@ -1,111 +1,158 @@ - + android:layout_height="match_parent" + android:background="?attr/colorSurface" + android:fitsSystemWindows="true"> - + android:background="?attr/colorSurface" + app:elevation="0dp" + app:liftOnScroll="false"> - + - - - - - - - + + android:layout_height="match_parent" + android:paddingHorizontal="?dialogPreferredPadding" + app:layout_behavior="@string/appbar_scrolling_view_behavior"> - + android:orientation="vertical" + android:paddingBottom="16dp"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/app/src/main/res/menu/menu_connection_error_dialog.xml b/app/src/main/res/menu/menu_connection_error_dialog.xml new file mode 100644 index 00000000..62b9404b --- /dev/null +++ b/app/src/main/res/menu/menu_connection_error_dialog.xml @@ -0,0 +1,9 @@ + + + + diff --git a/app/src/main/res/menu/menu_detail_action_bar.xml b/app/src/main/res/menu/menu_detail_action_bar.xml index b90c71fb..7018dfa2 100644 --- a/app/src/main/res/menu/menu_detail_action_bar.xml +++ b/app/src/main/res/menu/menu_detail_action_bar.xml @@ -26,6 +26,12 @@ android:icon="@drawable/ic_bolt_white_24dp" android:title="@string/detail_menu_disable_instant" app:showAsAction="ifRoom" /> + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c49c23fc..dd6ec893 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -292,11 +292,14 @@ Connection Error There was a problem connecting to the server. The app will keep trying to reconnect. + Server + Error message Show details Hide details No additional details available No error - Dismiss + Copy + Error details copied to clipboard Open