1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2025-01-11 18:35:49 +01:00

Respiratory rate: init

This commit is contained in:
a0z 2024-11-24 22:20:37 +01:00 committed by José Rebelo
parent 7edb1f5941
commit 9ae56ae88c
13 changed files with 866 additions and 1 deletions

View File

@ -127,7 +127,7 @@ public class GBApplication extends Application {
private static SharedPreferences sharedPrefs;
private static final String PREFS_VERSION = "shared_preferences_version";
//if preferences have to be migrated, increment the following and add the migration logic in migratePrefs below; see http://stackoverflow.com/questions/16397848/how-can-i-migrate-android-preferences-with-a-new-version
private static final int CURRENT_PREFS_VERSION = 44;
private static final int CURRENT_PREFS_VERSION = 45;
private static final LimitedQueue<Integer, String> mIDSenderLookup = new LimitedQueue<>(16);
private static GBPrefs prefs;
@ -1886,6 +1886,36 @@ public class GBApplication extends Application {
}
}
if (oldVersion < 45) {
// Add the new respiratory rate tab to all devices.
try (DBHandler db = acquireDB()) {
final DaoSession daoSession = db.getDaoSession();
final List<Device> activeDevices = DBHelper.getActiveDevices(daoSession);
for (final Device dbDevice : activeDevices) {
final SharedPreferences deviceSharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(dbDevice.getIdentifier());
final String chartsTabsValue = deviceSharedPrefs.getString("charts_tabs", null);
if (chartsTabsValue == null) {
continue;
}
final String newPrefValue;
if (!StringUtils.isBlank(chartsTabsValue)) {
newPrefValue = chartsTabsValue + ",respiratoryrate";
} else {
newPrefValue = "respiratoryrate";
}
final SharedPreferences.Editor deviceSharedPrefsEdit = deviceSharedPrefs.edit();
deviceSharedPrefsEdit.putString("charts_tabs", newPrefValue);
deviceSharedPrefsEdit.apply();
}
} catch (Exception e) {
Log.e(TAG, "Failed to migrate prefs to version 45", e);
}
}
editor.putString(PREFS_VERSION, Integer.toString(CURRENT_PREFS_VERSION));
editor.apply();
}

View File

@ -195,6 +195,8 @@ public class ActivityChartsActivity extends AbstractChartsActivity {
Intent intent = getIntent();
String mode = intent.getStringExtra(ActivityChartsActivity.EXTRA_MODE);
return CaloriesDailyFragment.newInstance(mode);
case "respiratoryrate":
return RespiratoryRateCollectionFragment.newInstance(enabledTabsList.size() == 1);
}
return new UnknownFragment();
@ -242,6 +244,8 @@ public class ActivityChartsActivity extends AbstractChartsActivity {
return getString(R.string.menuitem_weight);
case "calories":
return getString(R.string.calories);
case "respiratoryrate":
return getString(R.string.respiratoryRate);
}
return String.format(Locale.getDefault(), "Unknown %d", position);

View File

@ -0,0 +1,29 @@
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.RespiratoryRateFragmentAdapter;
public class RespiratoryRateCollectionFragment extends AbstractCollectionFragment {
public RespiratoryRateCollectionFragment() {
}
public static RespiratoryRateCollectionFragment newInstance(final boolean allowSwipe) {
final RespiratoryRateCollectionFragment fragment = new RespiratoryRateCollectionFragment();
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 RespiratoryRateFragmentAdapter(this, getChildFragmentManager());
}
}

View File

