Active data and exercises

The Wear OS form factor is well suited for situations when other form factors are less desirable, such as during exercise. In these situations, your app might need frequent data updates from sensors or might be actively helping the user manage a workout. Health Services provides APIs that make it easier to develop these types of experiences.

See the Exercise sample on GitHub.

Add dependencies

To add a dependency on Health Services, you must add the Google Maven repository to your project. For more information, see Google's Maven repository.

Then, in your module-level build.gradle file, add the following dependency:

Groovy

dependencies {
    implementation "androidx.health:health-services-client:1.0.0-beta03"
}

Kotlin

dependencies {
    implementation("androidx.health:health-services-client:1.0.0-beta03")
}

Use MeasureClient

With the MeasureClient APIs, your app registers callbacks to receive data for as long as you need. This is meant for situations in which your app is in use and requires rapid data updates. If possible, create this with a foreground UI so that the user is aware.

Check capabilities

Before registering for data updates, check that the device can provide the type of data your app needs. Checking capabilities first lets you enable or disable certain features or modify your app's UI to compensate for capabilities that are not available.

The following example shows how to check whether a device can provide the HEART_RATE_BPM data type:

val healthClient = HealthServices.getClient(this /*context*/)
val measureClient = healthClient.measureClient
lifecycleScope.launch {
    val capabilities = measureClient.getCapabilitiesAsync().await()
    supportsHeartRate = DataType.HEART_RATE_BPM in capabilities.supportedDataTypesMeasure
}

Register for data

Each callback you register is for a single data type. Note that some data types might have varying states of availability. For example, heart rate data might not be available when the device is not properly attached to the wrist.

It's important to minimize the amount of time that your callback is registered, as callbacks cause an increase in sensor sampling rates, which in turn increases power consumption.

The following example shows how to register and unregister a callback to receive HEART_RATE_BPM data:

val heartRateCallback = object : MeasureCallback {
    override fun onAvailabilityChanged(dataType: DeltaDataType<*, *>, availability: Availability) {
        if (availability is DataTypeAvailability) {
            // Handle availability change.
        }
    }

    override fun onDataReceived(data: DataPointContainer) {
        // Inspect data points.
    }
}
val healthClient = HealthServices.getClient(this /*context*/)
val measureClient = healthClient.measureClient

// Register the callback.
measureClient.registerMeasureCallback(DataType.Companion.HEART_RATE_BPM, heartRateCallback)

// Unregister the callback.
awaitClose {
    runBlocking {
        measureClient.unregisterMeasureCallbackAsync(DataType.Companion.HEART_RATE_BPM, heartRateCallback)
    }
}

Use ExerciseClient

Health Services provides first-class support for workout apps through the ExerciseClient. With ExerciseClient, your app can control when an exercise is in progress, add exercise goals, and get updates about the exercise state or other desired metrics. For more information, see the full list of exercise types that Health Services supports.

App structure

Use the following app structure when building an exercise app with Health Services. Keep your screens and navigation within a main activity. Manage the workout state, sensor data, ongoing activity, and data with a foreground service. Store data with Room, and use WorkManager to upload data.

When preparing for a workout and during the workout, your activity might be stopped for a variety of reasons. The user might switch to another app or return to the watch face. The system might display something on top of your activity, or the screen might turn off after a period of inactivity. Use a continuously running ForegroundService in conjunction with ExerciseClient to help ensure correct operation for the entire workout.

Using a ForegroundService lets you use the Ongoing Activity API to show an indicator on your watch surfaces, letting the user quickly return to the workout.

Using a ForegroundService is essential when requesting location data. Your manifest file must specify foregroundServiceType="location" and specify the appropriate permissions.

We recommend that you use AmbientModeSupport for your pre-workout activity that contains the prepare call, but not to display metrics during the workout. This is because Health Services batches workout data when the device screen is in ambient mode to save power, so the information displayed may not be recent.

Check capabilities

Each ExerciseType supports certain data types for metrics and for exercise goals. Check these capabilities at startup, because they can vary depending on the device. A device might not support a certain exercise type, or it might not have functionality, such as auto-pause. Additionally, the capabilities of a device might change over time, such as after a software update.

Query the device capabilities on app startup and store and process all of the following: the exercises that are supported by the platform, the features that are supported for each exercise, the data types supported for each exercise, and the permissions required for each of those data types.

