From 994bdae2d14c131d95f422bd5cd669d452cb71af Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 15 Jul 2026 21:31:34 +0000 Subject: [PATCH] feat(amethyst): keep the signer answering in the background The signer's relay subscription lives in the account scope and needs the shared NostrClient connected and the process alive to keep working while Amethyst is backgrounded or closed. Hook it into the existing always-on foreground service (and its five restart layers) instead of building a new one: - AlwaysOnNotificationServiceManager now starts/stops the layers when EITHER the notification service or nip46SignerEnabled is on (combined flow). - NotificationRelayService.isEnabled (the auto-restart guard) honors the signer flag too, so START_STICKY / watchdog / boot restart keep the signer up. - The signer screen notes that a background connection (ongoing notification) is what lets it answer requests while closed. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_015FHr2mu5SiHwYNR7evYUuF --- .../AlwaysOnNotificationServiceManager.kt | 21 +++++++++++++++---- .../notifications/NotificationRelayService.kt | 4 +++- .../settings/nip46/Nip46SignerScreen.kt | 6 ++++++ amethyst/src/main/res/values/strings.xml | 1 + 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/AlwaysOnNotificationServiceManager.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/AlwaysOnNotificationServiceManager.kt index 58c11797f9..4af3a66589 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/AlwaysOnNotificationServiceManager.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/AlwaysOnNotificationServiceManager.kt @@ -53,8 +53,10 @@ import kotlinx.coroutines.launch * this is the battery-saver "airplane mode". Persisted, so an explicit off survives * restarts and crashes. * - The **per-account participation** flag ([com.vitorpamplona.amethyst.model.AccountSettings.alwaysOnNotificationService], - * "Keep this account active in the background"). While the master is on, the service - * runs as long as **at least one** writable account participates. + * "Keep this account active in the background") **or** its NIP-46 signer toggle + * ([com.vitorpamplona.amethyst.model.AccountSettings.nip46SignerEnabled]). While the master is + * on, the service runs as long as **at least one** writable account has either flag on — the + * background signer relies on the same foreground service to keep answering requests. * * While the master is on, every saved writable account is kept loaded in * [AccountCacheState] so (a) its participation flag is observable and (b) GiftWraps @@ -83,6 +85,11 @@ class AlwaysOnNotificationServiceManager( * loaded writable account. The service layers run while the master is on AND at * least one account participates; the master overrides everything when off. * + * An account "participates" when either its always-on setting **or** its NIP-46 + * signer toggle is on: the foreground service (and its restart layers) keep the + * process + shared relay client alive, which is exactly what the background signer + * needs to keep answering requests, so the signer toggle keeps the layers up too. + * * Idempotent: safe to call again on account switch/login — it restarts the watch. */ @OptIn(ExperimentalCoroutinesApi::class) @@ -105,11 +112,17 @@ class AlwaysOnNotificationServiceManager( // Master on: keep every writable account loaded so its participation // flag is observable and its gift wraps can decrypt, then run the - // service only while at least one account is participating. + // service only while at least one account is participating. An account + // participates when its always-on setting OR its NIP-46 signer toggle is + // on — the background signer needs the same foreground service alive. startMultiAccountPreload() accountsCache.accounts .flatMapLatest { accounts -> - val flags = accounts.values.map { it.settings.alwaysOnNotificationService } + val flags = + accounts.values.map { account -> + account.settings.alwaysOnNotificationService + .combine(account.settings.nip46SignerEnabled) { alwaysOn, signer -> alwaysOn || signer } + } if (flags.isEmpty()) { flowOf(false) } else { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationRelayService.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationRelayService.kt index 2f48e5466b..c2fa2eab2d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationRelayService.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/notifications/NotificationRelayService.kt @@ -126,9 +126,11 @@ class NotificationRelayService : Service() { // the service itself once a participant exists. fun isEnabled(context: Context): Boolean = try { + // The service also backs the NIP-46 signer, so an account with the signer on + // participates just like one with always-on notifications on. LocalPreferences.isNotificationServiceEnabled() && Amethyst.instance.accountsCache.accounts.value.values.any { - it.settings.alwaysOnNotificationService.value + it.settings.alwaysOnNotificationService.value || it.settings.nip46SignerEnabled.value } } catch (e: Exception) { false diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/nip46/Nip46SignerScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/nip46/Nip46SignerScreen.kt index c15648de23..15ef1b7b98 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/nip46/Nip46SignerScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/nip46/Nip46SignerScreen.kt @@ -198,6 +198,12 @@ fun Nip46SignerScreen( count = connectedCount, onClick = { nav.nav(Route.ConnectedApps) }, ) + + Text( + stringResource(R.string.nip46_signer_background_hint), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) } } } diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 5b43f12ee2..6b81a1e108 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -926,6 +926,7 @@ Could not connect: %1$s This is a read-only account and cannot sign. Remote signer app + Amethyst keeps a background connection (shown as an ongoing notification) so it can answer signing requests while closed. Sign for other apps Turn on signer Live