mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-07-08 22:51:37 +02:00
320 lines
14 KiB
Java
320 lines
14 KiB
Java
|
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||
|
|
||
|
import android.content.Context;
|
||
|
import android.os.Build;
|
||
|
import android.os.Bundle;
|
||
|
import android.text.format.DateUtils;
|
||
|
import android.view.LayoutInflater;
|
||
|
import android.view.View;
|
||
|
import android.view.ViewGroup;
|
||
|
import android.widget.LinearLayout;
|
||
|
import android.widget.ProgressBar;
|
||
|
import android.widget.TextView;
|
||
|
import android.widget.Toast;
|
||
|
|
||
|
import androidx.annotation.Nullable;
|
||
|
import androidx.fragment.app.DialogFragment;
|
||
|
import androidx.fragment.app.FragmentActivity;
|
||
|
|
||
|
import org.slf4j.Logger;
|
||
|
import org.slf4j.LoggerFactory;
|
||
|
|
||
|
import java.util.Calendar;
|
||
|
import java.util.Date;
|
||
|
|
||
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||
|
import nodomain.freeyourgadget.gadgetbridge.database.DBAccess;
|
||
|
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||
|
import nodomain.freeyourgadget.gadgetbridge.model.DailyTotals;
|
||
|
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
||
|
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||
|
|
||
|
public class StepStreaksDashboard extends DialogFragment {
|
||
|
protected static final Logger LOG = LoggerFactory.getLogger(StepStreaksDashboard.class);
|
||
|
GBDevice gbDevice;
|
||
|
int stepsGoal;
|
||
|
boolean cancelTasks = false;
|
||
|
boolean backgroundTaskFinished = false;
|
||
|
private View fragmentView;
|
||
|
private final StepsStreaks stepsStreaks = new StepsStreaks();
|
||
|
private static final String GOAL = "goal";
|
||
|
private static final String PERIOD_CURRENT = "current";
|
||
|
private static final String PERIOD_TOTALS = "totals";
|
||
|
private static final int MAX_YEAR = 2015;
|
||
|
|
||
|
public StepStreaksDashboard() {
|
||
|
|
||
|
}
|
||
|
|
||
|
//Calculates some stats for longest streak (daily steps goal being reached for subsequent days
|
||
|
//without interruption (day with steps less then goal)
|
||
|
//Possible improvements/nice to haves:
|
||
|
//- cache values until new activity fetch is performed
|
||
|
//- create a parcel to allow screen rotation without recalculation
|
||
|
//- read the goals from the USER_ATTRIBUTES table. But, this would also require to be able
|
||
|
//to edit/add values there...
|
||
|
|
||
|
public static StepStreaksDashboard newInstance(int goal, GBDevice device) {
|
||
|
|
||
|
StepStreaksDashboard fragment = new StepStreaksDashboard();
|
||
|
Bundle args = new Bundle();
|
||
|
args.putInt(GOAL, goal);
|
||
|
args.putParcelable(GBDevice.EXTRA_DEVICE, device);
|
||
|
fragment.setArguments(args);
|
||
|
return fragment;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||
|
return inflater.inflate(R.layout.steps_streaks_dashboard, container);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onStop() {
|
||
|
super.onStop();
|
||
|
cancelTasks = true;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void onDestroy() {
|
||
|
super.onDestroy();
|
||
|
cancelTasks = true;
|
||
|
}
|
||
|
|
||
|
|
||
|
@Override
|
||
|
public void onViewCreated(final View view, @Nullable Bundle savedInstanceState) {
|
||
|
super.onViewCreated(view, savedInstanceState);
|
||
|
|
||
|
stepsGoal = getArguments().getInt(GOAL, 0);
|
||
|
gbDevice = getArguments().getParcelable(GBDevice.EXTRA_DEVICE);
|
||
|
fragmentView = view;
|
||
|
if (gbDevice == null) {
|
||
|
throw new IllegalArgumentException("Must provide a device when invoking this activity");
|
||
|
}
|
||
|
createTaskCalculateLatestStepsStreak("Visualizing data current", getActivity(), PERIOD_CURRENT).execute();
|
||
|
createTaskCalculateLatestStepsStreak("Visualizing data maximum", getActivity(), PERIOD_TOTALS).execute();
|
||
|
}
|
||
|
|
||
|
|
||
|
void indicate_progress(boolean inProgress) {
|
||
|
ProgressBar step_streak_dashboard_loading_circle = fragmentView.findViewById(R.id.step_streak_dashboard_loading_circle);
|
||
|
if (inProgress) {
|
||
|
step_streak_dashboard_loading_circle.setAlpha(0.4f); //make it a bit softer
|
||
|
} else {
|
||
|
step_streak_dashboard_loading_circle.setAlpha(0);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void populateData() {
|
||
|
|
||
|
LinearLayout current = getView().findViewById(R.id.step_streak_current_layout);
|
||
|
TextView days_current = current.findViewById(R.id.step_streak_days_value);
|
||
|
TextView average_current = current.findViewById(R.id.step_streak_average_value);
|
||
|
TextView total_current = current.findViewById(R.id.step_streak_total_value);
|
||
|
TextView date_current_value = current.findViewById(R.id.step_streak_current_date_value);
|
||
|
|
||
|
LinearLayout maximum = getView().findViewById(R.id.step_streak_maximum_layout);
|
||
|
TextView days_maximum = maximum.findViewById(R.id.step_streak_days_value);
|
||
|
TextView average_maximum = maximum.findViewById(R.id.step_streak_average_value);
|
||
|
TextView total_maximum = maximum.findViewById(R.id.step_streak_total_value);
|
||
|
TextView date_maximum_value = maximum.findViewById(R.id.step_streak_maximum_date_value);
|
||
|
|
||
|
LinearLayout total = getView().findViewById(R.id.step_streak_total_layout);
|
||
|
TextView days_total = total.findViewById(R.id.step_streak_days_value);
|
||
|
TextView days_total_label = total.findViewById(R.id.step_streak_days_label);
|
||
|
TextView total_total = total.findViewById(R.id.step_streak_total_value);
|
||
|
TextView date_total_value = total.findViewById(R.id.step_streak_total_date_value);
|
||
|
TextView date_total_label = total.findViewById(R.id.step_streak_total_label);
|
||
|
|
||
|
if (stepsStreaks.current.days > 0) {
|
||
|
current.setVisibility(View.VISIBLE);
|
||
|
days_current.setText(Integer.toString(stepsStreaks.current.days));
|
||
|
average_current.setText(Integer.toString(stepsStreaks.current.steps / stepsStreaks.current.days));
|
||
|
total_current.setText(Integer.toString(stepsStreaks.current.steps));
|
||
|
|
||
|
Date startDate = new Date(stepsStreaks.current.timestamp * 1000L);
|
||
|
Date endDate = DateTimeUtils.shiftByDays(startDate, stepsStreaks.current.days - 1); //first day is 1 not 0
|
||
|
date_current_value.setText(DateTimeUtils.formatDateRange(startDate, endDate));
|
||
|
}
|
||
|
|
||
|
if (stepsStreaks.maximum.days > 0) {
|
||
|
maximum.setVisibility(View.VISIBLE);
|
||
|
days_maximum.setText(Integer.toString(stepsStreaks.maximum.days));
|
||
|
average_maximum.setText(Integer.toString(stepsStreaks.maximum.steps / stepsStreaks.maximum.days));
|
||
|
total_maximum.setText(Integer.toString(stepsStreaks.maximum.steps));
|
||
|
|
||
|
Date startDate = new Date(stepsStreaks.maximum.timestamp * 1000L);
|
||
|
Date endDate = DateTimeUtils.shiftByDays(startDate, stepsStreaks.maximum.days - 1); //first day is 1 not 0
|
||
|
date_maximum_value.setText(DateTimeUtils.formatDateRange(startDate, endDate));
|
||
|
}
|
||
|
if (stepsStreaks.total.steps > 0 || backgroundTaskFinished) {
|
||
|
total.setVisibility(View.VISIBLE);
|
||
|
days_total_label.setText(R.string.steps_streaks_achievement_rate);
|
||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||
|
//labels here have diferent meaning, so we must also add proper hint
|
||
|
days_total_label.setTooltipText(getString(R.string.steps_streaks_total_days_hint_totals));
|
||
|
days_total.setTooltipText(getString(R.string.steps_streaks_total_days_hint_totals));
|
||
|
date_total_label.setTooltipText(getString(R.string.steps_streaks_total_steps_hint_totals));
|
||
|
}
|
||
|
|
||
|
days_total.setText(String.format("%.1f%%", 0.0));
|
||
|
if (stepsStreaks.total.total_days > 0) {
|
||
|
days_total.setText(String.format("%.1f%%", (float) stepsStreaks.total.days / stepsStreaks.total.total_days * 100));
|
||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||
|
total_total.setTooltipText(String.format(getString(R.string.steps_streaks_total_steps_average_hint), stepsStreaks.total.steps / stepsStreaks.total.total_days));
|
||
|
}
|
||
|
|
||
|
}
|
||
|
if (stepsStreaks.total.timestamp > 0) {
|
||
|
date_total_value.setVisibility(View.VISIBLE);
|
||
|
date_total_value.setText(String.format(getString(R.string.steps_streaks_since_date), DateTimeUtils.formatDate(new Date(stepsStreaks.total.timestamp * 1000L))));
|
||
|
} else {
|
||
|
date_total_value.setVisibility(View.GONE);
|
||
|
}
|
||
|
total_total.setText(Integer.toString(stepsStreaks.total.steps));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
protected TaskCalculateLatestStepsStreak createTaskCalculateLatestStepsStreak(String taskName, Context context, String period) {
|
||
|
return new TaskCalculateLatestStepsStreak(taskName, context, period);
|
||
|
}
|
||
|
|
||
|
public class TaskCalculateLatestStepsStreak extends DBAccess {
|
||
|
String period;
|
||
|
|
||
|
public TaskCalculateLatestStepsStreak(String taskName, Context context, String period) {
|
||
|
super(taskName, context);
|
||
|
this.period = period;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
protected void doInBackground(DBHandler db) {
|
||
|
switch (period) {
|
||
|
case PERIOD_CURRENT:
|
||
|
calculateStreakData(db, PERIOD_CURRENT, gbDevice, stepsGoal);
|
||
|
|
||
|
break;
|
||
|
case PERIOD_TOTALS:
|
||
|
calculateStreakData(db, PERIOD_TOTALS, gbDevice, stepsGoal);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
protected void onPreExecute() {
|
||
|
indicate_progress(true);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
protected void onPostExecute(Object o) {
|
||
|
super.onPostExecute(o);
|
||
|
FragmentActivity activity = getActivity();
|
||
|
if (activity != null && !activity.isFinishing() && !activity.isDestroyed()) {
|
||
|
if (period.equals(PERIOD_TOTALS)) {
|
||
|
backgroundTaskFinished = true;
|
||
|
indicate_progress(false);
|
||
|
}
|
||
|
populateData();
|
||
|
} else {
|
||
|
LOG.info("Not filling data because activity is not available anymore");
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void calculateStreakData(DBHandler db, String period, GBDevice device, int goal) {
|
||
|
Calendar day = Calendar.getInstance();
|
||
|
int streak_steps = 0;
|
||
|
int streak_days = 0;
|
||
|
int timestamp = 0;
|
||
|
|
||
|
int all_step_days = 0;
|
||
|
int all_streak_days = 0;
|
||
|
int all_steps = 0;
|
||
|
int firstDataTimestamp = 0;
|
||
|
|
||
|
DailyTotals dailyTotals = new DailyTotals();
|
||
|
ActivitySample firstSample = dailyTotals.getFirstSample(db, device);
|
||
|
if (firstSample == null) { //no data at all
|
||
|
return;
|
||
|
}
|
||
|
Calendar firstDate = Calendar.getInstance();
|
||
|
firstDate.setTime(DateTimeUtils.shiftByDays(new Date(firstSample.getTimestamp() * 1000L), -1));
|
||
|
//go one day back, to ensure we are before the first day, to calculate first day data as well
|
||
|
|
||
|
while (true) {
|
||
|
if (cancelTasks) {
|
||
|
GB.toast("Cancelling background jobs", Toast.LENGTH_SHORT, GB.INFO);
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
long[] daily_data = dailyTotals.getDailyTotalsForDevice(device, day, db);
|
||
|
int steps_this_day = (int) daily_data[0];
|
||
|
|
||
|
if (steps_this_day > 0) {
|
||
|
all_step_days++;
|
||
|
all_steps += steps_this_day;
|
||
|
firstDataTimestamp = (int) (day.getTimeInMillis() / 1000);
|
||
|
}
|
||
|
|
||
|
if (steps_this_day >= goal) {
|
||
|
streak_steps += steps_this_day;
|
||
|
streak_days++;
|
||
|
all_streak_days++;
|
||
|
timestamp = (int) (day.getTimeInMillis() / 1000);
|
||
|
Date newDate = DateTimeUtils.shiftByDays(new Date(day.getTimeInMillis()), -1);
|
||
|
day.setTime(newDate);
|
||
|
} else if (DateUtils.isToday(day.getTimeInMillis())) {
|
||
|
//if goal is not reached today, we might still get our steps later
|
||
|
// so do not count this day but do not interrupt
|
||
|
Date newDate = DateTimeUtils.shiftByDays(new Date(day.getTimeInMillis()), -1);
|
||
|
day.setTime(newDate);
|
||
|
} else {
|
||
|
if (period.equals(PERIOD_CURRENT)) {
|
||
|
stepsStreaks.current.days = streak_days;
|
||
|
stepsStreaks.current.steps = streak_steps;
|
||
|
stepsStreaks.current.timestamp = timestamp;
|
||
|
return;
|
||
|
} else if (period.equals(PERIOD_TOTALS)) {
|
||
|
//reset max
|
||
|
if (streak_days > stepsStreaks.maximum.days) {
|
||
|
stepsStreaks.maximum.steps = streak_steps;
|
||
|
stepsStreaks.maximum.days = streak_days;
|
||
|
stepsStreaks.maximum.timestamp = timestamp;
|
||
|
}
|
||
|
stepsStreaks.total.steps = all_steps;
|
||
|
stepsStreaks.total.days = all_streak_days;
|
||
|
stepsStreaks.total.total_days = all_step_days;
|
||
|
stepsStreaks.total.timestamp = firstDataTimestamp;
|
||
|
|
||
|
streak_days = 0;
|
||
|
streak_steps = 0;
|
||
|
Date newDate = DateTimeUtils.shiftByDays(new Date(day.getTimeInMillis()), -1);
|
||
|
day.setTime(newDate);
|
||
|
if (day.before(firstDate) || day.get(Calendar.YEAR) < MAX_YEAR) {
|
||
|
//avoid rolling back too far, if the data has a timestamp too far into future
|
||
|
//we could make this date configurable if needed for people who imported old data
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static class StepsStreak {
|
||
|
private int days = 0;
|
||
|
private int steps = 0;
|
||
|
private int timestamp;
|
||
|
private int total_days = 0;
|
||
|
}
|
||
|
|
||
|
private class StepsStreaks {
|
||
|
private StepsStreak current = new StepsStreak();
|
||
|
private StepsStreak maximum = new StepsStreak();
|
||
|
private StepsStreak total = new StepsStreak();
|
||
|
}
|
||
|
}
|
||
|
|