mirror of
https://github.com/minibits-cash/minibits_wallet.git
synced 2026-07-22 07:48:28 +00:00
Fix foreground service
This commit is contained in:
6
index.js
6
index.js
@@ -81,7 +81,11 @@ notifee.onForegroundEvent(async ({ type, detail }) => {
|
||||
|
||||
if (detail.pressAction && detail.pressAction.id === 'stop') {
|
||||
log.trace('[onForegroundEvent] Stopping foreground service')
|
||||
await notifee.stopForegroundService()
|
||||
// Route through closeNwcListener (not raw stopForegroundService) so pressing
|
||||
// Stop also tears down the adaptive poll and releases any in-flight NWC
|
||||
// pay_invoice immediately. It's a superset of stopping the service, and is
|
||||
// harmless for non-NWC task foreground services (which share the one service).
|
||||
await NotificationService.closeNwcListener()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -37,6 +37,53 @@ export type NotifyNwcRequestData = {
|
||||
|
||||
let _nwcQueue: any = undefined
|
||||
|
||||
// Tracks the active NWC foreground-service teardown loop so it can never run
|
||||
// twice and a stray poll can always be cleared. The poll is the ONLY automatic
|
||||
// closer of the shared foreground service, so its teardown must be idempotent
|
||||
// and exception-safe — a missed stop leaves the service to hit Android's
|
||||
// shortService timeout → ANR.
|
||||
let _nwcListenerPoll: ReturnType<typeof setInterval> | undefined = undefined
|
||||
let _nwcListenerClosing = false
|
||||
let _nwcListenerNotificationId: string | undefined = undefined
|
||||
|
||||
// Tear down the NWC listener / shared foreground service. Shared by the adaptive
|
||||
// poll AND the notification "Stop" action (index.js), so the Stop press releases
|
||||
// any in-flight pay_invoice immediately instead of waiting for the next poll tick.
|
||||
// Idempotent for the duration of a single teardown (guards a poll/Stop race), then
|
||||
// resets so a later task foreground service can still be stopped by its own Stop.
|
||||
const closeNwcListener = async function (): Promise<void> {
|
||||
if (_nwcListenerClosing) return
|
||||
_nwcListenerClosing = true
|
||||
|
||||
if (_nwcListenerPoll) {
|
||||
clearInterval(_nwcListenerPoll)
|
||||
_nwcListenerPoll = undefined
|
||||
}
|
||||
|
||||
// Tell any in-flight NWC pay_invoice that the background context is going away,
|
||||
// so it can stop waiting for its async melt result (no separate timeout needed —
|
||||
// the wait is bound to this listener's lifetime).
|
||||
EventEmitter.emit('ev_nwcListenerClosing', undefined)
|
||||
|
||||
try {
|
||||
if(Platform.OS === 'android') {
|
||||
log.trace('[closeNwcListener] Terminating Android foreground service')
|
||||
await stopForegroundService()
|
||||
} else {
|
||||
log.trace('[closeNwcListener] Closing iOS nwcSubscription')
|
||||
rootStoreInstance.nwcStore.resetSubscription()
|
||||
if(_nwcListenerNotificationId) {
|
||||
await cancelNotification(_nwcListenerNotificationId)
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
log.warn('[closeNwcListener] Failed to close NWC listener', {message: e.message})
|
||||
} finally {
|
||||
_nwcListenerNotificationId = undefined
|
||||
_nwcListenerClosing = false
|
||||
}
|
||||
}
|
||||
|
||||
const getNwcQueue = function () {
|
||||
if(!_nwcQueue) {
|
||||
_nwcQueue = new TaskQueue({
|
||||
@@ -383,31 +430,44 @@ const createNwcListenerNotification = async function () {
|
||||
}
|
||||
}
|
||||
|
||||
const listenerNotification = await notifee.displayNotification({
|
||||
title: NWC_LISTENER_NAME,
|
||||
body: 'Listening for NWC commands...',
|
||||
android: {
|
||||
channelId: NWC_CHANNEL_ID,
|
||||
asForegroundService: true,
|
||||
largeIcon: minibitsPngIcon,
|
||||
importance: AndroidImportance.HIGH,
|
||||
/*progress: {
|
||||
indeterminate: true,
|
||||
},*/
|
||||
actions: [
|
||||
{
|
||||
title: 'Stop',
|
||||
pressAction: {
|
||||
id: 'stop',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
ios: {
|
||||
categoryId: NWC_CHANNEL_ID,
|
||||
},
|
||||
data: {task: LISTEN_FOR_NWC_EVENTS}
|
||||
})
|
||||
let listenerNotification: string | undefined = undefined
|
||||
|
||||
try {
|
||||
listenerNotification = await notifee.displayNotification({
|
||||
title: NWC_LISTENER_NAME,
|
||||
body: 'Listening for NWC commands...',
|
||||
android: {
|
||||
channelId: NWC_CHANNEL_ID,
|
||||
asForegroundService: true,
|
||||
largeIcon: minibitsPngIcon,
|
||||
importance: AndroidImportance.HIGH,
|
||||
/*progress: {
|
||||
indeterminate: true,
|
||||
},*/
|
||||
actions: [
|
||||
{
|
||||
title: 'Stop',
|
||||
pressAction: {
|
||||
id: 'stop',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
ios: {
|
||||
categoryId: NWC_CHANNEL_ID,
|
||||
},
|
||||
data: {task: LISTEN_FOR_NWC_EVENTS}
|
||||
})
|
||||
} catch (e: any) {
|
||||
// Most likely ForegroundServiceStartNotAllowedException: the OS background
|
||||
// FGS-start grace window (granted by the high-priority FCM push) expired
|
||||
// before we reached here. Nothing started, so release any pay_invoice that
|
||||
// may be waiting on the listener lifetime and bail — do NOT start the poll,
|
||||
// or it would call stopForegroundService() on a service that never ran.
|
||||
log.warn('[createNwcListenerNotification] Failed to start foreground service', {message: e.message})
|
||||
EventEmitter.emit('ev_nwcListenerClosing', undefined)
|
||||
return
|
||||
}
|
||||
|
||||
if(Platform.OS === 'ios') {
|
||||
nwcStore.listenForNwcEvents()
|
||||
@@ -423,23 +483,17 @@ const createNwcListenerNotification = async function () {
|
||||
const IDLE_GRACE_MS = 10 * 1000
|
||||
let lastBusyAt = Date.now()
|
||||
|
||||
const closeListener = async () => {
|
||||
// Tell any in-flight NWC pay_invoice that the background context is going
|
||||
// away, so it can stop waiting for its async melt result (no separate
|
||||
// timeout needed — the wait is bound to this listener's lifetime).
|
||||
EventEmitter.emit('ev_nwcListenerClosing', undefined)
|
||||
|
||||
if(Platform.OS === 'android') {
|
||||
log.trace('[createNwcListenerNotification] Terminating Android foreground service')
|
||||
await stopForegroundService()
|
||||
} else {
|
||||
log.trace('[createNwcListenerNotification] Closing iOS nwcSubscription')
|
||||
nwcStore.resetSubscription()
|
||||
cancelNotification(listenerNotification)
|
||||
}
|
||||
// Defensive: kill any leftover poll from a prior listener so we never run two
|
||||
// teardown loops against the one shared foreground service. Record this
|
||||
// listener's notification id so closeNwcListener (poll or Stop action) can
|
||||
// cancel it on iOS.
|
||||
if (_nwcListenerPoll) {
|
||||
clearInterval(_nwcListenerPoll)
|
||||
_nwcListenerPoll = undefined
|
||||
}
|
||||
_nwcListenerNotificationId = listenerNotification
|
||||
|
||||
const poll = setInterval(async () => {
|
||||
_nwcListenerPoll = setInterval(async () => {
|
||||
const inFlight = SyncQueue.getSyncQueue().getAllTasksDetails(['idle', 'running']).length
|
||||
const now = Date.now()
|
||||
if (inFlight > 0) {
|
||||
@@ -449,12 +503,10 @@ const createNwcListenerNotification = async function () {
|
||||
const elapsed = now - startedAt
|
||||
|
||||
if (elapsed >= HARD_CAP_MS || (inFlight === 0 && idleFor >= IDLE_GRACE_MS)) {
|
||||
clearInterval(poll)
|
||||
log.trace('[createNwcListenerNotification] Closing NWC listener', {elapsed, idleFor, inFlight})
|
||||
await closeListener()
|
||||
await closeNwcListener()
|
||||
}
|
||||
}, 2000)
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -532,9 +584,16 @@ const areNotificationsEnabled = async function (): Promise<boolean> {
|
||||
}
|
||||
|
||||
|
||||
const stopForegroundService = async function (): Promise<void> {
|
||||
await notifee.stopForegroundService()
|
||||
log.trace('[stopForegroundService] completed')
|
||||
const stopForegroundService = async function (): Promise<void> {
|
||||
try {
|
||||
await notifee.stopForegroundService()
|
||||
log.trace('[stopForegroundService] completed')
|
||||
} catch (e: any) {
|
||||
// notifee throws if no foreground service is currently running (already
|
||||
// stopped by a finished task, the Stop action, or a prior teardown).
|
||||
// Harmless — callers treat stopping as best-effort and idempotent.
|
||||
log.warn('[stopForegroundService] Failed to stop foreground service', {message: e.message})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -552,5 +611,6 @@ export const NotificationService = {
|
||||
onForegroundNotification,
|
||||
areNotificationsEnabled,
|
||||
getDisplayedNotifications,
|
||||
stopForegroundService
|
||||
stopForegroundService,
|
||||
closeNwcListener
|
||||
}
|
||||
@@ -81,14 +81,29 @@ const _handleTaskResult = async (taskId: TaskId, result: WalletTaskResult | Tran
|
||||
|
||||
if(queue.getAllTasksDetails(['idle', 'running']).length === 0) {
|
||||
const notifications = await NotificationService.getDisplayedNotifications()
|
||||
// Do not stop the listener for NWC events, this one is handled via timeout
|
||||
for (const n of notifications) {
|
||||
if(n.notification.android?.asForegroundService) {
|
||||
if(n.notification.title && n.notification.title !== NWC_LISTENER_NAME) {
|
||||
log.trace('[_handleTaskResult] Stopping foreground service')
|
||||
await NotificationService.stopForegroundService()
|
||||
}
|
||||
}
|
||||
|
||||
// The NWC listener and task notifications share ONE Android foreground
|
||||
// service, and stopForegroundService() is all-or-nothing. If the NWC
|
||||
// listener is up, never stop here — it owns teardown via its own adaptive
|
||||
// poll, and stopping now would kill it mid-listen. Otherwise stop once for
|
||||
// a finished task's foreground service.
|
||||
const hasNwcListener = notifications.some(
|
||||
n => n.notification.android?.asForegroundService &&
|
||||
n.notification.title === NWC_LISTENER_NAME
|
||||
)
|
||||
|
||||
if(hasNwcListener) {
|
||||
log.trace('[_handleTaskResult] NWC listener active, leaving foreground service running')
|
||||
return
|
||||
}
|
||||
|
||||
const hasTaskForegroundService = notifications.some(
|
||||
n => n.notification.android?.asForegroundService
|
||||
)
|
||||
|
||||
if(hasTaskForegroundService) {
|
||||
log.trace('[_handleTaskResult] Stopping foreground service')
|
||||
await NotificationService.stopForegroundService()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user