diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityChartsActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityChartsActivity.java
index 743ae9129..75fd54df7 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityChartsActivity.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/ActivityChartsActivity.java
@@ -170,7 +170,7 @@ public class ActivityChartsActivity extends AbstractChartsActivity {
case "sleep":
return SleepCollectionFragment.newInstance(enabledTabsList.size() == 1);
case "heartrate":
- return new HeartRateDailyFragment();
+ return HeartRateCollectionFragment.newInstance(enabledTabsList.size() == 1);
case "hrvstatus":
return new HRVStatusFragment();
case "bodyenergy":
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/HeartRateCollectionFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/HeartRateCollectionFragment.java
new file mode 100644
index 000000000..50b0ef774
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/HeartRateCollectionFragment.java
@@ -0,0 +1,44 @@
+/* Copyright (C) 2024 a0z, José Rebelo
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.activities.charts;
+
+import android.os.Bundle;
+
+import androidx.fragment.app.FragmentManager;
+
+import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
+import nodomain.freeyourgadget.gadgetbridge.adapter.NestedFragmentAdapter;
+import nodomain.freeyourgadget.gadgetbridge.adapter.HeartRateFragmentAdapter;
+
+public class HeartRateCollectionFragment extends AbstractCollectionFragment {
+ public HeartRateCollectionFragment() {
+
+ }
+
+ public static HeartRateCollectionFragment newInstance(final boolean allowSwipe) {
+ final HeartRateCollectionFragment fragment = new HeartRateCollectionFragment();
+ final Bundle args = new Bundle();
+ args.putBoolean(ARG_ALLOW_SWIPE, allowSwipe);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public NestedFragmentAdapter getNestedFragmentAdapter(AbstractGBFragment fragment, FragmentManager childFragmentManager) {
+ return new HeartRateFragmentAdapter(this, getChildFragmentManager());
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/HeartRateDailyFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/HeartRateDailyFragment.java
deleted file mode 100644
index 10e19c922..000000000
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/HeartRateDailyFragment.java
+++ /dev/null
@@ -1,322 +0,0 @@
-package nodomain.freeyourgadget.gadgetbridge.activities.charts;
-
-import android.graphics.Color;
-import android.os.Build;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import androidx.core.content.ContextCompat;
-
-import com.github.mikephil.charting.charts.Chart;
-import com.github.mikephil.charting.charts.LineChart;
-import com.github.mikephil.charting.components.LegendEntry;
-import com.github.mikephil.charting.components.LimitLine;
-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.interfaces.datasets.ILineDataSet;
-
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Comparator;
-import java.util.Date;
-import java.util.List;
-
-import nodomain.freeyourgadget.gadgetbridge.GBApplication;
-import nodomain.freeyourgadget.gadgetbridge.R;
-import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils;
-import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
-import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
-import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
-import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
-import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
-import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
-import nodomain.freeyourgadget.gadgetbridge.model.HeartRateSample;
-import nodomain.freeyourgadget.gadgetbridge.util.Accumulator;
-import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
-
-public class HeartRateDailyFragment extends AbstractChartFragment {
-
- protected int HEARTRATE_COLOR;
- protected int CHART_TEXT_COLOR;
- protected int BACKGROUND_COLOR;
- protected int DESCRIPTION_COLOR;
- protected int LEGEND_TEXT_COLOR;
-
- private TextView mDateView;
- private TextView hrResting;
- private TextView hrAverage;
- private TextView hrMinimum;
- private TextView hrMaximum;
- private LineChart hrLineChart;
-
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- View rootView = inflater.inflate(R.layout.fragment_heart_rate, container, false);
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- rootView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
- getChartsHost().enableSwipeRefresh(scrollY == 0);
- });
- }
-
- mDateView = rootView.findViewById(R.id.hr_date_view);
- hrLineChart = rootView.findViewById(R.id.heart_rate_line_chart);
- hrResting = rootView.findViewById(R.id.hr_resting);
- hrAverage = rootView.findViewById(R.id.hr_average);
- hrMinimum = rootView.findViewById(R.id.hr_minimum);
- hrMaximum = rootView.findViewById(R.id.hr_maximum);
- final LinearLayout heartRateRestingWrapper = rootView.findViewById(R.id.hr_resting_wrapper);
-
- setupChart();
- refresh();
- setupLegend(hrLineChart);
-
- if (!supportsHeartRateRestingMeasurement()) {
- heartRateRestingWrapper.setVisibility(View.GONE);
- }
-
- return rootView;
- }
-
- public boolean supportsHeartRateRestingMeasurement() {
- final GBDevice device = getChartsHost().getDevice();
- return device.getDeviceCoordinator().supportsHeartRateRestingMeasurement(device);
- }
-
- protected List extends AbstractActivitySample> getActivitySamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
- SampleProvider extends ActivitySample> provider = device.getDeviceCoordinator().getSampleProvider(device, db.getDaoSession());
- return provider.getAllActivitySamplesHighRes(tsFrom, tsTo);
- }
-
- @Override
- public String getTitle() {
- return getString(R.string.heart_rate);
- }
-
- @Override
- protected void init() {
- Prefs prefs = GBApplication.getPrefs();
- CHART_TEXT_COLOR = GBApplication.getSecondaryTextColor(getContext());
- DESCRIPTION_COLOR = LEGEND_TEXT_COLOR = GBApplication.getTextColor(getContext());
- if (prefs.getBoolean("chart_heartrate_color", false)) {
- HEARTRATE_COLOR = ContextCompat.getColor(getContext(), R.color.chart_heartrate_alternative);
- }else{
- HEARTRATE_COLOR = ContextCompat.getColor(getContext(), R.color.chart_heartrate);
- }
- }
-
- @Override
- protected HeartRateDailyFragment.HeartRateData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
- Calendar day = Calendar.getInstance();
- day.setTime(chartsHost.getEndDate());
- day.add(Calendar.DATE, 0);
- day.set(Calendar.HOUR_OF_DAY, 0);
- day.set(Calendar.MINUTE, 0);
- day.set(Calendar.SECOND, 0);
- day.add(Calendar.HOUR, 0);
- int startTs = (int) (day.getTimeInMillis() / 1000);
- int endTs = startTs + 24 * 60 * 60 - 1;
-
- List extends ActivitySample> samples = getActivitySamples(db, device, startTs, endTs);
-
- int restingHeartRate = -1;
- if (supportsHeartRateRestingMeasurement()) {
- restingHeartRate = device.getDeviceCoordinator()
- .getHeartRateRestingSampleProvider(device, db.getDaoSession())
- .getAllSamples(startTs * 1000L, endTs * 1000L)
- .stream()
- .max(Comparator.comparingLong(HeartRateSample::getTimestamp))
- .map(HeartRateSample::getHeartRate)
- .orElse(-1);
- }
-
- return new HeartRateData(samples, restingHeartRate);
- }
-
- @Override
- protected void renderCharts() {
- hrLineChart.invalidate();
- }
-
- private void setupChart() {
- hrLineChart.setBackgroundColor(BACKGROUND_COLOR);
- hrLineChart.getDescription().setTextColor(DESCRIPTION_COLOR);
- hrLineChart.getDescription().setEnabled(false);
-
-
- XAxis x = hrLineChart.getXAxis();
- x.setDrawLabels(true);
- x.setDrawGridLines(false);
- x.setEnabled(true);
- x.setTextColor(CHART_TEXT_COLOR);
- x.setDrawLimitLinesBehindData(true);
- x.setPosition(XAxis.XAxisPosition.BOTTOM);
- x.setAxisMinimum(0f);
- x.setAxisMaximum(86400f);
-
- YAxis y = hrLineChart.getAxisLeft();
- y.setDrawGridLines(false);
- y.setDrawTopYLabelEntry(true);
- y.setTextColor(CHART_TEXT_COLOR);
- y.setEnabled(true);
- y.setAxisMaximum(HeartRateUtils.getInstance().getMaxHeartRate());
- y.setAxisMinimum(HeartRateUtils.getInstance().getMinHeartRate());
-
- YAxis yAxisRight = hrLineChart.getAxisRight();
- yAxisRight.setDrawGridLines(false);
- yAxisRight.setDrawLabels(true);
- yAxisRight.setDrawTopYLabelEntry(true);
- yAxisRight.setTextColor(CHART_TEXT_COLOR);
- yAxisRight.setAxisMaximum(HeartRateUtils.getInstance().getMaxHeartRate());
- yAxisRight.setAxisMinimum(HeartRateUtils.getInstance().getMinHeartRate());
-
- refresh();
- }
-
- @Override
- protected void setupLegend(Chart> chart) {
- List legendEntries = new ArrayList<>(1);
- LegendEntry hrEntry = new LegendEntry();
- hrEntry.label = getTitle();
- hrEntry.formColor = HEARTRATE_COLOR;
- legendEntries.add(hrEntry);
-
- if (GBApplication.getPrefs().getBoolean("charts_show_average", true)) {
- LegendEntry hrAverageEntry = new LegendEntry();
- hrAverageEntry.label = getString(R.string.hr_average);
- hrAverageEntry.formColor = Color.RED;
- legendEntries.add(hrAverageEntry);
- }
-
- //if (supportsHeartRateRestingMeasurement()) {
- // LegendEntry hrRestingEntry = new LegendEntry();
- // hrRestingEntry.label = getString(R.string.hr_resting);
- // hrRestingEntry.formColor = Color.GRAY;
- // legendEntries.add(hrRestingEntry);
- //}
-
- chart.getLegend().setCustom(legendEntries);
- chart.getLegend().setTextColor(LEGEND_TEXT_COLOR);
- chart.getLegend().setWordWrapEnabled(true);
- }
-
- protected LineDataSet createHeartRateDataSet(final List values) {
- LineDataSet dataSet = new LineDataSet(values, "Heart Rate");
- dataSet.setLineWidth(1.5f);
- dataSet.setMode(LineDataSet.Mode.HORIZONTAL_BEZIER);
- dataSet.setCubicIntensity(0.1f);
- dataSet.setDrawCircles(false);
- dataSet.setDrawValues(true);
- dataSet.setAxisDependency(YAxis.AxisDependency.RIGHT);
- dataSet.setColor(HEARTRATE_COLOR);
- dataSet.setValueTextColor(CHART_TEXT_COLOR);
- return dataSet;
- }
-
- @Override
- protected void updateChartsnUIThread(HeartRateDailyFragment.HeartRateData data) {
- Calendar day = Calendar.getInstance();
- day.setTime(getEndDate());
- day.add(Calendar.DATE, 0);
- day.set(Calendar.HOUR_OF_DAY, 0);
- day.set(Calendar.MINUTE, 0);
- day.set(Calendar.SECOND, 0);
- day.add(Calendar.HOUR, 0);
- int startTs = (int) (day.getTimeInMillis() / 1000);
- int endTs = startTs + 24 * 60 * 60 - 1;
- Date date = new Date((long) endTs * 1000);
- String formattedDate = new SimpleDateFormat("E, MMM dd").format(date);
- mDateView.setText(formattedDate);
-
- HeartRateUtils heartRateUtilsInstance = HeartRateUtils.getInstance();
- final TimestampTranslation tsTranslation = new TimestampTranslation();
- final List lineEntries = new ArrayList<>();
- List extends ActivitySample> samples = data.samples;
- final Accumulator accumulator = new Accumulator();
-
- final List lineDataSets = new ArrayList<>();
- int lastTsShorten = 0;
- for (int i =0; i < samples.size(); i++) {
- final ActivitySample sample = samples.get(i);
- final int tsShorten = tsTranslation.shorten(sample.getTimestamp());
- if (!heartRateUtilsInstance.isValidHeartRateValue(sample.getHeartRate())) {
- continue;
- }
- if (lastTsShorten == 0 || (tsShorten - lastTsShorten) <= 60 * HeartRateUtils.MAX_HR_MEASUREMENTS_GAP_MINUTES) {
- lineEntries.add(new Entry(tsShorten, sample.getHeartRate()));
- } else {
- if (!lineEntries.isEmpty()) {
- List clone = new ArrayList<>(lineEntries.size());
- clone.addAll(lineEntries);
- lineDataSets.add(createHeartRateDataSet(clone));
- lineEntries.clear();
- }
- }
- lastTsShorten = tsShorten;
- lineEntries.add(new Entry(tsShorten, sample.getHeartRate()));
- accumulator.add(sample.getHeartRate());
- }
-
- if (!lineEntries.isEmpty()) {
- lineDataSets.add(createHeartRateDataSet(lineEntries));
- }
-
- final int average = accumulator.getCount() > 0 ? (int) Math.round(accumulator.getAverage()) : -1;
- final int minimum = accumulator.getCount() > 0 ? (int) Math.round(accumulator.getMin()) : -1;
- final int maximum = accumulator.getCount() > 0 ? (int) Math.round(accumulator.getMax()) : -1;
-
- hrAverage.setText(average > 0 ? getString(R.string.bpm_value_unit, average) : "-");
- hrMinimum.setText(minimum > 0 ? getString(R.string.bpm_value_unit, minimum) : "-");
- hrMaximum.setText(maximum > 0 ? getString(R.string.bpm_value_unit, maximum) : "-");
- hrResting.setText(data.restingHeartRate > 0 ? getString(R.string.bpm_value_unit, data.restingHeartRate) : "-");
-
- if (minimum > 0) {
- hrLineChart.getAxisLeft().setAxisMinimum(Math.max(minimum - 30, 0));
- hrLineChart.getAxisRight().setAxisMinimum(Math.max(minimum - 30, 0));
- }
- if (maximum > 0) {
- hrLineChart.getAxisLeft().setAxisMaximum(maximum + 30);
- hrLineChart.getAxisRight().setAxisMaximum(maximum + 30);
- }
-
- hrLineChart.getXAxis().setValueFormatter(new SampleXLabelFormatter(tsTranslation, "HH:mm"));
- hrLineChart.setData(new LineData(lineDataSets));
-
- hrLineChart.getAxisLeft().removeAllLimitLines();
-
- if (average > 0 && GBApplication.getPrefs().getBoolean("charts_show_average", true)) {
- final LimitLine averageLine = new LimitLine(average);
- averageLine.setLineWidth(1.5f);
- averageLine.enableDashedLine(15f, 10f, 0f);
- averageLine.setLineColor(Color.RED);
- hrLineChart.getAxisLeft().addLimitLine(averageLine);
- }
-
- //if (data.restingHeartRate > 0) {
- // final LimitLine restingLine = new LimitLine(data.restingHeartRate);
- // restingLine.setLineWidth(1.5f);
- // restingLine.enableDashedLine(15f, 10f, 0f);
- // restingLine.setLineColor(Color.GRAY);
- // hrLineChart.getAxisLeft().addLimitLine(restingLine);
- //}
- }
-
- protected static class HeartRateData extends ChartsData {
- public List extends ActivitySample> samples;
- public int restingHeartRate;
-
- protected HeartRateData(List extends ActivitySample> samples, int restingHeartRate) {
- this.samples = samples;
- this.restingHeartRate = restingHeartRate;
- }
- }
-}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/HeartRatePeriodFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/HeartRatePeriodFragment.java
new file mode 100644
index 000000000..4ffd514c8
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/HeartRatePeriodFragment.java
@@ -0,0 +1,471 @@
+package nodomain.freeyourgadget.gadgetbridge.activities.charts;
+
+import android.graphics.Color;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.core.content.ContextCompat;
+
+import com.github.mikephil.charting.charts.Chart;
+import com.github.mikephil.charting.charts.LineChart;
+import com.github.mikephil.charting.components.LegendEntry;
+import com.github.mikephil.charting.components.LimitLine;
+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 java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
+import org.apache.commons.lang3.time.DateUtils;
+import org.apache.commons.lang3.tuple.Pair;
+
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
+import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.model.HeartRateSample;
+import nodomain.freeyourgadget.gadgetbridge.util.Accumulator;
+import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
+import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
+
+public class HeartRatePeriodFragment extends AbstractChartFragment {
+
+ static int SEC_PER_DAY = 24 * 60 * 60;
+ static int DATA_INVALID = -1;
+
+ protected int HEARTRATE_COLOR;
+ protected int HEARTRATE_MIN_COLOR;
+ protected int HEARTRATE_RESTING_COLOR;
+ protected int HEARTRATE_MAX_COLOR;
+ protected int CHART_TEXT_COLOR;
+ protected int BACKGROUND_COLOR;
+ protected int DESCRIPTION_COLOR;
+ protected int LEGEND_TEXT_COLOR;
+
+ private TextView mDateView;
+ private TextView hrResting;
+ private TextView hrAverage;
+ private TextView hrMinimum;
+ private TextView hrMaximum;
+ private LineChart hrLineChart;
+ private int TOTAL_DAYS;
+
+ public static HeartRatePeriodFragment newInstance(int totalDays) {
+ HeartRatePeriodFragment fragmentFirst = new HeartRatePeriodFragment();
+ Bundle args = new Bundle();
+ args.putInt("totalDays", totalDays);
+ fragmentFirst.setArguments(args);
+ return fragmentFirst;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ TOTAL_DAYS = getArguments() != null ? getArguments().getInt("totalDays") : 0;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View rootView = inflater.inflate(R.layout.fragment_heart_rate, container, false);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ rootView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
+ getChartsHost().enableSwipeRefresh(scrollY == 0);
+ });
+ }
+
+ mDateView = rootView.findViewById(R.id.hr_date_view);
+ hrLineChart = rootView.findViewById(R.id.heart_rate_line_chart);
+ hrResting = rootView.findViewById(R.id.hr_resting);
+ hrAverage = rootView.findViewById(R.id.hr_average);
+ hrMinimum = rootView.findViewById(R.id.hr_minimum);
+ hrMaximum = rootView.findViewById(R.id.hr_maximum);
+ final LinearLayout heartRateRestingWrapper = rootView.findViewById(R.id.hr_resting_wrapper);
+
+ setupChart();
+ refresh();
+ setupLegend(hrLineChart);
+
+ if (!supportsHeartRateRestingMeasurement()) {
+ heartRateRestingWrapper.setVisibility(View.GONE);
+ }
+
+ return rootView;
+ }
+
+ public boolean supportsHeartRateRestingMeasurement() {
+ final GBDevice device = getChartsHost().getDevice();
+ return device.getDeviceCoordinator().supportsHeartRateRestingMeasurement(device);
+ }
+
+ protected List extends AbstractActivitySample> getActivitySamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
+ SampleProvider extends ActivitySample> provider = device.getDeviceCoordinator().getSampleProvider(device, db.getDaoSession());
+ return provider.getAllActivitySamplesHighRes(tsFrom, tsTo);
+ }
+
+ @Override
+ public String getTitle() {
+ return getString(R.string.heart_rate);
+ }
+
+ @Override
+ protected void init() {
+ Prefs prefs = GBApplication.getPrefs();
+ CHART_TEXT_COLOR = GBApplication.getSecondaryTextColor(getContext());
+ DESCRIPTION_COLOR = LEGEND_TEXT_COLOR = GBApplication.getTextColor(getContext());
+ if (prefs.getBoolean("chart_heartrate_color", false)) {
+ HEARTRATE_COLOR = ContextCompat.getColor(getContext(), R.color.chart_heartrate_alternative);
+ }else{
+ HEARTRATE_COLOR = ContextCompat.getColor(getContext(), R.color.chart_heartrate);
+ }
+ HEARTRATE_MIN_COLOR = ContextCompat.getColor(getContext(), R.color.chart_heartrate_minimum);
+ HEARTRATE_MAX_COLOR = ContextCompat.getColor(getContext(), R.color.chart_heartrate_maximum);
+ HEARTRATE_RESTING_COLOR = ContextCompat.getColor(getContext(), R.color.chart_heartrate_resting);
+ }
+
+ private HeartRateData fetchHeartRateDataForDay(ChartsHost chartsHost, DBHandler db, GBDevice device, int startTs) {
+ int endTs = startTs + SEC_PER_DAY - 1;
+ List extends ActivitySample> samples = getActivitySamples(db, device, startTs, endTs);
+
+ int restingHeartRate = DATA_INVALID;
+ if (supportsHeartRateRestingMeasurement()) {
+ restingHeartRate = device.getDeviceCoordinator()
+ .getHeartRateRestingSampleProvider(device, db.getDaoSession())
+ .getAllSamples(startTs * 1000L, endTs * 1000L)
+ .stream()
+ .max(Comparator.comparingLong(HeartRateSample::getTimestamp))
+ .map(HeartRateSample::getHeartRate)
+ .orElse(DATA_INVALID);
+ }
+ HeartRateUtils heartRateUtilsInstance = HeartRateUtils.getInstance();
+ final Accumulator accumulator = new Accumulator();
+ for (int i = 0; i < samples.size(); i++) {
+ final ActivitySample sample = samples.get(i);
+ if (heartRateUtilsInstance.isValidHeartRateValue(sample.getHeartRate())) {
+ accumulator.add(sample.getHeartRate());
+ }
+ }
+
+ final int average = accumulator.getCount() > 0 ? (int) Math.round(accumulator.getAverage()) : DATA_INVALID;
+ final int minimum = accumulator.getCount() > 0 ? (int) Math.round(accumulator.getMin()) : DATA_INVALID;
+ final int maximum = accumulator.getCount() > 0 ? (int) Math.round(accumulator.getMax()) : DATA_INVALID;
+
+ return new HeartRateData(samples, restingHeartRate, average, minimum, maximum);
+ }
+
+ @Override
+ protected HeartRatePeriodData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
+ Pair startAndEndTs = getStartAndEndTS();
+ final int startTs = startAndEndTs.getKey();
+
+ List result = new ArrayList();
+ for (int i = 0; i < TOTAL_DAYS; i++) {
+ HeartRateData dayData = fetchHeartRateDataForDay(chartsHost, db, device, startTs + i * SEC_PER_DAY);
+ result.add(dayData);
+ }
+ return new HeartRatePeriodData(result);
+ }
+
+ @Override
+ protected void renderCharts() {
+ hrLineChart.invalidate();
+ }
+
+ private void setupChart() {
+ hrLineChart.setBackgroundColor(BACKGROUND_COLOR);
+ hrLineChart.getDescription().setTextColor(DESCRIPTION_COLOR);
+ hrLineChart.getDescription().setEnabled(false);
+
+ XAxis x = hrLineChart.getXAxis();
+ x.setDrawLabels(true);
+ x.setDrawGridLines(false);
+ x.setEnabled(true);
+ x.setTextColor(CHART_TEXT_COLOR);
+ x.setDrawLimitLinesBehindData(true);
+ x.setPosition(XAxis.XAxisPosition.BOTTOM);
+
+ YAxis yAxisLeft = hrLineChart.getAxisLeft();
+ yAxisLeft.setEnabled(true);
+ YAxis yAxisRight = hrLineChart.getAxisRight();
+ yAxisRight.setDrawLabels(true);
+
+ YAxis[] yAxixArr = {yAxisLeft, yAxisRight};
+ for (YAxis y : yAxixArr) {
+ y.setAxisMaximum(HeartRateUtils.getInstance().getMaxHeartRate());
+ y.setAxisMinimum(HeartRateUtils.getInstance().getMinHeartRate());
+ y.setDrawGridLines(false);
+ y.setDrawTopYLabelEntry(true);
+ y.setTextColor(CHART_TEXT_COLOR);
+ }
+
+ refresh();
+ }
+
+ @Override
+ protected void setupLegend(Chart> chart) {
+ List legendEntries = new ArrayList<>(4);
+
+ if (TOTAL_DAYS == 1) {
+ LegendEntry hrEntry = new LegendEntry();
+ hrEntry.label = getTitle();
+ hrEntry.formColor = HEARTRATE_COLOR;
+ legendEntries.add(hrEntry);
+ } else {
+ LegendEntry hrMinEntry = new LegendEntry();
+ hrMinEntry.label = getString(R.string.hr_minimum);
+ hrMinEntry.formColor = HEARTRATE_MIN_COLOR;
+ legendEntries.add(hrMinEntry);
+ }
+
+ if (supportsHeartRateRestingMeasurement() && TOTAL_DAYS != 1) {
+ LegendEntry hrRestingEntry = new LegendEntry();
+ hrRestingEntry.label = getString(R.string.hr_resting);
+ hrRestingEntry.formColor = HEARTRATE_RESTING_COLOR;
+ legendEntries.add(hrRestingEntry);
+ }
+
+ if (GBApplication.getPrefs().getBoolean("charts_show_average", true)) {
+ LegendEntry hrAverageEntry = new LegendEntry();
+ hrAverageEntry.label = getString(R.string.hr_average);
+ hrAverageEntry.formColor = TOTAL_DAYS != 1 ? HEARTRATE_COLOR : Color.RED;
+ legendEntries.add(hrAverageEntry);
+ }
+
+ if (TOTAL_DAYS != 1) {
+ LegendEntry hrMaxEntry = new LegendEntry();
+ hrMaxEntry.label = getString(R.string.hr_maximum);
+ hrMaxEntry.formColor = HEARTRATE_MAX_COLOR;
+ legendEntries.add(hrMaxEntry);
+ }
+
+ chart.getLegend().setCustom(legendEntries);
+ chart.getLegend().setTextColor(LEGEND_TEXT_COLOR);
+ chart.getLegend().setWordWrapEnabled(true);
+ }
+
+ protected LineDataSet createHeartRateDataSet(final List values, int color) {
+ LineDataSet dataSet = new LineDataSet(values, "Heart Rate");
+ dataSet.setLineWidth(1.5f);
+ dataSet.setMode(LineDataSet.Mode.HORIZONTAL_BEZIER);
+ dataSet.setCubicIntensity(0.1f);
+ dataSet.setDrawCircles(false);
+ dataSet.setDrawValues(true);
+ dataSet.setAxisDependency(YAxis.AxisDependency.RIGHT);
+ dataSet.setColor(color);
+ dataSet.setValueTextColor(CHART_TEXT_COLOR);
+ return dataSet;
+ }
+
+ private Pair getStartAndEndTS() {
+ Calendar day = Calendar.getInstance();
+ day.setTime(getEndDate());
+ day.add(Calendar.DATE, 0);
+ day.set(Calendar.HOUR_OF_DAY, 0);
+ day.set(Calendar.MINUTE, 0);
+ day.set(Calendar.SECOND, 0);
+ day.add(Calendar.HOUR, 0);
+ int startTs = (int) (day.getTimeInMillis() / 1000) - SEC_PER_DAY * (TOTAL_DAYS - 1);
+ int endTs = startTs + SEC_PER_DAY * TOTAL_DAYS - 1;
+ return Pair.of(startTs, endTs);
+ }
+
+ private void setStatistics(int average, int minimum, int maximum, int resting) {
+ hrAverage.setText(average > 0 ? getString(R.string.bpm_value_unit, average) : "-");
+ hrMinimum.setText(minimum > 0 ? getString(R.string.bpm_value_unit, minimum) : "-");
+ hrMaximum.setText(maximum > 0 ? getString(R.string.bpm_value_unit, maximum) : "-");
+ hrResting.setText(resting > 0 ? getString(R.string.bpm_value_unit, resting) : "-");
+
+ if (minimum > 0) {
+ hrLineChart.getAxisLeft().setAxisMinimum(Math.max(minimum - 30, 0));
+ hrLineChart.getAxisRight().setAxisMinimum(Math.max(minimum - 30, 0));
+ }
+ if (maximum > 0) {
+ hrLineChart.getAxisLeft().setAxisMaximum(maximum + 30);
+ hrLineChart.getAxisRight().setAxisMaximum(maximum + 30);
+ }
+ }
+
+ @Override
+ protected void updateChartsnUIThread(HeartRatePeriodData data) {
+ Pair startAndEndTs = getStartAndEndTS();
+ final int startTs = startAndEndTs.getKey();
+ final int endTs = startAndEndTs.getValue();
+
+ Date date = new Date((long) endTs * 1000);
+ mDateView.setText(DateTimeUtils.formatDaysUntil(TOTAL_DAYS, getTSEnd()));
+ final XAxis x = hrLineChart.getXAxis();
+ if (TOTAL_DAYS == 1) {
+ setOneDayData(data.samples.get(0), endTs);
+ x.setAxisMinimum(0f);
+ x.setAxisMaximum(86400f);
+ } else {
+ setMultipleDaysData(data, startTs, endTs);
+ x.setAxisMinimum(0);
+ // If the timestamp is used as XAxis, the chart library formats
+ // the labels not at 0:00, which causes a shift in the labels
+ x.setAxisMaximum(TOTAL_DAYS - 1);
+ }
+ }
+
+ private void setOneDayData(HeartRateData data, int endTs) {
+ Date date = new Date((long) endTs * 1000);
+ String formattedDate = new SimpleDateFormat("E, MMM dd").format(date);
+ mDateView.setText(formattedDate);
+
+ HeartRateUtils heartRateUtilsInstance = HeartRateUtils.getInstance();
+ final List lineEntries = new ArrayList<>();
+ List extends ActivitySample> samples = data.samples;
+ final TimestampTranslation tsTranslation = new TimestampTranslation();
+
+ final List lineDataSets = new ArrayList<>();
+ int lastTs = 0;
+ for (int i = 0; i < samples.size(); i++) {
+ final ActivitySample sample = samples.get(i);
+ if (!heartRateUtilsInstance.isValidHeartRateValue(sample.getHeartRate())) {
+ continue;
+ }
+ final int ts = sample.getTimestamp();
+ if (lastTs == 0 || (ts - lastTs) <= 60 * HeartRateUtils.MAX_HR_MEASUREMENTS_GAP_MINUTES) {
+ lineEntries.add(new Entry(tsTranslation.shorten(ts), sample.getHeartRate()));
+ } else {
+ if (!lineEntries.isEmpty()) {
+ List clone = new ArrayList<>(lineEntries.size());
+ clone.addAll(lineEntries);
+ lineDataSets.add(createHeartRateDataSet(clone, HEARTRATE_COLOR));
+ lineEntries.clear();
+ }
+ lineEntries.add(new Entry(ts, sample.getHeartRate()));
+ }
+ lastTs = ts;
+ }
+ hrLineChart.getXAxis().setValueFormatter(new SampleXLabelFormatter(tsTranslation, "HH:mm"));
+ if (!lineEntries.isEmpty()) {
+ lineDataSets.add(createHeartRateDataSet(lineEntries, HEARTRATE_COLOR));
+ }
+
+ setStatistics(data.average, data.minimum, data.maximum, data.restingHeartRate);
+
+ hrLineChart.setData(new LineData(lineDataSets));
+ hrLineChart.getAxisLeft().removeAllLimitLines();
+
+ if (data.average > 0 && GBApplication.getPrefs().getBoolean("charts_show_average", true)) {
+ final LimitLine averageLine = new LimitLine(data.average);
+ averageLine.setLineWidth(1.5f);
+ averageLine.enableDashedLine(15f, 10f, 0f);
+ averageLine.setLineColor(Color.RED);
+ hrLineChart.getAxisLeft().addLimitLine(averageLine);
+ }
+
+ //if (data.restingHeartRate > 0) {
+ // final LimitLine restingLine = new LimitLine(data.restingHeartRate);
+ // restingLine.setLineWidth(1.5f);
+ // restingLine.enableDashedLine(15f, 10f, 0f);
+ // restingLine.setLineColor(HEARTRATE_RESTING_COLOR);
+ // hrLineChart.getAxisLeft().addLimitLine(restingLine);
+ //}
+ }
+
+ private void setMultipleDaysData(HeartRatePeriodData data, int startTs, int endTs) {
+ List samples = data.samples;
+ final Accumulator avgAccumulator = new Accumulator();
+ final Accumulator minAccumulator = new Accumulator();
+ final Accumulator maxAccumulator = new Accumulator();
+ final Accumulator restingAccumulator = new Accumulator();
+
+ final ArrayList avgLineData = new ArrayList<>();
+ final ArrayList minLineData = new ArrayList<>();
+ final ArrayList maxLineData = new ArrayList<>();
+ final ArrayList restingLineData = new ArrayList<>();
+
+ for (int i = 0; i < samples.size(); i++) {
+ final HeartRateData hrData = samples.get(i);
+ if (hrData.average > 0) {
+ avgAccumulator.add(hrData.average);
+ avgLineData.add(new Entry(i, hrData.average));
+ }
+ if (hrData.minimum > 0) {
+ minAccumulator.add(hrData.minimum);
+ minLineData.add(new Entry(i, hrData.minimum));
+ }
+ if (hrData.maximum > 0) {
+ maxAccumulator.add(hrData.maximum);
+ maxLineData.add(new Entry(i, hrData.maximum));
+ }
+ if (hrData.restingHeartRate > 0) {
+ restingAccumulator.add(hrData.restingHeartRate);
+ restingLineData.add(new Entry(i, hrData.restingHeartRate));
+ }
+ }
+
+ final String fmt = TOTAL_DAYS == 7 ? "EEE" : "dd";
+ SimpleDateFormat formatDay = new SimpleDateFormat(fmt, Locale.getDefault());
+ ValueFormatter formatter = new ValueFormatter() {
+ @Override
+ public String getFormattedValue(float value) {
+ int ts = startTs + SEC_PER_DAY * (int)value;
+ return formatDay.format(new Date(ts * 1000L));
+ }
+ };
+ hrLineChart.getXAxis().setValueFormatter(formatter);
+
+ final int average = avgAccumulator.getCount() > 0 ? (int) Math.round(avgAccumulator.getAverage()) : DATA_INVALID;
+ final int minimum = minAccumulator.getCount() > 0 ? (int) Math.round(minAccumulator.getMin()) : DATA_INVALID;
+ final int maximum = maxAccumulator.getCount() > 0 ? (int) Math.round(maxAccumulator.getMax()) : DATA_INVALID;
+ final int restingAvg = restingAccumulator.getCount() > 0 ? (int) Math.round(restingAccumulator.getAverage()) : DATA_INVALID;
+ setStatistics(average, minimum, maximum, restingAvg);
+
+ List dataSets = new ArrayList();
+ if (GBApplication.getPrefs().getBoolean("charts_show_average", true)) {
+ dataSets.add(createHeartRateDataSet(avgLineData, HEARTRATE_COLOR));
+ }
+ dataSets.add(createHeartRateDataSet(minLineData, HEARTRATE_MIN_COLOR));
+ dataSets.add(createHeartRateDataSet(maxLineData, HEARTRATE_MAX_COLOR));
+ dataSets.add(createHeartRateDataSet(restingLineData, HEARTRATE_RESTING_COLOR));
+
+ hrLineChart.setData(new LineData(dataSets));
+ }
+
+ protected static class HeartRatePeriodData extends ChartsData {
+ public List samples;
+
+ protected HeartRatePeriodData(List samples) {
+ this.samples = samples;
+ }
+ }
+
+ protected static class HeartRateData extends ChartsData {
+ public List extends ActivitySample> samples;
+ public int restingHeartRate;
+ public int average;
+ public int minimum;
+ public int maximum;
+
+ protected HeartRateData(List extends ActivitySample> samples, int restingHeartRate, int average, int minimum, int maximum) {
+ this.samples = samples;
+ this.restingHeartRate = restingHeartRate;
+ this.average = average;
+ this.minimum = minimum;
+ this.maximum = maximum;
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/HeartRateFragmentAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/HeartRateFragmentAdapter.java
new file mode 100644
index 000000000..8882a5484
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/HeartRateFragmentAdapter.java
@@ -0,0 +1,31 @@
+package nodomain.freeyourgadget.gadgetbridge.adapter;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+
+
+import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
+import nodomain.freeyourgadget.gadgetbridge.activities.charts.DaySleepChartFragment;
+import nodomain.freeyourgadget.gadgetbridge.activities.charts.WeekSleepChartFragment;
+import nodomain.freeyourgadget.gadgetbridge.activities.charts.HeartRatePeriodFragment;
+
+public class HeartRateFragmentAdapter extends NestedFragmentAdapter {
+ public HeartRateFragmentAdapter(AbstractGBFragment fragment, FragmentManager childFragmentManager) {
+ super(fragment, childFragmentManager);
+ }
+
+ @NonNull
+ @Override
+ public Fragment createFragment(int position) {
+ switch (position) {
+ case 0:
+ return HeartRatePeriodFragment.newInstance(1);
+ case 1:
+ return HeartRatePeriodFragment.newInstance(7);
+ case 2:
+ return HeartRatePeriodFragment.newInstance(30);
+ }
+ return new HeartRatePeriodFragment();
+ }
+}
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 9d630812b..eb0289d09 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -21,6 +21,9 @@
#ffab40
#8B0000
#fadab1
+ #3F84E5
+ #01796F
+ #F42C04
#0054a3
#0054a3