diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index 3b1339e6b..fe0f90b97 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -71,6 +71,9 @@ limitations under the License.
+
+
+
PERMISSIONS = Set.of(WRITE_EXERCISE_PERMISSION, WRITE_EXERCISE_ROUTE_PERMISSION);
+ public static final Set 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() {
}
diff --git a/src/main/java/de/dennisguse/opentracks/io/healthconnect/exporter/HealthConnectWorker.java b/src/main/java/de/dennisguse/opentracks/io/healthconnect/exporter/HealthConnectWorker.java
index 65832d4f1..5d24b10f5 100644
--- a/src/main/java/de/dennisguse/opentracks/io/healthconnect/exporter/HealthConnectWorker.java
+++ b/src/main/java/de/dennisguse/opentracks/io/healthconnect/exporter/HealthConnectWorker.java
@@ -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 records = new ArrayList<>();
+
+ ExerciseData exerciseData = createExerciseData(contentProviderUtils, trackId);
+
+ //Avoid bug in HealthConnect: data must be within ExerciseSessionRecord.startTime and ExerciseSessionRecord.stopTime
+ List 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 records = Collections.singletonList(record);
Object ignored = BuildersKt.runBlocking(
kotlin.coroutines.EmptyCoroutineContext.INSTANCE,
(Function2) (scope, cont) -> client.insertRecords(records, (Continuation) cont));
@@ -140,30 +184,50 @@ public class HealthConnectWorker extends Worker {
return new ExportResult(track.name());
}
- private static ExerciseRoute createExerciseRoute(ContentProviderUtils contentProviderUtils, Track.Id trackId) {
- List route = new ArrayList<>();
+ private static ExerciseData createExerciseData(ContentProviderUtils contentProviderUtils, Track.Id trackId) {
+ List locations = new LinkedList<>();
+ List 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(
- trackPoint.getTime(),
- position.latitude(),
- position.longitude(),
- position.horizontalAccuracy() != null ? Length.meters(position.horizontalAccuracy().toM()) : null,
- position.verticalAccuracy() != null ? Length.meters(position.verticalAccuracy().toM()) : null,
- position.altitude() != null ? Length.meters(position.altitude().toM()) : null)
- );
+ if (position.hasLocation()) {
+ locations.add(new ExerciseRoute.Location(
+ trackPoint.getTime(),
+ position.latitude(),
+ position.longitude(),
+ position.horizontalAccuracy() != null ? Length.meters(position.horizontalAccuracy().toM()) : null,
+ position.verticalAccuracy() != null ? Length.meters(position.verticalAccuracy().toM()) : null,
+ position.altitude() != null ? Length.meters(position.altitude().toM()) : null)
+ );
+ }
+
+ if (position.hasSpeed()) {
+ speeds.add(new SpeedRecord.Sample(
+ trackPoint.getTime(),
+ Velocity.metersPerSecond(position.speed().toMPS())
+ ));
+ }
+
+ //TODO Cadence
+ //TODO Power
+ //TODO Heartrate
}
}
- return new ExerciseRoute(route);
+ return new ExerciseData(locations, speeds);
}
+ private record ExerciseData(
+ List locations,
+ List speeds
+ ) {}
+
+
private static int toExerciseType(ActivityType activityType) {
return switch (activityType) {
case RUNNING, STREET_RUNNING, TRAIL_RUNNING, TRACK_RUNNING -> ExerciseSessionRecord.EXERCISE_TYPE_RUNNING;