mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-08-25 16:50:44 +02:00
This adds a new dashboard-type view to Gadgetbridge. The new dashboard activity displays several widgets with aggregated statistics from multiple devices. New preferences are added to allow configuration of the dashboard and its widgets. A new bottom navigation bar is added to switch between the Dashboard and Devices views. Some issues that prompted this feature and provided inspiration for the implementation: - https://codeberg.org/Freeyourgadget/Gadgetbridge/issues/301 (More Intuitive User Interface) - https://codeberg.org/Freeyourgadget/Gadgetbridge/issues/3074 (Ability to merge historical data from several devices) Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/3478 Reviewed-by: José Rebelo <joserebelo@noreply.codeberg.org> Co-authored-by: Arjan Schrijver <a_gadgetbridge@anymore.nl> Co-committed-by: Arjan Schrijver <a_gadgetbridge@anymore.nl>
475 lines
27 KiB
Java
475 lines
27 KiB
Java
/* Copyright (C) 2023-2024 Arjan Schrijver
|
|
|
|
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 <http://www.gnu.org/licenses/>. */
|
|
package nodomain.freeyourgadget.gadgetbridge.activities.dashboard;
|
|
|
|
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.TreeMap;
|
|
|
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
|
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
|
|
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.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.abstract_chart_fragment_kind_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()) {
|
|
fillData();
|
|
} else {
|
|
draw();
|
|
}
|
|
|
|
return todayView;
|
|
}
|
|
|
|
@Override
|
|
public void onResume() {
|
|
super.onResume();
|
|
if (todayChart != null) fillData();
|
|
}
|
|
|
|
private void draw() {
|
|
// Prepare circular chart
|
|
long midDaySecond = dashboardData.timeFrom + (12 * 60 * 60);
|
|
int width = 500;
|
|
int height = 500;
|
|
int barWidth = 40;
|
|
int hourTextSp = 12;
|
|
float hourTextPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, hourTextSp, requireContext().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(getContext());
|
|
Map<Integer, String> hours = new HashMap<Integer, String>() {
|
|
{
|
|
put(3, "3");
|
|
put(6, normalClock ? "6" : "6am");
|
|
put(9, "9");
|
|
put(12, normalClock ? "12" : "12pm");
|
|
put(15, normalClock ? "15" : "3");
|
|
put(18, normalClock ? "18" : "6pm");
|
|
put(21, normalClock ? "21" : "9");
|
|
put(24, normalClock ? "24" : "12am");
|
|
}
|
|
};
|
|
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) {
|
|
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);
|
|
textPaint.getTextBounds(hours.get(24), 0, hours.get(24).length(), textBounds);
|
|
canvas.drawText(hours.get(24), width / 2f, clockMargin + clockStripesWidth + textBounds.height(), textPaint);
|
|
} else {
|
|
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);
|
|
textPaint.setTextAlign(Paint.Align.CENTER);
|
|
textPaint.getTextBounds(hours.get(24), 0, hours.get(24).length(), textBounds);
|
|
canvas.drawText(hours.get(24), width / 2f, textBounds.height(), textPaint);
|
|
}
|
|
|
|
// Draw generalized activities on circular chart
|
|
long secondIndex = dashboardData.timeFrom;
|
|
long currentTime = Calendar.getInstance().getTimeInMillis() / 1000;
|
|
synchronized (dashboardData.generalizedActivities) {
|
|
for (DashboardFragment.DashboardData.GeneralizedActivity activity : dashboardData.generalizedActivities) {
|
|
// Determine margin depending on 24h/12h mode
|
|
float margin = (mode_24h || activity.timeFrom >= midDaySecond) ? outerCircleMargin : innerCircleMargin;
|
|
// 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, 270 + (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, 270 + (secondIndex - dashboardData.timeFrom) / degreeFactor, (activity.timeFrom - secondIndex) / degreeFactor, false, paint);
|
|
}
|
|
float start_angle = 270 + (activity.timeFrom - dashboardData.timeFrom) / degreeFactor;
|
|
float sweep_angle = (activity.timeTo - activity.timeFrom) / degreeFactor;
|
|
if (activity.activityKind == ActivityKind.TYPE_NOT_MEASURED) {
|
|
paint.setStrokeWidth(barWidth / 3f);
|
|
paint.setColor(color_worn);
|
|
canvas.drawArc(margin, margin, width - margin, height - margin, start_angle, sweep_angle, false, paint);
|
|
} else if (activity.activityKind == ActivityKind.TYPE_NOT_WORN) {
|
|
paint.setStrokeWidth(barWidth / 3f);
|
|
paint.setColor(color_not_worn);
|
|
canvas.drawArc(margin, margin, width - margin, height - margin, start_angle, sweep_angle, false, paint);
|
|
} else if (activity.activityKind == ActivityKind.TYPE_LIGHT_SLEEP || activity.activityKind == ActivityKind.TYPE_SLEEP) {
|
|
paint.setStrokeWidth(barWidth);
|
|
paint.setColor(color_light_sleep);
|
|
canvas.drawArc(margin, margin, width - margin, height - margin, start_angle, sweep_angle, false, paint);
|
|
} else if (activity.activityKind == ActivityKind.TYPE_REM_SLEEP) {
|
|
paint.setStrokeWidth(barWidth);
|
|
paint.setColor(color_rem_sleep);
|
|
canvas.drawArc(margin, margin, width - margin, height - margin, start_angle, sweep_angle, false, paint);
|
|
} else if (activity.activityKind == ActivityKind.TYPE_DEEP_SLEEP) {
|
|
paint.setStrokeWidth(barWidth);
|
|
paint.setColor(color_deep_sleep);
|
|
canvas.drawArc(margin, margin, width - margin, height - margin, start_angle, sweep_angle, false, paint);
|
|
} else if (activity.activityKind == ActivityKind.TYPE_EXERCISE) {
|
|
paint.setStrokeWidth(barWidth);
|
|
paint.setColor(color_exercise);
|
|
canvas.drawArc(margin, margin, width - margin, height - margin, start_angle, sweep_angle, false, paint);
|
|
} else {
|
|
paint.setStrokeWidth(barWidth);
|
|
paint.setColor(color_activity);
|
|
canvas.drawArc(margin, margin, width - margin, height - margin, start_angle, sweep_angle, false, paint);
|
|
}
|
|
secondIndex = activity.timeTo;
|
|
}
|
|
}
|
|
// 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, 270 + (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, 270 + (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, 270 + (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, 270 + (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, 270 + (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, 270 + (secondIndex - dashboardData.timeFrom) / degreeFactor, (dashboardData.timeTo - secondIndex) / degreeFactor, false, paint);
|
|
}
|
|
|
|
todayChart.setImageBitmap(todayBitmap);
|
|
}
|
|
|
|
protected void fillData() {
|
|
if (todayView == null) return;
|
|
todayView.post(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
FillDataAsyncTask myAsyncTask = new FillDataAsyncTask();
|
|
myAsyncTask.execute();
|
|
}
|
|
});
|
|
}
|
|
|
|
private class FillDataAsyncTask extends AsyncTask<Void, Void, Void> {
|
|
private final TreeMap<Long, Integer> activityTimestamps = new TreeMap<>();
|
|
|
|
private void addActivity(long timeFrom, long timeTo, int 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 ActivityKind.TYPE_EXERCISE:
|
|
break;
|
|
case ActivityKind.TYPE_ACTIVITY:
|
|
if (activityKind == ActivityKind.TYPE_EXERCISE)
|
|
activityTimestamps.put(i, activityKind);
|
|
break;
|
|
case ActivityKind.TYPE_DEEP_SLEEP:
|
|
if (activityKind == ActivityKind.TYPE_EXERCISE ||
|
|
activityKind == ActivityKind.TYPE_ACTIVITY)
|
|
activityTimestamps.put(i, activityKind);
|
|
break;
|
|
case ActivityKind.TYPE_LIGHT_SLEEP:
|
|
if (activityKind == ActivityKind.TYPE_EXERCISE ||
|
|
activityKind == ActivityKind.TYPE_ACTIVITY ||
|
|
activityKind == ActivityKind.TYPE_DEEP_SLEEP)
|
|
activityTimestamps.put(i, activityKind);
|
|
break;
|
|
case ActivityKind.TYPE_REM_SLEEP:
|
|
if (activityKind == ActivityKind.TYPE_EXERCISE ||
|
|
activityKind == ActivityKind.TYPE_ACTIVITY ||
|
|
activityKind == ActivityKind.TYPE_DEEP_SLEEP ||
|
|
activityKind == ActivityKind.TYPE_LIGHT_SLEEP)
|
|
activityTimestamps.put(i, activityKind);
|
|
break;
|
|
case ActivityKind.TYPE_SLEEP:
|
|
if (activityKind == ActivityKind.TYPE_EXERCISE ||
|
|
activityKind == ActivityKind.TYPE_ACTIVITY ||
|
|
activityKind == ActivityKind.TYPE_DEEP_SLEEP ||
|
|
activityKind == ActivityKind.TYPE_LIGHT_SLEEP ||
|
|
activityKind == ActivityKind.TYPE_REM_SLEEP)
|
|
activityTimestamps.put(i, activityKind);
|
|
break;
|
|
default:
|
|
activityTimestamps.put(i, activityKind);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void calculateWornSessions(List<ActivitySample> 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 ((sample.getHeartRate() < 10 || sample.getTimestamp() > lastTimestamp + dashboardData.hrIntervalSecs) && firstTimestamp != lastTimestamp) {
|
|
LOG.info("Registered worn session from " + firstTimestamp + " to " + lastTimestamp);
|
|
addActivity(firstTimestamp, lastTimestamp, ActivityKind.TYPE_NOT_MEASURED);
|
|
if (sample.getHeartRate() < 10) {
|
|
firstTimestamp = 0;
|
|
lastTimestamp = 0;
|
|
} else {
|
|
firstTimestamp = sample.getTimestamp();
|
|
lastTimestamp = sample.getTimestamp();
|
|
}
|
|
continue;
|
|
}
|
|
lastTimestamp = sample.getTimestamp();
|
|
}
|
|
if (firstTimestamp != lastTimestamp) {
|
|
LOG.info("Registered worn session from " + firstTimestamp + " to " + lastTimestamp);
|
|
addActivity(firstTimestamp, lastTimestamp, ActivityKind.TYPE_NOT_MEASURED);
|
|
}
|
|
}
|
|
|
|
private void createGeneralizedActivities() {
|
|
DashboardFragment.DashboardData.GeneralizedActivity previous = null;
|
|
long midDaySecond = dashboardData.timeFrom + (12 * 60 * 60);
|
|
for (Map.Entry<Long, Integer> activity : activityTimestamps.entrySet()) {
|
|
long timestamp = activity.getKey();
|
|
int activityKind = activity.getValue();
|
|
if (previous == null || previous.activityKind != activityKind || (!mode_24h && timestamp == midDaySecond) || 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) {
|
|
// Retrieve activity data
|
|
dashboardData.generalizedActivities.clear();
|
|
List<GBDevice> devices = GBApplication.app().getDeviceManager().getDevices();
|
|
List<ActivitySample> allActivitySamples = new ArrayList<>();
|
|
List<ActivitySession> stepSessions = new ArrayList<>();
|
|
List<BaseActivitySummary> 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.TYPE_NOT_WORN && (sample.getKind() == ActivityKind.TYPE_NOT_MEASURED || (sample.getKind() & ActivityKind.TYPE_SLEEP) == 0))
|
|
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.TYPE_EXERCISE);
|
|
}
|
|
}
|
|
for (ActivitySession session : stepSessions) {
|
|
addActivity(session.getStartTime().getTime() / 1000, session.getEndTime().getTime() / 1000, ActivityKind.TYPE_ACTIVITY);
|
|
}
|
|
createGeneralizedActivities();
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
protected void onPostExecute(Void unused) {
|
|
super.onPostExecute(unused);
|
|
try {
|
|
draw();
|
|
} catch (IllegalStateException e) {
|
|
LOG.warn("calling draw() failed: " + e.getMessage());
|
|
}
|
|
}
|
|
}
|
|
} |