Ongoing Activity

In Wear OS, pairing an ongoing activity with an ongoing notification adds that notification to additional surfaces within the Wear OS user interface. This allows users to stay more engaged with long-running activities.

Ongoing notifications are typically used to indicate a notification has a background task that the user is actively engaged with or is pending in some way and therefore occupying the device.

For example, a Wear OS user may use a workout app to record a run from an activity and then navigate away from that app to start some other task.

When the user navigates away from the workout app, the app will usually transition to an ongoing notification tied to some background work (for example, services or alarm managers) to keep the user informed on their run. The notification provides the user updates and an easy way to tap back into the app.

However, to view the notification, the user has to swipe into the notification tray below the watch face and find the right notification. This isn't as convenient as other surfaces.

With the Ongoing Activity API, an app's ongoing notification can expose information to multiple, new, convenient surfaces on Wear OS to keep the user engaged.

For example, in this workout app, the information can appear on the user's watch face as a tappable running icon:

running-icon

Figure 1. Activity indicator

The Recents section of the global app launcher also lists any ongoing activities:

launcher

Figure 2. Global launcher

The following are good situations to use an ongoing notification tied to an ongoing activity:

timer

Figure 3. Timer: Actively counts down time and ends when the timer is paused/stopped.

map

Figure 4. Turn by turn navigation Announces directions to a destination. Ends when the user reaches the destination or stops navigation.

music

Figure 5. Media: Plays music throughout a session. Ends two minutes after the user pauses the session.

See the Ongoing Activity codelab on GitHub for an in-depth example.

Setup

To start using the Ongoing Activity API in your app, add the following dependencies to your app's build.gradle file:

dependencies {
  implementation "androidx.wear:wear-ongoing:1.0.0-alpha05"
  // Includes LocusIdCompat and new Notification categories for Ongoing Activity.
  implementation "androidx.core:core:1.5.0-rc01"
}

Start an ongoing activity

Get started with an ongoing activity.

Ongoing notification

As called out earlier, Ongoing activities are closely related to an Ongoing Notification.

They both work together to inform users of a task the user is actively engaged with, or is pending in some way and therefore occupying the device.

You must pair an ongoing activity with an ongoing notification.

There are many benefits to linking your ongoing activity to a notification, including the following:

  • Notifications are the fallback on devices that don’t support ongoing activities. The notification is the only surface your app will show while running in the background.
  • On Android 11 and higher, Wear OS hides the notification in the notification tray when the app is visible as an ongoing activity on additional surfaces.
  • The current implementation uses the Notification itself as the communication mechanism.

Ongoing Activity

It's simple to start an ongoing activity once you have an ongoing notification.

The following code sample contains comments to help you understand what each property means:

Kotlin

var builder = NotificationCompat.Builder(this, CHANNEL_ID)
      …
      .setSmallIcon(..)
      .setOngoing(true)

val ongoingActivityStatus = OngoingActivityStatus.Builder()
    // Sets the text used across various surfaces.
    .addTemplate(mainText)
    .build()

val ongoingActivity =
    OngoingActivity.Builder(
        applicationContext, NOTIFICATION_ID, notificationBuilder
    )
        // Sets the animated icon that will appear on the watch face in
        // active mode.
        // If it isn't set, the watch face will use the static icon in
        // active mode.
        .setAnimatedIcon(R.drawable.ic_walk)
        // Sets the icon that will appear on the watch face in ambient mode.
        // Falls back to Notification's smallIcon if not set.
        // If neither is set, an Exception is thrown.
        .setStaticIcon(R.drawable.ic_walk)
        // Sets the tap/touch event, so users can re-enter your app from the
        // other surfaces.
        // Falls back to Notification's contentIntent if not set.
        // If neither is set, an Exception is thrown.
        .setTouchIntent(activityPendingIntent)
        // In our case, sets the text used for the Ongoing Activity (more
        // options are available for timers and stopwatches).
        .setStatus(ongoingActivityStatus)
        .build()

ongoingActivity.apply(applicationContext)

