Native Android SDK with Health Connect integration
Overview
The Synheart Wear Kotlin SDK provides native Android support for reading biometric data from wearables via Health Connect, vendor cloud APIs (WHOOP, Garmin, Fitbit, Oura), and direct BLE heart-rate sensors.
Installation
Gradle (Kotlin DSL)
Published to Maven Central as ai.synheart:synheart-wear:
Call initialize() first: every other call throws SynheartWearException
(“SDK not initialized”) until it has run.requestPermissions() does not show the Health Connect permission screen.
It reports which of the requested permissions are already granted. Show the
screen yourself with Health Connect’s PermissionController contract, which
needs an Activity:
import androidx.health.connect.client.PermissionControllerimport androidx.health.connect.client.permission.HealthPermissionimport androidx.health.connect.client.records.HeartRateRecordimport androidx.health.connect.client.records.StepsRecordimport androidx.health.connect.client.records.TotalCaloriesBurnedRecordprivate val hcPermissions = setOf( HealthPermission.getReadPermission(HeartRateRecord::class), HealthPermission.getReadPermission(StepsRecord::class), HealthPermission.getReadPermission(TotalCaloriesBurnedRecord::class),)private val requestHc = registerForActivityResult( PermissionController.createRequestPermissionResultContract()) { granted -> if (granted.containsAll(hcPermissions)) readNow()}fun start() { lifecycleScope.launch { synheartWear.initialize() requestHc.launch(hcPermissions) }}fun readNow() { lifecycleScope.launch { val status = synheartWear.getPermissionStatus() // Map<PermissionType, Boolean> val metrics = synheartWear.readMetrics() Log.d(TAG, "HR: ${metrics.getMetric(MetricType.HR)} bpm") }}
streamHRV(windowMs = 5000L) emits HRV the same way.
Health Connect
Reads from any wearable companion app that syncs to Jetpack Health Connect — Fitbit, Garmin Connect, Samsung Health, Google Fit, Polar Flow, etc. (Apple Watch is not supported on Android — it only syncs to Apple Health on iOS.)
AndroidManifest.xml
The SDK’s own manifest declares 17 Health Connect read permissions plus
READ_HEALTH_DATA_IN_BACKGROUND, and the <queries> block for the Health
Connect app, so your app inherits them. (The Flutter plugin does not; Flutter
apps declare their own.) That list includes types the SDK does
not read today (oxygen saturation, respiratory rate, body temperature, weight,
height, body fat, lean body mass, speed). If your app doesn’t use them, remove
them so Play review doesn’t ask you to justify them:
READ_HEALTH_DATA_HISTORY is not in the inherited list. Declare it yourself to
read more than 30 days back; see
Health backfill.Play policy requires a permissions-rationale screen and a privacy-policy entry
point. Add both to your own manifest:
SleepNightSummary carries totalAsleepMinutes, deepMinutes and
remMinutes. OvernightPhysiologySummary carries hrvRmssdMs and
restingHrBpm.
Availability
val availability = HealthConnectAdapter(context).getAvailabilityStatus()if (!availability.isAvailable()) { // Health Connect missing or needs an update: send the user to the Play Store}// Or, without an instance:HealthConnectAdapter.isAvailable(context)HealthConnectAdapter.getSdkStatus(context)
Per-record retention is platform-controlled (typically ~30 days for HR, longer for sleep). For older data use a cloud provider.
Cloud providers (WHOOP / Garmin / Fitbit / Oura)
Cloud providers exist only when you pass a CloudConfig and list the vendor
in enabledAdapters. They are created during initialize().
getProvider() throws SynheartWearException when the vendor isn’t enabled or
cloudConfig is missing. isCloudAdapterEnabled(vendor) checks without
throwing.OAuth is mediated by the Synheart Wear API; the device never sees vendor access
tokens. Call provider.connect() to get an authorization URL, open it in a
browser, then call provider.connectWithCode(code, state, redirectUri) from
your deep-link handler. readMetricsFromProvider(...) reads a connected
vendor’s data as WearMetrics. Full code sample in the SDK README’s Cloud
Wearables section.
BLE Heart Rate Monitor
Connect directly to any standard Bluetooth LE heart rate monitor for real-time HR streaming.
Request BLUETOOTH_SCAN and BLUETOOTH_CONNECT at runtime on Android 12+
before scanning.
Usage
bleHrm is null until initialize() has run.
val synheartWear = SynheartWear( context = this, config = SynheartWearConfig( enabledAdapters = setOf(DeviceAdapter.BLE_HRM) ))lifecycleScope.launch { synheartWear.initialize() val bleHrm = synheartWear.bleHrm ?: return@launch // Scan for nearby HR monitors (default timeout 10 s) val devices = bleHrm.scan(timeoutMs = 10_000, namePrefix = "Polar") val device = devices.firstOrNull() ?: return@launch bleHrm.connect(deviceId = device.deviceId, sessionId = "my-session") // heartRateFlow never completes: collect it in its own job val job = launch { bleHrm.heartRateFlow.collect { sample -> Log.d(TAG, "BPM: ${sample.bpm}") sample.rrIntervalsMs?.let { rr -> Log.d(TAG, "RR: $rr") } } } // Later, when you're done: job.cancel() bleHrm.disconnect()}
Errors throw BleHrmException, whose errorCode is a BleHrmErrorCode:
PERMISSION_DENIED, BLUETOOTH_OFF, DEVICE_NOT_FOUND, SUBSCRIBE_FAILED or
DISCONNECTED. After an unexpected disconnect the provider retries three times
(1 s, 2 s, 4 s) before giving up.
Supported Devices
Works with any BLE device implementing the standard Heart Rate Profile (0x180D):
WHOOP (Broadcast HR mode)
Polar H10, OH1
Wahoo TICKR
Garmin HRM-Pro / HRM-Dual
Garmin Health SDK (Native RTS)
The GarminHealth facade provides native Garmin device integration for scanning, pairing, and real-time streaming using generic SDK-owned types.
Important: The Garmin Health SDK Real-Time Streaming (RTS) capability requires a separate license from Garmin. The underlying Garmin Health SDK code is proprietary to Garmin and is not distributed as open source. In the Maven Central build, initialize() succeeds but every data call throws SynheartWearException; licensed integrations build with the Garmin overlay.
import ai.synheart.wear.adapters.GarminHealthval garmin = GarminHealth(licenseKey = "your-garmin-sdk-key")garmin.initialize()synheartWear.setGarminHealth(garmin)lifecycleScope.launch { launch { garmin.scannedDevicesFlow.collect { devices -> Log.d(TAG, "Found ${devices.size} devices") } } garmin.startScanning(timeoutSeconds = 30)}// Once the user picks a device:val paired = garmin.pairDevice(scannedDevice)garmin.startStreaming(paired)
For cloud-based Garmin data (OAuth + webhooks), use the GARMIN cloud provider instead.
Local cache
SynheartWearConfig.enableLocalCaching (default true) keeps recent readings
on the device; getCachedSessions(), getCacheStats() and clearOldCache()
manage it, and purgeAllData() clears the cache and revokes consents.
enableEncryption is accepted but not applied in 0.4.2: the Kotlin cache is
written unencrypted. Don’t rely on it for data that must be encrypted at rest.
Errors
SDK calls throw SynheartWearException(message, cause, code). BLE calls throw
BleHrmException (see above).