@ -0,0 +1,175 @@
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
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.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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractRespiratoryRateSample;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class RespiratoryRateDailyFragment extends RespiratoryRateFragment<RespiratoryRateFragment.RespiratoryRateDay> {
protected static final Logger LOG = LoggerFactory.getLogger(BodyEnergyFragment.class);
private TextView mDateView;
private TextView sleepAvg;
private TextView awakeAvg;
private TextView lowest;
private TextView highest;
private LineChart respiratoryRateChart;
@Override
public void onResume() {
super.onResume();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_respiratory_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.rr_date_view);
awakeAvg = rootView.findViewById(R.id.awake_avg);
sleepAvg = rootView.findViewById(R.id.sleep_avg);
lowest = rootView.findViewById(R.id.day_lowest);
highest = rootView.findViewById(R.id.day_highest);
respiratoryRateChart = rootView.findViewById(R.id.respiratory_rate_line_chart);
setupRespiratoryRateChart();
refresh();
return rootView;
}
@Override
public String getTitle() {
return getString(R.string.respiratoryRate);
}
@Override
protected RespiratoryRateDay refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
Calendar day = Calendar.getInstance();
day.setTime(chartsHost.getEndDate());
String formattedDate = new SimpleDateFormat("E, MMM dd").format(chartsHost.getEndDate());
mDateView.setText(formattedDate);
List<RespiratoryRateDay> stepsDayList = getMyRespiratoryRateDaysData(db, day, device);
final RespiratoryRateDay RespiratoryRateDay;
if (stepsDayList.isEmpty()) {
LOG.error("Failed to get RespiratoryRateDay for {}", day);
List<? extends AbstractRespiratoryRateSample> s = new ArrayList<>();
RespiratoryRateDay = new RespiratoryRateDay(day, new ArrayList<>(), new ArrayList<>());
} else {
RespiratoryRateDay = stepsDayList.get(0);
}
return RespiratoryRateDay;
}
@Override
protected void updateChartsnUIThread(RespiratoryRateFragment.RespiratoryRateDay respiratoryRateDay) {
awakeAvg.setText(String.format(String.valueOf(respiratoryRateDay.awakeRateAvg)));
sleepAvg.setText(String.valueOf(respiratoryRateDay.sleepRateAvg));
lowest.setText(String.valueOf(respiratoryRateDay.rateLowest));
highest.setText(String.valueOf(respiratoryRateDay.rateHighest));
// Chart
final List<LegendEntry> legendEntries = new ArrayList<>(1);
final LegendEntry respiratoryRateEntry = new LegendEntry();
respiratoryRateEntry.label = getString(R.string.respiratoryRate);
respiratoryRateEntry.formColor = getResources().getColor(R.color.respiratory_rate_color);
legendEntries.add(respiratoryRateEntry);
respiratoryRateChart.getLegend().setTextColor(TEXT_COLOR);
respiratoryRateChart.getLegend().setCustom(legendEntries);
final List<Entry> lineEntries = new ArrayList<>();
final TimestampTranslation tsTranslation = new TimestampTranslation();
for (final AbstractRespiratoryRateSample sample : respiratoryRateDay.respiratoryRateSamples) {
int ts = (int) (sample.getTimestamp() / 1000L);
lineEntries.add(new Entry(tsTranslation.shorten(ts), (int) sample.getRespiratoryRate()));
}
respiratoryRateChart.getXAxis().setValueFormatter(new SampleXLabelFormatter(tsTranslation, "HH:mm"));
if (respiratoryRateDay.rateLowest > 0 && respiratoryRateDay.rateHighest > 0) {
final YAxis yAxisLeft = respiratoryRateChart.getAxisLeft();
yAxisLeft.setAxisMaximum(Math.max(respiratoryRateDay.rateHighest + 3, 20));
}
final LineDataSet lineDataSet = new LineDataSet(lineEntries, getString(R.string.respiratoryRate));
lineDataSet.setColor(getResources().getColor(R.color.respiratory_rate_color));
lineDataSet.setDrawCircles(false);
lineDataSet.setLineWidth(2f);
lineDataSet.setFillAlpha(255);
lineDataSet.setDrawCircles(false);
lineDataSet.setCircleColor(getResources().getColor(R.color.respiratory_rate_color));
lineDataSet.setAxisDependency(YAxis.AxisDependency.LEFT);
lineDataSet.setDrawValues(false);
lineDataSet.setMode(LineDataSet.Mode.CUBIC_BEZIER);
final List<ILineDataSet> lineDataSets = new ArrayList<>();
lineDataSets.add(lineDataSet);
final LineData lineData = new LineData(lineDataSets);
respiratoryRateChart.setData(lineData);
}
@Override
protected void renderCharts() {
respiratoryRateChart.invalidate();
}
protected void setupLegend(Chart<?> chart) {}
private void setupRespiratoryRateChart() {
respiratoryRateChart.getDescription().setEnabled(false);
respiratoryRateChart.setDoubleTapToZoomEnabled(false);
final XAxis xAxisBottom = respiratoryRateChart.getXAxis();
xAxisBottom.setPosition(XAxis.XAxisPosition.BOTTOM);
xAxisBottom.setDrawLabels(true);
xAxisBottom.setDrawGridLines(false);
xAxisBottom.setEnabled(true);
xAxisBottom.setDrawLimitLinesBehindData(true);
xAxisBottom.setTextColor(CHART_TEXT_COLOR);
xAxisBottom.setAxisMinimum(0f);
xAxisBottom.setAxisMaximum(86400f);
xAxisBottom.setLabelCount(7, true);
final YAxis yAxisLeft = respiratoryRateChart.getAxisLeft();
yAxisLeft.setDrawGridLines(true);
yAxisLeft.setAxisMinimum(0);
yAxisLeft.setAxisMaximum(20);
yAxisLeft.setDrawTopYLabelEntry(true);
yAxisLeft.setEnabled(true);
yAxisLeft.setTextColor(CHART_TEXT_COLOR);
final YAxis yAxisRight = respiratoryRateChart.getAxisRight();
yAxisRight.setEnabled(true);
yAxisRight.setDrawLabels(false);
yAxisRight.setDrawGridLines(false);
yAxisRight.setDrawAxisLine(true);
}
}

