Combine ConnectionState and error details
This commit is contained in:
parent
9f530ca623
commit
00a16f454b
11 changed files with 102 additions and 145 deletions
|
|
@ -28,7 +28,7 @@ data class Subscription(
|
|||
@Ignore val totalCount: Int = 0, // Total notifications
|
||||
@Ignore val newCount: Int = 0, // New notifications
|
||||
@Ignore val lastActive: Long = 0, // Unix timestamp
|
||||
@Ignore val state: ConnectionState = ConnectionState.NOT_APPLICABLE
|
||||
@Ignore val connectionDetails: ConnectionDetails = ConnectionDetails()
|
||||
) {
|
||||
constructor(
|
||||
id: Long,
|
||||
|
|
@ -64,7 +64,7 @@ data class Subscription(
|
|||
totalCount = 0,
|
||||
newCount = 0,
|
||||
lastActive = 0,
|
||||
state = ConnectionState.NOT_APPLICABLE
|
||||
connectionDetails = ConnectionDetails()
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -73,19 +73,22 @@ enum class ConnectionState {
|
|||
}
|
||||
|
||||
/**
|
||||
* Represents a connection error for a specific baseUrl.
|
||||
* Represents connection details for a specific baseUrl, including state and error info.
|
||||
* This is not persisted to the database, but kept in memory.
|
||||
*/
|
||||
data class ConnectionError(
|
||||
val baseUrl: String,
|
||||
val message: String,
|
||||
val throwable: Throwable?,
|
||||
val timestamp: Long = System.currentTimeMillis(),
|
||||
data class ConnectionDetails(
|
||||
val state: ConnectionState = ConnectionState.NOT_APPLICABLE,
|
||||
val error: String? = null,
|
||||
val throwable: Throwable? = null,
|
||||
val nextRetryTime: Long = 0L
|
||||
) {
|
||||
fun getStackTraceString(): String {
|
||||
return throwable?.stackTraceToString() ?: ""
|
||||
}
|
||||
|
||||
fun hasError(): Boolean {
|
||||
return error != null
|
||||
}
|
||||
}
|
||||
|
||||
data class SubscriptionWithMetadata(
|
||||
|
|
|
|||
|
|
@ -25,11 +25,8 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database)
|
|||
private val clientCertificateDao = database.clientCertificateDao()
|
||||
private val customHeaderDao = database.customHeaderDao()
|
||||
|
||||
private val connectionStates = ConcurrentHashMap<Long, ConnectionState>()
|
||||
private val connectionStatesLiveData = MutableLiveData(connectionStates)
|
||||
|
||||
private val connectionErrors = ConcurrentHashMap<String, ConnectionError>()
|
||||
private val connectionErrorsLiveData = MutableLiveData<Map<String, ConnectionError>>(emptyMap())
|
||||
private val connectionDetails = ConcurrentHashMap<String, ConnectionDetails>()
|
||||
private val connectionDetailsLiveData = MutableLiveData<Map<String, ConnectionDetails>>(connectionDetails)
|
||||
|
||||
// TODO Move these into an ApplicationState singleton
|
||||
val detailViewSubscriptionId = AtomicLong(0L) // Omg, what a hack ...
|
||||
|
|
@ -43,7 +40,7 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database)
|
|||
return subscriptionDao
|
||||
.listFlow()
|
||||
.asLiveData()
|
||||
.combineWith(connectionStatesLiveData) { subscriptionsWithMetadata, _ ->
|
||||
.combineWith(connectionDetailsLiveData) { subscriptionsWithMetadata, _ ->
|
||||
toSubscriptionList(subscriptionsWithMetadata.orEmpty())
|
||||
}
|
||||
}
|
||||
|
|
@ -500,7 +497,6 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database)
|
|||
|
||||
private fun toSubscriptionList(list: List<SubscriptionWithMetadata>): List<Subscription> {
|
||||
return list.map { s ->
|
||||
val connectionState = connectionStates.getOrElse(s.id) { ConnectionState.NOT_APPLICABLE }
|
||||
Subscription(
|
||||
id = s.id,
|
||||
baseUrl = s.baseUrl,
|
||||
|
|
@ -519,7 +515,7 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database)
|
|||
totalCount = s.totalCount,
|
||||
newCount = s.newCount,
|
||||
lastActive = s.lastActive,
|
||||
state = connectionState
|
||||
connectionDetails = connectionDetails[s.baseUrl] ?: ConnectionDetails()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -546,60 +542,34 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database)
|
|||
totalCount = s.totalCount,
|
||||
newCount = s.newCount,
|
||||
lastActive = s.lastActive,
|
||||
state = getState(s.id)
|
||||
connectionDetails = connectionDetails[s.baseUrl] ?: ConnectionDetails()
|
||||
)
|
||||
}
|
||||
|
||||
fun updateState(subscriptionIds: Collection<Long>, newState: ConnectionState) {
|
||||
var changed = false
|
||||
subscriptionIds.forEach { subscriptionId ->
|
||||
val state = connectionStates.getOrElse(subscriptionId) { ConnectionState.NOT_APPLICABLE }
|
||||
if (state !== newState) {
|
||||
changed = true
|
||||
if (newState == ConnectionState.NOT_APPLICABLE) {
|
||||
connectionStates.remove(subscriptionId)
|
||||
} else {
|
||||
connectionStates[subscriptionId] = newState
|
||||
}
|
||||
fun updateConnectionDetails(baseUrl: String, state: ConnectionState, error: String? = null, throwable: Throwable? = null, nextRetryTime: Long = 0L) {
|
||||
val details = ConnectionDetails(state, error, throwable, nextRetryTime)
|
||||
val current = connectionDetails[baseUrl]
|
||||
if (current != details) {
|
||||
if (state == ConnectionState.NOT_APPLICABLE && error == null) {
|
||||
connectionDetails.remove(baseUrl)
|
||||
} else {
|
||||
connectionDetails[baseUrl] = details
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
connectionStatesLiveData.postValue(connectionStates)
|
||||
connectionDetailsLiveData.postValue(connectionDetails.toMap())
|
||||
Log.d(TAG, "Connection details updated for $baseUrl: state=$state, error=$error, nextRetry=$nextRetryTime")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getState(subscriptionId: Long): ConnectionState {
|
||||
return connectionStatesLiveData.value!!.getOrElse(subscriptionId) { ConnectionState.NOT_APPLICABLE }
|
||||
fun getConnectionDetailsLiveData(): LiveData<Map<String, ConnectionDetails>> {
|
||||
return connectionDetailsLiveData
|
||||
}
|
||||
|
||||
fun getConnectionErrorsLiveData(): LiveData<Map<String, ConnectionError>> {
|
||||
return connectionErrorsLiveData
|
||||
fun getConnectionDetails(): Map<String, ConnectionDetails> {
|
||||
return connectionDetails.toMap()
|
||||
}
|
||||
|
||||
fun getConnectionErrors(): Map<String, ConnectionError> {
|
||||
return connectionErrors.toMap()
|
||||
}
|
||||
|
||||
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 (next retry at $nextRetryTime)")
|
||||
}
|
||||
|
||||
fun clearConnectionError(baseUrl: String) {
|
||||
if (connectionErrors.remove(baseUrl) != null) {
|
||||
connectionErrorsLiveData.postValue(connectionErrors.toMap())
|
||||
Log.d(TAG, "Connection error cleared for $baseUrl")
|
||||
}
|
||||
}
|
||||
|
||||
fun clearAllConnectionErrors() {
|
||||
if (connectionErrors.isNotEmpty()) {
|
||||
connectionErrors.clear()
|
||||
connectionErrorsLiveData.postValue(emptyMap())
|
||||
Log.d(TAG, "All connection errors cleared")
|
||||
}
|
||||
fun getConnectionDetailsForBaseUrl(baseUrl: String): ConnectionDetails? {
|
||||
return connectionDetails[baseUrl]
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -15,9 +15,8 @@ class JsonConnection(
|
|||
private val api: ApiService,
|
||||
private val user: User?,
|
||||
private val sinceId: String?,
|
||||
private val stateChangeListener: (Collection<Long>, ConnectionState) -> Unit,
|
||||
private val connectionDetailsListener: (Collection<Long>, ConnectionState, Throwable?, Long) -> Unit,
|
||||
private val notificationListener: (Subscription, Notification) -> Unit,
|
||||
private val errorListener: (String, Throwable, Long) -> Unit,
|
||||
private val serviceActive: () -> Boolean
|
||||
) : Connection {
|
||||
private val baseUrl = connectionId.baseUrl
|
||||
|
|
@ -51,9 +50,6 @@ class JsonConnection(
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// Call /json subscribe endpoint and loop until the call fails, is canceled,
|
||||
|
|
@ -61,23 +57,20 @@ class JsonConnection(
|
|||
try {
|
||||
call = api.subscribe(baseUrl, topicsStr, since, user, notify, fail)
|
||||
while (!failed.get() && !call.isCanceled() && isActive && serviceActive()) {
|
||||
stateChangeListener(subscriptionIds, ConnectionState.CONNECTED)
|
||||
connectionDetailsListener(subscriptionIds, ConnectionState.CONNECTED, null, 0L)
|
||||
Log.d(TAG,"[$url] Connection is active (failed=$failed, callCanceled=${call.isCanceled()}, jobActive=$isActive, serviceStarted=${serviceActive()}")
|
||||
delay(CONNECTION_LOOP_DELAY_MILLIS) // Resumes immediately if job is cancelled
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
||||
// If we're not cancelled yet, wait little before retrying (incremental back-off)
|
||||
if (isActive && serviceActive()) {
|
||||
if (isActive && serviceActive() && lastError != null) {
|
||||
retryMillis = nextRetryMillis(retryMillis, startTime)
|
||||
val nextRetryTime = System.currentTimeMillis() + retryMillis
|
||||
lastError?.let { errorListener(baseUrl, it, nextRetryTime) }
|
||||
connectionDetailsListener(subscriptionIds, ConnectionState.CONNECTING, lastError, nextRetryTime)
|
||||
Log.d(TAG, "[$url] Connection failed, retrying connection in ${retryMillis / 1000}s ...")
|
||||
delay(retryMillis)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -262,9 +262,9 @@ class SubscriberService : Service() {
|
|||
val connection = if (connectionId.connectionProtocol == Repository.CONNECTION_PROTOCOL_WS) {
|
||||
val alarmManager = getSystemService(ALARM_SERVICE) as AlarmManager
|
||||
val httpClient = HttpUtil.wsClient(this, connectionId.baseUrl)
|
||||
WsConnection(connectionId, repository, httpClient, user, customHeaders, since, ::onStateChanged, ::onNotificationReceived, ::onConnectionError, alarmManager)
|
||||
WsConnection(connectionId, repository, httpClient, user, customHeaders, since, ::onConnectionDetailsChanged, ::onNotificationReceived, alarmManager)
|
||||
} else {
|
||||
JsonConnection(connectionId, scope, repository, api, user, since, ::onStateChanged, ::onNotificationReceived, ::onConnectionError, serviceActive)
|
||||
JsonConnection(connectionId, scope, repository, api, user, since, ::onConnectionDetailsChanged, ::onNotificationReceived, serviceActive)
|
||||
}
|
||||
connections[connectionId] = connection
|
||||
connection.start()
|
||||
|
|
@ -305,22 +305,12 @@ class SubscriberService : Service() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun onStateChanged(subscriptionIds: Collection<Long>, state: ConnectionState) {
|
||||
repository.updateState(subscriptionIds, state)
|
||||
// Clear connection error when successfully connected
|
||||
if (state == ConnectionState.CONNECTED) {
|
||||
subscriptionIds.firstOrNull()?.let { subscriptionId ->
|
||||
val subscription = repository.getSubscription(subscriptionId)
|
||||
if (subscription != null) {
|
||||
repository.clearConnectionError(subscription.baseUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onConnectionError(baseUrl: String, throwable: Throwable, nextRetryTime: Long) {
|
||||
val message = throwable.message ?: "Unknown error"
|
||||
repository.updateConnectionError(baseUrl, message, throwable, nextRetryTime)
|
||||
private fun onConnectionDetailsChanged(subscriptionIds: Collection<Long>, state: ConnectionState, throwable: Throwable?, nextRetryTime: Long) {
|
||||
val subscriptionId = subscriptionIds.firstOrNull() ?: return
|
||||
val subscription = repository.getSubscription(subscriptionId) ?: return
|
||||
val baseUrl = subscription.baseUrl
|
||||
val errorMessage = throwable?.message
|
||||
repository.updateConnectionDetails(baseUrl, state, errorMessage, throwable, nextRetryTime)
|
||||
}
|
||||
|
||||
private fun onNotificationReceived(subscription: Subscription, notification: io.heckel.ntfy.db.Notification) {
|
||||
|
|
|
|||
|
|
@ -40,9 +40,8 @@ class WsConnection(
|
|||
private val user: User?,
|
||||
private val customHeaders: List<CustomHeader>,
|
||||
private val sinceId: String?,
|
||||
private val stateChangeListener: (Collection<Long>, ConnectionState) -> Unit,
|
||||
private val connectionDetailsListener: (Collection<Long>, ConnectionState, Throwable?, Long) -> Unit,
|
||||
private val notificationListener: (Subscription, Notification) -> Unit,
|
||||
private val errorListener: (String, Throwable, Long) -> Unit,
|
||||
private val alarmManager: AlarmManager
|
||||
) : Connection {
|
||||
private val parser = NotificationParser()
|
||||
|
|
@ -143,7 +142,7 @@ class WsConnection(
|
|||
if (errorCount > 0) {
|
||||
errorCount = 0
|
||||
}
|
||||
stateChangeListener(subscriptionIds, ConnectionState.CONNECTED)
|
||||
connectionDetailsListener(subscriptionIds, ConnectionState.CONNECTED, null, 0L)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -183,12 +182,11 @@ class WsConnection(
|
|||
Log.d(TAG, "$shortUrl (gid=$globalId, lid=$id): Connection marked as closed. Not retrying.")
|
||||
return@synchronize
|
||||
}
|
||||
stateChangeListener(subscriptionIds, ConnectionState.CONNECTING)
|
||||
state = State.Disconnected
|
||||
errorCount++
|
||||
val retrySeconds = RETRY_SECONDS.getOrNull(errorCount) ?: RETRY_SECONDS.last()
|
||||
val nextRetryTime = System.currentTimeMillis() + (retrySeconds * 1000L)
|
||||
errorListener(baseUrl, t, nextRetryTime)
|
||||
connectionDetailsListener(subscriptionIds, ConnectionState.CONNECTING, t, nextRetryTime)
|
||||
scheduleReconnect(retrySeconds)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,14 +17,14 @@ 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
|
||||
import io.heckel.ntfy.db.ConnectionError
|
||||
import io.heckel.ntfy.db.ConnectionDetails
|
||||
import io.heckel.ntfy.db.Repository
|
||||
import io.heckel.ntfy.service.SubscriberServiceManager
|
||||
import io.heckel.ntfy.util.copyToClipboard
|
||||
|
||||
class ConnectionErrorFragment : DialogFragment() {
|
||||
private lateinit var repository: Repository
|
||||
private var connectionErrors: Map<String, ConnectionError> = emptyMap()
|
||||
private var connectionDetails: Map<String, ConnectionDetails> = emptyMap()
|
||||
private var selectedBaseUrl: String? = null
|
||||
private var filterBaseUrl: String? = null
|
||||
|
||||
|
|
@ -57,12 +57,12 @@ class ConnectionErrorFragment : DialogFragment() {
|
|||
// Dependencies
|
||||
repository = Repository.getInstance(requireContext())
|
||||
|
||||
// Get connection errors, optionally filtered by baseUrl
|
||||
val allErrors = repository.getConnectionErrors()
|
||||
connectionErrors = if (filterBaseUrl != null) {
|
||||
allErrors.filterKeys { it == filterBaseUrl }
|
||||
// Get connection details with errors, optionally filtered by baseUrl
|
||||
val allDetails = repository.getConnectionDetails()
|
||||
connectionDetails = if (filterBaseUrl != null) {
|
||||
allDetails.filterKeys { it == filterBaseUrl }.filterValues { it.hasError() }
|
||||
} else {
|
||||
allErrors
|
||||
allDetails.filterValues { it.hasError() }
|
||||
}
|
||||
|
||||
// Build root view
|
||||
|
|
@ -97,7 +97,7 @@ class ConnectionErrorFragment : DialogFragment() {
|
|||
stackTraceTextView = view.findViewById(R.id.connection_error_dialog_stack_trace)
|
||||
|
||||
// Setup server dropdown if multiple errors
|
||||
val baseUrls = connectionErrors.keys.toList()
|
||||
val baseUrls = connectionDetails.keys.toList()
|
||||
if (baseUrls.size > 1) {
|
||||
serverLayout.visibility = View.VISIBLE
|
||||
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, baseUrls)
|
||||
|
|
@ -126,12 +126,12 @@ class ConnectionErrorFragment : DialogFragment() {
|
|||
dismiss()
|
||||
}
|
||||
|
||||
// Observe connection errors to update countdown when it changes
|
||||
repository.getConnectionErrorsLiveData().observe(this) { errors ->
|
||||
connectionErrors = if (filterBaseUrl != null) {
|
||||
errors.filterKeys { it == filterBaseUrl }
|
||||
// Observe connection details to update countdown when it changes
|
||||
repository.getConnectionDetailsLiveData().observe(this) { details ->
|
||||
connectionDetails = if (filterBaseUrl != null) {
|
||||
details.filterKeys { it == filterBaseUrl }.filterValues { it.hasError() }
|
||||
} else {
|
||||
errors
|
||||
details.filterValues { it.hasError() }
|
||||
}
|
||||
updateErrorDisplay()
|
||||
}
|
||||
|
|
@ -162,10 +162,10 @@ class ConnectionErrorFragment : DialogFragment() {
|
|||
}
|
||||
|
||||
private fun updateErrorDisplay() {
|
||||
val error = selectedBaseUrl?.let { connectionErrors[it] }
|
||||
if (error != null) {
|
||||
errorTextView.text = error.message
|
||||
stackTraceTextView.text = error.getStackTraceString().ifEmpty {
|
||||
val details = selectedBaseUrl?.let { connectionDetails[it] }
|
||||
if (details != null && details.hasError()) {
|
||||
errorTextView.text = details.error
|
||||
stackTraceTextView.text = details.getStackTraceString().ifEmpty {
|
||||
getString(R.string.connection_error_dialog_no_stack_trace)
|
||||
}
|
||||
} else {
|
||||
|
|
@ -177,9 +177,9 @@ class ConnectionErrorFragment : DialogFragment() {
|
|||
}
|
||||
|
||||
private fun updateCountdown() {
|
||||
val error = selectedBaseUrl?.let { connectionErrors[it] }
|
||||
if (error != null && error.nextRetryTime > 0) {
|
||||
val remainingMillis = error.nextRetryTime - System.currentTimeMillis()
|
||||
val details = selectedBaseUrl?.let { connectionDetails[it] }
|
||||
if (details != null && details.nextRetryTime > 0) {
|
||||
val remainingMillis = details.nextRetryTime - System.currentTimeMillis()
|
||||
if (remainingMillis > 0) {
|
||||
val remainingSeconds = (remainingMillis / 1000).toInt()
|
||||
countdownTextView.text = getString(R.string.connection_error_dialog_retry_countdown, remainingSeconds)
|
||||
|
|
@ -198,13 +198,14 @@ class ConnectionErrorFragment : DialogFragment() {
|
|||
}
|
||||
|
||||
private fun copyErrorToClipboard() {
|
||||
val error = selectedBaseUrl?.let { connectionErrors[it] } ?: return
|
||||
val baseUrl = selectedBaseUrl ?: return
|
||||
val details = connectionDetails[baseUrl] ?: return
|
||||
val text = buildString {
|
||||
appendLine("Server: ${error.baseUrl}")
|
||||
appendLine("Error: ${error.message}")
|
||||
appendLine("Server: $baseUrl")
|
||||
appendLine("Error: ${details.error}")
|
||||
appendLine()
|
||||
appendLine("Stack trace:")
|
||||
append(error.getStackTraceString().ifEmpty { "No stack trace available" })
|
||||
append(details.getStackTraceString().ifEmpty { "No stack trace available" })
|
||||
}
|
||||
copyToClipboard(requireContext(), "connection error", text)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -361,9 +361,9 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet
|
|||
SubscriberServiceManager.refresh(this)
|
||||
}
|
||||
|
||||
// Observe connection errors and update menu item visibility
|
||||
repository.getConnectionErrorsLiveData().observe(this) { errors ->
|
||||
showHideConnectionErrorMenuItem(errors)
|
||||
// Observe connection details and update menu item visibility
|
||||
repository.getConnectionDetailsLiveData().observe(this) { details ->
|
||||
showHideConnectionErrorMenuItem(details)
|
||||
}
|
||||
|
||||
// Mark this subscription as "open" so we don't receive notifications for it
|
||||
|
|
@ -513,7 +513,7 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet
|
|||
showHideInstantMenuItems(subscriptionInstant)
|
||||
showHideMutedUntilMenuItems(subscriptionMutedUntil)
|
||||
showHideCopyMenuItems(subscription.baseUrl)
|
||||
showHideConnectionErrorMenuItem(repository.getConnectionErrors())
|
||||
showHideConnectionErrorMenuItem(repository.getConnectionDetails())
|
||||
updateTitle(subscriptionDisplayName)
|
||||
}
|
||||
}
|
||||
|
|
@ -556,7 +556,7 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet
|
|||
showHideInstantMenuItems(subscriptionInstant)
|
||||
showHideMutedUntilMenuItems(subscriptionMutedUntil)
|
||||
showHideCopyMenuItems(subscriptionBaseUrl)
|
||||
showHideConnectionErrorMenuItem(repository.getConnectionErrors())
|
||||
showHideConnectionErrorMenuItem(repository.getConnectionDetails())
|
||||
|
||||
// Regularly check if "notification muted" time has passed
|
||||
// NOTE: This is done here, because then we know that we've initialized the menu items.
|
||||
|
|
@ -817,14 +817,15 @@ class DetailActivity : AppCompatActivity(), NotificationFragment.NotificationSet
|
|||
}
|
||||
}
|
||||
|
||||
private fun showHideConnectionErrorMenuItem(errors: Map<String, io.heckel.ntfy.db.ConnectionError>) {
|
||||
private fun showHideConnectionErrorMenuItem(details: Map<String, io.heckel.ntfy.db.ConnectionDetails>) {
|
||||
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)
|
||||
val hasError = details[subscriptionBaseUrl]?.hasError() == true
|
||||
connectionErrorItem?.isVisible = hasError
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -253,9 +253,9 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific
|
|||
SubscriberServiceManager.refresh(this)
|
||||
}
|
||||
|
||||
// Observe connection errors and update menu item visibility
|
||||
repository.getConnectionErrorsLiveData().observe(this) { errors ->
|
||||
showHideConnectionErrorMenuItem(errors)
|
||||
// Observe connection details and update menu item visibility
|
||||
repository.getConnectionDetailsLiveData().observe(this) { details ->
|
||||
showHideConnectionErrorMenuItem(details)
|
||||
}
|
||||
|
||||
// Battery banner
|
||||
|
|
@ -375,7 +375,7 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific
|
|||
override fun onResume() {
|
||||
super.onResume()
|
||||
showHideNotificationMenuItems()
|
||||
showHideConnectionErrorMenuItem(repository.getConnectionErrors())
|
||||
showHideConnectionErrorMenuItem(repository.getConnectionDetails())
|
||||
redrawList()
|
||||
}
|
||||
|
||||
|
|
@ -493,7 +493,7 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific
|
|||
}
|
||||
|
||||
showHideNotificationMenuItems()
|
||||
showHideConnectionErrorMenuItem(repository.getConnectionErrors())
|
||||
showHideConnectionErrorMenuItem(repository.getConnectionDetails())
|
||||
checkSubscriptionsMuted() // This is done here, because then we know that we've initialized the menu
|
||||
return true
|
||||
}
|
||||
|
|
@ -557,13 +557,14 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific
|
|||
}
|
||||
}
|
||||
|
||||
private fun showHideConnectionErrorMenuItem(errors: Map<String, io.heckel.ntfy.db.ConnectionError>) {
|
||||
private fun showHideConnectionErrorMenuItem(details: Map<String, io.heckel.ntfy.db.ConnectionDetails>) {
|
||||
if (!this::menu.isInitialized) {
|
||||
return
|
||||
}
|
||||
runOnUiThread {
|
||||
val connectionErrorItem = menu.findItem(R.id.main_menu_connection_error)
|
||||
connectionErrorItem?.isVisible = errors.isNotEmpty()
|
||||
val hasErrors = details.values.any { it.hasError() }
|
||||
connectionErrorItem?.isVisible = hasErrors
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ class MainAdapter(
|
|||
} else {
|
||||
context.getString(R.string.main_item_status_text_not_one, subscription.totalCount)
|
||||
}
|
||||
if (subscription.instant && subscription.state == ConnectionState.CONNECTING) {
|
||||
if (subscription.instant && subscription.connectionDetails.state == ConnectionState.CONNECTING) {
|
||||
statusMessage += ", " + context.getString(R.string.main_item_status_reconnecting)
|
||||
}
|
||||
val date = Date(subscription.lastActive * 1000)
|
||||
|
|
@ -120,7 +120,7 @@ class MainAdapter(
|
|||
statusView.text = statusMessage
|
||||
dateView.text = dateText
|
||||
dateView.visibility = if (isUnifiedPush) View.GONE else View.VISIBLE
|
||||
val showConnectionError = subscription.instant && subscription.state == ConnectionState.CONNECTING
|
||||
val showConnectionError = subscription.instant && subscription.connectionDetails.hasError()
|
||||
connectionErrorImageView.visibility = if (showConnectionError) View.VISIBLE else View.GONE
|
||||
notificationDisabledUntilImageView.visibility = if (showMutedUntilIcon) View.VISIBLE else View.GONE
|
||||
notificationDisabledForeverImageView.visibility = if (showMutedForeverIcon) View.VISIBLE else View.GONE
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/detail_menu_connection_error"
|
||||
android:icon="@drawable/ic_warning_white_24dp"
|
||||
android:title="@string/main_menu_connection_error"
|
||||
android:visible="false"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/detail_menu_notifications_enabled"
|
||||
android:icon="@drawable/ic_notifications_white_24dp"
|
||||
|
|
@ -26,12 +32,6 @@
|
|||
android:icon="@drawable/ic_bolt_white_24dp"
|
||||
android:title="@string/detail_menu_disable_instant"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/detail_menu_connection_error"
|
||||
android:icon="@drawable/ic_warning_white_24dp"
|
||||
android:title="@string/main_menu_connection_error"
|
||||
android:visible="false"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/detail_menu_settings"
|
||||
android:title="@string/detail_menu_settings" />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/main_menu_connection_error"
|
||||
android:icon="@drawable/ic_warning_white_24dp"
|
||||
android:title="@string/main_menu_connection_error"
|
||||
android:visible="false"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/main_menu_notifications_enabled"
|
||||
android:icon="@drawable/ic_notifications_white_24dp"
|
||||
|
|
@ -16,12 +22,6 @@
|
|||
android:icon="@drawable/ic_notifications_off_white_outline_24dp"
|
||||
android:title="@string/detail_menu_notifications_disabled_forever"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/main_menu_connection_error"
|
||||
android:icon="@drawable/ic_warning_white_24dp"
|
||||
android:title="@string/main_menu_connection_error"
|
||||
android:visible="false"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/main_menu_settings"
|
||||
android:title="@string/main_menu_settings_title" />
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue