Stop an account's IDLE watcher when the account is removed
IdleService read the account list once at startup, so a removed account's IDLE loop kept reconnecting (backing off harmlessly) until the service restarted, and a newly-added account wasn't watched until then either. - IdleService now observes the accounts and reconciles one IDLE watcher per account: it starts a watcher for an added account and cancels the watcher for a removed one, which closes that account's IDLE connection promptly via the existing cancellation path. - The app runs the service only while push is enabled AND at least one account exists, so it auto-starts on the first account and stops on the last. - Verified on the Android 17 emulator: two accounts held two IDLE connections; removing one dropped to a single connection with no retry loop, and the other kept idling. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -9,11 +9,13 @@ import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import org.libremail.data.settings.SettingsRepository
|
||||
import org.libremail.data.sync.SyncScheduler
|
||||
import org.libremail.domain.repository.AccountRepository
|
||||
import org.libremail.push.IdlePushManager
|
||||
|
||||
@HiltAndroidApp
|
||||
@@ -25,6 +27,8 @@ class LibreMailApplication : Application(), Configuration.Provider {
|
||||
|
||||
@Inject lateinit var settingsRepository: SettingsRepository
|
||||
|
||||
@Inject lateinit var accountRepository: AccountRepository
|
||||
|
||||
@Inject lateinit var idlePushManager: IdlePushManager
|
||||
|
||||
private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
@@ -37,12 +41,16 @@ class LibreMailApplication : Application(), Configuration.Provider {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
syncScheduler.schedulePeriodicSync()
|
||||
// Start or stop the IMAP IDLE push service to match the user's preference, reactively.
|
||||
// Run the IMAP IDLE push service only while it has something to do: the push setting is on
|
||||
// AND at least one account exists. This starts it when the first account is added and stops
|
||||
// it when the last is removed, reactively.
|
||||
appScope.launch {
|
||||
settingsRepository.settings
|
||||
.map { it.pushIdle }
|
||||
combine(
|
||||
settingsRepository.settings.map { it.pushIdle },
|
||||
accountRepository.observeAccounts().map { it.isNotEmpty() },
|
||||
) { pushEnabled, hasAccounts -> pushEnabled && hasAccounts }
|
||||
.distinctUntilChanged()
|
||||
.collect { enabled -> if (enabled) idlePushManager.start() else idlePushManager.stop() }
|
||||
.collect { active -> if (active) idlePushManager.start() else idlePushManager.stop() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,11 @@ import javax.inject.Inject
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
@@ -45,22 +47,35 @@ class IdleService : Service() {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
private var watching = false
|
||||
|
||||
/** Active IDLE watcher per account id, so we can start/stop them as accounts change. */
|
||||
private val watchers = mutableMapOf<String, Job>()
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
startAsForeground()
|
||||
if (!watching) {
|
||||
watching = true
|
||||
scope.launch { watchAllAccounts() }
|
||||
scope.launch { reconcileWatchers() }
|
||||
}
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
private suspend fun watchAllAccounts() {
|
||||
val accounts = accountDao.getAll().map { it.toDomain() }
|
||||
if (accounts.isEmpty()) {
|
||||
stopSelf()
|
||||
return
|
||||
/**
|
||||
* Observes the account list and keeps one IDLE watcher per account: a watcher is started for a
|
||||
* newly-added account and cancelled when its account is removed (which promptly closes that
|
||||
* account's IDLE connection). The service is started/stopped by the app based on whether any
|
||||
* accounts exist, so reaching zero here is just a transient state.
|
||||
*/
|
||||
private suspend fun reconcileWatchers() {
|
||||
accountDao.observeAll().collect { entities ->
|
||||
val accounts = entities.map { it.toDomain() }
|
||||
val currentIds = accounts.mapTo(mutableSetOf()) { it.id }
|
||||
(watchers.keys - currentIds).forEach { id -> watchers.remove(id)?.cancel() }
|
||||
accounts.forEach { account ->
|
||||
if (account.id !in watchers) {
|
||||
watchers[account.id] = scope.launch { watchAccount(account) }
|
||||
}
|
||||
}
|
||||
}
|
||||
accounts.forEach { account -> scope.launch { watchAccount(account) } }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user