From b5f7a634c01201c72ee20d28f78568a20d4e0210 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Wed, 1 Apr 2026 20:44:23 -0400 Subject: [PATCH] Connection lost v2 --- app/src/main/AndroidManifest.xml | 6 +- .../java/io/heckel/ntfy/app/Application.kt | 21 ++++ .../java/io/heckel/ntfy/backup/Backuper.kt | 5 + .../main/java/io/heckel/ntfy/db/Repository.kt | 28 ++++- .../heckel/ntfy/service/SubscriberService.kt | 101 +++++++++--------- .../ntfy/service/SubscriberServiceManager.kt | 21 +++- .../java/io/heckel/ntfy/ui/MainActivity.kt | 47 ++++++++ .../io/heckel/ntfy/ui/SettingsActivity.kt | 26 +++++ app/src/main/res/layout/activity_main.xml | 18 +++- app/src/main/res/values-bg/strings.xml | 4 +- app/src/main/res/values-de/strings.xml | 4 +- app/src/main/res/values-es/strings.xml | 4 +- app/src/main/res/values-et/strings.xml | 4 +- app/src/main/res/values-fr/strings.xml | 4 +- app/src/main/res/values-ja/strings.xml | 3 - app/src/main/res/values-nl/strings.xml | 4 +- app/src/main/res/values-zh-rCN/strings.xml | 4 +- app/src/main/res/values-zh-rTW/strings.xml | 4 +- app/src/main/res/values/strings.xml | 24 ++++- app/src/main/res/values/values.xml | 19 ++++ app/src/main/res/xml/main_preferences.xml | 6 ++ .../metadata/android/en-US/changelog/NEXT.txt | 3 + 22 files changed, 270 insertions(+), 90 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelog/NEXT.txt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6ca9d3c0..f2df392b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ + @@ -131,15 +132,14 @@ 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..bc06f046 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,13 @@ package io.heckel.ntfy.app import android.app.Application +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest 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 +29,21 @@ class Application : Application() { if (repository.getDynamicColorsEnabled()) { DynamicColors.applyToActivitiesIfAvailable(this) } + registerNetworkCallback() + } + + private fun registerNetworkCallback() { + val connectivityManager = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager + val networkRequest = NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .build() + connectivityManager.registerNetworkCallback(networkRequest, 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..fc3dd561 100644 --- a/app/src/main/java/io/heckel/ntfy/db/Repository.kt +++ b/app/src/main/java/io/heckel/ntfy/db/Repository.kt @@ -440,8 +440,18 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database) } } + fun getConnectionAlertSeconds(): Long { + return sharedPrefs.getLong(SHARED_PREFS_CONNECTION_ALERT_SECONDS, CONNECTION_ALERT_DEFAULT) + } + + fun setConnectionAlertSeconds(seconds: Long) { + sharedPrefs.edit { + putLong(SHARED_PREFS_CONNECTION_ALERT_SECONDS, seconds) + } + } + fun getConnectionAlertSnoozeUntil(): Long { - return sharedPrefs.getLong(SHARED_PREFS_CONNECTION_ALERT_SNOOZE_UNTIL, CONNECTION_ALERT_SNOOZE_UNTIL_DEFAULT) + return sharedPrefs.getLong(SHARED_PREFS_CONNECTION_ALERT_SNOOZE_UNTIL, 0L) } fun setConnectionAlertSnoozeUntil(timeMillis: Long) { @@ -608,6 +618,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 } @@ -639,9 +654,8 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database) const val SHARED_PREFS_WEBSOCKET_RECONNECT_REMIND_TIME = "WebSocketReconnectRemindTime" 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_SECONDS = "ConnectionAlertSeconds" 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_LAST_TOPICS = "LastTopics" private const val LAST_TOPICS_COUNT = 3 @@ -671,6 +685,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 = 5 * 60L + const val CONNECTION_ALERT_FIFTEEN_MINUTES = 15 * 60L + const val CONNECTION_ALERT_ONE_HOUR = 60 * 60L + const val CONNECTION_ALERT_THREE_HOURS = 3 * 60 * 60L + const val CONNECTION_ALERT_TWELVE_HOURS = 12 * 60 * 60L + 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..189537a0 100644 --- a/app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt +++ b/app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt @@ -11,6 +11,8 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.pm.ServiceInfo +import android.net.ConnectivityManager +import android.net.NetworkCapabilities import android.os.Build import android.os.IBinder import android.os.PowerManager @@ -24,8 +26,6 @@ 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 @@ -316,25 +316,40 @@ class SubscriberService : Service() { } private fun maybeShowConnectionAlert() { + val thresholdSeconds = repository.getConnectionAlertSeconds() + if (thresholdSeconds <= 0L) return + + // Don't show alert if the device has no network connectivity (e.g. airplane mode) + if (!isNetworkAvailable()) return + val now = System.currentTimeMillis() - // Check snooze / never-show-again + // Check snooze val snoozeUntil = repository.getConnectionAlertSnoozeUntil() - if (snoozeUntil == Repository.CONNECTION_ALERT_NEVER_SHOW) return 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) } } + private fun isNetworkAvailable(): Boolean { + val connectivityManager = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager + val network = connectivityManager.activeNetwork ?: return false + val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false + return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + } + private fun maybeAutoDismissConnectionAlert() { val allDetails = repository.getConnectionDetails() val anyStillDisconnected = allDetails.any { (_, details) -> @@ -345,36 +360,28 @@ 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, 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 channelId = NotificationService(this).toChannelId(NotificationService.DEFAULT_GROUP, PRIORITY_HIGH) - val notification = NotificationCompat.Builder(this, channelId) + val snoozeIntent = PendingIntent.getBroadcast(this, 0, + Intent(this, ConnectionAlertBroadcastReceiver::class.java).apply { action = CONNECTION_ALERT_ACTION_SNOOZE }, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + val disableIntent = PendingIntent.getBroadcast(this, 1, + Intent(this, ConnectionAlertBroadcastReceiver::class.java).apply { action = CONNECTION_ALERT_ACTION_DISABLE }, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + + val deleteIntent = PendingIntent.getBroadcast(this, 2, + Intent(this, ConnectionAlertBroadcastReceiver::class.java).apply { action = CONNECTION_ALERT_ACTION_SNOOZE }, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + + 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)) @@ -383,9 +390,9 @@ class SubscriberService : Service() { .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_HOURS), snoozeIntent).build()) + .addAction(NotificationCompat.Action.Builder(0, getString(R.string.connection_alert_action_disable), disableIntent).build()) .build() Log.d(TAG, "Showing connection alert notification") @@ -440,6 +447,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 +510,16 @@ 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_SNOOZE -> { - repository.setConnectionAlertSnoozeUntil( - System.currentTimeMillis() + CONNECTION_ALERT_SNOOZE_DURATION_MILLIS - ) - notificationManager.cancel(NOTIFICATION_CONNECTION_ALERT_ID) + repository.setConnectionAlertSnoozeUntil(System.currentTimeMillis() + CONNECTION_ALERT_SNOOZE_DURATION_MILLIS) } - CONNECTION_ALERT_ACTION_NEVER -> { - repository.setConnectionAlertSnoozeUntil(Repository.CONNECTION_ALERT_NEVER_SHOW) - notificationManager.cancel(NOTIFICATION_CONNECTION_ALERT_ID) + CONNECTION_ALERT_ACTION_DISABLE -> { + repository.setConnectionAlertSeconds(Repository.CONNECTION_ALERT_NEVER) } + else -> return } + notificationManager.cancel(NOTIFICATION_CONNECTION_ALERT_ID) } } @@ -531,16 +535,15 @@ class SubscriberService : Service() { 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*/ - private const val CONNECTION_ALERT_ACTION_DISMISS = "io.heckel.ntfy.CONNECTION_ALERT_DISMISS" + private const val CONNECTION_ALERT_SNOOZE_HOURS = 8 + private const val CONNECTION_ALERT_SNOOZE_DURATION_MILLIS = CONNECTION_ALERT_SNOOZE_HOURS * 60 * 60 * 1000L private const val CONNECTION_ALERT_ACTION_SNOOZE = "io.heckel.ntfy.CONNECTION_ALERT_SNOOZE" - private const val CONNECTION_ALERT_ACTION_NEVER = "io.heckel.ntfy.CONNECTION_ALERT_NEVER" + private const val CONNECTION_ALERT_ACTION_DISABLE = "io.heckel.ntfy.CONNECTION_ALERT_DISABLE" } } 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..8109e66d 100644 --- a/app/src/main/java/io/heckel/ntfy/service/SubscriberServiceManager.kt +++ b/app/src/main/java/io/heckel/ntfy/service/SubscriberServiceManager.kt @@ -1,7 +1,10 @@ package io.heckel.ntfy.service +import android.app.NotificationManager import android.content.Context import android.content.Intent +import android.net.ConnectivityManager +import android.net.NetworkCapabilities import androidx.core.content.ContextCompat import androidx.work.* import io.heckel.ntfy.app.Application @@ -40,8 +43,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 +60,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(NOTIFICATION_CONNECTION_ALERT_ID) + } Intent(context, SubscriberService::class.java).also { context.stopService(it) } @@ -71,6 +80,14 @@ class SubscriberServiceManager(private val context: Context) { companion object { const val TAG = "NtfySubscriberMgr" const val WORK_NAME_ONCE = "ServiceStartWorkerOnce" + private const val NOTIFICATION_CONNECTION_ALERT_ID = 2587 // Same as SubscriberService + + private fun isNetworkAvailable(context: Context): Boolean { + val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val network = connectivityManager.activeNetwork ?: return false + val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false + return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + } fun refresh(context: Context) { val manager = SubscriberServiceManager(context) 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..2ce74222 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,13 @@ 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.net.NetworkCapabilities +import android.net.NetworkRequest import android.os.Build import android.os.Bundle import android.provider.Settings @@ -95,6 +100,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 +348,22 @@ 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() } + } + } + val networkRequest = NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .build() + connectivityManager.registerNetworkCallback(networkRequest, 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 +399,7 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific super.onResume() showHideNotificationMenuItems() showHideConnectionErrorMenuItem(repository.getConnectionDetails()) + showHideNoNetworkBanner() redrawList() } @@ -425,6 +448,30 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific } } + private fun showHideNoNetworkBanner() { + val connectivityManager = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager + val network = connectivityManager.activeNetwork + val hasNetwork = if (network != null) { + val capabilities = connectivityManager.getNetworkCapabilities(network) + capabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) == true + } else { + false + } + val banner = findViewById(R.id.main_banner_no_network) + banner.visibility = if (hasNetwork) 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..ba8c9dc1 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt @@ -327,6 +327,32 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere } } + // Connection alert + val connectionAlertPrefId = context?.getString(R.string.settings_notifications_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_notifications_connection_alert_summary_never) + 30L -> "Alert after 30 seconds (testing)" + Repository.CONNECTION_ALERT_FIVE_MINUTES -> getString(R.string.settings_notifications_connection_alert_summary_five_minutes) + Repository.CONNECTION_ALERT_FIFTEEN_MINUTES -> getString(R.string.settings_notifications_connection_alert_summary_fifteen_minutes) + Repository.CONNECTION_ALERT_ONE_HOUR -> getString(R.string.settings_notifications_connection_alert_summary_one_hour) + Repository.CONNECTION_ALERT_THREE_HOURS -> getString(R.string.settings_notifications_connection_alert_summary_three_hours) + Repository.CONNECTION_ALERT_TWELVE_HOURS -> getString(R.string.settings_notifications_connection_alert_summary_twelve_hours) + else -> getString(R.string.settings_notifications_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/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 1bf20371..282e2c7a 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -232,6 +232,22 @@ + + + app:layout_constraintTop_toBottomOf="@id/main_banner_no_network"> Връзката е загубена От най-малко %2$d минути няма връзка с %1$s От най-малко %2$d минути няма връзка със сървърите на %1$d - Отхвърляне - Отлагане с 1ч - Да не се показва повече + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 41d8ddb6..dddbc759 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -474,7 +474,5 @@ 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 - Verwerfen - 1 Stunde schlummern - Niemals zeigen + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 2151e87c..129c9a79 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -413,9 +413,7 @@ 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 - Ignorar - Suspender 1h - Nunca mostrar + Su búsqueda no tuvo ningún resultado Buscar notificaciones Buscar en notificaciones diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 0d9b6459..c18876c5 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -464,7 +464,5 @@ Ühendus on katkenud Ühendus %1$s teenusega toimib vaid %2$d minuti(t) Ühendus %1$d serveriga toimib vaid %2$d minuti(t) - Loobu - Tukasta 1t - Ä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 c4b04a7e..22a1fda1 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -474,7 +474,5 @@ 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 - Ignorer - Sourdine 1h - Ne jamais montrer + diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index aea03527..d0ecf643 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -474,7 +474,4 @@ 接続が切断されました %2$d 分以上 %1$s に接続できませんでした %1$d 個のサーバーに %2$d 分以上接続できませんでした - 了解 - 1時間後に再通知 - 再表示しない diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index a566e76d..2ee259f6 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -378,9 +378,7 @@ 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 - Afwijzen - Sluimer voor 1 uur - Nooit laten zien + Publieer naar %1$s Uploaden: %1$s (%2$s / %3$s) Upload geannuleerd diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index bf4ef4a6..00489ea8 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -474,7 +474,5 @@ 连接丢失 已无法连接到 %1$s 超过 %2$d 分钟 已无法连接到 %1$d 台服务器超过 %2$d 分钟 - 关闭 - 延后 1 小时 - 永不显示 + diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 77191ab5..920d6956 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -474,7 +474,5 @@ 連線已中斷 超過 %2$d 分鐘無法連線至 %1$s 超過 %2$d 分鐘無法連線至 %1$d 個伺服器 - 關閉 - 延後 1 小時 - 永不顯示 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 986a33e4..f2eb6bce 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -29,6 +29,7 @@ Default Subscription Service + Connection alerts Listening for incoming notifications Subscribed to instant delivery topics Subscribed to one instant delivery topic @@ -49,11 +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 - Never show + 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 + Disable alerts %1$d notification(s) received @@ -115,6 +115,7 @@ Ask later Dismiss Grant now + No network Subscribe to topic @@ -370,6 +371,19 @@ After one week After one month After 3 months + Alert when connection is lost + Never alert when connection is lost + Alert after 5 minutes without connection + Alert after 15 minutes without connection + Alert after 1 hour without connection + Alert after 3 hours without connection + Alert after 12 hours without connection + Never + After 5 minutes + After 15 minutes + After 1 hour + After 3 hours + After 12 hours Keep alerting for highest priority Max priority notifications continuously alert until dismissed Max priority notifications only alert once diff --git a/app/src/main/res/values/values.xml b/app/src/main/res/values/values.xml index 0b45baf5..bc1e156e 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,24 @@ 1 0 + + @string/settings_notifications_connection_alert_never + After 30 seconds (testing) + @string/settings_notifications_connection_alert_five_minutes + @string/settings_notifications_connection_alert_fifteen_minutes + @string/settings_notifications_connection_alert_one_hour + @string/settings_notifications_connection_alert_three_hours + @string/settings_notifications_connection_alert_twelve_hours + + + 0 + 30 + 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..dd0dd085 100644 --- a/app/src/main/res/xml/main_preferences.xml +++ b/app/src/main/res/xml/main_preferences.xml @@ -29,6 +29,12 @@ app:key="@string/settings_notifications_insistent_max_priority_key" app:title="@string/settings_notifications_insistent_max_priority_title" app:defaultValue="false"/> +