Use ExerciseCapabilities.getExerciseTypeCapabilities() with your desired exercise type to see what kind of metrics you can request, what exercise goals you can configure, and what other features are available for that type. This is shown in the following example:

val healthClient = HealthServices.getClient(this /*context*/)
val exerciseClient = healthClient.exerciseClient
lifecycleScope.launch {
    val capabilities = exerciseClient.getCapabilitiesAsync().await()
    if (ExerciseType.RUNNING in capabilities.supportedExerciseTypes) {
        runningCapabilities =
            capabilities.getExerciseTypeCapabilities(ExerciseType.RUNNING)
    }
}

Inside the returned ExerciseTypeCapabilities, supportedDataTypes lists the data types that you can request data for. This varies by device, so take care not to request a DataType that isn't supported, or your request might fail.

The supportedGoals and supportedMilestones fields are maps. The keys are DataType objects and the values are sets of ComparisonType objects that you can use with the associated DataType. Use these to determine whether the exercise can support an exercise goal that you want to create.

If your app lets the user use auto-pause, you must check that this functionality is supported by the device using supportsAutoPauseAndResume. ExerciseClient rejects requests that are not supported on the device.

The following example checks the support for the HEART_RATE_BPM data type, the STEPS_TOTAL goal capability, and the auto-pause functionality:

// Whether we can request heart rate metrics.
supportsHeartRate = DataType.HEART_RATE_BPM in runningCapabilities.supportedDataTypes

// Whether we can make a one-time goal for aggregate steps.
val stepGoals = runningCapabilities.supportedGoals[DataType.STEPS_TOTAL]
supportsStepGoals = 
    (stepGoals != null && ComparisonType.GREATER_THAN_OR_EQUAL in stepGoals)

// Whether auto-pause is supported.
val supportsAutoPause = runningCapabilities.supportsAutoPauseAndResume

Register for exercise state updates

Exercise updates are delivered to a listener. Your app can only register a single listener at a time. Set up your listener before starting the workout, as shown in the following example. Your listener only receives updates about exercises your app owns.

val callback = object : ExerciseUpdateCallback {
    override fun onExerciseUpdateReceived(update: ExerciseUpdate) {
        val exerciseStateInfo = update.exerciseStateInfo
        val activeDuration = update.activeDurationCheckpoint
        val latestMetrics = update.latestMetrics
        val latestGoals = update.latestAchievedGoals
    }

    override fun onLapSummaryReceived(lapSummary: ExerciseLapSummary) {
        // For ExerciseTypes that support laps, this is called when a lap is marked.
    }

    override fun onAvailabilityChanged(
        dataType: DataType<*, *>,
        availability: Availability
    ) {
        // Called when the availability of a particular DataType changes.
        when {
            availability is LocationAvailability -> // Relates to Location/GPS.
            availability is DataTypeAvailability -> // Relates to another DataType.
        }
    }
}
exerciseClient.setUpdateCallback(callback)

Manage the exercise lifetime

Health Services supports, at most, one exercise at a time across all apps on the device. If an exercise is being tracked and a different app starts tracking a new exercise, the first exercise terminates.

Before starting your exercise, do the following:

  1. Check whether an exercise is already being tracked, and react accordingly. For example, ask the user for confirmation before overriding a previous exercise and starting to track a new one.
  2. Check whether another app has terminated your exercise, and react accordingly. For example, explain that another app has taken over tracking and that your exercise has stopped.

The following example shows how to check for an existing exercise with getCurrentExerciseInfoAsync:

lifecycleScope.launch {
    val exerciseInfo = exerciseClient.getCurrentExerciseInfoAsync().await()
    when (exerciseInfo.exerciseTrackedStatus) {
        OTHER_APP_IN_PROGRESS -> // Warn user before continuing, will stop the existing workout.
        OWNED_EXERCISE_IN_PROGRESS -> // This app has an existing workout.
        NO_EXERCISE_IN_PROGRESS -> // Start a fresh workout.
    }
}

Permissions

When using ExerciseClient, make sure your app requests and maintains the necessary permissions. If your app uses LOCATION data, make sure your app requests and maintains the appropriate permissions for that as well.

For all data types, before calling prepareExercise() or startExercise(), do the following:

  • Specify the appropriate permissions for the requested datatypes in your AndroidManifest.xml file.
  • Verify that the user has granted the necessary permissions. For more information, see Request app permissions. Health Services rejects the request if the necessary permissions are not already granted.

For location data, do the following additional steps:

