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:

dependencies {
  implementation 'androidx.health:health-services-client:1.0.0-alpha02'
  // For Kotlin, this library helps bridge between Futures and coroutines.
  implementation "androidx.concurrent:concurrent-futures-ktx:1.1.0"
}

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.

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 your device.

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.

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.

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

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. Exercise goals consist of a DataType and a condition. Exercise goals are triggered either once a condition is met (a one-time goal), or each time an amount is added (a milestone goal). 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.

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.

See the following example on how to terminate a workout 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
        }
        ...
    }

    ...
}

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.

App structure

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.

It is recommended that you use a ForegroundService in conjuction 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.