Restart service when network returns, even when app was killed
This commit is contained in:
parent
9137d94d46
commit
139f586be6
9 changed files with 57 additions and 10 deletions
|
|
@ -33,8 +33,17 @@ class Application : Application() {
|
|||
|
||||
private fun registerNetworkCallback() {
|
||||
val connectivityManager = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
// If there's already a default network at registration time, registerDefaultNetworkCallback
|
||||
// will synchronously deliver an initial onAvailable for it. That's not a real transition, so
|
||||
// skip the first onAvailable in that case to avoid a spurious reconnect on every cold start.
|
||||
var skipInitialAvailable = connectivityManager.activeNetwork != null
|
||||
connectivityManager.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
if (skipInitialAvailable) {
|
||||
skipInitialAvailable = false
|
||||
Log.d(TAG, "Skipping initial onAvailable for pre-existing default network ($network)")
|
||||
return
|
||||
}
|
||||
// Force reconnect of all WebSocket/JSON connections so they're rebound to the new
|
||||
// default network. This catches Wi-Fi <-> cellular handoffs and similar transitions
|
||||
// where the underlying socket is bound to a network that's no longer the default.
|
||||
|
|
@ -50,6 +59,8 @@ class Application : Application() {
|
|||
}
|
||||
}
|
||||
override fun onLost(network: Network) {
|
||||
// Once we've observed a loss, any subsequent onAvailable is a real transition.
|
||||
skipInitialAvailable = false
|
||||
Log.i(TAG, "Default network lost ($network); refreshing subscriber service")
|
||||
SubscriberServiceManager.refresh(this@Application)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import android.os.Build
|
|||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import android.os.SystemClock
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.NotificationCompat
|
||||
import io.heckel.ntfy.BuildConfig
|
||||
import io.heckel.ntfy.R
|
||||
|
|
@ -358,18 +359,18 @@ class SubscriberService : Service() {
|
|||
getString(R.string.connection_alert_text_multiple, disconnectedUrls.size, thresholdMinutes)
|
||||
}
|
||||
|
||||
val contentIntent = PendingIntent.getActivity(this, 0,
|
||||
val contentIntent = PendingIntent.getActivity(this, REQUEST_CODE_CONTENT,
|
||||
Intent(this, MainActivity::class.java), PendingIntent.FLAG_IMMUTABLE)
|
||||
val snoozeShortIntent = PendingIntent.getBroadcast(this, 0,
|
||||
val snoozeShortIntent = PendingIntent.getBroadcast(this, REQUEST_CODE_SNOOZE_SHORT,
|
||||
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,
|
||||
val snoozeIntent = PendingIntent.getBroadcast(this, REQUEST_CODE_SNOOZE_LONG,
|
||||
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,
|
||||
val neverAlertIntent = PendingIntent.getBroadcast(this, REQUEST_CODE_NEVER,
|
||||
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,
|
||||
val deleteIntent = PendingIntent.getBroadcast(this, REQUEST_CODE_DISMISS,
|
||||
Intent(this, ConnectionAlertBroadcastReceiver::class.java).apply { action = CONNECTION_ALERT_ACTION_DISMISS },
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
|
||||
|
|
@ -505,9 +506,11 @@ class SubscriberService : Service() {
|
|||
when (intent.action) {
|
||||
CONNECTION_ALERT_ACTION_DISMISS, CONNECTION_ALERT_ACTION_SNOOZE_SHORT -> {
|
||||
repository.setConnectionAlertSnoozeUntilTime(System.currentTimeMillis() + CONNECTION_ALERT_SNOOZE_SHORT_MILLIS)
|
||||
Toast.makeText(context, context.getString(R.string.connection_alert_snoozed_toast, CONNECTION_ALERT_SNOOZE_SHORT_HOURS), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
CONNECTION_ALERT_ACTION_SNOOZE_LONG -> {
|
||||
repository.setConnectionAlertSnoozeUntilTime(System.currentTimeMillis() + CONNECTION_ALERT_SNOOZE_LONG_MILLIS)
|
||||
Toast.makeText(context, context.getString(R.string.connection_alert_snoozed_toast, CONNECTION_ALERT_SNOOZE_LONG_HOURS), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
CONNECTION_ALERT_ACTION_NEVER -> {
|
||||
repository.setConnectionAlertSeconds(Repository.CONNECTION_ALERT_NEVER)
|
||||
|
|
@ -547,5 +550,13 @@ class SubscriberService : Service() {
|
|||
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"
|
||||
|
||||
// Unique request codes for connection alert PendingIntents. These must be distinct so
|
||||
// that each PendingIntent is treated as a separate entry by the system.
|
||||
private const val REQUEST_CODE_CONTENT = 0
|
||||
private const val REQUEST_CODE_SNOOZE_SHORT = 1
|
||||
private const val REQUEST_CODE_SNOOZE_LONG = 2
|
||||
private const val REQUEST_CODE_NEVER = 3
|
||||
private const val REQUEST_CODE_DISMISS = 4
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ class SubscriberServiceManager(private val context: Context) {
|
|||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
val app = context.applicationContext as Application
|
||||
val workManager = WorkManager.getInstance(context)
|
||||
val subscriptionIdsWithInstantStatus = app.repository.getSubscriptionIdsWithInstantStatus()
|
||||
val hasNetwork = isNetworkAvailable(context)
|
||||
val instantSubscriptions = subscriptionIdsWithInstantStatus.toList().filter { (_, instant) -> instant }.size
|
||||
|
|
@ -51,6 +52,9 @@ class SubscriberServiceManager(private val context: Context) {
|
|||
it.action = SubscriberService.Action.START.name
|
||||
try {
|
||||
ContextCompat.startForegroundService(context, it)
|
||||
// Service started successfully: cancel any pending "wait for network"
|
||||
// worker that may have been scheduled during an earlier network outage.
|
||||
workManager.cancelUniqueWork(WORK_NAME_ON_NETWORK_AVAILABLE)
|
||||
} catch (e: Exception) {
|
||||
// ForegroundServiceDidNotStartInTimeException or other exceptions can occur
|
||||
// due to race conditions or system constraints. We log and continue;
|
||||
|
|
@ -66,6 +70,23 @@ class SubscriberServiceManager(private val context: Context) {
|
|||
app.repository.clearConnectionDetails()
|
||||
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationManager.cancel(SubscriberService.NOTIFICATION_CONNECTION_ALERT_ID)
|
||||
// Schedule a one-time, network-constrained worker that restarts the service
|
||||
// as soon as network comes back. This works even if the process has been
|
||||
// killed by the OS while the service was stopped, because WorkManager /
|
||||
// JobScheduler is tied to the platform's connectivity monitor. Without this,
|
||||
// recovery when offline->online happens after process death would only occur
|
||||
// via the periodic NtfyAutoRestartWorkerPeriodic worker (up to ~30 minutes).
|
||||
// Doze mode may still defer the job until the next maintenance window, but
|
||||
// users with instant delivery are prompted to disable battery optimization.
|
||||
if (instantSubscriptions > 0) {
|
||||
val networkConstraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
val onNetworkRequest = OneTimeWorkRequest.Builder(ServiceStartWorker::class.java)
|
||||
.setConstraints(networkConstraints)
|
||||
.build()
|
||||
workManager.enqueueUniqueWork(WORK_NAME_ON_NETWORK_AVAILABLE, ExistingWorkPolicy.REPLACE, onNetworkRequest)
|
||||
}
|
||||
}
|
||||
Intent(context, SubscriberService::class.java).also {
|
||||
context.stopService(it)
|
||||
|
|
@ -79,6 +100,7 @@ class SubscriberServiceManager(private val context: Context) {
|
|||
companion object {
|
||||
const val TAG = "NtfySubscriberMgr"
|
||||
const val WORK_NAME_ONCE = "ServiceStartWorkerOnce"
|
||||
const val WORK_NAME_ON_NETWORK_AVAILABLE = "ServiceStartWorkerOnNetworkAvailable"
|
||||
|
||||
fun refresh(context: Context) {
|
||||
val manager = SubscriberServiceManager(context)
|
||||
|
|
|
|||
|
|
@ -461,7 +461,7 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific
|
|||
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}")
|
||||
Log.w(TAG, "Failed to unregister network callback: ${e.message}", e)
|
||||
}
|
||||
networkCallback = null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -331,7 +331,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
|||
connectionAlert?.value = repository.getConnectionAlertSeconds().toString()
|
||||
connectionAlert?.preferenceDataStore = object : PreferenceDataStore() {
|
||||
override fun putString(key: String?, value: String?) {
|
||||
val seconds = value?.toLongOrNull() ?:return
|
||||
val seconds = value?.toLongOrNull() ?: return
|
||||
repository.setConnectionAlertSeconds(seconds)
|
||||
}
|
||||
override fun getString(key: String?, defValue: String?): String {
|
||||
|
|
@ -339,7 +339,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere
|
|||
}
|
||||
}
|
||||
connectionAlert?.summaryProvider = Preference.SummaryProvider<ListPreference> { pref ->
|
||||
when (pref.value.toLongOrNull() ?: repository.getConnectionAlertSeconds()) {
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -553,4 +553,4 @@ fun deriveNotificationId(baseUrl: String, topic: String, sequenceId: String): In
|
|||
fun isNetworkAvailable(context: Context): Boolean {
|
||||
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
return connectivityManager.activeNetwork != null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@
|
|||
<!-- %1$d is the snooze duration in hours, currently set to 8 -->
|
||||
<string name="connection_alert_action_snooze">Snooze %1$dh</string>
|
||||
<string name="connection_alert_action_never">Never show</string>
|
||||
<!-- %1$d is the snooze duration in hours -->
|
||||
<string name="connection_alert_snoozed_toast">Connection lost alert snoozed for %1$dh</string>
|
||||
|
||||
<!-- Common refresh toasts -->
|
||||
<string name="refresh_message_result">%1$d notification(s) received</string>
|
||||
|
|
|
|||
|
|
@ -2,5 +2,5 @@
|
|||
<paths>
|
||||
<external-path name="external_files" path="."/>
|
||||
<cache-path name="cache_files" path="."/>
|
||||
<files-path name="files" path="."/>
|
||||
<files-path name="subscription_icons" path="subscriptionIcons/"/>
|
||||
</paths>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
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)
|
||||
* Restart the foreground service immediately when network returns, even if the app process was killed while offline
|
||||
* Improve battery life by increasing WebSocket client ping interval from 1 min to 5 min, and reconnect instantly on Wi-Fi/cellular/VPN transitions (ntfy-android#113, thanks to @ftilde for the investigation)
|
||||
* Disable UnifiedPush components when UnifiedPush is disabled in settings (ntfy-android#168, thanks to @p1gp1g for the contribution)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue