Add retry counter and button
This commit is contained in:
parent
4359019dae
commit
9f530ca623
9 changed files with 528 additions and 13 deletions
423
app/schemas/io.heckel.ntfy.db.Database/17.json
Normal file
423
app/schemas/io.heckel.ntfy.db.Database/17.json
Normal file
|
|
@ -0,0 +1,423 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 17,
|
||||
"identityHash": "3466bc18a5e477081c1cbd2defcb449f",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "Subscription",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `topic` TEXT NOT NULL, `instant` INTEGER NOT NULL, `mutedUntil` INTEGER NOT NULL, `minPriority` INTEGER NOT NULL, `autoDelete` INTEGER NOT NULL, `insistent` INTEGER NOT NULL, `lastNotificationId` TEXT, `icon` TEXT, `upAppId` TEXT, `upConnectorToken` TEXT, `displayName` TEXT, `dedicatedChannels` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "baseUrl",
|
||||
"columnName": "baseUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "topic",
|
||||
"columnName": "topic",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "instant",
|
||||
"columnName": "instant",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "mutedUntil",
|
||||
"columnName": "mutedUntil",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "minPriority",
|
||||
"columnName": "minPriority",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "autoDelete",
|
||||
"columnName": "autoDelete",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "insistent",
|
||||
"columnName": "insistent",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastNotificationId",
|
||||
"columnName": "lastNotificationId",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "icon",
|
||||
"columnName": "icon",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "upAppId",
|
||||
"columnName": "upAppId",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "upConnectorToken",
|
||||
"columnName": "upConnectorToken",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "displayName",
|
||||
"columnName": "displayName",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "dedicatedChannels",
|
||||
"columnName": "dedicatedChannels",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_Subscription_baseUrl_topic",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"baseUrl",
|
||||
"topic"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Subscription_baseUrl_topic` ON `${TABLE_NAME}` (`baseUrl`, `topic`)"
|
||||
},
|
||||
{
|
||||
"name": "index_Subscription_upConnectorToken",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"upConnectorToken"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_Subscription_upConnectorToken` ON `${TABLE_NAME}` (`upConnectorToken`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tableName": "Notification",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `subscriptionId` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL, `title` TEXT NOT NULL, `message` TEXT NOT NULL, `contentType` TEXT NOT NULL, `encoding` TEXT NOT NULL, `notificationId` INTEGER NOT NULL, `priority` INTEGER NOT NULL DEFAULT 3, `tags` TEXT NOT NULL, `click` TEXT NOT NULL, `actions` TEXT, `deleted` INTEGER NOT NULL, `icon_url` TEXT, `icon_contentUri` TEXT, `attachment_name` TEXT, `attachment_type` TEXT, `attachment_size` INTEGER, `attachment_expires` INTEGER, `attachment_url` TEXT, `attachment_contentUri` TEXT, `attachment_progress` INTEGER, PRIMARY KEY(`id`, `subscriptionId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "subscriptionId",
|
||||
"columnName": "subscriptionId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "timestamp",
|
||||
"columnName": "timestamp",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "message",
|
||||
"columnName": "message",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "contentType",
|
||||
"columnName": "contentType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "encoding",
|
||||
"columnName": "encoding",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "notificationId",
|
||||
"columnName": "notificationId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "priority",
|
||||
"columnName": "priority",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true,
|
||||
"defaultValue": "3"
|
||||
},
|
||||
{
|
||||
"fieldPath": "tags",
|
||||
"columnName": "tags",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "click",
|
||||
"columnName": "click",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "actions",
|
||||
"columnName": "actions",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "deleted",
|
||||
"columnName": "deleted",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "icon.url",
|
||||
"columnName": "icon_url",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "icon.contentUri",
|
||||
"columnName": "icon_contentUri",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "attachment.name",
|
||||
"columnName": "attachment_name",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "attachment.type",
|
||||
"columnName": "attachment_type",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "attachment.size",
|
||||
"columnName": "attachment_size",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "attachment.expires",
|
||||
"columnName": "attachment_expires",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "attachment.url",
|
||||
"columnName": "attachment_url",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "attachment.contentUri",
|
||||
"columnName": "attachment_contentUri",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "attachment.progress",
|
||||
"columnName": "attachment_progress",
|
||||
"affinity": "INTEGER"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id",
|
||||
"subscriptionId"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "User",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`baseUrl` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, PRIMARY KEY(`baseUrl`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "baseUrl",
|
||||
"columnName": "baseUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "username",
|
||||
"columnName": "username",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "password",
|
||||
"columnName": "password",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"baseUrl"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "Log",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `timestamp` INTEGER NOT NULL, `tag` TEXT NOT NULL, `level` INTEGER NOT NULL, `message` TEXT NOT NULL, `exception` TEXT)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "timestamp",
|
||||
"columnName": "timestamp",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "tag",
|
||||
"columnName": "tag",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "level",
|
||||
"columnName": "level",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "message",
|
||||
"columnName": "message",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "exception",
|
||||
"columnName": "exception",
|
||||
"affinity": "TEXT"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "CustomHeader",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`baseUrl` TEXT NOT NULL, `name` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`baseUrl`, `name`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "baseUrl",
|
||||
"columnName": "baseUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "value",
|
||||
"columnName": "value",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"baseUrl",
|
||||
"name"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "TrustedCertificate",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`baseUrl` TEXT NOT NULL, `pem` TEXT NOT NULL, PRIMARY KEY(`baseUrl`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "baseUrl",
|
||||
"columnName": "baseUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "pem",
|
||||
"columnName": "pem",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"baseUrl"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "ClientCertificate",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`baseUrl` TEXT NOT NULL, `p12Base64` TEXT NOT NULL, `password` TEXT NOT NULL, PRIMARY KEY(`baseUrl`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "baseUrl",
|
||||
"columnName": "baseUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "p12Base64",
|
||||
"columnName": "p12Base64",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "password",
|
||||
"columnName": "password",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"baseUrl"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3466bc18a5e477081c1cbd2defcb449f')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -80,7 +80,8 @@ data class ConnectionError(
|
|||
val baseUrl: String,
|
||||
val message: String,
|
||||
val throwable: Throwable?,
|
||||
val timestamp: Long = System.currentTimeMillis()
|
||||
val timestamp: Long = System.currentTimeMillis(),
|
||||
val nextRetryTime: Long = 0L
|
||||
) {
|
||||
fun getStackTraceString(): String {
|
||||
return throwable?.stackTraceToString() ?: ""
|
||||
|
|
|
|||
|
|
@ -580,11 +580,11 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database)
|
|||
return connectionErrors.toMap()
|
||||
}
|
||||
|
||||
fun updateConnectionError(baseUrl: String, message: String, throwable: Throwable?) {
|
||||
val error = ConnectionError(baseUrl, message, throwable)
|
||||
fun updateConnectionError(baseUrl: String, message: String, throwable: Throwable?, nextRetryTime: Long = 0L) {
|
||||
val error = ConnectionError(baseUrl, message, throwable, System.currentTimeMillis(), nextRetryTime)
|
||||
connectionErrors[baseUrl] = error
|
||||
connectionErrorsLiveData.postValue(connectionErrors.toMap())
|
||||
Log.d(TAG, "Connection error updated for $baseUrl: $message")
|
||||
Log.d(TAG, "Connection error updated for $baseUrl: $message (next retry at $nextRetryTime)")
|
||||
}
|
||||
|
||||
fun clearConnectionError(baseUrl: String) {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class JsonConnection(
|
|||
private val sinceId: String?,
|
||||
private val stateChangeListener: (Collection<Long>, ConnectionState) -> Unit,
|
||||
private val notificationListener: (Subscription, Notification) -> Unit,
|
||||
private val errorListener: (String, Throwable) -> Unit,
|
||||
private val errorListener: (String, Throwable, Long) -> Unit,
|
||||
private val serviceActive: () -> Boolean
|
||||
) : Connection {
|
||||
private val baseUrl = connectionId.baseUrl
|
||||
|
|
@ -47,11 +47,12 @@ class JsonConnection(
|
|||
notificationListener(subscription, notificationWithSubscriptionId)
|
||||
}
|
||||
val failed = AtomicBoolean(false)
|
||||
var lastError: Exception? = null
|
||||
val fail = { e: Exception ->
|
||||
failed.set(true)
|
||||
lastError = e
|
||||
if (isActive && serviceActive()) { // Avoid UI update races if we're restarting a connection
|
||||
stateChangeListener(subscriptionIds, ConnectionState.CONNECTING)
|
||||
errorListener(baseUrl, e)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -66,15 +67,17 @@ class JsonConnection(
|
|||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "[$url] Connection failed: ${e.message}", e)
|
||||
lastError = e
|
||||
if (isActive && serviceActive()) { // Avoid UI update races if we're restarting a connection
|
||||
stateChangeListener(subscriptionIds, ConnectionState.CONNECTING)
|
||||
errorListener(baseUrl, e)
|
||||
}
|
||||
}
|
||||
|
||||
// If we're not cancelled yet, wait little before retrying (incremental back-off)
|
||||
if (isActive && serviceActive()) {
|
||||
retryMillis = nextRetryMillis(retryMillis, startTime)
|
||||
val nextRetryTime = System.currentTimeMillis() + retryMillis
|
||||
lastError?.let { errorListener(baseUrl, it, nextRetryTime) }
|
||||
Log.d(TAG, "[$url] Connection failed, retrying connection in ${retryMillis / 1000}s ...")
|
||||
delay(retryMillis)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -318,9 +318,9 @@ class SubscriberService : Service() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun onConnectionError(baseUrl: String, throwable: Throwable) {
|
||||
private fun onConnectionError(baseUrl: String, throwable: Throwable, nextRetryTime: Long) {
|
||||
val message = throwable.message ?: "Unknown error"
|
||||
repository.updateConnectionError(baseUrl, message, throwable)
|
||||
repository.updateConnectionError(baseUrl, message, throwable, nextRetryTime)
|
||||
}
|
||||
|
||||
private fun onNotificationReceived(subscription: Subscription, notification: io.heckel.ntfy.db.Notification) {
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ class WsConnection(
|
|||
private val sinceId: String?,
|
||||
private val stateChangeListener: (Collection<Long>, ConnectionState) -> Unit,
|
||||
private val notificationListener: (Subscription, Notification) -> Unit,
|
||||
private val errorListener: (String, Throwable) -> Unit,
|
||||
private val errorListener: (String, Throwable, Long) -> Unit,
|
||||
private val alarmManager: AlarmManager
|
||||
) : Connection {
|
||||
private val parser = NotificationParser()
|
||||
|
|
@ -184,10 +184,11 @@ class WsConnection(
|
|||
return@synchronize
|
||||
}
|
||||
stateChangeListener(subscriptionIds, ConnectionState.CONNECTING)
|
||||
errorListener(baseUrl, t)
|
||||
state = State.Disconnected
|
||||
errorCount++
|
||||
val retrySeconds = RETRY_SECONDS.getOrNull(errorCount) ?: RETRY_SECONDS.last()
|
||||
val nextRetryTime = System.currentTimeMillis() + (retrySeconds * 1000L)
|
||||
errorListener(baseUrl, t, nextRetryTime)
|
||||
scheduleReconnect(retrySeconds)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package io.heckel.ntfy.ui
|
|||
import android.app.Dialog
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
|
|
@ -17,6 +19,7 @@ import com.google.android.material.textfield.TextInputLayout
|
|||
import io.heckel.ntfy.R
|
||||
import io.heckel.ntfy.db.ConnectionError
|
||||
import io.heckel.ntfy.db.Repository
|
||||
import io.heckel.ntfy.service.SubscriberServiceManager
|
||||
import io.heckel.ntfy.util.copyToClipboard
|
||||
|
||||
class ConnectionErrorFragment : DialogFragment() {
|
||||
|
|
@ -29,10 +32,20 @@ class ConnectionErrorFragment : DialogFragment() {
|
|||
private lateinit var serverLayout: TextInputLayout
|
||||
private lateinit var serverDropdown: AutoCompleteTextView
|
||||
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
|
||||
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private val countdownRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
updateCountdown()
|
||||
handler.postDelayed(this, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
if (activity == null) {
|
||||
throw IllegalStateException("Activity cannot be null")
|
||||
|
|
@ -77,6 +90,8 @@ class ConnectionErrorFragment : DialogFragment() {
|
|||
serverLayout = view.findViewById(R.id.connection_error_dialog_server_layout)
|
||||
serverDropdown = view.findViewById(R.id.connection_error_dialog_server_dropdown)
|
||||
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)
|
||||
|
|
@ -105,6 +120,22 @@ class ConnectionErrorFragment : DialogFragment() {
|
|||
updateDetailsVisibility(isChecked)
|
||||
}
|
||||
|
||||
// Retry now button
|
||||
retryChip.setOnClickListener {
|
||||
SubscriberServiceManager.refresh(requireContext())
|
||||
dismiss()
|
||||
}
|
||||
|
||||
// Observe connection errors to update countdown when it changes
|
||||
repository.getConnectionErrorsLiveData().observe(this) { errors ->
|
||||
connectionErrors = if (filterBaseUrl != null) {
|
||||
errors.filterKeys { it == filterBaseUrl }
|
||||
} else {
|
||||
errors
|
||||
}
|
||||
updateErrorDisplay()
|
||||
}
|
||||
|
||||
// Build dialog
|
||||
val dialog = Dialog(requireContext(), R.style.Theme_App_FullScreenDialog)
|
||||
dialog.setContentView(view)
|
||||
|
|
@ -120,6 +151,14 @@ class ConnectionErrorFragment : DialogFragment() {
|
|||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
}
|
||||
// Start countdown timer
|
||||
handler.post(countdownRunnable)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
// Stop countdown timer
|
||||
handler.removeCallbacks(countdownRunnable)
|
||||
}
|
||||
|
||||
private fun updateErrorDisplay() {
|
||||
|
|
@ -134,6 +173,24 @@ class ConnectionErrorFragment : DialogFragment() {
|
|||
stackTraceTextView.text = ""
|
||||
}
|
||||
updateDetailsVisibility(detailsChip.isChecked)
|
||||
updateCountdown()
|
||||
}
|
||||
|
||||
private fun updateCountdown() {
|
||||
val error = selectedBaseUrl?.let { connectionErrors[it] }
|
||||
if (error != null && error.nextRetryTime > 0) {
|
||||
val remainingMillis = error.nextRetryTime - System.currentTimeMillis()
|
||||
if (remainingMillis > 0) {
|
||||
val remainingSeconds = (remainingMillis / 1000).toInt()
|
||||
countdownTextView.text = getString(R.string.connection_error_dialog_retry_countdown, remainingSeconds)
|
||||
countdownTextView.visibility = View.VISIBLE
|
||||
} else {
|
||||
countdownTextView.text = getString(R.string.connection_error_dialog_retrying)
|
||||
countdownTextView.visibility = View.VISIBLE
|
||||
}
|
||||
} else {
|
||||
countdownTextView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateDetailsVisibility(visible: Boolean) {
|
||||
|
|
|
|||
|
|
@ -99,6 +99,33 @@
|
|||
app:layout_constraintStart_toEndOf="@id/connection_error_dialog_error_icon"
|
||||
app:layout_constraintTop_toBottomOf="@id/connection_error_dialog_server_layout" />
|
||||
|
||||
<!-- Countdown text -->
|
||||
<TextView
|
||||
android:id="@+id/connection_error_dialog_countdown"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
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" />
|
||||
|
||||
<!-- 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"
|
||||
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"
|
||||
|
|
@ -111,7 +138,7 @@
|
|||
app:chipStrokeWidth="0dp"
|
||||
app:checkedIconVisible="false"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/connection_error_dialog_error_text" />
|
||||
app:layout_constraintTop_toBottomOf="@id/connection_error_dialog_countdown" />
|
||||
|
||||
<!-- Stack trace (scrollable horizontally, no word wrap) -->
|
||||
<HorizontalScrollView
|
||||
|
|
@ -123,7 +150,7 @@
|
|||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/connection_error_dialog_details_chip">
|
||||
app:layout_constraintTop_toBottomOf="@id/connection_error_dialog_retry_chip">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="wrap_content"
|
||||
|
|
|
|||
|
|
@ -298,6 +298,9 @@
|
|||
<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_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>
|
||||
<string name="connection_error_dialog_retrying">Retrying…</string>
|
||||
|
||||
<!-- Notification popup -->
|
||||
<string name="notification_popup_action_open">Open</string>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue