Passive data updates are suited for apps that need to monitor Health Services
data in the background. This is meant for long-lived experiences where data
updates can be infrequent and spread over time. You can use passive data updates
when your app isn't in use or running at the time the update is sent. With
these APIs, you can chooose to receive all data points, or only data points
related to a particular DataType
.
See the Passive Data and Passive Goals samples 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" }
Kotlin
dependencies { implementation("androidx.health:health-services-client:1.0.0-alpha03") }
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>
Check capabilities
Before registering for data updates, check that the device can provide the type of background 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 passiveMonitoringClient = healthClient.passiveMonitoringClient lifecycleScope.launchWhenCreated { val capabilities = passiveMonitoringClient.capabilities.await() // Supported types for passive data collection supportsHeartRate = DataType.HEART_RATE_BPM in capabilities.supportedDataTypesPassiveMonitoring // Supported types for PassiveGoals supportsStepsGoal = DataType.STEPS in capabilities.supportedDataTypesEvents }
Java
HealthServicesClient healthClient = HealthServices.getClient(this /*context*/); PassiveMonitoringClient passiveClient = healthClient.getPassiveMonitoringClient(); ListenableFuture<PassiveMonitoringCapabilities> capabilitiesFuture = passiveClient.getCapabilities(); Futures.addCallback(capabilitiesFuture, new FutureCallback<PassiveMonitoringCapabilities>() { @Override public void onSuccess(@Nullable PassiveMonitoringCapabilities result) { supportsHeartRate = result .getSupportedDataTypesPassiveMonitoring() .contains(DataType.HEART_RATE_BPM) supportsStepsEvent = result .supportedDataTypesEvents() .contains(DataType.STEPS) } @Override public void onFailure(Throwable t) { // display an error } }, ContextCompat.getMainExecutor(this /*context*/));
Receive passive data
To receive data updates in the background, use the PassiveMonitoringClient
.
Your app must have a BroadcastReceiver
declared in its AndroidManifest.xml
.
When you register to receive updates from Health Services, they will be
delivered to this receiver.
Health Services batches updates, so you may receive data points of different types, or multiple data points of the same type. Also, data points and user activity states may reflect events that occurred in the past. For example, it can take some time to detect that the user is asleep, and the user activity update is published retroactively. Use the timestamps included in these objects to properly evaluate them.
Inside onReceive
, unpack the data using
PassiveMonitoringUpdate.fromIntent(intent)
. The returned
PassiveMonitoringUpdate
contains a list of data points for the requested
measurements. It also contains a list of UserActivityInfo
objects describing
the changes in the user's activity state and when those changes happened, as
shown in the following example:
class BackgroundDataReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// Check that the Intent is for passive data
if (intent?.action != PassiveMonitoringUpdate.ACTION_DATA) {
return
}
val update = PassiveMonitoringUpdate.fromIntent(intent) ?: return
// List of available data points
val dataPoints = update.dataPoints
// List of available user state info
val userActivityInfoList = update.userActivityInfoUpdates
}
}
Add the following receiver to your AndroidManifest.xml
inside the
<application>
tag:
<receiver
android:name=".BackgroundDataReceiver"
android:exported="true">
<intent-filter>
<action android:name="hs.passivemonitoring.DATA" />
</intent-filter>
</receiver>
Once you have a BroadcastReceiver
, register for updates using the
PassiveMonitoringClient
and provide a PassiveMonitoringConfig
.
When you want to stop receiving data, unregister.
Registration is tied to your app, and each app is allowed
at most one registration at a time. If you register more than once, the
previous registration will be replaced. This means your registration should
include all the data types you want to receive, and your BroadcastReceiver
should handle all of those types.
val dataTypes = setOf(DataType.HEART_RATE_BPM, DataType.STEPS)
val config = PassiveMonitoringConfig.builder()
.setDataTypes(dataTypes)
.setComponentName(ComponentName(context, BackgroundDataReceiver::class.java))
// To receive UserActivityState updates, ACTIVITY_RECOGNITION permission is required.
.setShouldIncludeUserActivityState(true)
.build()
lifecycleScope.launch {
HealthServices.getClient(context)
.passiveMonitoringClient
.registerDataCallback(config)
.await()
}
Use passive data
The dataPoints
property in PassiveMonitoringUpdate
contains a list of data
points for all of the types subscribed to. Obtain timestamps for each
DataPoint
by first calculating the boot timestamp, as shown in the following
example:
val bootInstant =
Instant.ofEpochMilli(System.currentTimeMillis() - SystemClock.elapsedRealtime())
This value can then be used with getStartInstant()
or getEndInstant()
for each data point accordingly.
Use UserActivityState
information
PassiveMonitoringUpdate
can provide high-level information on the user state,
such as whether the user is sleeping or exercising. To receive these updates do
the following:
- Ensure that the
ACTIVITY_RECOGNITION
permission is granted. - Set
setShouldIncludeUserActivityState(true)
in thePassiveMonitoringConfig
builder.
The userActivityInfoUpdates
property returns a list of states and
timestamps associated with the transition. If no state changes have occurred
since the last PassiveMonitoringUpdate
, the list returned may be empty.
// Inspect the last reported state change, if present
passiveMonitoringUpdate.userActivityInfoUpdates.lastOrNull()?.let { userActivityInfo ->
// When the transition to this state took place
val stateChangeInstant = userActivityInfo.stateChangeTime
// The high-level state of the user, e.g. USER_ACTIVITY_ASLEEP, USER_ACTIVITY_EXERCISE
val state = userActivityInfo.userActivityState
if (state == UserActivityState.USER_ACTIVITY_EXERCISE) {
// Obtain info about the exercise and whether it is owned by the app
val exerciseInfo = userActivityInfo.exerciseInfo
}
}
Subscribe to passive goals
Creating and registering passive goals follows a similar process as registering for data. However, unlike registering for data, each event is a separate registration and apps can register for multiple passive goals. A registration will only be replaced if the event matches a previous one requested by the same app.
Add the following to your AndroidManifest.xml file inside of the <application>
tag:
<receiver
android:name=".BackgroundGoalsReceiver"
android:exported="true">
<intent-filter>
<action android:name="hs.passivemonitoring.GOAL" />
</intent-filter>
</receiver>
When it's time to register, define the event(s) that interest you and register each one. Use the same event definitions when you want to unregister the events.
PassiveGoals can either be one-off goals, or be set to be repeated.
The following example is a one-off goal for when 1000 steps is reached. However,
changing the TriggerType
to REPEATED would cause events to be broadcast every
thousand steps.
For daily metrics, such as DAILY_STEPS and DAILY_CALORIES, setting these as REPEATED will result in the goal being triggered once per-day. For example, a DAILY_STEPS goal of 10000 steps will not trigger twice if 20000 steps are walked, and will reset at midnight local time.
// Create a goal for when 1000 steps are reached.
val thousandStepGoal = PassiveGoal(
DataTypeCondition(
DataType.STEPS,
Value.ofLong(1000),
ComparisonType.GREATER_THAN_OR_EQUAL),
PassiveGoal.TriggerType.ONCE
)
lifecycleScope.launch {
passiveClient.registerPassiveGoalCallback(
thousandStepGoal,
ComponentName(context, BackgroundGoalReceiver::class.java)
).await()
}
In the corresponding receiver code, first check the Intent action to ensure that
it is for a PassiveGoal
, then subsequently reconstruct the PassiveGoal
from
the Intent to determine what criteria caused the event:
class BackgroundGoalReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// Check that the Intent is for a passive goal
if (intent?.action != PassiveGoal.ACTION_GOAL) {
return
}
// Obtain the goal to determine which goal caused the event
val goal = PassiveGoal.fromIntent(intent) ?: return
// Check against goals defined by the app
if (goal == thousandStepGoal) {
// Take appropriate action on reaching goal
}
}
}
Restore registrations after boot
Passive data and goal registrations don't persist across device restarts. If
your app needs to maintain a passive data registration across device restarts,
you can do this with a BroadcastReceiver
that receives the
ACTION_BOOT_COMPLETED
system broadcast.
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
While the device is turning on, there could be a delay (of ten seconds or
longer) when registering for data. This may exceed the allowable execution time
of a BroadcastReceiver
. For this reason, instead of registering for data right
away, schedule registration to happen in the future using WorkManager
. For
more information, see Get started with
WorkManager.
class StartupReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != Intent.ACTION_BOOT_COMPLETED) return
// TODO: check permissions first.
WorkManager.getInstance(context).enqueue(
OneTimeWorkRequestBuilder<RegisterForPassiveDataWorker>().build()
)
}
}
class RegisterForPassiveDataWorker(
private val appContext: Context,
workerParams: WorkerParameters
) : Worker(appContext, workerParams) {
override fun doWork(): Result {
runBlocking {
HealthServices.getClient(appContext)
.passiveMonitoringClient
.registerDataCallback(...)
.await()
}
return Result.success()
}
}
Add the following receiver to your AndroidManifest.xml
inside the
<application>
tag:
<receiver
android:name=".StartupReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>