Add initial Wear OS heart rate provider support - #1378
Conversation
Introduces a "Wear OS devices" menu option in HRSettingsActivity. This allows users to select a paired Wear OS device running the RunnerUp companion app as their heart rate monitor source.
Introduces WearHRProvider in the phone module, which implements the HRProvider interface. This class is intended to manage heart rate data from connected Wear OS devices. The current implementation is a skeleton, with all methods being placeholders (stubs) for now.
Improves WearHRProvider to discover Wear OS devices that can provide heart rate data to RunnerUp by using the CapabilityClient. Details: - Introduces Constants.Wear.Capability.HEART_RATE_PROVIDER (runnerup_hr_provider) for wearables to advertise. - startScan() now queries CapabilityClient for nodes advertising this specific capability. - Discovered nodes are mapped to `HRDeviceRef` objects using their display name and ID.
Adds the wear.xml file, which defines the android_wear_capabilities string array. This array declares the wear app's heart rate monitoring capability, allowing the RunnerUp phone app to discover and interact with it.
This commit implements the connect and disconnect methods in WearHRProvider. New message paths for communication is also implemented. Key changes: - Connect: Sends a MSG_CMD_HR_START message to the Wear OS device to initiate heart rate data transmission. Updates connection status and notifies HRClient of the result. - Disconnect: Sends a MSG_CMD_HR_STOP message to the Wear OS device to stop heart rate data transmission. Updates connection status, notifies HRClient, and resets internal state. - Reset: A new method to clear connection and scanning states.
Add postToHRClient(Runnable) helper method to encapsulate the logic for safely posting runnable actions to the HRClient's handler thread.
Introduces HeartRateListenerService, extending WearableListenerService. This service acts as the entry point for commands sent from the phone to manage heart rate monitoring on the wearable (start and stop). The service is configured to filter messages only for the "/org.runnerup/hr" path. This initial version is mainly a skeleton, with all methods being placeholders (stubs) for now.
Introduces HeartRateService intended to manage heart rate sensor interactions and data transmission to the phone. This initial version includes: - Basic service lifecycle methods (onCreate, onStartCommand, onDestroy). - Placeholder methods for SensorEventListener callbacks (onSensorChanged, onAccuracyChanged). - A placeholder method for sending heart rate data to the phone (sendHeartRateToPhone(int).
HeartRateService now handles the lifecycle of the heart rate sensor: - Initializes SensorManager and obtains the TYPE_HEART_RATE sensor in onCreate(). - Registers as a SensorEventListener for heart rate events in onStartCommand(). - Unregisters the sensor listener in onDestroy() to release resources.
HeartRateListenerService now starts/stops HeartRateService based on messages from the phone. - onMessageReceived reacts to: - MSG_CMD_HR_START: Starts `HeartRateService`, passing source node ID. - MSG_CMD_HR_STOP: Stops `HeartRateService`. (Constants like `Constants.Wear.Path` and `Constants.Intents` are used for paths and extras respectively).
Enables the watch to send heart rate data to the connected phone. Key changes: - The phone's node ID is obtained from HeartRateService's start intent (extra: Constants.Intents.EXTRA_SOURCE_NODE_ID). - onSensorChanged now checks sensor accuracy and data is only processed if accuracy is not SENSOR_STATUS_UNRELIABLE or SENSOR_STATUS_NO_CONTACT. - Validated HR data is sent to the phone using MessageClient on path Constants.Wear.Path.MSG_DATA_HR.
WearHRProvider now listens for heart rate messages from Wear OS,
parses them, and makes the data available.
Key changes:
- connect(): Registers a MessageClient.OnMessageReceivedListener
to listen for incoming HR data.
- disconnect(): Removes the MessageClient.OnMessageReceivedListener
to stop listening (prevents resource leaks).
- onMessageReceived():
- Parses the incoming message payload to extract the heart rate
value (hrValue).
- Generates a local timestamp (hrTimestamp) and elapsed realtime
(hrElapsedRealtime) for the received data.
- The heart rate data (value, timestamp, elapsed realtime) is made
accessible via getters.
Introduces a runtime permission check for `BODY_SENSORS` within `HeartRateService` before attempting to start heart rate monitoring. - If permission is not granted, `RequestPermissionActivity` is launched. - `HeartRateService` now uses a `BroadcastReceiver` to listen for the permission result from this activity. - Monitoring only proceeds if permission is subsequently granted.
- Gracefully handle unavailable/disabled Google Play Services by preventing connection attempts. - Log errors to HRClient if scanning for a Wear OS device fails, instead of only logging internally.
The hrdevice module is a library and should not define its own <application> tag. This caused manifest merger conflicts, specifically using the hrdevice module's "@string/app_name" instead of the common module's version. This resulted in the app being labeled incorrectly as "HRDevice" instead of "RunnerUp". Removing the tag from the library module resolves the conflict and ensures the correct application name is used.
Updated `getSimplePermissionName` to handle permission strings with multiple sub-packages.
- Update `HeartRateService` to check and request permissions based on API level. - Use `HealthPermissions.READ_HEART_RATE` for Wear OS 6 (API 36) and higher. - Use `BODY_SENSORS` for Wear OS 5 (API 35) and lower. - Update `AndroidManifest.xml` to include the new health platform permission.
- Use a `BroadcastReceiver` to monitor battery status changes on the Wear OS device. - Calculate and log the current battery percentage.
There was a problem hiding this comment.
Pull request overview
This PR adds initial end-to-end support for using a paired Wear OS device as a heart rate (and watch battery) provider in RunnerUp, using the Wearable Data Layer for communication between the phone and watch apps.
Changes:
- Phone: adds a new “Wear OS devices” HR settings toggle and a new
WearHRProviderimplementation that discovers capable watches and receives HR/battery messages. - Wear: adds watch-side services to start/stop HR monitoring, read
Sensor.TYPE_HEART_RATE, monitor battery changes, and send readings back to the phone. - Common/shared: introduces Wear message paths, a shared capability name, and supporting preference/string resources.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| wear/src/main/res/values/wear.xml | Declares the Wear capability name intended to be advertised by the watch app. |
| wear/src/main/java/org/runnerup/view/RequestPermissionActivity.java | Improves permission name formatting for display (incl. health permission namespaces). |
| wear/src/main/java/org/runnerup/hr/HeartRateService.java | New watch service to read HR sensor + battery and message results to the phone. |
| wear/src/main/java/org/runnerup/hr/HeartRateListenerService.java | New listener service to receive phone start/stop commands and control HeartRateService. |
| wear/src/main/AndroidManifest.xml | Adds HR permissions and registers the new Wear HR services. |
| hrdevice/src/org/runnerup/hr/WearHRProvider.java | New phone-side HR provider using Wearable capability discovery + messaging for HR/battery. |
| hrdevice/src/org/runnerup/hr/HRManager.java | Wires the Wear provider into provider creation and provider listing. |
| hrdevice/res/values/strings.xml | Adds Wear OS preference key string resource. |
| hrdevice/build.gradle | Adds wearable/play-services dependency (and common module dependency) for the new provider. |
| hrdevice/AndroidManifest.xml | Removes unused <application> block from the hrdevice module manifest. |
| common/src/main/res/values/strings.xml | Adds UI string for “Wear OS devices”. |
| common/src/main/java/org/runnerup/common/util/Constants.java | Adds Wear message paths, capability constant, and source-node extra key. |
| app/src/main/org/runnerup/view/HRSettingsActivity.java | Adds menu item ↔ preference mapping for Wear OS devices toggle. |
| app/res/values/pref_keys.xml | Adds Wear OS preference key. |
| app/res/menu/hrsettings_menu.xml | Adds the “Wear OS devices” checkable menu item in HR settings. |
Suppressed comments (2)
wear/src/main/java/org/runnerup/hr/HeartRateService.java:189
- getRequiredHeartRatePermission() references HealthPermissions.READ_HEART_RATE. On devices where android.health.connect.* isn't present, class resolution can fail even though the branch is guarded. Use the literal permission string for API 36+ instead of HealthPermissions.
private String getRequiredHeartRatePermission() {
if (Build.VERSION.SDK_INT >= 36) {
return HealthPermissions.READ_HEART_RATE;
} else {
return Manifest.permission.BODY_SENSORS;
}
}
hrdevice/src/org/runnerup/hr/WearHRProvider.java:227
- MessageClient.sendMessage expects a non-null payload; passing null can trigger an NPE inside Play Services. Use an empty byte[] for STOP commands as well.
Wearable.getMessageClient(context).sendMessage(
connectedNodeId,
Constants.Wear.Path.MSG_CMD_HR_STOP,
null // No payload needed for the stop command
);
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- replace HealthPermissions.READ_HEART_RATE with a literal permission string (HeartRateService) - register HEART_RATE_PROVIDER as a Wear OS capability (AndroidManifest) - validate battery level and scale values before calculating battery percentage (WearHRProvider) - use empty byte arrays instead of null message payloads (WearHRProvider) - clean up wearable client references and unregister listeners in close() (WearHRProvider) - add defensive parsing and validation of incoming heart rate messages (WearHRProvider) - gate WearHRProvider registration on Wearable API availability checks
The lint warning is a false positive. The activity is launched with FLAG_ACTIVITY_NEW_TASK, which is required when starting an activity from a non-Activity context.
gerhardol
left a comment
There was a problem hiding this comment.
Brief look, no comments.
Note that Play updates of Wear have been rejected the last releases. Jonas did not get the updates approved (I do not recall the motivation right now).
|
Thanks for your note. I will contact Jonas to see if there is something I can help him with. |
This PR introduces initial support for using a Wear OS device as a heart rate provider in RunnerUp.
2026-07-29_21-00-56.mp4
Implementation overview
Phone app
HRSettingsActivity. This allows users to discover and connect to paired Wear OS devices running the RunnerUp Wear app.WearHRProvider, which implements the existingHRProviderinterface. By following the same pattern as the Bluetooth and ANT+ providers, the integration requires only minimal changes to the core application logic.Wear app
Two new services have been added to the Wear module:
HeartRateServiceResponsible for collecting heart rate and battery information from the watch.
SensorManagerusingSensor.TYPE_HEART_RATE.BroadcastReceiverlistening forIntent.ACTION_BATTERY_CHANGED.HeartRateListenerServiceExtends
WearableListenerServiceand acts as the communication layer between the phone and the watch.HeartRateServiceas needed.Compatibility note
The Wear module currently targets
minSdk 25, which makes it possible to support older Wear OS devices. Because of this, the implementation uses the traditional SensorManager API instead of the newer Health Services API, which requires Wear OS 3 (API 30) or later.Testing
The feature has been tested using both Android Studio emulators and physical devices (Pixel Watch 4 running Wear OS 7, paired with Pixel 9 Pro running Android 17). More than 50 workouts completed successfully. No stability issues observed, provided that the RunnerUp Wear app remains in the foreground (see limitations below). Battery consumption is approximately 20% per hour of activity on the Pixel 4.
Additional testing was performed by Toby2988 as described in issue #1366.
Known limitations
The watch app currently needs to remain in the foreground to ensure reliable heart rate delivery.
Follow-up work
The following improvements are planned in subsequent PRs:
tools:ignore="HealthConnectPolicy".