HealthConnect: send total distance, elevation gain and speed.

This commit is contained in:
Dennis Guse
2026-07-31 07:38:09 +02:00
committed by Dennis Guse
parent e9456919cb
commit 01ea65742b
3 changed files with 99 additions and 27 deletions
+3
View File
@@ -71,6 +71,9 @@ limitations under the License.
<uses-permission android:name="android.permission.health.WRITE_EXERCISE" />
<uses-permission android:name="android.permission.health.WRITE_EXERCISE_ROUTE" />
<uses-permission android:name="android.permission.health.WRITE_DISTANCE" />
<uses-permission android:name="android.permission.health.WRITE_ELEVATION_GAINED" />
<uses-permission android:name="android.permission.health.WRITE_SPEED" />
<application
android:name=".Startup"
@@ -5,7 +5,10 @@ import android.content.Context;
import androidx.health.connect.client.HealthConnectClient;
import androidx.health.connect.client.PermissionController;
import androidx.health.connect.client.permission.HealthPermission;
import androidx.health.connect.client.records.DistanceRecord;
import androidx.health.connect.client.records.ElevationGainedRecord;
import androidx.health.connect.client.records.ExerciseSessionRecord;
import androidx.health.connect.client.records.SpeedRecord;
import androidx.work.Data;
import androidx.work.ExistingWorkPolicy;
import androidx.work.OneTimeWorkRequest;
@@ -27,11 +30,13 @@ import kotlinx.coroutines.BuildersKt;
*/
public class HealthConnectUtils {
static final String WRITE_EXERCISE_PERMISSION = HealthPermission.getWritePermission(JvmClassMappingKt.getKotlinClass(ExerciseSessionRecord.class));
static final String WRITE_EXERCISE_ROUTE_PERMISSION = HealthPermission.PERMISSION_WRITE_EXERCISE_ROUTE;
//TODO Should be package private
public static final Set<String> PERMISSIONS = Set.of(WRITE_EXERCISE_PERMISSION, WRITE_EXERCISE_ROUTE_PERMISSION);
public static final Set<String> PERMISSIONS = Set.of(
HealthPermission.getWritePermission(JvmClassMappingKt.getKotlinClass(ExerciseSessionRecord.class)),
HealthPermission.getWritePermission(JvmClassMappingKt.getKotlinClass(DistanceRecord.class)),
HealthPermission.getWritePermission(JvmClassMappingKt.getKotlinClass(ElevationGainedRecord.class)),
HealthPermission.getWritePermission(JvmClassMappingKt.getKotlinClass(SpeedRecord.class)),
HealthPermission.PERMISSION_WRITE_EXERCISE_ROUTE
);
private HealthConnectUtils() {
}
@@ -6,16 +6,22 @@ import android.util.Log;
import androidx.annotation.NonNull;
import androidx.health.connect.client.HealthConnectClient;
import androidx.health.connect.client.records.DistanceRecord;
import androidx.health.connect.client.records.ElevationGainedRecord;
import androidx.health.connect.client.records.ExerciseRoute;
import androidx.health.connect.client.records.ExerciseSessionRecord;
import androidx.health.connect.client.records.Record;
import androidx.health.connect.client.records.SpeedRecord;
import androidx.health.connect.client.records.metadata.Device;
import androidx.health.connect.client.records.metadata.Metadata;
import androidx.health.connect.client.units.Length;
import androidx.health.connect.client.units.Velocity;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import de.dennisguse.opentracks.data.ContentProviderUtils;
@@ -111,27 +117,65 @@ public class HealthConnectWorker extends Worker {
throw new RuntimeException("Track not found: " + trackId);
}
ExerciseRoute exerciseRoute = createExerciseRoute(contentProviderUtils, trackId);
HealthConnectClient client = HealthConnectClient.getOrCreate(context);
Metadata metadata = Metadata.activelyRecorded(
new Device(Device.TYPE_PHONE, Build.MANUFACTURER, Build.MODEL),
track.uuid().toString()
);
ExerciseSessionRecord record = new ExerciseSessionRecord(
List<Record> records = new ArrayList<>();
ExerciseData exerciseData = createExerciseData(contentProviderUtils, trackId);
//Avoid bug in HealthConnect: data must be within ExerciseSessionRecord.startTime and ExerciseSessionRecord.stopTime
List<ExerciseRoute.Location> locations = exerciseData.locations.stream()
.filter(i -> track.startTime().toInstant().isBefore(i.getTime()) && track.stopTime().toInstant().isAfter(i.getTime()))
.toList();
records.add(new ExerciseSessionRecord(
track.startTime().toInstant(),
track.startTime().getOffset(),
track.stopTime().toInstant(),
track.stopTime().getOffset(),
Metadata.activelyRecorded(
new Device(Device.TYPE_PHONE, Build.MANUFACTURER, Build.MODEL),
track.uuid().toString()
),
metadata,
toExerciseType(track.activityType()),
track.name(),
track.description(),
Collections.emptyList(),
Collections.emptyList(),
exerciseRoute);
new ExerciseRoute(locations)));
records.add(new DistanceRecord(
track.startTime().toInstant(),
track.startTime().getOffset(),
track.stopTime().toInstant(),
track.stopTime().getOffset(),
Length.meters(track.statistics().totalDistance().toM()),
metadata
));
if (track.statistics().altitudeGainLoss() != null) {
records.add(new ElevationGainedRecord(
track.startTime().toInstant(),
track.startTime().getOffset(),
track.stopTime().toInstant(),
track.stopTime().getOffset(),
Length.meters(track.statistics().altitudeGainLoss().gain_m()),
metadata
));
}
if (!exerciseData.speeds.isEmpty()) {
records.add(new SpeedRecord(
track.startTime().toInstant(),
track.startTime().getOffset(),
track.stopTime().toInstant(),
track.stopTime().getOffset(),
exerciseData.speeds,
metadata
));
}
List<ExerciseSessionRecord> records = Collections.singletonList(record);
Object ignored = BuildersKt.runBlocking(
kotlin.coroutines.EmptyCoroutineContext.INSTANCE,
(Function2) (scope, cont) -> client.insertRecords(records, (Continuation) cont));
@@ -140,17 +184,19 @@ public class HealthConnectWorker extends Worker {
return new ExportResult(track.name());
}
private static ExerciseRoute createExerciseRoute(ContentProviderUtils contentProviderUtils, Track.Id trackId) {
List<ExerciseRoute.Location> route = new ArrayList<>();
private static ExerciseData createExerciseData(ContentProviderUtils contentProviderUtils, Track.Id trackId) {
List<ExerciseRoute.Location> locations = new LinkedList<>();
List<SpeedRecord.Sample> speeds = new LinkedList<>();
try (TrackPointIterator iterator = contentProviderUtils.getTrackPointIterator(trackId, null)) {
while (iterator.hasNext()) {
while (iterator.hasNext()) { //TODO iterator.iterator.forEachRemaining()
TrackPoint trackPoint = iterator.next();
if (trackPoint.type() != TrackPoint.Type.TRACKPOINT) continue;
if (trackPoint.type() != TrackPoint.Type.TRACKPOINT) continue; //TODO Include SEGMENT_START_AUTOMATIC?
Position position = trackPoint.position();
route.add(new ExerciseRoute.Location(
if (position.hasLocation()) {
locations.add(new ExerciseRoute.Location(
trackPoint.getTime(),
position.latitude(),
position.longitude(),
@@ -159,10 +205,28 @@ public class HealthConnectWorker extends Worker {
position.altitude() != null ? Length.meters(position.altitude().toM()) : null)
);
}
if (position.hasSpeed()) {
speeds.add(new SpeedRecord.Sample(
trackPoint.getTime(),
Velocity.metersPerSecond(position.speed().toMPS())
));
}
return new ExerciseRoute(route);
//TODO Cadence
//TODO Power
//TODO Heartrate
}
}
return new ExerciseData(locations, speeds);
}
private record ExerciseData(
List<ExerciseRoute.Location> locations,
List<SpeedRecord.Sample> speeds
) {}
private static int toExerciseType(ActivityType activityType) {
return switch (activityType) {