From dba9f32757a5164aed3d8e0b84fecfd22a6bad60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Sat, 21 Sep 2024 10:51:24 +0100 Subject: [PATCH] Use HR from track file if available --- .../ActivitySummariesChartFragment.java | 115 ++++++++++++++++-- .../ActivitySummariesGpsFragment.java | 89 +++++++------- .../activities/ActivitySummaryDetail.java | 83 ++++++------- .../charts/ActivityListingDetail.java | 2 +- .../charts/SampleXLabelFormatter.java | 6 +- 5 files changed, 193 insertions(+), 102 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummariesChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummariesChartFragment.java index fc038a73e..796a67c81 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummariesChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummariesChartFragment.java @@ -32,21 +32,30 @@ import com.github.mikephil.charting.components.XAxis; import com.github.mikephil.charting.components.YAxis; import com.github.mikephil.charting.data.Entry; import com.github.mikephil.charting.data.LineData; +import com.github.mikephil.charting.data.LineDataSet; +import com.github.mikephil.charting.formatter.ValueFormatter; +import com.github.mikephil.charting.interfaces.datasets.ILineDataSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.File; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.charts.AbstractActivityChartFragment; import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsData; import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsHost; import nodomain.freeyourgadget.gadgetbridge.activities.charts.DefaultChartsData; +import nodomain.freeyourgadget.gadgetbridge.activities.charts.SampleXLabelFormatter; +import nodomain.freeyourgadget.gadgetbridge.activities.charts.TimestampTranslation; import nodomain.freeyourgadget.gadgetbridge.database.DBAccess; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; @@ -54,17 +63,23 @@ public class ActivitySummariesChartFragment extends AbstractActivityChartFragmen private static final Logger LOG = LoggerFactory.getLogger(ActivitySummariesChartFragment.class); private LineChart mChart; - private int startTime; - private int endTime; - private GBDevice gbDevice; private View view; - public void setDateAndGetData(GBDevice gbDevice, long startTime, long endTime) { + // If a track file is being used (takes precedence over activity data) + private File trackFile; + + // If activity data is being used + private GBDevice gbDevice; + private int startTime; + private int endTime; + + public void setDateAndGetData(@Nullable File trackFile, GBDevice gbDevice, long startTime, long endTime) { + this.trackFile = trackFile; this.startTime = (int) startTime; this.endTime = (int) endTime; this.gbDevice = gbDevice; if (this.view != null) { - createLocalRefreshTask("Visualizing data", getActivity()).execute(); + createLocalRefreshTask("getting hr and activity", getActivity()).execute(); } } @@ -85,9 +100,9 @@ public class ActivitySummariesChartFragment extends AbstractActivityChartFragmen super.onViewCreated(view, savedInstanceState); init(); this.view = view; - if (this.gbDevice != null) { + if (this.trackFile != null || this.gbDevice != null) { setupChart(); - createLocalRefreshTask("Visualizing data", getActivity()).execute(); + createLocalRefreshTask("getting hr and activity", getActivity()).execute(); } } @@ -184,14 +199,24 @@ public class ActivitySummariesChartFragment extends AbstractActivityChartFragmen @Override protected void doInBackground(DBHandler handler) { - List samples = getAllSamples(handler, gbDevice, startTime, endTime); + final DefaultChartsData dcd; + final DefaultChartsData activitySamplesData = buildChartFromSamples(handler); - DefaultChartsData dcd = null; - try { - dcd = refresh(gbDevice, samples); - } catch (Exception e) { - LOG.debug("Unable to get charts data right now:", e); + if (trackFile != null) { + final List activityPoints = ActivitySummariesGpsFragment.getActivityPoints(trackFile) + .stream() + .filter(ap -> ap.getHeartRate() > 0) + .collect(Collectors.toList()); + + if (!activityPoints.isEmpty()) { + dcd = buildHeartRateChart(activityPoints, activitySamplesData); + } else { + dcd = activitySamplesData; + } + } else { + dcd = activitySamplesData; } + if (dcd != null) { mChart.setData(null); // workaround for https://github.com/PhilJay/MPAndroidChart/issues/2317 mChart.getXAxis().setValueFormatter(dcd.getXValueFormatter()); @@ -203,5 +228,69 @@ public class ActivitySummariesChartFragment extends AbstractActivityChartFragmen protected void onPostExecute(Object o) { mChart.invalidate(); } + + private DefaultChartsData buildChartFromSamples(DBHandler handler) { + final List samples = getAllSamples(handler, gbDevice, startTime, endTime); + + try { + return refresh(gbDevice, samples); + } catch (Exception e) { + LOG.error("Unable to get charts data right now", e); + } + + return null; + } + + private DefaultChartsData buildHeartRateChart(final List activityPoints, + final DefaultChartsData activitySamplesData) { + // If we have data from activity samples, we need to use the same TimestampTranslation so + // that the HR chart is aligned + // This is not ideal... + final TimestampTranslation tsTranslation; + if (activitySamplesData != null) { + final ValueFormatter xValueFormatter = activitySamplesData.getXValueFormatter(); + if (xValueFormatter instanceof SampleXLabelFormatter) { + tsTranslation = ((SampleXLabelFormatter) xValueFormatter).getTsTranslation(); + } else { + LOG.error("Unable to get TimestampTranslation from x value formatter - class changed?"); + tsTranslation = new TimestampTranslation(); + } + } else { + tsTranslation = new TimestampTranslation(); + } + + final List heartRateEntries = new ArrayList<>(activityPoints.size()); + int lastHrSampleTs = -1; + for (final ActivityPoint activityPoint : activityPoints) { + int ts = tsTranslation.shorten((int) (activityPoint.getTime().getTime() / 1000)); + if (lastHrSampleTs > -1 && ts - lastHrSampleTs > 1800 * HeartRateUtils.MAX_HR_MEASUREMENTS_GAP_MINUTES) { + heartRateEntries.add(createLineEntry(0, lastHrSampleTs + 1)); + heartRateEntries.add(createLineEntry(0, ts - 1)); + } + heartRateEntries.add(createLineEntry(activityPoint.getHeartRate(), ts)); + lastHrSampleTs = ts; + } + final LineDataSet heartRateSet = createHeartrateSet(heartRateEntries, "Heart Rate"); + + if (activitySamplesData != null) { + // if we have activity samples, replace the heart rate dataset + LineData data = activitySamplesData.getData(); + List dataSets = data.getDataSets(); + for (final ILineDataSet dataSet : dataSets) { + if ("Heart Rate".equals(dataSet.getLabel())) { + dataSets.remove(dataSet); + dataSets.add(heartRateSet); + return activitySamplesData; + } + } + // We failed to find a heart rate dataset, append ours + dataSets.add(heartRateSet); + return activitySamplesData; + } else { + final LineData lineData = new LineData(Collections.singletonList(heartRateSet)); + final ValueFormatter xValueFormatter = new SampleXLabelFormatter(tsTranslation, "HH:mm"); + return new DefaultChartsData<>(lineData, xValueFormatter); + } + } } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummariesGpsFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummariesGpsFragment.java index 25fa617fe..24138af25 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummariesGpsFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummariesGpsFragment.java @@ -32,23 +32,22 @@ import org.slf4j.LoggerFactory; import java.io.File; import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint; import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitFile; -import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitImporter; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitRecord; import nodomain.freeyourgadget.gadgetbridge.util.gpx.GpxParseException; import nodomain.freeyourgadget.gadgetbridge.util.gpx.GpxParser; -import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxFile; import static android.graphics.Bitmap.createBitmap; @@ -56,7 +55,7 @@ import static android.graphics.Bitmap.createBitmap; public class ActivitySummariesGpsFragment extends AbstractGBFragment { private static final Logger LOG = LoggerFactory.getLogger(ActivitySummariesGpsFragment.class); private ImageView gpsView; - private int CANVAS_SIZE = 360; + private final int CANVAS_SIZE = 360; private File inputFile; @Override @@ -79,51 +78,50 @@ public class ActivitySummariesGpsFragment extends AbstractGBFragment { private void processInBackgroundThread() { final Canvas canvas = createCanvas(gpsView); - new Thread(new Runnable() { - @Override - public void run() { - final List points = new ArrayList<>(); - if (inputFile.getName().endsWith(".gpx")) { - try (FileInputStream inputStream = new FileInputStream(inputFile)) { - final GpxParser gpxParser = new GpxParser(inputStream); - points.addAll(gpxParser.getGpxFile().getPoints()); - } catch (final IOException e) { - LOG.error("Failed to open {}", inputFile, e); - return; - } catch (final GpxParseException e) { - LOG.error("Failed to parse gpx file", e); - return; - } - } else if (inputFile.getName().endsWith(".fit")) { - try { - FitFile fitFile = FitFile.parseIncoming(inputFile); - for (final RecordData record : fitFile.getRecords()) { - if (record instanceof FitRecord) { - final ActivityPoint activityPoint = ((FitRecord) record).toActivityPoint(); - if (activityPoint.getLocation() != null) { - points.add(activityPoint.getLocation()); - } - } - } - } catch (final IOException e) { - LOG.error("Failed to open {}", inputFile, e); - return; - } catch (final Exception e) { - LOG.error("Failed to parse fit file", e); - return; - } - } else { - LOG.warn("Unknown file type {}", inputFile.getName()); - return; - } + new Thread(() -> { + final List points = getActivityPoints(inputFile) + .stream() + .map(ActivityPoint::getLocation) + .filter(Objects::nonNull) + .collect(Collectors.toList()); - if (!points.isEmpty()) { - drawTrack(canvas, points); - } + if (!points.isEmpty()) { + drawTrack(canvas, points); } }).start(); } + public static List getActivityPoints(final File trackFile) { + final List points = new ArrayList<>(); + if (trackFile.getName().endsWith(".gpx")) { + try (FileInputStream inputStream = new FileInputStream(trackFile)) { + final GpxParser gpxParser = new GpxParser(inputStream); + points.addAll(gpxParser.getGpxFile().getActivityPoints()); + } catch (final IOException e) { + LOG.error("Failed to open {}", trackFile, e); + } catch (final GpxParseException e) { + LOG.error("Failed to parse gpx file", e); + } + } else if (trackFile.getName().endsWith(".fit")) { + try { + FitFile fitFile = FitFile.parseIncoming(trackFile); + for (final RecordData record : fitFile.getRecords()) { + if (record instanceof FitRecord) { + points.add(((FitRecord) record).toActivityPoint()); + } + } + } catch (final IOException e) { + LOG.error("Failed to open {}", trackFile, e); + } catch (final Exception e) { + LOG.error("Failed to parse fit file", e); + } + } else { + LOG.warn("Unknown file type {}", trackFile.getName()); + } + + return points; + } + private void drawTrack(Canvas canvas, List trackPoints) { double maxLat = (Collections.max(trackPoints, new GPSCoordinate.compareLatitude())).getLatitude(); double minLat = (Collections.min(trackPoints, new GPSCoordinate.compareLatitude())).getLatitude(); @@ -154,11 +152,10 @@ public class ActivitySummariesGpsFragment extends AbstractGBFragment { } } - private Canvas createCanvas(ImageView imageView) { Bitmap bitmap = createBitmap(CANVAS_SIZE, CANVAS_SIZE, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); - canvas.drawColor(GBApplication.getWindowBackgroundColor(getActivity())); + canvas.drawColor(GBApplication.getWindowBackgroundColor(requireActivity())); //frame around, but it doesn't look so nice /* Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummaryDetail.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummaryDetail.java index 7da25505b..0ee5b1ae1 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummaryDetail.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ActivitySummaryDetail.java @@ -35,6 +35,7 @@ import android.util.TypedValue; import android.view.Gravity; import android.view.Menu; import android.view.MenuItem; +import android.view.SubMenu; import android.view.View; import android.view.ViewGroup; import android.view.animation.Animation; @@ -110,6 +111,9 @@ public class ActivitySummaryDetail extends AbstractGBActivity { String selectedGpxFile; File export_path = null; + private ActivitySummariesChartFragment activitySummariesChartFragment; + private ActivitySummariesGpsFragment activitySummariesGpsFragment; + public static int getAlternateColor(Context context) { TypedValue typedValue = new TypedValue(); Resources.Theme theme = context.getTheme(); @@ -171,8 +175,8 @@ public class ActivitySummaryDetail extends AbstractGBActivity { this, R.anim.bounceright); - final ActivitySummariesChartFragment activitySummariesChartFragment = new ActivitySummariesChartFragment(); - final ActivitySummariesGpsFragment activitySummariesGpsFragment = new ActivitySummariesGpsFragment(); + activitySummariesChartFragment = new ActivitySummariesChartFragment(); + activitySummariesGpsFragment = new ActivitySummariesGpsFragment(); getSupportFragmentManager() .beginTransaction() @@ -186,18 +190,8 @@ public class ActivitySummaryDetail extends AbstractGBActivity { BaseActivitySummary newItem = items.getNextItem(); if (newItem != null) { currentItem = newItem; - makeSummaryHeader(newItem); - makeSummaryContent(newItem); - activitySummariesChartFragment.setDateAndGetData(getGBDevice(currentItem.getDevice()), currentItem.getStartTime().getTime() / 1000, currentItem.getEndTime().getTime() / 1000); - if (itemHasGps()) { - showGpsCanvas(); - activitySummariesGpsFragment.set_data(getTrackFile()); - } else { - hideGpsCanvas(); - } - + refreshFromCurrentItem(); layout.startAnimation(animFadeRight); - updateMenuItems(); } else { layout.startAnimation(animBounceRight); } @@ -208,19 +202,8 @@ public class ActivitySummaryDetail extends AbstractGBActivity { BaseActivitySummary newItem = items.getPrevItem(); if (newItem != null) { currentItem = newItem; - makeSummaryHeader(newItem); - makeSummaryContent(newItem); - activitySummariesChartFragment.setDateAndGetData(getGBDevice(currentItem.getDevice()), currentItem.getStartTime().getTime() / 1000, currentItem.getEndTime().getTime() / 1000); - if (itemHasGps()) { - showGpsCanvas(); - activitySummariesGpsFragment.set_data(getTrackFile()); - } else { - hideGpsCanvas(); - } - - + refreshFromCurrentItem(); layout.startAnimation(animFadeLeft); - updateMenuItems(); } else { layout.startAnimation(animBounceLeft); } @@ -229,18 +212,9 @@ public class ActivitySummaryDetail extends AbstractGBActivity { currentItem = items.getItem(position); if (currentItem != null) { - makeSummaryHeader(currentItem); - makeSummaryContent(currentItem); - activitySummariesChartFragment.setDateAndGetData(getGBDevice(currentItem.getDevice()), currentItem.getStartTime().getTime() / 1000, currentItem.getEndTime().getTime() / 1000); - if (itemHasGps()) { - showGpsCanvas(); - activitySummariesGpsFragment.set_data(getTrackFile()); - } else { - hideGpsCanvas(); - } + refreshFromCurrentItem(); } - //allows long-press.switch of data being in raw form or recalculated ImageView activity_icon = findViewById(R.id.item_image); activity_icon.setOnLongClickListener(new View.OnLongClickListener() { @@ -382,6 +356,27 @@ public class ActivitySummaryDetail extends AbstractGBActivity { } + private void refreshFromCurrentItem() { + makeSummaryHeader(currentItem); + makeSummaryContent(currentItem); + + activitySummariesChartFragment.setDateAndGetData( + getTrackFile(), + getGBDevice(currentItem.getDevice()), + currentItem.getStartTime().getTime() / 1000, + currentItem.getEndTime().getTime() / 1000 + ); + + if (itemHasGps()) { + showGpsCanvas(); + activitySummariesGpsFragment.set_data(getTrackFile()); + } else { + hideGpsCanvas(); + } + + updateMenuItems(); + } + private File get_path() { File path = null; try { @@ -738,12 +733,18 @@ public class ActivitySummaryDetail extends AbstractGBActivity { } } - mOptionsMenu.findItem(R.id.activity_detail_overflowMenu).getSubMenu().findItem(R.id.activity_action_show_gpx).setVisible(hasGpx); - mOptionsMenu.findItem(R.id.activity_detail_overflowMenu).getSubMenu().findItem(R.id.activity_action_share_gpx).setVisible(hasGpx); - mOptionsMenu.findItem(R.id.activity_detail_overflowMenu).getSubMenu().findItem(R.id.activity_action_dev_share_raw_summary).setVisible(hasRawSummary); - mOptionsMenu.findItem(R.id.activity_detail_overflowMenu).getSubMenu().findItem(R.id.activity_action_dev_share_raw_details).setVisible(hasRawDetails); - final MenuItem devToolsMenu = mOptionsMenu.findItem(R.id.activity_detail_overflowMenu).getSubMenu().findItem(R.id.activity_action_dev_tools); - devToolsMenu.setVisible(devToolsMenu.getSubMenu().hasVisibleItems()); + if (mOptionsMenu != null) { + final SubMenu overflowMenu = mOptionsMenu.findItem(R.id.activity_detail_overflowMenu).getSubMenu(); + if (overflowMenu != null) { + overflowMenu.findItem(R.id.activity_action_show_gpx).setVisible(hasGpx); + overflowMenu.findItem(R.id.activity_action_share_gpx).setVisible(hasGpx); + overflowMenu.findItem(R.id.activity_action_dev_share_raw_summary).setVisible(hasRawSummary); + overflowMenu.findItem(R.id.activity_action_dev_share_raw_details).setVisible(hasRawDetails); + final MenuItem devToolsMenu = overflowMenu.findItem(R.id.activity_action_dev_tools); + final SubMenu devToolsSubMenu = devToolsMenu.getSubMenu(); + devToolsMenu.setVisible(devToolsSubMenu != null && devToolsSubMenu.hasVisibleItems()); + } + } } private void showGpsCanvas() { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityListingDetail.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityListingDetail.java index 99e0017bf..722c4c935 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityListingDetail.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityListingDetail.java @@ -81,7 +81,7 @@ public class ActivityListingDetail extends DialogFragment { .beginTransaction() .replace(R.id.chartsFragmentHolder, activitySummariesChartFragment) .commit(); - activitySummariesChartFragment.setDateAndGetData(gbDevice, tsFrom, tsTo); + activitySummariesChartFragment.setDateAndGetData(null, gbDevice, tsFrom, tsTo); ActivityListingAdapter stepListAdapter = new ActivityListingAdapter(getContext()); View activityItem = view.findViewById(R.id.activityItemHolder); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SampleXLabelFormatter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SampleXLabelFormatter.java index 576bf3696..bb5fdf38d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SampleXLabelFormatter.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SampleXLabelFormatter.java @@ -25,7 +25,7 @@ import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; -class SampleXLabelFormatter extends ValueFormatter { +public class SampleXLabelFormatter extends ValueFormatter { private final TimestampTranslation tsTranslation; @SuppressLint("SimpleDateFormat") private final SimpleDateFormat annotationDateFormat; @@ -46,4 +46,8 @@ class SampleXLabelFormatter extends ValueFormatter { final Date date = cal.getTime(); return annotationDateFormat.format(date); } + + public TimestampTranslation getTsTranslation() { + return tsTranslation; + } }