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 may need frequent data updates from sensors, or you may 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-alpha03"

    // To build applications that can read or write user's health and fitness records.
    implementation "androidx.health:health-connect-client:1.0.0-alpha01"
}

Kotlin

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

    // To build applications that can read or write user's health and fitness records.
    implementation("androidx.health:health-connect-client:1.0.0-alpha01")
}

In your AndroidManifest.xml file, add the following inside of the manifest tag so your app can interact with Health Services. For more information, see Package visibility.

<queries>
    <package android:name="com.google.android.wearable.healthservices" />
</queries>

Using 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. Ideally you should 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 beforehand allows you to enable or disable certain features, or modify your app's UI to compensate for capabilities that are not available.

Kotlin


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

Java


HealthServicesClient healthClient = HealthServices.getClient(this /*context*/);
ListenableFuture<MeasureCapabilities> capabilitiesFuture =
        healthClient.getCapabilities();
Futures.addCallback(capabilitiesFuture,
        new FutureCallback<Capabilities>() {
            @Override
            public void onSuccess(@Nullable Capabilities result) {
                boolean supportsHeartRate = result
                        .supportedDataTypesMeasure()
                        .contains(DataType.HEART_RATE_BPM)
            }

            @Override
            public void onFailure(Throwable t) {
                // display an error
            }
        },
        ContextCompat.getMainExecutor(this /*context*/));

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 may 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.

val heartRateCallback = object : MeasureCallback {
    override fun onAvailabilityChanged(type: DataType, availability: Availability) {
        if (availability is DataTypeAvailability) {
            // Handle availability change.
        }
    }

    override fun onData(dataPoints: List<DataPoint>) {
        // Inspect data points.
    }
}
val healthClient = HealthServices.getClient(this /*context*/)

// Register the callback.
lifecycleScope.launch {
    healthClient.measureClient
        .registerCallback(DataType.HEART_RATE_BPM, heartRateCallback)
        .await()
}

// Unregister the callback.
lifecycleScope.launch {
    healthClient.measureClient.unregisterCallback(heartRateCallback).await()
}

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 ExerciseTypes that Health Services supports.

App structure

Use the following app structure when building an exercise app with Health Services. Screens and navigation should exist within a main activity. Manage the workout state, sensor data, Ongoing Activity, and data with a foreground service. Store data with Room, and use Work Manager to upload data.

While preparing for, or during, a workout, your activity could 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 over the 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 ensure correct operation for the entire workout.

Using a ForegroundService allows you to use the Ongoing Activity API to show an indicator on your watch surfaces, allowing the user to 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.

It is recommended that you support Ambient Mode for your activities. For more information see Keep your app visible on Wear.

Check capabilities

For each ExerciseType, certain data types are supported for metrics and for exercise goals. Check these capabilities at startup as this can vary depending on the device. A device may not support a certain exercise type or it may not have functionality such as auto-pause. Additionally, the capabilities of a device could change over time, with a software update for example.

Your app should query the device capabilities on app startup and store and process the exercises that are supported by the platform, the features that are supported for each exercise (for example, auto pause), 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.

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

Inside the returned ExerciseTypeCapabilities, supportedExerciseTypes lists the DataTypes 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 where the keys are DataTypes and the values are a set of ComparisonTypes which 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 allows the user to use auto-pause or laps functionality, your app must check that these are supported by the device. Use supportsAutoPauseAndResume or supportsLaps respectively.ExerciseClient will reject requests that are not supported on the device.

// 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]
supportsStepGoals = 
    (stepGoals != null && ComparisonType.GREATER_THAN_OR_EQUAL in stepGoals)

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

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. Your listener will only receive updates about exercises your app owns.

val listener = object : ExerciseUpdateListener {
    override fun onExerciseUpdate(update: ExerciseUpdate) {
        // Process the latest information about the exercise.
        exerciseStatus = update.state // e.g. ACTIVE, USER_PAUSED, etc.
        activeDuration = update.activeDuration // Duration
        latestMetrics = update.latestMetrics // Map<DataType, List<DataPoint>>
        latestAggregateMetrics = update.latestAggregateMetrics // Map<DataType, AggregateDataPoint>
        latestGoals = update.latestAchievedGoals // Set<AchievedExerciseGoal>
        latestMilestones = update.latestMilestoneMarkerSummaries // Set<MilestoneMarkerSummary>

    }

    override fun onLapSummary(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
        }
    }
}
val exerciseClient = HealthServices.getClient(this /*context*/).exerciseClient

// Register the listener.
lifecycleScope.launch {
    exerciseClient.setUpdateListener(listener).await()
}

Manage the exercise lifetime

Health Services supports at most one exercise at a time across all apps on the device. If an app starts an exercise, it will cause any current exercise in another app to be terminated. Your app should check for other running exercises before starting your exercise, and react accordingly. For example, ask the user for confirmation before starting.

When the user launches your app, check whether your app has an existing workout running with Health Services,which it may make sense to resume. Health Services will not stop the workout automatically when your app closes, so an existing workout belonging to your app could already be in progress. If there is an active exercise owned by the current app, the app should transition to the screen for exercise tracking and continue to process exercise updates through the listener.

