wangzhibo
4 天以前 ae6f40460dcd56af6c5f60ba52c883854c3bac55
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
/*  Copyright (C) 2023-2024 Martin.JM
 
    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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
 
import android.graphics.Color;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
 
import androidx.core.content.ContextCompat;
 
import com.github.mikephil.charting.animation.Easing;
import com.github.mikephil.charting.charts.Chart;
import com.github.mikephil.charting.charts.LineChart;
import com.github.mikephil.charting.components.LegendEntry;
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.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.util.ArrayList;
import java.util.List;
import java.util.Locale;
 
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
 
// Based on StressChartFragment
 
public class Spo2ChartFragment extends AbstractChartFragment<Spo2ChartFragment.Spo2ChartsData> {
    protected static final Logger LOG = LoggerFactory.getLogger(Spo2ChartFragment.class);
 
    private LineChart mSpo2Chart;
 
    private int BACKGROUND_COLOR;
    private int DESCRIPTION_COLOR;
    private int CHART_TEXT_COLOR;
    private int LEGEND_TEXT_COLOR;
    private int CHART_LINE_COLOR;
 
    private String SPO2_AVERAGE_LABEL;
 
    private final Prefs prefs = GBApplication.getPrefs();
 
    private final boolean CHARTS_SLEEP_RANGE_24H = prefs.getBoolean("chart_sleep_range_24h", false);
    private final boolean SHOW_CHARTS_AVERAGE = prefs.getBoolean("charts_show_average", true);
 
    @Override
    protected void init() {
        BACKGROUND_COLOR = GBApplication.getBackgroundColor(requireContext());
        LEGEND_TEXT_COLOR = DESCRIPTION_COLOR = GBApplication.getTextColor(requireContext());
        CHART_TEXT_COLOR = GBApplication.getSecondaryTextColor(requireContext());
 
        if (prefs.getBoolean("chart_heartrate_color", false)) {
            CHART_LINE_COLOR = ContextCompat.getColor(requireContext(), R.color.chart_heartrate_alternative);
        } else {
            CHART_LINE_COLOR = ContextCompat.getColor(requireContext(), R.color.chart_heartrate);
        }
 
        SPO2_AVERAGE_LABEL = requireContext().getString(R.string.charts_legend_spo2_average);
    }
 
    @Override
    protected Spo2ChartsData refreshInBackground(final ChartsHost chartsHost, final DBHandler db, final GBDevice device) {
        final List<? extends Spo2Sample> samples = getSamples(db, device);
 
        LOG.info("Got {} SpO2 samples", samples.size());
 
        return new Spo2ChartsDataBuilder(samples).build();
    }
 
    protected LineDataSet createDataSet(final List<Entry> values) {
        final LineDataSet lineDataSet = new LineDataSet(values, "SpO2");
        lineDataSet.setColor(CHART_LINE_COLOR);
        lineDataSet.setDrawCircles(false);
        lineDataSet.setLineWidth(2.2f);
        lineDataSet.setFillAlpha(255);
        lineDataSet.setValueTextColor(CHART_TEXT_COLOR);
        lineDataSet.setAxisDependency(YAxis.AxisDependency.LEFT);
        lineDataSet.setValueFormatter(new ValueFormatter() {
            @Override
            public String getFormattedValue(float value) {
                return String.format(Locale.ROOT, "%d", (int) value);
            }
        });
        return lineDataSet;
    }
 
    @Override
    protected void updateChartsnUIThread(final Spo2ChartsData spo2Data) {
        final DefaultChartsData<LineData> chartsData = spo2Data.getChartsData();
        mSpo2Chart.setData(null); // workaround for https://github.com/PhilJay/MPAndroidChart/issues/2317
        mSpo2Chart.getXAxis().setValueFormatter(chartsData.getXValueFormatter());
        mSpo2Chart.setData(chartsData.getData());
        mSpo2Chart.getAxisLeft().removeAllLimitLines();
 
        LOG.debug("SpO2 average: {}", spo2Data.getAverage());
 
        if (spo2Data.getAverage() > 0 && SHOW_CHARTS_AVERAGE) {
            final LimitLine averageLine = new LimitLine(spo2Data.getAverage());
            averageLine.setLineColor(Color.RED);
            averageLine.setLineWidth(1.5f);
            averageLine.enableDashedLine(15f, 10f, 0f);
            mSpo2Chart.getAxisLeft().addLimitLine(averageLine);
        }
 
        mSpo2Chart.getAxisRight().setEnabled(false);
    }
 
    @Override
    public String getTitle() {
        return requireContext().getString(R.string.pref_header_spo2);
    }
 
    @Override
    public boolean isSingleDay() {
        return false;
    }
 
    @Override
    public View onCreateView(final LayoutInflater inflater,
                             final ViewGroup container,
                             final Bundle savedInstanceState) {
        final View rootView = inflater.inflate(R.layout.fragment_charts, container, false);
 
        mSpo2Chart = rootView.findViewById(R.id.activitysleepchart);
 
        setupLineChart();
 
        // refresh immediately instead of use refreshIfVisible(), for perceived performance
        refresh();
 
        return rootView;
    }
 
    private void setupLineChart() {
        mSpo2Chart.setBackgroundColor(BACKGROUND_COLOR);
        mSpo2Chart.getDescription().setTextColor(DESCRIPTION_COLOR);
        configureBarLineChartDefaults(mSpo2Chart);
 
        final XAxis x = mSpo2Chart.getXAxis();
        x.setDrawLabels(true);
        x.setDrawGridLines(false);
        x.setEnabled(true);
        x.setTextColor(CHART_TEXT_COLOR);
        x.setDrawLimitLinesBehindData(true);
 
        final YAxis yAxisLeft = mSpo2Chart.getAxisLeft();
        yAxisLeft.setDrawGridLines(true);
        yAxisLeft.setAxisMaximum(100f);
        yAxisLeft.setAxisMinimum(75f);
        yAxisLeft.setDrawTopYLabelEntry(false);
        yAxisLeft.setTextColor(CHART_TEXT_COLOR);
        yAxisLeft.setEnabled(true);
    }
 
    @Override
    protected void setupLegend(final Chart<?> chart) {
        final List<LegendEntry> legendEntries = new ArrayList<>(2);
 
        final LegendEntry entry = new LegendEntry();
        entry.label = requireContext().getString(R.string.pref_header_spo2);
        entry.formColor = CHART_LINE_COLOR;
        legendEntries.add(entry);
 
        if (SHOW_CHARTS_AVERAGE) {
            final LegendEntry averageEntry = new LegendEntry();
            averageEntry.label = SPO2_AVERAGE_LABEL;
            averageEntry.formColor = Color.RED;
            legendEntries.add(averageEntry);
        }
 
        chart.getLegend().setCustom(legendEntries);
        chart.getLegend().setTextColor(LEGEND_TEXT_COLOR);
    }
 
    @Override
    protected void renderCharts() {
        mSpo2Chart.animateX(ANIM_TIME, Easing.EaseInOutQuart);
    }
 
    private List<? extends Spo2Sample> getSamples(final DBHandler db, final GBDevice device) {
        final int tsStart = getTSStart();
        final int tsEnd = getTSEnd();
        final DeviceCoordinator coordinator = device.getDeviceCoordinator();
        final TimeSampleProvider<? extends Spo2Sample> sampleProvider = coordinator.getSpo2SampleProvider(device, db.getDaoSession());
        return sampleProvider.getAllSamples(tsStart * 1000L, tsEnd * 1000L);
    }
 
    protected class Spo2ChartsDataBuilder {
        private final List<? extends Spo2Sample> samples;
 
        private final TimestampTranslation tsTranslation = new TimestampTranslation();
 
        private final List<Entry> lineEntries = new ArrayList<>();
 
        long averageSum;
        long averageNumSamples;
 
        public Spo2ChartsDataBuilder(final List<? extends Spo2Sample> samples) {
            this.samples = samples;
        }
 
        private void reset() {
            tsTranslation.reset();
            lineEntries.clear();
 
            averageSum = 0;
            averageNumSamples = 0;
        }
 
        private void processSamples() {
            reset();
 
            for (final Spo2Sample sample : samples) {
                processSample(sample);
            }
        }
 
        private void processSample(final Spo2Sample sample) {
            final int ts = tsTranslation.shorten((int) (sample.getTimestamp() / 1000L));
            lineEntries.add(new Entry(ts, sample.getSpo2()));
 
            averageSum += sample.getSpo2();
            averageNumSamples += 1;
        }
 
        public Spo2ChartsData build() {
            processSamples();
 
            final List<ILineDataSet> lineDataSets = new ArrayList<>();
 
            lineDataSets.add(createDataSet(lineEntries));
 
            final LineData lineData = new LineData(lineDataSets);
            final ValueFormatter xValueFormatter = new SampleXLabelFormatter(tsTranslation, "HH:mm");
            final DefaultChartsData<LineData> chartsData = new DefaultChartsData<>(lineData, xValueFormatter);
            return new Spo2ChartsData(chartsData, Math.round((float) averageSum / averageNumSamples));
        }
    }
 
    protected static class Spo2ChartsData extends ChartsData {
        private final DefaultChartsData<LineData> chartsData;
        private final int average;
 
        public Spo2ChartsData(final DefaultChartsData<LineData> chartsData, final int average) {
            this.chartsData = chartsData;
            this.average = average;
        }
 
        public DefaultChartsData<LineData> getChartsData() {
            return chartsData;
        }
 
        public int getAverage() {
            return average;
        }
    }
}