Skip to content

Latest commit

 

History

History
415 lines (325 loc) · 25.5 KB

foreground-service.mdx

File metadata and controls

415 lines (325 loc) · 25.5 KB
title Foreground Service
description Display a notification when running an ongoing headless task.

Foreground services are an advanced Android concept which allows you to display notifications to your users when running long lived background tasks. The notification acts like any other notification, however it cannot be removed by the user and lives for the duration of the service.

The service task can continue while the application is in the background, subject to Android and device restrictions. Once the task completes, the service is stopped and the notification is removed. Examples of when to use a Foreground Service are:

  • Capturing on-going information (such as current location for a fitness or delivery app).
  • Playing media to the user.
  • Displaying important information to the user, such as directions.
  • Showing the status of a local device task, such as deleting files.

Only a single Foreground Service can be running at any one time for an application.

To create a long running task, Notify Kit exposes a registerForegroundService method. It should be registered outside of any React components as early as possible in your code (e.g. within the project index.js file).

The Foreground Service is not persisted between hot reloads.

Creating a service

Let's create our foreground service which we'll later hook into. The callback we register must be a promise, and is passed a notification object when called.

import notifee from 'react-native-notify-kit';

notifee.registerForegroundService(notification => {
  return new Promise(() => {
    // Long running task...
  });
});

Calling stopForegroundService() stops the active service and removes its notification. If no service is active or starting, the call is a no-op. Duplicate STOP calls are safe, and the library does not cold-start a ForegroundService just to stop it.

On Android 12+ (API 31+), a ForegroundServiceStartNotAllowedException in the defensive terminal STOP/null path is handled non-fatally. The 10.8.1 fix for Issue #81 also automatically handles a narrow START case: after process death, Android may retry or redeliver a START that the new process no longer tracks. If the command carries Android's RETRY/REDELIVERY flags and a valid request ID, and no foreground state or service runner has been acquired, denial of the initial startForeground() promotion is non-fatal. The library does not treat the denied promotion as successful or start a runner for that command. It attempts to stop that service instance while preserving other pending STARTs. If they prevent stopping, it re-evaluates the stop once none remain pending after definite rejection, provided the denied delivery is still current.

Separately, Issue #82 handles a qualified initial tracked START promotion denial on Android 15+ (API 35+). A valid tracked request can reach a new, never-promoted instance after the FGS budget is exhausted, and Android can deny the actual startForeground() call. Only ForegroundServiceStartNotAllowedException within that initial promotion boundary, with the implemented request, ownership, and generation eligibility checks still valid, is handled non-fatally. Only the denied request is terminalized; its runner is not started, other pending START obligations are preserved, and denial is not treated as successful promotion. This does not generalize to every FGS denial. Native diagnostics record cause=UNKNOWN; exception message text is not an authoritative diagnosis of Android's reason.

Other app-requested START failures, unrelated errors, and invalid configurations retain their existing error handling. Neither recovery bypasses Android's background restrictions or guarantees service survival or restart under Android/OEM policies. The device can stop the service (e.g. low memory), as can a user force quit from the application settings.

Attach a notification

To attach a new notification to the service, the asForegroundService property on the notification object can be set to true:

import notifee, { AndroidColor } from 'react-native-notify-kit';

notifee.displayNotification({
  title: 'Foreground service',
  body: 'This notification will exist for the lifetime of the service runner',
  android: {
    channelId,
    asForegroundService: true,
    color: AndroidColor.RED,
    colorized: true,
  },
});

Running this code will create a foreground service notification which is bound to the runner we registered with registerForegroundService above. As our example never returns the promise, the notification will exist for the lifetime of the service, subject to Android timeouts and device restrictions.

You may also notice we have provided color and colorized properties. When setting colorized to true on a foreground service notification, the color will be used to change the entire background color of the notification, which is not possible on standard notifications.

Building a long lived task

A long lived task can use intervals or subscribe to events to update the notification until it completes or you call stopForegroundService(), subject to Android timeouts and device restrictions. Stopping the native service does not automatically cancel application-owned JavaScript work.

For example, we could build a task which subscribes to an event handler:

