Engage Wear OS users in new ways with the Ongoing Activity API

1. Introduction

a3457956a1735674.gif

Watch the animation above (Ongoing Activity demo). Note: Animated gifs only animate through one time. If you missed the animation, reload the page.

From the watch face, you will notice an animated icon appears (for a timer) and if a user taps on it, it launches the app powering the timer.

An Ongoing Activity is a new feature in Wear OS that allows an Ongoing Notification to appear on additional surfaces within the Wear OS user interface which 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 (for example, playing music) or is pending in some way and therefore occupying the device (for example, a file download, sync operation, or an active network connection).

For example, a Wear OS user may use a timer app to time an event and then navigate away from that app to start some other task (review the weather, start a workout, etc.).

When the user navigates away from the timer app, the app will generally transition to an Ongoing Notification tied to some background work (services, alarm managers, etc.) to keep the user informed of how much time is left on their timer.

The user can view the Notification for updates and can interact with that Notification as well.

However, to view the Notification, the user will still have to swipe into the notification tray below the watch face and find the right Notification which isn't as convenient as other surfaces.

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

In the case of the Timer app, the information can appear on the user's watch face as a tappable icon (activity indicator at the bottom of screenshot):

b65ade9bf43c526e.png

The Timer app can also appear in the Recents section of the global app launcher which lists any Ongoing Activities:

1bc84dd2bd2d2b5d.png

Global Launcher

Best of all, you only need to add about 10 lines of code to do it!

What you will learn

  • Create and customize an Ongoing Activity
  • Tie an Ongoing Activity to an Ongoing Notification
  • Test an Ongoing Activity on a device/emulator
  • Add interaction to your Ongoing Activity (tap)

What you will build

You'll extend an existing Walk Tracking App with a custom Ongoing Activity that displays the Walking Points for an active walk on the main watch face and in the Recents section of the app launcher.

Prerequisites

2. Getting set up

In this step, you will set up your environment and download a starter project.

What you will need

  • Android Studio
  • Wear OS emulator (API level >= 30)
  • Ongoing Activities are only available on API level 30 or greater
  • New to emulators? Here's how to set it up.

Download code

If you have git installed, you can simply run the command below to clone the code from this repo. To check whether git is installed, type git --version in the terminal or command line and verify that it executes correctly.

git clone https://github.com/android/codelab-ongoing-activity.git
cd ongoing-activity

If you do not have git, you can click the following button to download all the code for this codelab:

You can run either module in Android Studio at any time by changing the run configuration in the toolbar.

b059413b0cf9113a.png

Open project in Android Studio

  1. On the Welcome to Android Studio window select Open
  2. Select the folder [Download Location]
  3. When Android Studio has imported the project, test that you can run the start and finished modules on a Wear OS emulator or physical device.
  4. The start module should look like the screenshot below. It's where you will be doing all your work.

a40cc0c7e7e28bcf.png

