Health Connect: Sync resting heart rate

This commit is contained in:
José Rebelo
2026-01-04 17:33:32 +00:00
parent 0962c5e225
commit 71f9457b93
4 changed files with 67 additions and 0 deletions
+1
View File
@@ -127,6 +127,7 @@
<uses-permission android:name="android.permission.health.WRITE_POWER" />
<uses-permission android:name="android.permission.health.WRITE_EXERCISE_ROUTE" />
<uses-permission android:name="android.permission.health.WRITE_RESPIRATORY_RATE" />
<uses-permission android:name="android.permission.health.WRITE_RESTING_HEART_RATE" />
<uses-sdk tools:overrideLibrary="androidx.health.connect.client"/>
<uses-feature
@@ -58,6 +58,7 @@ object HealthConnectPermissionManager {
SPO2,
TEMPERATURE,
RESPIRATORY_RATE,
RESTING_HEART_RATE,
WORKOUTS
}
@@ -77,6 +78,7 @@ object HealthConnectPermissionManager {
HealthConnectDataType.WEIGHT -> setOf(HealthPermission.getWritePermission(WeightRecord::class))
HealthConnectDataType.SPO2 -> setOf(HealthPermission.getWritePermission(OxygenSaturationRecord::class))
HealthConnectDataType.RESPIRATORY_RATE -> setOf(HealthPermission.getWritePermission(RespiratoryRateRecord::class))
HealthConnectDataType.RESTING_HEART_RATE -> setOf(HealthPermission.getWritePermission(RestingHeartRateRecord::class))
HealthConnectDataType.TEMPERATURE -> setOf(
HealthPermission.getWritePermission(BodyTemperatureRecord::class),
HealthPermission.getWritePermission(SkinTemperatureRecord::class)
@@ -600,6 +600,10 @@ class HealthConnectUtils {
healthConnectClient, gbDevice, metadata, offset,
currentSliceStartTs, currentSliceEndTs, grantedPermissions
))
HealthConnectPermissionManager.HealthConnectDataType.RESTING_HEART_RATE -> sliceStats.add(RestingHeartRateSyncer.sync(
healthConnectClient, gbDevice, metadata, offset,
currentSliceStartTs, currentSliceEndTs, grantedPermissions
))
HealthConnectPermissionManager.HealthConnectDataType.WORKOUTS -> {
// Sync explicitly recorded workouts from BaseActivitySummary
val coordinator = gbDevice.deviceCoordinator
@@ -696,6 +700,7 @@ class HealthConnectUtils {
HealthConnectPermissionManager.HealthConnectDataType.VO2MAX -> coordinator.getVo2MaxSampleProvider(device, db.daoSession)
HealthConnectPermissionManager.HealthConnectDataType.HRV -> coordinator.getHrvValueSampleProvider(device, db.daoSession)
HealthConnectPermissionManager.HealthConnectDataType.RESPIRATORY_RATE -> coordinator.getRespiratoryRateSampleProvider(device, db.daoSession)
HealthConnectPermissionManager.HealthConnectDataType.RESTING_HEART_RATE -> coordinator.getHeartRateRestingSampleProvider(device, db.daoSession)
HealthConnectPermissionManager.HealthConnectDataType.WEIGHT -> coordinator.getWeightSampleProvider(device, db.daoSession)
// For SpO2 and Temperature, there might be a specific provider or fallback to general sample provider
HealthConnectPermissionManager.HealthConnectDataType.SPO2 -> coordinator.getSpo2SampleProvider(device, db.daoSession) // Potentially add fallback if needed: ?: coordinator.getSampleProvider(device, db.daoSession)
@@ -0,0 +1,59 @@
/* Copyright (C) 2026 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.util.healthconnect.syncers
import androidx.health.connect.client.records.RestingHeartRateRecord
import androidx.health.connect.client.records.metadata.Metadata
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice
import nodomain.freeyourgadget.gadgetbridge.model.HeartRateSample
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.time.Instant
import java.time.ZoneOffset
import kotlin.reflect.KClass
internal object RestingHeartRateSyncer : AbstractTimeSampleSyncer<HeartRateSample, RestingHeartRateRecord>() {
override val logger: Logger = LoggerFactory.getLogger(RestingHeartRateSyncer::class.java)
override val recordClass: KClass<RestingHeartRateRecord> = RestingHeartRateRecord::class
override fun getSampleProvider(
gbDevice: GBDevice,
daoSession: DaoSession
): TimeSampleProvider<out HeartRateSample>? {
return gbDevice.deviceCoordinator.getHeartRateRestingSampleProvider(gbDevice, daoSession)
}
override fun convertSample(
sample: HeartRateSample,
offset: ZoneOffset,
metadata: Metadata,
deviceName: String
): RestingHeartRateRecord? {
if (sample.heartRate !in 20..250) {
return null
}
return RestingHeartRateRecord(
time = Instant.ofEpochMilli(sample.timestamp),
zoneOffset = offset,
beatsPerMinute = sample.heartRate.toLong(),
metadata = metadata
)
}
}