Prepare for a workout

Some sensors, like GPS or heart rate, might take a short time to warm up, or the user might want to see their data before starting their workout. The optional prepareExerciseAsync() method lets sensors warm up and data be received without starting the timer for the workout. The activeDuration is not affected by this preparation time.

Before making the call to prepareExerciseAsync(), check the following:

  • Check the location setting in the platform. If it is off, notify the user that the location is not allowed and can't be tracked in the app, and prompt for the user to enable it. This is a device-wide setting the user controls in the main Settings menu; it is different than the app-level permissions check.
  • Confirm that your app has runtime permissions for body sensors, activity recognition, and fine location. For missing permissions, prompt the user for runtime permissions, providing adequate context. If the user does not grant a specific permission, remove the data types associated with that permission from the call to prepareExerciseAsync(). If neither body sensor nor location permissions are given, do not call prepareExerciseAsync(). The app can still get step-based distance, pace, speed, and other metrics that do not require those permissions.

Do the following to ensure that your call to prepareExerciseAsync() can succeed:

  • Use AmbientModeSupport for the pre-workout activity that contains the prepare call.
  • Call prepareExerciseAsync() from your foreground service. If it is not in a service and is tied to the activity lifecycle, then the sensor preparation might be unnecessarily killed.
  • Call endExercise() to turn off the sensors and reduce power usage if the user navigates away from the pre-workout activity.

The following example shows how to call prepareExerciseAsync():

val warmUpConfig = WarmUpConfig(
    ExerciseType.RUNNING,
    setOf(
        DataType.HEART_RATE_BPM,
        DataType.LOCATION
    )
)

exerciseClient.prepareExerciseAsync(warmUpConfig).await()

// Data and availability updates are delivered to the registered listener.

Once the app is in the PREPARING state, sensor availability updates are delivered in the ExerciseUpdateCallback through onAvailabilityChanged(). This information can then be presented to the user so they can decide whether to start their workout.

Start the workout

When you want to start an exercise, create an ExerciseConfig to configure the exercise type, the data types for which you want to receive metrics, and any exercise goals or milestones.

Exercise goals consist of a DataType and a condition. Exercise goals are a one-time goal that are triggered when a condition is met, such as when the user runs a certain distance. An exercise milestone can also be set. Exercise milestones can be triggered multiple times, such as each time the user runs a certain point past their set distance.

The following sample shows how to create one goal of each type:

const val CALORIES_THRESHOLD = 250.0
const val DISTANCE_THRESHOLD = 1_000.0 // meters

suspend fun startExercise() {
    // Types for which we want to receive metrics.
    val dataTypes = setOf(
        DataType.HEART_RATE_BPM,
        DataType.CALORIES_TOTAL,
        DataType.DISTANCE
    )

    // Create a one-time goal.
    val calorieGoal = ExerciseGoal.createOneTimeGoal(
        DataTypeCondition(
            dataType = DataType.CALORIES_TOTAL,
            threshold = CALORIES_THRESHOLD,
            comparisonType = ComparisonType.GREATER_THAN_OR_EQUAL
        )
    )

    // Create a milestone goal. To make a milestone for every kilometer, set the initial
    // threshold to 1km and the period to 1km.
    val distanceGoal = ExerciseGoal.createMilestone(
        condition = DataTypeCondition(
            dataType = DataType.DISTANCE_TOTAL,
            threshold = DISTANCE_THRESHOLD,
            comparisonType = ComparisonType.GREATER_THAN_OR_EQUAL
        ),
        period = DISTANCE_THRESHOLD
    )

    val config = ExerciseConfig(
        exerciseType = ExerciseType.RUNNING,
        dataTypes = dataTypes,
        isAutoPauseAndResumeEnabled = false,
        isGpsEnabled = true,
        exerciseGoals = mutableListOf<ExerciseGoal<Double>>(calorieGoal, distanceGoal)
    )
    exerciseClient.startExerciseAsync(config).await()
}

For certain exercise types, you can also mark laps. Health Services provides an ExerciseLapSummary with metrics aggregated over the lap period.

The previous example shows the use of isGpsEnabled, which must be true when requesting location data. However, using GPS can also assist with other metrics. If the ExerciseConfig specifies distance as a DataType, this defaults to using steps to estimate distance. By optionally enabling GPS, location information can be used instead to estimate distance.

Pause, resume, and end a workout

