diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/charts/DefaultWorkoutCharts.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/charts/DefaultWorkoutCharts.java index 1bfbf531df..22233b5384 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/charts/DefaultWorkoutCharts.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/charts/DefaultWorkoutCharts.java @@ -41,7 +41,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.Accumulator; public class DefaultWorkoutCharts { public static List buildDefaultCharts(final Context context, - final List activityPoints, + final List activityPoints, final ActivityKind activityKind) { final ActivityKind.CycleUnit cycleUnit = ActivityKind.getCycleUnit(activityKind); final List charts = new LinkedList<>(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/entries/ActivitySummaryGroup.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/entries/ActivitySummaryGroup.kt index 638ef4a593..ff59e9e05c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/entries/ActivitySummaryGroup.kt +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/entries/ActivitySummaryGroup.kt @@ -176,6 +176,8 @@ object ActivitySummaryGroup { ActivitySummaryEntries.JUMPS, ActivitySummaryEntries.JUMP_RATE_AVG, ActivitySummaryEntries.JUMP_RATE_MAX, + ActivitySummaryEntries.JUMP_ROPE_LONGEST_STREAK, + ActivitySummaryEntries.JUMP_ROPE_INTERRUPTIONS, ) ) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryEntries.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryEntries.java index be0b7902c2..eca8469646 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryEntries.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/ActivitySummaryEntries.java @@ -154,6 +154,8 @@ public class ActivitySummaryEntries { public static final String JUMP_RATE_AVG = "avgJumpRate"; public static final String JUMP_RATE_MAX = "maxJumpRate"; public static final String JUMPS = "totalJumps"; + public static final String JUMP_ROPE_LONGEST_STREAK = "jump_rope_longest_streak"; + public static final String JUMP_ROPE_INTERRUPTIONS = "jump_rope_interruptions"; public static final String SWIM_STYLE = "swimStyle"; public static final String SWOLF_INDEX = "swolfIndex"; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiWorkoutGbParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiWorkoutGbParser.java index b74f4cc4a1..430121bc3b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiWorkoutGbParser.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiWorkoutGbParser.java @@ -45,6 +45,7 @@ import de.greenrobot.dao.query.QueryBuilder; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity; +import nodomain.freeyourgadget.gadgetbridge.activities.charts.TimestampTranslation; import nodomain.freeyourgadget.gadgetbridge.activities.workouts.charts.DefaultWorkoutCharts; import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryProgressEntry; import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryTableRowEntry; @@ -88,6 +89,36 @@ import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; public class HuaweiWorkoutGbParser implements ActivitySummaryParser { private static final Logger LOG = LoggerFactory.getLogger(HuaweiWorkoutGbParser.class); + public static class HuaweiActivityPoint extends ActivityPoint { + private int swolf = -1; + private int strokeRate = -1; + private int frequency = -1; + + public int getSwolf() { + return swolf; + } + + public void setSwolf(int swolf) { + this.swolf = swolf; + } + + public int getStrokeRate() { + return strokeRate; + } + + public void setStrokeRate(int strokeRate) { + this.strokeRate = strokeRate; + } + + public int getFrequency() { + return frequency; + } + + public void setFrequency(int frequency) { + this.frequency = frequency; + } + } + // TODO: Might be nicer to propagate the exceptions, so they can be handled upstream private final GBDevice gbDevice; @@ -104,6 +135,82 @@ public class HuaweiWorkoutGbParser implements ActivitySummaryParser { return parseWorkout(summary, forDetails).getSummary(); } + private static WorkoutChart createSwolfChart(final Context context, + final List swolfDataPoints) { + final String label = String.format("%s", context.getString(R.string.swolfIndex)); + final LineDataSet dataset = DefaultWorkoutCharts.createLineDataSet(context, swolfDataPoints, label, ContextCompat.getColor(context, R.color.chart_line_swolf)); + return new WorkoutChart( + "swolf", + context.getString(R.string.swolfIndex), + ActivitySummaryEntries.GROUP_SWIMMING, + new LineData(dataset), + null, + DefaultWorkoutCharts.getUnitString(context, ActivitySummaryEntries.UNIT_NONE) + ); + } + + private static WorkoutChart createStrokeRateChart(final Context context, + final List strokesDataPoints) { + final String label = String.format("%s (%s)", context.getString(R.string.stroke_rate), DefaultWorkoutCharts.getUnitString(context, ActivitySummaryEntries.UNIT_STROKES_PER_MINUTE)); + final LineDataSet dataset = DefaultWorkoutCharts.createLineDataSet(context, strokesDataPoints, label, ContextCompat.getColor(context, R.color.chart_line_stroke_rate)); + return new WorkoutChart( + "strokesRate", + context.getString(R.string.stroke_rate), + ActivitySummaryEntries.GROUP_STROKES, + new LineData(dataset), + null, + DefaultWorkoutCharts.getUnitString(context, ActivitySummaryEntries.UNIT_STROKES_PER_MINUTE) + ); + } + + private static WorkoutChart createFrequencyChart(final Context context, + final List frequencyDataPoints) { + final String label = String.format("%s (%s)", context.getString(R.string.Speed), DefaultWorkoutCharts.getUnitString(context, ActivitySummaryEntries.UNIT_JUMPS_PER_MINUTE)); + final LineDataSet dataset = DefaultWorkoutCharts.createLineDataSet(context, frequencyDataPoints, label, ContextCompat.getColor(context, R.color.chart_line_speed)); + return new WorkoutChart( + "frequency", + context.getString(R.string.Speed), + ActivitySummaryEntries.GROUP_JUMPS, + new LineData(dataset), + null, + DefaultWorkoutCharts.getUnitString(context, ActivitySummaryEntries.UNIT_JUMPS_PER_MINUTE) + ); + } + + private List buildHuaweiCharts(final Context context, + final List activityPoints) { + + final List charts = new LinkedList<>(); + final TimestampTranslation tsTranslation = new TimestampTranslation(); + final List swolfDataPoints = new ArrayList<>(); + final List strokeRateDataPoints = new ArrayList<>(); + final List frequencyDataPoints = new ArrayList<>(); + + for (int i = 0; i <= activityPoints.size() - 1; i++) { + final HuaweiActivityPoint point = activityPoints.get(i); + final long tsShorten = tsTranslation.shorten((int) point.getTime().getTime()); + if (point.getSwolf() >= 0) { + swolfDataPoints.add(new Entry(tsShorten, point.getSwolf())); + } + if (point.getStrokeRate() >= 0) { + strokeRateDataPoints.add(new Entry(tsShorten, point.getStrokeRate())); + } + if (point.getFrequency() >= 0) { + frequencyDataPoints.add(new Entry(tsShorten, point.getFrequency())); + } + } + if (!swolfDataPoints.isEmpty()) { + charts.add(createSwolfChart(context, swolfDataPoints)); + } + if (!strokeRateDataPoints.isEmpty()) { + charts.add(createStrokeRateChart(context, strokeRateDataPoints)); + } + if (!frequencyDataPoints.isEmpty()) { + charts.add(createFrequencyChart(context, frequencyDataPoints)); + } + return charts; + } + @Override public nodomain.freeyourgadget.gadgetbridge.model.workout.Workout parseWorkout(BaseActivitySummary summary, final boolean forDetails) { if (!forDetails) { @@ -126,12 +233,13 @@ public class HuaweiWorkoutGbParser implements ActivitySummaryParser { LOG.warn("Failed to find huawei summary for {}", summary.getStartTime()); return new nodomain.freeyourgadget.gadgetbridge.model.workout.Workout(summary, ActivitySummaryData.fromJson(summary.getSummaryData())); } - final List activityPoints = new ArrayList<>(); + final List activityPoints = new ArrayList<>(); final ActivitySummaryData activitySummaryData = updateBaseSummary(session, huaweiSummaries.get(0), summary, activityPoints); final ActivityKind activityKind = ActivityKind.fromCode(summary.getActivityKind()); final List charts = new LinkedList<>(); if (!activityPoints.isEmpty()) { charts.addAll(DefaultWorkoutCharts.buildDefaultCharts(context, activityPoints, activityKind)); + charts.addAll(buildHuaweiCharts(context, activityPoints)); } byte[] recoveryHR = huaweiSummaries.get(0).getRecoveryHeartRates(); if (recoveryHR != null && recoveryHR.length > 0) { @@ -421,7 +529,7 @@ public class HuaweiWorkoutGbParser implements ActivitySummaryParser { summary.getStartTimestamp() ); - final List activityPoints = new ArrayList<>(); + final List activityPoints = new ArrayList<>(); updateBaseSummary(session, summary, baseSummary, activityPoints); session.getBaseActivitySummaryDao().insertOrReplace(baseSummary); @@ -527,7 +635,8 @@ public class HuaweiWorkoutGbParser implements ActivitySummaryParser { public ActivitySummaryData updateBaseSummary(final DaoSession session, final HuaweiWorkoutSummarySample summary, - final BaseActivitySummary baseSummary, final List activityPoints) { + final BaseActivitySummary baseSummary, + final List activityPoints) { ActivitySummaryData summaryData = new ActivitySummaryData(); @@ -618,6 +727,15 @@ public class HuaweiWorkoutGbParser implements ActivitySummaryParser { summaryData.add(ActivitySummaryEntries.MAXIMUM_OXYGEN_UPTAKE, oxygenUptake, ActivitySummaryEntries.UNIT_ML_KG_MIN); } + if (summary.getLongestStreak() > 0) { + summaryData.add(ActivitySummaryEntries.JUMP_ROPE_LONGEST_STREAK, summary.getLongestStreak(), ActivitySummaryEntries.UNIT_JUMPS); + } + + if (summary.getTripped() > 0) { + summaryData.add(ActivitySummaryEntries.JUMP_ROPE_INTERRUPTIONS, summary.getTripped(), ActivitySummaryEntries.UNIT_NONE); + } + + Integer summaryMinAltitude = summary.getMinAltitude(); Integer summaryMaxAltitude = summary.getMaxAltitude(); Integer elevationGain = summary.getElevationGain(); @@ -676,6 +794,8 @@ public class HuaweiWorkoutGbParser implements ActivitySummaryParser { int sumAltitudeUp = 0; int sumAltitudeDown = 0; + int maxFrequency = -1; + //NOTE: The method of retrieving HR zones from the Huawei watch is not discovered. It may not return zones. // So they are calculated based on config. HeartRateZonesConfig HRZonesCfg = null; @@ -705,9 +825,10 @@ public class HuaweiWorkoutGbParser implements ActivitySummaryParser { int dataIdx = 0; for (HuaweiWorkoutDataSample dataSample : dataSamples) { - ActivityPoint ac = new ActivityPoint(); + HuaweiActivityPoint ac = new HuaweiActivityPoint(); ac.setTime(new Date(dataSample.getTimestamp() * 1000L)); + if (HRZonesCfg != null) { int zoneIdx = HRZonesCfg.getZoneByMethod(dataSample.getHeartRate() & 0xFF, zoneCalculateMethod); if (zoneIdx != -1 && dataIdx < (dataSamples.size() - 1)) { @@ -777,12 +898,15 @@ public class HuaweiWorkoutGbParser implements ActivitySummaryParser { maxSwolf = dataSample.getSwolf(); if (dataSample.getSwolf() < minSwolf) minSwolf = dataSample.getSwolf(); + + ac.setSwolf(dataSample.getSwolf()); } if (dataSample.getStrokeRate() != -1) { strokeRate += dataSample.getStrokeRate(); strokeRateCount += 1; if (dataSample.getStrokeRate() > maxStrokeRate) maxStrokeRate = dataSample.getStrokeRate(); + ac.setStrokeRate(dataSample.getStrokeRate()); } if (dataSample.getHeartRate() != -1 && dataSample.getHeartRate() != 0) { int hr = dataSample.getHeartRate() & 0xff; @@ -795,6 +919,11 @@ public class HuaweiWorkoutGbParser implements ActivitySummaryParser { ac.setHeartRate(dataSample.getHeartRate() & 0xff); } + if (dataSample.getFrequency() != -1) { + if (dataSample.getFrequency() > maxFrequency) + maxFrequency = dataSample.getFrequency(); + ac.setFrequency(dataSample.getFrequency()); + } if (dataSample.getCalories() != -1) sumCalories += dataSample.getCalories(); if (dataSample.getCyclingPower() != -1) { @@ -830,7 +959,6 @@ public class HuaweiWorkoutGbParser implements ActivitySummaryParser { activityPoints.add(ac); } - if (HRZonesCfg != null) { final double totalTime = Arrays.stream(HRZones).sum(); final List zoneOrder = Arrays.asList(ActivitySummaryEntries.HR_ZONE_WARM_UP, ActivitySummaryEntries.HR_ZONE_FAT_BURN, ActivitySummaryEntries.HR_ZONE_AEROBIC, ActivitySummaryEntries.HR_ZONE_ANAEROBIC, ActivitySummaryEntries.HR_ZONE_EXTREME); @@ -894,7 +1022,11 @@ public class HuaweiWorkoutGbParser implements ActivitySummaryParser { } if (avgStepRate > 0) { - summaryData.add(ActivitySummaryEntries.STEP_RATE_AVG, avgStepRate, ActivitySummaryEntries.UNIT_SPM); + if (type == ActivityKind.JUMP_ROPING) { + summaryData.add(ActivitySummaryEntries.JUMP_RATE_AVG, avgStepRate, ActivitySummaryEntries.UNIT_JUMPS_PER_MINUTE); + } else { + summaryData.add(ActivitySummaryEntries.STEP_RATE_AVG, avgStepRate, ActivitySummaryEntries.UNIT_SPM); + } } if (cadenceCount > 0) { @@ -984,6 +1116,10 @@ public class HuaweiWorkoutGbParser implements ActivitySummaryParser { elevationLoss = sumAltitudeDown; } } + + if (maxFrequency > 0) { + summaryData.add(ActivitySummaryEntries.JUMP_RATE_MAX, maxFrequency, ActivitySummaryEntries.UNIT_JUMPS_PER_MINUTE); + } } if (avgSwolf > 0) { diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 79d69e56ca..1d0369a6b6 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -94,6 +94,8 @@ #66BB6A #F44336 #6A0DAD + #F9DB22 + #6495ED #ffff8800 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 344fbd1a1d..417b31ced6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2578,9 +2578,12 @@ Average Stroke Rate Max Stroke Rate Total Strokes + Stroke Rate Average Jump Rate Max Jump Rate Total Jumps + Longest Streak + Interruptions Average Lap Pace SWOLF Average swolf