notifee.registerForegroundService(() => {
  return new Promise(() => {
    // Example task subscriber
    onTaskUpdate(async task => {
      if (task.complete) {
        await notifee.stopForegroundService();
      }
    });
  });
});

The example code above would show the notification to the user until a new task event with complete being true is sent.

Updating an existing notification

Foreground notifications behave like any other notification, and can display anything (progress indicators, images etc). Whilst our service is running, we can also update the current notification to display different content:

notifee.registerForegroundService(notification => {
  return new Promise(() => {
    // Example task subscriber
    onTaskUpdate(async task => {
      if (task.update) {
        notifee.displayNotification({
          id: notification.id,
          body: notification.body,
          android: {
            ...notification.android,
            progress: {
              max: task.update.total,
              current: task.update.current,
            },
          },
        });
      }

      if (task.complete) {
        await notifee.stopForegroundService();
      }
    });
  });
});

Above we've updated the callback handler for the onTaskUpdate method. Each task.update call submits a new progress indicator position. Repeated displayNotification() calls with the same ID update the active foreground-service notification and keep the same service runner. NotifyKit tracks the latest update whose Android platform call returned normally for re-post and timeout handling; a normal return does not confirm that Android rendered the update. See Issue #75. Only a single foreground-service notification can exist for your application at any one time.

Handling interactions

Much like other notifications, we can subscribe to events when the user interacts with the Foreground Service notification. The service task runs in its own context, allowing us to subscribe to events within itself.

For example, to stop the foreground service when a user presses an action, we can subscribe to the event inside of the task:

import notifee, { EventType } from 'react-native-notify-kit';

// Create the task runner
notifee.registerForegroundService(notification => {
  return new Promise(() => {
    notifee.onForegroundEvent(async ({ type, detail }) => {
      if (type === EventType.ACTION_PRESS && detail.pressAction.id === 'stop') {
        await notifee.stopForegroundService();
      }
    });
  });
});

A notification can then be created with a Quick Action which triggers the event:

notifee.displayNotification({
  title: 'Foreground Service Notification',
  body: 'Press the Quick Action to stop the service',
  android: {
    channelId,
    actions: [
      {
        title: 'Stop',
        pressAction: {
          id: 'stop',
        },
      },
    ],
  },
});

It's important to note, for background events there can only be only a single handler registered at one time. It's recommended to listen to your service events in your application's main onBackgroundEvent callback handler so you can continue to listen to other events outside of the service.

Specifying foreground service types

On Android 14+ (API level 34+) you must declare a foregroundServiceType on the library's ForegroundService component in your app's AndroidManifest.xml, otherwise calling displayNotification({ android: { asForegroundService: true } }) will throw a SecurityException (the library logs an early-abort error with the missing declaration before Android crashes).

Breaking change in v9.1.13: the library previously shipped android:foregroundServiceType="shortService" on its own ForegroundService component, but that default was not safe to force on every consumer (Google Play policy, 3-minute shortService timeout, etc.) and was removed. Each app must now pick the types that match its use case and declare them explicitly. Existing apps upgrading from earlier versions must add this declaration.

<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
  <application>
    ...
    <!-- Single service type -->
    <service
      android:name="app.notifee.core.ForegroundService"
      android:foregroundServiceType="location"
      tools:node="merge" />

    <!-- Or, multiple service types combined with | -->
    <service
      android:name="app.notifee.core.ForegroundService"
      android:foregroundServiceType="location|camera|microphone"
      tools:node="merge" />
  </application>
</manifest>

The tools:node="merge" marker tells the Android manifest merger to add the foregroundServiceType attribute to the library's existing <service> declaration (which has no foregroundServiceType of its own). You can also use the older tools:replace="android:foregroundServiceType" — both work.

When you create a foreground service on Android 14+, the OS verifies the app holds the matching runtime permissions for the declared type (for example, RECORD_AUDIO for microphone). If the permission is missing, the system throws a SecurityException. You can also scope the types per-invocation by setting foregroundServiceTypes on the notification — this is useful when the app has been granted additional permissions at runtime and you want to update the running service:

import notifee, { AndroidForegroundServiceType } from 'react-native-notify-kit';