Workouts can be paused, resumed, and ended using the appropriate method, such as pauseExerciseAsync() or endExerciseAsync().

Use the state from ExerciseUpdate as the source of truth. The workout is not considered paused when the call to pauseExerciseAsync() returns, but instead when that state is reflected in the ExerciseUpdate message. This is especially important to consider when it comes to UI states. If the user presses pause, disable the pause button and call pauseExerciseAsync() on Health Services. Wait for Health Services to reach the paused state using ExerciseUpdate.exerciseStateInfo.state, and then switch the button to resume. This is because Health Services state updates can take longer to be delivered than the button press, so if you tie all UI changes to button presses the UI can get out of sync with the Health Services state.

Keep this in mind in the following situations:

  • Auto-pause is enabled: the workout can pause or start without user interaction.
  • Another app starts a workout: your workout might be terminated without user interaction.

If your app’s workout is terminated by another app, your app must gracefully handle the termination:

  • Save the partial workout state so that a user’s progress is not erased.
  • Remove the Ongoing Activity icon and send the user a notification letting them know that their workout was ended by another app.

Also, handle the case where permissions are revoked during an ongoing exercise. This is sent using the isEnded state, with an ExerciseEndReason of AUTO_END_PERMISSION_LOST. Handle this case in a similar way to the termination case: save the partial state, remove the Ongoing Activity icon, and send a notification about what happened to the user.

The following example shows how to check for termination correctly:

val callback = object : ExerciseUpdateCallback {
    override fun onExerciseUpdateReceived(update: ExerciseUpdate) {
        if (update.exerciseStateInfo.state.isEnded) {
            // Workout has either been ended by the user, or otherwise terminated
        }
        ...
    }
    ...
}

Manage active duration

During an exercise, an app can display the active duration of the workout. The app, Health Services, and the device Micro Controller Unity (MCU)—the low-power processor responsible for exercise tracking—all need to be in sync, with the same current active duration. To help manage this, Health Services sends an ActiveDurationCheckpoint that provides an anchor point from which the app can start its timer.

Because the active duration is sent from the MCU and can take a small amount of time to arrive in the app, ActiveDurationCheckpoint contains two properties:

  • activeDuration: how long the exercise has been active for
  • time: when the active duration was calculated

Therefore, in the app the active duration of an exercise can be calculated from ActiveDurationCheckpoint using the following equation:

(now() - checkpoint.time) + checkpoint.activeDuration

This accounts for the small delta between active duration being calculated on the MCU and arriving at the app. This can be used to seed a chronometer in the app and help ensure the app’s timer is perfectly aligned with the time in Health Services and the MCU.

If the exercise is paused, the app waits to restart the timer in the UI until the calculated time has gone past what the UI currently displays. This is because the pause signal reaches Health Services and the MCU with a slight delay. For example, if the app is paused at t=10 seconds, Health Services might not deliver the PAUSED update to the app until t=10.2 seconds.

Work with data from ExerciseClient

Metrics for the data types your app has registered for are delivered in ExerciseUpdate messages.

The processor delivers messages only when awake or when a maximum reporting period is reached, such as every 150 seconds. Do not rely on the ExerciseUpdate frequency to advance a chronometer with the activeDuration. See the Exercise sample on GitHub for an example of how to implement an independent chronometer.

When a user starts a workout, ExerciseUpdate messages can be delivered frequently, such as every second. As the user starts the workout, the screen might turn off. Health Services can then deliver data less often, but still sampled at the same frequency, to avoid waking the main processor. When the user looks at the screen, any data in the process of being batched is immediately delivered to your app.

Timestamps

The point-in-time of each data point represents the duration since the device booted. To convert this to a timestamp, do the following:

val bootInstant =
    Instant.ofEpochMilli(System.currentTimeMillis() - SystemClock.elapsedRealtime())

This value can then be used with getStartInstant() or getEndInstant() for each data point.

Data accuracy

Some data types can have accuracy information associated with each data point. This is represented in the accuracy property.

HrAccuracy and LocationAccuracy classes can be populated for the HEART_RATE_BPM and LOCATION data types, respectively. Where present, use the accuracy property to determine whether each data point is of sufficient accuracy for your application.

Store and upload data

Use Room to persist data delivered from Health Services. Data upload happens at the end of the exercise using a mechanism like Work Manager. This ensures that network calls to upload data are deferred until the exercise is over, minimizing power consumption during the exercise and simplifying the work.