/* Copyright (C) 2015-2020 0nse, Andreas Shimokawa, Carsten Pfeiffer, Daniele Gobbetti, Dikay900, Pavel Elagin, vanous, walkjivefly This file is part of Gadgetbridge. Gadgetbridge is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Gadgetbridge is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.activities.charts; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.AsyncTask; import android.os.Bundle; import android.util.TypedValue; import android.view.View; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.fragment.app.FragmentActivity; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import com.github.mikephil.charting.charts.BarChart; import com.github.mikephil.charting.charts.BarLineChartBase; import com.github.mikephil.charting.charts.Chart; import com.github.mikephil.charting.components.YAxis; 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.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.HashSet; import java.util.List; import java.util.Set; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment; import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils; import nodomain.freeyourgadget.gadgetbridge.database.DBAccess; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; /** * A base class fragment to be used with ChartsActivity. The fragment can supply * a title to be displayed in the activity by returning non-null in #getTitle() * Broadcast events can be received by overriding #onReceive(Context,Intent). * The chart can be refreshed by calling #refresh() * Implement refreshInBackground(DBHandler, GBDevice) to fetch the samples from the DB, * and add the samples to the chart. The actual rendering, which must be performed in the UI * thread, must be done in #renderCharts(). * Access functionality of the hosting activity with #getHost() *

* The hosting ChartsHost activity provides a section for displaying a date or date range * being the basis for the chart, as well as two buttons for moving backwards and forward * in time. The date is held by the activity, so that it can be shared by multiple chart * fragments. It is still the responsibility of the (currently visible) chart fragment * to set the desired date in the ChartsActivity via #setDateRange(Date,Date). * The default implementations #handleDatePrev(Date,Date) and #handleDateNext(Date,Date) * shift the date by one day. */ public abstract class AbstractChartFragment extends AbstractGBFragment { protected final int ANIM_TIME = 250; private static final Logger LOG = LoggerFactory.getLogger(AbstractChartFragment.class); private final Set mIntentFilterActions; private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { AbstractChartFragment.this.onReceive(context, intent); } }; private boolean mChartDirty = true; private AsyncTask refreshTask; public boolean isChartDirty() { return mChartDirty; } @Override public abstract String getTitle(); public boolean supportsHeartrate(GBDevice device) { DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device); return coordinator != null && coordinator.supportsHeartRateMeasurement(device); } protected static final class ActivityConfig { public final int type; public final String label; public final Integer color; public ActivityConfig(int kind, String label, Integer color) { this.type = kind; this.label = label; this.color = color; } } protected ActivityConfig akActivity; protected ActivityConfig akLightSleep; protected ActivityConfig akDeepSleep; protected ActivityConfig akNotWorn; protected int BACKGROUND_COLOR; protected int DESCRIPTION_COLOR; protected int CHART_TEXT_COLOR; protected int LEGEND_TEXT_COLOR; protected int HEARTRATE_COLOR; protected int HEARTRATE_FILL_COLOR; protected int AK_ACTIVITY_COLOR; protected int AK_DEEP_SLEEP_COLOR; protected int AK_LIGHT_SLEEP_COLOR; protected int AK_NOT_WORN_COLOR; protected String HEARTRATE_LABEL; protected String HEARTRATE_AVERAGE_LABEL; protected AbstractChartFragment(String... intentFilterActions) { mIntentFilterActions = new HashSet<>(); if (intentFilterActions != null) { mIntentFilterActions.addAll(Arrays.asList(intentFilterActions)); } mIntentFilterActions.add(ChartsHost.DATE_NEXT_DAY); mIntentFilterActions.add(ChartsHost.DATE_PREV_DAY); mIntentFilterActions.add(ChartsHost.DATE_NEXT_WEEK); mIntentFilterActions.add(ChartsHost.DATE_PREV_WEEK); mIntentFilterActions.add(ChartsHost.DATE_NEXT_MONTH); mIntentFilterActions.add(ChartsHost.DATE_PREV_MONTH); mIntentFilterActions.add(ChartsHost.REFRESH); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); init(); IntentFilter filter = new IntentFilter(); for (String action : mIntentFilterActions) { filter.addAction(action); } LocalBroadcastManager.getInstance(getActivity()).registerReceiver(mReceiver, filter); } protected void init() { Prefs prefs = GBApplication.getPrefs(); TypedValue runningColor = new TypedValue(); BACKGROUND_COLOR = GBApplication.getBackgroundColor(getContext()); LEGEND_TEXT_COLOR = DESCRIPTION_COLOR = GBApplication.getTextColor(getContext()); CHART_TEXT_COLOR = ContextCompat.getColor(getContext(), R.color.secondarytext); if (prefs.getBoolean("chart_heartrate_color", false)) { HEARTRATE_COLOR = ContextCompat.getColor(getContext(), R.color.chart_heartrate_alternative); }else{ HEARTRATE_COLOR = ContextCompat.getColor(getContext(), R.color.chart_heartrate); } HEARTRATE_FILL_COLOR = ContextCompat.getColor(getContext(), R.color.chart_heartrate_fill); getContext().getTheme().resolveAttribute(R.attr.chart_activity, runningColor, true); AK_ACTIVITY_COLOR = runningColor.data; getContext().getTheme().resolveAttribute(R.attr.chart_deep_sleep, runningColor, true); AK_DEEP_SLEEP_COLOR = runningColor.data; getContext().getTheme().resolveAttribute(R.attr.chart_light_sleep, runningColor, true); AK_LIGHT_SLEEP_COLOR = runningColor.data; getContext().getTheme().resolveAttribute(R.attr.chart_not_worn, runningColor, true); AK_NOT_WORN_COLOR = runningColor.data; HEARTRATE_LABEL = getContext().getString(R.string.charts_legend_heartrate); HEARTRATE_AVERAGE_LABEL = getContext().getString(R.string.charts_legend_heartrate_average); akActivity = new ActivityConfig(ActivityKind.TYPE_ACTIVITY, getString(R.string.abstract_chart_fragment_kind_activity), AK_ACTIVITY_COLOR); akLightSleep = new ActivityConfig(ActivityKind.TYPE_LIGHT_SLEEP, getString(R.string.abstract_chart_fragment_kind_light_sleep), AK_LIGHT_SLEEP_COLOR); akDeepSleep = new ActivityConfig(ActivityKind.TYPE_DEEP_SLEEP, getString(R.string.abstract_chart_fragment_kind_deep_sleep), AK_DEEP_SLEEP_COLOR); akNotWorn = new ActivityConfig(ActivityKind.TYPE_NOT_WORN, getString(R.string.abstract_chart_fragment_kind_not_worn), AK_NOT_WORN_COLOR); } private void setStartDate(Date date) { getChartsHost().setStartDate(date); } @Nullable protected ChartsHost getChartsHost() { return (ChartsHost) getActivity(); } private void setEndDate(Date date) { getChartsHost().setEndDate(date); } public Date getStartDate() { return getChartsHost().getStartDate(); } public Date getEndDate() { return getChartsHost().getEndDate(); } /** * Called when this fragment has been fully scrolled into the activity. * * @see #isVisibleInActivity() * @see #onMadeInvisibleInActivity() */ @Override protected void onMadeVisibleInActivity() { super.onMadeVisibleInActivity(); showDateBar(true); if (isChartDirty()) { refresh(); } } protected void showDateBar(boolean show) { getChartsHost().getDateBar().setVisibility(show ? View.VISIBLE : View.GONE); } @Override public void onDestroy() { super.onDestroy(); LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(mReceiver); } protected void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (ChartsHost.REFRESH.equals(action)) { refresh(); } else if (ChartsHost.DATE_NEXT_DAY.equals(action)) { handleDate(getStartDate(), getEndDate(),+1); } else if (ChartsHost.DATE_PREV_DAY.equals(action)) { handleDate(getStartDate(), getEndDate(),-1); } else if (ChartsHost.DATE_NEXT_WEEK.equals(action)) { handleDate(getStartDate(), getEndDate(),+7); } else if (ChartsHost.DATE_PREV_WEEK.equals(action)) { handleDate(getStartDate(), getEndDate(),-7); } else if (ChartsHost.DATE_NEXT_MONTH.equals(action)) { //calculate dates to jump by month but keep subsequent logic working int time1 = DateTimeUtils.shiftMonths((int )(getStartDate().getTime()/1000), 1); int time2 = DateTimeUtils.shiftMonths((int )(getEndDate().getTime()/1000), 1); Date date1 = DateTimeUtils.shiftByDays(new Date(time1 * 1000L), 30); Date date2 = DateTimeUtils.shiftByDays(new Date(time2 * 1000L), 30); handleDate(date1, date2,-30); } else if (ChartsHost.DATE_PREV_MONTH.equals(action)) { int time1 = DateTimeUtils.shiftMonths((int )(getStartDate().getTime()/1000), -1); int time2 = DateTimeUtils.shiftMonths((int )(getEndDate().getTime()/1000), -1); Date date1 = DateTimeUtils.shiftByDays(new Date(time1 * 1000L), -30); Date date2 = DateTimeUtils.shiftByDays(new Date(time2 * 1000L), -30); handleDate(date1, date2,30); } } /** * Default implementation shifts the dates by one day, if visible * and calls #refreshIfVisible(). * * @param startDate * @param endDate * @param Offset */ protected void handleDate(Date startDate, Date endDate, Integer Offset) { if (isVisibleInActivity()) { if (!shiftDates(startDate, endDate, Offset)) { return; } } refreshIfVisible(); } protected void refreshIfVisible() { if (isVisibleInActivity()) { refresh(); } else { mChartDirty = true; } } /** * Shifts the given dates by offset days. offset may be positive or negative. * * @param startDate * @param endDate * @param offset a positive or negative number of days to shift the dates * @return true if the shift was successful and false otherwise */ protected boolean shiftDates(Date startDate, Date endDate, int offset) { Date newStart = DateTimeUtils.shiftByDays(startDate, offset); Date newEnd = DateTimeUtils.shiftByDays(endDate, offset); Date now = new Date(); if (newEnd.after(now)) { //allow to jump to the end (now) if week/month reach after now newEnd=now; newStart=DateTimeUtils.shiftByDays(now,-1); } return setDateRange(newStart, newEnd); } protected Integer getColorFor(int activityKind) { switch (activityKind) { case ActivityKind.TYPE_DEEP_SLEEP: return akDeepSleep.color; case ActivityKind.TYPE_LIGHT_SLEEP: return akLightSleep.color; case ActivityKind.TYPE_ACTIVITY: return akActivity.color; } return akActivity.color; } protected SampleProvider getProvider(DBHandler db, GBDevice device) { DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(device); return coordinator.getSampleProvider(device, db.getDaoSession()); } /** * Returns all kinds of samples for the given device. * To be called from a background thread. * * @param device * @param tsFrom * @param tsTo */ protected List getAllSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) { SampleProvider provider = getProvider(db, device); return provider.getAllActivitySamples(tsFrom, tsTo); } protected List getActivitySamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) { SampleProvider provider = getProvider(db, device); return provider.getActivitySamples(tsFrom, tsTo); } protected List getSleepSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) { SampleProvider provider = getProvider(db, device); return provider.getSleepSamples(tsFrom, tsTo); } protected void configureChartDefaults(Chart chart) { chart.getXAxis().setValueFormatter(new TimestampValueFormatter()); chart.getDescription().setText(""); // if enabled, the chart will always start at zero on the y-axis chart.setNoDataText(getString(R.string.chart_no_data_synchronize)); // disable value highlighting chart.setHighlightPerTapEnabled(false); // enable touch gestures chart.setTouchEnabled(true); // commented out: this has weird bugs/side-effects at least on WeekStepsCharts // where only the first Day-label is drawn, because AxisRenderer.computeAxisValues(float,float) // appears to have an overflow when calculating 'n' (number of entries) // chart.getXAxis().setGranularity(60*5); setupLegend(chart); } protected void configureBarLineChartDefaults(BarLineChartBase chart) { configureChartDefaults(chart); if (chart instanceof BarChart) { ((BarChart) chart).setFitBars(true); } // enable scaling and dragging chart.setDragEnabled(true); chart.setScaleEnabled(true); // if disabled, scaling can be done on x- and y-axis separately // chart.setPinchZoom(true); chart.setDrawGridBackground(false); } /** * This method will invoke a background task to read the data from the * database, analyze it, prepare it for the charts and eventually call * #renderCharts */ protected void refresh() { ChartsHost chartsHost = getChartsHost(); if (chartsHost != null) { if (chartsHost.getDevice() != null) { mChartDirty = false; updateDateInfo(getStartDate(), getEndDate()); if (refreshTask != null && refreshTask.getStatus() != AsyncTask.Status.FINISHED) { refreshTask.cancel(true); } refreshTask = createRefreshTask("Visualizing data", getActivity()).execute(); } } } /** * This method reads the data from the database, analyzes and prepares it for * the charts. This will be called from a background task, so there must not be * any UI access. #updateChartsInUIThread and #renderCharts will be automatically called after this method. */ protected abstract ChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device); /** * Triggers the actual (re-) rendering of the chart. * Always called from the UI thread. */ protected abstract void renderCharts(); public DefaultChartsData refresh(GBDevice gbDevice, List samples) { // Calendar cal = GregorianCalendar.getInstance(); // cal.clear(); TimestampTranslation tsTranslation = new TimestampTranslation(); // Date date; // String dateStringFrom = ""; // String dateStringTo = ""; // ArrayList xLabels = null; LOG.info("" + getTitle() + ": number of samples:" + samples.size()); LineData lineData; if (samples.size() > 1) { boolean annotate = true; boolean use_steps_as_movement; int last_type = ActivityKind.TYPE_UNKNOWN; int numEntries = samples.size(); List activityEntries = new ArrayList<>(numEntries); List deepSleepEntries = new ArrayList<>(numEntries); List lightSleepEntries = new ArrayList<>(numEntries); List notWornEntries = new ArrayList<>(numEntries); boolean hr = supportsHeartrate(gbDevice); List heartrateEntries = hr ? new ArrayList(numEntries) : null; List colors = new ArrayList<>(numEntries); // this is kinda inefficient... int lastHrSampleIndex = -1; HeartRateUtils heartRateUtilsInstance = HeartRateUtils.getInstance(); for (int i = 0; i < numEntries; i++) { ActivitySample sample = samples.get(i); int type = sample.getKind(); int ts = tsTranslation.shorten(sample.getTimestamp()); // System.out.println(ts); // ts = i; // determine start and end dates // if (i == 0) { // cal.setTimeInMillis(ts * 1000L); // make sure it's converted to long // date = cal.getTime(); // dateStringFrom = dateFormat.format(date); // } else if (i == samples.size() - 1) { // cal.setTimeInMillis(ts * 1000L); // same here // date = cal.getTime(); // dateStringTo = dateFormat.format(date); // } float movement = sample.getIntensity(); float value = movement; switch (type) { case ActivityKind.TYPE_DEEP_SLEEP: if (last_type != type) { //FIXME: this is ugly but it works (repeated in each case) deepSleepEntries.add(createLineEntry(0, ts - 1)); lightSleepEntries.add(createLineEntry(0, ts)); notWornEntries.add(createLineEntry(0, ts)); activityEntries.add(createLineEntry(0, ts)); } deepSleepEntries.add(createLineEntry(value + SleepUtils.Y_VALUE_DEEP_SLEEP, ts)); break; case ActivityKind.TYPE_LIGHT_SLEEP: if (last_type != type) { lightSleepEntries.add(createLineEntry(0, ts - 1)); deepSleepEntries.add(createLineEntry(0, ts)); notWornEntries.add(createLineEntry(0, ts)); activityEntries.add(createLineEntry(0, ts)); } lightSleepEntries.add(createLineEntry(value, ts)); break; case ActivityKind.TYPE_NOT_WORN: if (last_type != type) { notWornEntries.add(createLineEntry(0, ts - 1)); lightSleepEntries.add(createLineEntry(0, ts)); deepSleepEntries.add(createLineEntry(0, ts)); activityEntries.add(createLineEntry(0, ts)); } notWornEntries.add(createLineEntry(SleepUtils.Y_VALUE_DEEP_SLEEP, ts)); //a small value, just to show something on the graphs break; default: // short steps = sample.getSteps(); // if (use_steps_as_movement && steps != 0) { // // I'm not sure using steps for this is actually a good idea // movement = steps; // } // value = ((float) movement) / movement_divisor; if (last_type != type) { activityEntries.add(createLineEntry(0, ts - 1)); lightSleepEntries.add(createLineEntry(0, ts)); notWornEntries.add(createLineEntry(0, ts)); deepSleepEntries.add(createLineEntry(0, ts)); } activityEntries.add(createLineEntry(value, ts)); } if (hr && sample.getKind() != ActivityKind.TYPE_NOT_WORN && heartRateUtilsInstance.isValidHeartRateValue(sample.getHeartRate())) { if (lastHrSampleIndex > -1 && ts - lastHrSampleIndex > 1800*HeartRateUtils.MAX_HR_MEASUREMENTS_GAP_MINUTES) { heartrateEntries.add(createLineEntry(0, lastHrSampleIndex + 1)); heartrateEntries.add(createLineEntry(0, ts - 1)); } heartrateEntries.add(createLineEntry(sample.getHeartRate(), ts)); lastHrSampleIndex = ts; } String xLabel = ""; if (annotate) { // cal.setTimeInMillis((ts + tsOffset) * 1000L); // date = cal.getTime(); // String dateString = annotationDateFormat.format(date); // xLabel = dateString; // if (last_type != type) { // if (isSleep(last_type) && !isSleep(type)) { // // woken up // LimitLine line = new LimitLine(i, dateString); // line.enableDashedLine(8, 8, 0); // line.setTextColor(Color.WHITE); // line.setTextSize(15); // chart.getXAxis().addLimitLine(line); // } else if (!isSleep(last_type) && isSleep(type)) { // // fallen asleep // LimitLine line = new LimitLine(i, dateString); // line.enableDashedLine(8, 8, 0); // line.setTextSize(15); // line.setTextColor(Color.WHITE); // chart.getXAxis().addLimitLine(line); // } // } } last_type = type; } List lineDataSets = new ArrayList<>(); LineDataSet activitySet = createDataSet(activityEntries, akActivity.color, "Activity"); lineDataSets.add(activitySet); LineDataSet deepSleepSet = createDataSet(deepSleepEntries, akDeepSleep.color, "Deep Sleep"); lineDataSets.add(deepSleepSet); LineDataSet lightSleepSet = createDataSet(lightSleepEntries, akLightSleep.color, "Light Sleep"); lineDataSets.add(lightSleepSet); LineDataSet notWornSet = createDataSet(notWornEntries, akNotWorn.color, "Not worn"); lineDataSets.add(notWornSet); if (hr && heartrateEntries.size() > 0) { LineDataSet heartrateSet = createHeartrateSet(heartrateEntries, "Heart Rate"); lineDataSets.add(heartrateSet); } lineData = new LineData(lineDataSets); // chart.setDescription(getString(R.string.sleep_activity_date_range, dateStringFrom, dateStringTo)); // chart.setDescriptionPosition(?, ?); } else { lineData = new LineData(); } ValueFormatter xValueFormatter = new SampleXLabelFormatter(tsTranslation); return new DefaultChartsData(lineData, xValueFormatter); } /** * Implement this to supply the samples to be displayed. * * @param db * @param device * @param tsFrom * @param tsTo * @return */ protected abstract List getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo); protected abstract void setupLegend(Chart chart); protected Entry createLineEntry(float value, int xValue) { return new Entry(xValue, value); } protected LineDataSet createDataSet(List values, Integer color, String label) { LineDataSet set1 = new LineDataSet(values, label); set1.setColor(color); // set1.setDrawCubic(true); // set1.setCubicIntensity(0.2f); set1.setDrawFilled(true); set1.setDrawCircles(false); // set1.setLineWidth(2f); // set1.setCircleSize(5f); set1.setFillColor(color); set1.setFillAlpha(255); set1.setDrawValues(false); // set1.setHighLightColor(Color.rgb(128, 0, 255)); // set1.setColor(Color.rgb(89, 178, 44)); set1.setValueTextColor(CHART_TEXT_COLOR); set1.setAxisDependency(YAxis.AxisDependency.LEFT); return set1; } protected LineDataSet createHeartrateSet(List values, String label) { LineDataSet set1 = new LineDataSet(values, label); set1.setLineWidth(2.2f); set1.setColor(HEARTRATE_COLOR); // set1.setDrawCubic(true); set1.setMode(LineDataSet.Mode.HORIZONTAL_BEZIER); set1.setCubicIntensity(0.1f); set1.setDrawCircles(false); // set1.setCircleRadius(2f); // set1.setDrawFilled(true); // set1.setColor(getResources().getColor(android.R.color.background_light)); // set1.setCircleColor(HEARTRATE_COLOR); // set1.setFillColor(ColorTemplate.getHoloBlue()); // set1.setHighLightColor(Color.rgb(128, 0, 255)); // set1.setColor(Color.rgb(89, 178, 44)); set1.setDrawValues(true); set1.setValueTextColor(CHART_TEXT_COLOR); set1.setAxisDependency(YAxis.AxisDependency.RIGHT); return set1; } protected RefreshTask createRefreshTask(String task, Context context) { return new RefreshTask(task, context); } public class RefreshTask extends DBAccess { private ChartsData chartsData; public RefreshTask(String task, Context context) { super(task, context); } @Override protected void doInBackground(DBHandler db) { ChartsHost chartsHost = getChartsHost(); if (chartsHost != null) { chartsData = refreshInBackground(chartsHost, db, chartsHost.getDevice()); } else { cancel(true); } } @Override protected void onPostExecute(Object o) { super.onPostExecute(o); FragmentActivity activity = getActivity(); if (activity != null && !activity.isFinishing() && !activity.isDestroyed()) { updateChartsnUIThread(chartsData); renderCharts(); } else { LOG.info("Not rendering charts because activity is not available anymore"); } } } protected abstract void updateChartsnUIThread(ChartsData chartsData); /** * Returns true if the date was successfully shifted, and false if the shift * was ignored, e.g. when the to-value is in the future. * * @param from * @param to */ public boolean setDateRange(Date from, Date to) { if (from.compareTo(to) > 0) { throw new IllegalArgumentException("Bad date range: " + from + ".." + to); } Date now = new Date(); if (to.after(now) || //do not refresh chart if we reached now to.getTime()/10000 == (getEndDate().getTime()/10000)) { return false; } setStartDate(from); setEndDate(to); return true; } protected void updateDateInfo(Date from, Date to) { if (from.equals(to)) { getChartsHost().setDateInfo(DateTimeUtils.formatDate(from)); } else { getChartsHost().setDateInfo(DateTimeUtils.formatDateRange(from, to)); } } protected List getSamples(DBHandler db, GBDevice device) { int tsStart = getTSStart(); int tsEnd = getTSEnd(); List samples = (List) getSamples(db, device, tsStart, tsEnd); ensureStartAndEndSamples(samples, tsStart, tsEnd); // List samples2 = new ArrayList<>(); // int min = Math.min(samples.size(), 10); // int min = Math.min(samples.size(), 10); // for (int i = 0; i < min; i++) { // samples2.add(samples.get(i)); // } // return samples2; return samples; } protected List getSamplesofSleep(DBHandler db, GBDevice device) { int SLEEP_HOUR_LIMIT = 12; int tsStart = getTSStart(); Calendar day = GregorianCalendar.getInstance(); day.setTimeInMillis(tsStart * 1000L); day.set(Calendar.HOUR_OF_DAY, SLEEP_HOUR_LIMIT); day.set(Calendar.MINUTE, 0); day.set(Calendar.SECOND, 0); tsStart = toTimestamp(day.getTime()); int tsEnd = getTSEnd(); day.setTimeInMillis(tsEnd* 1000L); day.set(Calendar.HOUR_OF_DAY, SLEEP_HOUR_LIMIT); day.set(Calendar.MINUTE, 0); day.set(Calendar.SECOND, 0); tsEnd = toTimestamp(day.getTime()); List samples = (List) getSamples(db, device, tsStart, tsEnd); ensureStartAndEndSamples(samples, tsStart, tsEnd); return samples; } protected void ensureStartAndEndSamples(List samples, int tsStart, int tsEnd) { if (samples == null || samples.isEmpty()) { return; } ActivitySample lastSample = samples.get(samples.size() - 1); if (lastSample.getTimestamp() < tsEnd) { samples.add(createTrailingActivitySample(lastSample, tsEnd)); } ActivitySample firstSample = samples.get(0); if (firstSample.getTimestamp() > tsStart) { samples.add(createTrailingActivitySample(firstSample, tsStart)); } } private ActivitySample createTrailingActivitySample(ActivitySample referenceSample, int timestamp) { TrailingActivitySample sample = new TrailingActivitySample(); if (referenceSample instanceof AbstractActivitySample) { AbstractActivitySample reference = (AbstractActivitySample) referenceSample; sample.setUserId(reference.getUserId()); sample.setDeviceId(reference.getDeviceId()); sample.setProvider(reference.getProvider()); } sample.setTimestamp(timestamp); return sample; } private int getTSEnd() { return toTimestamp(getEndDate()); } private int getTSStart() { return toTimestamp(getStartDate()); } private int toTimestamp(Date date) { return (int) ((date.getTime() / 1000)); } public static class DefaultChartsData> extends ChartsData { private final T data; private ValueFormatter xValueFormatter; public DefaultChartsData(T data, ValueFormatter xValueFormatter) { this.xValueFormatter = xValueFormatter; this.data = data; } public ValueFormatter getXValueFormatter() { return xValueFormatter; } public T getData() { return data; } } protected static class SampleXLabelFormatter extends ValueFormatter { private final TimestampTranslation tsTranslation; SimpleDateFormat annotationDateFormat = new SimpleDateFormat("HH:mm"); // SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm"); Calendar cal = GregorianCalendar.getInstance(); public SampleXLabelFormatter(TimestampTranslation tsTranslation) { this.tsTranslation = tsTranslation; } // TODO: this does not work. Cannot use precomputed labels @Override public String getFormattedValue(float value) { cal.clear(); int ts = (int) value; cal.setTimeInMillis(tsTranslation.toOriginalValue(ts) * 1000L); Date date = cal.getTime(); String dateString = annotationDateFormat.format(date); return dateString; } } protected static class PreformattedXIndexLabelFormatter extends ValueFormatter { private ArrayList xLabels; public PreformattedXIndexLabelFormatter(ArrayList xLabels) { this.xLabels = xLabels; } @Override public String getFormattedValue(float value) { int index = (int) value; if (xLabels == null || index >= xLabels.size()) { return String.valueOf(value); } return xLabels.get(index); } } /** * Awkward class that helps in translating long timestamp * values to float (sic!) values. It basically rebases all * timestamps to a base (the very first) timestamp value. * * It does this so that the large timestamp values can be used * floating point values, where the mantissa is just 24 bits. */ public static class TimestampTranslation { private int tsOffset = -1; public int shorten(int timestamp) { if (tsOffset == -1) { tsOffset = timestamp; return 0; } return timestamp - tsOffset; } public int toOriginalValue(int timestamp) { if (tsOffset == -1) { return timestamp; } return timestamp + tsOffset; } } }