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"/> +