diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java index 9c67cfa73..e137acc90 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java @@ -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 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 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(); } 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 bea30c78d..85c9f899a 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 @@ -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); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/RespiratoryRateCollectionFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/RespiratoryRateCollectionFragment.java new file mode 100644 index 000000000..1097a3c38 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/RespiratoryRateCollectionFragment.java @@ -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()); + } +} + diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/RespiratoryRateDailyFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/RespiratoryRateDailyFragment.java new file mode 100644 index 000000000..22f418ac6 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/RespiratoryRateDailyFragment.java @@ -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 { + 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 stepsDayList = getMyRespiratoryRateDaysData(db, day, device); + final RespiratoryRateDay RespiratoryRateDay; + if (stepsDayList.isEmpty()) { + LOG.error("Failed to get RespiratoryRateDay for {}", day); + List 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 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 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 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); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/RespiratoryRateFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/RespiratoryRateFragment.java new file mode 100644 index 000000000..e4bddd87b --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/RespiratoryRateFragment.java @@ -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 extends AbstractChartFragment { + 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 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 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 activitySamples = getAllActivitySamples(db, device, startTs, endTs); + SleepAnalysis sleepAnalysis = new SleepAnalysis(); + List sleepSessions = sleepAnalysis.calculateSleepSessions(activitySamples); + List 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 getSamplesOfDay(DBHandler db, GBDevice device, int startTs, int endTs) { + return getRespiratoryRateSamples(db, device, startTs, endTs); + } + + protected List getRespiratoryRateSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) { + TimeSampleProvider provider = device.getDeviceCoordinator().getRespiratoryRateSampleProvider(device, db.getDaoSession()); + return (List) provider.getAllSamples(tsFrom * 1000L, tsTo * 1000L); + } + + protected List getAllActivitySamples(DBHandler db, GBDevice device, int startTs, int endTs) { + SampleProvider 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 respiratoryRateSamples; + List sleepSessions; + + protected RespiratoryRateDay(Calendar day, List respiratoryRateSamples, List 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; + } + } + + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/RespiratoryRatePeriodFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/RespiratoryRatePeriodFragment.java new file mode 100644 index 000000000..e8d69553c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/RespiratoryRatePeriodFragment.java @@ -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 { + 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 respiratoryRateDaysData = getMyRespiratoryRateDaysData(db, day, device); + return new RespiratoryRateData(respiratoryRateDaysData); + } + + protected LineDataSet createDataSet(final List 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 lineAwakeRateAvgEntries = new ArrayList<>(); + List 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 lineDataSets = new ArrayList<>(); + lineDataSets.add(awakeDataSet); + lineDataSets.add(sleepDataSet); + + List 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 days; + int awakeRateAvg; + int sleepRateAvg; + + protected RespiratoryRateData(List 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); + } + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/RespiratoryRateFragmentAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/RespiratoryRateFragmentAdapter.java new file mode 100644 index 000000000..5394197fa --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/RespiratoryRateFragmentAdapter.java @@ -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(); + } +} diff --git a/app/src/main/res/layout/fragment_respiratory_rate.xml b/app/src/main/res/layout/fragment_respiratory_rate.xml new file mode 100644 index 000000000..3b765765e --- /dev/null +++ b/app/src/main/res/layout/fragment_respiratory_rate.xml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_respiratory_rate_period.xml b/app/src/main/res/layout/fragment_respiratory_rate_period.xml new file mode 100644 index 000000000..5e1505c1d --- /dev/null +++ b/app/src/main/res/layout/fragment_respiratory_rate_period.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 96fca17fd..dbc15a901 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -3133,6 +3133,7 @@ @string/p_temperature @string/p_weight @string/p_calories + @string/p_respiratory_rate @@ -3153,6 +3154,7 @@ @string/p_temperature @string/p_weight @string/p_calories + @string/p_respiratory_rate diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index cfe69639d..5e22dfab8 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -63,6 +63,7 @@ #00c9bf #fa4502 #1f49f2 + #163ede #858585 #ffe2e2e5 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 633389a40..c37131ffa 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -928,6 +928,7 @@ Current MTU of %1$d is too low, please enable high MTU in the device settings and disconnect/re-connect the device. Steps Calories + Respiratory Rate Active calories Distance Clock @@ -1018,6 +1019,10 @@ Light AVG REM AVG Awake AVG + Awake AVG + Sleep AVG + Lowest + Highest - --:-- %1s, %1s @@ -2186,6 +2191,7 @@ Maximum Minimum Average + Respiratory Rate Steps Steps AVG Steps Total diff --git a/app/src/main/res/values/values.xml b/app/src/main/res/values/values.xml index 07c5668cd..6e0e57872 100644 --- a/app/src/main/res/values/values.xml +++ b/app/src/main/res/values/values.xml @@ -113,6 +113,7 @@ heartrate livestats spo2 + respiratoryrate temperature weight