diff --git a/app/src/main/kotlin/org/libremail/LibreMailApplication.kt b/app/src/main/kotlin/org/libremail/LibreMailApplication.kt index facdb01..01395c2 100644 --- a/app/src/main/kotlin/org/libremail/LibreMailApplication.kt +++ b/app/src/main/kotlin/org/libremail/LibreMailApplication.kt @@ -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() } } } } diff --git a/app/src/main/kotlin/org/libremail/push/IdleService.kt b/app/src/main/kotlin/org/libremail/push/IdleService.kt index 3c2b9b9..46247ff 100644 --- a/app/src/main/kotlin/org/libremail/push/IdleService.kt +++ b/app/src/main/kotlin/org/libremail/push/IdleService.kt @@ -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() + 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) } } } /**