> ## Documentation Index
> Fetch the complete documentation index at: https://documentation.onesignal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Android Live Notifications

> Build dynamic, real-time Android notifications on top of OneSignal push. Pattern that mirrors iOS Live Activities by routing a structured payload through a Notification Service Extension to update a single, persistent notification.

Build Live Notifications on Android by sending a structured payload over OneSignal push and updating a single notification in place from your `NotificationServiceExtension`. The pattern mirrors iOS Live Activities, but it is implemented in your app code rather than provided by OneSignal as a managed feature. Use it for progress bars, sports scores, ride status, or any other content that should update inside one persistent notification instead of stacking new pushes.

Live Notifications require the user to have push notifications enabled.

## Requirements

* Latest [OneSignal Android SDK](./mobile-sdk-setup) integrated in your app.
* The end user has granted push notification permission.
* A `NotificationServiceExtension` in your app to intercept and render Live Notifications. See [Service extensions](./service-extensions#android-notification-service-extension) for background.

***

## How it works

OneSignal is the transport. Your app code defines the schema and renders the UI. The flow:

1. Your backend calls the [Create message API](/reference/create-message) with a `collapse_id` and a structured `live_notification` object inside `data`.
2. OneSignal delivers the push to the device.
3. Your `NotificationServiceExtension` intercepts every push, looks for the `live_notification` key, and dispatches based on its `event` (`start`, `update`, or `end`).
4. Your code calls `NotificationManager.notify(id, …)` with the **same Android notification ID** each time, which updates the existing notification rather than posting a new one.

<Note>
  The `live_notification` payload, its field names, and the `start` / `update` / `end` event values are a convention introduced by this guide. They are not enforced by OneSignal. You can rename or restructure anything as long as your service extension and your sending backend agree.
</Note>

***

## Live Notifications vs. standard push

Unlike regular push notifications, which post a new notification each time, Live Notifications use one persistent notification updated over time. Two things make this work:

* **`collapse_id` on the API side** keeps later messages tied to the same logical notification. See [Collapse ID (mobile push)](./push#collapse-id-mobile-push).
* **A stable Android notification ID on the device side** lets `NotificationManager.notify(id, …)` replace the visible notification's contents instead of stacking a new one.

***

## Implementation

### 1. Add a Notification Service Extension

Create a class that implements `INotificationServiceExtension`. The class below is complete and runnable: it creates the notification channel on first use, parses the `live_notification` payload, dispatches by event, and renders a progress notification as an example.

```kotlin NotificationServiceExtension.kt theme={null}
package com.onesignal.sample.android

import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.graphics.BitmapFactory
import android.os.Build
import androidx.annotation.Keep
import androidx.core.app.NotificationCompat
import com.onesignal.notifications.INotificationReceivedEvent
import com.onesignal.notifications.INotificationServiceExtension
import org.json.JSONObject
import java.util.logging.Logger

@Keep
class NotificationServiceExtension : INotificationServiceExtension {

    companion object {
        private val logger = Logger.getLogger(NotificationServiceExtension::class.java.name)

        // Map each Live Notification `key` to a stable Android notification ID.
        // Reusing the same ID is what lets `notify(...)` update the visible
        // notification instead of posting a new one.
        private val keyMap = mapOf(
            "progress" to 1001,
            // Add additional Live Notification keys here.
        )

        private const val PROGRESS_CHANNEL_ID = "live_notifications_progress"

        @Volatile
        private var notificationChannelsCreated = false
    }

    override fun onNotificationReceived(event: INotificationReceivedEvent) {
        val context = event.context
        val notificationManager =
            context.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
                ?: run {
                    logger.warning("NotificationManager not available.")
                    return
                }

        if (!notificationChannelsCreated) {
            createNotificationChannels(notificationManager)
            notificationChannelsCreated = true
        }

        val payload = event.notification.additionalData?.optJSONObject("live_notification")
        if (payload == null) {
            // Not a Live Notification. Let OneSignal display the push normally.
            return
        }

        // Render the Live Notification ourselves; skip OneSignal's default display.
        event.preventDefault()
        handleLiveNotification(payload, notificationManager, context)
    }

    private fun handleLiveNotification(
        payload: JSONObject,
        notificationManager: NotificationManager,
        context: Context,
    ) {
        val key = payload.optString("key").takeIf { it.isNotEmpty() } ?: run {
            logger.warning("Live Notification missing 'key'.")
            return
        }
        val notificationId = keyMap[key] ?: run {
            logger.warning("Unknown Live Notification key: $key")
            return
        }
        val updates = payload.optJSONObject("event_updates") ?: JSONObject()

        when (val eventType = payload.optString("event")) {
            "start", "update" -> when (key) {
                "progress" -> updateProgressNotification(
                    updates, notificationId, context, notificationManager,
                )
                // Route additional keys to their builders here.
            }
            "end" -> notificationManager.cancel(notificationId)
            else -> logger.warning("Unknown Live Notification event: $eventType")
        }
    }

    private fun updateProgressNotification(
        updates: JSONObject,
        notificationId: Int,
        context: Context,
        notificationManager: NotificationManager,
    ) {
        val currentProgress = updates.optInt("current_progress", 0)

        val builder = NotificationCompat.Builder(context, PROGRESS_CHANNEL_ID)
            .setContentTitle("Download in progress")
            .setContentText("$currentProgress% complete")
            .setSmallIcon(android.R.drawable.ic_media_play)
            .setLargeIcon(
                BitmapFactory.decodeResource(
                    context.resources,
                    android.R.drawable.ic_dialog_info,
                ),
            )
            .setOngoing(true)
            .setOnlyAlertOnce(true)
            .setProgress(100, currentProgress, false)
            .setAutoCancel(false)

        notificationManager.notify(notificationId, builder.build())
    }

    private fun createNotificationChannels(notificationManager: NotificationManager) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return

        val progressChannel = NotificationChannel(
            PROGRESS_CHANNEL_ID,
            "Progress Live Notifications",
            NotificationManager.IMPORTANCE_LOW,
        ).apply {
            description = "Progress updates that replace themselves in place."
            setShowBadge(false)
            enableVibration(false)
            setSound(null, null)
        }
        notificationManager.createNotificationChannel(progressChannel)
    }
}
```

<Note>
  The `@Keep` annotation prevents R8 or ProGuard from stripping or renaming this class when minification is enabled. See [Service extensions](./service-extensions#android-notification-service-extension) for more on writing service extensions.
</Note>

<Tip>
  Channels for Live Notifications should be low importance with badges, vibration, and sound disabled so frequent updates don't alert the user. See [Android notification categories](./android-notification-categories).
</Tip>

### 2. Register the extension in the Android Manifest

Add the `com.onesignal.NotificationServiceExtension` meta-data tag inside `<application>`, pointing to the fully qualified name of your class.

<CodeGroup>
  ```xml AndroidManifest.xml theme={null}
  <meta-data android:name="com.onesignal.NotificationServiceExtension"
             android:value="com.onesignal.sample.android.NotificationServiceExtension" />
  ```

  ```xml Example AndroidManifest.xml theme={null}
  <?xml version="1.0" encoding="utf-8"?>
  <manifest xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:tools="http://schemas.android.com/tools">

      <application
          android:allowBackup="true"
          android:dataExtractionRules="@xml/data_extraction_rules"
          android:fullBackupContent="@xml/backup_rules"
          android:icon="@mipmap/ic_launcher"
          android:name=".MainApplication"
          android:label="@string/app_name"
          android:roundIcon="@mipmap/ic_launcher_round"
          android:supportsRtl="true"
          android:theme="@style/Theme.OneSignalAndroidSample"
          tools:targetApi="31">
          <meta-data android:name="com.onesignal.NotificationServiceExtension"
              android:value="com.onesignal.sample.android.NotificationServiceExtension" />
          <activity
              android:name=".MainActivity"
              android:exported="true">
              <intent-filter>
                  <action android:name="android.intent.action.MAIN" />
                  <category android:name="android.intent.category.LAUNCHER" />
              </intent-filter>
          </activity>
      </application>

  </manifest>
  ```
</CodeGroup>

### 3. Define your Live Notification payload

Each Live Notification carries a `live_notification` object inside the push's `data` field. The schema is a convention you control. The shape used in this guide:

| Property           | Required | Description                                                                                                                            |
| ------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `key`              | Yes      | Identifies which Live Notification UI to render and which Android notification ID to reuse.                                            |
| `event`            | Yes      | Lifecycle event: `start`, `update`, or `end`.                                                                                          |
| `event_attributes` | No       | Static data passed once on `start`. Use it for fields that don't change for the life of the notification (team names, order ID, etc.). |
| `event_updates`    | No       | Dynamic data passed on `start` and every `update`. The shape is whatever your builder consumes (scores, percentages, ETAs).            |

**Example payload (start of a sports score Live Notification):**

```json theme={null}
{
  "key": "celtics-vs-lakers",
  "event": "start",
  "event_attributes": {
    "homeTeam": "Celtics",
    "awayTeam": "Lakers",
    "game": "Finals Game 1"
  },
  "event_updates": {
    "quarter": 1,
    "homeScore": 0,
    "awayScore": 0
  }
}
```

### 4. Handle the lifecycle events

Your service extension dispatches on the `event` field. The example code above does this in `handleLiveNotification`.

| Event    | Description                                                                | Required fields                     |
| -------- | -------------------------------------------------------------------------- | ----------------------------------- |
| `start`  | Creates the Live Notification with static and dynamic data.                | `event_attributes`, `event_updates` |
| `update` | Updates the existing Live Notification with new dynamic data.              | `event_updates`                     |
| `end`    | Removes the Live Notification by calling `NotificationManager.cancel(id)`. | None                                |

<Warning>
  Always reuse the **same Android notification ID** for a given `key` across `start`, `update`, and `end`. Using a new ID posts a separate notification instead of updating the existing one.
</Warning>

***

## Send a Live Notification

Send `start`, `update`, and `end` events with the [Create message API](/reference/create-message). Every request must:

* Set the **same `collapse_id`** so OneSignal collapses later messages onto the first one. The ID must be unique to that specific Live Notification instance.
* Target the same set of users (typically with `include_aliases.external_id`).
* Use `isAndroid: true` to restrict delivery to Android push subscriptions.

### Start

Send `event: "start"` to create the Live Notification. Initialize both static (`event_attributes`) and dynamic (`event_updates`) data.

```bash Start theme={null}
curl -X POST "https://api.onesignal.com/notifications" \
  -H "Authorization: Key YOUR_APP_API_KEY" \
  -H "Content-Type: application/json; charset=utf-8" \
  -d '{
    "app_id": "YOUR_APP_ID",
    "isAndroid": true,
    "collapse_id": "UNIQUE_LIVE_NOTIFICATION_ID",
    "data": {
      "live_notification": {
        "key": "progress",
        "event": "start",
        "event_attributes": {},
        "event_updates": {
          "current_progress": 0
        }
      }
    },
    "headings": { "en": "Start" },
    "contents": { "en": "Starting Live Notification" },
    "include_aliases": { "external_id": ["EID1", "EID2"] },
    "target_channel": "push"
  }'
```

### Update

Send `event: "update"` with new `event_updates` data. You can update as many times as you like after `start`.

```bash Update theme={null}
curl -X POST "https://api.onesignal.com/notifications" \
  -H "Authorization: Key YOUR_APP_API_KEY" \
  -H "Content-Type: application/json; charset=utf-8" \
  -d '{
    "app_id": "YOUR_APP_ID",
    "isAndroid": true,
    "collapse_id": "UNIQUE_LIVE_NOTIFICATION_ID",
    "data": {
      "live_notification": {
        "key": "progress",
        "event": "update",
        "event_updates": {
          "current_progress": 80
        }
      }
    },
    "headings": { "en": "Update" },
    "contents": { "en": "Updating Live Notification" },
    "include_aliases": { "external_id": ["EID1", "EID2"] },
    "target_channel": "push"
  }'
```

### End

Send `event: "end"` to dismiss the Live Notification. The service extension calls `NotificationManager.cancel(id)` to remove it from the user's device.

```bash End theme={null}
curl -X POST "https://api.onesignal.com/notifications" \
  -H "Authorization: Key YOUR_APP_API_KEY" \
  -H "Content-Type: application/json; charset=utf-8" \
  -d '{
    "app_id": "YOUR_APP_ID",
    "isAndroid": true,
    "collapse_id": "UNIQUE_LIVE_NOTIFICATION_ID",
    "data": {
      "live_notification": {
        "key": "progress",
        "event": "end"
      }
    },
    "headings": { "en": "Ending" },
    "contents": { "en": "Ending Live Notification" },
    "include_aliases": { "external_id": ["EID1", "EID2"] },
    "target_channel": "push"
  }'
```

***

## FAQ

### Are Android Live Notifications a managed OneSignal feature?

No. They are a pattern you implement in your `NotificationServiceExtension` on top of OneSignal push and Android's `NotificationManager`. OneSignal does not track Live Notification state, manage update lifecycles, or expose Live Notifications in the dashboard. The schema and event names in this guide are conventions you can change.

### Why do I need both `collapse_id` and a stable Android notification ID?

They solve different problems. `collapse_id` deduplicates messages on the OneSignal side and APNs/FCM side so the device only processes the latest one if updates arrive in a burst. The stable Android notification ID is what `NotificationManager.notify(id, …)` uses to replace the visible notification's contents instead of posting a new one. You need both.

### How often can I send updates?

Frequent updates are fine, but use a **low importance** channel (`IMPORTANCE_LOW`) with badges, vibration, and sound disabled. High importance with frequent updates will alert the user every time and is the most common reason support tickets get opened for this pattern.

### What happens if the user kills the app or device reboots?

The Android notification persists on the lock screen and notification shade until your app cancels it or the user dismisses it. The service extension only runs when a push arrives, so any in-memory state (like progress) must be encoded in `event_updates`, not held in app memory.

### Can I do this on iOS too?

iOS has a managed equivalent: [Live Activities](./live-activities). It uses ActivityKit and is built into OneSignal as a first-class feature with its own [Start](/reference/start-live-activity) and [Update](/reference/update-live-activity-api) APIs. Use Live Activities on iOS rather than this pattern.

***

## Related pages

<Columns cols={2}>
  <Card title="Service extensions" icon="puzzle-piece" href="./service-extensions">
    Intercept and customize push notifications before display on Android and iOS.
  </Card>

  <Card title="Create message API" icon="paper-plane" href="/reference/create-message">
    The API used to send the start, update, and end pushes for a Live Notification.
  </Card>

  <Card title="Live Activities (iOS)" icon="apple" href="./live-activities">
    The managed iOS equivalent built on ActivityKit.
  </Card>

  <Card title="Android notification categories" icon="bell" href="./android-notification-categories">
    Configure channels so frequent Live Notification updates don't alert the user.
  </Card>
</Columns>
