WIP Connection error dialog
This commit is contained in:
parent
1fa72d9bfd
commit
bdcecee3e9
14 changed files with 364 additions and 4 deletions
|
|
@ -72,6 +72,21 @@ enum class ConnectionState {
|
|||
NOT_APPLICABLE, CONNECTING, CONNECTED
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a connection error for a specific baseUrl.
|
||||
* 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()
|
||||
) {
|
||||
fun getStackTraceString(): String {
|
||||
return throwable?.stackTraceToString() ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
data class SubscriptionWithMetadata(
|
||||
val id: Long,
|
||||
val baseUrl: String,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database)
|
|||
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())
|
||||
|
||||
// TODO Move these into an ApplicationState singleton
|
||||
val detailViewSubscriptionId = AtomicLong(0L) // Omg, what a hack ...
|
||||
val mediaPlayer = MediaPlayer()
|
||||
|
|
@ -569,6 +572,36 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database)
|
|||
return connectionStatesLiveData.value!!.getOrElse(subscriptionId) { ConnectionState.NOT_APPLICABLE }
|
||||
}
|
||||
|
||||
fun getConnectionErrorsLiveData(): LiveData<Map<String, ConnectionError>> {
|
||||
return connectionErrorsLiveData
|
||||
}
|
||||
|
||||
fun getConnectionErrors(): Map<String, ConnectionError> {
|
||||
return connectionErrors.toMap()
|
||||
}
|
||||
|
||||
fun updateConnectionError(baseUrl: String, message: String, throwable: Throwable?) {
|
||||
val error = ConnectionError(baseUrl, message, throwable)
|
||||
connectionErrors[baseUrl] = error
|
||||
connectionErrorsLiveData.postValue(connectionErrors.toMap())
|
||||
Log.d(TAG, "Connection error updated for $baseUrl: $message")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SHARED_PREFS_ID = "MainPreferences"
|
||||
const val SHARED_PREFS_POLL_WORKER_VERSION = "PollWorkerVersion"
|
||||
|
|
|
|||
|
|
@ -17,6 +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 serviceActive: () -> Boolean
|
||||
) : Connection {
|
||||
private val baseUrl = connectionId.baseUrl
|
||||
|
|
@ -46,10 +47,11 @@ class JsonConnection(
|
|||
notificationListener(subscription, notificationWithSubscriptionId)
|
||||
}
|
||||
val failed = AtomicBoolean(false)
|
||||
val fail = { _: Exception ->
|
||||
val fail = { e: Exception ->
|
||||
failed.set(true)
|
||||
if (isActive && serviceActive()) { // Avoid UI update races if we're restarting a connection
|
||||
stateChangeListener(subscriptionIds, ConnectionState.CONNECTING)
|
||||
errorListener(baseUrl, e)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -66,6 +68,7 @@ class JsonConnection(
|
|||
Log.e(TAG, "[$url] Connection failed: ${e.message}", e)
|
||||
if (isActive && serviceActive()) { // Avoid UI update races if we're restarting a connection
|
||||
stateChangeListener(subscriptionIds, ConnectionState.CONNECTING)
|
||||
errorListener(baseUrl, e)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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, alarmManager)
|
||||
WsConnection(connectionId, repository, httpClient, user, customHeaders, since, ::onStateChanged, ::onNotificationReceived, ::onConnectionError, alarmManager)
|
||||
} else {
|
||||
JsonConnection(connectionId, scope, repository, api, user, since, ::onStateChanged, ::onNotificationReceived, serviceActive)
|
||||
JsonConnection(connectionId, scope, repository, api, user, since, ::onStateChanged, ::onNotificationReceived, ::onConnectionError, serviceActive)
|
||||
}
|
||||
connections[connectionId] = connection
|
||||
connection.start()
|
||||
|
|
@ -307,6 +307,20 @@ 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) {
|
||||
val message = throwable.message ?: "Unknown error"
|
||||
repository.updateConnectionError(baseUrl, message, throwable)
|
||||
}
|
||||
|
||||
private fun onNotificationReceived(subscription: Subscription, notification: io.heckel.ntfy.db.Notification) {
|
||||
|
|
|
|||
|
|
@ -42,6 +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 alarmManager: AlarmManager
|
||||
) : Connection {
|
||||
private val parser = NotificationParser()
|
||||
|
|
@ -183,6 +184,7 @@ class WsConnection(
|
|||
return@synchronize
|
||||
}
|
||||
stateChangeListener(subscriptionIds, ConnectionState.CONNECTING)
|
||||
errorListener(baseUrl, t)
|
||||
state = State.Disconnected
|
||||
errorCount++
|
||||
val retrySeconds = RETRY_SECONDS.getOrNull(errorCount) ?: RETRY_SECONDS.last()
|
||||
|
|
|
|||
112
app/src/main/java/io/heckel/ntfy/ui/ConnectionErrorFragment.kt
Normal file
112
app/src/main/java/io/heckel/ntfy/ui/ConnectionErrorFragment.kt
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
package io.heckel.ntfy.ui
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ImageView
|
||||
import android.widget.ScrollView
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import io.heckel.ntfy.R
|
||||
import io.heckel.ntfy.db.ConnectionError
|
||||
import io.heckel.ntfy.db.Repository
|
||||
|
||||
class ConnectionErrorFragment : DialogFragment() {
|
||||
private lateinit var repository: Repository
|
||||
private var connectionErrors: Map<String, ConnectionError> = emptyMap()
|
||||
private var selectedBaseUrl: String? = null
|
||||
private var detailsVisible = false
|
||||
|
||||
private lateinit var serverSpinner: Spinner
|
||||
private lateinit var errorTextView: TextView
|
||||
private lateinit var showDetailsTextView: TextView
|
||||
private lateinit var detailsScrollView: ScrollView
|
||||
private lateinit var stackTraceTextView: TextView
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
if (activity == null) {
|
||||
throw IllegalStateException("Activity cannot be null")
|
||||
}
|
||||
|
||||
// Dependencies
|
||||
repository = Repository.getInstance(requireContext())
|
||||
connectionErrors = repository.getConnectionErrors()
|
||||
|
||||
// Build root view
|
||||
val view = requireActivity().layoutInflater.inflate(R.layout.fragment_connection_error_dialog, null)
|
||||
|
||||
// Get view references
|
||||
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)
|
||||
detailsScrollView = view.findViewById(R.id.connection_error_dialog_details_scroll)
|
||||
stackTraceTextView = view.findViewById(R.id.connection_error_dialog_stack_trace)
|
||||
|
||||
// Setup server spinner if multiple errors
|
||||
val baseUrls = connectionErrors.keys.toList()
|
||||
if (baseUrls.size > 1) {
|
||||
serverSpinner.visibility = View.VISIBLE
|
||||
val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, baseUrls)
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
serverSpinner.adapter = adapter
|
||||
serverSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||
selectedBaseUrl = baseUrls[position]
|
||||
updateErrorDisplay()
|
||||
}
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||
}
|
||||
} else {
|
||||
serverSpinner.visibility = View.GONE
|
||||
}
|
||||
|
||||
// Select first error by default
|
||||
selectedBaseUrl = baseUrls.firstOrNull()
|
||||
updateErrorDisplay()
|
||||
|
||||
// Toggle details visibility
|
||||
showDetailsTextView.setOnClickListener {
|
||||
detailsVisible = !detailsVisible
|
||||
updateDetailsVisibility()
|
||||
}
|
||||
|
||||
return MaterialAlertDialogBuilder(requireContext())
|
||||
.setView(view)
|
||||
.setPositiveButton(R.string.connection_error_dialog_dismiss) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.create()
|
||||
}
|
||||
|
||||
private fun updateErrorDisplay() {
|
||||
val error = selectedBaseUrl?.let { connectionErrors[it] }
|
||||
if (error != null) {
|
||||
errorTextView.text = error.message
|
||||
stackTraceTextView.text = error.getStackTraceString().ifEmpty {
|
||||
getString(R.string.connection_error_dialog_no_stack_trace)
|
||||
}
|
||||
} else {
|
||||
errorTextView.text = getString(R.string.connection_error_dialog_no_error)
|
||||
stackTraceTextView.text = ""
|
||||
}
|
||||
updateDetailsVisibility()
|
||||
}
|
||||
|
||||
private fun updateDetailsVisibility() {
|
||||
if (detailsVisible) {
|
||||
detailsScrollView.visibility = View.VISIBLE
|
||||
showDetailsTextView.text = getString(R.string.connection_error_dialog_hide_details)
|
||||
} else {
|
||||
detailsScrollView.visibility = View.GONE
|
||||
showDetailsTextView.text = getString(R.string.connection_error_dialog_show_details)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "NtfyConnectionErrorFragment"
|
||||
}
|
||||
}
|
||||
|
|
@ -253,6 +253,11 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific
|
|||
SubscriberServiceManager.refresh(this)
|
||||
}
|
||||
|
||||
// Observe connection errors and update menu item visibility
|
||||
repository.getConnectionErrorsLiveData().observe(this) { errors ->
|
||||
showHideConnectionErrorMenuItem(errors)
|
||||
}
|
||||
|
||||
// Battery banner
|
||||
val batteryBanner = findViewById<View>(R.id.main_banner_battery) // Banner visibility is toggled in onResume()
|
||||
val dontAskAgainButton = findViewById<Button>(R.id.main_banner_battery_dontaskagain)
|
||||
|
|
@ -550,6 +555,16 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific
|
|||
}
|
||||
}
|
||||
|
||||
private fun showHideConnectionErrorMenuItem(errors: Map<String, io.heckel.ntfy.db.ConnectionError>) {
|
||||
if (!this::menu.isInitialized) {
|
||||
return
|
||||
}
|
||||
runOnUiThread {
|
||||
val connectionErrorItem = menu.findItem(R.id.main_menu_connection_error)
|
||||
connectionErrorItem?.isVisible = errors.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.main_menu_notifications_enabled -> {
|
||||
|
|
@ -564,6 +579,10 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific
|
|||
onNotificationSettingsClick(enable = true)
|
||||
true
|
||||
}
|
||||
R.id.main_menu_connection_error -> {
|
||||
onConnectionErrorClick()
|
||||
true
|
||||
}
|
||||
R.id.main_menu_settings -> {
|
||||
startActivity(Intent(this, SettingsActivity::class.java))
|
||||
true
|
||||
|
|
@ -607,6 +626,12 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific
|
|||
}
|
||||
}
|
||||
|
||||
private fun onConnectionErrorClick() {
|
||||
Log.d(TAG, "Showing connection error dialog")
|
||||
val connectionErrorFragment = ConnectionErrorFragment()
|
||||
connectionErrorFragment.show(supportFragmentManager, ConnectionErrorFragment.TAG)
|
||||
}
|
||||
|
||||
override fun onNotificationMutedUntilChanged(mutedUntilTimestamp: Long) {
|
||||
repository.setGlobalMutedUntil(mutedUntilTimestamp)
|
||||
showHideNotificationMenuItems()
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ class MainAdapter(
|
|||
private val nameView: TextView = itemView.findViewById(R.id.main_item_text)
|
||||
private val statusView: TextView = itemView.findViewById(R.id.main_item_status)
|
||||
private val dateView: TextView = itemView.findViewById(R.id.main_item_date)
|
||||
private val connectionErrorImageView: View = itemView.findViewById(R.id.main_item_connection_error_image)
|
||||
private val notificationDisabledUntilImageView: View = itemView.findViewById(R.id.main_item_notification_disabled_until_image)
|
||||
private val notificationDisabledForeverImageView: View = itemView.findViewById(R.id.main_item_notification_disabled_forever_image)
|
||||
private val instantImageView: View = itemView.findViewById(R.id.main_item_instant_image)
|
||||
|
|
@ -119,6 +120,8 @@ 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
|
||||
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
|
||||
instantImageView.visibility = if (subscription.instant && BuildConfig.FIREBASE_AVAILABLE) View.VISIBLE else View.GONE
|
||||
|
|
|
|||
9
app/src/main/res/drawable/ic_warning_gray_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_warning_gray_24dp.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#757575"
|
||||
android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z"/>
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/ic_warning_white_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_warning_white_24dp.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z"/>
|
||||
</vector>
|
||||
111
app/src/main/res/layout/fragment_connection_error_dialog.xml
Normal file
111
app/src/main/res/layout/fragment_connection_error_dialog.xml
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/connection_error_dialog_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:text="@string/connection_error_dialog_title"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/connection_error_dialog_server_spinner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@id/connection_error_dialog_title"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/connection_error_dialog_icon"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:src="@drawable/ic_warning_amber_24dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/connection_error_dialog_server_spinner"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/connection_error_dialog_message"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/connection_error_dialog_message"
|
||||
android:textAlignment="center"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
app:layout_constraintTop_toBottomOf="@id/connection_error_dialog_icon"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/connection_error_dialog_error_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:textAlignment="center"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
app:layout_constraintTop_toBottomOf="@id/connection_error_dialog_message"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/connection_error_dialog_show_details"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/connection_error_dialog_show_details"
|
||||
android:textAlignment="center"
|
||||
android:textColor="?android:attr/colorAccent"
|
||||
android:textStyle="bold"
|
||||
android:padding="8dp"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
app:layout_constraintTop_toBottomOf="@id/connection_error_dialog_error_text"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/connection_error_dialog_details_scroll"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:maxHeight="200dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@id/connection_error_dialog_show_details"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/connection_error_dialog_stack_trace"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp"
|
||||
android:background="@android:color/darker_gray"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:fontFamily="monospace"
|
||||
android:textSize="10sp" />
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
android:layout_marginStart="12dp" app:layout_constraintStart_toEndOf="@+id/main_item_image"
|
||||
app:layout_constraintVertical_bias="0.0" android:textAppearance="@style/TextAppearance.AppCompat.Medium"
|
||||
android:textColor="?android:attr/textColorPrimary" android:layout_marginTop="10dp"
|
||||
app:layout_constraintEnd_toStartOf="@id/main_item_notification_disabled_until_image"/>
|
||||
app:layout_constraintEnd_toStartOf="@id/main_item_connection_error_image"/>
|
||||
<TextView
|
||||
android:text="89 notifications, reconnecting ... This may wrap in the case of UnifiedPush"
|
||||
android:layout_width="0dp"
|
||||
|
|
@ -31,6 +31,14 @@
|
|||
app:layout_constraintTop_toBottomOf="@+id/main_item_text" app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:layout_marginBottom="10dp" app:layout_constrainedWidth="true"
|
||||
app:layout_constraintEnd_toStartOf="@id/main_item_new" android:layout_marginEnd="10dp"/>
|
||||
<ImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="24dp" app:srcCompat="@drawable/ic_warning_amber_24dp"
|
||||
android:id="@+id/main_item_connection_error_image"
|
||||
app:layout_constraintTop_toTopOf="@+id/main_item_text"
|
||||
app:layout_constraintEnd_toStartOf="@+id/main_item_notification_disabled_until_image"
|
||||
android:paddingTop="3dp" android:layout_marginEnd="3dp"
|
||||
android:visibility="gone"/>
|
||||
<ImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="24dp" app:srcCompat="@drawable/ic_notifications_off_time_gray_outline_24dp"
|
||||
|
|
|
|||
|
|
@ -16,6 +16,12 @@
|
|||
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" />
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@
|
|||
<string name="main_menu_report_bug_title">Report a bug</string>
|
||||
<string name="main_menu_docs_title">Read the docs</string>
|
||||
<string name="main_menu_rate_title">Rate the app ⭐</string>
|
||||
<string name="main_menu_connection_error">Connection error</string>
|
||||
|
||||
<!-- Main activity: Action mode -->
|
||||
<string name="main_action_mode_menu_unsubscribe">Unsubscribe</string>
|
||||
|
|
@ -288,6 +289,15 @@
|
|||
<string name="notification_dialog_tomorrow">Until tomorrow</string>
|
||||
<string name="notification_dialog_forever">Until resumed</string>
|
||||
|
||||
<!-- Connection error dialog -->
|
||||
<string name="connection_error_dialog_title">Connection Error</string>
|
||||
<string name="connection_error_dialog_message">There was a problem connecting to the server. The app will keep trying to reconnect.</string>
|
||||
<string name="connection_error_dialog_show_details">Show details</string>
|
||||
<string name="connection_error_dialog_hide_details">Hide 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_dismiss">Dismiss</string>
|
||||
|
||||
<!-- Notification popup -->
|
||||
<string name="notification_popup_action_open">Open</string>
|
||||
<string name="notification_popup_action_browse">Browse</string>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue