diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractWeekChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractWeekChartFragment.java deleted file mode 100644 index a2c37297fe..0000000000 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/AbstractWeekChartFragment.java +++ /dev/null @@ -1,356 +0,0 @@ -/* Copyright (C) 2017-2024 Alberto, Andreas Shimokawa, Carsten Pfeiffer, - Daniele Gobbetti, José Rebelo, Pavel Elagin, Petr Vaněk, a0z - - 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.app.Activity; -import android.graphics.Color; - -import com.github.mikephil.charting.charts.BarChart; -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.ChartData; -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.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.Calendar; -import java.util.List; -import java.util.Locale; - -import nodomain.freeyourgadget.gadgetbridge.GBApplication; -import nodomain.freeyourgadget.gadgetbridge.R; -import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; -import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider; -import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; -import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmounts; -import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; -import nodomain.freeyourgadget.gadgetbridge.model.SleepScoreSample; -import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue; - - -public abstract class AbstractWeekChartFragment extends AbstractActivityChartFragment { - protected static final Logger LOG = LoggerFactory.getLogger(AbstractWeekChartFragment.class); - protected int TOTAL_DAYS = getRangeDays(); - protected int TOTAL_DAYS_FOR_AVERAGE = 0; - - protected Locale mLocale; - protected int mTargetValue = 0; - - protected BarChart mWeekChart; - - private final int mOffsetHours = getOffsetHours(); - - protected String getWeeksChartsLabel(Calendar day){ - if (TOTAL_DAYS > 7) { - //month, show day date - return String.valueOf(day.get(Calendar.DAY_OF_MONTH)); - } - else{ - //week, show short day name - return day.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT, mLocale); - } - } - protected WeekChartsData refreshWeekBeforeData(DBHandler db, BarChart barChart, Calendar day, GBDevice device) { - day = (Calendar) day.clone(); // do not modify the caller's argument - day.add(Calendar.DATE, -TOTAL_DAYS + 1); - List entries = new ArrayList<>(); - ArrayList labels = new ArrayList(); - - long balance = 0; - long daily_balance = 0; - TOTAL_DAYS_FOR_AVERAGE=0; - List sleepScoreEntities = new ArrayList<>(); - final List sleepScoreDataSets = new ArrayList<>(); - for (int counter = 0; counter < TOTAL_DAYS; counter++) { - // Sleep stages - ActivityAmounts amounts = getActivityAmountsForDay(db, day, device); - daily_balance=calculateBalance(amounts); - if (daily_balance > 0) { - TOTAL_DAYS_FOR_AVERAGE++; - } - balance += daily_balance; - entries.add(new BarEntry(counter, getTotalsForActivityAmounts(amounts))); - labels.add(getWeeksChartsLabel(day)); - // Sleep score - if (supportsSleepScore()) { - List sleepScoreSamples = getSleepScoreSamples(db, device, day); - if (!sleepScoreSamples.isEmpty() && sleepScoreSamples.get(0).getSleepScore() > 0) { - sleepScoreEntities.add(new Entry(counter, sleepScoreSamples.get(0).getSleepScore())); - } else { - if (!sleepScoreEntities.isEmpty()) { - List clone = new ArrayList<>(sleepScoreEntities.size()); - clone.addAll(sleepScoreEntities); - sleepScoreDataSets.add(createSleepScoreDataSet(clone)); - sleepScoreEntities.clear(); - } - } - } - day.add(Calendar.DATE, 1); - } - if (!sleepScoreEntities.isEmpty()) { - sleepScoreDataSets.add(createSleepScoreDataSet(sleepScoreEntities)); - } - final LineData sleepScoreLineData = new LineData(sleepScoreDataSets); - sleepScoreLineData.setHighlightEnabled(false); - - BarDataSet set = new BarDataSet(entries, ""); - set.setColors(getColors()); - set.setValueFormatter(getBarValueFormatter()); - - BarData barData = new BarData(set); - barData.setValueTextColor(Color.GRAY); //prevent tearing other graph elements with the black text. Another approach would be to hide the values cmpletely with data.setDrawValues(false); - barData.setValueTextSize(10f); - - barChart.getAxisLeft().setAxisMaximum(Math.max(set.getYMax(), mTargetValue) + 60); - - LimitLine target = new LimitLine(mTargetValue); - target.setLineWidth(1.5f); - target.enableDashedLine(15f, 10f, 0f); - target.setLineColor(getResources().getColor(R.color.chart_deep_sleep_dark)); - barChart.getAxisLeft().removeAllLimitLines(); - barChart.getAxisLeft().addLimitLine(target); - - float average = 0; - if (TOTAL_DAYS_FOR_AVERAGE > 0) { - average = Math.abs(balance / TOTAL_DAYS_FOR_AVERAGE); - } - LimitLine average_line = new LimitLine(average); - average_line.setLineWidth(1.5f); - average_line.enableDashedLine(15f, 10f, 0f); - average_line.setLabel(getString(R.string.average, getAverage(average))); - - if (average > (mTargetValue)) { - average_line.setLineColor(Color.GREEN); - average_line.setTextColor(Color.GREEN); - } - else { - average_line.setLineColor(Color.RED); - average_line.setTextColor(Color.RED); - } - if (average > 0) { - if (GBApplication.getPrefs().getBoolean("charts_show_average", true)) { - barChart.getAxisLeft().addLimitLine(average_line); - } - } - - if (supportsSleepScore()) { - return new WeekChartsData(barData, new PreformattedXIndexLabelFormatter(labels), getBalanceMessage(balance, mTargetValue), sleepScoreLineData); - } - return new WeekChartsData(barData, new PreformattedXIndexLabelFormatter(labels), getBalanceMessage(balance, mTargetValue)); - } - - protected List getSleepScoreSamples(DBHandler db, GBDevice device, Calendar day) { - 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; - - TimeSampleProvider provider = device.getDeviceCoordinator().getSleepScoreProvider(device, db.getDaoSession()); - return (List) provider.getAllSamples(startTs * 1000L, endTs * 1000L); - } - - protected LineDataSet createSleepScoreDataSet(final List values) { - final LineDataSet lineDataSet = new LineDataSet(values, getString(R.string.sleep_score)); - lineDataSet.setColor(getResources().getColor(R.color.chart_light_sleep_light)); - lineDataSet.setDrawCircles(false); - lineDataSet.setLineWidth(2f); - lineDataSet.setFillAlpha(255); - lineDataSet.setCircleRadius(5f); - lineDataSet.setDrawCircles(true); - lineDataSet.setDrawCircleHole(true); - lineDataSet.setCircleColor(getResources().getColor(R.color.chart_light_sleep_light)); - lineDataSet.setAxisDependency(YAxis.AxisDependency.LEFT); - lineDataSet.setDrawValues(true); - lineDataSet.setValueTextSize(10f); - lineDataSet.setValueTextColor(CHART_TEXT_COLOR); - lineDataSet.setValueFormatter(new ValueFormatter() { - @Override - public String getFormattedValue(float value) { - return String.format(Locale.ROOT, "%d", (int) value); - } - }); - return lineDataSet; - }; - - protected void setupWeekChart() { - mWeekChart.setBackgroundColor(BACKGROUND_COLOR); - mWeekChart.getDescription().setTextColor(DESCRIPTION_COLOR); - mWeekChart.getDescription().setText(""); - mWeekChart.setFitBars(true); - - configureBarLineChartDefaults(mWeekChart); - - XAxis x = mWeekChart.getXAxis(); - x.setDrawLabels(true); - x.setDrawGridLines(false); - x.setEnabled(true); - x.setTextColor(CHART_TEXT_COLOR); - x.setDrawLimitLinesBehindData(true); - x.setPosition(XAxis.XAxisPosition.BOTTOM); - - YAxis y = mWeekChart.getAxisLeft(); - y.setDrawGridLines(false); - y.setDrawTopYLabelEntry(false); - y.setTextColor(CHART_TEXT_COLOR); - y.setDrawZeroLine(true); - y.setSpaceBottom(0); - y.setAxisMinimum(0); - y.setValueFormatter(getYAxisFormatter()); - y.setEnabled(true); - - YAxis yAxisRight = mWeekChart.getAxisRight(); - yAxisRight.setDrawGridLines(false); - yAxisRight.setEnabled(false); - yAxisRight.setDrawLabels(false); - yAxisRight.setDrawTopYLabelEntry(false); - yAxisRight.setTextColor(CHART_TEXT_COLOR); - } - - private List getSamplesOfDay(DBHandler db, Calendar day, int offsetHours, GBDevice device) { - 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, offsetHours); - - startTs = (int) (day.getTimeInMillis() / 1000); - endTs = startTs + 24 * 60 * 60 - 1; - - return getSamples(db, device, startTs, endTs); - } - - @Override - protected List getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) { - return super.getAllSamples(db, device, tsFrom, tsTo); - } - - protected static class MyChartsData extends ChartsData { - private final WeekChartsData weekBeforeData; - - MyChartsData(WeekChartsData weekBeforeData) { - this.weekBeforeData = weekBeforeData; - } - - WeekChartsData getWeekBeforeData() { - return weekBeforeData; - } - } - - protected ActivityAmounts getActivityAmountsForDay(DBHandler db, Calendar day, GBDevice device) { - - LimitedQueue activityAmountCache = null; - ActivityAmounts amounts = null; - - Activity activity = getActivity(); - int key = (int) (day.getTimeInMillis() / 1000) + (mOffsetHours * 3600); - if (activity != null) { - activityAmountCache = ((ActivityChartsActivity) activity).mActivityAmountCache; - amounts = activityAmountCache.lookup(key); - } - - if (amounts == null) { - ActivityAnalysis analysis = new ActivityAnalysis(); - amounts = analysis.calculateActivityAmounts(getSamplesOfDay(db, day, mOffsetHours, device)); - if (activityAmountCache != null) { - activityAmountCache.add(key, amounts); - } - } - - return amounts; - } - - private int getRangeDays(){ - if (GBApplication.getPrefs().getBoolean("charts_range", true)) { - return 30;} - else{ - return 7; - } - } - - public boolean supportsSleepScore() { - final GBDevice device = getChartsHost().getDevice(); - return device.getDeviceCoordinator().supportsSleepScore(device); - } - - abstract String getAverage(float value); - - abstract int getGoal(); - - abstract int getOffsetHours(); - - abstract float[] getTotalsForActivityAmounts(ActivityAmounts activityAmounts); - - abstract String formatPieValue(long value); - - abstract String[] getPieLabels(); - - abstract ValueFormatter getPieValueFormatter(); - - abstract ValueFormatter getBarValueFormatter(); - - abstract ValueFormatter getYAxisFormatter(); - - abstract int[] getColors(); - - abstract String getPieDescription(int targetValue); - - protected abstract long calculateBalance(ActivityAmounts amounts); - - protected abstract String getBalanceMessage(long balance, int targetValue); - - protected class WeekChartsData> extends DefaultChartsData { - private final String balanceMessage; - private LineData sleepScoresLineData; - - public WeekChartsData(T data, PreformattedXIndexLabelFormatter xIndexLabelFormatter, String balanceMessage) { - super(data, xIndexLabelFormatter); - this.balanceMessage = balanceMessage; - } - - public WeekChartsData(T data, PreformattedXIndexLabelFormatter xIndexLabelFormatter, String balanceMessage, LineData sleepScores) { - super(data, xIndexLabelFormatter); - this.balanceMessage = balanceMessage; - this.sleepScoresLineData = sleepScores; - } - - public String getBalanceMessage() { - return balanceMessage; - } - - public LineData getSleepScoreData() { return sleepScoresLineData; } - } -} 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 05f66dc8d8..4c934ebe6a 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 @@ -177,7 +177,7 @@ public class ActivityChartsActivity extends AbstractChartsActivity { case "vo2max": return new VO2MaxFragment(); case "stress": - return new StressChartFragment(); + return StressCollectionFragment.newInstance(enabledTabsList.size() == 1); case "pai": return new PaiChartFragment(); case "stepsweek": diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/Spo2ChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/Spo2ChartFragment.java index f235cab9c0..3a08b8902c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/Spo2ChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/Spo2ChartFragment.java @@ -53,7 +53,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; -// Based on StressChartFragment +// Based on StressDailyFragment public class Spo2ChartFragment extends AbstractChartFragment { protected static final Logger LOG = LoggerFactory.getLogger(Spo2ChartFragment.class); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StressCollectionFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StressCollectionFragment.java new file mode 100644 index 0000000000..e3568a12c8 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StressCollectionFragment.java @@ -0,0 +1,44 @@ +/* Copyright (C) 2024 a0z, José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.activities.charts; + +import android.os.Bundle; + +import androidx.fragment.app.FragmentManager; + +import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment; +import nodomain.freeyourgadget.gadgetbridge.adapter.NestedFragmentAdapter; +import nodomain.freeyourgadget.gadgetbridge.adapter.StressFragmentAdapter; + +public class StressCollectionFragment extends AbstractCollectionFragment { + public StressCollectionFragment() { + + } + + public static StressCollectionFragment newInstance(final boolean allowSwipe) { + final StressCollectionFragment fragment = new StressCollectionFragment(); + 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 StressFragmentAdapter(this, getChildFragmentManager()); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StressChartFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StressDailyFragment.java similarity index 82% rename from app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StressChartFragment.java rename to app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StressDailyFragment.java index 64ad312132..c20b94d80d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StressChartFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StressDailyFragment.java @@ -16,7 +16,6 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.activities.charts; -import android.content.Context; import android.graphics.Color; import android.os.Build; import android.os.Bundle; @@ -29,8 +28,6 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; -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; @@ -64,15 +61,13 @@ 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.Prefs; -public class StressChartFragment extends AbstractChartFragment { - protected static final Logger LOG = LoggerFactory.getLogger(StressChartFragment.class); +public class StressDailyFragment extends StressFragment { + protected static final Logger LOG = LoggerFactory.getLogger(StressDailyFragment.class); private LineChart mStressChart; private PieChart mStressLevelsPieChart; @@ -82,27 +77,17 @@ public class StressChartFragment extends AbstractChartFragment samples = getSamples(db, device, tsStart, tsEnd); + final List samples = getStressSamples(db, device, tsStart, tsEnd); LOG.info("Got {} stress samples", samples.size()); @@ -196,11 +181,6 @@ public class StressChartFragment extends AbstractChartFragment 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); - } + final List legendEntries = createLegendEntries(chart); if (!CHARTS_SLEEP_RANGE_24H && SHOW_CHARTS_AVERAGE) { final LegendEntry averageEntry = new LegendEntry(); @@ -303,54 +276,6 @@ public class StressChartFragment extends AbstractChartFragment getSamples(final DBHandler db, final GBDevice device, int tsStart, int tsEnd) { - final DeviceCoordinator coordinator = device.getDeviceCoordinator(); - final TimeSampleProvider sampleProvider = coordinator.getStressSampleProvider(device, db.getDaoSession()); - return sampleProvider.getAllSamples(tsStart * 1000L, tsEnd * 1000L); - } - - protected void ensureStartAndEndSamples(final List samples, int tsStart, int tsEnd) { - if (samples == null || samples.isEmpty()) { - return; - } - - final long tsEndMillis = tsEnd * 1000L; - final long tsStartMillis = tsStart * 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; @@ -407,8 +332,6 @@ public class StressChartFragment extends AbstractChartFragment sampleRate * 10) { // More than 15 minutes since last sample @@ -567,43 +488,4 @@ public class StressChartFragment extends AbstractChartFragment extends AbstractChartFragment { + + protected int BACKGROUND_COLOR; + protected int DESCRIPTION_COLOR; + protected int CHART_TEXT_COLOR; + protected int LEGEND_TEXT_COLOR; + + @Override + public String getTitle() { + return getString(R.string.menuitem_stress); + } + + @Override + protected void init() { + BACKGROUND_COLOR = GBApplication.getBackgroundColor(requireContext()); + LEGEND_TEXT_COLOR = DESCRIPTION_COLOR = GBApplication.getTextColor(requireContext()); + CHART_TEXT_COLOR = GBApplication.getSecondaryTextColor(requireContext()); + } + + protected List getStressSamples(DBHandler db, GBDevice device, int tsStart, int tsEnd) { + final DeviceCoordinator coordinator = device.getDeviceCoordinator(); + final TimeSampleProvider sampleProvider = coordinator.getStressSampleProvider(device, db.getDaoSession()); + return sampleProvider.getAllSamples(tsStart * 1000L, tsEnd * 1000L); + } + + protected void ensureStartAndEndSamples(final List samples, int tsStart, int tsEnd) { + if (samples == null || samples.isEmpty()) { + return; + } + + final long tsEndMillis = tsEnd * 1000L; + final long tsStartMillis = tsStart * 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 List createLegendEntries(Chart chart) { + List legendEntries = new ArrayList<>(StressType.values().length); + + for (final StressType stressType : StressType.values()) { + final LegendEntry entry = new LegendEntry(); + entry.label = stressType.getLabel(requireContext()); + entry.formColor = stressType.getColor(requireContext()); + legendEntries.add(entry); + } + + return legendEntries; + } + + protected Map calculateStressTotals(List samples, int[] stressRanges, int sampleRate) { + Map result = new HashMap<>(); + for (StressType type : StressType.values()) { + result.put(type, 0); + } + + StressSample prevSample = null; + for (StressSample sample : samples) { + if (prevSample != null && sample.getStress() >= 0) { + long durationSinceLastSample = sample.getTimestamp() - prevSample.getTimestamp(); + durationSinceLastSample = Math.min(durationSinceLastSample, sampleRate * 10 * 1000); // Cap at 10x sample rate + + StressType type = StressType.fromStress(prevSample.getStress(), stressRanges); + if (type != StressType.UNKNOWN) { + int seconds = (int) (durationSinceLastSample / 1000); + result.put(type, result.get(type) + seconds); + } + } + prevSample = sample; + } + + return result; + } + + protected int calculateAverageStress(List samples) { + long sum = 0; + int count = 0; + + for (StressSample sample : samples) { + if (sample.getStress() > 0) { + sum += sample.getStress(); + count++; + } + } + + return count > 0 ? Math.round((float) sum / count) : 0; + } + + 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; + } + } + + public enum StressType { + UNKNOWN(R.string.unknown, R.color.chart_stress_unknown), + RELAXED(R.string.stress_relaxed, R.color.chart_stress_relaxed), + MILD(R.string.stress_mild, R.color.chart_stress_mild), + MODERATE(R.string.stress_moderate, R.color.chart_stress_moderate), + HIGH(R.string.stress_high, R.color.chart_stress_high); + + 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, final int[] stressRanges) { + if (stress < stressRanges[0]) { + return StressType.UNKNOWN; + } else if (stress < stressRanges[1]) { + return StressType.RELAXED; + } else if (stress < stressRanges[2]) { + return StressType.MILD; + } else if (stress < stressRanges[3]) { + return StressType.MODERATE; + } else { + return StressType.HIGH; + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StressPeriodFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StressPeriodFragment.java new file mode 100644 index 0000000000..dd0b7916d8 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/StressPeriodFragment.java @@ -0,0 +1,513 @@ +/* Copyright (C) 2023-2024 + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.activities.charts; + +import android.os.Build; +import android.os.Bundle; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.style.RelativeSizeSpan; +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.PieChart; +import com.github.mikephil.charting.components.Legend; +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.BarData; +import com.github.mikephil.charting.data.BarDataSet; +import com.github.mikephil.charting.data.BarEntry; +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.IBarDataSet; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.Locale; +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.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.StressSample; +import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; + +public class StressPeriodFragment extends StressFragment { + protected static final Logger LOG = LoggerFactory.getLogger(StressPeriodFragment.class); + + protected int TOTAL_DAYS = getRangeDays(); + protected int TOTAL_DAYS_FOR_AVERAGE = 0; + + private TextView relaxedStressTimeText; + private TextView mildStressTimeText; + private TextView moderateStressTimeText; + private TextView highStressTimeText; + private TextView stressDatesText; + private PieChart mStressLevelsPieChart; + private BarChart mWeekChart; + + private MyStressWeeklyData myStressWeeklyData; + private boolean showStressLevelInPercents = false; + protected Locale mLocale; + + public static StressPeriodFragment newInstance(int totalDays) { + StressPeriodFragment fragment = new StressPeriodFragment(); + Bundle args = new Bundle(); + args.putInt("totalDays", totalDays); + fragment.setArguments(args); + return fragment; + } + + @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) { + mLocale = getResources().getConfiguration().locale; + View rootView = inflater.inflate(R.layout.fragment_weekstress_chart, container, false); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + rootView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> { + getChartsHost().enableSwipeRefresh(scrollY == 0); + }); + } + + mWeekChart = rootView.findViewById(R.id.weekstresschart); + mStressLevelsPieChart = rootView.findViewById(R.id.stress_pie_chart); + relaxedStressTimeText = rootView.findViewById(R.id.stress_chart_relaxed_time); + mildStressTimeText = rootView.findViewById(R.id.stress_chart_mild_time); + moderateStressTimeText = rootView.findViewById(R.id.stress_chart_moderate_time); + highStressTimeText = rootView.findViewById(R.id.stress_chart_high_time); + stressDatesText = rootView.findViewById(R.id.stress_dates); + + setupPieChart(); + setupWeekChart(); + refresh(); + + return rootView; + } + + @Override + protected MyChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) { + Calendar day = Calendar.getInstance(); + day.setTime(chartsHost.getEndDate()); + DefaultChartsData weekBeforeData = refreshWeekBeforeStressData(db, mWeekChart, day, device); + myStressWeeklyData = getMyStressWeeklyData(db, day, device); + + return new MyChartsData(weekBeforeData); + } + + @Override + protected void updateChartsnUIThread(MyChartsData mcd) { + setupLegend(mWeekChart); + + mWeekChart.setData(null); // workaround for https://github.com/PhilJay/MPAndroidChart/issues/2317 + mWeekChart.setData(mcd.getWeekBeforeData().getData()); + mWeekChart.getXAxis().setValueFormatter(mcd.getWeekBeforeData().getXValueFormatter()); + mWeekChart.getBarData().setValueTextSize(10f); + + updatePieChart(); + updateStressTimeTexts(); + + stressDatesText.setText(DateTimeUtils.formatDaysUntil(TOTAL_DAYS, getTSEnd())); + } + + private void updatePieChart() { + List pieEntries = new ArrayList<>(); + List pieColors = new ArrayList<>(); + + if (TOTAL_DAYS_FOR_AVERAGE > 0 && myStressWeeklyData != null) { + long totalTime = myStressWeeklyData.totalStressTime(); + if (totalTime > 0) { + if (myStressWeeklyData.totalRelaxed() > 0) { + pieEntries.add(new PieEntry(myStressWeeklyData.totalRelaxed(), + StressType.RELAXED.getLabel(getContext()))); + pieColors.add(StressType.RELAXED.getColor(getContext())); + } + if (myStressWeeklyData.totalMild() > 0) { + pieEntries.add(new PieEntry(myStressWeeklyData.totalMild(), + StressType.MILD.getLabel(getContext()))); + pieColors.add(StressType.MILD.getColor(getContext())); + } + if (myStressWeeklyData.totalModerate() > 0) { + pieEntries.add(new PieEntry(myStressWeeklyData.totalModerate(), + StressType.MODERATE.getLabel(getContext()))); + pieColors.add(StressType.MODERATE.getColor(getContext())); + } + if (myStressWeeklyData.totalHigh() > 0) { + pieEntries.add(new PieEntry(myStressWeeklyData.totalHigh(), + StressType.HIGH.getLabel(getContext()))); + pieColors.add(StressType.HIGH.getColor(getContext())); + } + } + } + + if (pieEntries.isEmpty()) { + pieEntries.add(new PieEntry(1)); + pieColors.add(getResources().getColor(R.color.gauge_line_color)); + } + + PieDataSet pieDataSet = new PieDataSet(pieEntries, ""); + pieDataSet.setColors(pieColors); + pieDataSet.setValueTextColor(DESCRIPTION_COLOR); + pieDataSet.setValueTextSize(13f); + pieDataSet.setXValuePosition(PieDataSet.ValuePosition.OUTSIDE_SLICE); + pieDataSet.setYValuePosition(PieDataSet.ValuePosition.OUTSIDE_SLICE); + pieDataSet.setDrawValues(false); + pieDataSet.setSliceSpace(2f); + PieData pieData = new PieData(pieDataSet); + + mStressLevelsPieChart.setData(pieData); + + // Set center text with average stress level + if (myStressWeeklyData != null && myStressWeeklyData.averageStress() > 0) { + int avgStress = myStressWeeklyData.averageStress(); + int noc = String.valueOf(avgStress).length(); + SpannableString centerText = new SpannableString(avgStress + "\n" + + getContext().getString(R.string.stress_average)); + centerText.setSpan(new RelativeSizeSpan(1.75f), 0, noc, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + centerText.setSpan(new RelativeSizeSpan(0.72f), noc, centerText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + mStressLevelsPieChart.setCenterText(centerText); + } else { + SpannableString centerText = new SpannableString("-\n" + + getContext().getString(R.string.stress_average)); + centerText.setSpan(new RelativeSizeSpan(1.25f), 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + centerText.setSpan(new RelativeSizeSpan(0.72f), 2, centerText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + mStressLevelsPieChart.setCenterText(centerText); + } + } + + private void updateStressTimeTexts() { + if (TOTAL_DAYS_FOR_AVERAGE > 0 && myStressWeeklyData != null) { + int relaxedAvg = (int) (myStressWeeklyData.totalRelaxed() / TOTAL_DAYS_FOR_AVERAGE); + int mildAvg = (int) (myStressWeeklyData.totalMild() / TOTAL_DAYS_FOR_AVERAGE); + int moderateAvg = (int) (myStressWeeklyData.totalModerate() / TOTAL_DAYS_FOR_AVERAGE); + int highAvg = (int) (myStressWeeklyData.totalHigh() / TOTAL_DAYS_FOR_AVERAGE); + + if (showStressLevelInPercents) { + long totalDailyAvg = relaxedAvg + mildAvg + moderateAvg + highAvg; + relaxedStressTimeText.setText(String.format(Locale.ROOT, "%d%%", + totalDailyAvg > 0 ? Math.round(100f * relaxedAvg / totalDailyAvg) : 0)); + mildStressTimeText.setText(String.format(Locale.ROOT, "%d%%", + totalDailyAvg > 0 ? Math.round(100f * mildAvg / totalDailyAvg) : 0)); + moderateStressTimeText.setText(String.format(Locale.ROOT, "%d%%", + totalDailyAvg > 0 ? Math.round(100f * moderateAvg / totalDailyAvg) : 0)); + highStressTimeText.setText(String.format(Locale.ROOT, "%d%%", + totalDailyAvg > 0 ? Math.round(100f * highAvg / totalDailyAvg) : 0)); + } else { + relaxedStressTimeText.setText(DateTimeUtils.formatDurationHoursMinutes(relaxedAvg, TimeUnit.SECONDS)); + mildStressTimeText.setText(DateTimeUtils.formatDurationHoursMinutes(mildAvg, TimeUnit.SECONDS)); + moderateStressTimeText.setText(DateTimeUtils.formatDurationHoursMinutes(moderateAvg, TimeUnit.SECONDS)); + highStressTimeText.setText(DateTimeUtils.formatDurationHoursMinutes(highAvg, TimeUnit.SECONDS)); + } + } else { + relaxedStressTimeText.setText("-"); + mildStressTimeText.setText("-"); + moderateStressTimeText.setText("-"); + highStressTimeText.setText("-"); + } + } + + private MyStressWeeklyData getMyStressWeeklyData(DBHandler db, Calendar day, GBDevice device) { + day = (Calendar) day.clone(); // do not modify the caller's argument + day.add(Calendar.DATE, -TOTAL_DAYS + 1); + TOTAL_DAYS_FOR_AVERAGE = 0; + + long relaxedWeeklyTotal = 0; + long mildWeeklyTotal = 0; + long moderateWeeklyTotal = 0; + long highWeeklyTotal = 0; + long totalStressTime = 0; + long avgStressSum = 0; + long avgStressSamples = 0; + + int[] stressRanges = device.getDeviceCoordinator().getStressRanges(); + showStressLevelInPercents = device.getDeviceCoordinator().showStressLevelInPercents(); + + DeviceCoordinator coordinator = device.getDeviceCoordinator(); + int sampleRate = 60; // Default sample rate + int[] params = coordinator.getStressChartParameters(); + if (params != null && params.length > 0) { + sampleRate = params[0]; + } + + for (int counter = 0; counter < TOTAL_DAYS; counter++) { + Calendar dayStart = (Calendar) day.clone(); + Calendar dayEnd = (Calendar) day.clone(); + dayEnd.add(Calendar.DAY_OF_MONTH, 1); + + List samples = getStressSamples(db, device, + (int) (dayStart.getTimeInMillis() / 1000), + (int) (dayEnd.getTimeInMillis() / 1000)); + + if (!samples.isEmpty()) { + TOTAL_DAYS_FOR_AVERAGE++; + + Map dailyTotals = calculateStressTotals(samples, stressRanges, sampleRate); + relaxedWeeklyTotal += dailyTotals.getOrDefault(StressType.RELAXED, 0); + mildWeeklyTotal += dailyTotals.getOrDefault(StressType.MILD, 0); + moderateWeeklyTotal += dailyTotals.getOrDefault(StressType.MODERATE, 0); + highWeeklyTotal += dailyTotals.getOrDefault(StressType.HIGH, 0); + + // Calculate average stress for the day + int dailyAverage = calculateAverageStress(samples); + if (dailyAverage > 0) { + avgStressSum += dailyAverage; + avgStressSamples++; + } + } + + day.add(Calendar.DATE, 1); + } + + totalStressTime = relaxedWeeklyTotal + mildWeeklyTotal + moderateWeeklyTotal + highWeeklyTotal; + int averageStress = avgStressSamples > 0 ? Math.round((float) avgStressSum / avgStressSamples) : 0; + + return new MyStressWeeklyData(relaxedWeeklyTotal, mildWeeklyTotal, moderateWeeklyTotal, + highWeeklyTotal, totalStressTime, averageStress); + } + + private DefaultChartsData refreshWeekBeforeStressData(DBHandler db, BarChart chart, Calendar day, GBDevice device) { + day = (Calendar) day.clone(); + day.set(Calendar.HOUR_OF_DAY, 0); + day.set(Calendar.MINUTE, 0); + day.set(Calendar.SECOND, 0); + day.set(Calendar.MILLISECOND, 0); + day.add(Calendar.DATE, -TOTAL_DAYS + 1); + List entries = new ArrayList<>(); + ArrayList labels = new ArrayList<>(); + + int[] colors = new int[TOTAL_DAYS * 5]; // 4 stress types + unknown type + int colorIndex = 0; + + DeviceCoordinator coordinator = device.getDeviceCoordinator(); + int sampleRate = 60; + int[] params = coordinator.getStressChartParameters(); + if (params != null && params.length > 0) { + sampleRate = params[0]; + } + + Calendar now = Calendar.getInstance(); + + for (int counter = 0; counter < TOTAL_DAYS; counter++) { + Calendar dayStart = (Calendar) day.clone(); + Calendar dayEnd = (Calendar) day.clone(); + dayEnd.add(Calendar.DAY_OF_MONTH, 1); + + List samples = getStressSamples(db, device, + (int) (dayStart.getTimeInMillis() / 1000), + (int) (dayEnd.getTimeInMillis() / 1000)); + + Map dailyTotals = calculateStressTotals(samples, + device.getDeviceCoordinator().getStressRanges(), sampleRate); + + float[] yValues = new float[5]; // For stacked bar chart + int idx = 0; + + float totalMinutesTracked = dailyTotals.values().stream().reduce(0, Integer::sum) / 60f; + + // Calculate the total possible minutes for this day, excluding future time + float totalPossibleMinutes; + if (dayEnd.before(now) || dayEnd.equals(now)) { + // Full day in the past + totalPossibleMinutes = 24 * 60; + } else if (dayStart.after(now)) { + // Full day in the future + totalPossibleMinutes = 0; + } else { + // Partial day (current day) - only count minutes up to now + long minutesFromStartToNow = (now.getTimeInMillis() - dayStart.getTimeInMillis()) / (1000 * 60); + totalPossibleMinutes = Math.max(0, minutesFromStartToNow); + } + + float untrackedMins = Math.max(totalPossibleMinutes - totalMinutesTracked, 0); + + yValues[idx++] = dailyTotals.get(StressType.HIGH) / 60f; + yValues[idx++] = dailyTotals.get(StressType.MODERATE) / 60f; + yValues[idx++] = dailyTotals.get(StressType.MILD) / 60f; + yValues[idx++] = dailyTotals.get(StressType.RELAXED) / 60f; + yValues[idx++] = untrackedMins; + + colors[colorIndex++] = StressType.HIGH.getColor(getContext()); + colors[colorIndex++] = StressType.MODERATE.getColor(getContext()); + colors[colorIndex++] = StressType.MILD.getColor(getContext()); + colors[colorIndex++] = StressType.RELAXED.getColor(getContext()); + colors[colorIndex++] = StressType.UNKNOWN.getColor(getContext()); + + entries.add(new BarEntry(counter, yValues)); + + labels.add(TOTAL_DAYS > 7 + ? String.valueOf(day.get(Calendar.DAY_OF_MONTH)) + : day.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT, mLocale)); + + day.add(Calendar.DATE, 1); + } + + BarDataSet set = new BarDataSet(entries, ""); + set.setDrawValues(false); + set.setAxisDependency(YAxis.AxisDependency.LEFT); + set.setColors(colors); + set.setStackLabels(new String[]{ + StressType.HIGH.getLabel(getContext()), + StressType.MODERATE.getLabel(getContext()), + StressType.MILD.getLabel(getContext()), + StressType.RELAXED.getLabel(getContext()), + StressType.UNKNOWN.getLabel(getContext()) + }); + + ArrayList dataSets = new ArrayList<>(); + dataSets.add(set); + + BarData barData = new BarData(dataSets); + barData.setBarWidth(0.9f); + + return new DefaultChartsData<>(barData, new PreformattedXIndexLabelFormatter(labels)); + } + + private void setupPieChart() { + mStressLevelsPieChart.setBackgroundColor(BACKGROUND_COLOR); + mStressLevelsPieChart.getDescription().setTextColor(DESCRIPTION_COLOR); + mStressLevelsPieChart.setEntryLabelColor(DESCRIPTION_COLOR); + mStressLevelsPieChart.getDescription().setText(""); + mStressLevelsPieChart.setNoDataText(""); + mStressLevelsPieChart.setTouchEnabled(false); + mStressLevelsPieChart.setCenterTextColor(GBApplication.getTextColor(getContext())); + mStressLevelsPieChart.setCenterTextSize(18f); + mStressLevelsPieChart.setHoleColor(requireContext().getResources().getColor(R.color.transparent)); + mStressLevelsPieChart.setHoleRadius(85); + mStressLevelsPieChart.setDrawEntryLabels(false); + mStressLevelsPieChart.getLegend().setEnabled(false); + } + + protected void setupWeekChart() { + mWeekChart.setBackgroundColor(BACKGROUND_COLOR); + mWeekChart.getDescription().setTextColor(DESCRIPTION_COLOR); + mWeekChart.getDescription().setText(""); + mWeekChart.setFitBars(true); + + configureBarLineChartDefaults(mWeekChart); + + XAxis x = mWeekChart.getXAxis(); + x.setDrawLabels(true); + x.setDrawGridLines(false); + x.setEnabled(true); + x.setTextColor(CHART_TEXT_COLOR); + x.setDrawLimitLinesBehindData(true); + x.setPosition(XAxis.XAxisPosition.BOTTOM); + if (TOTAL_DAYS > 7) { + x.setSpaceMin(0); + } + + YAxis y = mWeekChart.getAxisLeft(); + y.setDrawGridLines(false); + y.setEnabled(true); + y.setDrawTopYLabelEntry(false); + y.setTextColor(CHART_TEXT_COLOR); + y.setDrawZeroLine(true); + y.setSpaceBottom(0); + y.setAxisMinimum(0); + y.setAxisMaximum(24 * 60); + y.setValueFormatter(getYAxisFormatter()); + + YAxis yAxisRight = mWeekChart.getAxisRight(); + yAxisRight.setDrawGridLines(false); + yAxisRight.setEnabled(false); + yAxisRight.setDrawLabels(false); + yAxisRight.setDrawTopYLabelEntry(false); + yAxisRight.setTextColor(CHART_TEXT_COLOR); + + if (TOTAL_DAYS > 7) { + mWeekChart.setRenderer(new AngledLabelsChartRenderer(mWeekChart, mWeekChart.getAnimator(), + mWeekChart.getViewPortHandler())); + } else { + mWeekChart.setScaleEnabled(false); + mWeekChart.setTouchEnabled(false); + } + } + + @Override + protected void setupLegend(Chart chart) { + List legendEntries = createLegendEntries(chart); + chart.getLegend().setCustom(legendEntries); + chart.getLegend().setTextColor(LEGEND_TEXT_COLOR); + chart.getLegend().setWordWrapEnabled(true); + chart.getLegend().setHorizontalAlignment(Legend.LegendHorizontalAlignment.CENTER); + } + + @Override + protected void renderCharts() { + mWeekChart.invalidate(); + mStressLevelsPieChart.invalidate(); + } + + private int getRangeDays() { + return GBApplication.getPrefs().getBoolean("charts_range", true) ? 30 : 7; + } + + ValueFormatter getYAxisFormatter() { + return new ValueFormatter() { + @Override + public String getFormattedValue(float value) { + return DateTimeUtils.minutesToHHMM((int) value); + } + }; + } + + @Override + protected boolean isSingleDay() { + return false; + } + + protected static class MyChartsData extends ChartsData { + private final DefaultChartsData weekBeforeData; + + public MyChartsData(DefaultChartsData weekBeforeData) { + this.weekBeforeData = weekBeforeData; + } + + public DefaultChartsData getWeekBeforeData() { + return weekBeforeData; + } + } + + private record MyStressWeeklyData(long totalRelaxed, + long totalMild, + long totalModerate, + long totalHigh, + long totalStressTime, + int averageStress) { + } +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardStressSimpleWidget.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardStressSimpleWidget.java index d1f1149235..8f8667fee2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardStressSimpleWidget.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/DashboardStressSimpleWidget.java @@ -21,7 +21,7 @@ import android.os.Bundle; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment; -import nodomain.freeyourgadget.gadgetbridge.activities.charts.StressChartFragment; +import nodomain.freeyourgadget.gadgetbridge.activities.charts.StressDailyFragment; import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.data.DashboardStressData; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; @@ -56,7 +56,7 @@ public class DashboardStressSimpleWidget extends AbstractGaugeWidget { return; } - final int color = StressChartFragment.StressType.fromStress( + final int color = StressDailyFragment.StressType.fromStress( stressData.value, stressData.ranges ).getColor(GBApplication.getContext()); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/data/DashboardStressData.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/data/DashboardStressData.java index 3106ae6b87..c40cc3d307 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/data/DashboardStressData.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/dashboard/data/DashboardStressData.java @@ -24,7 +24,7 @@ import java.util.List; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment; -import nodomain.freeyourgadget.gadgetbridge.activities.charts.StressChartFragment; +import nodomain.freeyourgadget.gadgetbridge.activities.charts.StressDailyFragment; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.StressSample; @@ -42,7 +42,7 @@ public class DashboardStressData implements Serializable { GBDevice stressDevice = null; double averageStress = -1; - final int[] totalTime = new int[StressChartFragment.StressType.values().length]; + final int[] totalTime = new int[StressDailyFragment.StressType.values().length]; try (DBHandler dbHandler = GBApplication.acquireDB()) { for (GBDevice dev : devices) { @@ -57,8 +57,8 @@ public class DashboardStressData implements Serializable { averageStress = samples.stream() .mapToInt(StressSample::getStress) .peek(stress -> { - final StressChartFragment.StressType stressType = StressChartFragment.StressType.fromStress(stress, stressRanges); - if (stressType != StressChartFragment.StressType.UNKNOWN) { + final StressDailyFragment.StressType stressType = StressDailyFragment.StressType.fromStress(stress, stressRanges); + if (stressType != StressDailyFragment.StressType.UNKNOWN) { totalTime[stressType.ordinal() - 1] += 60; } }) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/StressFragmentAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/StressFragmentAdapter.java new file mode 100644 index 0000000000..a3141d5187 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/StressFragmentAdapter.java @@ -0,0 +1,27 @@ +package nodomain.freeyourgadget.gadgetbridge.adapter; + +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; + +import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment; +import nodomain.freeyourgadget.gadgetbridge.activities.charts.StressDailyFragment; +import nodomain.freeyourgadget.gadgetbridge.activities.charts.StressPeriodFragment; + +public class StressFragmentAdapter extends NestedFragmentAdapter { + public StressFragmentAdapter(AbstractGBFragment fragment, FragmentManager childFragmentManager) { + super(fragment, childFragmentManager); + } + + @Override + public Fragment createFragment(int position) { + switch (position) { + case 0: + return new StressDailyFragment(); + case 1: + return StressPeriodFragment.newInstance(7); + case 2: + return StressPeriodFragment.newInstance(30); + } + return new StressDailyFragment(); + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_weekstress_chart.xml b/app/src/main/res/layout/fragment_weekstress_chart.xml new file mode 100644 index 0000000000..b7b847351d --- /dev/null +++ b/app/src/main/res/layout/fragment_weekstress_chart.xml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file