diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractSampleProvider.java index 60681bf1c..c78d7e095 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractSampleProvider.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractSampleProvider.java @@ -137,6 +137,12 @@ public abstract class AbstractSampleProvider i return sample; } + /** + * Get the activity samples between two timestamps. Exactly one every minute. + * @param timestamp_from Start timestamp + * @param timestamp_to End timestamp + * @return Exactly one sample for every minute + */ protected List getGBActivitySamples(int timestamp_from, int timestamp_to) { QueryBuilder qb = getSampleDao().queryBuilder(); Property timestampProperty = getTimestampSampleProperty(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiSampleProvider.java index ae609906d..eea8be467 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiSampleProvider.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiSampleProvider.java @@ -16,21 +16,16 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.devices.huawei; -import android.content.SharedPreferences; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; -import java.util.Iterator; import java.util.List; import de.greenrobot.dao.AbstractDao; import de.greenrobot.dao.Property; import de.greenrobot.dao.query.QueryBuilder; -import nodomain.freeyourgadget.gadgetbridge.GBApplication; -import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; @@ -44,11 +39,12 @@ import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySampleD import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; -import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FitnessData; public class HuaweiSampleProvider extends AbstractSampleProvider { /* - * We save all data by saving a marker at the begin and end. + * We save all data by saving a marker at the begin and end. We do not actively use these for + * showing the data at the moment, but the samples are still saved as such, to keep the table + * entries consistent. * Meaning of fields that are not self-explanatory: * - `otherTimestamp` * The timestamp of the other marker, if it's larger this is the begin, otherwise the end @@ -289,296 +285,189 @@ public class HuaweiSampleProvider extends AbstractSampleProvider getGBActivitySamples(int timestamp_from, int timestamp_to) { - // Note that the result of this function has to be sorted by timestamp! - List rawSamples = getRawOrderedActivitySamples(timestamp_from, timestamp_to); - List workoutSamples = getRawOrderedWorkoutSamplesWithHeartRate(timestamp_from, timestamp_to); - - List workoutSpans = getWorkoutSpans(rawSamples, workoutSamples, 5); List processedSamples = new ArrayList<>(); - Iterator itRawSamples = rawSamples.iterator(); - Iterator itWorkoutSamples = workoutSamples.iterator(); - - HuaweiActivitySample nextRawSample = null; - if (itRawSamples.hasNext()) - nextRawSample = itRawSamples.next(); - HuaweiWorkoutDataSample nextWorkoutSample = null; - if (itWorkoutSamples.hasNext()) - nextWorkoutSample = itWorkoutSamples.next(); - - SampleLoopState state = new SampleLoopState(); - if (nextRawSample != null) { - state.deviceId = nextRawSample.getDeviceId(); - state.userId = nextRawSample.getUserId(); + for (int timestamp = timestamp_from; timestamp <= timestamp_to; timestamp += 60) { + processedSamples.add(createDummySample(timestamp)); } - while (nextRawSample != null || nextWorkoutSample != null) { - if (nextRawSample == null || (nextWorkoutSample != null && nextWorkoutSample.getTimestamp() < nextRawSample.getTimestamp())) { - processWorkoutSample(processedSamples, state, nextWorkoutSample); - nextWorkoutSample = itWorkoutSamples.hasNext() ? itWorkoutSamples.next() : null; - } else { - boolean sampleInWorkout = isInWorkout(workoutSpans, nextRawSample.getTimestamp()); - if (sampleInWorkout) { - nextRawSample.setHeartRate(ActivitySample.NOT_MEASURED); - nextRawSample.setRawIntensity(0); - } - processRawSample(processedSamples, state, nextRawSample); - nextRawSample = itRawSamples.hasNext() ? itRawSamples.next() : null; - } - } - processedSamples = interpolate(processedSamples); + overlayActivitySamples(processedSamples, timestamp_from, timestamp_to); + overlayWorkoutSamples(processedSamples, timestamp_from, timestamp_to); return processedSamples; } - /* - * Calculates the timespans: [start, end] of workouts - * Normal activities should not be processed when in middle of workout - **/ - private List getWorkoutSpans(List activity, List workout, int threshold) { - List validActivitySpans = new ArrayList<>(); - - Iterator activityIterator = activity.iterator(); - Iterator workoutIterator = workout.iterator(); - - HuaweiActivitySample currentActivity = activityIterator.hasNext() ? activityIterator.next() : null; - HuaweiWorkoutDataSample currentWorkout = workoutIterator.hasNext() ? workoutIterator.next() : null; - - int consecutiveActivityCount = 0; - Integer spanStart = null; - - int workoutEnd = 0; - while (currentActivity != null || currentWorkout != null) { - if (currentWorkout == null || (currentActivity != null && currentActivity.getTimestamp() < currentWorkout.getTimestamp())) { - // handle activity - if (spanStart != null) { - // We're in workout, check for activity interruption - consecutiveActivityCount++; - if (consecutiveActivityCount > threshold) { - // Enough activity samples to interrupt the workout - validActivitySpans.add(new int[]{spanStart, workoutEnd}); - spanStart = null; - consecutiveActivityCount = 0; - } - } - currentActivity = activityIterator.hasNext() ? activityIterator.next() : null; - } else { - // handle workout - if (spanStart == null) { - spanStart = currentWorkout.getTimestamp(); - } - workoutEnd = currentWorkout.getTimestamp(); - consecutiveActivityCount = 0; - currentWorkout = workoutIterator.hasNext() ? workoutIterator.next() : null; - } - } - - // If there's an open valid span at the end, close it - if (spanStart != null) { - validActivitySpans.add(new int[]{spanStart, workoutEnd}); - } - - return validActivitySpans; - } - - private boolean isInWorkout(List validSpans, int timestamp) { - for (int[] span : validSpans) { - if (timestamp > span[0] && timestamp < span[1]) { - return true; - } - } - return false; - } - - private List interpolate(List processedSamples) { - List retv = new ArrayList<>(); - - if (processedSamples.isEmpty()) - return retv; - - HuaweiActivitySample lastSample = processedSamples.get(0); - retv.add(lastSample); - for (int i = 1; i < processedSamples.size() - 1; i++) { - HuaweiActivitySample sample = processedSamples.get(i); - - int timediff = sample.getTimestamp() - lastSample.getTimestamp(); - if (timediff > 60) { - if (lastSample.getRawKind() != -1 && sample.getRawKind() != lastSample.getRawKind()) { - HuaweiActivitySample postSample = new HuaweiActivitySample( - lastSample.getTimestamp() + 1, - lastSample.getDeviceId(), - lastSample.getUserId(), - 0, - (byte) 0x00, - ActivitySample.NOT_MEASURED, - 0, - ActivitySample.NOT_MEASURED, - ActivitySample.NOT_MEASURED, - ActivitySample.NOT_MEASURED, - ActivitySample.NOT_MEASURED, - ActivitySample.NOT_MEASURED - ); - postSample.setProvider(this); - retv.add(postSample); - } - - if (sample.getRawKind() != -1 && sample.getRawKind() != lastSample.getRawKind()) { - HuaweiActivitySample preSample = new HuaweiActivitySample( - sample.getTimestamp() - 1, - sample.getDeviceId(), - sample.getUserId(), - 0, - (byte) 0x00, - ActivitySample.NOT_MEASURED, - 0, - ActivitySample.NOT_MEASURED, - ActivitySample.NOT_MEASURED, - ActivitySample.NOT_MEASURED, - ActivitySample.NOT_MEASURED, - ActivitySample.NOT_MEASURED - ); - preSample.setProvider(this); - retv.add(preSample); - } - } - - retv.add(sample); - lastSample = sample; - } - - if (lastSample.getRawKind() != -1) { - HuaweiActivitySample postSample = new HuaweiActivitySample( - lastSample.getTimestamp() + 1, - lastSample.getDeviceId(), - lastSample.getUserId(), - 0, - (byte) 0x00, - ActivitySample.NOT_MEASURED, - 0, - ActivitySample.NOT_MEASURED, - ActivitySample.NOT_MEASURED, - ActivitySample.NOT_MEASURED, - ActivitySample.NOT_MEASURED, - ActivitySample.NOT_MEASURED - ); - postSample.setProvider(this); - retv.add(postSample); - } - - return retv; - } - - private void processRawSample(List processedSamples, SampleLoopState state, HuaweiActivitySample sample) { - // Filter on Source 0x0d, Type 0x01, until we know what it is and how we should handle them. - // Just showing them currently has some issues. - if (sample.getSource() == FitnessData.MessageData.sleepId && sample.getRawKind() == RawTypes.UNKNOWN) - return; - - HuaweiActivitySample lastSample = null; - - boolean isStartMarker = sample.getTimestamp() < sample.getOtherTimestamp(); - - // Handle preferences for wakeup status ignore - can fix some quirks on some devices - if (sample.getRawKind() == 0x08) { - SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()); - if (isStartMarker && prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_IGNORE_WAKEUP_STATUS_START, false)) - return; - if (!isStartMarker && prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_IGNORE_WAKEUP_STATUS_END, false)) - return; - } - - // Backdate the end marker by one - otherwise the interpolation fails - if (sample.getTimestamp() > sample.getOtherTimestamp()) - sample.setTimestamp(sample.getTimestamp() - 1); - - if (!processedSamples.isEmpty()) - lastSample = processedSamples.get(processedSamples.size() - 1); - if (lastSample != null && lastSample.getTimestamp() == sample.getTimestamp()) { - // Merge the samples - only if there isn't any data yet, except the kind - - if (lastSample.getRawKind() == -1) - lastSample.setRawKind(sample.getRawKind()); - // Do overwrite the kind if the new sample is a starting sample - if (isStartMarker && sample.getRawKind() != -1) { - lastSample.setRawKind(sample.getRawKind()); - lastSample.setOtherTimestamp(sample.getOtherTimestamp()); // Necessary for interpolation - } - - if (lastSample.getRawIntensity() == -1) - lastSample.setRawIntensity(sample.getRawIntensity()); - if (lastSample.getSteps() == -1) - lastSample.setSteps(sample.getSteps()); - if (lastSample.getCalories() == -1) - lastSample.setCalories(sample.getCalories()); - if (lastSample.getDistance() == -1) - lastSample.setDistance(sample.getDistance()); - if (lastSample.getSpo() == -1) - lastSample.setSpo(sample.getSpo()); - if (lastSample.getHeartRate() == -1) - lastSample.setHeartRate(sample.getHeartRate()); - if (lastSample.getSource() != sample.getSource()) - lastSample.setSource((byte) 0x00); - } else { - if (state.sleepModifier != 0) - sample.setRawKind(state.sleepModifier); - processedSamples.add(sample); - } - - if ( - (sample.getSource() == FitnessData.MessageData.sleepId || sample.getSource() == 0x0a) // Sleep sources - && (sample.getRawKind() == RawTypes.LIGHT_SLEEP || sample.getRawKind() == RawTypes.DEEP_SLEEP) // Sleep types - ) { - if (isStartMarker) - state.sleepModifier = sample.getRawKind(); - else - state.sleepModifier = 0; - } - } - - private void processWorkoutSample(List processedSamples, SampleLoopState state, HuaweiWorkoutDataSample workoutSample) { - processRawSample(processedSamples, state, convertWorkoutSampleToActivitySample(workoutSample, state)); - } - - private HuaweiActivitySample convertWorkoutSampleToActivitySample(HuaweiWorkoutDataSample workoutSample, SampleLoopState state) { - int hr = workoutSample.getHeartRate() & 0xFF; - HuaweiActivitySample newSample = new HuaweiActivitySample( - workoutSample.getTimestamp(), - state.deviceId, - state.userId, + private HuaweiActivitySample createDummySample(int timestamp) { + HuaweiActivitySample activitySample = new HuaweiActivitySample( + timestamp, + -1, + -1, 0, (byte) 0x00, ActivitySample.NOT_MEASURED, + 0, ActivitySample.NOT_MEASURED, ActivitySample.NOT_MEASURED, ActivitySample.NOT_MEASURED, ActivitySample.NOT_MEASURED, - ActivitySample.NOT_MEASURED, - hr - ); - newSample.setProvider(this); - return newSample; + ActivitySample.NOT_MEASURED); + activitySample.setProvider(this); + return activitySample; + } + + /* + * For every activity sample, it adds the data into the following processed sample. + * If there are multiple activity samples, the steps, calories, and distance is added together. + * For the SpO and HR only the last value is used. + */ + private void overlayActivitySamples(List processedSamples, int timestamp_from, int timestamp_to) { + List activitySamples = getRawOrderedActivitySamples(timestamp_from, timestamp_to); + + int currentIndex = 0; + + boolean hasData = false; + + int stepCount = ActivitySample.NOT_MEASURED; + int calorieCount = ActivitySample.NOT_MEASURED; + int distanceCount = ActivitySample.NOT_MEASURED; + + int lastSpo = ActivitySample.NOT_MEASURED; + int lastHr = ActivitySample.NOT_MEASURED; + + int stateModifier = ActivitySample.NOT_MEASURED; + + for (HuaweiActivitySample activitySample : activitySamples) { + // Skip the processed samples that are before this activity sample + while (activitySample.getTimestamp() > processedSamples.get(currentIndex).getTimestamp()) { + // Add data to current index sample + if (hasData || stateModifier != ActivitySample.NOT_MEASURED) + processedSamples.get(currentIndex).setRawIntensity(1); + processedSamples.get(currentIndex).setSteps(stepCount); + processedSamples.get(currentIndex).setCalories(calorieCount); + processedSamples.get(currentIndex).setDistance(distanceCount); + processedSamples.get(currentIndex).setSpo(lastSpo); + processedSamples.get(currentIndex).setHeartRate(lastHr); + processedSamples.get(currentIndex).setRawKind(stateModifier); + + // Reset counters + hasData = false; + stepCount = ActivitySample.NOT_MEASURED; + calorieCount = ActivitySample.NOT_MEASURED; + distanceCount = ActivitySample.NOT_MEASURED; + lastSpo = ActivitySample.NOT_MEASURED; + lastHr = ActivitySample.NOT_MEASURED; + + currentIndex += 1; + if (currentIndex > processedSamples.size()) + return; + } + + // Update data + if (activitySample.getSteps() != ActivitySample.NOT_MEASURED) { + if (stepCount == ActivitySample.NOT_MEASURED) + stepCount = 0; + stepCount += activitySample.getSteps(); + hasData = true; + } + if (activitySample.getCalories() != ActivitySample.NOT_MEASURED) { + if (calorieCount == ActivitySample.NOT_MEASURED) + calorieCount = 0; + calorieCount += activitySample.getCalories(); + hasData = true; + } + if (activitySample.getDistance() != ActivitySample.NOT_MEASURED) { + if (distanceCount == ActivitySample.NOT_MEASURED) + distanceCount = 0; + distanceCount += activitySample.getDistance(); + hasData = true; + } + if (activitySample.getSpo() != ActivitySample.NOT_MEASURED) { + lastSpo = activitySample.getSpo(); + hasData = true; + } + if (activitySample.getHeartRate() != ActivitySample.NOT_MEASURED) { + lastHr = activitySample.getHeartRate(); + hasData = true; + } + if (activitySample.getRawKind() != ActivitySample.NOT_MEASURED) { + if (activitySample.getTimestamp() < activitySample.getOtherTimestamp()) { + // Starting of modifier + stateModifier = activitySample.getRawKind(); + } else { + // End of modifier, remove it if it was for the same state + if (activitySample.getRawKind() == stateModifier) + stateModifier = ActivitySample.NOT_MEASURED; + } + } + } + + // If there is still data, it has to be part of the next index of processed samples + currentIndex += 1; + if (currentIndex >= processedSamples.size()) + return; + if (hasData || stateModifier != ActivitySample.NOT_MEASURED) + processedSamples.get(currentIndex).setRawIntensity(10); + processedSamples.get(currentIndex).setSteps(stepCount); + processedSamples.get(currentIndex).setCalories(calorieCount); + processedSamples.get(currentIndex).setDistance(distanceCount); + processedSamples.get(currentIndex).setSpo(lastSpo); + processedSamples.get(currentIndex).setHeartRate(lastHr); + processedSamples.get(currentIndex).setRawKind(stateModifier); + } + + /* + * For every workout sample, it adds the data into the following processed sample. + * It also detects if it is still in the same workout, and resets the HR and intensity for the + * samples in between, see #4126 for the reasoning. + * NOTE: Huawei devices tend to generate a lot more data - mine up to every 5 seconds. Most of + * this is lost in the conversion to data by the minute. It only shows the most recent value. + */ + private void overlayWorkoutSamples(List processedSamples, int timestamp_from, int timestamp_to) { + int currentIndex = 0; + + int lastHr = ActivitySample.NOT_MEASURED; + + List workoutSamples = getRawOrderedWorkoutSamplesWithHeartRate(timestamp_from, timestamp_to); + + for (int i = 0; i < workoutSamples.size(); i++) { + // Look ahead to see if this is still the same workout + boolean inWorkout = i != 0 && workoutSamples.get(i).getWorkoutId() == workoutSamples.get(i - 1).getWorkoutId(); + + // Skip the processed sample that are before this workout sample + while (workoutSamples.get(i).getTimestamp() > processedSamples.get(currentIndex).getTimestamp()) { + if (inWorkout) { + processedSamples.get(currentIndex).setHeartRate(lastHr); + processedSamples.get(currentIndex).setRawIntensity(0); + } + + // Reset + lastHr = ActivitySample.NOT_MEASURED; + + currentIndex += 1; + if (currentIndex > processedSamples.size()) + return; + } + + if (workoutSamples.get(i).getHeartRate() != ActivitySample.NOT_MEASURED) + lastHr = workoutSamples.get(i).getHeartRate() & 0xFF; + } + + // If there is still data, it has to be part of the next index of processed samples + // Data being present implies it's still in a workout + currentIndex += 1; + if (currentIndex >= processedSamples.size()) + return; + if (lastHr != ActivitySample.NOT_MEASURED) { + processedSamples.get(currentIndex).setHeartRate(lastHr); + processedSamples.get(currentIndex).setRawIntensity(0); + } } }