notifee.displayNotification({
  title: 'Foreground service',
  body: 'This notification will exist for the lifetime of the service runner',
  android: {
    channelId,
    asForegroundService: true,
    foregroundServiceTypes: [
      AndroidForegroundServiceType.FOREGROUND_SERVICE_TYPE_CAMERA,
      AndroidForegroundServiceType.FOREGROUND_SERVICE_TYPE_MICROPHONE,
    ],
  },
});

If no foregroundServiceTypes property is provided on the notification, the types are taken from the manifest.

If a new permission is granted while the service is running, posting the same notification (same id and channelId) with the updated foregroundServiceTypes array updates the running service with the new types.

Android 15+ data sync timeouts

For apps targeting Android 15+ (API 35+), Android limits background dataSync foreground execution to a cumulative 6 hours in a 24-hour period. This budget is shared by all services of that type in the application, rather than being a separate timer for each notification. A new START or process recreation does not automatically reset it. Bringing the app to the foreground resets the allowance under Android's rules. The effective remaining time is not exposed through a reliable ordinary public API. mediaProcessing has its own separately tracked budget; shortService has shorter limits. See Foreground service timeouts and Android 15 behavior changes.

When the budget is exhausted, Android can invoke onTimeout() and allows only a few seconds for service teardown. The application process must be able to execute that handling in time; otherwise Android can crash it with RemoteServiceException$ForegroundServiceDidNotStopInTimeException.

The Android foreground-service timeout path now prioritizes foreground demotion, coordinates conditional service stopping with concurrent START requests, and protects newer service state from stale timeout cleanup. It uses STOP_FOREGROUND_REMOVE to remove the foreground notification, snapshots the latest valid notification for the event, and protects runner, cached notification, and ownership state when retiring a foreground lifetime. Duplicate and stale callbacks are checked against that lifetime before cleanup. Conditional stopping requires current authority and preserves pending STARTs, including obligations whose acceptance is unknown.

A conditional stop can be refused when later STARTs exist, or deferred when stopping is not authorized. Accepted, refused/unknown, and failed outcomes are distinct; a normal return from stopForeground() is not independent confirmation from Android's Activity Manager (AMS). Demotion and stop failures remain observable in native diagnostics and are not converted to success. If a callback's provenance is ambiguous but its type matches the current foreground type, handling may prioritize demotion to reduce crash risk; this can end a newer foreground lifetime early.

The native timeout handling is hardened when Android delivers the callback and the application process can execute it. Device-level process suspension may still prevent timely handling. A callback that never executes cannot initiate teardown, and process survival is not guaranteed under Android/OEM suspension. Issue #82 reports crashes on vivo/iQOO/OnePlus Android 15/16 and hypothesizes an OEM CPU freeze; that specific cause is unconfirmed. Qualification on those devices under the original conditions and reproduction of the reported OEM freeze are NOT_RUN. The responsive Pixel timeout test does not establish that these OEM crashes are eliminated.

Handling FG_TIMEOUT

EventType.FG_TIMEOUT = 9 is Android-only. Native handling attempts this event best-effort when it handles a foreground service timeout and a notification snapshot is available. EventDetail can contain notification, optional startId, and optional fgsType. The API 34 single-parameter callback can report fgsType = -1 because it supplies no explicit type.

Register foreground and background listeners at the usual application entry points. For example, in index.js, before AppRegistry.registerComponent, add this branch to your existing background handler (only one can be registered):

import notifee, { EventType } from 'react-native-notify-kit';

notifee.onBackgroundEvent(async ({ type, detail }) => {
  if (type === EventType.FG_TIMEOUT) {
    console.log('Foreground service timeout', {
      notificationId: detail.notification?.id,
      startId: detail.startId,
      fgsType: detail.fgsType,
    });
    // Signal your application-owned task to stop and checkpoint if appropriate.
    // Do not immediately start another foreground service here.
  }
});

Use the same event check in your onForegroundEvent listener for foreground delivery and unsubscribe when its component unmounts. See Events for listener registration and background/headless constraints. The event is not an AMS teardown acknowledgment, does not guarantee JavaScript task cancellation, and does not authorize a new START. If the process is suspended or terminated, JavaScript may never run the handler. Delivery is not guaranteed exactly once in every Android condition.

