1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-12-28 19:45:50 +01:00

Workout Details: Add tables and progress bars

This commit is contained in:
José Rebelo 2024-09-29 23:00:58 +01:00 committed by José Rebelo
parent 5192304d29
commit 3e327e2924
19 changed files with 898 additions and 424 deletions

View File

@ -43,7 +43,9 @@ import android.view.animation.AnimationUtils;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.FrameLayout;
import androidx.gridlayout.widget.GridLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ScrollView;
@ -57,9 +59,8 @@ import androidx.core.content.FileProvider;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -67,19 +68,20 @@ import java.io.File;
import java.io.FileFilter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.WorkoutValueFormatter;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryEntry;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummarySimpleEntry;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
@ -88,6 +90,7 @@ import nodomain.freeyourgadget.gadgetbridge.export.GPXExporter;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryItems;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryJsonSummary;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
@ -104,7 +107,6 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
private static final Logger LOG = LoggerFactory.getLogger(ActivitySummaryDetail.class);
BaseActivitySummary currentItem = null;
private GBDevice gbDevice;
private boolean show_raw_data = false;
private int alternateColor;
private Menu mOptionsMenu;
List<String> filesGpxList = new ArrayList<>();
@ -115,6 +117,8 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
private ActivitySummariesChartFragment activitySummariesChartFragment;
private ActivitySummariesGpsFragment activitySummariesGpsFragment;
private final WorkoutValueFormatter workoutValueFormatter = new WorkoutValueFormatter();
public static int getAlternateColor(Context context) {
TypedValue typedValue = new TypedValue();
Resources.Theme theme = context.getTheme();
@ -220,7 +224,7 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
ImageView activity_icon = findViewById(R.id.item_image);
activity_icon.setOnLongClickListener(new View.OnLongClickListener() {
public boolean onLongClick(View v) {
show_raw_data = !show_raw_data;
workoutValueFormatter.toggleRawData();
if (currentItem != null) {
makeSummaryHeader(currentItem);
makeSummaryContent(currentItem);
@ -342,7 +346,7 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
TextView activity_name = findViewById(R.id.activityname);
activity_name.setText(activityname);
if (activityname == null || (activityname != null && activityname.length() < 1)) {
if (StringUtils.isBlank(activityname)) {
activity_name.setVisibility(View.GONE);
} else {
activity_name.setVisibility(View.VISIBLE);
@ -426,160 +430,64 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
final ActivitySummaryParser summaryParser = coordinator.getActivitySummaryParser(gbDevice, this);
//make view of data from summaryData of item
String units = GBApplication.getPrefs().getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM, GBApplication.getContext().getString(R.string.p_unit_metric));
String UNIT_IMPERIAL = GBApplication.getContext().getString(R.string.p_unit_imperial);
LinearLayout fieldLayout = findViewById(R.id.summaryDetails);
fieldLayout.removeAllViews(); //remove old widgets
ActivitySummaryJsonSummary activitySummaryJsonSummary = new ActivitySummaryJsonSummary(summaryParser, item);
JSONObject data = activitySummaryJsonSummary.getSummaryGroupedList(); //get list, grouped by groups
Map<String, List<Pair<String, ActivitySummaryEntry>>> data = activitySummaryJsonSummary.getSummaryGroupedList(); //get list, grouped by groups
if (data == null) return;
Iterator<String> keys = data.keys();
DecimalFormat df = new DecimalFormat("#.##");
for (final Map.Entry<String, List<Pair<String, ActivitySummaryEntry>>> group : data.entrySet()) {
final String groupKey = group.getKey();
final List<Pair<String, ActivitySummaryEntry>> entries = group.getValue();
while (keys.hasNext()) {
String key = keys.next();
try {
JSONArray innerList = (JSONArray) data.get(key);
TableRow label_row = new TableRow(ActivitySummaryDetail.this);
TextView label_field = new TextView(ActivitySummaryDetail.this);
label_field.setId(View.generateViewId());
label_field.setTextSize(18);
label_field.setPadding(dpToPx(8), dpToPx(20), 0, dpToPx(20));
label_field.setTypeface(null, Typeface.BOLD);
label_field.setText(workoutValueFormatter.getStringResourceByName(groupKey));
label_row.addView(label_field);
fieldLayout.addView(label_row);
TableRow label_row = new TableRow(ActivitySummaryDetail.this);
TextView label_field = new TextView(ActivitySummaryDetail.this);
label_field.setId(View.generateViewId());
label_field.setTextSize(18);
label_field.setPadding(dpToPx(8), dpToPx(20), 0, dpToPx(20));
label_field.setTypeface(null, Typeface.BOLD);
label_field.setText(String.format("%s", getStringResourceByName(key)));
label_row.addView(label_field);
fieldLayout.addView(label_row);
GridLayout gridLayout = new GridLayout(ActivitySummaryDetail.this);
gridLayout.setBackgroundColor(getResources().getColor(R.color.gauge_line_color));
gridLayout.setColumnCount(2);
gridLayout.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
int lastRow = (int) Math.floor((innerList.length() - 1) / 2);
int i;
for (i = 0; i < innerList.length(); i++) {
LinearLayout linearLayout = generateLinearLayout(i, lastRow);
// Value
TextView valueTextView = new TextView(ActivitySummaryDetail.this);
valueTextView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
valueTextView.setText(String.format("%s", "-"));
valueTextView.setTextSize(20);
// Label
TextView labelTextView = new TextView(ActivitySummaryDetail.this);
labelTextView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
labelTextView.setTextSize(12);
JSONObject innerData = innerList.getJSONObject(i);
String unit = innerData.getString("unit");
String name = innerData.getString("name");
labelTextView.setText(getStringResourceByName(name));
if (!unit.equals("string")) {
double value = innerData.getDouble("value");
if (!show_raw_data) {
//special casing here + imperial units handling
switch (unit) {
case UNIT_CM:
if (units.equals(UNIT_IMPERIAL)) {
value = value * 0.0328084;
unit = "ft";
}
break;
case UNIT_METERS_PER_SECOND:
if (units.equals(UNIT_IMPERIAL)) {
value = value * 2.236936D;
unit = "mi_h";
} else { //metric
value = value * 3.6;
unit = "km_h";
}
break;
case UNIT_SECONDS_PER_M:
if (units.equals(UNIT_IMPERIAL)) {
value = value * (1609.344 / 60D);
unit = "minutes_mi";
} else { //metric
value = value * (1000 / 60D);
unit = "minutes_km";
}
break;
case UNIT_SECONDS_PER_KM:
if (units.equals(UNIT_IMPERIAL)) {
value = value / 60D * 1.609344;
unit = "minutes_mi";
} else { //metric
value = value / 60D;
unit = "minutes_km";
}
break;
case UNIT_METERS:
if (units.equals(UNIT_IMPERIAL)) {
value = value * 3.28084D;
unit = "ft";
if (value > 6000) {
value = value * 0.0001893939D;
unit = "mi";
}
} else { //metric
if (value > 2000) {
value = value / 1000;
unit = "km";
}
}
break;
}
}
if (unit.equals("seconds") && !show_raw_data) { //rather then plain seconds, show formatted duration
valueTextView.setText(DateTimeUtils.formatDurationHoursMinutes((long) value, TimeUnit.SECONDS));
} else if (unit.equals("minutes_km") || unit.equals("minutes_mi")) {
// Format pace
valueTextView.setText(String.format(
Locale.getDefault(),
"%d:%02d %s",
(int) Math.floor(value), (int) Math.round(60 * (value - (int) Math.floor(value))),
getStringResourceByName(unit)
));
} else {
valueTextView.setText(String.format("%s %s", df.format(value), getStringResourceByName(unit)));
}
} else {
valueTextView.setText(getStringResourceByName(innerData.getString("value"))); //we could optimize here a bit and only do this for particular activities (swim at the moment...)
}
linearLayout.addView(valueTextView);
linearLayout.addView(labelTextView);
gridLayout.addView(linearLayout);
GridLayout gridLayout = new GridLayout(ActivitySummaryDetail.this);
gridLayout.setBackgroundColor(getResources().getColor(R.color.gauge_line_color));
gridLayout.setColumnCount(2);
gridLayout.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
int totalCells = entries.stream().mapToInt(e -> e.getRight().getColumnSpan()).sum();
totalCells += totalCells & 1; // round up to nearest even number, since we have 2 columns
int cellNumber = 0;
for (final Pair<String, ActivitySummaryEntry> entry : entries) {
final int columnSpan = entry.getRight().getColumnSpan();
LinearLayout linearLayout = generateLinearLayout(cellNumber, cellNumber + 2 >= totalCells, columnSpan);
entry.getRight().populate(entry.getLeft(), linearLayout, workoutValueFormatter);
gridLayout.addView(linearLayout);
cellNumber += columnSpan;
}
if (gridLayout.getChildCount() > 0) {
if (cellNumber % 2 != 0) {
final LinearLayout emptyLayout = generateLinearLayout(cellNumber, true, 1);
new ActivitySummarySimpleEntry(null, "", "string").populate("", emptyLayout, workoutValueFormatter);
gridLayout.addView(emptyLayout);
}
if (gridLayout.getChildCount() > 0) {
if (gridLayout.getChildCount() % 2 != 0) {
gridLayout.addView(generateLinearLayout(i, lastRow));
}
fieldLayout.addView(gridLayout);
}
} catch (JSONException e) {
LOG.error("SportsActivity", e);
fieldLayout.addView(gridLayout);
}
}
}
public LinearLayout generateLinearLayout(int i, int lastRow) {
public LinearLayout generateLinearLayout(int i, boolean lastRow, int columnSize) {
LinearLayout linearLayout = new LinearLayout(ActivitySummaryDetail.this);
GridLayout.LayoutParams columnParams = new GridLayout.LayoutParams();
columnParams.columnSpec = GridLayout.spec(i % 2 == 0 ? 0 : 1, 1);
GridLayout.LayoutParams layoutParams = new GridLayout.LayoutParams(
GridLayout.spec(GridLayout.UNDEFINED, GridLayout.FILL,1f),
GridLayout.spec(GridLayout.UNDEFINED, 1, GridLayout.FILL,1f)
GridLayout.spec(GridLayout.UNDEFINED, GridLayout.FILL, 1f),
GridLayout.spec(GridLayout.UNDEFINED, columnSize, GridLayout.FILL, 1f)
);
layoutParams.width = 0;
linearLayout.setLayoutParams(layoutParams);
linearLayout.setOrientation(LinearLayout.VERTICAL);
linearLayout.setGravity(Gravity.CENTER);
linearLayout.setPadding(dpToPx(15), dpToPx(15),dpToPx(15), dpToPx(15));
linearLayout.setPadding(dpToPx(15), dpToPx(15), dpToPx(15), dpToPx(15));
linearLayout.setBackgroundColor(GBApplication.getWindowBackgroundColor(ActivitySummaryDetail.this));
int marginLeft = 0;
int marginTop = 0;
@ -593,7 +501,7 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
marginTop = 2;
marginLeft = 1;
}
if (i / 2 >= lastRow) {
if (lastRow) {
marginBottom = 2;
}
layoutParams.setMargins(dpToPx(marginLeft), dpToPx(marginTop), dpToPx(marginRight), dpToPx(marginBottom));
@ -606,17 +514,6 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
return Math.round(dp * density);
}
private String getStringResourceByName(String aString) {
String packageName = getPackageName();
int resId = getResources().getIdentifier(aString, "string", packageName);
if (resId == 0) {
//LOG.warn("SportsActivity " + "Missing string in strings:" + aString);
return aString;
} else {
return getString(resId);
}
}
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
final int itemId = item.getItemId();
@ -631,7 +528,7 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
viewGpxTrack(ActivitySummaryDetail.this);
return true;
} else if (itemId == R.id.activity_action_share_gpx) {
shareGpxTrack(ActivitySummaryDetail.this, currentItem);
shareGpxTrack(ActivitySummaryDetail.this);
return true;
} else if (itemId == R.id.activity_action_dev_share_raw_summary) {
shareRawSummary(ActivitySummaryDetail.this, currentItem);
@ -701,7 +598,7 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
}
}
private void shareGpxTrack(final Context context, final BaseActivitySummary summary) {
private void shareGpxTrack(final Context context) {
final File trackFile = getTrackFile();
if (trackFile == null) {
GB.toast(getApplicationContext(), "No GPX track in this activity", Toast.LENGTH_LONG, GB.INFO);
@ -736,6 +633,7 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
final File cacheDir = getCacheDir();
final File rawCacheDir = new File(cacheDir, "gpx");
//noinspection ResultOfMethodCallIgnored
rawCacheDir.mkdir();
final File gpxFile = new File(rawCacheDir, file.getName().replace(".fit", ".gpx"));
@ -823,16 +721,10 @@ public class ActivitySummaryDetail extends AbstractGBActivity {
return true;
}
}
final String summaryData = currentItem.getSummaryData();
if (summaryData != null && summaryData.contains(INTERNAL_HAS_GPS)) {
try {
final JSONObject summaryDataObject = new JSONObject(summaryData);
final JSONObject internalHasGps = summaryDataObject.getJSONObject(INTERNAL_HAS_GPS);
return "true".equals(internalHasGps.optString("value", "false"));
} catch (final JSONException e) {
LOG.error("Failed to parse summary data json", e);
return false;
}
final String summaryDataJson = currentItem.getSummaryData();
if (summaryDataJson != null && summaryDataJson.contains(INTERNAL_HAS_GPS)) {
final ActivitySummaryData summaryData = ActivitySummaryData.fromJson(summaryDataJson);
return summaryData != null && summaryData.getBoolean(INTERNAL_HAS_GPS, false);
}
return false;

View File

@ -0,0 +1,144 @@
package nodomain.freeyourgadget.gadgetbridge.activities.workouts;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_CM;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_KG;
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 static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS_PER_M;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.DecimalFormat;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
public class WorkoutValueFormatter {
private static final Logger LOG = LoggerFactory.getLogger(WorkoutValueFormatter.class);
private boolean show_raw_data = false;
private final String units;
private final String UNIT_IMPERIAL;
private final DecimalFormat df = new DecimalFormat("#.##");
public WorkoutValueFormatter() {
this.units = GBApplication.getPrefs().getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM, GBApplication.getContext().getString(R.string.p_unit_metric));
this.UNIT_IMPERIAL = GBApplication.getContext().getString(R.string.p_unit_imperial);
}
public void toggleRawData() {
this.show_raw_data = !show_raw_data;
}
public String formatValue(final Object rawValue, String unit) {
if (ActivitySummaryEntries.UNIT_RAW_STRING.equals(unit)) {
return String.valueOf(rawValue);
}
if (rawValue instanceof CharSequence || ActivitySummaryEntries.UNIT_STRING.equals(unit)) {
// we could optimize here a bit and only do this for particular activities (swim at the moment...)
try {
return getStringResourceByName(String.valueOf(rawValue));
} catch (final Exception e) {
LOG.error("Failed to get string resource by name for {}", rawValue);
return String.valueOf(rawValue);
}
}
double value = ((Number) rawValue).doubleValue();
if (!show_raw_data) {
//special casing here + imperial units handling
switch (unit) {
case UNIT_KG:
if (units.equals(UNIT_IMPERIAL)) {
value = value * 2.2046226f;
unit = "lb";
}
break;
case UNIT_CM:
if (units.equals(UNIT_IMPERIAL)) {
value = value * 0.0328084;
unit = "ft";
}
break;
case UNIT_METERS_PER_SECOND:
if (units.equals(UNIT_IMPERIAL)) {
value = value * 2.236936D;
unit = "mi_h";
} else { //metric
value = value * 3.6;
unit = "km_h";
}
break;
case UNIT_SECONDS_PER_M:
if (units.equals(UNIT_IMPERIAL)) {
value = value * (1609.344 / 60D);
unit = "minutes_mi";
} else { //metric
value = value * (1000 / 60D);
unit = "minutes_km";
}
break;
case UNIT_SECONDS_PER_KM:
if (units.equals(UNIT_IMPERIAL)) {
value = value / 60D * 1.609344;
unit = "minutes_mi";
} else { //metric
value = value / 60D;
unit = "minutes_km";
}
break;
case UNIT_METERS:
if (units.equals(UNIT_IMPERIAL)) {
value = value * 3.28084D;
unit = "ft";
if (value > 6000) {
value = value * 0.0001893939D;
unit = "mi";
}
} else { //metric
if (value > 2000) {
value = value / 1000;
unit = "km";
}
}
break;
}
}
if (unit.equals("seconds") && !show_raw_data) { //rather then plain seconds, show formatted duration
return DateTimeUtils.formatDurationHoursMinutes((long) value, TimeUnit.SECONDS);
} else if (unit.equals("minutes_km") || unit.equals("minutes_mi")) {
// Format pace
return String.format(
Locale.getDefault(),
"%d:%02d %s",
(int) Math.floor(value), (int) Math.round(60 * (value - (int) Math.floor(value))),
getStringResourceByName(unit)
);
} else {
return String.format("%s %s", df.format(value), getStringResourceByName(unit));
}
}
public String getStringResourceByName(String aString) {
String packageName = BuildConfig.APPLICATION_ID;
int resId = GBApplication.getContext().getResources().getIdentifier(aString, "string", packageName);
if (resId == 0) {
//LOG.warn("SportsActivity " + "Missing string in strings:" + aString);
return aString;
} else {
return GBApplication.getContext().getString(resId);
}
}
}

View File

@ -0,0 +1,23 @@
package nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries;
import android.widget.LinearLayout;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.WorkoutValueFormatter;
public abstract class ActivitySummaryEntry {
private final String group;
public ActivitySummaryEntry(final String group) {
this.group = group;
}
public String getGroup() {
return group;
}
public abstract int getColumnSpan();
public abstract void populate(final String key,
final LinearLayout linearLayout,
final WorkoutValueFormatter workoutValueFormatter);
}

View File

@ -0,0 +1,68 @@
package nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries;
import android.content.Context;
import android.view.Gravity;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.WorkoutValueFormatter;
public class ActivitySummaryProgressEntry extends ActivitySummarySimpleEntry {
private final int progress;
public ActivitySummaryProgressEntry(final Object value, final String unit, final int progress) {
this(null, value, unit, progress);
}
public ActivitySummaryProgressEntry(final String group, final Object value, final String unit, final int progress) {
super(group, value, unit);
this.progress = progress;
}
public int getProgress() {
return progress;
}
@Override
public int getColumnSpan() {
return 2;
}
@Override
public void populate(final String key, final LinearLayout linearLayout, final WorkoutValueFormatter workoutValueFormatter) {
final Context context = linearLayout.getContext();
// Label
final TextView labelTextView = new TextView(context);
labelTextView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
labelTextView.setTextSize(12);
labelTextView.setText(workoutValueFormatter.getStringResourceByName(key));
// Value
final TextView valueTextView = new TextView(context);
valueTextView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
valueTextView.setText(String.format("%s", "-"));
valueTextView.setTextSize(12);
valueTextView.setGravity(Gravity.END);
valueTextView.setText(workoutValueFormatter.formatValue(getValue(), getUnit()));
// Layout for the labels, so the value is at the right
final LinearLayout labelsLinearLayout = new LinearLayout(context);
labelsLinearLayout.setOrientation(LinearLayout.HORIZONTAL);
labelsLinearLayout.addView(labelTextView);
labelsLinearLayout.addView(valueTextView);
final LinearLayout progressLayout = new LinearLayout(context);
final ProgressBar progressBar = new ProgressBar(context, null, android.R.attr.progressBarStyleHorizontal);
progressBar.setIndeterminate(false);
progressBar.setProgress(progress);
progressBar.setVisibility(View.VISIBLE);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
progressLayout.addView(progressBar, params);
linearLayout.addView(labelsLinearLayout);
linearLayout.addView(progressLayout);
}
}

View File

@ -0,0 +1,57 @@
package nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries;
import android.content.Context;
import android.widget.LinearLayout;
import android.widget.TextView;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.WorkoutValueFormatter;
public class ActivitySummarySimpleEntry extends ActivitySummaryEntry {
private final Object value;
private final String unit;
public ActivitySummarySimpleEntry(final Object value, final String unit) {
this(null, value, unit);
}
public ActivitySummarySimpleEntry(final String group, final Object value, final String unit) {
super(group);
this.value = value;
this.unit = unit;
}
public Object getValue() {
return value;
}
public String getUnit() {
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();
// Value
final TextView valueTextView = new TextView(context);
valueTextView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
valueTextView.setText(context.getString(R.string.stats_empty_value));
valueTextView.setTextSize(20);
valueTextView.setText(workoutValueFormatter.formatValue(value, unit));
// Label
final TextView labelTextView = new TextView(context);
labelTextView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
labelTextView.setTextSize(12);
labelTextView.setText(workoutValueFormatter.getStringResourceByName(key));
linearLayout.addView(valueTextView);
linearLayout.addView(labelTextView);
}
}

View File

@ -0,0 +1,72 @@
package nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries;
import android.graphics.Typeface;
import android.view.Gravity;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.gridlayout.widget.GridLayout;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.WorkoutValueFormatter;
public class ActivitySummaryTableRowEntry extends ActivitySummaryEntry {
private final List<ActivitySummaryValue> columns;
private final boolean isHeader;
private final boolean boldFirstColumn;
public ActivitySummaryTableRowEntry(final List<ActivitySummaryValue> columns) {
this(null, columns, false, false);
}
public ActivitySummaryTableRowEntry(final String group,
final List<ActivitySummaryValue> columns,
final boolean isHeader,
final boolean boldFirstColumn) {
super(group);
this.columns = columns;
this.isHeader = isHeader;
this.boldFirstColumn = boldFirstColumn;
}
@Override
public int getColumnSpan() {
return 2;
}
@Override
public void populate(final String key, final LinearLayout linearLayout, final WorkoutValueFormatter workoutValueFormatter) {
final GridLayout rowLayout = new GridLayout(linearLayout.getContext());
rowLayout.setColumnCount(columns.size());
rowLayout.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
for (int i = 0; i < columns.size(); i++) {
final LinearLayout cellLayout = new LinearLayout(linearLayout.getContext());
final GridLayout.LayoutParams columnParams = new GridLayout.LayoutParams();
columnParams.columnSpec = GridLayout.spec(i, columns.size());
final GridLayout.LayoutParams layoutParams = new GridLayout.LayoutParams(
GridLayout.spec(GridLayout.UNDEFINED, GridLayout.FILL, 1f),
GridLayout.spec(GridLayout.UNDEFINED, 1, GridLayout.FILL, 1f)
);
layoutParams.width = 0;
cellLayout.setLayoutParams(layoutParams);
cellLayout.setOrientation(LinearLayout.VERTICAL);
cellLayout.setGravity(Gravity.CENTER);
final TextView columnTextView = new TextView(linearLayout.getContext());
columnTextView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT));
columnTextView.setText(columns.get(i).format(workoutValueFormatter));
columnTextView.setTextSize(12);
if (isHeader || (i == 0 && boldFirstColumn)) {
columnTextView.setTypeface(null, Typeface.BOLD);
}
cellLayout.addView(columnTextView);
rowLayout.addView(cellLayout);
}
linearLayout.addView(rowLayout);
}
}

View File

@ -0,0 +1,22 @@
package nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.WorkoutValueFormatter;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries;
public class ActivitySummaryValue {
private final Object value;
private final String unit;
public ActivitySummaryValue(final Object value, final String unit) {
this.value = value;
this.unit = unit;
}
public ActivitySummaryValue(final String value) {
this(value, ActivitySummaryEntries.UNIT_STRING);
}
public String format(final WorkoutValueFormatter formatter) {
return formatter.formatValue(value, unit);
}
}

View File

@ -26,8 +26,6 @@ import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -48,6 +46,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryJsonSummary;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
@ -205,21 +204,17 @@ public class ActivitySummariesAdapter extends AbstractActivityListingAdapter<Bas
final ActivitySummaryParser summaryParser = coordinator.getActivitySummaryParser(device, getContext());
final ActivitySummaryJsonSummary activitySummaryJsonSummary = new ActivitySummaryJsonSummary(summaryParser, sportitem);
JSONObject summarySubdata = activitySummaryJsonSummary.getSummaryData(false);
ActivitySummaryData summarySubdata = activitySummaryJsonSummary.getSummaryData(false);
if (summarySubdata != null) {
try {
if (summarySubdata.has("caloriesBurnt")) {
caloriesBurntSum += summarySubdata.getJSONObject("caloriesBurnt").getDouble("value");
}
if (summarySubdata.has("distanceMeters")) {
distanceSum += summarySubdata.getJSONObject("distanceMeters").getDouble("value");
}
if (summarySubdata.has("activeSeconds")) {
activeSecondsSum += summarySubdata.getJSONObject("activeSeconds").getDouble("value");
}
} catch (JSONException e) {
LOG.error("SportsActivity", e);
if (summarySubdata.has("caloriesBurnt")) {
caloriesBurntSum += summarySubdata.getNumber("caloriesBurnt", 0).doubleValue();
}
if (summarySubdata.has("distanceMeters")) {
distanceSum += summarySubdata.getNumber("distanceMeters", 0).doubleValue();
}
if (summarySubdata.has("activeSeconds")) {
activeSecondsSum += summarySubdata.getNumber("activeSeconds", 0).doubleValue();
}
}
}

View File

@ -19,8 +19,6 @@ package nodomain.freeyourgadget.gadgetbridge.devices;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -42,6 +40,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.model.Vo2MaxSample;
@ -165,7 +164,7 @@ public class WorkoutVo2MaxSampleProvider implements Vo2MaxSampleProvider<Vo2MaxS
}
private void fillSummaryData(final DeviceCoordinator coordinator,
final Collection<BaseActivitySummary> summaries) {
final Collection<BaseActivitySummary> summaries) {
ActivitySummaryParser activitySummaryParser = coordinator.getActivitySummaryParser(device, GBApplication.getContext());
for (final BaseActivitySummary summary : summaries) {
if (summary.getSummaryData() == null) {
@ -230,46 +229,45 @@ public class WorkoutVo2MaxSampleProvider implements Vo2MaxSampleProvider<Vo2MaxS
@Nullable
public static GarminVo2maxSample fromActivitySummary(final BaseActivitySummary summary) {
if (summary.getSummaryData() == null) {
final String summaryDataJson = summary.getSummaryData();
if (summaryDataJson == null) {
return null;
}
if (!summary.getSummaryData().contains(ActivitySummaryEntries.MAXIMUM_OXYGEN_UPTAKE)) {
if (!summaryDataJson.contains(ActivitySummaryEntries.MAXIMUM_OXYGEN_UPTAKE)) {
return null;
}
try {
final JSONObject summaryDataObject = new JSONObject(summary.getSummaryData());
final JSONObject vo2jsonObj = summaryDataObject.getJSONObject(ActivitySummaryEntries.MAXIMUM_OXYGEN_UPTAKE);
final double value = vo2jsonObj.optDouble("value", 0);
if (value == 0) {
return null;
}
final Vo2MaxSample.Type type;
switch (ActivityKind.fromCode(summary.getActivityKind())) {
case INDOOR_RUNNING:
case OUTDOOR_RUNNING:
case CROSS_COUNTRY_RUNNING:
case RUNNING:
type = Vo2MaxSample.Type.RUNNING;
break;
case CYCLING:
case INDOOR_CYCLING:
case HANDCYCLING:
case HANDCYCLING_INDOOR:
case MOTORCYCLING:
case OUTDOOR_CYCLING:
type = Vo2MaxSample.Type.CYCLING;
break;
default:
type = Vo2MaxSample.Type.ANY;
}
return new GarminVo2maxSample(summary.getStartTime().getTime(), type, (float) value);
} catch (final JSONException e) {
LOG.error("Failed to parse summary data json", e);
final ActivitySummaryData summaryData = ActivitySummaryData.fromJson(summaryDataJson);
if (summaryData == null) {
return null;
}
final double value = summaryData.getNumber(ActivitySummaryEntries.MAXIMUM_OXYGEN_UPTAKE, 0).doubleValue();
if (value == 0) {
return null;
}
final Vo2MaxSample.Type type;
switch (ActivityKind.fromCode(summary.getActivityKind())) {
case INDOOR_RUNNING:
case OUTDOOR_RUNNING:
case CROSS_COUNTRY_RUNNING:
case RUNNING:
type = Vo2MaxSample.Type.RUNNING;
break;
case CYCLING:
case INDOOR_CYCLING:
case HANDCYCLING:
case HANDCYCLING_INDOOR:
case MOTORCYCLING:
case OUTDOOR_CYCLING:
type = Vo2MaxSample.Type.CYCLING;
break;
default:
type = Vo2MaxSample.Type.ANY;
}
return new GarminVo2maxSample(summary.getStartTime().getTime(), type, (float) value);
}
}
}

View File

@ -11,12 +11,12 @@ import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryTableRowEntry;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryValue;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
@ -31,7 +31,6 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitSet;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitSport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitTimeInZone;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
public class GarminWorkoutParser implements ActivitySummaryParser {
@ -227,33 +226,53 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
}
if (!sets.isEmpty()) {
final boolean isMetric = GBApplication.getPrefs().isMetricUnits();
final boolean anyReps = sets.stream().anyMatch(s -> s.getRepetitions() != null);
final boolean anyWeight = sets.stream().anyMatch(s -> s.getWeight() != null);
final List<ActivitySummaryValue> header = new LinkedList<>();
header.add(new ActivitySummaryValue("set"));
header.add(new ActivitySummaryValue("workout_set_reps"));
header.add(new ActivitySummaryValue("menuitem_weight"));
header.add(new ActivitySummaryValue("activity_detail_duration_label"));
summaryData.add(
"sets_header",
new ActivitySummaryTableRowEntry(
SETS,
header,
true,
true
)
);
int i = 1;
for (final FitSet set : sets) {
if (set.getSetType() != null && set.getDuration() != null && set.getSetType() == 1) {
final StringBuilder sb = new StringBuilder();
final List<ActivitySummaryValue> columns = new LinkedList<>();
columns.add(new ActivitySummaryValue(i, UNIT_NONE));
if (set.getRepetitions() != null) {
if (set.getWeight() != null) {
if (isMetric) {
sb.append(context.getString(R.string.workout_set_repetitions_weight_kg, set.getRepetitions(), set.getWeight()));
} else {
sb.append(context.getString(R.string.workout_set_repetitions_weight_lbs, set.getRepetitions(), set.getWeight() * 2.2046226f));
}
} else {
sb.append(context.getString(R.string.workout_set_repetitions, set.getRepetitions()));
}
sb.append(", ");
columns.add(new ActivitySummaryValue(String.valueOf(set.getRepetitions())));
} else {
columns.add(new ActivitySummaryValue("stats_empty_value"));
}
sb.append(DateTimeUtils.formatDurationHoursMinutes(set.getDuration().longValue(), TimeUnit.SECONDS));
if (set.getWeight() != null) {
columns.add(new ActivitySummaryValue(set.getWeight(), UNIT_KG));
} else {
columns.add(new ActivitySummaryValue("stats_empty_value"));
}
columns.add(new ActivitySummaryValue(set.getDuration().longValue(), UNIT_SECONDS));
summaryData.add(
SETS,
context.getString(R.string.workout_set_i, i),
sb.toString()
"set_" + i,
new ActivitySummaryTableRowEntry(
SETS,
columns,
false,
true
)
);
i++;
}
@ -261,8 +280,8 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
}
summaryData.add(
INTERNAL_HAS_GPS,
String.valueOf(activityPoints.stream().anyMatch(p -> p.getLocation() != null))
INTERNAL_HAS_GPS,
String.valueOf(activityPoints.stream().anyMatch(p -> p.getLocation() != null))
);
summary.setSummaryData(summaryData.toString());

View File

@ -24,8 +24,11 @@ import org.apache.commons.lang3.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryProgressEntry;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.proto.HuamiProtos;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
@ -121,12 +124,22 @@ public class ZeppOsActivitySummaryParser extends HuamiActivitySummaryParser {
if (summaryProto.hasHeartRateZones()) {
// TODO hr zones bpm?
if (summaryProto.getHeartRateZones().getZoneTimeCount() == 6) {
summaryData.add(HR_ZONE_NA, summaryProto.getHeartRateZones().getZoneTime(0), UNIT_SECONDS);
summaryData.add(HR_ZONE_WARM_UP, summaryProto.getHeartRateZones().getZoneTime(1), UNIT_SECONDS);
summaryData.add(HR_ZONE_FAT_BURN, summaryProto.getHeartRateZones().getZoneTime(2), UNIT_SECONDS);
summaryData.add(HR_ZONE_AEROBIC, summaryProto.getHeartRateZones().getZoneTime(3), UNIT_SECONDS);
summaryData.add(HR_ZONE_ANAEROBIC, summaryProto.getHeartRateZones().getZoneTime(4), UNIT_SECONDS);
summaryData.add(HR_ZONE_EXTREME, summaryProto.getHeartRateZones().getZoneTime(5), UNIT_SECONDS);
final double totalTime = summaryProto.getHeartRateZones().getZoneTimeList()
.stream()
.mapToInt(v -> v)
.sum();
final List<String> zoneOrder = Arrays.asList(HR_ZONE_NA, HR_ZONE_WARM_UP, HR_ZONE_FAT_BURN, HR_ZONE_AEROBIC, HR_ZONE_ANAEROBIC, HR_ZONE_EXTREME);
for (int i = 0; i < zoneOrder.size(); i++) {
summaryData.add(
zoneOrder.get(i),
new ActivitySummaryProgressEntry(
summaryProto.getHeartRateZones().getZoneTime(i),
UNIT_SECONDS,
(int) ((100 * summaryProto.getHeartRateZones().getZoneTime(i)) / totalTime)
)
);
}
} else {
LOG.warn("Unexpected number of HR zones {}", summaryProto.getHeartRateZones().getZoneTimeCount());
}

View File

@ -16,61 +16,141 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.model;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitImporter;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import com.google.gson.typeadapters.RuntimeTypeAdapterFactory;
import org.apache.commons.lang3.StringUtils;
import java.lang.reflect.Type;
import java.util.LinkedHashMap;
import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryEntry;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryProgressEntry;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummarySimpleEntry;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryTableRowEntry;
/**
* A small wrapper for a JSONObject, with helper methods to add activity summary data in the format
* Gadgetbridge expects.
*/
public class ActivitySummaryData extends JSONObject {
private static final Logger LOG = LoggerFactory.getLogger(FitImporter.class);
public class ActivitySummaryData {
private static final Gson GSON = new GsonBuilder()
.registerTypeAdapterFactory(RuntimeTypeAdapterFactory
.of(ActivitySummaryEntry.class, "type")
.registerSubtype(ActivitySummarySimpleEntry.class, null) // no type for backwards compatibility
.registerSubtype(ActivitySummaryProgressEntry.class, "progress")
.registerSubtype(ActivitySummaryTableRowEntry.class, "tableRow")
.recognizeSubtypes()
)
//.serializeNulls()
//.setPrettyPrinting()
.create();
public void add(final String key, final float value, final String unit) {
private final LinkedHashMap<String, ActivitySummaryEntry> entries;
public ActivitySummaryData() {
this.entries = new LinkedHashMap<>();
}
public ActivitySummaryData(final LinkedHashMap<String, ActivitySummaryEntry> entries) {
this.entries = entries;
}
public void add(final String key, final Number value, final String unit) {
add(null, key, value, unit);
}
public void add(final String key, final double value, final String unit) {
add(null, key, value, unit);
public void add(final String group, final String key, final Number value, final String unit) {
if (value.doubleValue() != 0) {
entries.put(key, new ActivitySummarySimpleEntry(group, value, unit));
}
}
public void add(final String key, final String value) {
add(null, key, value);
}
public void add(final String group, final String key, final double value, final String unit) {
if (value > 0) {
try {
final JSONObject innerData = new JSONObject();
if (group != null) {
innerData.put("group", group);
}
innerData.put("value", value);
innerData.put("unit", unit);
put(key, innerData);
} catch (final JSONException e) {
LOG.error("This should never happen", e);
}
public void add(final String group, final String key, final String value) {
if (StringUtils.isBlank(key) || StringUtils.isBlank(value)) {
return;
}
entries.put(key, new ActivitySummarySimpleEntry(group, value, ActivitySummaryEntries.UNIT_STRING));
}
public void add(final String group, final String key, final String value) {
if (key != null && !key.isEmpty() && value != null && !value.isEmpty()) {
try {
final JSONObject innerData = new JSONObject();
if (group != null) {
innerData.put("group", group);
}
innerData.put("value", value);
innerData.put("unit", "string");
put(key, innerData);
} catch (final JSONException e) {
LOG.error("This should never happen", e);
}
public void add(final String key, final ActivitySummaryEntry entry) {
entries.put(key, entry);
}
public Set<String> getKeys() {
return entries.keySet();
}
public ActivitySummaryEntry get(final String key) {
return entries.get(key);
}
public boolean has(final String key) {
return entries.containsKey(key);
}
public Number getNumber(final String key, final Number defaultValue) {
final ActivitySummaryEntry entry = entries.get(key);
if (!(entry instanceof ActivitySummarySimpleEntry)) {
return defaultValue;
}
final ActivitySummarySimpleEntry simpleEntry = (ActivitySummarySimpleEntry) entry;
final Object value = simpleEntry.getValue();
if (!(value instanceof Number)) {
return defaultValue;
}
return ((Number) value).doubleValue();
}
public boolean getBoolean(final String key, final boolean defaultValue) {
final ActivitySummaryEntry entry = entries.get(key);
if (!(entry instanceof ActivitySummarySimpleEntry)) {
return defaultValue;
}
final ActivitySummarySimpleEntry simpleEntry = (ActivitySummarySimpleEntry) entry;
final Object value = simpleEntry.getValue();
if (value instanceof Boolean) {
return (boolean) value;
}
if (!(value instanceof String)) {
return defaultValue;
}
return Boolean.parseBoolean((String) value);
}
@Nullable
public static ActivitySummaryData fromJson(final String string) {
if (StringUtils.isBlank(string)) {
return null;
}
final Type type = new TypeToken<LinkedHashMap<String, ActivitySummaryEntry>>(){}.getType();
final LinkedHashMap<String, ActivitySummaryEntry> entries = GSON.fromJson(string, type);
return new ActivitySummaryData(entries);
}
@NonNull
@Override
public String toString() {
return toJson();
}
public String toJson() {
return GSON.toJson(entries);
}
}

View File

@ -138,8 +138,20 @@ public class ActivitySummaryEntries {
public static final String UNIT_STROKES_PER_SECOND = "strokes_second";
public static final String UNIT_YARD = "yard";
public static final String UNIT_DEGREES = "degrees";
public static final String UNIT_STRING = "string";
public static final String UNIT_RAW_STRING = "raw_string";
public static final String UNIT_KG = "kg";
public static final String GROUP_PACE = "Pace";
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_HEART_RATE_ZONES = "HeartRateZones";
public static final String GROUP_STROKES = "Strokes";
public static final String GROUP_SWIMMING = "Swimming";
public static final String GROUP_TRAINING_EFFECT = "TrainingEffect";
public static final String GROUP_LAPS = "laps";
public static final String GROUP_RUNNING_FORM = "RunningForm";
/**
* Used to signal that this activity has a gps track. This is currently used by ActivitySummaryDetail

View File

@ -19,77 +19,63 @@ package nodomain.freeyourgadget.gadgetbridge.model;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.*;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryEntry;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
public class ActivitySummaryJsonSummary {
private static final Logger LOG = LoggerFactory.getLogger(ActivitySummaryJsonSummary.class);
private JSONObject groupData;
private JSONObject summaryData;
private JSONObject summaryGroupedList;
private ActivitySummaryParser summaryParser;
private BaseActivitySummary baseActivitySummary;
private Map<String, List<String>> groupData;
private ActivitySummaryData summaryData;
private Map<String, List<Pair<String, ActivitySummaryEntry>>> summaryGroupedList;
private final ActivitySummaryParser summaryParser;
private final BaseActivitySummary baseActivitySummary;
public ActivitySummaryJsonSummary(final ActivitySummaryParser summaryParser, BaseActivitySummary baseActivitySummary){
this.summaryParser=summaryParser;
this.baseActivitySummary=baseActivitySummary;
public ActivitySummaryJsonSummary(final ActivitySummaryParser summaryParser, BaseActivitySummary baseActivitySummary) {
this.summaryParser = summaryParser;
this.baseActivitySummary = baseActivitySummary;
}
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
if (item.getBaseAltitude() != null && item.getBaseAltitude() != -20000) {
JSONObject baseAltitudeValues;
try {
baseAltitudeValues = new JSONObject();
baseAltitudeValues.put("value", item.getBaseAltitude());
baseAltitudeValues.put("unit", "meters");
jsonSummary.put("baseAltitude", baseAltitudeValues);
} catch (JSONException e) {
e.printStackTrace();
}
}
if (jsonSummary.has("distanceMeters") && jsonSummary.has("activeSeconds")) {
JSONObject averageSpeed;
try {
JSONObject distanceMeters = (JSONObject) jsonSummary.get("distanceMeters");
JSONObject activeSeconds = (JSONObject) jsonSummary.get("activeSeconds");
double distance = distanceMeters.getDouble("value");
double duration = activeSeconds.getDouble("value");
averageSpeed = new JSONObject();
averageSpeed.put("value", distance / duration);
averageSpeed.put("unit", "meters_second");
jsonSummary.put("averageSpeed", averageSpeed);
} catch (JSONException e) {
e.printStackTrace();
}
}
private ActivitySummaryData setSummaryData(BaseActivitySummary item, final boolean forDetails) {
final ActivitySummaryData summary = ActivitySummaryData.fromJson(getCorrectSummary(item, forDetails));
if (summary == null) {
return null;
}
return jsonSummary;
//add additionally computed values here
if (item.getBaseAltitude() != null && item.getBaseAltitude() != -20000 && !summary.has("baseAltitude")) {
summary.add("baseAltitude", item.getBaseAltitude(), UNIT_METERS);
}
if (!summary.has("averageSpeed") && summary.has("distanceMeters") && summary.has("activeSeconds")) {
double distance = summary.getNumber("distanceMeters", 0).doubleValue();
double duration = summary.getNumber("activeSeconds", 1).doubleValue();
summary.add("averageSpeed", distance / duration, UNIT_METERS_PER_SECOND);
}
return summary;
}
public JSONObject getSummaryData(final boolean forDetails){
public ActivitySummaryData getSummaryData(final boolean forDetails) {
//returns json with summaryData
if (summaryData==null) summaryData=setSummaryData(baseActivitySummary, forDetails);
if (summaryData == null) summaryData = setSummaryData(baseActivitySummary, forDetails);
return summaryData;
}
private String getCorrectSummary(BaseActivitySummary item, final boolean forDetails){
private String getCorrectSummary(BaseActivitySummary item, final boolean forDetails) {
if (summaryParser == null) {
return item.getSummaryData();
}
try {
item = summaryParser.parseBinaryData(item, forDetails);
} catch (final Exception e) {
@ -98,144 +84,119 @@ public class ActivitySummaryJsonSummary {
return item.getSummaryData();
}
private JSONObject getJSONSummary(String sumData){
JSONObject summarySubdata = null;
if (sumData != null) {
try {
summarySubdata = new JSONObject(sumData);
} catch (JSONException e) {
}
}
return summarySubdata;
}
public JSONObject getSummaryGroupedList() {
public Map<String, List<Pair<String, ActivitySummaryEntry>>> getSummaryGroupedList() {
//returns list grouped by activity groups as per createActivitySummaryGroups
if (summaryData==null) summaryData=setSummaryData(baseActivitySummary, true);
if (summaryGroupedList==null) summaryGroupedList=setSummaryGroupedList(summaryData);
if (summaryData == null) summaryData = setSummaryData(baseActivitySummary, true);
if (summaryGroupedList == null) summaryGroupedList = setSummaryGroupedList(summaryData);
return summaryGroupedList;
}
private JSONObject setSummaryGroupedList(JSONObject summaryDatalist){
this.groupData = createActivitySummaryGroups(); //structure for grouping activities into groups, when vizualizing
if (summaryDatalist == null) return null;
Iterator<String> keys = summaryDatalist.keys();
private Map<String, List<Pair<String, ActivitySummaryEntry>>> setSummaryGroupedList(ActivitySummaryData activitySummaryData) {
this.groupData = createActivitySummaryGroups(); //structure for grouping activities into groups, when visualizing
final Map<String, JSONArray> activeGroups = new LinkedHashMap<>();
if (activitySummaryData == null) return null;
Iterator<String> keys = activitySummaryData.getKeys().iterator();
final Map<String, List<Pair<String, ActivitySummaryEntry>>> activeGroups = new LinkedHashMap<>();
// Initialize activeGroups with the initial expected order and empty arrays
final Iterator<String> names = this.groupData.keys();
while (names.hasNext()) {
activeGroups.put(names.next(), new JSONArray());
for (final String key : this.groupData.keySet()) {
activeGroups.put(key, new LinkedList<>());
}
while (keys.hasNext()) {
String key = keys.next();
if (INTERNAL_HAS_GPS.equals(key)) {
if (key.startsWith("internal")) {
continue;
}
try {
JSONObject innerData = (JSONObject) summaryDatalist.get(key);
Object value = innerData.get("value");
String unit = innerData.getString("unit");
// Use the group if specified in the entry, otherwise fallback to the array below
String groupName = innerData.optString("group", getGroup(key));
ActivitySummaryEntry item = activitySummaryData.get(key);
// Use the group if specified in the entry, otherwise fallback to the array below
String groupName = item.getGroup() != null ? item.getGroup() : getDefaultGroup(key);
JSONArray group = activeGroups.get(groupName);
if (group == null) {
// This group is not defined in createActivitySummaryGroups - add it to the end
group = new JSONArray();
activeGroups.put(groupName, group);
}
JSONObject item = new JSONObject();
item.put("name", key);
item.put("value", value);
item.put("unit", unit);
group.put(item);
} catch (JSONException e) {
LOG.error("SportsActivity internal error building grouped summary", e);
List<Pair<String, ActivitySummaryEntry>> group = activeGroups.get(groupName);
if (group == null) {
// This group is not defined in createActivitySummaryGroups - add it to the end
group = new LinkedList<>();
activeGroups.put(groupName, group);
}
group.add(Pair.of(key, item));
}
// Convert activeGroups to the expected JSONObject
// activeGroups is already ordered
final JSONObject grouped = new JSONObject();
for (final Map.Entry<String, JSONArray> entry : activeGroups.entrySet()) {
if (entry.getValue().length() == 0) {
final Map<String, List<Pair<String, ActivitySummaryEntry>>> grouped = new LinkedHashMap<>();
for (final Map.Entry<String, List<Pair<String, ActivitySummaryEntry>>> entry : activeGroups.entrySet()) {
if (entry.getValue().isEmpty()) {
// empty group
continue;
}
try {
grouped.put(entry.getKey(), entry.getValue());
} catch (JSONException e) {
LOG.error("SportsActivity internal error building grouped summary", e);
}
grouped.put(entry.getKey(), entry.getValue());
}
return grouped;
}
private String getGroup(String searchItem) {
// NB: Default group must be present in group JSONObject created by createActivitySummaryGroups
String defaultGroup = "Activity";
private String getDefaultGroup(final String searchItem) {
final String defaultGroup = GROUP_ACTIVITY;
if (groupData == null) return defaultGroup;
Iterator<String> keys = groupData.keys();
while (keys.hasNext()) {
String key = keys.next();
try {
JSONArray itemList = (JSONArray) groupData.get(key);
for (int i = 0; i < itemList.length(); i++) {
if (itemList.getString(i).equals(searchItem)) {
return key;
}
for (final String groupKey : groupData.keySet()) {
final List<String> itemList = groupData.get(groupKey);
if (itemList == null) {
continue;
}
for (final String itemKey : itemList) {
if (itemKey.equals(searchItem)) {
return groupKey;
}
} catch (JSONException e) {
LOG.error("SportsActivity", e);
}
}
// NB: Default group must be present in group JSONObject created by createActivitySummaryGroups
return defaultGroup;
}
/** @noinspection ArraysAsListWithZeroOrOneArgument*/
private JSONObject createActivitySummaryGroups(){
final Map<String, List<String>> groupDefinitions = new LinkedHashMap<String, List<String>>() {{
/**
* @noinspection ArraysAsListWithZeroOrOneArgument
*/
private static Map<String, List<String>> createActivitySummaryGroups() {
return new LinkedHashMap<String, List<String>>() {{
// NB: Default group Activity must be present in this definition, otherwise it wouldn't
// be shown.
put("Activity", Arrays.asList(
put(GROUP_ACTIVITY, Arrays.asList(
DISTANCE_METERS, STEPS, STEP_RATE_SUM, ACTIVE_SECONDS, CALORIES_BURNT,
STRIDE_TOTAL, HR_AVG, HR_MAX, HR_MIN, STRIDE_AVG, STRIDE_MAX, STRIDE_MIN,
STEP_LENGTH_AVG
));
put("Speed", Arrays.asList(
put(GROUP_SPEED, Arrays.asList(
SPEED_AVG, SPEED_MAX, SPEED_MIN, PACE_AVG_SECONDS_KM, PACE_MIN,
PACE_MAX, "averageSpeed2", CADENCE_AVG, CADENCE_MAX, CADENCE_MIN,
STEP_RATE_AVG
));
put("Elevation", Arrays.asList(
put(GROUP_ELEVATION, Arrays.asList(
ASCENT_METERS, DESCENT_METERS, ALTITUDE_MAX, ALTITUDE_MIN, ALTITUDE_AVG,
ALTITUDE_BASE, ASCENT_SECONDS, DESCENT_SECONDS, FLAT_SECONDS, ASCENT_DISTANCE,
DESCENT_DISTANCE, FLAT_DISTANCE, ELEVATION_GAIN, ELEVATION_LOSS
));
put("HeartRateZones", Arrays.asList(
put(GROUP_HEART_RATE_ZONES, Arrays.asList(
HR_ZONE_NA, HR_ZONE_WARM_UP, HR_ZONE_FAT_BURN, HR_ZONE_AEROBIC, HR_ZONE_ANAEROBIC,
HR_ZONE_EXTREME
));
put("Strokes", Arrays.asList(
put(GROUP_STROKES, Arrays.asList(
STROKE_DISTANCE_AVG, STROKE_AVG_PER_SECOND, STROKES,
STROKE_RATE_AVG, STROKE_RATE_MAX
));
put("Swimming", Arrays.asList(
put(GROUP_SWIMMING, Arrays.asList(
SWOLF_INDEX, SWOLF_AVG, SWOLF_MAX, SWOLF_MIN, SWIM_STYLE
));
put("TrainingEffect", Arrays.asList(
put(GROUP_TRAINING_EFFECT, Arrays.asList(
TRAINING_EFFECT_AEROBIC, TRAINING_EFFECT_ANAEROBIC, WORKOUT_LOAD,
MAXIMUM_OXYGEN_UPTAKE, RECOVERY_TIME, LACTATE_THRESHOLD_HR
));
put("laps", Arrays.asList(
put(GROUP_LAPS, Arrays.asList(
LAP_PACE_AVERAGE, LAPS, LANE_LENGTH
));
put("Pace", Arrays.asList(
put(GROUP_PACE, Arrays.asList(
));
put("RunningForm", Arrays.asList(
put(GROUP_RUNNING_FORM, Arrays.asList(
GROUND_CONTACT_TIME_AVG, IMPACT_AVG, IMPACT_MAX, SWING_ANGLE_AVG,
FORE_FOOT_LANDINGS, MID_FOOT_LANDINGS, BACK_FOOT_LANDINGS,
EVERSION_ANGLE_AVG, EVERSION_ANGLE_MAX
@ -243,7 +204,5 @@ public class ActivitySummaryJsonSummary {
put(SETS, Arrays.asList(
));
}};
return new JSONObject(groupDefinitions);
}
}

View File

@ -174,20 +174,13 @@ class BangleJSActivityTrack {
ActivitySummaryData summaryData = BangleJSWorkoutParser.dataFromPoints(banglePoints);
summary.setSummaryData(summaryData.toString());
ActivityKind activityKind;
final JSONObject speedAvgObj = summaryData.optJSONObject(SPEED_AVG);
if (speedAvgObj != null) {
double speedAvg;
try {
speedAvg = speedAvgObj.getDouble("value");
} catch (JSONException e) {
LOG.error("Failed to get speed avg");
speedAvg = -1;
}
if ((float) 3 > speedAvg) {
activityKind = ActivityKind.WALKING;
} else {
activityKind = ActivityKind.RUNNING;
}
final double speedAvg = summaryData.getNumber(SPEED_AVG, -1).doubleValue();
if (speedAvg >= 10) {
activityKind = ActivityKind.ACTIVITY;
} else if (speedAvg >= 3) {
activityKind = ActivityKind.RUNNING;
} else if (speedAvg >= 0) {
activityKind = ActivityKind.WALKING;
} else {
activityKind = ActivityKind.ACTIVITY;
}

View File

@ -197,6 +197,7 @@ public class JsonBackupPreferences {
.registerSubtype(IntegerPreferenceValue.class, INTEGER)
.registerSubtype(LongPreferenceValue.class, LONG)
.registerSubtype(StringPreferenceValue.class, STRING)
.registerSubtype(StringSetPreferenceValue.class, HASHSET);
.registerSubtype(StringSetPreferenceValue.class, HASHSET)
.recognizeSubtypes();
}
}

View File

@ -2231,6 +2231,7 @@
<string name="cyclingPowerMax">Max cycling power</string>
<string name="workoutSets">Sets</string>
<string name="workout_set_i">Set %1d</string>
<string name="workout_set_reps">Repetitions</string>
<string name="workout_set_repetitions">%1d x</string>
<string name="workout_set_repetitions_weight_kg">%1d x %2$.2f kg</string>
<string name="workout_set_repetitions_weight_lbs">%1d x %2$.2f lbs</string>

View File

@ -7,6 +7,10 @@ import com.google.gson.GsonBuilder;
import org.junit.Test;
/**
* Test our changes to the RuntimeTypeAdapterFactory, which allow for serialization and deserialization
* of a null type label.
*/
public class RuntimeTypeAdapterFactoryTest {
private static final Gson GSON = new GsonBuilder()
.registerTypeAdapterFactory(RuntimeTypeAdapterFactory

View File

@ -0,0 +1,121 @@
package nodomain.freeyourgadget.gadgetbridge.model;
import static org.junit.Assert.*;
import org.junit.Test;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryEntry;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryProgressEntry;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummarySimpleEntry;
public class ActivitySummaryDataTest {
/**
* Ensure that we can still deserialize old workouts that consisted of manual json and had
* no explicit type.
*/
@Test
public void deserializeOld() {
final String json = "{\n" +
" \"activeSeconds\": {\n" +
" \"group\": \"Some Group\",\n" +
" \"value\": 3828,\n" +
" \"unit\": \"seconds\"\n" +
" },\n" +
" \"averageHR\": {\n" +
" \"value\": 81,\n" +
" \"unit\": \"bpm\"\n" +
" },\n" +
" \"maxHR\": {\n" +
" \"value\": 115,\n" +
" \"unit\": \"bpm\"\n" +
" },\n" +
" \"minHR\": {\n" +
" \"value\": 61,\n" +
" \"unit\": \"bpm\"\n" +
" },\n" +
" \"caloriesBurnt\": {\n" +
" \"value\": 228,\n" +
" \"unit\": \"calories_unit\"\n" +
" },\n" +
" \"hrZoneNa\": {\n" +
" \"value\": 3365,\n" +
" \"unit\": \"seconds\"\n" +
" },\n" +
" \"hrZoneWarmUp\": {\n" +
" \"value\": 447,\n" +
" \"unit\": \"seconds\"\n" +
" },\n" +
" \"aerobicTrainingEffect\": {\n" +
" \"value\": 0.20000000298023224,\n" +
" \"unit\": \"\"\n" +
" },\n" +
" \"currentWorkoutLoad\": {\n" +
" \"value\": 1,\n" +
" \"unit\": \"\"\n" +
" }\n" +
"}";
final ActivitySummaryData summaryData = ActivitySummaryData.fromJson(json);
assertNotNull(summaryData);
final Map<String, ActivitySummaryEntry> expected = new LinkedHashMap<String, ActivitySummaryEntry>() {{
put("activeSeconds", new ActivitySummarySimpleEntry("Some Group", 3828, "seconds"));
put("averageHR", new ActivitySummarySimpleEntry(81, "bpm"));
put("maxHR", new ActivitySummarySimpleEntry(115, "bpm"));
put("minHR", new ActivitySummarySimpleEntry(61, "bpm"));
put("caloriesBurnt", new ActivitySummarySimpleEntry(228, "calories_unit"));
put("hrZoneNa", new ActivitySummarySimpleEntry(3365, "seconds"));
put("hrZoneWarmUp", new ActivitySummarySimpleEntry(447, "seconds"));
put("aerobicTrainingEffect", new ActivitySummarySimpleEntry(0.20000000298023224, ""));
put("currentWorkoutLoad", new ActivitySummarySimpleEntry(1, ""));
}};
final List<String> keys = new ArrayList<>(summaryData.getKeys());
assertEquals(new ArrayList<>(expected.keySet()), keys);
for (final Map.Entry<String, ActivitySummaryEntry> e : expected.entrySet()) {
final ActivitySummaryEntry jsonEntry = summaryData.get(e.getKey());
assertTrue(jsonEntry instanceof ActivitySummarySimpleEntry);
assertEquals(e.getValue().getGroup(), jsonEntry.getGroup());
Number expectedValue = (Number) ((ActivitySummarySimpleEntry) e.getValue()).getValue();
Number actualValue = (Number) ((ActivitySummarySimpleEntry) jsonEntry).getValue();
assertEquals(expectedValue.doubleValue(), actualValue.doubleValue(), 0.000000001d);
assertEquals(((ActivitySummarySimpleEntry) e.getValue()).getUnit(), ((ActivitySummarySimpleEntry) jsonEntry).getUnit());
}
}
@Test
public void deserializeSerializeNew() {
final String json = "{" +
"\"test_progress\":{" +
"\"type\":\"progress\"," +
"\"progress\":51," +
"\"value\":3828.0," +
"\"unit\":\"seconds\"" +
"}" +
"}";
final ActivitySummaryData summaryData = ActivitySummaryData.fromJson(json);
assertNotNull(summaryData);
ActivitySummaryEntry activitySummaryEntry = summaryData.get("test_progress");
assertTrue(activitySummaryEntry instanceof ActivitySummaryProgressEntry);
ActivitySummaryProgressEntry testProgress = (ActivitySummaryProgressEntry) activitySummaryEntry;
assertEquals(3828, ((Number) testProgress.getValue()).doubleValue(), 0.000000001d);
assertEquals("seconds", testProgress.getUnit());
assertEquals(51, testProgress.getProgress());
assertEquals(json, summaryData.toJson());
}
}