From d064e751c4f4697be93604d7dcc361533606effe Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Sat, 3 Jan 2026 12:45:50 -0500 Subject: [PATCH] Remove foreground service restart() function, in an attempt to avoid crashes --- .../java/io/heckel/ntfy/service/Connection.kt | 9 ++++++- .../heckel/ntfy/service/SubscriberService.kt | 24 ++++++++++++++++--- .../ntfy/service/SubscriberServiceManager.kt | 6 ----- .../java/io/heckel/ntfy/ui/MainActivity.kt | 2 +- .../io/heckel/ntfy/ui/SettingsActivity.kt | 16 +++++-------- 5 files changed, 36 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/io/heckel/ntfy/service/Connection.kt b/app/src/main/java/io/heckel/ntfy/service/Connection.kt index 71a98be4..9994dbe4 100644 --- a/app/src/main/java/io/heckel/ntfy/service/Connection.kt +++ b/app/src/main/java/io/heckel/ntfy/service/Connection.kt @@ -6,7 +6,14 @@ interface Connection { fun since(): String? } +/** + * Represents a unique connection identifier that changes every time a + * connection needs to be re-established. + */ data class ConnectionId( val baseUrl: String, - val topicsToSubscriptionIds: Map + val topicsToSubscriptionIds: Map, + val connectionProtocol: String, + val credentialsHash: Int, // Hash of "username:password" or 0 if no user + val headersHash: Int // Hash of sorted headers or 0 if none ) 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 52fd412f..6ba57470 100644 --- a/app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt +++ b/app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt @@ -206,9 +206,27 @@ class SubscriberService : Service() { val instantSubscriptions = repository.getSubscriptions() .filter { s -> s.instant } val activeConnectionIds = connections.keys().toList().toSet() + val connectionProtocol = repository.getConnectionProtocol() val desiredConnectionIds = instantSubscriptions // Set - .groupBy { s -> ConnectionId(s.baseUrl, emptyMap()) } - .map { entry -> entry.key.copy(topicsToSubscriptionIds = entry.value.associate { s -> s.topic to s.id }) } + .groupBy { s -> s.baseUrl } + .map { (baseUrl, subs) -> + // Create a unique connection ID for each base URL. Each change in the connection ID will + // trigger a new connection, and close existing connections. We want to make sure that when the + // connection protocol (JSON/WS), the user or the custom headers are updated, that we kill existing + // connections and start new ones. + val credentialsHash = repository.getUser(baseUrl)?.let { "${it.username}:${it.password}".hashCode() } ?: 0 + val headersHash = repository.getCustomHeadersForServer(baseUrl) + .sortedBy { "${it.name}:${it.value}" } + .joinToString(",") { "${it.name}:${it.value}" } + .hashCode() + ConnectionId( + baseUrl = baseUrl, + topicsToSubscriptionIds = subs.associate { s -> s.topic to s.id }, + connectionProtocol = connectionProtocol, + credentialsHash = credentialsHash, + headersHash = headersHash + ) + } .toSet() val newConnectionIds = desiredConnectionIds.subtract(activeConnectionIds) val obsoleteConnectionIds = activeConnectionIds.subtract(desiredConnectionIds) @@ -237,7 +255,7 @@ class SubscriberService : Service() { val since = sinceByBaseUrl[connectionId.baseUrl] ?: "none" val serviceActive = { isServiceStarted } val user = repository.getUser(connectionId.baseUrl) - val connection = if (repository.getConnectionProtocol() == Repository.CONNECTION_PROTOCOL_WS) { + val connection = if (connectionId.connectionProtocol == Repository.CONNECTION_PROTOCOL_WS) { val alarmManager = getSystemService(ALARM_SERVICE) as AlarmManager WsConnection(connectionId, repository, user, since, ::onStateChanged, ::onNotificationReceived, alarmManager) } else { 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 c51d3039..71f51b48 100644 --- a/app/src/main/java/io/heckel/ntfy/service/SubscriberServiceManager.kt +++ b/app/src/main/java/io/heckel/ntfy/service/SubscriberServiceManager.kt @@ -26,12 +26,6 @@ class SubscriberServiceManager(private val context: Context) { workManager.enqueueUniqueWork(WORK_NAME_ONCE, ExistingWorkPolicy.KEEP, startServiceRequest) // Unique avoids races! } - fun restart() { - Intent(context, SubscriberService::class.java).also { intent -> - context.stopService(intent) // Service will auto-restart - } - } - /** * Starts or stops the foreground service by figuring out how many instant delivery subscriptions * exist. If there's > 0, then we need a foreground service. 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 d61bccd2..f364e16c 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt @@ -304,7 +304,7 @@ class MainActivity : AppCompatActivity(), AddFragment.SubscribeListener, Notific } wsEnableButton.setOnClickListener { repository.setConnectionProtocol(Repository.CONNECTION_PROTOCOL_WS) - SubscriberServiceManager(this).restart() + SubscriberServiceManager(this).refresh() wsBanner.visibility = View.GONE // Maybe show WebSocketReconnectBanner 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 8098f98b..c720af56 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt @@ -700,7 +700,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere override fun putString(key: String?, value: String?) { val proto = value ?: repository.getConnectionProtocol() repository.setConnectionProtocol(proto) - restartService() + serviceManager.refresh() // Refresh to switch connections between WS and JSON stream } override fun getString(key: String?, defValue: String?): String { return repository.getConnectionProtocol() @@ -737,10 +737,6 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere repository.setAutoDownloadMaxSize(autoDownloadSelectionCopy) } - private fun restartService() { - serviceManager.restart() // Service will auto-restart - } - private fun copyLogsToClipboard(scrub: Boolean) { lifecycleScope.launch(Dispatchers.IO) { val context = context ?: return@launch @@ -1056,7 +1052,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere override fun onUpdateUser(dialog: DialogFragment, user: User) { lifecycleScope.launch(Dispatchers.IO) { repository.updateUser(user) - serviceManager.restart() // Editing does not change the user ID + serviceManager.refresh() runOnUiThread { if (this@SettingsActivity::userSettingsFragment.isInitialized) { userSettingsFragment.reload() @@ -1068,7 +1064,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere override fun onDeleteUser(dialog: DialogFragment, baseUrl: String) { lifecycleScope.launch(Dispatchers.IO) { repository.deleteUser(baseUrl) - serviceManager.restart() + serviceManager.refresh() runOnUiThread { if (this@SettingsActivity::userSettingsFragment.isInitialized) { userSettingsFragment.reload() @@ -1080,7 +1076,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere override fun onAddCustomHeader(dialog: DialogFragment, header: io.heckel.ntfy.db.CustomHeader) { lifecycleScope.launch(Dispatchers.IO) { repository.addCustomHeader(header) - serviceManager.restart() // Restart to apply new headers + serviceManager.refresh() // Refresh to apply new headers runOnUiThread { if (this@SettingsActivity::customHeaderSettingsFragment.isInitialized) { customHeaderSettingsFragment.reload() @@ -1092,7 +1088,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere override fun onUpdateCustomHeader(dialog: DialogFragment, oldHeader: io.heckel.ntfy.db.CustomHeader, newHeader: io.heckel.ntfy.db.CustomHeader) { lifecycleScope.launch(Dispatchers.IO) { repository.updateCustomHeader(oldHeader, newHeader) - serviceManager.restart() // Restart to apply header changes + serviceManager.refresh() // Refresh to apply header changes runOnUiThread { if (this@SettingsActivity::customHeaderSettingsFragment.isInitialized) { customHeaderSettingsFragment.reload() @@ -1104,7 +1100,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere override fun onDeleteCustomHeader(dialog: DialogFragment, header: io.heckel.ntfy.db.CustomHeader) { lifecycleScope.launch(Dispatchers.IO) { repository.deleteCustomHeader(header) - serviceManager.restart() + serviceManager.refresh() runOnUiThread { if (this@SettingsActivity::customHeaderSettingsFragment.isInitialized) { customHeaderSettingsFragment.reload()