lifecycleScope.launch {
    val exerciseInfo = exerciseClient.currentExerciseInfo.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, there are further considerations with permissions also.

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

  • Determine which permissions are required by consulting the DataType reference docs.
  • Specify these permissions in AndroidManifest.xml and check that the user has granted the necessary permissions. For more information, see Request app permissions. Health Services will reject 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 may take a short time to warm up or the user may want to see data before starting their workout. The optional prepareExercise() method allows for sensors to warm up and data to be received, without starting the timer for the workout. This allows activeDuration to be unaffected.

Before making the call to prepareExercise() your app should run a few checks:

  • Your app should check the location setting in the platform. If it is off, notify the user that the location is not allowed and cannot be tracked in the app, and prompt for the user to enable it. Note that this is a device-wide settings check (set by the user in the main Settings menu) and is different than the app-level permissions check.
  • Confirm 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 type(s) associated with that permission from the prepare call. If neither body sensor nor location permissions are given, do not call prepare. 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 prepare call can succeed:

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

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

val dataTypes = setOf(
    DataType.HEART_RATE_BPM,
    DataType.LOCATION
)
val warmUpConfig = WarmUpConfig.Builder()
    .setDataTypes(dataTypes)
    .setExerciseType(ExerciseType.RUNNING)
    .build()
exerciseClient.prepareExercise(warmUpConfig).await()

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

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, which are triggered when a condition is met (for example, the user ran 5km). Or, an exercise milestone can be set, which will be triggered multiple times, for example, each time the user runs another 1km. The following sample shows 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.LOCATION

    )
    // Types for which we want to receive aggregate metrics.
    val aggregateDataTypes = setOf(
        DataType.DISTANCE,
        // "Total" here refers not to the aggregation but to basal + activity.
        DataType.TOTAL_CALORIES
    )

    // Create a one-time goal.
    val calorieGoal = ExerciseGoal.createOneTimeGoal(
        condition = DataTypeCondition(
            dataType = DataType.TOTAL_CALORIES,
            threshold = Value.ofDouble(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,
            threshold = Value.ofDouble(DISTANCE_THRESHOLD),
            comparisonType = ComparisonType.GREATER_THAN_OR_EQUAL
        ),
        period = Value.ofDouble(DISTANCE_THRESHOLD)
    )
    val config = ExerciseConfig.builder()
        .setExerciseType(ExerciseType.RUNNING)
        .setDataTypes(dataTypes)
        .setAggregateDataTypes(aggregateDataTypes)
        .setExerciseGoals(listOf(calorieGoal, distanceGoal))
        .setShouldEnableAutoPauseAndResume(false)
        // Required for GPS for LOCATION data type, optional for some other types.
        .setShouldEnableGps(true)
        .build()
    HealthServices.getClient(this /*context*/)
        .exerciseClient
        .startExercise(config)
        .await()
}

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

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

Pausing, resuming and ending the workout

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

Use the state from ExerciseUpdate as the source of truth. The workout is not considered paused when the call to pauseExercise() 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, your app should disable the pause button and call pauseExercise() on Health Services. The app should wait for Health Services to reach the Paused state via ExerciseUpdate.ExerciseState and then switch the button to Resume. This is because Health Services state updates may take a bit longer to be delivered than the button press, so if you tie all UI changes to the button presses it may get out of sync with the Health Services state.

Keep this in mind in the following situations:

  • Auto-pause is enabled: The workout may pause or start without user-interaction.
  • Another app starts a workout: Your workout may be terminated without user interaction.

If your app’s workout is terminated by another app, your app must gracefully handle this possible termination. If terminated, your app should:

  • 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.

Your app should also handle the case where permissions are revoked during an ongoing exercise. This would be sent via the isEnded state AUTO_ENDED_PERMISSIONS_LOST. This should be handled in a similar way to the termination case. The app should save the partial state, remove Ongoing Activity icon, and send a notification about what happened to the user.

See the following example on how check for termination correctly:

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

    ...
}

Managing Active Duration

During an exercise, an app may need to display the current active duration of the workout. The app, Health Services, and the device 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 which provides an anchor point from which the app can start its timer.

As the active duration is sent from the MCU, and may take a small time to arrive in the app, ActiveDurationCheckpoint contains two properties:

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

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

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

This will account 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 will ensure that the app’s timer is perfectly aligned with the time in Health Services and the MCU.

If the exercise is paused, the app should wait to restart the timer in the UI until the calculated time has gone past what the UI is currently displaying. This is because the pause signal may reach 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 following bullets describe how ExerciseClient delivers data:

  • Aggregate and non-aggregate data is separated into two different properties, latestAggregateMetrics and latestMetrics.
  • In any ExerciseUpdate, aggregate metrics include only the latest value for each DataType.
  • In contrast, non-aggregate metrics are lists of data points for each DataType. This represents all the samples taken since the last delivery of data.
  • ExerciseClient may batch deliveries of data, delivering only when the processor is awake, or a maximum reporting period reached, such as every 150 seconds.

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

Aggregate metrics

Aggregate metrics come in two different forms, CumulativeDataPoint and StatisticalDataPoint.

For example, DataType.DISTANCE is represented as a CumulativeDataPoint when aggregated, providing total distance. DataType.HEART_RATE_BPM is represented as a StatisticalDataPoint, providing min, max and average.

The DataTypes class provides helper methods such as isCumulativeDataType and isStatisticalDataType to help in determining how to cast each AggregateDataPoint.

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 may have accuracy information associated with each data point. This is represented in the accuracyproperty.

HrAccuracy and LocationAccuracy classes may 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 or not.

Store and upload data

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