View File

@ -0,0 +1,146 @@
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractRespiratoryRateSample;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.RespiratoryRateSample;
abstract class RespiratoryRateFragment<T extends ChartsData> extends AbstractChartFragment<T> {
protected static final Logger LOG = LoggerFactory.getLogger(StepsDailyFragment.class);
protected int CHART_TEXT_COLOR;
protected int TEXT_COLOR;
protected int LEGEND_TEXT_COLOR;
protected int BACKGROUND_COLOR;
protected int DESCRIPTION_COLOR;
protected int TOTAL_DAYS = 1;
@Override
public String getTitle() {
return getString(R.string.respiratoryRate);
}
@Override
protected void init() {
TEXT_COLOR = GBApplication.getTextColor(requireContext());
CHART_TEXT_COLOR = GBApplication.getSecondaryTextColor(requireContext());
BACKGROUND_COLOR = GBApplication.getBackgroundColor(getContext());
LEGEND_TEXT_COLOR = DESCRIPTION_COLOR = GBApplication.getTextColor(getContext());
CHART_TEXT_COLOR = GBApplication.getSecondaryTextColor(getContext());
}
protected List<RespiratoryRateFragment.RespiratoryRateDay> getMyRespiratoryRateDaysData(DBHandler db, Calendar day, GBDevice device) {
day = (Calendar) day.clone(); // do not modify the caller's argument
day.add(Calendar.DATE, -TOTAL_DAYS + 1);
List<RespiratoryRateDay> daysData = new ArrayList<>();;
for (int counter = 0; counter < TOTAL_DAYS; counter++) {
int startTs;
int endTs;
day = (Calendar) day.clone(); // do not modify the caller's argument
day.set(Calendar.HOUR_OF_DAY, 0);
day.set(Calendar.MINUTE, 0);
day.set(Calendar.SECOND, 0);
day.add(Calendar.HOUR, 0);
startTs = (int) (day.getTimeInMillis() / 1000);
endTs = startTs + 24 * 60 * 60 - 1;
List<? extends ActivitySample> activitySamples = getAllActivitySamples(db, device, startTs, endTs);
SleepAnalysis sleepAnalysis = new SleepAnalysis();
List<SleepAnalysis.SleepSession> sleepSessions = sleepAnalysis.calculateSleepSessions(activitySamples);
List<? extends AbstractRespiratoryRateSample> samples = getRespiratoryRateSamples(db, device, startTs, endTs);
Calendar d = (Calendar) day.clone();
daysData.add(new RespiratoryRateDay(d, samples, sleepSessions));
day.add(Calendar.DATE, 1);
}
return daysData;
}
protected List<? extends AbstractRespiratoryRateSample> getSamplesOfDay(DBHandler db, GBDevice device, int startTs, int endTs) {
return getRespiratoryRateSamples(db, device, startTs, endTs);
}
protected List<AbstractRespiratoryRateSample> getRespiratoryRateSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
TimeSampleProvider<? extends RespiratoryRateSample> provider = device.getDeviceCoordinator().getRespiratoryRateSampleProvider(device, db.getDaoSession());
return (List<AbstractRespiratoryRateSample>) provider.getAllSamples(tsFrom * 1000L, tsTo * 1000L);
}
protected List<? extends ActivitySample> getAllActivitySamples(DBHandler db, GBDevice device, int startTs, int endTs) {
SampleProvider<? extends ActivitySample> provider = device.getDeviceCoordinator().getSampleProvider(device, db.getDaoSession());
return provider.getAllActivitySamples(startTs, endTs);
}
protected static class RespiratoryRateDay extends ChartsData {
public int awakeRateAvg;
public int sleepRateAvg;
public int rateLowest;
public int rateHighest;
public Calendar day;
List<? extends AbstractRespiratoryRateSample> respiratoryRateSamples;
List<SleepAnalysis.SleepSession> sleepSessions;
protected RespiratoryRateDay(Calendar day, List<? extends AbstractRespiratoryRateSample> respiratoryRateSamples, List<SleepAnalysis.SleepSession> sleepSessions) {
this.day = day;
this.respiratoryRateSamples = respiratoryRateSamples;
this.sleepSessions = sleepSessions;
float awakeRateTotal = 0;
int awakeCounter = 0;
float sleepRateTotal = 0;
int sleepCounter = 0;
float lowest = 0;
float highest = 0;
if (!this.respiratoryRateSamples.isEmpty()) {
for (AbstractRespiratoryRateSample sample : this.respiratoryRateSamples) {
if (isSleepSample(sample)) {
sleepRateTotal += sample.getRespiratoryRate();
sleepCounter++;
} else {
awakeRateTotal += sample.getRespiratoryRate();
awakeCounter++;
}
if (sample.getRespiratoryRate() > highest) {
highest = sample.getRespiratoryRate();
}
if (sample.getRespiratoryRate() < lowest || lowest == 0) {
lowest = sample.getRespiratoryRate();
}
}
}
if (awakeRateTotal > 0) {
this.awakeRateAvg = Math.round(awakeRateTotal / awakeCounter);
}
if (sleepRateTotal > 0) {
this.sleepRateAvg = Math.round(sleepRateTotal / sleepCounter);
}
this.rateLowest = (int) lowest;
this.rateHighest = (int) highest;
}
private boolean isSleepSample(AbstractRespiratoryRateSample sample) {
if (this.sleepSessions.isEmpty()) {
return true;
}
for (SleepAnalysis.SleepSession session : this.sleepSessions) {
if (sample.getTimestamp() >= session.getSleepStart().getTime() && sample.getTimestamp() <= session.getSleepEnd().getTime()) {
return true;
}
}
return false;
}
}
}

