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:
parent
5192304d29
commit
3e327e2924
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user