notificationManager.notify(NOTIFICATION_ID, builder.build())

Java

NotificationCompat.Builder builder = NotificationCompat.Builder(this, CHANNEL_ID)
      …
      .setSmallIcon(..)
      .setOngoing(true);

OngoingActivityStatus ongoingActivityStatus = OngoingActivityStatus.Builder()
    // Sets the text used across various surfaces.
    .addTemplate(mainText)
    .build();

OngoingActivity ongoingActivity =
    OngoingActivity.Builder(
        applicationContext, NOTIFICATION_ID, notificationBuilder
    )
        // Sets the animated icon that will appear on the watch face in
        // active mode.
        // If it isn't set, the watch face will use the static icon in
        // active mode.
        .setAnimatedIcon(R.drawable.ic_walk)
        // Sets the icon that will appear on the watch face in ambient mode.
        // Falls back to Notification's smallIcon if not set.
        // If neither is set, an Exception is thrown.
        .setStaticIcon(R.drawable.ic_walk)
        // Sets the tap/touch event, so users can re-enter your app from the
        // other surfaces.
        // Falls back to Notification's contentIntent if not set.
        // If neither is set, an Exception is thrown.
        .setTouchIntent(activityPendingIntent)
        // In our case, sets the text used for the Ongoing Activity (more
        // options are available for timers and stopwatches).
        .setStatus(ongoingActivityStatus)
        .build();

ongoingActivity.apply(applicationContext);

notificationManager.notify(NOTIFICATION_ID, builder.build());

The following steps call out the most important part of the previous example:

  1. Call .setOngoing(true) on the NotificationCompat.Builder and set any optional fields.

  2. Create an OngoingActivityStatus to represent the text. (Other status options are covered in the next section.)

  3. Create an OngoingActivity and set a notification ID (required).

  4. Call apply() on OngoingActivity with the context.

  5. Call notificationManager.notify() and pass in the same notification ID as the ongoing activity to tie them together.

Status

The Status allows developers to expose the current, live status of the OngoingActivity to the user on new surfaces, like the Recents section of the launcher. To use the feature, use the Status.Builder.

In most cases, developers need only to add a template that represents the text they want to appear in the Recents section of the app launcher.

Developers can customize how text appears with Spans using the addTemplate() method and specifying any dynamic parts of the text as a Status.Part.

The following example shows how to make the word "time" appear in red. The example uses a Status.TimerPart which allows us to represent a timer or Status.StopwatchPart to represent a stopwatch in the Recents section of the app launcher.

Kotlin

val htmlStatus =
        "<p>The <font color=\"red\">time</font> on your current #type# is #time#.</p>"

val statusTemplate =
        Html.fromHtml(
                htmlStatus,
                Html.FROM_HTML_MODE_COMPACT
        )

// Creates a 5 minute timer.
// Note the use of SystemClock.elapsedRealtime(), not System.currentTimeMillis()
val runStartTime = SystemClock.elapsedRealtime() + TimeUnit.MINUTES.toMillis(5)