View File

@ -0,0 +1,243 @@
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.TextView;
import com.github.mikephil.charting.charts.BarChart;
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.BarData;
import com.github.mikephil.charting.data.BarDataSet;
import com.github.mikephil.charting.data.BarEntry;
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.apache.commons.lang3.time.DateUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class RespiratoryRatePeriodFragment extends RespiratoryRateFragment<RespiratoryRatePeriodFragment.RespiratoryRateData> {
protected static final Logger LOG = LoggerFactory.getLogger(BodyEnergyFragment.class);
private TextView mDateView;
private TextView sleepAvg;
private TextView awakeAvg;
private LineChart respiratoryRateChart;
public static RespiratoryRatePeriodFragment newInstance (int totalDays) {
RespiratoryRatePeriodFragment fragmentFirst = new RespiratoryRatePeriodFragment();
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_respiratory_rate_period, 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.rr_date_view);
sleepAvg = rootView.findViewById(R.id.sleep_avg);
awakeAvg = rootView.findViewById(R.id.awake_avg);
respiratoryRateChart = rootView.findViewById(R.id.respiratory_rate_line_chart);
setupRespiratoryRateChart();
refresh();
return rootView;
}
protected void setupRespiratoryRateChart() {
respiratoryRateChart.getDescription().setEnabled(false);
if (TOTAL_DAYS <= 7) {
respiratoryRateChart.setTouchEnabled(false);
respiratoryRateChart.setPinchZoom(false);
}
respiratoryRateChart.getDescription().setEnabled(false);
respiratoryRateChart.setDoubleTapToZoomEnabled(false);
final XAxis xAxisBottom = respiratoryRateChart.getXAxis();
xAxisBottom.setPosition(XAxis.XAxisPosition.BOTTOM);
xAxisBottom.setDrawLabels(true);
xAxisBottom.setDrawGridLines(false);
xAxisBottom.setEnabled(true);
xAxisBottom.setDrawLimitLinesBehindData(true);
xAxisBottom.setTextColor(CHART_TEXT_COLOR);
xAxisBottom.setLabelCount(7, true);
xAxisBottom.setAxisMinimum(0);
xAxisBottom.setAxisMaximum(TOTAL_DAYS - 1);
final YAxis yAxisLeft = respiratoryRateChart.getAxisLeft();
yAxisLeft.setDrawGridLines(true);
yAxisLeft.setAxisMinimum(0);
yAxisLeft.setAxisMaximum(20);
yAxisLeft.setDrawTopYLabelEntry(true);
yAxisLeft.setEnabled(true);
yAxisLeft.setTextColor(CHART_TEXT_COLOR);
final YAxis yAxisRight = respiratoryRateChart.getAxisRight();
yAxisRight.setEnabled(true);
yAxisRight.setDrawLabels(false);
yAxisRight.setDrawGridLines(false);
yAxisRight.setDrawAxisLine(true);
}
@Override
public String getTitle() {
return getString(R.string.respiratoryRate);
}
@Override
protected RespiratoryRateData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
Calendar day = Calendar.getInstance();
Date to = new Date((long) this.getTSEnd() * 1000);
Date from = DateUtils.addDays(to,-(TOTAL_DAYS - 1));
String toFormattedDate = new SimpleDateFormat("E, MMM dd").format(to);
String fromFormattedDate = new SimpleDateFormat("E, MMM dd").format(from);
mDateView.setText(fromFormattedDate + " - " + toFormattedDate);
day.setTime(to);
List<RespiratoryRateDay> respiratoryRateDaysData = getMyRespiratoryRateDaysData(db, day, device);
return new RespiratoryRateData(respiratoryRateDaysData);
}
protected LineDataSet createDataSet(final List<Entry> values, String label, int color) {
final LineDataSet lineDataSet = new LineDataSet(values, label);
lineDataSet.setColor(getResources().getColor(color));
lineDataSet.setDrawCircles(false);
lineDataSet.setLineWidth(2f);
lineDataSet.setFillAlpha(255);
lineDataSet.setCircleRadius(5f);
lineDataSet.setDrawCircles(true);
lineDataSet.setDrawCircleHole(true);
lineDataSet.setCircleColor(getResources().getColor(color));
lineDataSet.setAxisDependency(YAxis.AxisDependency.LEFT);
lineDataSet.setDrawValues(false);
return lineDataSet;
}
@Override
protected void updateChartsnUIThread(RespiratoryRateData respiratoryRateData) {
respiratoryRateChart.setData(null);
sleepAvg.setText(String.valueOf(respiratoryRateData.sleepRateAvg));
awakeAvg.setText(String.valueOf(respiratoryRateData.awakeRateAvg));
List<Entry> lineAwakeRateAvgEntries = new ArrayList<>();
List<Entry> lineSleepRateEntries = new ArrayList<>();
for (int i = 0; i < TOTAL_DAYS; i++) {
RespiratoryRateDay day = respiratoryRateData.days.get(i);
if (day.awakeRateAvg > 0) {
lineAwakeRateAvgEntries.add(new Entry(i, day.awakeRateAvg));
}
if (day.sleepRateAvg > 0) {
lineSleepRateEntries.add(new Entry(i, day.sleepRateAvg));
}
}
LineDataSet awakeDataSet = createDataSet(lineAwakeRateAvgEntries, getString(R.string.awake_avg), R.color.respiratory_rate_color);
LineDataSet sleepDataSet = createDataSet(lineSleepRateEntries, getString(R.string.awake_avg), R.color.chart_light_sleep_light);
final List<ILineDataSet> lineDataSets = new ArrayList<>();
lineDataSets.add(awakeDataSet);
lineDataSets.add(sleepDataSet);
List<LegendEntry> legendEntries = new ArrayList<>(1);
LegendEntry awakeEntry = new LegendEntry();
awakeEntry.label = getString(R.string.awake_avg);
awakeEntry.formColor = getResources().getColor(R.color.respiratory_rate_color);
LegendEntry sleepEntry = new LegendEntry();
sleepEntry.label = getString(R.string.sleep_avg);
sleepEntry.formColor = getResources().getColor(R.color.chart_light_sleep_light);
legendEntries.add(awakeEntry);
legendEntries.add(sleepEntry);
respiratoryRateChart.getLegend().setTextColor(LEGEND_TEXT_COLOR);
respiratoryRateChart.getLegend().setCustom(legendEntries);
final LineData lineData = new LineData(lineDataSets);
respiratoryRateChart.setData(lineData);
final XAxis x = respiratoryRateChart.getXAxis();
x.setValueFormatter(getRespiratoryRateChartDayValueFormatter(respiratoryRateData));
}
ValueFormatter getRespiratoryRateChartDayValueFormatter(RespiratoryRateData RespiratoryRateData) {
return new ValueFormatter() {
@Override
public String getFormattedValue(float value) {
RespiratoryRateFragment.RespiratoryRateDay day = RespiratoryRateData.days.get((int) value);
String pattern = TOTAL_DAYS > 7 ? "dd" : "EEE";
SimpleDateFormat formatLetterDay = new SimpleDateFormat(pattern, Locale.getDefault());
return formatLetterDay.format(new Date(day.day.getTimeInMillis()));
}
};
}
@Override
protected void renderCharts() {
respiratoryRateChart.invalidate();
}
protected void setupLegend(Chart<?> chart) {}
protected static class RespiratoryRateData extends ChartsData {
List<RespiratoryRateDay> days;
int awakeRateAvg;
int sleepRateAvg;
protected RespiratoryRateData(List<RespiratoryRateDay> days) {
this.days = days;
int awakeTotal = 0;
int sleepTotal = 0;
int awakeCounter = 0;
int sleepCounter = 0;
for(RespiratoryRateDay day: days) {
if (day.awakeRateAvg > 0) {
awakeTotal += day.awakeRateAvg;
awakeCounter++;
}
if (day.sleepRateAvg > 0) {
sleepTotal += day.sleepRateAvg;
sleepCounter++;
}
}
if (awakeTotal > 0) {
this.awakeRateAvg = Math.round((float) awakeTotal / awakeCounter);
}
if (sleepTotal > 0) {
this.sleepRateAvg = Math.round((float) sleepTotal / sleepCounter);
}
}
}
}

