/* Copyright (C) 2023-2024 Arjan Schrijver, José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see . */
package nodomain.freeyourgadget.gadgetbridge.activities.dashboard;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.format.DateFormat;
import android.text.style.ForegroundColorSpan;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.StepAnalysis;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySession;
import nodomain.freeyourgadget.gadgetbridge.util.DashboardUtils;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
/**
* A simple {@link AbstractDashboardWidget} subclass.
* Use the {@link DashboardTodayWidget#newInstance} factory method to
* create an instance of this fragment.
*/
public class DashboardTodayWidget extends AbstractDashboardWidget {
private static final Logger LOG = LoggerFactory.getLogger(DashboardTodayWidget.class);
private View todayView;
private ImageView todayChart;
private boolean mode_24h;
public DashboardTodayWidget() {
// Required empty public constructor
}
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* @param dashboardData An instance of DashboardFragment.DashboardData.
* @return A new instance of fragment DashboardTodayWidget.
*/
public static DashboardTodayWidget newInstance(DashboardFragment.DashboardData dashboardData) {
DashboardTodayWidget fragment = new DashboardTodayWidget();
Bundle args = new Bundle();
args.putSerializable(ARG_DASHBOARD_DATA, dashboardData);
fragment.setArguments(args);
return fragment;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
todayView = inflater.inflate(R.layout.dashboard_widget_today, container, false);
todayChart = todayView.findViewById(R.id.dashboard_today_chart);
// Determine whether to draw a single or a double chart. In case 24h mode is selected,
// use just the outer chart (chart_12_24) for all data.
Prefs prefs = GBApplication.getPrefs();
mode_24h = prefs.getBoolean("dashboard_widget_today_24h", false);
// Initialize legend
TextView legend = todayView.findViewById(R.id.dashboard_piechart_legend);
SpannableString l_not_worn = new SpannableString("■ " + getString(R.string.abstract_chart_fragment_kind_not_worn));
l_not_worn.setSpan(new ForegroundColorSpan(color_not_worn), 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
SpannableString l_worn = new SpannableString("■ " + getString(R.string.activity_type_worn));
l_worn.setSpan(new ForegroundColorSpan(color_worn), 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
SpannableString l_activity = new SpannableString("■ " + getString(R.string.activity_type_activity));
l_activity.setSpan(new ForegroundColorSpan(color_activity), 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
SpannableString l_exercise = new SpannableString("■ " + getString(R.string.activity_type_exercise));
l_exercise.setSpan(new ForegroundColorSpan(color_exercise), 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
SpannableString l_deep_sleep = new SpannableString("■ " + getString(R.string.activity_type_deep_sleep));
l_deep_sleep.setSpan(new ForegroundColorSpan(color_deep_sleep), 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
SpannableString l_light_sleep = new SpannableString("■ " + getString(R.string.activity_type_light_sleep));
l_light_sleep.setSpan(new ForegroundColorSpan(color_light_sleep), 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
SpannableString l_rem_sleep = new SpannableString("■ " + getString(R.string.activity_type_rem_sleep));
l_rem_sleep.setSpan(new ForegroundColorSpan(color_rem_sleep), 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
SpannableStringBuilder legendBuilder = new SpannableStringBuilder();
legend.setText(legendBuilder.append(l_not_worn).append(" ").append(l_worn).append("\n").append(l_activity).append(" ").append(l_exercise).append("\n").append(l_light_sleep).append(" ").append(l_deep_sleep).append(" ").append(l_rem_sleep));
legend.setVisibility(prefs.getBoolean("dashboard_widget_today_legend", true) ? View.VISIBLE : View.GONE);
if (!dashboardData.generalizedActivities.isEmpty()) {
draw();
}
return todayView;
}
@Override
public void onResume() {
super.onResume();
if (todayChart != null) fillData();
}
private void draw() {
Prefs prefs = GBApplication.getPrefs();
boolean upsideDown24h = prefs.getBoolean("dashboard_widget_today_24h_upside_down", false);
boolean showYesterday = prefs.getBoolean("dashboard_widget_today_show_yesterday", false);
// Prepare circular chart
long currentDayStart = dashboardData.timeTo - 86400;
long midDaySecond = currentDayStart + (12 * 60 * 60);
int width = Resources.getSystem().getDisplayMetrics().widthPixels;
int height = width;
int barWidth = Math.round(width * 0.08f);
int hourTextSp = Math.round(width * 0.024f);
float hourTextPixels = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP,
hourTextSp,
GBApplication.getContext().getResources().getDisplayMetrics()
);
float outerCircleMargin = mode_24h ? barWidth / 2f : barWidth / 2f + hourTextPixels * 1.3f;
float innerCircleMargin = outerCircleMargin + barWidth * 1.3f;
float degreeFactor = mode_24h ? 240 : 120;
Bitmap todayBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(todayBitmap);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.STROKE);
// Draw clock stripes
float clockMargin = outerCircleMargin + (mode_24h ? barWidth : barWidth * 2.3f);
int clockStripesInterval = mode_24h ? 15 : 30;
float clockStripesWidth = barWidth / 3f;
paint.setStrokeWidth(clockStripesWidth);
paint.setColor(color_worn);
for (int i = 0; i < 360; i += clockStripesInterval) {
canvas.drawArc(clockMargin, clockMargin, width - clockMargin, height - clockMargin, i, 1, false, paint);
}
// Draw hours
boolean normalClock = DateFormat.is24HourFormat(GBApplication.getContext());
Map hours = new HashMap() {
{
put(0, normalClock ? (mode_24h ? "0" : "12") : "12pm");
put(3, "3");
put(6, normalClock ? "6" : "6am");
put(9, "9");
put(12, normalClock ? (mode_24h ? "12" : "0") : "12am");
put(15, normalClock ? "15" : "3");
put(18, normalClock ? "18" : "6pm");
put(21, normalClock ? "21" : "9");
}
};
Paint textPaint = new Paint();
textPaint.setAntiAlias(true);
textPaint.setColor(color_worn);
textPaint.setTextSize(hourTextPixels);
textPaint.setTextAlign(Paint.Align.CENTER);
Rect textBounds = new Rect();
if (mode_24h && upsideDown24h) {
textPaint.getTextBounds(hours.get(0), 0, hours.get(0).length(), textBounds);
canvas.drawText(hours.get(0), width / 2f, height - (clockMargin + clockStripesWidth), textPaint);
textPaint.getTextBounds(hours.get(6), 0, hours.get(6).length(), textBounds);
canvas.drawText(hours.get(6), clockMargin + clockStripesWidth + textBounds.width() / 2f, height / 2f + textBounds.height() / 2f, textPaint);
textPaint.getTextBounds(hours.get(12), 0, hours.get(12).length(), textBounds);
canvas.drawText(hours.get(12), width / 2f, clockMargin + clockStripesWidth + textBounds.height(), textPaint);
textPaint.getTextBounds(hours.get(18), 0, hours.get(18).length(), textBounds);
canvas.drawText(hours.get(18), width - (clockMargin + clockStripesWidth + textBounds.width()), height / 2f + textBounds.height() / 2f, textPaint);
} else if (mode_24h) {
textPaint.getTextBounds(hours.get(0), 0, hours.get(0).length(), textBounds);
canvas.drawText(hours.get(0), width / 2f, clockMargin + clockStripesWidth + textBounds.height(), textPaint);
textPaint.getTextBounds(hours.get(6), 0, hours.get(6).length(), textBounds);
canvas.drawText(hours.get(6), width - (clockMargin + clockStripesWidth + textBounds.width()), height / 2f + textBounds.height() / 2f, textPaint);
textPaint.getTextBounds(hours.get(12), 0, hours.get(12).length(), textBounds);
canvas.drawText(hours.get(12), width / 2f, height - (clockMargin + clockStripesWidth), textPaint);
textPaint.getTextBounds(hours.get(18), 0, hours.get(18).length(), textBounds);
canvas.drawText(hours.get(18), clockMargin + clockStripesWidth + textBounds.width() / 2f, height / 2f + textBounds.height() / 2f, textPaint);
} else {
textPaint.getTextBounds(hours.get(0), 0, hours.get(0).length(), textBounds);
canvas.drawText(hours.get(0), width / 2f, textBounds.height(), textPaint);
textPaint.getTextBounds(hours.get(3), 0, hours.get(3).length(), textBounds);
canvas.drawText(hours.get(3), width - (clockMargin + clockStripesWidth + textBounds.width()), height / 2f + textBounds.height() / 2f, textPaint);
textPaint.getTextBounds(hours.get(6), 0, hours.get(6).length(), textBounds);
canvas.drawText(hours.get(6), width / 2f, height - (clockMargin + clockStripesWidth), textPaint);
textPaint.getTextBounds(hours.get(9), 0, hours.get(9).length(), textBounds);
canvas.drawText(hours.get(9), clockMargin + clockStripesWidth + textBounds.width() / 2f, height / 2f + textBounds.height() / 2f, textPaint);
textPaint.getTextBounds(hours.get(12), 0, hours.get(12).length(), textBounds);
canvas.drawText(hours.get(12), width / 2f, clockMargin + clockStripesWidth + textBounds.height(), textPaint);
textPaint.getTextBounds(hours.get(15), 0, hours.get(15).length(), textBounds);
canvas.drawText(hours.get(15), (float) (width - Math.ceil(textBounds.width() / 2f)), height / 2f + textBounds.height() / 2f, textPaint);
textPaint.getTextBounds(hours.get(18), 0, hours.get(18).length(), textBounds);
canvas.drawText(hours.get(18), width / 2f, height - textBounds.height() / 2f, textPaint);
textPaint.setTextAlign(Paint.Align.LEFT);
textPaint.getTextBounds(hours.get(21), 0, hours.get(21).length(), textBounds);
canvas.drawText(hours.get(21), 1, height / 2f + textBounds.height() / 2f, textPaint);
}
// Draw generalized activities on circular chart
long secondIndex = dashboardData.timeFrom;
long currentTime = Calendar.getInstance().getTimeInMillis() / 1000;
boolean dayIsToday = !(dashboardData.timeTo < currentTime);
int startAngle = mode_24h && upsideDown24h ? 90 : 270;
synchronized (dashboardData.generalizedActivities) {
for (DashboardFragment.DashboardData.GeneralizedActivity activity : dashboardData.generalizedActivities) {
// Determine margin
float margin = innerCircleMargin;
if (mode_24h || activity.timeFrom >= midDaySecond) {
margin = outerCircleMargin;
}
if (!mode_24h && showYesterday && dayIsToday) {
if (activity.timeFrom < currentDayStart && activity.timeFrom > midDaySecond - 86400) {
margin = outerCircleMargin;
}
}
// Skip activities from before 24h ago (to prevent double drawing the same position)
if (showYesterday && dayIsToday && (activity.timeTo < currentTime - 86400)) {
continue;
}
// Draw inactive slices
if (!mode_24h && secondIndex < midDaySecond && activity.timeFrom >= midDaySecond) {
paint.setStrokeWidth(barWidth / 3f);
paint.setColor(color_unknown);
canvas.drawArc(innerCircleMargin, innerCircleMargin, width - innerCircleMargin, height - innerCircleMargin, startAngle + (secondIndex - dashboardData.timeFrom) / degreeFactor, (midDaySecond - secondIndex) / degreeFactor, false, paint);
secondIndex = midDaySecond;
}
if (activity.timeFrom > secondIndex) {
paint.setStrokeWidth(barWidth / 3f);
paint.setColor(color_unknown);
canvas.drawArc(margin, margin, width - margin, height - margin, startAngle + (secondIndex - dashboardData.timeFrom) / degreeFactor, (activity.timeFrom - secondIndex) / degreeFactor, false, paint);
}
float start_angle = startAngle + (activity.timeFrom - dashboardData.timeFrom) / degreeFactor;
float sweep_angle = (activity.timeTo - activity.timeFrom) / degreeFactor;
if (activity.activityKind == ActivityKind.NOT_MEASURED) {
paint.setStrokeWidth(barWidth / 3f);
paint.setColor(color_worn);
if (showYesterday && dayIsToday && activity.timeFrom < currentDayStart) {
paint.setAlpha(64);
}
canvas.drawArc(margin, margin, width - margin, height - margin, start_angle, sweep_angle, false, paint);
} else if (activity.activityKind == ActivityKind.NOT_WORN) {
paint.setStrokeWidth(barWidth / 3f);
paint.setColor(color_not_worn);
if (showYesterday && dayIsToday && activity.timeFrom < currentDayStart) {
paint.setAlpha(64);
}
canvas.drawArc(margin, margin, width - margin, height - margin, start_angle, sweep_angle, false, paint);
} else if (activity.activityKind == ActivityKind.LIGHT_SLEEP || activity.activityKind == ActivityKind.SLEEP_ANY) {
paint.setStrokeWidth(barWidth);
paint.setColor(color_light_sleep);
if (showYesterday && dayIsToday && activity.timeFrom < currentDayStart) {
paint.setAlpha(64);
}
canvas.drawArc(margin, margin, width - margin, height - margin, start_angle, sweep_angle, false, paint);
} else if (activity.activityKind == ActivityKind.REM_SLEEP) {
paint.setStrokeWidth(barWidth);
paint.setColor(color_rem_sleep);
if (showYesterday && dayIsToday && activity.timeFrom < currentDayStart) {
paint.setAlpha(64);
}
canvas.drawArc(margin, margin, width - margin, height - margin, start_angle, sweep_angle, false, paint);
} else if (activity.activityKind == ActivityKind.DEEP_SLEEP) {
paint.setStrokeWidth(barWidth);
paint.setColor(color_deep_sleep);
if (showYesterday && dayIsToday && activity.timeFrom < currentDayStart) {
paint.setAlpha(64);
}
canvas.drawArc(margin, margin, width - margin, height - margin, start_angle, sweep_angle, false, paint);
} else if (activity.activityKind == ActivityKind.AWAKE_SLEEP) {
paint.setStrokeWidth(barWidth);
paint.setColor(color_awake_sleep);
if (showYesterday && dayIsToday && activity.timeFrom < currentDayStart) {
paint.setAlpha(64);
}
canvas.drawArc(margin, margin, width - margin, height - margin, start_angle, sweep_angle, false, paint);
} else if (activity.activityKind == ActivityKind.EXERCISE) {
paint.setStrokeWidth(barWidth);
paint.setColor(color_exercise);
if (showYesterday && dayIsToday && activity.timeFrom < currentDayStart) {
paint.setAlpha(64);
}
canvas.drawArc(margin, margin, width - margin, height - margin, start_angle, sweep_angle, false, paint);
} else {
paint.setStrokeWidth(barWidth);
paint.setColor(color_activity);
if (showYesterday && dayIsToday && activity.timeFrom < currentDayStart) {
paint.setAlpha(64);
}
canvas.drawArc(margin, margin, width - margin, height - margin, start_angle, sweep_angle, false, paint);
}
secondIndex = activity.timeTo;
}
}
// Draw indicator for current time
if (prefs.getBoolean("dashboard_widget_today_time_indicator", false) && currentTime < dashboardData.timeTo) {
float margin = (mode_24h || currentTime >= midDaySecond) ? outerCircleMargin : innerCircleMargin;
paint.setStrokeWidth(barWidth);
paint.setColor(GBApplication.getTextColor(requireContext()));
canvas.drawArc(margin, margin, width - margin, height - margin, startAngle + (currentTime - dashboardData.timeFrom) / degreeFactor, 300 / degreeFactor, false, paint);
}
// Fill remaining time until current time in 12h mode before midday
if (!mode_24h && currentTime < midDaySecond) {
// Fill inner bar up until current time
paint.setStrokeWidth(barWidth / 3f);
paint.setColor(color_unknown);
canvas.drawArc(innerCircleMargin, innerCircleMargin, width - innerCircleMargin, height - innerCircleMargin, startAngle + (secondIndex - dashboardData.timeFrom) / degreeFactor, (currentTime - secondIndex) / degreeFactor, false, paint);
// Fill inner bar up until midday
paint.setStrokeWidth(barWidth / 3f);
paint.setColor(color_unknown);
canvas.drawArc(innerCircleMargin, innerCircleMargin, width - innerCircleMargin, height - innerCircleMargin, startAngle + (currentTime - dashboardData.timeFrom) / degreeFactor, (midDaySecond - currentTime) / degreeFactor, false, paint);
// Fill outer bar up until midnight
paint.setStrokeWidth(barWidth / 3f);
paint.setColor(color_unknown);
canvas.drawArc(outerCircleMargin, outerCircleMargin, width - outerCircleMargin, height - outerCircleMargin, 0, 360, false, paint);
}
// Fill remaining time until current time in 24h mode or in 12h mode after midday
if ((mode_24h || currentTime >= midDaySecond) && currentTime < dashboardData.timeTo) {
// Fill inner bar up until midday
if (!mode_24h && secondIndex < midDaySecond) {
paint.setStrokeWidth(barWidth / 3f);
paint.setColor(color_unknown);
canvas.drawArc(innerCircleMargin, innerCircleMargin, width - innerCircleMargin, height - innerCircleMargin, startAngle + (secondIndex - dashboardData.timeFrom) / degreeFactor, (midDaySecond - secondIndex) / degreeFactor, false, paint);
secondIndex = midDaySecond;
}
// Fill outer bar up until current time
paint.setStrokeWidth(barWidth / 3f);
paint.setColor(color_unknown);
canvas.drawArc(outerCircleMargin, outerCircleMargin, width - outerCircleMargin, height - outerCircleMargin, startAngle + (secondIndex - dashboardData.timeFrom) / degreeFactor, (currentTime - secondIndex) / degreeFactor, false, paint);
// Fill outer bar up until midnight
paint.setStrokeWidth(barWidth / 3f);
paint.setColor(color_unknown);
canvas.drawArc(outerCircleMargin, outerCircleMargin, width - outerCircleMargin, height - outerCircleMargin, startAngle + (currentTime - dashboardData.timeFrom) / degreeFactor, (dashboardData.timeTo - currentTime) / degreeFactor, false, paint);
}
// Only when displaying a past day
if (secondIndex < dashboardData.timeTo && currentTime > dashboardData.timeTo) {
// Fill outer bar up until midnight
paint.setStrokeWidth(barWidth / 3f);
paint.setColor(color_unknown);
canvas.drawArc(outerCircleMargin, outerCircleMargin, width - outerCircleMargin, height - outerCircleMargin, startAngle + (secondIndex - dashboardData.timeFrom) / degreeFactor, (dashboardData.timeTo - secondIndex) / degreeFactor, false, paint);
}
todayChart.setImageBitmap(todayBitmap);
}
protected void fillData() {
if (todayView == null) return;
Prefs prefs = GBApplication.getPrefs();
if (prefs.getBoolean("dashboard_widget_today_show_yesterday", false)) {
Calendar today = Calendar.getInstance();
Calendar dashboardDate = Calendar.getInstance();
dashboardDate.setTimeInMillis((dashboardData.timeFrom + 1) * 1000L);
if (DateTimeUtils.isSameDay(today, dashboardDate)) {
dashboardData.timeFrom -= 86400;
}
}
todayView.post(new Runnable() {
@Override
public void run() {
FillDataAsyncTask myAsyncTask = new FillDataAsyncTask();
myAsyncTask.execute();
}
});
}
private class FillDataAsyncTask extends AsyncTask {
private final HashMap activityTimestamps = new HashMap<>();
/**
* Add per-second activities to `activityTimestamps`
*/
private void addActivity(long timeFrom, long timeTo, ActivityKind activityKind) {
for (long i = timeFrom; i <= timeTo; i++) {
// If the current timestamp isn't saved yet, do so immediately
if (activityTimestamps.get(i) == null) {
activityTimestamps.put(i, activityKind);
continue;
}
// If the current timestamp is already saved, compare the activity kinds and
// keep the most 'important' one
switch (activityTimestamps.get(i)) {
case EXERCISE:
break;
case ACTIVITY:
if (activityKind == ActivityKind.EXERCISE)
activityTimestamps.put(i, activityKind);
break;
case DEEP_SLEEP:
if (activityKind == ActivityKind.EXERCISE ||
activityKind == ActivityKind.ACTIVITY)
activityTimestamps.put(i, activityKind);
break;
case LIGHT_SLEEP:
if (activityKind == ActivityKind.EXERCISE ||
activityKind == ActivityKind.ACTIVITY ||
activityKind == ActivityKind.DEEP_SLEEP)
activityTimestamps.put(i, activityKind);
break;
case REM_SLEEP:
if (activityKind == ActivityKind.EXERCISE ||
activityKind == ActivityKind.ACTIVITY ||
activityKind == ActivityKind.DEEP_SLEEP ||
activityKind == ActivityKind.LIGHT_SLEEP)
activityTimestamps.put(i, activityKind);
break;
case AWAKE_SLEEP:
if (activityKind == ActivityKind.EXERCISE ||
activityKind == ActivityKind.ACTIVITY ||
activityKind == ActivityKind.DEEP_SLEEP ||
activityKind == ActivityKind.LIGHT_SLEEP ||
activityKind == ActivityKind.REM_SLEEP)
activityTimestamps.put(i, activityKind);
break;
case SLEEP_ANY:
case NOT_MEASURED:
if (activityKind == ActivityKind.EXERCISE ||
activityKind == ActivityKind.ACTIVITY ||
activityKind == ActivityKind.DEEP_SLEEP ||
activityKind == ActivityKind.LIGHT_SLEEP ||
activityKind == ActivityKind.REM_SLEEP ||
activityKind == ActivityKind.AWAKE_SLEEP)
activityTimestamps.put(i, activityKind);
break;
default:
activityTimestamps.put(i, activityKind);
break;
}
}
}
/**
* Add NOT_MEASURED (worn) activities for every successful heart rate measurement
*/
private void calculateWornSessions(List samples) {
int firstTimestamp = 0;
int lastTimestamp = 0;
for (ActivitySample sample : samples) {
if (sample.getHeartRate() < 10 && firstTimestamp == 0) continue;
if (firstTimestamp == 0) firstTimestamp = sample.getTimestamp();
if (lastTimestamp == 0) lastTimestamp = sample.getTimestamp();
if (HeartRateUtils.getInstance().isValidHeartRateValue(sample.getHeartRate())
&& sample.getTimestamp() > lastTimestamp + dashboardData.hrIntervalSecs
&& firstTimestamp != lastTimestamp) {
LOG.debug("Registered worn session from {} to {}", firstTimestamp, lastTimestamp);
addActivity(firstTimestamp, lastTimestamp, ActivityKind.NOT_MEASURED);
if (sample.getHeartRate() < 10) {
firstTimestamp = 0;
lastTimestamp = 0;
} else {
firstTimestamp = sample.getTimestamp();
lastTimestamp = sample.getTimestamp();
}
continue;
}
if (HeartRateUtils.getInstance().isValidHeartRateValue(sample.getHeartRate())) {
lastTimestamp = sample.getTimestamp();
}
}
if (firstTimestamp != lastTimestamp) {
LOG.debug("Registered worn session from {} to {}", firstTimestamp, lastTimestamp);
addActivity(firstTimestamp, lastTimestamp, ActivityKind.NOT_MEASURED);
}
}
/**
* Merge per-second activities from `activityTimestamps` into generalized activity ranges
* with minute-based resolution
*/
private void createGeneralizedActivities() {
long currentTime = Calendar.getInstance().getTimeInMillis() / 1000;
long midDaySecond = dashboardData.timeTo - (12 * 60 * 60);
DashboardFragment.DashboardData.GeneralizedActivity previous = null;
List> sortedActivityTimestamps = activityTimestamps.entrySet()
.stream()
.sorted(Map.Entry.comparingByKey())
.collect(Collectors.toList());
for (Map.Entry activity : sortedActivityTimestamps) {
long timestamp = activity.getKey();
ActivityKind activityKind = activity.getValue();
// Start a new merged activity on certain conditions
if (previous == null ||
previous.activityKind != activityKind ||
(!mode_24h && timestamp == midDaySecond) ||
(!mode_24h && timestamp == midDaySecond - 86400) ||
timestamp == dashboardData.timeTo - 86400 ||
timestamp == currentTime - 86400 ||
previous.timeTo < timestamp - 60) {
previous = new DashboardFragment.DashboardData.GeneralizedActivity(activityKind, timestamp, timestamp);
dashboardData.generalizedActivities.add(previous);
} else {
previous.timeTo = timestamp;
}
}
}
@Override
protected Void doInBackground(Void... params) {
final long nanoStart = System.nanoTime();
// Retrieve activity data
dashboardData.generalizedActivities.clear();
List devices = GBApplication.app().getDeviceManager().getDevices();
List allActivitySamples = new ArrayList<>();
List stepSessions = new ArrayList<>();
List activitySummaries = null;
try (DBHandler dbHandler = GBApplication.acquireDB()) {
for (GBDevice dev : devices) {
if ((dashboardData.showAllDevices || dashboardData.showDeviceList.contains(dev.getAddress())) && dev.getDeviceCoordinator().supportsActivityTracking()) {
List extends ActivitySample> activitySamples = DashboardUtils.getAllSamples(dbHandler, dev, dashboardData);
allActivitySamples.addAll(activitySamples);
StepAnalysis stepAnalysis = new StepAnalysis();
stepSessions.addAll(stepAnalysis.calculateStepSessions(activitySamples));
}
}
activitySummaries = DashboardUtils.getWorkoutSamples(dbHandler, dashboardData);
} catch (Exception e) {
LOG.warn("Could not retrieve activity amounts: ", e);
}
Collections.sort(allActivitySamples, (lhs, rhs) -> Integer.valueOf(lhs.getTimestamp()).compareTo(rhs.getTimestamp()));
// Determine worn sessions from heart rate samples
calculateWornSessions(allActivitySamples);
// Integrate various data from multiple devices
for (ActivitySample sample : allActivitySamples) {
// Handle only TYPE_NOT_WORN and TYPE_SLEEP (including variants) here
if (sample.getKind() != ActivityKind.NOT_WORN && (sample.getKind() == ActivityKind.NOT_MEASURED || !ActivityKind.isSleep(sample.getKind())))
continue;
// Add to day results
addActivity(sample.getTimestamp(), sample.getTimestamp() + 60, sample.getKind());
}
if (activitySummaries != null) {
for (BaseActivitySummary baseActivitySummary : activitySummaries) {
addActivity(baseActivitySummary.getStartTime().getTime() / 1000, baseActivitySummary.getEndTime().getTime() / 1000, ActivityKind.EXERCISE);
}
}
for (ActivitySession session : stepSessions) {
addActivity(session.getStartTime().getTime() / 1000, session.getEndTime().getTime() / 1000, ActivityKind.ACTIVITY);
}
// Merge per-second activities
createGeneralizedActivities();
final long nanoEnd = System.nanoTime();
final long executionTime = (nanoEnd - nanoStart) / 1000000;
LOG.debug("fillData for {} took {}ms", DashboardTodayWidget.this.getClass().getSimpleName(), executionTime);
return null;
}
@Override
protected void onPostExecute(final Void unused) {
super.onPostExecute(unused);
try {
draw();
} catch (final Exception e) {
LOG.error("calling draw() failed", e);
}
}
}
}