mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2025-01-11 18:35:49 +01:00
Respiratory rate: init
This commit is contained in:
parent
7edb1f5941
commit
9ae56ae88c
@ -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();
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
116
app/src/main/res/layout/fragment_respiratory_rate.xml
Normal file
116
app/src/main/res/layout/fragment_respiratory_rate.xml
Normal 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>
|
80
app/src/main/res/layout/fragment_respiratory_rate_period.xml
Normal file
80
app/src/main/res/layout/fragment_respiratory_rate_period.xml
Normal 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>
|
@ -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>
|
||||
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user