View File

@ -0,0 +1,32 @@
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.RespiratoryRateDailyFragment;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.RespiratoryRatePeriodFragment;
public class RespiratoryRateFragmentAdapter extends NestedFragmentAdapter {
protected FragmentManager fragmentManager;
public RespiratoryRateFragmentAdapter(AbstractGBFragment fragment, FragmentManager childFragmentManager) {
super(fragment, childFragmentManager);
fragmentManager = childFragmentManager;
}
@NonNull
@Override
public Fragment createFragment(int position) {
switch (position) {
case 0:
return new RespiratoryRateDailyFragment();
case 1:
return RespiratoryRatePeriodFragment.newInstance(7);
case 2:
return RespiratoryRatePeriodFragment.newInstance(30);
}
return new RespiratoryRateDailyFragment();
}
}

View File

@ -0,0 +1,116 @@
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/rr_date_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:layout_marginBottom="20dp"
android:gravity="center"
android:textSize="20sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="300sp">
<com.github.mikephil.charting.charts.LineChart
android:id="@+id/respiratory_rate_line_chart"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="2" />
</LinearLayout>
<GridLayout
android:background="@color/gauge_line_color"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:columnCount="2"
android:layout_marginTop="15dp"
>
<LinearLayout
style="@style/GridTile"
android:layout_marginEnd="1dp"
android:layout_marginTop="2dp"
>
<TextView
android:id="@+id/awake_avg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/stats_empty_value"
android:textSize="20sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/awake_avg"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
style="@style/GridTile"
android:layout_marginStart="1dp"
android:layout_marginTop="2dp"
>
<TextView
android:id="@+id/sleep_avg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/stats_empty_value"
android:textSize="20sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/sleep_avg"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
style="@style/GridTile"
android:layout_marginStart="1dp"
android:layout_marginTop="2dp"
>
<TextView
android:id="@+id/day_lowest"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/stats_empty_value"
android:textSize="20sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/lowest"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
style="@style/GridTile"
android:layout_marginStart="1dp"
android:layout_marginTop="2dp"
>
<TextView
android:id="@+id/day_highest"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/stats_empty_value"
android:textSize="20sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/highest"
android:textSize="12sp" />
</LinearLayout>
</GridLayout>
</LinearLayout>
</ScrollView>

