Connection lost v2
This commit is contained in:
parent
3b9b4b7fb3
commit
b5f7a634c0
22 changed files with 270 additions and 90 deletions
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
<!-- Permissions -->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <!-- To check network availability before showing connection alerts -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <!-- For instant delivery foregrounds service -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/> <!-- For instant delivery foregrounds service on SDK >= 34 -->
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/> <!-- To keep foreground service awake; soon not needed anymore -->
|
||||
|
|
@ -131,15 +132,14 @@
|
|||
android:enabled="true"
|
||||
android:exported="false"/>
|
||||
|
||||
<!-- Broadcast receiver for connection alert notification actions (dismiss, snooze, never show again) -->
|
||||
<!-- Broadcast receiver for connection alert notification actions (snooze, disable) -->
|
||||
<receiver
|
||||
android:name=".service.SubscriberService$ConnectionAlertBroadcastReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="io.heckel.ntfy.CONNECTION_ALERT_DISMISS"/>
|
||||
<action android:name="io.heckel.ntfy.CONNECTION_ALERT_SNOOZE"/>
|
||||
<action android:name="io.heckel.ntfy.CONNECTION_ALERT_NEVER"/>
|
||||
<action android:name="io.heckel.ntfy.CONNECTION_ALERT_DISABLE"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String>?,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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<String>) {
|
||||
private fun showConnectionAlertNotification(disconnectedUrls: Set<String>, 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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<TextView>(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<View>(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) {
|
||||
|
|
|
|||
|
|
@ -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<ListPreference> { 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)
|
||||
|
|
|
|||
|
|
@ -232,6 +232,22 @@
|
|||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/main_banner_no_network"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/main_banner_no_network_text"
|
||||
android:textAlignment="center"
|
||||
android:paddingTop="6dp"
|
||||
android:paddingBottom="6dp"
|
||||
android:background="?attr/colorSurfaceVariant"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/main_banner_websocket_reconnect" />
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/main_subscriptions_list_container"
|
||||
android:layout_width="match_parent"
|
||||
|
|
@ -239,7 +255,7 @@
|
|||
android:visibility="visible"
|
||||
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/main_banner_websocket_reconnect">
|
||||
app:layout_constraintTop_toBottomOf="@id/main_banner_no_network">
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/main_subscriptions_list"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
|||
|
|
@ -474,7 +474,5 @@
|
|||
<string name="connection_alert_title">Връзката е загубена</string>
|
||||
<string name="connection_alert_text_one">От най-малко %2$d минути няма връзка с %1$s</string>
|
||||
<string name="connection_alert_text_multiple">От най-малко %2$d минути няма връзка със сървърите на %1$d</string>
|
||||
<string name="connection_alert_action_dismiss">Отхвърляне</string>
|
||||
<string name="connection_alert_action_snooze">Отлагане с 1ч</string>
|
||||
<string name="connection_alert_action_never">Да не се показва повече</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -474,7 +474,5 @@
|
|||
<string name="connection_alert_title">Verbindung verloren</string>
|
||||
<string name="connection_alert_text_one">Es konnte länger als %2$d Minuten lang keine Verbindung zu %1$s hergestellt werden</string>
|
||||
<string name="connection_alert_text_multiple">Es konnte länger als %2$d Minuten lang keine Verbindung zu %1$d Servern hergestellt werden</string>
|
||||
<string name="connection_alert_action_dismiss">Verwerfen</string>
|
||||
<string name="connection_alert_action_snooze">1 Stunde schlummern</string>
|
||||
<string name="connection_alert_action_never">Niemals zeigen</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -413,9 +413,7 @@
|
|||
<string name="connection_alert_title">Conexión perdida</string>
|
||||
<string name="connection_alert_text_one">No se pudo conectar a %1$s por más de %2$d minutos</string>
|
||||
<string name="connection_alert_text_multiple">No se pudo conectar a %1$d servidores por más de %2$d minutos</string>
|
||||
<string name="connection_alert_action_dismiss">Ignorar</string>
|
||||
<string name="connection_alert_action_snooze">Suspender 1h</string>
|
||||
<string name="connection_alert_action_never">Nunca mostrar</string>
|
||||
|
||||
<string name="detail_no_search_results">Su búsqueda no tuvo ningún resultado</string>
|
||||
<string name="detail_menu_search">Buscar notificaciones</string>
|
||||
<string name="detail_menu_search_hint">Buscar en notificaciones</string>
|
||||
|
|
|
|||
|
|
@ -464,7 +464,5 @@
|
|||
<string name="connection_alert_title">Ühendus on katkenud</string>
|
||||
<string name="connection_alert_text_one">Ühendus %1$s teenusega toimib vaid %2$d minuti(t)</string>
|
||||
<string name="connection_alert_text_multiple">Ühendus %1$d serveriga toimib vaid %2$d minuti(t)</string>
|
||||
<string name="connection_alert_action_dismiss">Loobu</string>
|
||||
<string name="connection_alert_action_snooze">Tukasta 1t</string>
|
||||
<string name="connection_alert_action_never">Ära näita iialgi</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -474,7 +474,5 @@
|
|||
<string name="connection_alert_title">Connexion perdue</string>
|
||||
<string name="connection_alert_text_one">Impossible de se connecter à %1$s depuis plus de %2$d minutes</string>
|
||||
<string name="connection_alert_text_multiple">Impossible de se connecter à %1$d serveurs depuis plus de %2$d minutes</string>
|
||||
<string name="connection_alert_action_dismiss">Ignorer</string>
|
||||
<string name="connection_alert_action_snooze">Sourdine 1h</string>
|
||||
<string name="connection_alert_action_never">Ne jamais montrer</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -474,7 +474,4 @@
|
|||
<string name="connection_alert_title">接続が切断されました</string>
|
||||
<string name="connection_alert_text_one">%2$d 分以上 %1$s に接続できませんでした</string>
|
||||
<string name="connection_alert_text_multiple">%1$d 個のサーバーに %2$d 分以上接続できませんでした</string>
|
||||
<string name="connection_alert_action_dismiss">了解</string>
|
||||
<string name="connection_alert_action_snooze">1時間後に再通知</string>
|
||||
<string name="connection_alert_action_never">再表示しない</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -378,9 +378,7 @@
|
|||
<string name="connection_alert_title">Verbinding verbroken</string>
|
||||
<string name="connection_alert_text_one">Kan geen verbinding maken met %1$s voor meer dan %2$d minuten</string>
|
||||
<string name="connection_alert_text_multiple">Kan geen verbinding maken met %1$d servers voor meer dan %2$d minuten</string>
|
||||
<string name="connection_alert_action_dismiss">Afwijzen</string>
|
||||
<string name="connection_alert_action_snooze">Sluimer voor 1 uur</string>
|
||||
<string name="connection_alert_action_never">Nooit laten zien</string>
|
||||
|
||||
<string name="publish_dialog_title">Publieer naar %1$s</string>
|
||||
<string name="publish_dialog_uploading">Uploaden: %1$s (%2$s / %3$s)</string>
|
||||
<string name="publish_dialog_upload_cancelled">Upload geannuleerd</string>
|
||||
|
|
|
|||
|
|
@ -474,7 +474,5 @@
|
|||
<string name="connection_alert_title">连接丢失</string>
|
||||
<string name="connection_alert_text_one">已无法连接到 %1$s 超过 %2$d 分钟</string>
|
||||
<string name="connection_alert_text_multiple">已无法连接到 %1$d 台服务器超过 %2$d 分钟</string>
|
||||
<string name="connection_alert_action_dismiss">关闭</string>
|
||||
<string name="connection_alert_action_snooze">延后 1 小时</string>
|
||||
<string name="connection_alert_action_never">永不显示</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -474,7 +474,5 @@
|
|||
<string name="connection_alert_title">連線已中斷</string>
|
||||
<string name="connection_alert_text_one">超過 %2$d 分鐘無法連線至 %1$s</string>
|
||||
<string name="connection_alert_text_multiple">超過 %2$d 分鐘無法連線至 %1$d 個伺服器</string>
|
||||
<string name="connection_alert_action_dismiss">關閉</string>
|
||||
<string name="connection_alert_action_snooze">延後 1 小時</string>
|
||||
<string name="connection_alert_action_never">永不顯示</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
<!-- Notification channels -->
|
||||
<string name="channel_notifications_group_default_name">Default</string>
|
||||
<string name="channel_subscriber_service_name">Subscription Service</string>
|
||||
<string name="channel_connection_alert_name">Connection alerts</string>
|
||||
<string name="channel_subscriber_notification_title">Listening for incoming notifications</string>
|
||||
<string name="channel_subscriber_notification_instant_text">Subscribed to instant delivery topics</string>
|
||||
<string name="channel_subscriber_notification_instant_text_one">Subscribed to one instant delivery topic</string>
|
||||
|
|
@ -49,11 +50,10 @@
|
|||
|
||||
<!-- Connection alert notification -->
|
||||
<string name="connection_alert_title">Connection lost</string>
|
||||
<string name="connection_alert_text_one">Unable to connect to %1$s for more than %2$d minutes</string>
|
||||
<string name="connection_alert_text_multiple">Unable to connect to %1$d servers for more than %2$d minutes</string>
|
||||
<string name="connection_alert_action_dismiss">Dismiss</string>
|
||||
<string name="connection_alert_action_snooze">Snooze 1h</string>
|
||||
<string name="connection_alert_action_never">Never show</string>
|
||||
<string name="connection_alert_text_one">Unable to connect to %1$s for more than %2$d minutes. Check your network connection.</string>
|
||||
<string name="connection_alert_text_multiple">Unable to connect to %1$d servers for more than %2$d minutes. Check your network connection.</string>
|
||||
<string name="connection_alert_action_snooze">Snooze %1$dh</string>
|
||||
<string name="connection_alert_action_disable">Disable alerts</string>
|
||||
|
||||
<!-- Common refresh toasts -->
|
||||
<string name="refresh_message_result">%1$d notification(s) received</string>
|
||||
|
|
@ -115,6 +115,7 @@
|
|||
<string name="main_banner_websocket_reconnect_button_remind_later">Ask later</string>
|
||||
<string name="main_banner_websocket_reconnect_button_dismiss">Dismiss</string>
|
||||
<string name="main_banner_websocket_reconnect_button_enable_now">Grant now</string>
|
||||
<string name="main_banner_no_network_text">No network</string>
|
||||
|
||||
<!-- Add dialog -->
|
||||
<string name="add_dialog_title">Subscribe to topic</string>
|
||||
|
|
@ -370,6 +371,19 @@
|
|||
<string name="settings_notifications_auto_delete_one_week">After one week</string>
|
||||
<string name="settings_notifications_auto_delete_one_month">After one month</string>
|
||||
<string name="settings_notifications_auto_delete_three_months">After 3 months</string>
|
||||
<string name="settings_notifications_connection_alert_title">Alert when connection is lost</string>
|
||||
<string name="settings_notifications_connection_alert_summary_never">Never alert when connection is lost</string>
|
||||
<string name="settings_notifications_connection_alert_summary_five_minutes">Alert after 5 minutes without connection</string>
|
||||
<string name="settings_notifications_connection_alert_summary_fifteen_minutes">Alert after 15 minutes without connection</string>
|
||||
<string name="settings_notifications_connection_alert_summary_one_hour">Alert after 1 hour without connection</string>
|
||||
<string name="settings_notifications_connection_alert_summary_three_hours">Alert after 3 hours without connection</string>
|
||||
<string name="settings_notifications_connection_alert_summary_twelve_hours">Alert after 12 hours without connection</string>
|
||||
<string name="settings_notifications_connection_alert_never">Never</string>
|
||||
<string name="settings_notifications_connection_alert_five_minutes">After 5 minutes</string>
|
||||
<string name="settings_notifications_connection_alert_fifteen_minutes">After 15 minutes</string>
|
||||
<string name="settings_notifications_connection_alert_one_hour">After 1 hour</string>
|
||||
<string name="settings_notifications_connection_alert_three_hours">After 3 hours</string>
|
||||
<string name="settings_notifications_connection_alert_twelve_hours">After 12 hours</string>
|
||||
<string name="settings_notifications_insistent_max_priority_title">Keep alerting for highest priority</string>
|
||||
<string name="settings_notifications_insistent_max_priority_summary_enabled">Max priority notifications continuously alert until dismissed</string>
|
||||
<string name="settings_notifications_insistent_max_priority_summary_disabled">Max priority notifications only alert once</string>
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@
|
|||
<string name="detail_settings_notifications_min_priority_key" translatable="false">SubscriptionMinPriority</string>
|
||||
<string name="detail_settings_notifications_auto_delete_key" translatable="false">SubscriptionAutoDelete</string>
|
||||
<string name="detail_settings_notifications_insistent_max_priority_key" translatable="false">SubscriptionInsistentMaxPriority</string>
|
||||
<string name="settings_notifications_connection_alert_key" translatable="false">ConnectionAlert</string>
|
||||
<string name="detail_settings_appearance_header_key" translatable="false">SubscriptionAppearance</string>
|
||||
<string name="detail_settings_appearance_icon_set_key" translatable="false">SubscriptionIconSet</string>
|
||||
<string name="detail_settings_appearance_icon_remove_key" translatable="false">SubscriptionIconRemove</string>
|
||||
|
|
@ -167,6 +168,24 @@
|
|||
<item>1</item>
|
||||
<item>0</item>
|
||||
</string-array>
|
||||
<string-array name="settings_notifications_connection_alert_entries">
|
||||
<item>@string/settings_notifications_connection_alert_never</item>
|
||||
<item>After 30 seconds (testing)</item>
|
||||
<item>@string/settings_notifications_connection_alert_five_minutes</item>
|
||||
<item>@string/settings_notifications_connection_alert_fifteen_minutes</item>
|
||||
<item>@string/settings_notifications_connection_alert_one_hour</item>
|
||||
<item>@string/settings_notifications_connection_alert_three_hours</item>
|
||||
<item>@string/settings_notifications_connection_alert_twelve_hours</item>
|
||||
</string-array>
|
||||
<string-array name="settings_notifications_connection_alert_values">
|
||||
<item>0</item>
|
||||
<item>30</item>
|
||||
<item>300</item>
|
||||
<item>900</item>
|
||||
<item>3600</item>
|
||||
<item>10800</item>
|
||||
<item>43200</item>
|
||||
</string-array>
|
||||
<string-array name="settings_advanced_connection_protocol_entries">
|
||||
<item>@string/settings_advanced_connection_protocol_entry_jsonhttp</item>
|
||||
<item>@string/settings_advanced_connection_protocol_entry_ws</item>
|
||||
|
|
|
|||
|
|
@ -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"/>
|
||||
<ListPreference
|
||||
app:key="@string/settings_notifications_connection_alert_key"
|
||||
app:title="@string/settings_notifications_connection_alert_title"
|
||||
app:entries="@array/settings_notifications_connection_alert_entries"
|
||||
app:entryValues="@array/settings_notifications_connection_alert_values"
|
||||
app:defaultValue="0"/>
|
||||
<Preference
|
||||
app:key="@string/settings_notifications_channel_prefs_key"
|
||||
app:title="@string/settings_notifications_channel_prefs_title"
|
||||
|
|
|
|||
3
fastlane/metadata/android/en-US/changelog/NEXT.txt
Normal file
3
fastlane/metadata/android/en-US/changelog/NEXT.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
Features:
|
||||
* Add configurable "Alert when connection is lost" setting (#1665, #1662, #1652, #1655, thanks to @tintamarre, @sjozs, @TheRealOne78, and @DAE51D for reporting)
|
||||
* Suppress connection alerts and stop foreground service when there is no network (ntfy-android#165, thanks to @tintamarre for the contribution)
|
||||
Loading…
Add table
Reference in a new issue