Planning dataSync work

  • Make long tasks interruptible and save progress or checkpoints where appropriate; handle cancellation in your own task.
  • Plan for bounded foreground execution and coordinate all dataSync services sharing the application's budget.
  • Do not immediately restart after FG_TIMEOUT without considering Android's budget and START restrictions.
  • Consider WorkManager or other appropriate alternatives for deferrable work. WorkManager is not a guaranteed bypass of FGS limits.
  • Account for OEM execution restrictions and battery policies. Select a foreground service type that matches the actual work, rather than choosing another type just to avoid a timeout.

Voluntary best-effort run limit (API 35+)

android.foregroundServiceBestEffortStopAfterMs optionally requests an attempt to demote and stop the current foreground-service run after an elapsed duration. It is disabled when omitted or undefined. It requires Android API 35+, asForegroundService: true, and an effective foreground service type of dataSync only. If foregroundServiceTypes is omitted, the merged manifest must resolve to dataSync only. The receiver and FGS must use the application's supported process. Explicit mixed types and incompatible options are rejected before native START where possible. Android-only properties follow the library's existing platform validation behavior on iOS; there is no native iOS implementation of this feature.

The duration must be a finite safe integer from 60,000 to 18,000,000 milliseconds, inclusive. Native validation also checks the range and elapsed-deadline overflow. These bounds are product choices, not Android guarantees. The deadline starts at the first successful native promotion and uses SystemClock.elapsedRealtime(), including deep sleep. Same-ID updates preserve the original deadline. For an already promoted same-ID run with no unresolved START, repeating the same duration is accepted. Any run-limit-bearing request while a START is unresolved is rejected; retry only after it settles. Enabling or changing the option during a live run is rejected. Omission on an update does not disable the existing limit. A new run needs its own successful promotion; a type/epoch change revokes previous authority without implicitly rearming it.

Scheduling uses inexact AlarmManager.setAndAllowWhileIdle() with an explicit immutable PendingIntent to an internal receiver. No new exact-alarm permission, battery exemption, polling, or maintenance alarm is needed. Delivery may be delayed by several minutes or may never happen. Earlier diagnostic Pixel runs observed delivery delays of several minutes; those historical measurements are not qualification of the current public API or an upper bound. The app-wide cumulative dataSync budget is unknown to the library and may be consumed by other services or prior runs before this voluntary deadline.

This option is not a hard maximum and does not guarantee removal of ForegroundServiceDidNotStopInTimeException. OEM freeze behavior can prevent useful execution. Efficacy on vivo/iQOO/OnePlus has not been established. It is not a substitute for Android framework timeout handling, cooperative cancellation, or checkpoints.

A resolved displayNotification() Promise confirms notification/START submission through the existing API. Alarm registration happens later, after promotion; the Promise does not confirm that protection is active. Scheduling, ledger/storage, AlarmManager availability, owner validation and cancellation failures produce native diagnostics under tag ForegroundServiceRunLimit (SCHEDULE_FAILED, STORAGE_FAILED, ARM_FAILED/ARM_REJECTED, CANCEL_FAILED). ALARM_ARMED is emitted only after registration and the ARMED ledger commit succeed. Partial failure revokes native authority and attempts ticket-specific cancellation; it does not automatically stop or retry the application task. A failed cancellation cannot give a stale envelope authority over a successor.

A valid expiry attempts stopForeground(STOP_FOREGROUND_REMOVE), removing the owned foreground notification, followed by a selective conditional stop. Pending or unknown START obligations can defer stopping. Native return markers and stopSelfResult=true are not AMS acknowledgements. A stale/duplicate ticket cannot stop a newer run. Process recreation never restores an old lifetime from storage, and the receiver never cold-starts a FGS.

