fix: unwrap GiftWraps for all writable accounts when always-on is enabled
When the always-on notification service is on, preload every saved writable account into AccountCacheState so each one's newNotesPreProcessor subscribes to LocalCache.live.newEventBundles. Without this, relay-delivered GiftWraps addressed to non-active accounts would sit in LocalCache undeconstructed, and NotificationDispatcher never fires for them. The preload observes LocalPreferences.accountsFlow so accounts added during a session (login flow) join automatically. On disable, the collector is cancelled and every cached account except the active one is released — users with the setting off keep today's single-account battery footprint. Loading an account is idempotent (AccountCacheState checks cache first), so this cannot double-init or conflict with PushWrapDecryptor's on-demand loadAccount calls. Loading does not affect the active account selected by AccountSessionManager, so the UI is unaffected.
This commit is contained in:
@@ -395,8 +395,17 @@ class AppModules(
|
||||
)
|
||||
}
|
||||
|
||||
// Manages always-on notification service lifecycle
|
||||
val alwaysOnNotificationServiceManager = AlwaysOnNotificationServiceManager(appContext, applicationIOScope)
|
||||
// Manages always-on notification service lifecycle. Preloads every saved
|
||||
// writable account while enabled so GiftWraps for non-active accounts still
|
||||
// get unwrapped by their owning account's newNotesPreProcessor.
|
||||
val alwaysOnNotificationServiceManager =
|
||||
AlwaysOnNotificationServiceManager(
|
||||
context = appContext,
|
||||
scope = applicationIOScope,
|
||||
accountsCache = accountsCache,
|
||||
localPreferences = LocalPreferences,
|
||||
activePubKeyProvider = { sessionManager.loggedInAccount()?.pubKey },
|
||||
)
|
||||
|
||||
// Observes LocalCache for notification-relevant events and routes them to
|
||||
// EventNotificationConsumer. Sources: FCM, UnifiedPush, Pokey, active relay
|
||||
|
||||
+37
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.model.accountsCache
|
||||
|
||||
import android.content.ContentResolver
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AccountSettings
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
@@ -68,6 +69,42 @@ class AccountCacheState(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads every saved account that can sign (has a private key or an external signer)
|
||||
* into the cache. Safe to call repeatedly — [loadAccount] is idempotent, so already
|
||||
* loaded accounts are returned as-is. Used by the always-on notification service so
|
||||
* GiftWraps addressed to non-active accounts still get unwrapped and notified.
|
||||
*/
|
||||
suspend fun loadAllWritableAccounts(localPreferences: LocalPreferences) {
|
||||
localPreferences.allSavedAccounts().forEach { savedAccount ->
|
||||
if (!savedAccount.hasPrivKey && !savedAccount.loggedInWithExternalSigner) return@forEach
|
||||
try {
|
||||
val accountSettings = localPreferences.loadAccountConfigFromEncryptedStorage(savedAccount.npub) ?: return@forEach
|
||||
loadAccount(accountSettings)
|
||||
} catch (e: Exception) {
|
||||
if (e is kotlinx.coroutines.CancellationException) throw e
|
||||
Log.w("AccountCacheState", "Failed to preload account ${savedAccount.npub}: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels and removes every cached account whose pubkey is not in [keepPubkeys].
|
||||
* Used to release accounts that were preloaded for background notification handling
|
||||
* when the always-on service is turned off, while preserving the active account.
|
||||
*/
|
||||
fun retainOnly(keepPubkeys: Set<HexKey>) {
|
||||
accounts.update { existingAccounts ->
|
||||
val toRemove = existingAccounts.filterKeys { it !in keepPubkeys }
|
||||
if (toRemove.isEmpty()) {
|
||||
existingAccounts
|
||||
} else {
|
||||
toRemove.values.forEach { it.scope.cancel() }
|
||||
existingAccounts.minus(toRemove.keys)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadAccount(accountSettings: AccountSettings): Account =
|
||||
loadAccount(
|
||||
signer =
|
||||
|
||||
+60
@@ -21,8 +21,12 @@
|
||||
package com.vitorpamplona.amethyst.service.notifications
|
||||
|
||||
import android.content.Context
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
@@ -40,16 +44,27 @@ import kotlinx.coroutines.launch
|
||||
* When enabled, all layers activate. When disabled, all layers deactivate.
|
||||
* The manager watches the account's alwaysOnNotificationService setting
|
||||
* and reacts to changes in real time.
|
||||
*
|
||||
* While enabled, every saved writable account is kept loaded in
|
||||
* [AccountCacheState] so GiftWraps addressed to any of them (delivered via
|
||||
* open relay subscriptions) get unwrapped by the owning account's
|
||||
* `newNotesPreProcessor`. Without this, wraps for non-active accounts would
|
||||
* sit in [com.vitorpamplona.amethyst.model.LocalCache] with no subscriber
|
||||
* able to decrypt them.
|
||||
*/
|
||||
class AlwaysOnNotificationServiceManager(
|
||||
private val context: Context,
|
||||
private val scope: CoroutineScope,
|
||||
private val accountsCache: AccountCacheState,
|
||||
private val localPreferences: LocalPreferences,
|
||||
private val activePubKeyProvider: () -> HexKey?,
|
||||
) {
|
||||
companion object {
|
||||
private const val TAG = "AlwaysOnNotifManager"
|
||||
}
|
||||
|
||||
private var watchJob: Job? = null
|
||||
private var preloadJob: Job? = null
|
||||
private var wasEnabled = false
|
||||
|
||||
/**
|
||||
@@ -76,6 +91,8 @@ class AlwaysOnNotificationServiceManager(
|
||||
fun stop() {
|
||||
watchJob?.cancel()
|
||||
watchJob = null
|
||||
preloadJob?.cancel()
|
||||
preloadJob = null
|
||||
}
|
||||
|
||||
private fun enableAllLayers() {
|
||||
@@ -91,6 +108,8 @@ class AlwaysOnNotificationServiceManager(
|
||||
ServiceWatchdogManager.schedule(context)
|
||||
|
||||
// L2 (FCM) and L4 (BOOT_COMPLETED) are always active via manifest
|
||||
|
||||
startMultiAccountPreload()
|
||||
}
|
||||
|
||||
private fun disableAllLayers() {
|
||||
@@ -104,5 +123,46 @@ class AlwaysOnNotificationServiceManager(
|
||||
|
||||
// L5: Cancel watchdog alarm
|
||||
ServiceWatchdogManager.cancel(context)
|
||||
|
||||
stopMultiAccountPreload()
|
||||
}
|
||||
|
||||
/**
|
||||
* Preloads every saved writable account into [AccountCacheState] and keeps the set
|
||||
* in sync by observing [LocalPreferences.accountsFlow]. New accounts added while
|
||||
* the service is enabled (login flow) are picked up automatically.
|
||||
*
|
||||
* Note: the first [LocalPreferences.accountsFlow] emission is `null` (lazily
|
||||
* populated). We still call [AccountCacheState.loadAllWritableAccounts] on every
|
||||
* emission — its suspend call to `allSavedAccounts()` triggers flow population,
|
||||
* and subsequent [loadAccount] calls are idempotent on already-loaded accounts.
|
||||
*/
|
||||
private fun startMultiAccountPreload() {
|
||||
preloadJob?.cancel()
|
||||
preloadJob =
|
||||
scope.launch {
|
||||
localPreferences.accountsFlow().collect {
|
||||
try {
|
||||
accountsCache.loadAllWritableAccounts(localPreferences)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
Log.w(TAG, "Multi-account preload failed: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the preload collector and releases every cached account except the
|
||||
* currently active one, so users with the setting off return to single-account
|
||||
* memory/battery footprint.
|
||||
*/
|
||||
private fun stopMultiAccountPreload() {
|
||||
preloadJob?.cancel()
|
||||
preloadJob = null
|
||||
val active = activePubKeyProvider()
|
||||
if (active != null) {
|
||||
accountsCache.retainOnly(setOf(active))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user