Try out the app by starting a walking workout. You should notice that you start earning points every ~3 seconds. (The app uses mock data, so you don't have to actually walk around.)

Try swiping out of the app. If you navigate below the watch face to the navigation tray, you should be able to find the Ongoing Notification that continues tracking your walking points.

Tapping into it looks like this:

ac48b3ddc2a093bb.png36fed11735ef4807.png

Feel free to stop the walk.

By the end of this code lab, the same walking information will appear on the watch face and in the Recents section of the global app launcher.

App showing up on the watch face (see Activity Indicator at the bottom):

59747518d70b053a.png

App showing up in Recents section of the App launcher:

4817a55e6722629d.png

Explore the start code

In the start module:

  • build.gradle contains a basic app configuration. It includes the dependencies necessary to create an Ongoing Activity.
  • manifest > AndroidManifest.xml includes the parts necessary to mark this as a Wear OS application.
  • java > ... > data > WalkingWorkoutsRepository.kt links to the WalkingWorkoutsDataStore.kt class to store walking points and status of a walk workout. Don't worry about the details, you won't need to review these classes for this code lab.
  • java > ... > MainApplication.kt creates a singleton of the repository. Don't worry about the details, you won't need to review these classes for this code lab.
  • java > ... > ForegroundOnlyWalkingWorkoutService.kt contains code for starting and stopping a walking workout. If a workout is active and the user navigates away from the app, it will unbind from the Activity and start an Ongoing Notification to keep the user informed of their workout points (using mocked data). This is where we will add our Ongoing Activity code (near the Notification code).
  • java > ... > MainActivity.kt contains a UI for the user to start/stop a walking workout. The activity binds to the service above to let that handle all the workout tasks.
  • java > ... > MainViewModel.kt is a simple ViewModel to handle non-UI code in MainActivity.kt. Don't worry about the details, you won't need to review these classes for this code lab.

3. Review the app

The app is already a functioning Walking Workout app.

As you've seen in earlier steps, you can launch the app and start/stop a walking workout. As time passes during an active workout, you accrue walking points.

Points are reset for each new walk.

If you navigate away from the app during an active workout, you can swipe down to see an Ongoing Notification that continues to update you on your progress.

From that Notification, you can either stop the workout from the Notification or open the app.

Normally, you would calculate the walking points from location and sensor data with a proprietary algorithm. In our case, we are just mocking the data to keep things simple.

How does it work?

The MainActivity creates the user interface, links to a ViewModel for updates on workout status / walking points, and binds to a service.

When a workout starts or stops, the MainActivity calls a start or stop equivalent method in the Service to handle the heavy lifting of tracking the workout.

If the user navigates away, the MainActivity simply unbinds from the service.

Most of the magic happens in the ForegroundOnlyWalkingWorkoutService. This is the class that starts/stops the workouts and saves any changes in status or walking points to the repository.

The service also transitions to a foreground service and ties itself to an Ongoing Notification if the user navigates away from the MainActivity during a session.

The Ongoing Notification carries on the work of tracking a workout and is tied to the service above as a Foreground Service.

Don't worry if you don't understand all the details. The important thing is to understand this is a functioning app with its Notification already built out. We just want to extend that Notification to be visible on more surfaces with an Ongoing Activity.

That Notification code is in the ForegroundOnlyWalkingWorkoutService , and that is where you do all the work for this code lab.

4. Create an Ongoing Activity

Review the dependencies

In this step, we won't be coding. Instead we will review the Ongoing Activity dependencies.

Open the app/build.gradle file in the start module and search for "TODO: Review dependencies for Ongoing Activity".

You should see this:

Step 1

    // TODO: Review dependencies for Ongoing Activity.
    implementation libs.androidx.wear.ongoing
    // Includes LocusIdCompat and new Notification categories for Ongoing Activity.
    implementation libs.androidx.core.ktx

The first dependency is required to use the Wear OS Ongoing Activity API.

The second dependency is to get the latest features of the Notification APIs that support various features used in conjunction with an Ongoing Activity. The following two features can apply to Ongoing Notifications and therefore can also be applied to Ongoing Activities:

  • Category - Android uses some pre-defined system-wide categories to determine whether to disturb the user with a given notification when the user has enabled Do Not Disturb mode. The Category determines the priority of the Ongoing Activity on the watch face. New categories were added recently to support Wear.
  • LocusId - Locus is a new concept introduced in Android 10 (API Level 29) and lets the Android system correlate state between different subsystems such as content capture, shortcuts, and notifications. If you have multiple launchers, you can use the locus id to tie your Ongoing Activity to a specific dynamic Shortcut, so it appears properly in the Recents section of the app launcher.

Review the Ongoing Notification code

In this step, we won't be doing any coding. Instead, we will just review the Notification code.

Open the ForegroundOnlyWalkingWorkoutService.kt file in the start module and search for "TODO: Review Notification builder code".

You should see this:

Step 2

// TODO: Review Notification builder code.
val notificationBuilder = notificationCompatBuilder
    .setStyle(bigTextStyle)
    .setContentTitle(titleText)
    .setContentText(mainText)
    .setSmallIcon(R.mipmap.ic_launcher)
    .setDefaults(NotificationCompat.DEFAULT_ALL)
    // Makes Notification an Ongoing Notification (a Notification with a background task).
    .setOngoing(true)
    // Android uses some pre-defined system-wide categories to determine whether to
    // disturb the user with a given notification when the user has enabled Do Not Disturb
    // mode. The Category determines the priority of the Ongoing Activity and new
    // categories were added recently to support Wear
    .setCategory(NotificationCompat.CATEGORY_WORKOUT)
    .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
    .addAction(
        R.drawable.ic_walk, getString(R.string.launch_activity),
        activityPendingIntent
    )
    .addAction(
        R.drawable.ic_cancel,
        getString(R.string.stop_walking_workout_notification_text),
        servicePendingIntent
    )

// TODO: Create an Ongoing Activity.
// SKIP TODO FOR REVIEW STEP

return notificationBuilder.build()

Review the code above and read the comments (skip the later TODO section, that's for a later step).

There is much more Notification code above this chunk that sets everything up for this builder.

However, for this codelab, we just need to focus on the setOngoing() call and setCategory() in the notification builder.

The Category helps Wear OS determine the priority of the Notification for the watch face.

The setOngoing() call makes our Notification an Ongoing Notification, that is, a Notification with a background task that the user is actively engaged with, for example, tracking a walking workout.

We create this Notification when the user has an active walking workout and navigates away from the MainActivity.

Create an Ongoing Activity

An Ongoing Activity must be tied to an Ongoing Notification. We have our Ongoing Notification, so now let's create an Ongoing Activity.

Search for "TODO: Create an Ongoing Activity" and replace the "// SKIP TODO FOR REVIEW STEP" line with the code below.

Step 4

// TODO: Create an Ongoing Activity.
val ongoingActivityStatus = Status.Builder()
    // Sets the text used across various surfaces.
    .addTemplate(mainText)
    .build()

val ongoingActivity =
    OngoingActivity.Builder(applicationContext, NOTIFICATION_ID, notificationBuilder