mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Add weekly/monthly stress charts
This commit is contained in:
-356
@@ -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 <https://www.gnu.org/licenses/>. */
|
|
||||||
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<AbstractWeekChartFragment.MyChartsData> {
|
|
||||||
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<BarData> 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<BarEntry> entries = new ArrayList<>();
|
|
||||||
ArrayList<String> labels = new ArrayList<String>();
|
|
||||||
|
|
||||||
long balance = 0;
|
|
||||||
long daily_balance = 0;
|
|
||||||
TOTAL_DAYS_FOR_AVERAGE=0;
|
|
||||||
List<Entry> sleepScoreEntities = new ArrayList<>();
|
|
||||||
final List<ILineDataSet> 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<? extends SleepScoreSample> 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<Entry> 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<SleepScoreSample> 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<? extends SleepScoreSample> provider = device.getDeviceCoordinator().getSleepScoreProvider(device, db.getDaoSession());
|
|
||||||
return (List<SleepScoreSample>) provider.getAllSamples(startTs * 1000L, endTs * 1000L);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected LineDataSet createSleepScoreDataSet(final List<Entry> 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<? extends ActivitySample> 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<? extends ActivitySample> 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<BarData> weekBeforeData;
|
|
||||||
|
|
||||||
MyChartsData(WeekChartsData<BarData> weekBeforeData) {
|
|
||||||
this.weekBeforeData = weekBeforeData;
|
|
||||||
}
|
|
||||||
|
|
||||||
WeekChartsData<BarData> getWeekBeforeData() {
|
|
||||||
return weekBeforeData;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected ActivityAmounts getActivityAmountsForDay(DBHandler db, Calendar day, GBDevice device) {
|
|
||||||
|
|
||||||
LimitedQueue<Integer, ActivityAmounts> 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<T extends ChartData<?>> extends DefaultChartsData<T> {
|
|
||||||
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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-1
@@ -177,7 +177,7 @@ public class ActivityChartsActivity extends AbstractChartsActivity {
|
|||||||
case "vo2max":
|
case "vo2max":
|
||||||
return new VO2MaxFragment();
|
return new VO2MaxFragment();
|
||||||
case "stress":
|
case "stress":
|
||||||
return new StressChartFragment();
|
return StressCollectionFragment.newInstance(enabledTabsList.size() == 1);
|
||||||
case "pai":
|
case "pai":
|
||||||
return new PaiChartFragment();
|
return new PaiChartFragment();
|
||||||
case "stepsweek":
|
case "stepsweek":
|
||||||
|
|||||||
+1
-1
@@ -53,7 +53,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
|
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||||
|
|
||||||
// Based on StressChartFragment
|
// Based on StressDailyFragment
|
||||||
|
|
||||||
public class Spo2ChartFragment extends AbstractChartFragment<Spo2ChartFragment.Spo2ChartsData> {
|
public class Spo2ChartFragment extends AbstractChartFragment<Spo2ChartFragment.Spo2ChartsData> {
|
||||||
protected static final Logger LOG = LoggerFactory.getLogger(Spo2ChartFragment.class);
|
protected static final Logger LOG = LoggerFactory.getLogger(Spo2ChartFragment.class);
|
||||||
|
|||||||
+44
@@ -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 <https://www.gnu.org/licenses/>. */
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-123
@@ -16,7 +16,6 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
@@ -29,8 +28,6 @@ import android.view.View;
|
|||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
|
|
||||||
import com.github.mikephil.charting.animation.Easing;
|
import com.github.mikephil.charting.animation.Easing;
|
||||||
import com.github.mikephil.charting.charts.Chart;
|
import com.github.mikephil.charting.charts.Chart;
|
||||||
import com.github.mikephil.charting.charts.LineChart;
|
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.GBApplication;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
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.impl.GBDevice;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
|
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||||
|
|
||||||
public class StressChartFragment extends AbstractChartFragment<StressChartFragment.StressChartsData> {
|
public class StressDailyFragment extends StressFragment<StressDailyFragment.StressChartsData> {
|
||||||
protected static final Logger LOG = LoggerFactory.getLogger(StressChartFragment.class);
|
protected static final Logger LOG = LoggerFactory.getLogger(StressDailyFragment.class);
|
||||||
|
|
||||||
private LineChart mStressChart;
|
private LineChart mStressChart;
|
||||||
private PieChart mStressLevelsPieChart;
|
private PieChart mStressLevelsPieChart;
|
||||||
@@ -82,27 +77,17 @@ public class StressChartFragment extends AbstractChartFragment<StressChartFragme
|
|||||||
private TextView stressChartHighTime;
|
private TextView stressChartHighTime;
|
||||||
private TextView stressDate;
|
private TextView stressDate;
|
||||||
|
|
||||||
private int BACKGROUND_COLOR;
|
|
||||||
private int DESCRIPTION_COLOR;
|
|
||||||
private int CHART_TEXT_COLOR;
|
|
||||||
private int LEGEND_TEXT_COLOR;
|
|
||||||
|
|
||||||
private String STRESS_AVERAGE_LABEL;
|
private String STRESS_AVERAGE_LABEL;
|
||||||
|
|
||||||
private final Prefs prefs = GBApplication.getPrefs();
|
private final Prefs prefs = GBApplication.getPrefs();
|
||||||
|
|
||||||
private final boolean CHARTS_SLEEP_RANGE_24H = prefs.getBoolean("chart_sleep_range_24h", false);
|
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);
|
private final boolean SHOW_CHARTS_AVERAGE = prefs.getBoolean("charts_show_average", true);
|
||||||
|
|
||||||
|
|
||||||
private boolean showStressLevelInPercents = false;
|
private boolean showStressLevelInPercents = false;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void init() {
|
protected void init() {
|
||||||
BACKGROUND_COLOR = GBApplication.getBackgroundColor(requireContext());
|
super.init();
|
||||||
LEGEND_TEXT_COLOR = DESCRIPTION_COLOR = GBApplication.getTextColor(requireContext());
|
|
||||||
CHART_TEXT_COLOR = GBApplication.getSecondaryTextColor(requireContext());
|
|
||||||
|
|
||||||
STRESS_AVERAGE_LABEL = requireContext().getString(R.string.charts_legend_stress_average);
|
STRESS_AVERAGE_LABEL = requireContext().getString(R.string.charts_legend_stress_average);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +101,7 @@ public class StressChartFragment extends AbstractChartFragment<StressChartFragme
|
|||||||
day.set(Calendar.SECOND, 0);
|
day.set(Calendar.SECOND, 0);
|
||||||
final int tsStart = (int) (day.getTimeInMillis() / 1000);
|
final int tsStart = (int) (day.getTimeInMillis() / 1000);
|
||||||
tsEnd = tsStart + 24 * 60 * 60 - 1;
|
tsEnd = tsStart + 24 * 60 * 60 - 1;
|
||||||
final List<? extends StressSample> samples = getSamples(db, device, tsStart, tsEnd);
|
final List<? extends StressSample> samples = getStressSamples(db, device, tsStart, tsEnd);
|
||||||
|
|
||||||
LOG.info("Got {} stress samples", samples.size());
|
LOG.info("Got {} stress samples", samples.size());
|
||||||
|
|
||||||
@@ -196,11 +181,6 @@ public class StressChartFragment extends AbstractChartFragment<StressChartFragme
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getTitle() {
|
|
||||||
return getString(R.string.menuitem_stress);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(final LayoutInflater inflater,
|
public View onCreateView(final LayoutInflater inflater,
|
||||||
final ViewGroup container,
|
final ViewGroup container,
|
||||||
@@ -277,14 +257,7 @@ public class StressChartFragment extends AbstractChartFragment<StressChartFragme
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void setupLegend(final Chart<?> chart) {
|
protected void setupLegend(final Chart<?> chart) {
|
||||||
final List<LegendEntry> legendEntries = new ArrayList<>(StressType.values().length + 1);
|
final List<LegendEntry> legendEntries = createLegendEntries(chart);
|
||||||
|
|
||||||
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) {
|
if (!CHARTS_SLEEP_RANGE_24H && SHOW_CHARTS_AVERAGE) {
|
||||||
final LegendEntry averageEntry = new LegendEntry();
|
final LegendEntry averageEntry = new LegendEntry();
|
||||||
@@ -303,54 +276,6 @@ public class StressChartFragment extends AbstractChartFragment<StressChartFragme
|
|||||||
mStressLevelsPieChart.invalidate();
|
mStressLevelsPieChart.invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<? extends StressSample> getSamples(final DBHandler db, final GBDevice device, int tsStart, int tsEnd) {
|
|
||||||
final DeviceCoordinator coordinator = device.getDeviceCoordinator();
|
|
||||||
final TimeSampleProvider<? extends StressSample> sampleProvider = coordinator.getStressSampleProvider(device, db.getDaoSession());
|
|
||||||
return sampleProvider.getAllSamples(tsStart * 1000L, tsEnd * 1000L);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void ensureStartAndEndSamples(final List<StressSample> 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 {
|
protected class StressChartsDataBuilder {
|
||||||
private static final int UNKNOWN_VAL = 2;
|
private static final int UNKNOWN_VAL = 2;
|
||||||
|
|
||||||
@@ -407,8 +332,6 @@ public class StressChartFragment extends AbstractChartFragment<StressChartFragme
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void processSample(final StressSample sample) {
|
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(), stressRanges);
|
final StressType stressType = StressType.fromStress(sample.getStress(), stressRanges);
|
||||||
final int ts = tsTranslation.shorten((int) (sample.getTimestamp() / 1000L));
|
final int ts = tsTranslation.shorten((int) (sample.getTimestamp() / 1000L));
|
||||||
|
|
||||||
@@ -442,8 +365,6 @@ public class StressChartFragment extends AbstractChartFragment<StressChartFragme
|
|||||||
} else {
|
} else {
|
||||||
set(ts, stressType, sample.getStress());
|
set(ts, stressType, sample.getStress());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
if (ts - previousTs > sampleRate * 10) {
|
if (ts - previousTs > sampleRate * 10) {
|
||||||
// More than 15 minutes since last sample
|
// More than 15 minutes since last sample
|
||||||
@@ -567,43 +488,4 @@ public class StressChartFragment extends AbstractChartFragment<StressChartFragme
|
|||||||
return totalStressTime;
|
return totalStressTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+178
@@ -0,0 +1,178 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
|
import com.github.mikephil.charting.charts.Chart;
|
||||||
|
import com.github.mikephil.charting.components.LegendEntry;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
abstract class StressFragment<D extends ChartsData> extends AbstractChartFragment<D> {
|
||||||
|
|
||||||
|
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<? extends StressSample> getStressSamples(DBHandler db, GBDevice device, int tsStart, int tsEnd) {
|
||||||
|
final DeviceCoordinator coordinator = device.getDeviceCoordinator();
|
||||||
|
final TimeSampleProvider<? extends StressSample> sampleProvider = coordinator.getStressSampleProvider(device, db.getDaoSession());
|
||||||
|
return sampleProvider.getAllSamples(tsStart * 1000L, tsEnd * 1000L);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void ensureStartAndEndSamples(final List<StressSample> 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<LegendEntry> createLegendEntries(Chart<?> chart) {
|
||||||
|
List<LegendEntry> 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<StressType, Integer> calculateStressTotals(List<? extends StressSample> samples, int[] stressRanges, int sampleRate) {
|
||||||
|
Map<StressType, Integer> 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<? extends StressSample> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+513
@@ -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 <https://www.gnu.org/licenses/>. */
|
||||||
|
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<StressPeriodFragment.MyChartsData> {
|
||||||
|
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<BarData> 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<PieEntry> pieEntries = new ArrayList<>();
|
||||||
|
List<Integer> 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<? extends StressSample> samples = getStressSamples(db, device,
|
||||||
|
(int) (dayStart.getTimeInMillis() / 1000),
|
||||||
|
(int) (dayEnd.getTimeInMillis() / 1000));
|
||||||
|
|
||||||
|
if (!samples.isEmpty()) {
|
||||||
|
TOTAL_DAYS_FOR_AVERAGE++;
|
||||||
|
|
||||||
|
Map<StressType, Integer> 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<BarData> 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<BarEntry> entries = new ArrayList<>();
|
||||||
|
ArrayList<String> 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<? extends StressSample> samples = getStressSamples(db, device,
|
||||||
|
(int) (dayStart.getTimeInMillis() / 1000),
|
||||||
|
(int) (dayEnd.getTimeInMillis() / 1000));
|
||||||
|
|
||||||
|
Map<StressType, Integer> 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<IBarDataSet> 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<LegendEntry> 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<BarData> weekBeforeData;
|
||||||
|
|
||||||
|
public MyChartsData(DefaultChartsData<BarData> weekBeforeData) {
|
||||||
|
this.weekBeforeData = weekBeforeData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DefaultChartsData<BarData> getWeekBeforeData() {
|
||||||
|
return weekBeforeData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record MyStressWeeklyData(long totalRelaxed,
|
||||||
|
long totalMild,
|
||||||
|
long totalModerate,
|
||||||
|
long totalHigh,
|
||||||
|
long totalStressTime,
|
||||||
|
int averageStress) {
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -21,7 +21,7 @@ import android.os.Bundle;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
|
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.activities.dashboard.data.DashboardStressData;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ public class DashboardStressSimpleWidget extends AbstractGaugeWidget {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final int color = StressChartFragment.StressType.fromStress(
|
final int color = StressDailyFragment.StressType.fromStress(
|
||||||
stressData.value,
|
stressData.value,
|
||||||
stressData.ranges
|
stressData.ranges
|
||||||
).getColor(GBApplication.getContext());
|
).getColor(GBApplication.getContext());
|
||||||
|
|||||||
+4
-4
@@ -24,7 +24,7 @@ import java.util.List;
|
|||||||
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
|
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.database.DBHandler;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
|
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
|
||||||
@@ -42,7 +42,7 @@ public class DashboardStressData implements Serializable {
|
|||||||
GBDevice stressDevice = null;
|
GBDevice stressDevice = null;
|
||||||
double averageStress = -1;
|
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()) {
|
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||||
for (GBDevice dev : devices) {
|
for (GBDevice dev : devices) {
|
||||||
@@ -57,8 +57,8 @@ public class DashboardStressData implements Serializable {
|
|||||||
averageStress = samples.stream()
|
averageStress = samples.stream()
|
||||||
.mapToInt(StressSample::getStress)
|
.mapToInt(StressSample::getStress)
|
||||||
.peek(stress -> {
|
.peek(stress -> {
|
||||||
final StressChartFragment.StressType stressType = StressChartFragment.StressType.fromStress(stress, stressRanges);
|
final StressDailyFragment.StressType stressType = StressDailyFragment.StressType.fromStress(stress, stressRanges);
|
||||||
if (stressType != StressChartFragment.StressType.UNKNOWN) {
|
if (stressType != StressDailyFragment.StressType.UNKNOWN) {
|
||||||
totalTime[stressType.ordinal() - 1] += 60;
|
totalTime[stressType.ordinal() - 1] += 60;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
+27
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.charts.ActivityChartsActivity$PlaceholderFragment">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/stress_dates"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:layout_marginTop="15dp"
|
||||||
|
android:layout_marginBottom="10dp" />
|
||||||
|
|
||||||
|
<com.github.mikephil.charting.charts.PieChart
|
||||||
|
android:id="@+id/stress_pie_chart"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="200dp"
|
||||||
|
android:layout_marginTop="15dp"
|
||||||
|
android:layout_marginBottom="15dp" />
|
||||||
|
|
||||||
|
<GridLayout
|
||||||
|
android:background="@color/gauge_line_color"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:columnCount="2">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
style="@style/GridTile"
|
||||||
|
android:layout_marginBottom="0dp">
|
||||||
|
<View
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="5dp"
|
||||||
|
android:background="@color/chart_stress_relaxed"
|
||||||
|
android:layout_marginBottom="10dp" />
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/stress_chart_relaxed_time"
|
||||||
|
android:text="@string/stats_empty_value"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/stress_relaxed"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
style="@style/GridTile"
|
||||||
|
android:layout_marginBottom="0dp">
|
||||||
|
<View
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="5dp"
|
||||||
|
android:background="@color/chart_stress_mild"
|
||||||
|
android:layout_marginBottom="10dp" />
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/stress_chart_mild_time"
|
||||||
|
android:text="@string/stats_empty_value"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/stress_mild"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
style="@style/GridTile"
|
||||||
|
android:layout_marginBottom="0dp">
|
||||||
|
<View
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="5dp"
|
||||||
|
android:background="@color/chart_stress_moderate"
|
||||||
|
android:layout_marginBottom="10dp" />
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/stress_chart_moderate_time"
|
||||||
|
android:text="@string/stats_empty_value"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/stress_moderate"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
style="@style/GridTile"
|
||||||
|
android:layout_marginBottom="0dp">
|
||||||
|
<View
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="5dp"
|
||||||
|
android:background="@color/chart_stress_high"
|
||||||
|
android:layout_marginBottom="10dp" />
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/stress_chart_high_time"
|
||||||
|
android:text="@string/stats_empty_value"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/stress_high"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
</GridLayout>
|
||||||
|
|
||||||
|
<com.github.mikephil.charting.charts.BarChart
|
||||||
|
android:id="@+id/weekstresschart"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="350dp"
|
||||||
|
android:layout_marginTop="15dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
Reference in New Issue
Block a user