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 getActivitySamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) { - SampleProvider 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 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 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 samples; - public int restingHeartRate; - - protected HeartRateData(List 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 getActivitySamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) { + SampleProvider 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 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 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 samples; + public int restingHeartRate; + public int average; + public int minimum; + public int maximum; + + protected HeartRateData(List 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