View File

@ -0,0 +1,80 @@
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/rr_date_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:layout_marginBottom="20dp"
android:gravity="center"
android:textSize="20sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="300sp">
<com.github.mikephil.charting.charts.LineChart
android:id="@+id/respiratory_rate_line_chart"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="2" />
</LinearLayout>
<GridLayout
android:background="@color/gauge_line_color"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:columnCount="2"
android:layout_marginTop="15dp"
>
<LinearLayout
style="@style/GridTile"
android:layout_marginEnd="1dp"
android:layout_marginTop="2dp"
>
<TextView
android:id="@+id/sleep_avg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/stats_empty_value"
android:textSize="20sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/sleep_avg"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
style="@style/GridTile"
android:layout_marginStart="1dp"
android:layout_marginTop="2dp"
>
<TextView
android:id="@+id/awake_avg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/stats_empty_value"
android:textSize="20sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/awake_avg"
android:textSize="12sp" />
</LinearLayout>
</GridLayout>
</LinearLayout>
</ScrollView>

View File

@ -3133,6 +3133,7 @@
<item>@string/p_temperature</item>
<item>@string/p_weight</item>
<item>@string/p_calories</item>
<item>@string/p_respiratory_rate</item>
</string-array>
<string-array name="pref_charts_tabs_items_default">
@ -3153,6 +3154,7 @@
<item>@string/p_temperature</item>
<item>@string/p_weight</item>
<item>@string/p_calories</item>
<item>@string/p_respiratory_rate</item>
</string-array>

