From f487bc78769242e39de66c659e5488d119a36f67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Mon, 26 Aug 2024 10:46:43 +0100 Subject: [PATCH] Garmin: Re-parse workout summary when opening details page Ensures that new fields and other fixes get displayed properly. --- .../adapter/ActivitySummariesAdapter.java | 2 +- .../workout/CmfWorkoutSummaryParser.java | 11 +- .../devices/garmin/GarminCoordinator.java | 7 + .../devices/garmin/GarminWorkoutParser.java | 219 ++++++++++++++++++ .../huami/HuamiActivitySummaryParser.java | 8 +- .../zeppos/ZeppOsActivitySummaryParser.java | 16 +- .../activity/TestActivitySummaryParser.java | 2 +- .../model/ActivitySummaryJsonSummary.java | 17 +- .../model/ActivitySummaryParser.java | 12 +- .../devices/cmfwatchpro/CmfActivitySync.java | 2 +- .../devices/garmin/fit/FitImporter.java | 182 ++------------- .../fetch/FetchSportsSummaryOperation.java | 2 +- .../activity/impl/WorkoutSummaryParser.java | 13 +- 13 files changed, 297 insertions(+), 196 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminWorkoutParser.java diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/ActivitySummariesAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/ActivitySummariesAdapter.java index 70ef278b1..a66465faf 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/ActivitySummariesAdapter.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/ActivitySummariesAdapter.java @@ -207,7 +207,7 @@ public class ActivitySummariesAdapter extends AbstractActivityListingAdapter getSampleProvider(final GBDevice device, DaoSession session) { return new GarminActivitySampleProvider(device, session); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminWorkoutParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminWorkoutParser.java new file mode 100644 index 000000000..13795b9b1 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminWorkoutParser.java @@ -0,0 +1,219 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.garmin; + +import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.*; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Optional; + +import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitFile; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.enums.GarminSport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitPhysiologicalMetrics; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitRecord; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitSession; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitSport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitTimeInZone; + +public class GarminWorkoutParser implements ActivitySummaryParser { + private static final Logger LOG = LoggerFactory.getLogger(GarminWorkoutParser.class); + + private final List timesInZone = new ArrayList<>(); + private final List activityPoints = new ArrayList<>(); + private FitSession session = null; + private FitSport sport = null; + private FitPhysiologicalMetrics physiologicalMetrics = null; + + @Override + public BaseActivitySummary parseBinaryData(final BaseActivitySummary summary, final boolean forDetails) { + if (!forDetails) { + // Our parsing is too slow, especially without a RecyclerView + return summary; + } + + final long nanoStart = System.nanoTime(); + + reset(); + + final String rawDetailsPath = summary.getRawDetailsPath(); + if (rawDetailsPath == null) { + LOG.warn("No rawDetailsPath"); + return summary; + } + final File file = new File(rawDetailsPath); + if (!file.isFile() || !file.canRead()) { + LOG.warn("Unable to read {}", file); + return summary; + } + + final FitFile fitFile; + try { + fitFile = FitFile.parseIncoming(file); + } catch (final IOException e) { + LOG.error("Failed to parse fit file", e); + return summary; + } + + for (final RecordData record : fitFile.getRecords()) { + handleRecord(record); + } + + updateSummary(summary); + + final long nanoEnd = System.nanoTime(); + final long executionTime = (nanoEnd - nanoStart) / 1000000; + LOG.trace("Updating summary took {}ms", executionTime); + + return summary; + } + + public void reset() { + timesInZone.clear(); + activityPoints.clear(); + session = null; + sport = null; + physiologicalMetrics = null; + } + + public boolean handleRecord(final RecordData record) { + if (record instanceof FitRecord) { + activityPoints.add(((FitRecord) record).toActivityPoint()); + } else if (record instanceof FitSession) { + LOG.debug("Session: {}", record); + if (session != null) { + LOG.warn("Got multiple sessions - NOT SUPPORTED: {}", record); + } else { + // We only support 1 session + session = (FitSession) record; + } + } else if (record instanceof FitPhysiologicalMetrics) { + LOG.debug("Physiological Metrics: {}", record); + physiologicalMetrics = (FitPhysiologicalMetrics) record; + } else if (record instanceof FitSport) { + LOG.debug("Sport: {}", record); + if (sport != null) { + LOG.warn("Got multiple sports - NOT SUPPORTED: {}", record); + } else { + // We only support 1 sport + sport = (FitSport) record; + } + } else if (record instanceof FitTimeInZone) { + LOG.trace("Time in zone: {}", record); + timesInZone.add((FitTimeInZone) record); + } else { + return false; + } + + return true; + } + + public void updateSummary(final BaseActivitySummary summary) { + if (session == null) { + LOG.error("Got workout, but no session"); + return; + } + + final ActivitySummaryData summaryData = new ActivitySummaryData(); + + final ActivityKind activityKind; + if (sport != null) { + summary.setName(sport.getName()); + activityKind = getActivityKind(sport.getSport(), sport.getSubSport()); + } else { + activityKind = getActivityKind(session.getSport(), session.getSubSport()); + } + summary.setActivityKind(activityKind.getCode()); + + if (session.getTotalElapsedTime() != null) { + summary.setEndTime(new Date(summary.getStartTime().getTime() + session.getTotalElapsedTime().intValue())); + } + + if (session.getTotalTimerTime() != null) { + summaryData.add(ACTIVE_SECONDS, session.getTotalTimerTime() / 1000f, UNIT_SECONDS); + } + if (session.getTotalDistance() != null) { + summaryData.add(DISTANCE_METERS, session.getTotalDistance() / 100f, UNIT_METERS); + } + if (session.getTotalCalories() != null) { + summaryData.add(CALORIES_BURNT, session.getTotalCalories(), UNIT_KCAL); + } + if (session.getEstimatedSweatLoss() != null) { + summaryData.add(ESTIMATED_SWEAT_LOSS, session.getEstimatedSweatLoss(), UNIT_ML); + } + if (session.getAverageHeartRate() != null) { + summaryData.add(HR_AVG, session.getAverageHeartRate(), UNIT_BPM); + } + if (session.getMaxHeartRate() != null) { + summaryData.add(HR_MAX, session.getMaxHeartRate(), UNIT_BPM); + } + if (session.getTotalAscent() != null) { + summaryData.add(ASCENT_DISTANCE, session.getTotalAscent(), UNIT_METERS); + } + if (session.getTotalDescent() != null) { + summaryData.add(DESCENT_DISTANCE, session.getTotalDescent(), UNIT_METERS); + } + + for (final FitTimeInZone fitTimeInZone : timesInZone) { + // Find the first time in zone for the session (assumes single-session) + if (fitTimeInZone.getReferenceMessage() != null && fitTimeInZone.getReferenceMessage() == 18) { + final Double[] timeInZone = fitTimeInZone.getTimeInZone(); + if (timeInZone != null && timeInZone.length == 6) { + summaryData.add(HR_ZONE_NA, timeInZone[0].floatValue(), UNIT_SECONDS); + summaryData.add(HR_ZONE_WARM_UP, timeInZone[1].floatValue(), UNIT_SECONDS); + summaryData.add(HR_ZONE_FAT_BURN, timeInZone[2].floatValue(), UNIT_SECONDS); + summaryData.add(HR_ZONE_AEROBIC, timeInZone[3].floatValue(), UNIT_SECONDS); + summaryData.add(HR_ZONE_ANAEROBIC, timeInZone[4].floatValue(), UNIT_SECONDS); + summaryData.add(HR_ZONE_EXTREME, timeInZone[5].floatValue(), UNIT_SECONDS); + } + break; + } + } + + if (physiologicalMetrics != null) { + if (physiologicalMetrics.getAerobicEffect() != null) { + summaryData.add(TRAINING_EFFECT_AEROBIC, physiologicalMetrics.getAerobicEffect(), UNIT_NONE); + } + if (physiologicalMetrics.getAnaerobicEffect() != null) { + summaryData.add(TRAINING_EFFECT_ANAEROBIC, physiologicalMetrics.getAnaerobicEffect(), UNIT_NONE); + } + if (physiologicalMetrics.getMetMax() != null) { + summaryData.add(MAXIMUM_OXYGEN_UPTAKE, physiologicalMetrics.getMetMax().floatValue() * 3.5f, UNIT_ML_KG_MIN); + } + if (physiologicalMetrics.getRecoveryTime() != null) { + summaryData.add(RECOVERY_TIME, physiologicalMetrics.getRecoveryTime() * 60, UNIT_SECONDS); + } + if (physiologicalMetrics.getLactateThresholdHeartRate() != null) { + summaryData.add(LACTATE_THRESHOLD_HR, physiologicalMetrics.getLactateThresholdHeartRate(), UNIT_BPM); + } + } + + summary.setSummaryData(summaryData.toString()); + } + + private static ActivityKind getActivityKind(final Integer sport, final Integer subsport) { + final Optional garminSport = GarminSport.fromCodes(sport, subsport); + if (garminSport.isPresent()) { + return garminSport.get().getActivityKind(); + } else { + LOG.warn("Unknown garmin sport {}/{}", sport, subsport); + + final Optional optGarminSportFallback = GarminSport.fromCodes(sport, 0); + if (!optGarminSportFallback.isEmpty()) { + return optGarminSportFallback.get().getActivityKind(); + } + } + + return ActivityKind.UNKNOWN; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiActivitySummaryParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiActivitySummaryParser.java index acc1f3674..1ae37374c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiActivitySummaryParser.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/HuamiActivitySummaryParser.java @@ -40,7 +40,7 @@ public class HuamiActivitySummaryParser implements ActivitySummaryParser { private static final Logger LOG = LoggerFactory.getLogger(HuamiActivityDetailsParser.class); protected ActivitySummaryData summaryData = new ActivitySummaryData(); - public BaseActivitySummary parseBinaryData(BaseActivitySummary summary) { + public BaseActivitySummary parseBinaryData(BaseActivitySummary summary, final boolean forDetails) { Date startTime = summary.getStartTime(); if (startTime == null) { LOG.error("Due to a bug, we can only parse the summary when startTime is already set"); @@ -57,7 +57,11 @@ public class HuamiActivitySummaryParser implements ActivitySummaryParser { } protected void parseBinaryData(BaseActivitySummary summary, Date startTime) { - ByteBuffer buffer = ByteBuffer.wrap(summary.getRawSummaryData()).order(ByteOrder.LITTLE_ENDIAN); + final byte[] rawSummaryData = summary.getRawSummaryData(); + if (rawSummaryData == null) { + return; + } + final ByteBuffer buffer = ByteBuffer.wrap(rawSummaryData).order(ByteOrder.LITTLE_ENDIAN); short version = buffer.getShort(); // version LOG.debug("Got sport summary version " + version + " total bytes=" + buffer.capacity()); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/zeppos/ZeppOsActivitySummaryParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/zeppos/ZeppOsActivitySummaryParser.java index 40dde78fb..b4ed99e2b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/zeppos/ZeppOsActivitySummaryParser.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/zeppos/ZeppOsActivitySummaryParser.java @@ -45,6 +45,10 @@ public class ZeppOsActivitySummaryParser extends HuamiActivitySummaryParser { @Override protected void parseBinaryData(final BaseActivitySummary summary, final Date startTime) { final byte[] rawData = summary.getRawSummaryData(); + if (rawData == null) { + return; + } + final int version = (rawData[0] & 0xff) | ((rawData[1] & 0xff) << 8); if (version != 0x8000) { LOG.warn("Unexpected binary data version {}, parsing might fail", version); @@ -85,7 +89,7 @@ public class ZeppOsActivitySummaryParser extends HuamiActivitySummaryParser { summary.setBaseLatitude(summaryProto.getLocation().getBaseLatitude()); summary.setBaseAltitude(summaryProto.getLocation().getBaseAltitude() / 2); // TODO: Min/Max Latitude/Longitude - summaryData.add(ALTITUDE_BASE, summaryProto.getLocation().getBaseAltitude() / 2, UNIT_METERS); + summaryData.add(ALTITUDE_BASE, summaryProto.getLocation().getBaseAltitude() / 2f, UNIT_METERS); } if (summaryProto.hasHeartRate()) { @@ -136,12 +140,12 @@ public class ZeppOsActivitySummaryParser extends HuamiActivitySummaryParser { } if (summaryProto.hasAltitude()) { - summaryData.add(ALTITUDE_MAX, summaryProto.getAltitude().getMaxAltitude() / 200, UNIT_METERS); - summaryData.add(ALTITUDE_MIN, summaryProto.getAltitude().getMinAltitude() / 200, UNIT_METERS); - summaryData.add(ALTITUDE_AVG, summaryProto.getAltitude().getAvgAltitude() / 200, UNIT_METERS); + summaryData.add(ALTITUDE_MAX, summaryProto.getAltitude().getMaxAltitude() / 200f, UNIT_METERS); + summaryData.add(ALTITUDE_MIN, summaryProto.getAltitude().getMinAltitude() / 200f, UNIT_METERS); + summaryData.add(ALTITUDE_AVG, summaryProto.getAltitude().getAvgAltitude() / 200f, UNIT_METERS); // TODO totalClimbing - summaryData.add(ELEVATION_GAIN, summaryProto.getAltitude().getElevationGain() / 100, UNIT_METERS); - summaryData.add(ELEVATION_LOSS, summaryProto.getAltitude().getElevationLoss() / 100, UNIT_METERS); + summaryData.add(ELEVATION_GAIN, summaryProto.getAltitude().getElevationGain() / 100f, UNIT_METERS); + summaryData.add(ELEVATION_LOSS, summaryProto.getAltitude().getElevationLoss() / 100f, UNIT_METERS); } if (summaryProto.hasElevation()) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/activity/TestActivitySummaryParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/activity/TestActivitySummaryParser.java index 19503354f..a62ceb738 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/activity/TestActivitySummaryParser.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/test/activity/TestActivitySummaryParser.java @@ -21,7 +21,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser; public class TestActivitySummaryParser implements ActivitySummaryParser { @Override - public BaseActivitySummary parseBinaryData(final BaseActivitySummary summary) { + public BaseActivitySummary parseBinaryData(final BaseActivitySummary summary, final boolean forDetails) { return summary; } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryJsonSummary.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryJsonSummary.java index 82238d142..afe2960ea 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryJsonSummary.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryJsonSummary.java @@ -26,7 +26,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Arrays; -import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; @@ -47,8 +46,8 @@ public class ActivitySummaryJsonSummary { this.baseActivitySummary=baseActivitySummary; } - private JSONObject setSummaryData(BaseActivitySummary item){ - String summary = getCorrectSummary(item); + private JSONObject setSummaryData(BaseActivitySummary item, final boolean forDetails){ + String summary = getCorrectSummary(item, forDetails); JSONObject jsonSummary = getJSONSummary(summary); if (jsonSummary != null) { //add additionally computed values here @@ -84,16 +83,16 @@ public class ActivitySummaryJsonSummary { return jsonSummary; } - public JSONObject getSummaryData(){ + public JSONObject getSummaryData(final boolean forDetails){ //returns json with summaryData - if (summaryData==null) summaryData=setSummaryData(baseActivitySummary); + if (summaryData==null) summaryData=setSummaryData(baseActivitySummary, forDetails); return summaryData; } - private String getCorrectSummary(BaseActivitySummary item){ - if (item.getRawSummaryData() != null) { + private String getCorrectSummary(BaseActivitySummary item, final boolean forDetails){ + if (item.getRawSummaryData() != null || item.getRawDetailsPath() != null) { try { - item = summaryParser.parseBinaryData(item); + item = summaryParser.parseBinaryData(item, forDetails); } catch (final Exception e) { LOG.error("Failed to re-parse corrected summary", e); } @@ -114,7 +113,7 @@ public class ActivitySummaryJsonSummary { public JSONObject getSummaryGroupedList() { //returns list grouped by activity groups as per createActivitySummaryGroups - if (summaryData==null) summaryData=setSummaryData(baseActivitySummary); + if (summaryData==null) summaryData=setSummaryData(baseActivitySummary, true); if (summaryGroupedList==null) summaryGroupedList=setSummaryGroupedList(summaryData); return summaryGroupedList; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryParser.java index 1be80011a..527d8611b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryParser.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryParser.java @@ -19,5 +19,15 @@ package nodomain.freeyourgadget.gadgetbridge.model; import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary; public interface ActivitySummaryParser { - BaseActivitySummary parseBinaryData(BaseActivitySummary summary); + /** + * Re-parse an existing {@link BaseActivitySummary}, updating it from the existing binary data. + * + * @param summary the existing {@link BaseActivitySummary}. It's not guaranteed that it + * contains any raw binary data. + * @param forDetails whether the parsing is for the details page. If this is false, the parser + * should avoid slow operations such as reading and parsing raw files from + * storage. + * @return the update {@link BaseActivitySummary} + */ + BaseActivitySummary parseBinaryData(BaseActivitySummary summary, final boolean forDetails); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/cmfwatchpro/CmfActivitySync.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/cmfwatchpro/CmfActivitySync.java index 8ee2c8ec7..d4d2010bd 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/cmfwatchpro/CmfActivitySync.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/cmfwatchpro/CmfActivitySync.java @@ -408,7 +408,7 @@ public class CmfActivitySync { summary.setActivityKind(ActivityKind.UNKNOWN.getCode()); try { - summary = summaryParser.parseBinaryData(summary); + summary = summaryParser.parseBinaryData(summary, true); } catch (final Exception e) { LOG.error("Failed to parse workout summary", e); GB.toast(getContext(), "Failed to parse workout summary", Toast.LENGTH_LONG, GB.ERROR, e); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitImporter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitImporter.java index f93d5da4c..b0c56539e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitImporter.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitImporter.java @@ -1,34 +1,5 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit; -import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.ACTIVE_SECONDS; -import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.ASCENT_DISTANCE; -import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.CALORIES_BURNT; -import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.DESCENT_DISTANCE; -import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.DISTANCE_METERS; -import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.ESTIMATED_SWEAT_LOSS; -import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_AVG; -import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_MAX; -import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_AEROBIC; -import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_ANAEROBIC; -import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_EXTREME; -import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_FAT_BURN; -import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_NA; -import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_WARM_UP; -import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.LACTATE_THRESHOLD_HR; -import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.MAXIMUM_OXYGEN_UPTAKE; -import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.RECOVERY_TIME; -import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.TRAINING_EFFECT_AEROBIC; -import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.TRAINING_EFFECT_ANAEROBIC; -import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_BPM; -import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_HOURS; -import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_KCAL; -import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_METERS; -import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_ML; -import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_ML_KG_MIN; -import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_NONE; -import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS; -import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.WORKOUT_LOAD; - import android.content.Context; import android.widget.Toast; @@ -43,7 +14,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.SortedMap; import java.util.TreeMap; @@ -59,6 +29,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminHrvValueSampleP import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminSleepStageSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminSpo2SampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminStressSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminWorkoutParser; import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary; import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; @@ -74,10 +45,8 @@ import nodomain.freeyourgadget.gadgetbridge.entities.GarminStressSample; import nodomain.freeyourgadget.gadgetbridge.entities.User; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; -import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; -import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData; -import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.enums.GarminSport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.FileType; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionHrvStatus; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionSleepStage; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitEvent; @@ -109,25 +78,29 @@ public class FitImporter { private final List sleepStageSamples = new ArrayList<>(); private final List hrvSummarySamples = new ArrayList<>(); private final List hrvValueSamples = new ArrayList<>(); - private final List timesInZone = new ArrayList<>(); - private final List activityPoints = new ArrayList<>(); private final Map unknownRecords = new HashMap<>(); private FitFileId fileId = null; - private FitSession session = null; - private FitSport sport = null; - private FitPhysiologicalMetrics physiologicalMetrics = null; + + private final GarminWorkoutParser workoutParser = new GarminWorkoutParser(); public FitImporter(final Context context, final GBDevice gbDevice) { this.context = context; this.gbDevice = gbDevice; } + /** @noinspection StatementWithEmptyBody*/ public void importFile(final File file) throws IOException { reset(); final FitFile fitFile = FitFile.parseIncoming(file); for (final RecordData record : fitFile.getRecords()) { + if (fileId != null && fileId.getType() == FileType.FILETYPE.ACTIVITY) { + if (workoutParser.handleRecord(record)) { + continue; + } + } + final Long ts = record.getComputedTimestamp(); if (record instanceof FitFileId) { @@ -203,29 +176,15 @@ public class FitImporter { } events.add(sample); } else if (record instanceof FitRecord) { - activityPoints.add(((FitRecord) record).toActivityPoint()); + // handled in workout parser } else if (record instanceof FitSession) { - LOG.debug("Session: {}", record); - if (session != null) { - LOG.warn("Got multiple sessions - NOT SUPPORTED: {}", record); - } else { - // We only support 1 session - session = (FitSession) record; - } + // handled in workout parser } else if (record instanceof FitPhysiologicalMetrics) { - LOG.debug("Physiological Metrics: {}", record); - physiologicalMetrics = (FitPhysiologicalMetrics) record; + // handled in workout parser } else if (record instanceof FitSport) { - LOG.debug("Sport: {}", record); - if (sport != null) { - LOG.warn("Got multiple sports - NOT SUPPORTED: {}", record); - } else { - // We only support 1 sport - sport = (FitSport) record; - } + // handled in workout parser } else if (record instanceof FitTimeInZone) { - LOG.trace("Time in zone: {}", record); - timesInZone.add((FitTimeInZone) record); + // handled in workout parser } else if (record instanceof FitHrvSummary) { final FitHrvSummary hrvSummary = (FitHrvSummary) record; LOG.trace("HRV summary at {}: {}", ts, record); @@ -315,11 +274,6 @@ public class FitImporter { } private void persistWorkout(final File file) { - if (session == null) { - LOG.error("Got workout from {}, but no session", fileId); - return; - } - LOG.debug("Persisting workout for {}", fileId); final BaseActivitySummary summary; @@ -335,87 +289,9 @@ public class FitImporter { return; } - final ActivitySummaryData summaryData = new ActivitySummaryData(); + workoutParser.updateSummary(summary); - final ActivityKind activityKind; - if (sport != null) { - summary.setName(sport.getName()); - activityKind = getActivityKind(sport.getSport(), sport.getSubSport()); - } else { - activityKind = getActivityKind(session.getSport(), session.getSubSport()); - } - summary.setActivityKind(activityKind.getCode()); - - if (session.getTotalElapsedTime() == null) { - LOG.error("No elapsed time for {}", fileId); - return; - } - summary.setEndTime(new Date(summary.getStartTime().getTime() + session.getTotalElapsedTime().intValue())); - - if (session.getTotalTimerTime() != null) { - summaryData.add(ACTIVE_SECONDS, session.getTotalTimerTime() / 1000f, UNIT_SECONDS); - } - if (session.getTotalDistance() != null) { - summaryData.add(DISTANCE_METERS, session.getTotalDistance() / 100f, UNIT_METERS); - } - if (session.getTotalCalories() != null) { - summaryData.add(CALORIES_BURNT, session.getTotalCalories(), UNIT_KCAL); - } - if (session.getEstimatedSweatLoss() != null) { - summaryData.add(ESTIMATED_SWEAT_LOSS, session.getEstimatedSweatLoss(), UNIT_ML); - } - if (session.getAverageHeartRate() != null) { - summaryData.add(HR_AVG, session.getAverageHeartRate(), UNIT_BPM); - } - if (session.getMaxHeartRate() != null) { - summaryData.add(HR_MAX, session.getMaxHeartRate(), UNIT_BPM); - } - if (session.getTotalAscent() != null) { - summaryData.add(ASCENT_DISTANCE, session.getTotalAscent(), UNIT_METERS); - } - if (session.getTotalDescent() != null) { - summaryData.add(DESCENT_DISTANCE, session.getTotalDescent(), UNIT_METERS); - } - - for (final FitTimeInZone fitTimeInZone : timesInZone) { - // Find the firt time in zone for the session (assumes single-session) - if (fitTimeInZone.getReferenceMessage() != null && fitTimeInZone.getReferenceMessage() == 18) { - final Double[] timeInZone = fitTimeInZone.getTimeInZone(); - if (timeInZone != null && timeInZone.length == 6) { - summaryData.add(HR_ZONE_NA, timeInZone[0].floatValue(), UNIT_SECONDS); - summaryData.add(HR_ZONE_WARM_UP, timeInZone[1].floatValue(), UNIT_SECONDS); - summaryData.add(HR_ZONE_FAT_BURN, timeInZone[2].floatValue(), UNIT_SECONDS); - summaryData.add(HR_ZONE_AEROBIC, timeInZone[3].floatValue(), UNIT_SECONDS); - summaryData.add(HR_ZONE_ANAEROBIC, timeInZone[4].floatValue(), UNIT_SECONDS); - summaryData.add(HR_ZONE_EXTREME, timeInZone[5].floatValue(), UNIT_SECONDS); - } - break; - } - } - - if (physiologicalMetrics != null) { - // TODO lactate_threshold_heart_rate - if (physiologicalMetrics.getAerobicEffect() != null) { - summaryData.add(TRAINING_EFFECT_AEROBIC, physiologicalMetrics.getAerobicEffect(), UNIT_NONE); - } - if (physiologicalMetrics.getAnaerobicEffect() != null) { - summaryData.add(TRAINING_EFFECT_ANAEROBIC, physiologicalMetrics.getAnaerobicEffect(), UNIT_NONE); - } - if (physiologicalMetrics.getMetMax() != null) { - summaryData.add(MAXIMUM_OXYGEN_UPTAKE, physiologicalMetrics.getMetMax().floatValue() * 3.5f, UNIT_ML_KG_MIN); - } - if (physiologicalMetrics.getRecoveryTime() != null) { - summaryData.add(RECOVERY_TIME, physiologicalMetrics.getRecoveryTime() / 60f, UNIT_HOURS); - } - if (physiologicalMetrics.getLactateThresholdHeartRate() != null) { - summaryData.add(LACTATE_THRESHOLD_HR, physiologicalMetrics.getLactateThresholdHeartRate(), UNIT_BPM); - } - } - - summary.setSummaryData(summaryData.toString()); - if (file != null) { - summary.setRawDetailsPath(file.getAbsolutePath()); - } + summary.setRawDetailsPath(file.getAbsolutePath()); try (DBHandler dbHandler = GBApplication.acquireDB()) { final DaoSession session = dbHandler.getDaoSession(); @@ -431,22 +307,6 @@ public class FitImporter { } } - private ActivityKind getActivityKind(final Integer sport, final Integer subsport) { - final Optional garminSport = GarminSport.fromCodes(sport, subsport); - if (garminSport.isPresent()) { - return garminSport.get().getActivityKind(); - } else { - LOG.warn("Unknown garmin sport {}/{}", sport, subsport); - - final Optional optGarminSportFallback = GarminSport.fromCodes(sport, 0); - if (!optGarminSportFallback.isEmpty()) { - return optGarminSportFallback.get().getActivityKind(); - } - } - - return ActivityKind.UNKNOWN; - } - protected static BaseActivitySummary findOrCreateBaseActivitySummary(final DaoSession session, final Device device, final User user, @@ -484,13 +344,9 @@ public class FitImporter { sleepStageSamples.clear(); hrvSummarySamples.clear(); hrvValueSamples.clear(); - timesInZone.clear(); - activityPoints.clear(); unknownRecords.clear(); fileId = null; - session = null; - sport = null; - physiologicalMetrics = null; + workoutParser.reset(); } private void persistActivitySamples() { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/fetch/FetchSportsSummaryOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/fetch/FetchSportsSummaryOperation.java index 445b266de..4204f1f8f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/fetch/FetchSportsSummaryOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/fetch/FetchSportsSummaryOperation.java @@ -81,7 +81,7 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation { summary.setStartTime(getLastStartTimestamp().getTime()); // due to a bug this has to be set summary.setRawSummaryData(buffer.toByteArray()); try { - summary = summaryParser.parseBinaryData(summary); + summary = summaryParser.parseBinaryData(summary, true); } catch (final Exception e) { GB.toast(getContext(), "Failed to parse activity summary", Toast.LENGTH_LONG, GB.ERROR, e); return false; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/impl/WorkoutSummaryParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/impl/WorkoutSummaryParser.java index d626f1cef..c797db033 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/impl/WorkoutSummaryParser.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/activity/impl/WorkoutSummaryParser.java @@ -28,7 +28,6 @@ import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries. import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_EXTREME; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_FAT_BURN; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_WARM_UP; -import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.LANE_LENGTH; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.LAPS; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.LAP_PACE_AVERAGE; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.PACE_AVG_SECONDS_KM; @@ -42,19 +41,16 @@ import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries. import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.STROKE_RATE_AVG; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.SWIM_STYLE; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.SWOLF_AVG; -import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.SWOLF_INDEX; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.TIME_END; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.TIME_START; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.TRAINING_EFFECT_AEROBIC; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.TRAINING_EFFECT_ANAEROBIC; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_BPM; -import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_CM; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_HOURS; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_KCAL; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_KMPH; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_LAPS; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_METERS; -import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_METERS_PER_SECOND; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_NONE; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS_PER_KM; @@ -71,13 +67,11 @@ import android.widget.Toast; import androidx.annotation.Nullable; -import org.apache.commons.lang3.ArrayUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.util.Arrays; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; @@ -108,7 +102,7 @@ public class WorkoutSummaryParser extends XiaomiActivityParser implements Activi summary.setRawSummaryData(bytes); try { - summary = parseBinaryData(summary); + summary = parseBinaryData(summary, true); } catch (final Exception e) { LOG.error("Failed to parse workout summary", e); GB.toast(support.getContext(), "Failed to parse workout summary", Toast.LENGTH_LONG, GB.ERROR, e); @@ -144,8 +138,11 @@ public class WorkoutSummaryParser extends XiaomiActivityParser implements Activi } @Override - public BaseActivitySummary parseBinaryData(final BaseActivitySummary summary) { + public BaseActivitySummary parseBinaryData(final BaseActivitySummary summary, final boolean forDetails) { final byte[] data = summary.getRawSummaryData(); + if (data == null) { + return summary; + } final int arrCrc32 = CheckSums.getCRC32(data, 0, data.length - 4); final int expectedCrc32 = BLETypeConversions.toUint32(data, data.length - 4);