Workout details: Code cleanup

This commit is contained in:
José Rebelo
2025-08-18 00:01:00 +01:00
parent 89281fdd6b
commit 244b46c7f2
9 changed files with 140 additions and 158 deletions
@@ -336,6 +336,7 @@ class WorkoutDetailsFragment : Fragment(), MenuProvider {
private fun updateFragments(workout: Workout) {
val trackFile = ActivitySummaryUtils.getTrackFile(workout.summary)
// If there's a device-specific HR chart, prefer it over the default one
if (workout.charts.any { chart -> chart.group == ActivitySummaryEntries.GROUP_HEART_RATE }) {
binding.heartRateChartWrapper.visibility = View.GONE
} else {
@@ -368,18 +369,6 @@ class WorkoutDetailsFragment : Fragment(), MenuProvider {
chart: WorkoutChart
) {
if (includeHeader) {
val separator1 = View(context).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
(2 * resources.displayMetrics.density).toInt()
)
val typedValue = TypedValue()
context.theme.resolveAttribute(R.attr.row_separator, typedValue, true)
setBackgroundColor(ContextCompat.getColor(context, typedValue.resourceId))
}
chartsLayout.addView(separator1)
val chartTitle = TextView(context).apply {
id = View.generateViewId()
layoutParams = LinearLayout.LayoutParams(
@@ -394,19 +383,10 @@ class WorkoutDetailsFragment : Fragment(), MenuProvider {
val paddingPx = (16 * resources.displayMetrics.density).toInt()
setPadding(paddingPx, paddingPx, paddingPx, paddingPx)
}
chartsLayout.addView(createSeparator())
chartsLayout.addView(chartTitle)
val separator2 = View(context).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
(2 * resources.displayMetrics.density).toInt()
)
val typedValue = TypedValue()
context.theme.resolveAttribute(R.attr.row_separator, typedValue, true)
setBackgroundColor(ContextCompat.getColor(context, typedValue.resourceId))
}
chartsLayout.addView(separator2)
chartsLayout.addView(createSeparator())
}
val chartsFragmentHolder = FrameLayout(requireContext()).apply {
@@ -458,8 +438,11 @@ class WorkoutDetailsFragment : Fragment(), MenuProvider {
chartsFragmentHolder.addView(lineChart)
chartsLayout.addView(chartsFragmentHolder)
chartsLayout.addView(createSeparator())
}
val separator3 = View(context).apply {
private fun createSeparator(): View {
return View(context).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
(2 * resources.displayMetrics.density).toInt()
@@ -469,7 +452,6 @@ class WorkoutDetailsFragment : Fragment(), MenuProvider {
context.theme.resolveAttribute(R.attr.row_separator, typedValue, true)
setBackgroundColor(ContextCompat.getColor(context, typedValue.resourceId))
}
chartsLayout.addView(separator3)
}
private fun workoutHasGps(summary: BaseActivitySummary): Boolean {
@@ -0,0 +1,107 @@
package nodomain.freeyourgadget.gadgetbridge.activities.workouts.charts;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_BPM;
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_SECONDS_PER_KM;
import android.content.Context;
import android.graphics.Color;
import com.github.mikephil.charting.components.YAxis;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.data.LineData;
import com.github.mikephil.charting.data.LineDataSet;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.SpeedYLabelFormatter;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.TimestampTranslation;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries;
import nodomain.freeyourgadget.gadgetbridge.model.workout.WorkoutChart;
public class DefaultWorkoutCharts {
public static List<WorkoutChart> buildDefaultCharts(final Context context,
final List<ActivityPoint> activityPoints,
final ActivityKind activityKind) {
final List<WorkoutChart> charts = new LinkedList<>();
final TimestampTranslation tsTranslation = new TimestampTranslation();
final List<Entry> heartRateDataPoints = new ArrayList<>();
final List<Entry> speedDataPoints = new ArrayList<>();
final List<Entry> elevationDataPoints = new ArrayList<>();
boolean hasSpeedValues = false;
for (int i = 0; i <= activityPoints.size() - 1; i++) {
final ActivityPoint point = activityPoints.get(i);
final long tsShorten = tsTranslation.shorten((int) point.getTime().getTime());
if (point.getHeartRate() > 0) {
heartRateDataPoints.add(new Entry(tsShorten, point.getHeartRate()));
}
if (point.getLocation() != null) {
elevationDataPoints.add(new Entry(tsShorten, (float) point.getLocation().getAltitude()));
}
speedDataPoints.add(new Entry(tsShorten, point.getSpeed()));
if (!hasSpeedValues && point.getSpeed() > 0) {
hasSpeedValues = true;
}
}
if (!heartRateDataPoints.isEmpty()) {
final String label = String.format("%s(%s)", context.getString(R.string.heart_rate), getUnitString(context, UNIT_BPM));
final LineDataSet dataset = createDataSet(context, heartRateDataPoints, label, Color.RED);
charts.add(new WorkoutChart(context.getString(R.string.heart_rate), ActivitySummaryEntries.GROUP_HEART_RATE, new LineData(dataset)));
}
if (hasSpeedValues && !speedDataPoints.isEmpty()) {
if (ActivityKind.isPaceActivity(activityKind)) {
final String label = String.format("%s (%s)", context.getString(R.string.Pace), getUnitString(context, UNIT_SECONDS_PER_KM));
final LineDataSet dataset = createDataSet(context, speedDataPoints, label, Color.BLUE);
charts.add(new WorkoutChart(context.getString(R.string.Pace), ActivitySummaryEntries.GROUP_SPEED, new LineData(dataset), new SpeedYLabelFormatter(UNIT_SECONDS_PER_KM)));
} else {
final String label = String.format("%s (%s)", context.getString(R.string.Speed), getUnitString(context, UNIT_METERS_PER_SECOND));
final LineDataSet dataset = createDataSet(context, speedDataPoints, label, Color.BLUE);
charts.add(new WorkoutChart(context.getString(R.string.Speed), ActivitySummaryEntries.GROUP_SPEED, new LineData(dataset), new SpeedYLabelFormatter(UNIT_METERS_PER_SECOND)));
}
}
if (!elevationDataPoints.isEmpty()) {
final String label = String.format("%s (%s)", context.getString(R.string.Elevation), getUnitString(context, UNIT_METERS));
LineDataSet dataset = createDataSet(context, elevationDataPoints, label, Color.GREEN);
charts.add(new WorkoutChart(context.getString(R.string.Elevation), ActivitySummaryEntries.GROUP_ELEVATION, new LineData(dataset)));
}
return charts;
}
public static String getUnitString(final Context context, final String unit) {
final int resId = context.getResources().getIdentifier(unit, "string", context.getPackageName());
if (resId != 0) {
return context.getString(resId);
}
return "";
}
public static LineDataSet createDataSet(final Context context,
final List<Entry> entities,
final String label,
final int color) {
final LineDataSet dataSet = new LineDataSet(entities, label);
dataSet.setMode(LineDataSet.Mode.HORIZONTAL_BEZIER);
dataSet.setCubicIntensity(0.05f);
dataSet.setDrawCircles(false);
dataSet.setAxisDependency(YAxis.AxisDependency.RIGHT);
dataSet.setColor(color);
dataSet.setValueTextColor(GBApplication.getSecondaryTextColor(context));
dataSet.setLineWidth(1.5f);
dataSet.setHighlightLineWidth(2f);
dataSet.setDrawValues(false);
dataSet.setDrawHorizontalHighlightIndicator(false);
return dataSet;
}
}
@@ -2,16 +2,12 @@ package nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.WorkoutValueFormatter;
public abstract class ActivitySummaryEntry implements Cloneable {
private String group;
public abstract class ActivitySummaryEntry {
private final String group;
protected int columnSpan;
public ActivitySummaryEntry(String group) {
public ActivitySummaryEntry(final String group) {
this.group = group;
}
@@ -19,29 +15,9 @@ public abstract class ActivitySummaryEntry implements Cloneable {
return group;
}
public void setGroup(String group) {
this.group = group;
}
public int getColumnSpan() {
return columnSpan;
}
public void setColumnSpan(int columnSpan) {
this.columnSpan = columnSpan;
}
public abstract int getColumnSpan();
public abstract void populate(final String key,
final LinearLayout linearLayout,
final WorkoutValueFormatter workoutValueFormatter);
@NonNull
@Override
public ActivitySummaryEntry clone() {
try {
return (ActivitySummaryEntry) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
@@ -14,17 +14,11 @@ object ActivitySummaryGroup {
activitySummaryData.keys
.filterNot { it.startsWith("internal") }
.forEach { key ->
var item = activitySummaryData[key]
val item = activitySummaryData[key]
// Use the group if specified in the entry, otherwise fallback to the default mapping from getDefaultGroup
val groups: List<String> = item.group?.let { listOf(it) } ?: getDefaultGroups(key)
for ((index, groupName) in groups.withIndex()) {
if (index > 0) {
item = item.clone();
}
if (groupName == ActivitySummaryEntries.GROUP_OTHER) {
item.columnSpan = 2;
}
for (groupName in groups) {
// If the group is not defined the default groups, it will be added to the end
val group = activeGroups.getOrPut(groupName) { mutableListOf() }
group.add(Pair.of<String, ActivitySummaryEntry>(key, item))
@@ -19,14 +19,9 @@ public class ActivitySummarySimpleEntry extends ActivitySummaryEntry {
}
public ActivitySummarySimpleEntry(final String group, final Object value, final String unit) {
this(group, value, unit, 1);
}
public ActivitySummarySimpleEntry(final String group, final Object value, final String unit, final int columnSpan) {
super(group);
this.value = value;
this.unit = unit;
this.columnSpan = columnSpan;
}
public Object getValue() {
@@ -37,6 +32,11 @@ public class ActivitySummarySimpleEntry extends ActivitySummaryEntry {
return unit;
}
@Override
public int getColumnSpan() {
return 1;
}
@Override
public void populate(final String key, final LinearLayout linearLayout, final WorkoutValueFormatter workoutValueFormatter) {
final Context context = linearLayout.getContext();
@@ -53,10 +53,10 @@ public class ActivitySummarySimpleEntry extends ActivitySummaryEntry {
labelTextView.setTextSize(12);
labelTextView.setText(workoutValueFormatter.getStringResourceByName(key));
if (columnSpan == 1) {
if (getColumnSpan() == 1) {
linearLayout.addView(valueTextView);
linearLayout.addView(labelTextView);
} else if (columnSpan == 2) {
} else if (getColumnSpan() == 2) {
// Label
labelTextView.setTextSize(14);
labelTextView.setMaxLines(1);
@@ -80,7 +80,7 @@ public class ActivitySummarySimpleEntry extends ActivitySummaryEntry {
linearLayout.addView(labelsLinearLayout);
} else {
throw new IllegalArgumentException("Invalid columnSpan " + columnSpan);
throw new IllegalArgumentException("Invalid columnSpan " + getColumnSpan());
}
}
}
@@ -86,7 +86,6 @@ import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_KMPH;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_LB;
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_MILLISECONDS;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_ML;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_ML_KG_MIN;
@@ -95,17 +94,10 @@ import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_PERCENTAGE;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_RPM;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS_PER_KM;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_STROKES_PER_LENGTH;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_WATT;
import android.content.Context;
import android.graphics.Color;
import com.github.mikephil.charting.components.YAxis;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.data.LineData;
import com.github.mikephil.charting.data.LineDataSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -119,11 +111,8 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.SpeedYLabelFormatter;
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.ActivitySummaryTableBuilder;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryValue;
@@ -131,7 +120,6 @@ 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.ActivitySummaryEntries;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.model.workout.Workout;
import nodomain.freeyourgadget.gadgetbridge.model.workout.WorkoutChart;
@@ -170,10 +158,6 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
private final List<FitSet> sets = new ArrayList<>();
private final List<FitLap> laps = new ArrayList<>();
TimestampTranslation tsTranslation = new TimestampTranslation();
ActivityKind activityKind;
public GarminWorkoutParser(final Context context) {
this.context = context;
}
@@ -219,48 +203,10 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
}
final ActivitySummaryData activitySummaryData = updateSummary(summary);
final ActivityKind activityKind = ActivityKind.fromCode(summary.getActivityKind());
final List<WorkoutChart> charts = new LinkedList<>();
if (!this.activityPoints.isEmpty()) {
final List<Entry> heartRateDataPoints = new ArrayList<>();
final List<Entry> speedDataPoints = new ArrayList<>();
final List<Entry> elevationDataPoints = new ArrayList<>();
boolean hasSpeedValues = false;
for (int i=0; i<=activityPoints.size()-1; i++) {
ActivityPoint point = activityPoints.get(i);
long tsShorten = tsTranslation.shorten((int) point.getTime().getTime());
if (point.getHeartRate() > 0) {
heartRateDataPoints.add(new Entry(tsShorten, point.getHeartRate()));
}
if (point.getLocation() != null) {
elevationDataPoints.add(new Entry(tsShorten, (float) point.getLocation().getAltitude()));
}
speedDataPoints.add(new Entry(tsShorten, point.getSpeed()));
if (!hasSpeedValues && point.getSpeed() > 0) {
hasSpeedValues = true;
}
}
if (!heartRateDataPoints.isEmpty()) {
String label = String.format("%s(%s)", context.getString(R.string.heart_rate), getUnitString(UNIT_BPM));
LineDataSet dataset = createDataSet(heartRateDataPoints, label, Color.RED);
charts.add(new WorkoutChart(context.getString(R.string.heart_rate), ActivitySummaryEntries.GROUP_HEART_RATE, new LineData(dataset)));
}
if (hasSpeedValues && !speedDataPoints.isEmpty()) {
if (ActivityKind.isPaceActivity(activityKind)) {
String label = String.format("%s(%s)", context.getString(R.string.Pace), getUnitString(UNIT_SECONDS_PER_KM));
LineDataSet dataset = createDataSet(speedDataPoints, label, Color.BLUE);
charts.add(new WorkoutChart(context.getString(R.string.Pace), ActivitySummaryEntries.GROUP_SPEED, new LineData(dataset), new SpeedYLabelFormatter(UNIT_SECONDS_PER_KM)));
} else {
String label = String.format("%s(%s)", context.getString(R.string.Speed), getUnitString(UNIT_METERS_PER_SECOND));
LineDataSet dataset = createDataSet(speedDataPoints, label, Color.BLUE);
charts.add(new WorkoutChart(context.getString(R.string.Speed), ActivitySummaryEntries.GROUP_SPEED, new LineData(dataset), new SpeedYLabelFormatter(UNIT_METERS_PER_SECOND)));
}
}
if (!elevationDataPoints.isEmpty()) {
String label = String.format("%s(%s)", context.getString(R.string.Elevation), getUnitString(UNIT_METERS));
LineDataSet dataset = createDataSet(elevationDataPoints, label, Color.GREEN);
charts.add(new WorkoutChart(context.getString(R.string.Elevation), ActivitySummaryEntries.GROUP_ELEVATION, new LineData(dataset)));
}
charts.addAll(DefaultWorkoutCharts.buildDefaultCharts(context, activityPoints, activityKind));
}
final long nanoEnd = System.nanoTime();
@@ -274,29 +220,6 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
);
}
public String getUnitString(String unit) {
int resId = context.getResources().getIdentifier(unit, "string", context.getPackageName());
if (resId != 0) {
return context.getString(resId);
}
return "";
}
public LineDataSet createDataSet(List<Entry> entities, String label, int color) {
LineDataSet dataSet = new LineDataSet(entities, label);
dataSet.setMode(LineDataSet.Mode.HORIZONTAL_BEZIER);
dataSet.setCubicIntensity(0.05f);
dataSet.setDrawCircles(false);
dataSet.setAxisDependency(YAxis.AxisDependency.RIGHT);
dataSet.setColor(color);
dataSet.setValueTextColor(GBApplication.getSecondaryTextColor(context));
dataSet.setLineWidth(1.5f);
dataSet.setHighlightLineWidth(2f);
dataSet.setDrawValues(false);
dataSet.setDrawHorizontalHighlightIndicator(false);
return dataSet;
}
public void reset() {
timesInZone.clear();
activityPoints.clear();
@@ -373,6 +296,7 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
return summaryData;
}
final ActivityKind activityKind;
if (sport != null) {
if (StringUtils.isNullOrEmpty(summary.getName())) {
summary.setName(sport.getName());
@@ -754,8 +678,8 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
int i = 1;
for (final FitDiveGas gas : diveGases) {
int helium = gas.getHeliumContent() != null ? gas.getHeliumContent(): 0;
int oxygen = gas.getOxygenContent() != null ? gas.getOxygenContent(): 0;
int helium = gas.getHeliumContent() != null ? gas.getHeliumContent() : 0;
int oxygen = gas.getOxygenContent() != null ? gas.getOxygenContent() : 0;
int nitrogen = 100 - helium - oxygen;
tableBuilder.addRow(
"gas_" + i,
@@ -227,8 +227,8 @@ public class ActivitySummaryEntries {
public static final String GROUP_ACTIVITY = "Activity";
public static final String GROUP_SPEED = "Speed";
public static final String GROUP_ELEVATION = "Elevation";
public static final String GROUP_POWER = "Power";
public static final String GROUP_HEART_RATE = "HeartRate";
public static final String GROUP_POWER = "workout_power";
public static final String GROUP_HEART_RATE = "heart_rate";
public static final String GROUP_OTHER = "Other";
public static final String GROUP_HEART_RATE_ZONES = "HeartRateZones";
public static final String GROUP_STROKES = "Strokes";
+2 -2
View File
@@ -1289,7 +1289,7 @@
<string name="seconds_km">sec/km</string>
<string name="averageStrokeDistance">Average Stroke Distance</string>
<string name="averageHR">Heartrate</string>
<string name="averageKMPaceSeconds">Avg Pace</string>
<string name="averageKMPaceSeconds">Pace</string>
<string name="totalStride">Total stride</string>
<string name="activeSeconds">Active</string>
<string name="caloriesBurnt">Calories</string>
@@ -1390,7 +1390,7 @@
<string name="reminder_no_free_slots_description">The device has no free slots for reminders (total slots: %1$s)</string>
<string name="prefs_reserve_reminder_calendar_summary">Number of calendar events that will be synchronized</string>
<string name="mi2_prefs_do_not_disturb_lift_wrist">Activate display upon lift during Do Not Disturb</string>
<string name="maxHR">Max Heart Rate</string>
<string name="maxHR">Max Heartrate</string>
<string name="spm">steps/min</string>
<string name="minSpeed">Minimum</string>
<string name="averageCadence">Average Cadence</string>
+1 -2
View File
@@ -2713,8 +2713,7 @@
<string name="Swimming">Swimming</string>
<string name="Distance">Distance</string>
<string name="Elevation">Elevation</string>
<string name="Power">Power</string>
<string name="HeartRate">Heart rate</string>
<string name="workout_power">Power</string>
<string name="Speed">Speed</string>
<string name="Activity">Activity</string>
<string name="Steps">Steps</string>