View File

@ -63,6 +63,7 @@
<color name="steps_color" type="color">#00c9bf</color>
<color name="calories_color" type="color">#fa4502</color>
<color name="calories_resting_color" type="color">#1f49f2</color>
<color name="respiratory_rate_color" type="color">#163ede</color>
<color name="value_line_color" type="color">#858585</color>
<color name="row_separator_light" type="color">#ffe2e2e5</color>

View File

@ -928,6 +928,7 @@
<string name="updatefirmwareoperation_failed_low_mtu">Current MTU of %1$d is too low, please enable high MTU in the device settings and disconnect/re-connect the device.</string>
<string name="chart_steps">Steps</string>
<string name="calories">Calories</string>
<string name="respiratoryrate">Respiratory Rate</string>
<string name="active_calories">Active calories</string>
<string name="distance">Distance</string>
<string name="clock">Clock</string>
@ -1018,6 +1019,10 @@
<string name="sleep_colored_stats_light_avg">Light AVG</string>
<string name="sleep_colored_stats_rem_avg">REM AVG</string>
<string name="sleep_colored_stats_awake_avg">Awake AVG</string>
<string name="awake_avg">Awake AVG</string>
<string name="sleep_avg">Sleep AVG</string>
<string name="lowest">Lowest</string>
<string name="highest">Highest</string>
<string name="stats_empty_value">-</string>
<string name="time_empty_value" translatable="false">--:--</string>
<string name="date_placeholders__date__time">%1s, %1s</string>
@ -2186,6 +2191,7 @@
<string name="maxAltitude">Maximum</string>
<string name="minAltitude">Minimum</string>
<string name="averageAltitude">Average</string>
<string name="respiratoryRate">Respiratory Rate</string>
<string name="steps">Steps</string>
<string name="steps_avg">Steps AVG</string>
<string name="steps_total">Steps Total</string>

View File

@ -113,6 +113,7 @@
<item name="p_heartrate" type="string">heartrate</item>
<item name="p_live_stats" type="string">livestats</item>
<item name="p_spo2" type="string">spo2</item>
<item name="p_respiratory_rate" type="string">respiratoryrate</item>
<item name="p_temperature" type="string">temperature</item>
<item name="p_weight" type="string">weight</item>