EventType.FG_RUN_LIMIT_REACHED = 10 reports that native code handled an accepted voluntary expiry after essential teardown attempts, with the latest owned notification. Delivery is best-effort, outside lifecycle locks; the bridge is not required for native teardown. No event is guaranteed after process death or without a usable React context. detail.foregroundServiceRunLimit contains:

  • requestedDurationMs, deadlineElapsed, deliveredElapsed (elapsed milliseconds).
  • demotion: returned or failed; returned is not an AMS acknowledgement.
  • conditionalStop: accepted, refused, deferred or failed.

There is no stopped, amsState, completion or crash-safety certificate. FG_TIMEOUT = 9 remains exclusively the Android framework timeout signal; voluntary expiry never synthesizes it. Scheduling failure does not emit a misleading voluntary-expiry event; use native diagnostics.

Android demotion does not cancel a JavaScript Promise, fetch/network operation, timer, application task or server work. Use idempotent checkpoints, cooperative cancellation (for example AbortController), and app-owned task-completion tracking. A run-limit event can trigger cooperative cancellation when received; it is not the only cancellation mechanism. Do not automatically restart a FGS on expiry to bypass Android's cumulative budget.

import notifee, { AndroidForegroundServiceType, EventType } from 'react-native-notify-kit';
import { Platform } from 'react-native';

const controller = new AbortController();
const unsubscribe = notifee.onForegroundEvent(({ type, detail }) => {
  if (type === EventType.FG_RUN_LIMIT_REACHED && detail.notification?.id === 'sync-run') {
    controller.abort(); // Your task must actually observe this signal; delivery is best-effort.
  }
});

if (Platform.OS === 'android') {
  const channelId = await notifee.createChannel({ id: 'sync', name: 'Sync' });
  await notifee.displayNotification({
    id: 'sync-run',
    title: 'Syncing',
    android: {
      channelId,
      asForegroundService: true,
      foregroundServiceTypes: [AndroidForegroundServiceType.FOREGROUND_SERVICE_TYPE_DATA_SYNC],
      ...(Number(Platform.Version) >= 35
        ? { foregroundServiceBestEffortStopAfterMs: 30 * 60 * 1000 }
        : {}),
    },
  });
}
// Register application work with registerForegroundService separately, pass controller.signal
// to cancellable operations, persist checkpoints, and unsubscribe when this owner is disposed.

Controlling display timing on Android 12+

Android 12 (API 31) introduced a system-imposed delay of up to 10 seconds before a foreground service notification is displayed, to give short-lived services time to finish before bothering the user. The library overrides this by default so foreground service notifications display immediately, matching pre-Android-12 behavior. You can opt out via foregroundServiceBehavior:

import notifee, { AndroidForegroundServiceBehavior } from 'react-native-notify-kit';

notifee.displayNotification({
  title: 'Deferred foreground service',
  body: 'Displayed after the platform default delay.',
  android: {
    channelId,
    asForegroundService: true,
    foregroundServiceBehavior: AndroidForegroundServiceBehavior.DEFERRED,
  },
});

Values:

  • AndroidForegroundServiceBehavior.IMMEDIATE — library default when asForegroundService: true. Displays the notification immediately. Corresponds to NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE.
  • AndroidForegroundServiceBehavior.DEFERRED — restores the Android 12+ deferred behavior (up to 10 s delay). Corresponds to NotificationCompat.FOREGROUND_SERVICE_DEFERRED.
  • AndroidForegroundServiceBehavior.DEFAULT — framework-neutral default. Corresponds to NotificationCompat.FOREGROUND_SERVICE_DEFAULT.

The property is ignored when asForegroundService is false or not set, and has no effect below API 31.

Warmup (advanced)

For latency-sensitive apps that call displayNotification({ asForegroundService: true }) very early after process start (e.g. from a launch deeplink), notifee.prewarmForegroundService() pre-loads the foreground service classes and warms the INotificationManager Binder proxy on a background thread. It's idempotent and safe to call multiple times; on iOS it resolves immediately as a no-op.

import notifee from 'react-native-notify-kit';

// Call early in your app startup — e.g. in index.js or App.tsx
notifee.prewarmForegroundService();

Most apps do not need this — the library warms automatically via its InitProvider content provider during app startup. The opt-in API is an escape hatch for lazy-loaded libraries, post-splash-screen warmup, or low-end devices where the automatic warmup hasn't completed by the first notification.