diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6ca9d3c0..4305b77a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,6 +3,7 @@
+
@@ -131,14 +132,15 @@
android:enabled="true"
android:exported="false"/>
-
+
-
+
+
diff --git a/app/src/main/java/io/heckel/ntfy/app/Application.kt b/app/src/main/java/io/heckel/ntfy/app/Application.kt
index d594d670..da9219ea 100644
--- a/app/src/main/java/io/heckel/ntfy/app/Application.kt
+++ b/app/src/main/java/io/heckel/ntfy/app/Application.kt
@@ -1,8 +1,11 @@
package io.heckel.ntfy.app
import android.app.Application
+import android.net.ConnectivityManager
+import android.net.Network
import com.google.android.material.color.DynamicColors
import io.heckel.ntfy.db.Repository
+import io.heckel.ntfy.service.SubscriberServiceManager
import io.heckel.ntfy.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -24,5 +27,18 @@ class Application : Application() {
if (repository.getDynamicColorsEnabled()) {
DynamicColors.applyToActivitiesIfAvailable(this)
}
+ registerNetworkCallback()
+ }
+
+ private fun registerNetworkCallback() {
+ val connectivityManager = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
+ connectivityManager.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() {
+ override fun onAvailable(network: Network) {
+ SubscriberServiceManager.refresh(this@Application)
+ }
+ override fun onLost(network: Network) {
+ SubscriberServiceManager.refresh(this@Application)
+ }
+ })
}
}
diff --git a/app/src/main/java/io/heckel/ntfy/backup/Backuper.kt b/app/src/main/java/io/heckel/ntfy/backup/Backuper.kt
index 2dafa839..2bb28909 100644
--- a/app/src/main/java/io/heckel/ntfy/backup/Backuper.kt
+++ b/app/src/main/java/io/heckel/ntfy/backup/Backuper.kt
@@ -90,6 +90,9 @@ class Backuper(val context: Context) {
if (settings.mutedUntil != null) {
repository.setGlobalMutedUntil(settings.mutedUntil)
}
+ if (settings.connectionAlertSeconds != null) {
+ repository.setConnectionAlertSeconds(settings.connectionAlertSeconds)
+ }
if (settings.lastSharedTopics != null) {
settings.lastSharedTopics.forEach { repository.addLastShareTopic(it) }
}
@@ -278,6 +281,7 @@ class Backuper(val context: Context) {
recordLogs = repository.getRecordLogs(),
defaultBaseUrl = repository.getDefaultBaseUrl() ?: "",
mutedUntil = repository.getGlobalMutedUntil(),
+ connectionAlertSeconds = repository.getConnectionAlertSeconds(),
lastSharedTopics = repository.getLastShareTopics()
)
}
@@ -421,6 +425,7 @@ data class Settings(
val recordLogs: Boolean?,
val defaultBaseUrl: String?,
val mutedUntil: Long?,
+ val connectionAlertSeconds: Long?,
val lastSharedTopics: List?,
)
diff --git a/app/src/main/java/io/heckel/ntfy/db/Repository.kt b/app/src/main/java/io/heckel/ntfy/db/Repository.kt
index a6716c74..4333180d 100644
--- a/app/src/main/java/io/heckel/ntfy/db/Repository.kt
+++ b/app/src/main/java/io/heckel/ntfy/db/Repository.kt
@@ -440,13 +440,24 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database)
}
}
- fun getConnectionAlertSnoozeUntil(): Long {
- return sharedPrefs.getLong(SHARED_PREFS_CONNECTION_ALERT_SNOOZE_UNTIL, CONNECTION_ALERT_SNOOZE_UNTIL_DEFAULT)
+ fun getConnectionAlertSeconds(): Long {
+ return sharedPrefs.getLong(SHARED_PREFS_CONNECTION_ALERT_SECONDS, CONNECTION_ALERT_DEFAULT)
}
- fun setConnectionAlertSnoozeUntil(timeMillis: Long) {
+ fun setConnectionAlertSeconds(seconds: Long) {
sharedPrefs.edit {
- putLong(SHARED_PREFS_CONNECTION_ALERT_SNOOZE_UNTIL, timeMillis)
+ putLong(SHARED_PREFS_CONNECTION_ALERT_SECONDS, seconds)
+ putLong(SHARED_PREFS_CONNECTION_ALERT_SNOOZE_UNTIL_TIME, 0L)
+ }
+ }
+
+ fun getConnectionAlertSnoozeUntilTime(): Long {
+ return sharedPrefs.getLong(SHARED_PREFS_CONNECTION_ALERT_SNOOZE_UNTIL_TIME, 0L)
+ }
+
+ fun setConnectionAlertSnoozeUntilTime(timeMillis: Long) {
+ sharedPrefs.edit {
+ putLong(SHARED_PREFS_CONNECTION_ALERT_SNOOZE_UNTIL_TIME, timeMillis)
}
}
@@ -608,6 +619,11 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database)
return connectionDetails.toMap()
}
+ fun clearConnectionDetails() {
+ connectionDetails.clear()
+ connectionDetailsLiveData.postValue(emptyMap())
+ }
+
fun getConnectionForceReconnectVersion(baseUrl: String): Long {
return connectionForceReconnectVersions[baseUrl] ?: 0L
}
@@ -634,14 +650,13 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database)
const val SHARED_PREFS_INSISTENT_MAX_PRIORITY_ENABLED = "InsistentMaxPriority"
const val SHARED_PREFS_RECORD_LOGS_ENABLED = "RecordLogs"
const val SHARED_PREFS_MESSAGE_BAR_ENABLED = "MessageBarEnabled"
- const val SHARED_PREFS_BATTERY_OPTIMIZATIONS_REMIND_TIME = "BatteryOptimizationsRemindTime"
- const val SHARED_PREFS_WEBSOCKET_REMIND_TIME = "JsonStreamRemindTime" // "Use WebSocket" banner (used to be JSON stream deprecation banner)
- const val SHARED_PREFS_WEBSOCKET_RECONNECT_REMIND_TIME = "WebSocketReconnectRemindTime"
+ const val SHARED_PREFS_BATTERY_OPTIMIZATIONS_REMIND_TIME = "BatteryOptimizationsRemindTime" // Timestamp as millis
+ const val SHARED_PREFS_WEBSOCKET_REMIND_TIME = "JsonStreamRemindTime" // "Use WebSocket" banner (used to be JSON stream deprecation banner), timestamp as millis
+ const val SHARED_PREFS_WEBSOCKET_RECONNECT_REMIND_TIME = "WebSocketReconnectRemindTime" // Timestamp as millis
const val SHARED_PREFS_UNIFIED_PUSH_BASE_URL = "UnifiedPushBaseURL" // Legacy key required for migration to DefaultBaseURL
const val SHARED_PREFS_DEFAULT_BASE_URL = "DefaultBaseURL"
- const val SHARED_PREFS_CONNECTION_ALERT_SNOOZE_UNTIL = "ConnectionAlertSnoozeUntil"
- const val CONNECTION_ALERT_SNOOZE_UNTIL_DEFAULT = 0L
- const val CONNECTION_ALERT_NEVER_SHOW = Long.MAX_VALUE
+ const val SHARED_PREFS_CONNECTION_ALERT_SECONDS = "ConnectionAlertSeconds"
+ const val SHARED_PREFS_CONNECTION_ALERT_SNOOZE_UNTIL_TIME = "ConnectionAlertSnoozeUntilTime" // Timestamp in millis
const val SHARED_PREFS_LAST_TOPICS = "LastTopics"
private const val LAST_TOPICS_COUNT = 3
@@ -653,12 +668,14 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database)
const val MUTED_UNTIL_FOREVER = 1L
const val MUTED_UNTIL_TOMORROW = 2L
- private const val ONE_MB = 1024 * 1024L
+ private const val ONE_MB_BYTES = 1024 * 1024L
const val AUTO_DOWNLOAD_NEVER = 0L // Values must match values.xml
const val AUTO_DOWNLOAD_ALWAYS = 1L
- const val AUTO_DOWNLOAD_DEFAULT = ONE_MB
+ const val AUTO_DOWNLOAD_DEFAULT = ONE_MB_BYTES
+
+ private const val ONE_HOUR_SECONDS = 60 * 60L
+ private const val ONE_DAY_SECONDS = 24 * ONE_HOUR_SECONDS
- private const val ONE_DAY_SECONDS = 24 * 60 * 60L
const val AUTO_DELETE_USE_GLOBAL = -1L // Values must match values.xml
const val AUTO_DELETE_NEVER = 0L
const val AUTO_DELETE_ONE_DAY_SECONDS = ONE_DAY_SECONDS
@@ -671,6 +688,14 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database)
const val INSISTENT_MAX_PRIORITY_USE_GLOBAL = -1 // Values must match values.xml
const val INSISTENT_MAX_PRIORITY_ENABLED = 1 // 0 = Disabled (but not needed in code)
+ const val CONNECTION_ALERT_NEVER = 0L
+ const val CONNECTION_ALERT_FIVE_MINUTES_SECONDS = 5 * 60L
+ const val CONNECTION_ALERT_FIFTEEN_MINUTES_SECONDS = 15 * 60L
+ const val CONNECTION_ALERT_ONE_HOUR_SECONDS = ONE_HOUR_SECONDS
+ const val CONNECTION_ALERT_THREE_HOURS_SECONDS = 3 * ONE_HOUR_SECONDS
+ const val CONNECTION_ALERT_TWELVE_HOURS_SECONDS = 12 * ONE_HOUR_SECONDS
+ const val CONNECTION_ALERT_DEFAULT = CONNECTION_ALERT_NEVER
+
const val CONNECTION_PROTOCOL_JSONHTTP = "jsonhttp"
const val CONNECTION_PROTOCOL_WS = "ws"
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 56d3233c..06b4a7c1 100644
--- a/app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt
+++ b/app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt
@@ -24,12 +24,12 @@ import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.db.Subscription
import io.heckel.ntfy.msg.ApiService
import io.heckel.ntfy.msg.NotificationDispatcher
-import io.heckel.ntfy.msg.NotificationService
-import io.heckel.ntfy.util.PRIORITY_HIGH
import io.heckel.ntfy.ui.Colors
import io.heckel.ntfy.ui.MainActivity
import io.heckel.ntfy.util.HttpUtil
import io.heckel.ntfy.util.Log
+import io.heckel.ntfy.util.isNetworkAvailable
+import io.heckel.ntfy.util.shortUrl
import io.heckel.ntfy.util.topicUrl
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
@@ -316,22 +316,28 @@ class SubscriberService : Service() {
}
private fun maybeShowConnectionAlert() {
- val now = System.currentTimeMillis()
+ val thresholdSeconds = repository.getConnectionAlertSeconds()
+ if (thresholdSeconds <= 0L) return
- // Check snooze / never-show-again
- val snoozeUntil = repository.getConnectionAlertSnoozeUntil()
- if (snoozeUntil == Repository.CONNECTION_ALERT_NEVER_SHOW) return
+ // Don't show alert if the device has no network connectivity (e.g. airplane mode)
+ if (!isNetworkAvailable(this)) return
+
+ // Check snooze
+ val now = System.currentTimeMillis()
+ val snoozeUntil = repository.getConnectionAlertSnoozeUntilTime()
if (snoozeUntil > now) return
- // Check if any connection has been in error for 15+ minutes
+ // Check if any connection has been in error for longer than the threshold
+ val thresholdMillis = thresholdSeconds * 1000L
val allDetails = repository.getConnectionDetails()
val disconnectedUrls = allDetails.filter { (_, details) ->
details.hasError() && details.firstErrorTime > 0L &&
- (now - details.firstErrorTime) >= CONNECTION_ALERT_THRESHOLD_MILLIS
+ (now - details.firstErrorTime) >= thresholdMillis
}.keys
if (disconnectedUrls.isNotEmpty()) {
- showConnectionAlertNotification(disconnectedUrls)
+ val thresholdMinutes = (thresholdSeconds / 60).toInt()
+ showConnectionAlertNotification(disconnectedUrls, thresholdMinutes)
}
}
@@ -345,47 +351,40 @@ class SubscriberService : Service() {
}
}
- private fun showConnectionAlertNotification(disconnectedUrls: Set) {
+ private fun showConnectionAlertNotification(disconnectedUrls: Set, thresholdMinutes: Int) {
val text = if (disconnectedUrls.size == 1) {
- getString(R.string.connection_alert_text_one, disconnectedUrls.first(), CONNECTION_ALERT_THRESHOLD_MINUTES)
+ getString(R.string.connection_alert_text_one, shortUrl(disconnectedUrls.first()), thresholdMinutes)
} else {
- getString(R.string.connection_alert_text_multiple, disconnectedUrls.size, CONNECTION_ALERT_THRESHOLD_MINUTES)
+ getString(R.string.connection_alert_text_multiple, disconnectedUrls.size, thresholdMinutes)
}
- val dismissIntent = Intent(this, ConnectionAlertBroadcastReceiver::class.java).apply {
- action = CONNECTION_ALERT_ACTION_DISMISS
- }
- val dismissPendingIntent = PendingIntent.getBroadcast(this, 0, dismissIntent,
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
-
- val snoozeIntent = Intent(this, ConnectionAlertBroadcastReceiver::class.java).apply {
- action = CONNECTION_ALERT_ACTION_SNOOZE
- }
- val snoozePendingIntent = PendingIntent.getBroadcast(this, 1, snoozeIntent,
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
-
- val neverIntent = Intent(this, ConnectionAlertBroadcastReceiver::class.java).apply {
- action = CONNECTION_ALERT_ACTION_NEVER
- }
- val neverPendingIntent = PendingIntent.getBroadcast(this, 2, neverIntent,
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
-
val contentIntent = PendingIntent.getActivity(this, 0,
Intent(this, MainActivity::class.java), PendingIntent.FLAG_IMMUTABLE)
+ val snoozeShortIntent = PendingIntent.getBroadcast(this, 0,
+ Intent(this, ConnectionAlertBroadcastReceiver::class.java).apply { action = CONNECTION_ALERT_ACTION_SNOOZE_SHORT },
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+ val snoozeIntent = PendingIntent.getBroadcast(this, 0,
+ Intent(this, ConnectionAlertBroadcastReceiver::class.java).apply { action = CONNECTION_ALERT_ACTION_SNOOZE_LONG },
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+ val neverAlertIntent = PendingIntent.getBroadcast(this, 0,
+ Intent(this, ConnectionAlertBroadcastReceiver::class.java).apply { action = CONNECTION_ALERT_ACTION_NEVER },
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+ val deleteIntent = PendingIntent.getBroadcast(this, 0,
+ Intent(this, ConnectionAlertBroadcastReceiver::class.java).apply { action = CONNECTION_ALERT_ACTION_DISMISS },
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
- val channelId = NotificationService(this).toChannelId(NotificationService.DEFAULT_GROUP, PRIORITY_HIGH)
- val notification = NotificationCompat.Builder(this, channelId)
+ val notification = NotificationCompat.Builder(this, NOTIFICATION_CONNECTION_ALERT_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setColor(Colors.notificationIcon(this))
.setContentTitle(getString(R.string.connection_alert_title))
.setContentText(text)
.setStyle(NotificationCompat.BigTextStyle().bigText(text))
.setContentIntent(contentIntent)
- .setAutoCancel(true)
.setOnlyAlertOnce(true)
- .addAction(NotificationCompat.Action.Builder(0, getString(R.string.connection_alert_action_dismiss), dismissPendingIntent).build())
- .addAction(NotificationCompat.Action.Builder(0, getString(R.string.connection_alert_action_snooze), snoozePendingIntent).build())
- .addAction(NotificationCompat.Action.Builder(0, getString(R.string.connection_alert_action_never), neverPendingIntent).build())
+ .setDeleteIntent(deleteIntent)
+ .addAction(NotificationCompat.Action.Builder(0, getString(R.string.connection_alert_action_snooze, CONNECTION_ALERT_SNOOZE_SHORT_HOURS), snoozeShortIntent).build())
+ .addAction(NotificationCompat.Action.Builder(0, getString(R.string.connection_alert_action_snooze, CONNECTION_ALERT_SNOOZE_LONG_HOURS), snoozeIntent).build())
+ .addAction(NotificationCompat.Action.Builder(0, getString(R.string.connection_alert_action_never), neverAlertIntent).build())
.build()
Log.d(TAG, "Showing connection alert notification")
@@ -440,6 +439,9 @@ class SubscriberService : Service() {
it
}
notificationManager.createNotificationChannel(channel)
+ val connectionAlertChannelName = getString(R.string.channel_connection_alert_name)
+ val connectionAlertChannel = NotificationChannel(NOTIFICATION_CONNECTION_ALERT_CHANNEL_ID, connectionAlertChannelName, NotificationManager.IMPORTANCE_DEFAULT)
+ notificationManager.createNotificationChannel(connectionAlertChannel)
return notificationManager
}
@@ -500,22 +502,19 @@ class SubscriberService : Service() {
Log.d(TAG, "ConnectionAlertBroadcastReceiver: action=${intent.action}")
val repository = Repository.getInstance(context)
val notificationManager = context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
-
when (intent.action) {
- CONNECTION_ALERT_ACTION_DISMISS -> {
- notificationManager.cancel(NOTIFICATION_CONNECTION_ALERT_ID)
+ CONNECTION_ALERT_ACTION_DISMISS, CONNECTION_ALERT_ACTION_SNOOZE_SHORT -> {
+ repository.setConnectionAlertSnoozeUntilTime(System.currentTimeMillis() + CONNECTION_ALERT_SNOOZE_SHORT_MILLIS)
}
- CONNECTION_ALERT_ACTION_SNOOZE -> {
- repository.setConnectionAlertSnoozeUntil(
- System.currentTimeMillis() + CONNECTION_ALERT_SNOOZE_DURATION_MILLIS
- )
- notificationManager.cancel(NOTIFICATION_CONNECTION_ALERT_ID)
+ CONNECTION_ALERT_ACTION_SNOOZE_LONG -> {
+ repository.setConnectionAlertSnoozeUntilTime(System.currentTimeMillis() + CONNECTION_ALERT_SNOOZE_LONG_MILLIS)
}
CONNECTION_ALERT_ACTION_NEVER -> {
- repository.setConnectionAlertSnoozeUntil(Repository.CONNECTION_ALERT_NEVER_SHOW)
- notificationManager.cancel(NOTIFICATION_CONNECTION_ALERT_ID)
+ repository.setConnectionAlertSeconds(Repository.CONNECTION_ALERT_NEVER)
}
+ else -> return
}
+ notificationManager.cancel(NOTIFICATION_CONNECTION_ALERT_ID)
}
}
@@ -529,18 +528,24 @@ class SubscriberService : Service() {
const val SERVICE_START_WORKER_VERSION = BuildConfig.VERSION_CODE
const val SERVICE_START_WORKER_WORK_NAME_PERIODIC = "NtfyAutoRestartWorkerPeriodic" // Do not change!
+ private const val ONE_HOUR_SECONDS = 60 * 60L
+ private const val ONE_HOUR_MILLIS = ONE_HOUR_SECONDS * 1000L
+
private const val WAKE_LOCK_TAG = "SubscriberService:lock"
private const val NOTIFICATION_CHANNEL_ID = "ntfy-subscriber"
+ private const val NOTIFICATION_CONNECTION_ALERT_CHANNEL_ID = "ntfy-connection-alert"
private const val NOTIFICATION_GROUP_ID = "io.heckel.ntfy.NOTIFICATION_GROUP_SERVICE"
private const val NOTIFICATION_SERVICE_ID = 2586
private const val NOTIFICATION_RECEIVED_WAKELOCK_TIMEOUT_MILLIS = 10 * 60 * 1000L /*10 minutes*/
- private const val NOTIFICATION_CONNECTION_ALERT_ID = 2587
- private const val CONNECTION_ALERT_THRESHOLD_MINUTES = 15
- private const val CONNECTION_ALERT_THRESHOLD_MILLIS = CONNECTION_ALERT_THRESHOLD_MINUTES * 60 * 1000L
- private const val CONNECTION_ALERT_SNOOZE_DURATION_MILLIS = 60 * 60 * 1000L /*1 hour*/
+ const val NOTIFICATION_CONNECTION_ALERT_ID = 2587
+ private const val CONNECTION_ALERT_SNOOZE_SHORT_HOURS = 1
+ private const val CONNECTION_ALERT_SNOOZE_SHORT_MILLIS = CONNECTION_ALERT_SNOOZE_SHORT_HOURS * ONE_HOUR_MILLIS
+ private const val CONNECTION_ALERT_SNOOZE_LONG_HOURS = 8
+ private const val CONNECTION_ALERT_SNOOZE_LONG_MILLIS = CONNECTION_ALERT_SNOOZE_LONG_HOURS * ONE_HOUR_MILLIS
private const val CONNECTION_ALERT_ACTION_DISMISS = "io.heckel.ntfy.CONNECTION_ALERT_DISMISS"
- private const val CONNECTION_ALERT_ACTION_SNOOZE = "io.heckel.ntfy.CONNECTION_ALERT_SNOOZE"
+ private const val CONNECTION_ALERT_ACTION_SNOOZE_SHORT = "io.heckel.ntfy.CONNECTION_ALERT_SNOOZE_SHORT"
+ private const val CONNECTION_ALERT_ACTION_SNOOZE_LONG = "io.heckel.ntfy.CONNECTION_ALERT_SNOOZE_LONG"
private const val CONNECTION_ALERT_ACTION_NEVER = "io.heckel.ntfy.CONNECTION_ALERT_NEVER"
}
}
diff --git a/app/src/main/java/io/heckel/ntfy/service/SubscriberServiceManager.kt b/app/src/main/java/io/heckel/ntfy/service/SubscriberServiceManager.kt
index 71f51b48..f577b10c 100644
--- a/app/src/main/java/io/heckel/ntfy/service/SubscriberServiceManager.kt
+++ b/app/src/main/java/io/heckel/ntfy/service/SubscriberServiceManager.kt
@@ -1,11 +1,13 @@
package io.heckel.ntfy.service
+import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import androidx.core.content.ContextCompat
import androidx.work.*
import io.heckel.ntfy.app.Application
import io.heckel.ntfy.util.Log
+import io.heckel.ntfy.util.isNetworkAvailable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -40,8 +42,9 @@ class SubscriberServiceManager(private val context: Context) {
withContext(Dispatchers.IO) {
val app = context.applicationContext as Application
val subscriptionIdsWithInstantStatus = app.repository.getSubscriptionIdsWithInstantStatus()
+ val hasNetwork = isNetworkAvailable(context)
val instantSubscriptions = subscriptionIdsWithInstantStatus.toList().filter { (_, instant) -> instant }.size
- if (instantSubscriptions > 0) {
+ if (instantSubscriptions > 0 && hasNetwork) {
// We have instant subscriptions, start the service
Log.d(TAG, "ServiceStartWorker: Starting foreground service (work ID: ${id})")
Intent(context, SubscriberService::class.java).also {
@@ -56,9 +59,14 @@ class SubscriberServiceManager(private val context: Context) {
}
}
} else {
- // No instant subscriptions, stop the service using stopService()
+ // No instant subscriptions (or no network), stop the service using stopService()
// This avoids ForegroundServiceDidNotStartInTimeException, see #1520
Log.d(TAG, "ServiceStartWorker: Stopping service (work ID: ${id})")
+ if (!hasNetwork) {
+ app.repository.clearConnectionDetails()
+ val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ notificationManager.cancel(SubscriberService.NOTIFICATION_CONNECTION_ALERT_ID)
+ }
Intent(context, SubscriberService::class.java).also {
context.stopService(it)
}
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 251a4ba0..4a1ceb87 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt
@@ -6,8 +6,11 @@ import android.animation.AnimatorListenerAdapter
import android.app.AlarmManager
import android.app.AlertDialog
import android.content.ActivityNotFoundException
+import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
+import android.net.ConnectivityManager
+import android.net.Network
import android.os.Build
import android.os.Bundle
import android.provider.Settings
@@ -63,6 +66,7 @@ import io.heckel.ntfy.util.displayName
import io.heckel.ntfy.util.formatDateShort
import io.heckel.ntfy.util.isDarkThemeOn
import io.heckel.ntfy.util.isIgnoringBatteryOptimizations
+import io.heckel.ntfy.util.isNetworkAvailable
import io.heckel.ntfy.util.maybeSplitTopicUrl
import io.heckel.ntfy.util.randomSubscriptionId
import io.heckel.ntfy.util.shortUrl
@@ -95,6 +99,7 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific
private lateinit var fab: FloatingActionButton
// Other stuff
+ private var networkCallback: ConnectivityManager.NetworkCallback? = null
private var workManager: WorkManager? = null // Context-dependent
private var dispatcher: NotificationDispatcher? = null // Context-dependent
private var appBaseUrl: String? = null // Context-dependent
@@ -342,6 +347,19 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific
}
}
+ // Network state banner
+ showHideNoNetworkBanner()
+ val connectivityManager = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
+ networkCallback = object : ConnectivityManager.NetworkCallback() {
+ override fun onAvailable(network: Network) {
+ runOnUiThread { showHideNoNetworkBanner() }
+ }
+ override fun onLost(network: Network) {
+ runOnUiThread { showHideNoNetworkBanner() }
+ }
+ }
+ connectivityManager.registerDefaultNetworkCallback(networkCallback!!)
+
// Hide links that lead to payments, see https://github.com/binwiederhier/ntfy/issues/1463
val howToLink = findViewById(R.id.main_how_to_link)
howToLink.isVisible = BuildConfig.PAYMENT_LINKS_AVAILABLE
@@ -377,6 +395,7 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific
super.onResume()
showHideNotificationMenuItems()
showHideConnectionErrorMenuItem(repository.getConnectionDetails())
+ showHideNoNetworkBanner()
redrawList()
}
@@ -425,6 +444,22 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific
}
}
+ private fun showHideNoNetworkBanner() {
+ val banner = findViewById(R.id.main_banner_no_network)
+ banner.visibility = if (isNetworkAvailable(this)) View.GONE else View.VISIBLE
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ try {
+ val connectivityManager = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
+ networkCallback?.let { connectivityManager.unregisterNetworkCallback(it) }
+ } catch (e: Exception) {
+ Log.d(TAG, "Failed to unregister network callback: ${e.message}")
+ }
+ networkCallback = null
+ }
+
private fun schedulePeriodicPollWorker() {
val workerVersion = repository.getPollWorkerVersion()
val workPolicy = if (workerVersion == PollWorker.VERSION) {
diff --git a/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt
index 8ebf2f87..f7914af5 100644
--- a/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt
+++ b/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt
@@ -10,9 +10,7 @@ import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.provider.Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM
-import android.text.TextUtils
import android.view.View
-import android.widget.Button
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
@@ -42,7 +40,6 @@ import kotlinx.coroutines.launch
import okhttp3.RequestBody.Companion.toRequestBody
import java.text.SimpleDateFormat
import java.util.*
-import java.util.concurrent.TimeUnit
/**
* Main settings
@@ -327,6 +324,31 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
}
}
+ // Connection alert
+ val connectionAlertPrefId = context?.getString(R.string.settings_advanced_connection_alert_key) ?: return
+ val connectionAlert: ListPreference? = findPreference(connectionAlertPrefId)
+ connectionAlert?.value = repository.getConnectionAlertSeconds().toString()
+ connectionAlert?.preferenceDataStore = object : PreferenceDataStore() {
+ override fun putString(key: String?, value: String?) {
+ val seconds = value?.toLongOrNull() ?:return
+ repository.setConnectionAlertSeconds(seconds)
+ }
+ override fun getString(key: String?, defValue: String?): String {
+ return repository.getConnectionAlertSeconds().toString()
+ }
+ }
+ connectionAlert?.summaryProvider = Preference.SummaryProvider { pref ->
+ when (pref.value.toLongOrNull() ?: repository.getConnectionAlertSeconds()) {
+ Repository.CONNECTION_ALERT_NEVER -> getString(R.string.settings_advanced_connection_alert_summary_never)
+ Repository.CONNECTION_ALERT_FIVE_MINUTES_SECONDS -> getString(R.string.settings_advanced_connection_alert_summary_five_minutes)
+ Repository.CONNECTION_ALERT_FIFTEEN_MINUTES_SECONDS -> getString(R.string.settings_advanced_connection_alert_summary_fifteen_minutes)
+ Repository.CONNECTION_ALERT_ONE_HOUR_SECONDS -> getString(R.string.settings_advanced_connection_alert_summary_one_hour)
+ Repository.CONNECTION_ALERT_THREE_HOURS_SECONDS -> getString(R.string.settings_advanced_connection_alert_summary_three_hours)
+ Repository.CONNECTION_ALERT_TWELVE_HOURS_SECONDS -> getString(R.string.settings_advanced_connection_alert_summary_twelve_hours)
+ else -> getString(R.string.settings_advanced_connection_alert_summary_never) // Must match default const
+ }
+ }
+
// Dark mode
val darkModePrefId = context?.getString(R.string.settings_general_dark_mode_key) ?: return
val darkMode: ListPreference? = findPreference(darkModePrefId)
diff --git a/app/src/main/java/io/heckel/ntfy/util/Util.kt b/app/src/main/java/io/heckel/ntfy/util/Util.kt
index e472fcef..3c857c8e 100644
--- a/app/src/main/java/io/heckel/ntfy/util/Util.kt
+++ b/app/src/main/java/io/heckel/ntfy/util/Util.kt
@@ -10,6 +10,7 @@ import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.drawable.RippleDrawable
+import android.net.ConnectivityManager
import android.net.Uri
import android.os.Build
import android.os.PowerManager
@@ -546,3 +547,10 @@ fun deriveNotificationId(baseUrl: String, topic: String, sequenceId: String): In
val hash = composite.hashCode()
return if (hash == 0 || hash == Int.MIN_VALUE) 1 else abs(hash)
}
+
+// We check for any active network, not specifically for internet connectivity,
+// because the ntfy server may be on a LAN without internet access.
+fun isNetworkAvailable(context: Context): Boolean {
+ val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+ return connectivityManager.activeNetwork != null
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_wifi_off_gray_24dp.xml b/app/src/main/res/drawable/ic_wifi_off_gray_24dp.xml
new file mode 100644
index 00000000..9dbc7e1d
--- /dev/null
+++ b/app/src/main/res/drawable/ic_wifi_off_gray_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 1bf20371..1642bb78 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -232,6 +232,28 @@
+
+
+
+
+ app:layout_constraintTop_toBottomOf="@id/main_banner_no_network">
Търсене в известията
Подразбирани настройки
Въведете адрес на услуга, напр. https://ntfy.example.com
+ Връзката е загубена
+ От най-малко %2$d минути няма връзка с %1$s
+ От най-малко %2$d минути няма връзка със сървърите на %1$d
+ Отлагане с %1$dч
+ Да не се показва повече
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 5067c2dc..71049eed 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -471,4 +471,9 @@
In Benachrichtigungen suchen
Eine gültige Service-URL eingeben, z. B. https://ntfy.example.com
Benachrichtigungen suchen
+ Verbindung verloren
+ Es konnte länger als %2$d Minuten lang keine Verbindung zu %1$s hergestellt werden
+ Es konnte länger als %2$d Minuten lang keine Verbindung zu %1$d Servern hergestellt werden
+ %1$d Std. schlummern
+ Niemals zeigen
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 8df653cc..4961a23e 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -141,8 +141,8 @@
Descargar automáticamente archivos adjuntos de hasta %1$s
Nunca descargar nada automáticamente
Siempre descargar todo automáticamente
- Si es más pequeño que 100 KB
- Si es más pequeño que 500 KB
+ Si es inferior a 100 kB
+ Si es inferior a 500 kB
Si es más pequeño que 5 MB
Eliminar notificaciones
Nunca eliminar automáticamente las notificaciones
@@ -342,11 +342,11 @@
Descartar
Conceder ahora
Alarmas exactas
- ntfy puede programar alarmas exactas (utilizadas para reconectar WebSockets en segundo plano)
- ntfy no puede programar alarmas exactas. Haga clic para conceder el permiso ahora
+ ntfy puede programar alarmas exactas. Las alarmas exactas son requeridas para reconectar WebSockets en segundo plano. Haga clic para revocar este permiso.
+ ntfy no puede programar alarmas exactas. Las alarmas exactas son requeridas para reconectar WebSockets en segundo plano. Haga clic para conceder el permiso.
Publicar a %1$s
Título
- p.e. Alguien está en la puerta
+ ej. Alguien está en la puerta
Mensaje
Etiquetas
Prioridad
@@ -407,6 +407,73 @@
Añadir certificados a la lista de confiados y administrar certificados de cliente para mTLS
Certificados confiados
ej. https://ntfy.example.com
- Subject
+ Asunto
Error de conexión
+ Restaurar valores por defecto
+ Conexión perdida
+ No se pudo conectar a %1$s por más de %2$d minutos
+ No se pudo conectar a %1$d servidores por más de %2$d minutos
+ Suspender %1$dh
+ Nunca mostrar
+ Su búsqueda no tuvo ningún resultado
+ Buscar notificaciones
+ Buscar en notificaciones
+ ej. alerta, peligro
+ Email
+ Markdown
+ Email
+ ej. phil@example.com
+ ej. 30m, 1h, today 9pm (Siempre en inglés)
+ ej. https://example.com/flowers.jpg
+ ej. ilies.jpg
+ ej. +1234567890
+ Error de conexión
+ Hubo un problema al conectar a %1$s. La aplicación continuará intentando reconectar en segundo plano.
+ Conexión rechazada. Puede que el servidor esté desconectado o la dirección sea incorrecta.
+ WebSocket no soportado. Puede que el servidor no soporte conexiones WebSocket, o que la dirección sea incorrecta.
+ No autorizado. El servidor devolvió una respuesta HTTP 401/403. Por favor, compruebe su usuario y contraseña.
+ Reintentar ahora
+ Reintentando en %1$ds…
+ Reintentando…
+ Certificado fijado, emitido por %1$s, expira %2$s, usado para conexiones a %3$s
+ Certificado fijado, emitido por %1$s, expirado, usado para conexiones a %2$s
+ Añadir un certificado de confianza
+ Importa un certificado en el almacén de confianza (PEM). Al conectarse al servidor ntfy, se confiará en este certificado.
+ Certificados de cliente (mTLS)
+ Certificado de cliente, emitido por %1$s, expira %2$s, usado para conectar a %3$s
+ Certificado de cliente, emitido por %1$s, expirado, usado para conectar a %2$s
+ Añadir un certificado de cliente
+ Importar certificado para autenticación TLS mutua (PKCS#12). Este certificado se utilizará al conectarse al servidor.
+ Archivo de certificado no válido
+ Archivo PKCS#12 inválido
+ No se puede añadir el usuario si se ha configurado una cabecera Authorization personalizada para este servidor
+ Introduce una URL del servicio válida, ej. (https://ntfy.example.com)
+ Añadir encabezado personalizado
+ Editar encabezado personalizado
+ Nombre de la cabecera (ej. CF-Access-Client-Id)
+ Valor de la cabecera (ej. 9f3c2e4a1b2d4e)
+ Añade una cabecera HTTP personalizada que se enviará con cada solicitud al servidor especificado.
+ Puedes editar el nombre o el valor de la cabecera seleccionada, o eliminarla.
+ El nombre de la cabecera contiene caracteres no válidos
+ Detalles del certificado
+ Advertencia de seguridad
+ Añadir certificado de confianza
+ Tu conexión no es privada
+ El certificado del servidor no es de confianza. Es posible que atacantes estén intentando robar tu información. No continúes a menos que sepas por qué este certificado no es de confianza.
+ Has seleccionado un archivo de certificado. Revisa los detalles a continuación antes de añadirlo a tus certificados de confianza.
+ Este certificado se utiliza para las conexiones a la URL del servicio indicada abajo. Has añadido esta excepción manualmente.
+ Introduce la URL del servicio a la que debe fijarse este certificado. El certificado solo será de confianza para esta URL.
+ Advertencia: Este certificado ha caducado.
+ Advertencia: Este certificado aún no es válido.
+ URL no válida
+ No se puede cargar el certificado: %1$s
+ Confiar
+ Certificado de cliente
+ Añadir certificado de cliente
+ Introduce la URL del servicio para la que debe usarse este certificado y la contraseña del archivo PKCS#12.
+ Revisa los detalles del certificado y guarda para añadir este certificado de cliente. Este certificado se utilizará para autenticarse con el servidor.
+ Contraseña
+ Contraseña incorrecta o archivo PKCS#12 no válido
+ Contraseña no válida o archivo PKCS#12 dañado
+ URL del servicio no válida
diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml
index e46f2ad4..c0639a4a 100644
--- a/app/src/main/res/values-et/strings.xml
+++ b/app/src/main/res/values-et/strings.xml
@@ -461,4 +461,9 @@
Otsi teavitustest
Taasta algväärtused
Sisesta korrektne teenuse võrguaadress, nt https://ntfy.toredomeen.com
+ Ühendus on katkenud
+ Ühendus %1$s teenusega toimib vaid %2$d minuti(t)
+ Ühendus %1$d serveriga toimib vaid %2$d minuti(t)
+ Tukasta %1$dt
+ Ära näita iialgi
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index c57b708c..ac32bd0f 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -347,10 +347,10 @@
Couleurs dynamiques
Utiliser les couleurs de thème ntfy
Utiliser les couleurs dynamiques du système
- e.g. Quelqu\'un est à la porte
+ par ex. Quelqu\'un est à la porte
Message
Étiquettes
- e.g. warning, skull
+ par ex. attention, crâne
Priorité
Publier
Impossible de publier le message : %1$s
@@ -390,7 +390,7 @@
Afficher la barre de message
Barre de message affichée en bas du sujet
Bouton Publier affiché en bas du sujet
- En-têtes HTTP personnalisés
+ En-têtes personnalisés
Définissez des en-têtes HTTP personnalisés qui seront envoyés avec chaque demande, par exemple, si votre serveur ntfy se trouve derrière un proxy ou un tunnel authentifié.
Ajouter un en-tête
Ajouter un en-tête pour un serveur
@@ -471,4 +471,9 @@
Rechercher dans les notifications
Réinitialiser par défaut
Entrez une URL de service valide, par ex. https://ntfy.example.com
+ Connexion perdue
+ Impossible de se connecter à %1$s depuis plus de %2$d minutes
+ Impossible de se connecter à %1$d serveurs depuis plus de %2$d minutes
+ Sourdine %1$dh
+ Ne jamais montrer
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index c2a22474..a1c4aeae 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -471,4 +471,9 @@
通知内容を検索します
既定にリセット
有効なサービスURLを入力してください、例: https://ntfy.example.com
+ 接続が切断されました
+ %2$d 分以上 %1$s に接続できませんでした
+ %1$d 個のサーバーに %2$d 分以上接続できませんでした
+ %1$d時間後に再通知
+ 再表示しない
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index 27e6ed8e..eb1ca62e 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -364,4 +364,64 @@
Fout bij publiceren: %1$s
Kon bericht niet publiceren: %1$s (code %2$d)
Bericht gepubliceerd
+ Terug naar standaardinstellingen
+ Geldig vanaf
+ Uitgever
+ SHA-256 vingerafdruk
+ Onderwerp
+ Onjuiste server URL
+ Vul de server URL in waar dit certificaat voor moet worden gebruikt, en het wachtwoord voor het PKCS#12 bestand.
+ Beoordeel de certificaat instellingen en sla op om het certificaat toe te voegen. Dit certificaat zal worden gebruikt om te authenticeren met de server.
+ Wachtwoord
+ Onjuist wachtwoord of onjuist PKCS#12 bestand
+ Onjuist wachtwoord of beschadigd PKCS#12 bestand
+ Verbinding verbroken
+ Kan geen verbinding maken met %1$s voor meer dan %2$d minuten
+ Kan geen verbinding maken met %1$d servers voor meer dan %2$d minuten
+ Sluimer voor %1$d uur
+ Nooit laten zien
+ Publieer naar %1$s
+ Uploaden: %1$s (%2$s / %3$s)
+ Upload geannuleerd
+ Titel
+ Labels
+ Prioriteit
+ Klik URL
+ Email
+ Vertraging
+ Markdown
+ Insluiten via URL
+ Bestand insluiten
+ Telefoongesprek
+ Klik URL
+ bijvoorbeeld https://example.com/alerts/1234
+ Email
+ bijvoorbeeld jan@jansen.nl
+ Vertraging bezorging
+ Attachment URL
+ bijvoorbeeld https://example.com/flowers.jpg
+ Attachment bestandsnaam
+ bijvoorbeeld lilies.jpg
+ Telefoongesprek
+ bijvoorbeel +1234567890
+ Bekijk de documentatie voor voorbeelden en een uitgebreide beschrijving van alle publiceer-functies.
+ Berichtenbalk
+ Publiceer bericht
+ Meer opties
+ Fout bij verbinden
+ Er was een probleem bij het verbinden met %1$s. De app zal blijven proberen in de achtergrond.
+ Verbinding geweigerd. De server is mogelijk niet bereikbaar of het adres is onjuist.
+ WebSocket niet ondersteund. De server ondersteund mogelijk geen WebSochet, of het adres is onjuist.
+ Niet toegestaan. De server gaf een HTTP 401/403 antwoord. Controleer uw gebruikersnaam en/of wachtwoord.
+ Probeer opnieuw
+ Nieuwe poging in %1$d…
+ Opnieuw proberen…
+ Taal
+ Systeemstandaarden worden gebruikt
+ Systeemstandaarden
+ Dynamische kleuren
+ Dynamische systeemkleuren worden gebruikt
+ Ntfy thema kleuren worden gebruikt
+ Toon berichtenbalk
+ Berichtenbalk getoond onderaan onderwerpweergave
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index cae5a5c0..f8bc7a9d 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -341,4 +341,135 @@
Alarmes exatos
O ntfy pode agendar alarmes exatos, eles são usados para reconectar WebSockets em segundo plano. Clique para revogar a permissão.
O ntfy não pode agendar alarmes exatos, eles são usados para reconectar WebSockets em segundo plano. Clique para conceder a permissão.
+ Próximo
+ Adicionar
+ Apagar
+ Restaurar Padrões
+ ex. https://ntfy.example.com
+ Dono
+ Emissor
+ Assinatura SHA-256
+ Válido de
+ Válido até
+ Certificado adicionado
+ Certificado removido
+ Notificação de alerta de conexão
+ Não foi possível conectar à %1$s por mais de %2$d minutos
+ Não foi possível conectar aos servidores %1$d por mais de %2$d minutos
+ Adiar %1$dh
+ Nunca mostrar
+ Erro de conexão
+ Certificado do servidor não confiável
+ Sua busca não retornou resultados
+ Notificações de pesquisa
+ Buscar em notificações
+ Publicar em %1$s
+ Título
+ ex. Tem alguém na porta
+ Mensagem
+ Tags
+ ex. Alerta, skull
+ Prioridade
+ Publicar
+ Não foi possível publicar a mensagem: %1$s
+ Não foi possível publicar a mensagem: %1$s (código %2$d)
+ Mensagem publicada
+ Subindo: %1$s (%2$s / %3$s)
+ Upload cancelado
+ Título
+ Etiquetas
+ Prioridade
+ Click URL
+ Email
+ Atraso
+ Markdown
+ Anexar com URL
+ Anexar arquivo local
+ Chamada telefônica
+ Click URL
+ ex. https://example.com/alerts/1234
+ Email
+ ex. phil@example.com
+ Atraso na entrega
+ URL do anexo
+ ex. https://example.com/flowers.jpg
+ Nome de arquivo do anexo
+ ex. lilies.jpg
+ Chamada telefônica
+ ex. +1234567890
+ Para exemplos e uma descrição detalhada de todos os recursos de publicação, por favor consulte documentation.
+ Barra de mensagens
+ Publicar mensagem
+ Mais opções
+ Confie
+ Certificado cliente
+ Adicionar certificado cliente
+ Insira a URL de serviço para o qual este certificado deve ser usado e a senha para o arquivo PKCS#12.
+ Revisar os detalhes do certificado e salvar para adicionar este certificado de cliente. Este certificado será usado para autenticar com o servidor.
+ Senha
+ Senha incorreta ou arquivo PKCS#12 inválido
+ Senha inválida ou arquivo PKCS#12 corrompido
+ URL de serviço inválida
+ ex: 30m, 1h, today 9pm (expressões de tempo apenas em inglês)
+ Publicar notificação
+ Falha na conexão
+ Houve um problema ao conectar em %1$s. O app vai continuar tentando reconectar em segundo plano.
+ Conexão recusada. O servidor pode estar desligado ou o endereço pode estar incorreto.
+ WebSocket não suportado. O servidor pode não suportar conexões WebSocket ou o endereço pode estar incorreto.
+ Não autorizado. O servidor retornou uma resposta HTTP 401/403. Favor verificar se seu usuário e senha estão corretos.
+ Tentar de novo agora
+ Tentando de novo em %1$d…
+ Tentando de novo…
+ Idioma
+ Usando padrão do sistema
+ Padrão do sistema
+ Cores dinâmicas
+ Usando as cores dinâmicas do sistema
+ Usando as cores do tema ntfy
+ Mostrar barra de mensagens
+ Barra de mensagens mostrada abaixo da visualização de tópico
+ Botão publicar mostrado abaixo da visualização de tópico
+ Cabeçalhos customizados
+ Definir cabeçalhos HTTP personalizados que serão enviados junto com cada requisição, por ex. se seu servidor ntfy está atrás de um túnel ou proxy autenticado.
+ Adicionar cabeçalho
+ Adicionar um cabeçalho para um servidor
+ Cabeçalhos são incluídos em cada requisição HTTP. Cada servidor ntfy pode ter seu próprio conjunto de cabeçalhos personalizados.
+ Gerenciar certificados
+ Adicionar certificados para a trust store e gerenciar certificados de cliente para mTLS
+ Certificados confiáveis
+ Certificado fixado, emitido por %1$s,expira em %2$s, usado para conexões em %3$s
+ Certificado fixado, emitido por %1$s, expiradom usado para conexões em %2$s
+ Adicionar um certificado confiável
+ Importar um certificado para a trust store (PEM). Ao conectar ao servidor ntfy, este certificado será considerado confiável.
+ Certificado de cliente (mTLS)
+ Certificado de cliente, emitido por %1$s, expira em %2$s, usado para conexões com %3$s
+ Certificado de cliente, emitido por %1$s, expirado, usado para conexões com%2$s
+ Adicionar um certificado de cliente
+ Importar certificado para autenticação TLS mútua (PKCS#12). Este certificado será usado ao conectar com o servidor.
+ Arquivo de certificado inválido
+ Arquivo PKCS#12 inválido
+ Não é possível adicionar usuário se o cabeçalho de autorização personalizada está configurado para este servidor
+ Insira uma URL de serviço válida, por ex. https://ntfy.example.com
+ Adicionar cabeçalho personalizado
+ Editar cabeçalho personalizado
+ Nome do cabeçalho (ex. CF-Access-Client-Id)
+ Valor do cabeçalho (ex. 9f3c2e4a1b2d4e)
+ Adicionar um cabeçalho HTTP personalizado que será enviado com cada requisição para o servidor especificado.
+ Você pode editar o nome/valor do cabeçalho para o cabeçalho selecionado, ou apaga-lo.
+ O nome do cabeçalho contém caracteres inválidos
+ Este cabeçalho está reservado e configurado pelo ntfy
+ Não é possível adicionar um cabeçalho de autorização se um usuário está configurado para este servidor
+ Um cabeçalho com este nome já existe para este servidor
+ Detalhes do certificado
+ Alerta de segurança
+ Adicionar certificado confiável
+ Sua conexão não é privada
+ O certificado do servidor não é confiável. Atacantes podem estar tentando roubar suas informações. Não prossiga a menos que você saiba porque este certificado não é confiável.
+ Você selecionou um arquivo de certificado. Revise os detalhes abaixo antes de adiciona-lo aos seus certificados confiáveis.
+ Este certificado é usado para conexões à URL de serviço abaixo. Você adicionou esta exceção manualmente.
+ Insira a URL de serviço para o qual este certificado deveria ser fixado. O certificado será considerado confiável apenas para esta URL.
+ Alerta: Este certificado expirou.
+ Alerta: Este certificado ainda não é válido.
+ URL inválida
+ Não foi possível carregar certificado: %1$s
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index 64dad04a..b3b6fb93 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -345,4 +345,26 @@
Alarmes exactos
O ntfy pode agendar alarmes exatos. Alarmes exatos são necessários para reconectar os WebSockets em segundo plano. Clique para revogar a permissão.
O ntfy não pode agendar alarmes exatos. Alarmes exatos são necessários para reconectar os WebSockets em segundo plano. Clique para conceder a permissão.
+ Próximo
+ Adicionar
+ Apagar
+ Restaurar valores padrão
+ ex. https://ntfy.example.com
+ Assunto
+ Emissor
+ Impressão digital SHA-256
+ Válido desde
+ Válido até
+ Certificado adicionado
+ Certificado apagado
+ Conexão perdida
+ ex. Alguém está à porta
+ Mensagem
+ Etiquetas
+ ex. aviso, perigo
+ Prioridade
+ Publicar
+ Não foi possível publicar a mensagem: %1$s
+ Não foi possível publicar a mensagem: %1$s (código %2$d)
+ Mensagem publicada
diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml
index ec8b43f6..7b23b0bd 100644
--- a/app/src/main/res/values-ro/strings.xml
+++ b/app/src/main/res/values-ro/strings.xml
@@ -343,4 +343,7 @@
Alarme exacte
ntfy poate programa alarme exacte. Alarmele exacte sunt necesare pentru a reconecta WebSocket-urile în fundal. Faceți clic pentru a revoca permisiunea.
ntfy nu poate programa alarme exacte. Alarmele exacte sunt necesare pentru a reconecta WebSockets în fundal. Faceți clic pentru a acorda permisiunea.
+ Următorul
+ Adaugă
+ Şterge
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index c1d90d31..166976ae 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -471,4 +471,9 @@
在通知中搜索
重置为默认
输入有效的服务 URL,如 https://ntfy.example.com
+ 连接丢失
+ 已无法连接到 %1$s 超过 %2$d 分钟
+ 已无法连接到 %1$d 台服务器超过 %2$d 分钟
+ 延后 %1$d 小时
+ 永不显示
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index b272e0b7..a5553469 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -471,4 +471,9 @@
通知中的搜尋
重設為預設值
請輸入有效的服務 URL,例如:https://ntfy.example.com
+ 連線已中斷
+ 超過 %2$d 分鐘無法連線至 %1$s
+ 超過 %2$d 分鐘無法連線至 %1$d 個伺服器
+ 延後 %1$d 小時
+ 永不顯示
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 986a33e4..5979fb69 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -28,7 +28,8 @@
Default
- Subscription Service
+ Background service
+ Connection alerts
Listening for incoming notifications
Subscribed to instant delivery topics
Subscribed to one instant delivery topic
@@ -49,10 +50,10 @@
Connection lost
- Unable to connect to %1$s for more than %2$d minutes
- Unable to connect to %1$d servers for more than %2$d minutes
- Dismiss
- Snooze 1h
+ Unable to connect to %1$s for more than %2$d minutes. Check your network connection.
+ Unable to connect to %1$d servers for more than %2$d minutes. Check your network connection.
+
+ Snooze %1$dh
Never show
@@ -115,6 +116,7 @@
Ask later
Dismiss
Grant now
+ You\'re offline
Subscribe to topic
@@ -415,6 +417,19 @@
Restore successful
Restore failed: %1$s
Advanced
+ Alert when disconnected
+ Never notify when the ntfy server cannot be reached
+ Notify if the ntfy server cannot be reached for more than 5 minutes
+ Notify if the ntfy server cannot be reached for more than 15 minutes
+ Notify if the ntfy server cannot be reached for more than 1 hour
+ Notify if the ntfy server cannot be reached for more than 3 hours
+ Notify if the ntfy server cannot be reached for more than 12 hours
+ Never
+ After 5 minutes
+ After 15 minutes
+ After 1 hour
+ After 3 hours
+ After 12 hours
Broadcast messages
Apps can receive incoming notifications as broadcasts
Apps cannot receive notifications as broadcasts
diff --git a/app/src/main/res/values/values.xml b/app/src/main/res/values/values.xml
index 0b45baf5..0bb4bca9 100644
--- a/app/src/main/res/values/values.xml
+++ b/app/src/main/res/values/values.xml
@@ -47,6 +47,7 @@
SubscriptionMinPriority
SubscriptionAutoDelete
SubscriptionInsistentMaxPriority
+ ConnectionAlert
SubscriptionAppearance
SubscriptionIconSet
SubscriptionIconRemove
@@ -167,6 +168,22 @@
- 1
- 0
+
+ - @string/settings_advanced_connection_alert_never
+ - @string/settings_advanced_connection_alert_five_minutes
+ - @string/settings_advanced_connection_alert_fifteen_minutes
+ - @string/settings_advanced_connection_alert_one_hour
+ - @string/settings_advanced_connection_alert_three_hours
+ - @string/settings_advanced_connection_alert_twelve_hours
+
+
+ - 0
+ - 300
+ - 900
+ - 3600
+ - 10800
+ - 43200
+
- @string/settings_advanced_connection_protocol_entry_jsonhttp
- @string/settings_advanced_connection_protocol_entry_ws
diff --git a/app/src/main/res/xml/main_preferences.xml b/app/src/main/res/xml/main_preferences.xml
index 1ef47d3e..c738b6c3 100644
--- a/app/src/main/res/xml/main_preferences.xml
+++ b/app/src/main/res/xml/main_preferences.xml
@@ -77,6 +77,12 @@
app:summary="@string/settings_backup_restore_restore_summary"/>
+