Remove foreground service restart() function, in an attempt to avoid crashes

This commit is contained in:
Philipp Heckel 2026-01-03 12:45:50 -05:00
parent 50d3e9b038
commit d064e751c4
5 changed files with 36 additions and 21 deletions

View file

@ -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<String, Long>
val topicsToSubscriptionIds: Map<String, Long>,
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
)

View file

@ -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<ConnectionId>
.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 {

View file

@ -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.

View file

@ -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

View file

@ -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()