Skip to content

Add initial Wear OS heart rate provider support - #1378

Open
roberi wants to merge 25 commits into
jonasoreland:masterfrom
roberi:feature/initial-wear-hr-support
Open

Add initial Wear OS heart rate provider support#1378
roberi wants to merge 25 commits into
jonasoreland:masterfrom
roberi:feature/initial-wear-hr-support

Conversation

@roberi

@roberi roberi commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

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

  • Added a new Wear OS devices option to HRSettingsActivity. This allows users to discover and connect to paired Wear OS devices running the RunnerUp Wear app.
  • Added a new heart rate provider, WearHRProvider, which implements the existing HRProvider interface. 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:

HeartRateService

Responsible for collecting heart rate and battery information from the watch.

  • Heart rate data is obtained through SensorManager using Sensor.TYPE_HEART_RATE.
  • Battery status is monitored through a BroadcastReceiver listening for Intent.ACTION_BATTERY_CHANGED.

HeartRateListenerService

Extends WearableListenerService and acts as the communication layer between the phone and the watch.

  • Receives START and STOP commands from the phone via the Wearable Data API.
  • Starts and stops HeartRateService as 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:

  • Convert HeartRateService into a foreground service (or merge it with the existing foreground notification) to ensure reliable operation when the display is off or another app is active.
  • Improve error reporting and recovery during device discovery and connection establishment, including watch-side permission failures.
  • Address the new READ_HEART_RATE requirements introduced on Wear OS 6 (API 36). The current implementation only suppresses the manifest warning through tools:ignore="HealthConnectPolicy".

roberi added 22 commits August 13, 2026 13:24
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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread wear/src/main/java/org/runnerup/hr/HeartRateService.java Outdated
Comment thread wear/src/main/AndroidManifest.xml
Comment thread wear/src/main/java/org/runnerup/hr/HeartRateService.java Outdated
Comment thread hrdevice/src/org/runnerup/hr/WearHRProvider.java Outdated
Comment thread hrdevice/src/org/runnerup/hr/WearHRProvider.java Outdated
Comment thread hrdevice/src/org/runnerup/hr/WearHRProvider.java
Comment thread hrdevice/src/org/runnerup/hr/HRManager.java Outdated
roberi added 3 commits August 14, 2026 12:09
- 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.
@roberi roberi linked an issue Aug 14, 2026 that may be closed by this pull request

@gerhardol gerhardol left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@roberi

roberi commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for your note. I will contact Jonas to see if there is something I can help him with.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Use Smartwatch (e.g. Pixel Watch) as HR sensor

3 participants