diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java index a80e6e17b..86c3f2d43 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java @@ -101,6 +101,8 @@ import static nodomain.freeyourgadget.gadgetbridge.util.GB.NOTIFICATION_ID_ERROR import com.jakewharton.threetenabp.AndroidThreeTen; +import org.apache.commons.lang3.StringUtils; + /** * Main Application class that initializes and provides access to certain things like * logging and DB access. @@ -116,7 +118,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 = 19; + private static final int CURRENT_PREFS_VERSION = 20; private static LimitedQueue mIDSenderLookup = new LimitedQueue(16); private static Prefs prefs; @@ -1197,12 +1199,42 @@ public class GBApplication extends Application { } catch (Exception e) { Log.w(TAG, "error acquiring DB lock"); } - if (oldVersion < 19) { - //remove old ble scanning prefences, now unsupported - editor.remove("disable_new_ble_scanning"); - } + } + if (oldVersion < 19) { + //remove old ble scanning prefences, now unsupported + editor.remove("disable_new_ble_scanning"); + } + + if (oldVersion < 20) { + // Add the new stress 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 + ",stress"; + } else { + newPrefValue = "stress"; + } + + final SharedPreferences.Editor deviceSharedPrefsEdit = deviceSharedPrefs.edit(); + deviceSharedPrefsEdit.putString("charts_tabs", newPrefValue); + deviceSharedPrefsEdit.apply(); + } + } catch (Exception e) { + Log.w(TAG, "error acquiring DB lock"); } + } editor.putString(PREFS_VERSION, Integer.toString(CURRENT_PREFS_VERSION)); editor.apply(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java index 3d3398f3e..3c24d1377 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractChartFragment.java @@ -32,11 +32,11 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager; import com.github.mikephil.charting.charts.BarChart; import com.github.mikephil.charting.charts.BarLineChartBase; import com.github.mikephil.charting.charts.Chart; -import com.github.mikephil.charting.data.Entry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; import java.util.HashSet; @@ -71,6 +71,8 @@ public abstract class AbstractChartFragment extends Abstra protected final int ANIM_TIME = 250; private static final Logger LOG = LoggerFactory.getLogger(AbstractChartFragment.class); + @SuppressLint("SimpleDateFormat") + protected final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); private final Set mIntentFilterActions; private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @@ -307,6 +309,8 @@ public abstract class AbstractChartFragment extends Abstra * #renderCharts */ protected void refresh() { + LOG.info("Refreshing data for {} from {} to {}", getTitle(), sdf.format(getStartDate()), sdf.format(getEndDate())); + ChartsHost chartsHost = getChartsHost(); if (chartsHost != null) { if (chartsHost.getDevice() != null) { 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 3e3be81e1..1c8b25703 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 @@ -48,7 +48,7 @@ public class ActivityChartsActivity extends AbstractChartsActivity { @Override protected int getRecordedDataType() { - return RecordedDataTypes.TYPE_ACTIVITY; + return RecordedDataTypes.TYPE_ACTIVITY | RecordedDataTypes.TYPE_STRESS; } @Override @@ -83,6 +83,9 @@ public class ActivityChartsActivity extends AbstractChartsActivity { if (!coordinator.supportsRealtimeData()) { tabList.remove("livestats"); } + if (!coordinator.supportsStressMeasurement()) { + tabList.remove("stress"); + } return tabList; } @@ -112,6 +115,8 @@ public class ActivityChartsActivity extends AbstractChartsActivity { return new SleepChartFragment(); case "sleepweek": return new WeekSleepChartFragment(); + case "stress": + return new StressChartFragment(); case "stepsweek": return new WeekStepsChartFragment(); case "speedzones": @@ -154,6 +159,8 @@ public class ActivityChartsActivity extends AbstractChartsActivity { return getString(R.string.sleepchart_your_sleep); case "sleepweek": return getSleepTitle(); + case "stress": + return getString(R.string.menuitem_stress); case "stepsweek": return getStepsTitle(); case "speedzones": diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StressChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StressChartFragment.java new file mode 100644 index 000000000..d587e900a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StressChartFragment.java @@ -0,0 +1,477 @@ +/* Copyright (C) 2023 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.activities.charts; + +import android.content.Context; +import android.graphics.Color; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.core.content.ContextCompat; + +import com.github.mikephil.charting.animation.Easing; +import com.github.mikephil.charting.charts.Chart; +import com.github.mikephil.charting.charts.LineChart; +import com.github.mikephil.charting.charts.PieChart; +import com.github.mikephil.charting.components.LegendEntry; +import com.github.mikephil.charting.components.LimitLine; +import com.github.mikephil.charting.components.XAxis; +import com.github.mikephil.charting.components.YAxis; +import com.github.mikephil.charting.data.Entry; +import com.github.mikephil.charting.data.LineData; +import com.github.mikephil.charting.data.LineDataSet; +import com.github.mikephil.charting.data.PieData; +import com.github.mikephil.charting.data.PieDataSet; +import com.github.mikephil.charting.data.PieEntry; +import com.github.mikephil.charting.formatter.ValueFormatter; +import com.github.mikephil.charting.interfaces.datasets.ILineDataSet; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.StressSample; +import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; +import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; + +public class StressChartFragment extends AbstractChartFragment { + protected static final Logger LOG = LoggerFactory.getLogger(ActivitySleepChartFragment.class); + + private LineChart mStressChart; + private PieChart mStressLevelsPieChart; + + private int BACKGROUND_COLOR; + private int DESCRIPTION_COLOR; + private int CHART_TEXT_COLOR; + private int LEGEND_TEXT_COLOR; + + private String STRESS_AVERAGE_LABEL; + + private final Prefs prefs = GBApplication.getPrefs(); + + private final boolean CHARTS_SLEEP_RANGE_24H = prefs.getBoolean("chart_sleep_range_24h", false); + private final boolean SHOW_CHARTS_AVERAGE = prefs.getBoolean("charts_show_average", true); + + @Override + protected void init() { + BACKGROUND_COLOR = GBApplication.getBackgroundColor(requireContext()); + LEGEND_TEXT_COLOR = DESCRIPTION_COLOR = GBApplication.getTextColor(requireContext()); + CHART_TEXT_COLOR = ContextCompat.getColor(requireContext(), R.color.secondarytext); + + STRESS_AVERAGE_LABEL = requireContext().getString(R.string.charts_legend_stress_average); + } + + @Override + protected StressChartsData refreshInBackground(final ChartsHost chartsHost, final DBHandler db, final GBDevice device) { + final List samples = getSamples(db, device); + + LOG.info("Got {} stress samples", samples.size()); + + ensureStartAndEndSamples((List) samples); + + return new StressChartsDataBuilder(samples).build(); + } + + protected LineDataSet createDataSet(final StressType stressType, final List values) { + final LineDataSet lineDataSet = new LineDataSet(values, stressType.getLabel(requireContext())); + lineDataSet.setColor(stressType.getColor(requireContext())); + lineDataSet.setDrawFilled(true); + lineDataSet.setDrawCircles(false); + lineDataSet.setFillColor(stressType.getColor(requireContext())); + lineDataSet.setFillAlpha(255); + lineDataSet.setDrawValues(false); + lineDataSet.setValueTextColor(CHART_TEXT_COLOR); + lineDataSet.setAxisDependency(YAxis.AxisDependency.LEFT); + return lineDataSet; + } + + @Override + protected void updateChartsnUIThread(final StressChartsData stressData) { + final PieData pieData = stressData.getPieData(); + if (stressData.getAverage() > 0) { + mStressLevelsPieChart.setCenterText(requireContext().getString(R.string.average, String.valueOf(stressData.getAverage()))); + } else { + mStressLevelsPieChart.setCenterText(requireContext().getString(R.string.no_data)); + } + mStressLevelsPieChart.setData(pieData); + + final DefaultChartsData chartsData = stressData.getChartsData(); + mStressChart.setData(null); // workaround for https://github.com/PhilJay/MPAndroidChart/issues/2317 + mStressChart.getXAxis().setValueFormatter(chartsData.getXValueFormatter()); + mStressChart.setData(chartsData.getData()); + mStressChart.getAxisRight().removeAllLimitLines(); + + if (stressData.getAverage() > 0) { + final LimitLine averageLine = new LimitLine(stressData.getAverage()); + averageLine.setLineColor(Color.RED); + averageLine.setLineWidth(0.1f); + mStressChart.getAxisRight().addLimitLine(averageLine); + } + } + + @Override + public String getTitle() { + return getString(R.string.menuitem_stress); + } + + @Override + public View onCreateView(final LayoutInflater inflater, + final ViewGroup container, + final Bundle savedInstanceState) { + final View rootView = inflater.inflate(R.layout.fragment_stresschart, container, false); + + mStressChart = rootView.findViewById(R.id.stress_line_chart); + mStressLevelsPieChart = rootView.findViewById(R.id.stress_pie_chart); + + setupLineChart(); + setupPieChart(); + + // refresh immediately instead of use refreshIfVisible(), for perceived performance + refresh(); + + return rootView; + } + + private void setupPieChart() { + mStressLevelsPieChart.setBackgroundColor(BACKGROUND_COLOR); + mStressLevelsPieChart.getDescription().setTextColor(DESCRIPTION_COLOR); + mStressLevelsPieChart.setEntryLabelColor(DESCRIPTION_COLOR); + mStressLevelsPieChart.getDescription().setText(""); + mStressLevelsPieChart.setNoDataText(""); + mStressLevelsPieChart.getLegend().setEnabled(false); + } + + private void setupLineChart() { + mStressChart.setBackgroundColor(BACKGROUND_COLOR); + mStressChart.getDescription().setTextColor(DESCRIPTION_COLOR); + configureBarLineChartDefaults(mStressChart); + + final XAxis x = mStressChart.getXAxis(); + x.setDrawLabels(true); + x.setDrawGridLines(false); + x.setEnabled(true); + x.setTextColor(CHART_TEXT_COLOR); + x.setDrawLimitLinesBehindData(true); + + final YAxis yAxisLeft = mStressChart.getAxisLeft(); + yAxisLeft.setDrawGridLines(true); + yAxisLeft.setAxisMaximum(100f); + yAxisLeft.setAxisMinimum(0); + yAxisLeft.setDrawTopYLabelEntry(false); + yAxisLeft.setTextColor(CHART_TEXT_COLOR); + yAxisLeft.setEnabled(true); + + final YAxis yAxisRight = mStressChart.getAxisRight(); + yAxisRight.setDrawGridLines(false); + yAxisRight.setEnabled(true); + yAxisRight.setDrawLabels(false); + yAxisRight.setDrawTopYLabelEntry(true); + yAxisRight.setTextColor(CHART_TEXT_COLOR); + yAxisRight.setAxisMaximum(100f); + yAxisRight.setAxisMinimum(0); + } + + @Override + protected void setupLegend(final Chart chart) { + final List legendEntries = new ArrayList<>(StressType.values().length + 1); + + for (final StressType stressType : StressType.values()) { + final LegendEntry entry = new LegendEntry(); + entry.label = stressType.getLabel(requireContext()); + entry.formColor = stressType.getColor(requireContext()); + legendEntries.add(entry); + } + + if (!CHARTS_SLEEP_RANGE_24H && SHOW_CHARTS_AVERAGE) { + final LegendEntry averageEntry = new LegendEntry(); + averageEntry.label = STRESS_AVERAGE_LABEL; + averageEntry.formColor = Color.RED; + legendEntries.add(averageEntry); + } + + chart.getLegend().setCustom(legendEntries); + chart.getLegend().setTextColor(LEGEND_TEXT_COLOR); + } + + @Override + protected void renderCharts() { + mStressChart.animateX(ANIM_TIME, Easing.EaseInOutQuart); + mStressLevelsPieChart.invalidate(); + } + + private List getSamples(final DBHandler db, final GBDevice device) { + final int tsStart = getTSStart(); + final int tsEnd = getTSEnd(); + final DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device); + final TimeSampleProvider sampleProvider = coordinator.getStressSampleProvider(device, db.getDaoSession()); + return sampleProvider.getAllSamples(tsStart * 1000L, tsEnd * 1000L); + } + + protected void ensureStartAndEndSamples(final List samples) { + if (samples == null || samples.isEmpty()) { + return; + } + + final long tsEndMillis = getTSEnd() * 1000L; + final long tsStartMillis = getTSStart() * 1000L; + + final StressSample lastSample = samples.get(samples.size() - 1); + if (lastSample.getTimestamp() < tsEndMillis) { + samples.add(new EmptyStressSample(tsEndMillis)); + } + + final StressSample firstSample = samples.get(0); + if (firstSample.getTimestamp() > tsStartMillis) { + samples.add(0, new EmptyStressSample(tsStartMillis)); + } + } + + protected static final class EmptyStressSample implements StressSample { + private final long ts; + + public EmptyStressSample(final long ts) { + this.ts = ts; + } + + @Override + public Type getType() { + return Type.AUTOMATIC; + } + + @Override + public int getStress() { + return -1; + } + + @Override + public long getTimestamp() { + return ts; + } + } + + protected class StressChartsDataBuilder { + private static final int UNKNOWN_VAL = 2; + + private final List samples; + + private final TimestampTranslation tsTranslation = new TimestampTranslation(); + + private final Map> lineEntriesPerLevel = new HashMap<>(); + private final Map accumulator = new HashMap<>(); + + int previousTs; + int currentTypeStartTs; + StressType previousStressType; + long averageSum; + long averageNumSamples; + + public StressChartsDataBuilder(final List samples) { + this.samples = samples; + } + + private void reset() { + tsTranslation.reset(); + lineEntriesPerLevel.clear(); + accumulator.clear(); + for (final StressType stressType : StressType.values()) { + lineEntriesPerLevel.put(stressType, new ArrayList<>()); + accumulator.put(stressType, 0); + } + previousTs = 0; + currentTypeStartTs = 0; + previousStressType = StressType.UNKNOWN; + } + + private void processSamples() { + reset(); + + for (final StressSample sample : samples) { + processSample(sample); + } + + // Add the last block, if any + if (currentTypeStartTs != previousTs) { + set(previousTs, previousStressType, samples.get(samples.size() - 1).getStress()); + } + } + + private void processSample(final StressSample sample) { + //LOG.debug("Processing sample {} {}", sdf.format(new Date(sample.getTimestamp())), sample.getStress()); + + final StressType stressType = StressType.fromStress(sample.getStress()); + final int ts = tsTranslation.shorten((int) (sample.getTimestamp() / 1000L)); + + if (ts == 0) { + // First sample + previousTs = ts; + currentTypeStartTs = ts; + previousStressType = stressType; + set(ts, stressType, sample.getStress()); + return; + } + + if (ts - previousTs > 60 * 10) { + // More than 15 minutes since last sample + // Set to unknown right after the last sample we got until the current time + int lastEndTs = Math.min(previousTs + 60 * 5, ts - 1); + set(lastEndTs, StressType.UNKNOWN, UNKNOWN_VAL); + set(ts - 1, StressType.UNKNOWN, UNKNOWN_VAL); + } + + if (!stressType.equals(previousStressType)) { + currentTypeStartTs = ts; + } + + set(ts, stressType, sample.getStress()); + + accumulator.put(stressType, accumulator.get(stressType) + 60); + + if (stressType != StressType.UNKNOWN) { + averageSum += sample.getStress(); + averageNumSamples++; + } + + previousStressType = stressType; + previousTs = ts; + } + + private void set(final int ts, final StressType stressType, final int stress) { + for (final Map.Entry> stressTypeListEntry : lineEntriesPerLevel.entrySet()) { + if (stressTypeListEntry.getKey() == stressType) { + stressTypeListEntry.getValue().add(new Entry(ts, stress)); + } else { + stressTypeListEntry.getValue().add(new Entry(ts, 0)); + } + } + } + + public StressChartsData build() { + processSamples(); + + final List lineDataSets = new ArrayList<>(); + final List pieEntries = new ArrayList<>(); + final List pieColors = new ArrayList<>(); + + for (final StressType stressType : StressType.values()) { + final List stressEntries = lineEntriesPerLevel.get(stressType); + lineDataSets.add(createDataSet(stressType, stressEntries)); + + final Integer stressTime = accumulator.get(stressType); + if (stressType != StressType.UNKNOWN && stressTime != null && stressTime != 0) { + pieEntries.add(new PieEntry(stressTime, stressType.getLabel(requireContext()))); + pieColors.add(stressType.getColor(requireContext())); + } + } + + final PieDataSet pieDataSet = new PieDataSet(pieEntries, ""); + pieDataSet.setValueFormatter(new ValueFormatter() { + @Override + public String getFormattedValue(float value) { + return DateTimeUtils.formatDurationHoursMinutes((long) value, TimeUnit.SECONDS); + } + }); + pieDataSet.setColors(pieColors); + pieDataSet.setValueTextColor(DESCRIPTION_COLOR); + pieDataSet.setValueTextSize(13f); + pieDataSet.setXValuePosition(PieDataSet.ValuePosition.OUTSIDE_SLICE); + pieDataSet.setYValuePosition(PieDataSet.ValuePosition.OUTSIDE_SLICE); + final PieData pieData = new PieData(pieDataSet); + + final LineData lineData = new LineData(lineDataSets); + final ValueFormatter xValueFormatter = new SampleXLabelFormatter(tsTranslation); + final DefaultChartsData chartsData = new DefaultChartsData<>(lineData, xValueFormatter); + return new StressChartsData(pieData, chartsData, Math.round((float) averageSum / averageNumSamples)); + } + } + + protected static class StressChartsData extends ChartsData { + private final PieData pieData; + private final DefaultChartsData chartsData; + private final int average; + + public StressChartsData(final PieData pieData, final DefaultChartsData chartsData, final int average) { + this.pieData = pieData; + this.chartsData = chartsData; + this.average = average; + } + + public PieData getPieData() { + return pieData; + } + + public DefaultChartsData getChartsData() { + return chartsData; + } + + public int getAverage() { + return average; + } + } + + protected enum StressType { + UNKNOWN(R.string.unknown, R.color.chart_not_worn_light), + RELAXED(R.string.stress_relaxed, R.color.chart_rem_sleep_dark), + MILD(R.string.stress_mild, R.color.chart_activity_dark), + MODERATE(R.string.stress_moderate, R.color.chart_heartrate), + HIGH(R.string.stress_high, R.color.chart_heartrate_alternative), + ; + + private final int labelId; + private final int colorId; + + StressType(final int labelId, final int colorId) { + this.labelId = labelId; + this.colorId = colorId; + } + + public String getLabel(final Context context) { + return context.getString(labelId); + } + + public int getColor(final Context context) { + return ContextCompat.getColor(context, colorId); + } + + public static StressType fromStress(final int stress) { + if (stress < 0) { + return StressType.UNKNOWN; + } else if (stress < 40) { + return StressType.RELAXED; + } else if (stress < 60) { + return StressType.MILD; + } else if (stress < 80) { + return StressType.MODERATE; + } else { + return StressType.HIGH; + } + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/TimestampTranslation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/TimestampTranslation.java index 6a9e51605..4b3aa99f9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/TimestampTranslation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/TimestampTranslation.java @@ -42,4 +42,8 @@ public class TimestampTranslation { } return timestamp + tsOffset; } + + public void reset() { + tsOffset = -1; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java index bd83b0363..5b1faed1d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java @@ -1669,16 +1669,16 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements this.fetchOperationQueue.add(new HuamiFetchDebugLogsOperation(this)); } + if ((dataTypes & RecordedDataTypes.TYPE_STRESS) != 0 && coordinator.supportsStressMeasurement()) { + this.fetchOperationQueue.add(new FetchStressAutoOperation(this)); + this.fetchOperationQueue.add(new FetchStressManualOperation(this)); + } + if (Huami2021Coordinator.experimentalFeatures(getDevice())) { if ((dataTypes & RecordedDataTypes.TYPE_SPO2) != 0 && coordinator.supportsSpo2()) { this.fetchOperationQueue.add(new FetchSpo2NormalOperation(this)); } - if ((dataTypes & RecordedDataTypes.TYPE_STRESS) != 0 && coordinator.supportsStressMeasurement()) { - this.fetchOperationQueue.add(new FetchStressAutoOperation(this)); - this.fetchOperationQueue.add(new FetchStressManualOperation(this)); - } - if ((dataTypes & RecordedDataTypes.TYPE_HEART_RATE) != 0 && coordinator.supportsHeartRateStats()) { this.fetchOperationQueue.add(new FetchHeartRateManualOperation(this)); this.fetchOperationQueue.add(new FetchHeartRateMaxOperation(this)); diff --git a/app/src/main/res/layout-land/fragment_stresschart.xml b/app/src/main/res/layout-land/fragment_stresschart.xml new file mode 100644 index 000000000..0358198bb --- /dev/null +++ b/app/src/main/res/layout-land/fragment_stresschart.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_stresschart.xml b/app/src/main/res/layout/fragment_stresschart.xml new file mode 100644 index 000000000..c04373f5d --- /dev/null +++ b/app/src/main/res/layout/fragment_stresschart.xml @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index a459e2663..a9f8355eb 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -2459,6 +2459,7 @@ @string/weeksleepchart_sleep_a_week @string/weeksleepchart_sleep_a_month @string/weekstepschart_steps_a_month + @string/menuitem_stress @string/stats_title @string/liveactivity_live_activity @@ -2469,6 +2470,7 @@ @string/p_sleep @string/p_sleep_week @string/p_steps_week + @string/p_stress @string/p_speed_zones @string/p_live_stats @@ -2479,6 +2481,7 @@ @string/p_sleep @string/p_sleep_week @string/p_steps_week + @string/p_stress @string/p_speed_zones @string/p_live_stats diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4507d9bca..f66525a8a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1028,6 +1028,7 @@ Heart rate Heart rate Heart rate average + Stress average Daily target: calories burnt Daily target: distance in meters Daily target: active time in minutes @@ -1887,6 +1888,10 @@ Blood Oxygen Ambient Sound Control Device Information + Relaxed + Mild + Moderate + High Mode Off Noise Cancelling diff --git a/app/src/main/res/values/values.xml b/app/src/main/res/values/values.xml index 5e68e7c07..a0eda9aa3 100644 --- a/app/src/main/res/values/values.xml +++ b/app/src/main/res/values/values.xml @@ -101,6 +101,7 @@ sleep sleepweek stepsweek + stress speedzones livestats