/* Copyright (C) 2017-2024 Andreas Shimokawa, Carsten Pfeiffer, 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.os.Build; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; import android.widget.TextView; import com.github.mikephil.charting.charts.Chart; import com.github.mikephil.charting.charts.LineChart; 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.formatter.ValueFormatter; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.time.DateUtils; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.concurrent.TimeUnit; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmount; import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmounts; import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser; import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; public class WeekSleepChartFragment extends AbstractWeekChartFragment { private TextView awakeSleepTimeText; private LinearLayout awakeSleepTimeTextWrapper; private TextView remSleepTimeText; private LinearLayout remSleepTimeTextWrapper; private TextView deepSleepTimeText; private TextView lightSleepTimeText; private TextView sleepDatesText; private MySleepWeeklyData mySleepWeeklyData; private LinearLayout sleepScoreWrapper; private LineChart sleepScoreChart; public static WeekSleepChartFragment newInstance ( int totalDays ) { WeekSleepChartFragment fragmentFirst = new WeekSleepChartFragment(); Bundle args = new Bundle(); args.putInt("totalDays", totalDays); fragmentFirst.setArguments(args); return fragmentFirst; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); TOTAL_DAYS = getArguments() != null ? getArguments().getInt("totalDays") : 0; } private MySleepWeeklyData getMySleepWeeklyData(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 awakeWeeklyTotal = 0; long remWeeklyTotal = 0; long deepWeeklyTotal = 0; long lightWeeklyTotal = 0; for (int counter = 0; counter < TOTAL_DAYS; counter++) { ActivityAmounts amounts = getActivityAmountsForDay(db, day, device); if (calculateBalance(amounts) > 0) { TOTAL_DAYS_FOR_AVERAGE++; } float[] totalAmounts = getTotalsForActivityAmounts(amounts); int i = 0; deepWeeklyTotal += (long) totalAmounts[i++]; lightWeeklyTotal += (long) totalAmounts[i++]; if (supportsRemSleep(getChartsHost().getDevice())) { remWeeklyTotal += (long) totalAmounts[i++]; } if (supportsAwakeSleep(getChartsHost().getDevice())) { awakeWeeklyTotal += (long) totalAmounts[i++]; } day.add(Calendar.DATE, 1); } return new MySleepWeeklyData(awakeWeeklyTotal, remWeeklyTotal, deepWeeklyTotal, lightWeeklyTotal); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { mLocale = getResources().getConfiguration().locale; View rootView = inflater.inflate(R.layout.fragment_weeksleep_chart, container, false); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { rootView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> { getChartsHost().enableSwipeRefresh(scrollY == 0); }); } final int goal = getGoal(); if (goal >= 0) { mTargetValue = goal; } mWeekChart = rootView.findViewById(R.id.weekstepschart); sleepScoreWrapper = rootView.findViewById(R.id.sleep_score_wrapper); sleepScoreChart = rootView.findViewById(R.id.sleep_score_chart); remSleepTimeText = rootView.findViewById(R.id.sleep_chart_legend_rem_time); remSleepTimeTextWrapper = rootView.findViewById(R.id.sleep_chart_legend_rem_time_wrapper); awakeSleepTimeText = rootView.findViewById(R.id.sleep_chart_legend_awake_time); awakeSleepTimeTextWrapper = rootView.findViewById(R.id.sleep_chart_legend_awake_time_wrapper); deepSleepTimeText = rootView.findViewById(R.id.sleep_chart_legend_deep_time); lightSleepTimeText = rootView.findViewById(R.id.sleep_chart_legend_light_time); sleepDatesText = rootView.findViewById(R.id.sleep_dates); mBalanceView = rootView.findViewById(R.id.balance); if (!supportsSleepScore()) { sleepScoreWrapper.setVisibility(View.GONE); } else { setupSleepScoreChart(); } setupWeekChart(); // refresh immediately instead of use refreshIfVisible(), for perceived performance refresh(); return rootView; } protected void setupWeekChart() { super.setupWeekChart(); if (TOTAL_DAYS > 7) { mWeekChart.setRenderer(new AngledLabelsChartRenderer(mWeekChart, mWeekChart.getAnimator(), mWeekChart.getViewPortHandler())); } else { mWeekChart.setScaleEnabled(false); mWeekChart.setTouchEnabled(false); } } @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); if (supportsSleepScore()) { sleepScoreChart.setData(null); sleepScoreChart.getXAxis().setValueFormatter(mcd.getWeekBeforeData().getXValueFormatter()); sleepScoreChart.getLegend().setTextColor(LEGEND_TEXT_COLOR); sleepScoreChart.setData(mcd.getWeekBeforeData().getSleepScoreData()); } // The last value is for awake time, which we do not want to include in the "total sleep time" final int barIgnoreLast = supportsAwakeSleep(getChartsHost().getDevice()) ? 1 : 0; mWeekChart.getBarData().setValueFormatter(new BarChartStackedTimeValueFormatter(false, "", 0, barIgnoreLast)); if (TOTAL_DAYS_FOR_AVERAGE > 0) { float avgDeep = Math.abs(this.mySleepWeeklyData.getTotalDeep() / TOTAL_DAYS_FOR_AVERAGE); deepSleepTimeText.setText(DateTimeUtils.formatDurationHoursMinutes((int) avgDeep, TimeUnit.MINUTES)); float avgLight = Math.abs(this.mySleepWeeklyData.getTotalLight() / TOTAL_DAYS_FOR_AVERAGE); lightSleepTimeText.setText(DateTimeUtils.formatDurationHoursMinutes((int) avgLight, TimeUnit.MINUTES)); float avgRem = Math.abs(this.mySleepWeeklyData.getTotalRem() / TOTAL_DAYS_FOR_AVERAGE); remSleepTimeText.setText(DateTimeUtils.formatDurationHoursMinutes((int) avgRem, TimeUnit.MINUTES)); float avgAwake = Math.abs(this.mySleepWeeklyData.getTotalAwake() / TOTAL_DAYS_FOR_AVERAGE); awakeSleepTimeText.setText(DateTimeUtils.formatDurationHoursMinutes((int) avgAwake, TimeUnit.MINUTES)); } else { deepSleepTimeText.setText("-"); lightSleepTimeText.setText("-"); remSleepTimeText.setText("-"); awakeSleepTimeText.setText("-"); } if (!supportsRemSleep(getChartsHost().getDevice())) { remSleepTimeTextWrapper.setVisibility(View.GONE); } if (!supportsAwakeSleep(getChartsHost().getDevice())) { awakeSleepTimeTextWrapper.setVisibility(View.GONE); } Date to = new Date((long) this.getTSEnd() * 1000); Date from = DateUtils.addDays(to,-(TOTAL_DAYS - 1)); String toFormattedDate = new SimpleDateFormat("E, MMM dd").format(to); String fromFormattedDate = new SimpleDateFormat("E, MMM dd").format(from); sleepDatesText.setText(fromFormattedDate + " - " + toFormattedDate); mBalanceView.setText(mcd.getWeekBeforeData().getBalanceMessage()); } @Override protected MyChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) { Calendar day = Calendar.getInstance(); day.setTime(chartsHost.getEndDate()); //NB: we could have omitted the day, but this way we can move things to the past easily WeekChartsData weekBeforeData = refreshWeekBeforeData(db, mWeekChart, day, device); mySleepWeeklyData = getMySleepWeeklyData(db, day, device); return new MyChartsData(weekBeforeData); } private void setupSleepScoreChart() { final XAxis xAxisBottom = sleepScoreChart.getXAxis(); xAxisBottom.setPosition(XAxis.XAxisPosition.BOTTOM); xAxisBottom.setDrawLabels(true); xAxisBottom.setDrawGridLines(false); xAxisBottom.setEnabled(true); xAxisBottom.setDrawLimitLinesBehindData(true); xAxisBottom.setTextColor(CHART_TEXT_COLOR); xAxisBottom.setAxisMinimum(0f); xAxisBottom.setAxisMaximum(TOTAL_DAYS-1); xAxisBottom.setGranularity(1f); xAxisBottom.setGranularityEnabled(true); final YAxis yAxisLeft = sleepScoreChart.getAxisLeft(); yAxisLeft.setDrawGridLines(true); yAxisLeft.setAxisMaximum(100); yAxisLeft.setAxisMinimum(0); yAxisLeft.setDrawTopYLabelEntry(true); yAxisLeft.setEnabled(true); yAxisLeft.setTextColor(CHART_TEXT_COLOR); final YAxis yAxisRight = sleepScoreChart.getAxisRight(); yAxisRight.setEnabled(true); yAxisRight.setDrawLabels(false); yAxisRight.setDrawGridLines(false); yAxisRight.setDrawAxisLine(true); sleepScoreChart.setDoubleTapToZoomEnabled(false); sleepScoreChart.getDescription().setEnabled(false); if (TOTAL_DAYS <= 7) { sleepScoreChart.setScaleEnabled(false); sleepScoreChart.setTouchEnabled(false); } } @Override protected void renderCharts() { mWeekChart.invalidate(); sleepScoreChart.invalidate(); } @Override public String getTitle() { if (GBApplication.getPrefs().getBoolean("charts_range", true)) { return getString(R.string.weeksleepchart_sleep_a_month); } else{ return getString(R.string.weeksleepchart_sleep_a_week); } } @Override String getPieDescription(int targetValue) { return getString(R.string.weeksleepchart_today_sleep_description, DateTimeUtils.formatDurationHoursMinutes(targetValue, TimeUnit.MINUTES)); } @Override int getGoal() { return GBApplication.getPrefs().getInt(ActivityUser.PREF_USER_SLEEP_DURATION, 8) * 60; } @Override int getOffsetHours() { return -12; } @Override protected long calculateBalance(ActivityAmounts activityAmounts) { long balance = 0; for (ActivityAmount amount : activityAmounts.getAmounts()) { if (amount.getActivityKind() == ActivityKind.DEEP_SLEEP || amount.getActivityKind() == ActivityKind.LIGHT_SLEEP || amount.getActivityKind() == ActivityKind.REM_SLEEP) { balance += amount.getTotalSeconds(); } } return (int) (balance / 60); } @Override protected String getBalanceMessage(long balance, int targetValue) { if (balance > 0) { final long totalBalance = balance - ((long)targetValue * TOTAL_DAYS_FOR_AVERAGE); if (totalBalance > 0) return getString(R.string.overslept, getHM(totalBalance)); else return getString(R.string.lack_of_sleep, getHM(Math.abs(totalBalance))); } else return getString(R.string.no_data); } @Override float[] getTotalsForActivityAmounts(ActivityAmounts activityAmounts) { long totalSecondsDeepSleep = 0; long totalSecondsLightSleep = 0; long totalSecondsRemSleep = 0; long totalSecondsAwakeSleep = 0; for (ActivityAmount amount : activityAmounts.getAmounts()) { if (amount.getActivityKind() == ActivityKind.DEEP_SLEEP) { totalSecondsDeepSleep += amount.getTotalSeconds(); } else if (amount.getActivityKind() == ActivityKind.LIGHT_SLEEP) { totalSecondsLightSleep += amount.getTotalSeconds(); } else if (amount.getActivityKind() == ActivityKind.REM_SLEEP) { totalSecondsRemSleep += amount.getTotalSeconds(); } else if (amount.getActivityKind() == ActivityKind.AWAKE_SLEEP) { totalSecondsAwakeSleep += amount.getTotalSeconds(); } } int totalMinutesDeepSleep = (int) (totalSecondsDeepSleep / 60); int totalMinutesLightSleep = (int) (totalSecondsLightSleep / 60); int totalMinutesRemSleep = (int) (totalSecondsRemSleep / 60); int totalMinutesAwakeSleep = (int) (totalSecondsAwakeSleep / 60); float[] activityAmountsTotals = {totalMinutesDeepSleep, totalMinutesLightSleep}; if (supportsRemSleep(getChartsHost().getDevice())) { activityAmountsTotals = ArrayUtils.add(activityAmountsTotals, totalMinutesRemSleep); } if (supportsAwakeSleep(getChartsHost().getDevice())) { activityAmountsTotals = ArrayUtils.add(activityAmountsTotals, totalMinutesAwakeSleep); } return activityAmountsTotals; } @Override protected String formatPieValue(long value) { return DateTimeUtils.formatDurationHoursMinutes(value, TimeUnit.MINUTES); } @Override String[] getPieLabels() { String[] labels = { getString(R.string.abstract_chart_fragment_kind_deep_sleep), getString(R.string.abstract_chart_fragment_kind_light_sleep) }; if (supportsRemSleep(getChartsHost().getDevice())) { labels = ArrayUtils.add(labels, getString(R.string.abstract_chart_fragment_kind_rem_sleep)); } if (supportsAwakeSleep(getChartsHost().getDevice())) { labels = ArrayUtils.add(labels, getString(R.string.abstract_chart_fragment_kind_awake_sleep)); } return labels; } @Override ValueFormatter getPieValueFormatter() { return new ValueFormatter() { @Override public String getFormattedValue(float value) { return formatPieValue((long) value); } }; } @Override ValueFormatter getBarValueFormatter() { return new ValueFormatter() { @Override public String getFormattedValue(float value) { return DateTimeUtils.minutesToHHMM((int) value); } }; } @Override ValueFormatter getYAxisFormatter() { return new ValueFormatter() { @Override public String getFormattedValue(float value) { return DateTimeUtils.minutesToHHMM((int) value); } }; } @Override int[] getColors() { int[] colors = {akDeepSleep.color, akLightSleep.color}; if (supportsRemSleep(getChartsHost().getDevice())) { colors = ArrayUtils.add(colors, akRemSleep.color); } if (supportsAwakeSleep(getChartsHost().getDevice())) { colors = ArrayUtils.add(colors, akAwakeSleep.color); } return colors; } @Override protected void setupLegend(Chart chart) { List legendEntries = new ArrayList<>(2); LegendEntry lightSleepEntry = new LegendEntry(); lightSleepEntry.label = getActivity().getString(R.string.sleep_colored_stats_light); lightSleepEntry.formColor = akLightSleep.color; legendEntries.add(lightSleepEntry); LegendEntry deepSleepEntry = new LegendEntry(); deepSleepEntry.label = getActivity().getString(R.string.sleep_colored_stats_deep); deepSleepEntry.formColor = akDeepSleep.color; legendEntries.add(deepSleepEntry); if (supportsRemSleep(getChartsHost().getDevice())) { LegendEntry remSleepEntry = new LegendEntry(); remSleepEntry.label = getActivity().getString(R.string.sleep_colored_stats_rem); remSleepEntry.formColor = akRemSleep.color; legendEntries.add(remSleepEntry); } if (supportsAwakeSleep(getChartsHost().getDevice())) { LegendEntry awakeSleepEntry = new LegendEntry(); awakeSleepEntry.label = getActivity().getString(R.string.abstract_chart_fragment_kind_awake_sleep); awakeSleepEntry.formColor = akAwakeSleep.color; legendEntries.add(awakeSleepEntry); } chart.getLegend().setCustom(legendEntries); chart.getLegend().setTextColor(LEGEND_TEXT_COLOR); chart.getLegend().setWordWrapEnabled(true); chart.getLegend().setHorizontalAlignment(Legend.LegendHorizontalAlignment.CENTER); } private String getHM(long value) { return DateTimeUtils.formatDurationHoursMinutes(value, TimeUnit.MINUTES); } @Override String getAverage(float value) { return getHM((long)value); } private static class MySleepWeeklyData { private long totalAwake; private long totalRem; private long totalDeep; private long totalLight; private int totalDaysForAverage; public MySleepWeeklyData(long totalAwake, long totalRem, long totalDeep, long totalLight) { this.totalDeep = totalDeep; this.totalRem = totalRem; this.totalAwake = totalAwake; this.totalLight = totalLight; this.totalDaysForAverage = 0; } public long getTotalAwake() { return this.totalAwake; } public long getTotalRem() { return this.totalRem; } public long getTotalDeep() { return this.totalDeep; } public long getTotalLight() { return this.totalLight; } public int getTotalDaysForAverage() { return this.totalDaysForAverage; } } }