mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-08-20 06:11:22 +02:00
540 lines
20 KiB
Java
540 lines
20 KiB
Java
/* Copyright (C) 2015-2020 Andreas Shimokawa, Carsten Pfeiffer, Cre3per,
|
|
Daniele Gobbetti, Dikay900, Pavel, Pavel Elagin
|
|
|
|
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.charts;
|
|
|
|
import android.content.BroadcastReceiver;
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
import android.content.IntentFilter;
|
|
import android.graphics.Paint;
|
|
import android.os.Bundle;
|
|
import android.view.LayoutInflater;
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
import android.view.WindowManager;
|
|
import android.widget.TextView;
|
|
import android.widget.Toast;
|
|
|
|
import com.github.mikephil.charting.charts.BarLineChartBase;
|
|
import com.github.mikephil.charting.charts.Chart;
|
|
import com.github.mikephil.charting.components.LimitLine;
|
|
import com.github.mikephil.charting.components.XAxis;
|
|
import com.github.mikephil.charting.components.YAxis;
|
|
import com.github.mikephil.charting.data.BarData;
|
|
import com.github.mikephil.charting.data.BarDataSet;
|
|
import com.github.mikephil.charting.data.BarEntry;
|
|
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.utils.Utils;
|
|
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.concurrent.Executors;
|
|
import java.util.concurrent.ScheduledExecutorService;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
import androidx.annotation.Nullable;
|
|
import androidx.fragment.app.FragmentActivity;
|
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
|
import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils;
|
|
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
|
|
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
|
|
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
|
|
|
public class LiveActivityFragment extends AbstractChartFragment {
|
|
private static final Logger LOG = LoggerFactory.getLogger(LiveActivityFragment.class);
|
|
private static final int MAX_STEPS_PER_MINUTE = 300;
|
|
private static final int MIN_STEPS_PER_MINUTE = 60;
|
|
private static final int RESET_COUNT = 10; // reset the max steps per minute value every 10s
|
|
|
|
private BarEntry totalStepsEntry;
|
|
private BarEntry stepsPerMinuteEntry;
|
|
private BarDataSet mStepsPerMinuteData;
|
|
private BarDataSet mTotalStepsData;
|
|
private LineDataSet mHistorySet;
|
|
private BarLineChartBase mStepsPerMinuteHistoryChart;
|
|
private CustomBarChart mStepsPerMinuteCurrentChart;
|
|
private CustomBarChart mTotalStepsChart;
|
|
private TextView mMaxHeartRateView;
|
|
|
|
private final Steps mSteps = new Steps();
|
|
private ScheduledExecutorService pulseScheduler;
|
|
private int maxStepsResetCounter;
|
|
private LineDataSet mHeartRateSet;
|
|
private int mHeartRate;
|
|
private int mMaxHeartRate = 0;
|
|
private TimestampTranslation tsTranslation;
|
|
|
|
private class Steps {
|
|
private int steps;
|
|
private int lastTimestamp;
|
|
private int currentStepsPerMinute;
|
|
private int maxStepsPerMinute;
|
|
private int lastStepsPerMinute;
|
|
|
|
public int getStepsPerMinute(boolean reset) {
|
|
lastStepsPerMinute = currentStepsPerMinute;
|
|
int result = currentStepsPerMinute;
|
|
if (reset) {
|
|
currentStepsPerMinute = 0;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
public int getTotalSteps() {
|
|
return steps;
|
|
}
|
|
|
|
public int getMaxStepsPerMinute() {
|
|
return maxStepsPerMinute;
|
|
}
|
|
|
|
public void updateCurrentSteps(int stepsDelta, int timestamp) {
|
|
try {
|
|
if (steps == 0) {
|
|
steps += stepsDelta;
|
|
lastTimestamp = timestamp;
|
|
return;
|
|
}
|
|
|
|
int timeDelta = timestamp - lastTimestamp;
|
|
currentStepsPerMinute = calculateStepsPerMinute(stepsDelta, timeDelta);
|
|
if (currentStepsPerMinute > maxStepsPerMinute) {
|
|
maxStepsPerMinute = currentStepsPerMinute;
|
|
maxStepsResetCounter = 0;
|
|
}
|
|
steps += stepsDelta;
|
|
lastTimestamp = timestamp;
|
|
} catch (Exception ex) {
|
|
GB.toast(LiveActivityFragment.this.getContext(), ex.getMessage(), Toast.LENGTH_SHORT, GB.ERROR, ex);
|
|
}
|
|
}
|
|
|
|
private int calculateStepsPerMinute(int stepsDelta, int seconds) {
|
|
if (stepsDelta == 0) {
|
|
return 0; // not walking or not enough data per mills?
|
|
}
|
|
if (seconds <= 0) {
|
|
throw new IllegalArgumentException("delta in seconds is <= 0 -- time change?");
|
|
}
|
|
|
|
int oneMinute = 60;
|
|
float factor = oneMinute / seconds;
|
|
int result = (int) (stepsDelta * factor);
|
|
if (result > MAX_STEPS_PER_MINUTE) {
|
|
// ignore, return previous value instead
|
|
result = lastStepsPerMinute;
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
|
@Override
|
|
public void onReceive(Context context, Intent intent) {
|
|
String action = intent.getAction();
|
|
switch (action) {
|
|
case DeviceService.ACTION_REALTIME_SAMPLES: {
|
|
ActivitySample sample = (ActivitySample) intent.getSerializableExtra(DeviceService.EXTRA_REALTIME_SAMPLE);
|
|
addSample(sample);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
private void addSample(ActivitySample sample) {
|
|
int heartRate = sample.getHeartRate();
|
|
int timestamp = tsTranslation.shorten(sample.getTimestamp());
|
|
if (HeartRateUtils.getInstance().isValidHeartRateValue(heartRate)) {
|
|
setCurrentHeartRate(heartRate, timestamp);
|
|
}
|
|
int steps = sample.getSteps();
|
|
if (steps > 0) {
|
|
addEntries(steps, timestamp);
|
|
}
|
|
}
|
|
|
|
private int translateTimestampFrom(Intent intent) {
|
|
return translateTimestamp(intent.getLongExtra(DeviceService.EXTRA_TIMESTAMP, System.currentTimeMillis()));
|
|
}
|
|
|
|
private int translateTimestamp(long tsMillis) {
|
|
int timestamp = (int) (tsMillis / 1000); // translate to seconds
|
|
return tsTranslation.shorten(timestamp); // and shorten
|
|
}
|
|
|
|
private void setCurrentHeartRate(int heartRate, int timestamp) {
|
|
addHistoryDataSet(true);
|
|
mHeartRate = heartRate;
|
|
if (mMaxHeartRate < mHeartRate) {
|
|
mMaxHeartRate = mHeartRate;
|
|
}
|
|
mMaxHeartRateView.setText(getContext().getString(R.string.live_activity_max_heart_rate, heartRate, mMaxHeartRate));
|
|
}
|
|
|
|
private int getCurrentHeartRate() {
|
|
int result = mHeartRate;
|
|
mHeartRate = -1;
|
|
return result;
|
|
}
|
|
|
|
private void addEntries(int steps, int timestamp) {
|
|
mSteps.updateCurrentSteps(steps, timestamp);
|
|
if (++maxStepsResetCounter > RESET_COUNT) {
|
|
maxStepsResetCounter = 0;
|
|
mSteps.maxStepsPerMinute = 0;
|
|
}
|
|
// Or: count down the steps until goal reached? And then flash GOAL REACHED -> Set stretch goal
|
|
LOG.info("Steps: " + steps + ", total: " + mSteps.getTotalSteps() + ", current: " + mSteps.getStepsPerMinute(false));
|
|
|
|
// addEntries();
|
|
}
|
|
|
|
private void addEntries(int timestamp) {
|
|
mTotalStepsChart.setSingleEntryYValue(mSteps.getTotalSteps());
|
|
YAxis stepsPerMinuteCurrentYAxis = mStepsPerMinuteCurrentChart.getAxisLeft();
|
|
int maxStepsPerMinute = mSteps.getMaxStepsPerMinute();
|
|
// int extraRoom = maxStepsPerMinute/5;
|
|
// buggy in MPAndroidChart? Disable.
|
|
// stepsPerMinuteCurrentYAxis.setAxisMaxValue(Math.max(MIN_STEPS_PER_MINUTE, maxStepsPerMinute + extraRoom));
|
|
LimitLine target = new LimitLine(maxStepsPerMinute);
|
|
stepsPerMinuteCurrentYAxis.removeAllLimitLines();
|
|
stepsPerMinuteCurrentYAxis.addLimitLine(target);
|
|
|
|
int stepsPerMinute = mSteps.getStepsPerMinute(true);
|
|
mStepsPerMinuteCurrentChart.setSingleEntryYValue(stepsPerMinute);
|
|
|
|
if (!addHistoryDataSet(false)) {
|
|
return;
|
|
}
|
|
|
|
ChartData data = mStepsPerMinuteHistoryChart.getData();
|
|
if (stepsPerMinute < 0) {
|
|
stepsPerMinute = 0;
|
|
}
|
|
mHistorySet.addEntry(new Entry(timestamp, stepsPerMinute));
|
|
int hr = getCurrentHeartRate();
|
|
if (hr > HeartRateUtils.getInstance().getMinHeartRate()) {
|
|
mHeartRateSet.addEntry(new Entry(timestamp, hr));
|
|
}
|
|
}
|
|
|
|
private boolean addHistoryDataSet(boolean force) {
|
|
if (mStepsPerMinuteHistoryChart.getData() == null) {
|
|
// ignore the first default value to keep the "no-data-description" visible
|
|
if (force || mSteps.getTotalSteps() > 0) {
|
|
LineData data = new LineData();
|
|
data.addDataSet(mHistorySet);
|
|
data.addDataSet(mHeartRateSet);
|
|
mStepsPerMinuteHistoryChart.setData(data);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
@Nullable
|
|
@Override
|
|
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
|
IntentFilter filterLocal = new IntentFilter();
|
|
filterLocal.addAction(DeviceService.ACTION_REALTIME_SAMPLES);
|
|
tsTranslation = new TimestampTranslation();
|
|
|
|
View rootView = inflater.inflate(R.layout.fragment_live_activity, container, false);
|
|
|
|
mStepsPerMinuteCurrentChart = rootView.findViewById(R.id.livechart_steps_per_minute_current);
|
|
mTotalStepsChart = rootView.findViewById(R.id.livechart_steps_total);
|
|
mStepsPerMinuteHistoryChart = rootView.findViewById(R.id.livechart_steps_per_minute_history);
|
|
|
|
totalStepsEntry = new BarEntry(1, 0);
|
|
stepsPerMinuteEntry = new BarEntry(1, 0);
|
|
|
|
mStepsPerMinuteData = setupCurrentChart(mStepsPerMinuteCurrentChart, stepsPerMinuteEntry, getString(R.string.live_activity_current_steps_per_minute));
|
|
mStepsPerMinuteData.setDrawValues(true);
|
|
mStepsPerMinuteData.setValueTextColor(DESCRIPTION_COLOR);
|
|
mTotalStepsData = setupTotalStepsChart(mTotalStepsChart, totalStepsEntry, getString(R.string.live_activity_total_steps));
|
|
mTotalStepsData.setDrawValues(true);
|
|
mTotalStepsData.setValueTextColor(DESCRIPTION_COLOR);
|
|
setupHistoryChart(mStepsPerMinuteHistoryChart);
|
|
mMaxHeartRateView = rootView.findViewById(R.id.livechart_max_heart_rate);
|
|
|
|
LocalBroadcastManager.getInstance(getActivity()).registerReceiver(mReceiver, filterLocal);
|
|
|
|
return rootView;
|
|
}
|
|
|
|
@Override
|
|
public void onPause() {
|
|
enableRealtimeTracking(false);
|
|
super.onPause();
|
|
}
|
|
|
|
@Override
|
|
public void onResume() {
|
|
super.onResume();
|
|
enableRealtimeTracking(true);
|
|
}
|
|
|
|
private ScheduledExecutorService startActivityPulse() {
|
|
ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
|
|
service.scheduleAtFixedRate(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
FragmentActivity activity = LiveActivityFragment.this.getActivity();
|
|
if (activity != null && !activity.isFinishing() && !activity.isDestroyed()) {
|
|
activity.runOnUiThread(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
pulse();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}, 0, getPulseIntervalMillis(), TimeUnit.MILLISECONDS);
|
|
return service;
|
|
}
|
|
|
|
private void stopActivityPulse() {
|
|
if (pulseScheduler != null) {
|
|
pulseScheduler.shutdownNow();
|
|
pulseScheduler = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called in the UI thread.
|
|
*/
|
|
private void pulse() {
|
|
addEntries(translateTimestamp(System.currentTimeMillis()));
|
|
|
|
LineData historyData = (LineData) mStepsPerMinuteHistoryChart.getData();
|
|
if (historyData == null) {
|
|
return;
|
|
}
|
|
|
|
historyData.notifyDataChanged();
|
|
mTotalStepsData.notifyDataSetChanged();
|
|
mStepsPerMinuteData.notifyDataSetChanged();
|
|
mStepsPerMinuteHistoryChart.notifyDataSetChanged();
|
|
|
|
renderCharts();
|
|
|
|
// have to enable it again and again to keep it measuring
|
|
GBApplication.deviceService().onEnableRealtimeHeartRateMeasurement(true);
|
|
}
|
|
|
|
private int getPulseIntervalMillis() {
|
|
return 1000;
|
|
}
|
|
|
|
@Override
|
|
protected void onMadeVisibleInActivity() {
|
|
super.onMadeVisibleInActivity();
|
|
enableRealtimeTracking(true);
|
|
}
|
|
|
|
private void enableRealtimeTracking(boolean enable) {
|
|
if (enable && pulseScheduler != null) {
|
|
// already running
|
|
return;
|
|
}
|
|
|
|
GBApplication.deviceService().onEnableRealtimeSteps(enable);
|
|
GBApplication.deviceService().onEnableRealtimeHeartRateMeasurement(enable);
|
|
if (enable) {
|
|
if (getActivity() != null) {
|
|
getActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
|
}
|
|
pulseScheduler = startActivityPulse();
|
|
} else {
|
|
stopActivityPulse();
|
|
if (getActivity() != null) {
|
|
getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onMadeInvisibleInActivity() {
|
|
enableRealtimeTracking(false);
|
|
super.onMadeInvisibleInActivity();
|
|
}
|
|
|
|
@Override
|
|
public void onDestroyView() {
|
|
onMadeInvisibleInActivity();
|
|
LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(mReceiver);
|
|
super.onDestroyView();
|
|
}
|
|
|
|
private BarDataSet setupCurrentChart(CustomBarChart chart, BarEntry entry, String title) {
|
|
mStepsPerMinuteCurrentChart.getAxisLeft().setAxisMaximum(MAX_STEPS_PER_MINUTE);
|
|
return setupCommonChart(chart, entry, title);
|
|
}
|
|
|
|
private BarDataSet setupCommonChart(CustomBarChart chart, BarEntry entry, String title) {
|
|
chart.setSinglAnimationEntry(entry);
|
|
|
|
// chart.getXAxis().setPosition(XAxis.XAxisPosition.TOP);
|
|
chart.getXAxis().setDrawLabels(false);
|
|
chart.getXAxis().setEnabled(false);
|
|
chart.getXAxis().setTextColor(CHART_TEXT_COLOR);
|
|
chart.getAxisLeft().setTextColor(CHART_TEXT_COLOR);
|
|
|
|
chart.setBackgroundColor(BACKGROUND_COLOR);
|
|
chart.getDescription().setTextColor(DESCRIPTION_COLOR);
|
|
chart.getDescription().setText(title);
|
|
// chart.setNoDataTextDescription("");
|
|
chart.setNoDataText("");
|
|
chart.getAxisRight().setEnabled(false);
|
|
|
|
List<BarEntry> entries = new ArrayList<>();
|
|
List<Integer> colors = new ArrayList<>();
|
|
|
|
entries.add(entry);
|
|
colors.add(akActivity.color);
|
|
colors.add(akActivity.color);
|
|
colors.add(akActivity.color);
|
|
// //we don't want labels
|
|
// xLabels.add("");
|
|
// xLabels.add("");
|
|
// xLabels.add("");
|
|
|
|
BarDataSet set = new BarDataSet(entries, "");
|
|
set.setDrawValues(false);
|
|
set.setColors(colors);
|
|
BarData data = new BarData(set);
|
|
// data.setGroupSpace(0);
|
|
chart.setData(data);
|
|
|
|
chart.getLegend().setEnabled(false);
|
|
|
|
return set;
|
|
}
|
|
|
|
private BarDataSet setupTotalStepsChart(CustomBarChart chart, BarEntry entry, String label) {
|
|
mTotalStepsChart.getAxisLeft().addLimitLine(new LimitLine(GBApplication.getPrefs().getInt(ActivityUser.PREF_USER_STEPS_GOAL, ActivityUser.defaultUserStepsGoal), "ss")); // TODO: use daily goal - already reached steps
|
|
mTotalStepsChart.getAxisLeft().setAxisMinimum(0);
|
|
mTotalStepsChart.setAutoScaleMinMaxEnabled(true);
|
|
return setupCommonChart(chart, entry, label); // at the moment, these look the same
|
|
}
|
|
|
|
private void setupHistoryChart(BarLineChartBase chart) {
|
|
configureBarLineChartDefaults(chart);
|
|
|
|
chart.setTouchEnabled(false); // no zooming or anything, because it's updated all the time
|
|
chart.setBackgroundColor(BACKGROUND_COLOR);
|
|
chart.getDescription().setTextColor(DESCRIPTION_COLOR);
|
|
chart.getDescription().setText(getString(R.string.live_activity_steps_per_minute_history));
|
|
chart.setNoDataText(getString(R.string.live_activity_start_your_activity));
|
|
chart.getLegend().setEnabled(false);
|
|
Paint infoPaint = chart.getPaint(Chart.PAINT_INFO);
|
|
infoPaint.setTextSize(Utils.convertDpToPixel(20f));
|
|
infoPaint.setFakeBoldText(true);
|
|
chart.setPaint(infoPaint, Chart.PAINT_INFO);
|
|
|
|
XAxis x = chart.getXAxis();
|
|
x.setDrawLabels(true);
|
|
x.setDrawGridLines(false);
|
|
x.setEnabled(true);
|
|
x.setTextColor(CHART_TEXT_COLOR);
|
|
x.setValueFormatter(new SampleXLabelFormatter(tsTranslation));
|
|
x.setDrawLimitLinesBehindData(true);
|
|
|
|
YAxis y = chart.getAxisLeft();
|
|
y.setDrawGridLines(false);
|
|
y.setDrawTopYLabelEntry(false);
|
|
y.setTextColor(CHART_TEXT_COLOR);
|
|
y.setEnabled(true);
|
|
y.setAxisMinimum(0);
|
|
|
|
YAxis yAxisRight = chart.getAxisRight();
|
|
yAxisRight.setDrawGridLines(false);
|
|
yAxisRight.setEnabled(true);
|
|
yAxisRight.setDrawLabels(true);
|
|
yAxisRight.setDrawTopYLabelEntry(false);
|
|
yAxisRight.setTextColor(CHART_TEXT_COLOR);
|
|
|
|
mHistorySet = new LineDataSet(new ArrayList<Entry>(), getString(R.string.live_activity_steps_history));
|
|
mHistorySet.setAxisDependency(YAxis.AxisDependency.LEFT);
|
|
mHistorySet.setColor(akActivity.color);
|
|
mHistorySet.setDrawCircles(false);
|
|
mHistorySet.setMode(LineDataSet.Mode.CUBIC_BEZIER);
|
|
mHistorySet.setDrawFilled(true);
|
|
mHistorySet.setDrawValues(false);
|
|
|
|
mHeartRateSet = createHeartrateSet(new ArrayList<Entry>(), getString(R.string.live_activity_heart_rate));
|
|
mHeartRateSet.setDrawValues(false);
|
|
}
|
|
|
|
@Override
|
|
public String getTitle() {
|
|
return getContext().getString(R.string.liveactivity_live_activity);
|
|
}
|
|
|
|
@Override
|
|
protected void showDateBar(boolean show) {
|
|
// never show the data bar
|
|
super.showDateBar(false);
|
|
}
|
|
|
|
@Override
|
|
protected void refresh() {
|
|
// do nothing, we don't have any db interaction
|
|
}
|
|
|
|
@Override
|
|
protected ChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
|
|
@Override
|
|
protected void updateChartsnUIThread(ChartsData chartsData) {
|
|
throw new UnsupportedOperationException();
|
|
}
|
|
|
|
@Override
|
|
protected void renderCharts() {
|
|
mStepsPerMinuteCurrentChart.animateY(150);
|
|
mTotalStepsChart.animateY(150);
|
|
mStepsPerMinuteHistoryChart.invalidate();
|
|
}
|
|
|
|
@Override
|
|
protected List<ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
|
|
throw new UnsupportedOperationException("no db access supported for live activity");
|
|
}
|
|
|
|
@Override
|
|
protected void setupLegend(Chart chart) {
|
|
// no legend
|
|
}
|
|
}
|