val status = new Status.Builder()
   .addTemplate(statusTemplate)
   .addPart("type", Status.TextPart("run"))
   .addPart("time", Status.StopwatchPart(runStartTime)
   .build()

Java

String htmlStatus =
        "<p>The <font color=\"red\">time</font> on your current #type# is #time#.</p>";

Spanned statusTemplate =
        Html.fromHtml(
                htmlStatus,
                Html.FROM_HTML_MODE_COMPACT
        );

// Creates a 5 minute timer.
// Note the use of SystemClock.elapsedRealtime(), not System.currentTimeMillis()
Long runStartTime = SystemClock.elapsedRealtime() + TimeUnit.MINUTES.toMillis(5);

Status status = new Status.Builder()
   .addTemplate(statusTemplate)
   .addPart("type", new Status.TextPart("run"))
   .addPart("time", new Status.StopwatchPart(runStartTime)
   .build();

To reference a part from the template, use the name surrounded by ‘#’. To produce ‘#’ in the output, use ‘##’ in the template.

The previous example uses HTMLCompat to generate a CharSequence to pass to the template, which is easier than manually defining a Spannable object.

Additional customizations

Besides Status, developers can customize their ongoing activity or notifications in the following ways. These customizations may or may not be used based on the OEM's implementation.

Ongoing Notification

  • The category set determines the priority of the ongoing activity.
    • CATEGORY_CALL: An incoming call (voice or video) or similar synchronous communication request
    • CATEGORY_NAVIGATION: A map or turn-by-turn navigation.
    • CATEGORY_TRANSPORT: Media transport control for playback
    • CATEGORY_ALARM: An alarm or timer
    • CATEGORY_WORKOUT: A workout (new)
    • CATEGORY_LOCATION_SHARING: Temporary location sharing (new)
    • CATEGORY_STOPWATCH: Stopwatch (new) \

Ongoing Activity * Animated Icon: A black and white vector, preferably with a transparent background. Displays on the watch face during active mode. If this is not provided, the default notification icon is used.

  • Static icon: A vector icon with transparent background. Displays on the watch face in ambient mode. If the animated icon isn't set, the static icon is used on the watch face for active mode. If this is not provided, the notification icon is used. If neither is set an exception is thrown. (The icon used in the app launcher will still use the app icon.)

  • OngoingActivityStatus: Plain text or a Chronometer. Displays in the Recents section of the app launcher. If not provided, the notification “context text” is used.

  • Touch Intent: A PendingIntent used to switch back to the app if the user taps on the ongoing activity icon. Displays on the watch face or on the launcher item. It can be different from the original intent used to launch the app. If not provided, the notification’s content intent is used, if neither is set an exception is thrown. \

  • LocusId: An ID that assigns the launcher shortcut that the ongoing activity corresponds to. Displays on the launcher in the Recents section while the activity is ongoing. If not provided, the launcher will hide all app items in the Recents section from the same package and only show the ongoing activity. \

  • Ongoing Activity ID, An ID used to disambiguate calls to fromExistingOngoingActivityfromExistingOngoingActivity() when an application has more than one ongoing activity.

Update an ongoing activity

In most cases, developers will create a new ongoing notification and a new ongoing activity when they need to update the data on the screen. However, the Ongoing Activity API also offers helper methods to update an OngoingActivity if you wish to retain an instance rather than recreate it.

If the app is running in the background, it can send updates to the Ongoing Activity API, but this should be infrequent. The update method may ignore calls that are too close to each other. A few updates per minute is reasonable.

To update the ongoing activity and the posted notification, use the object you created before and call update() as shown in the following example:

Kotlin

ongoingActivity.update(context, newStatus)

Java

ongoingActivity.update(context, newStatus);

As a convenience, there is a static method to create an ongoing activity.

Kotlin

OngoingActivity.recoverOngoingActivity(context)
               .update(context, newStatus)

Java

OngoingActivity.recoverOngoingActivity(context)
               .update(context, newStatus);

Stop an ongoing activity

When the app is finished running as an ongoing activity, it only needs to cancel the ongoing notification.

It’s up to the app if it wants to cancel the notification or ongoing activity when it comes to the foreground, then recreate them when going back into the background.

Best practices

Remember the following things when working with the Ongoing Activity API:

  • Always call ongoingActivity.apply(context) before calling notificationManager.notify(...).
  • Always set a static icon for your Ongoing Activity either explicitly or as a fallback via the notification. If you don't, you will get a IllegalArgumentException.

  • Icons should be black and white vectors with a transparent background.

  • Always set a touch intent for your ongoing activity either explicitly or as a fallback using the notification. If you don't, you will get an IllegalArgumentException.

  • For NotificationCompat, use the core androidx library core:1.5.0-alpha05+ which includes the LocusIdCompat and new categories (workout, stopwatch, or location sharing).

  • If your app has more than one MAIN LAUNCHER activity declared in the manifest, publish a dynamic shortcut and associate it with your ongoing activity using LocusId.