mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-06-14 09:00:04 +02:00
492 lines
21 KiB
Java
492 lines
21 KiB
Java
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit;
|
|
|
|
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.ACTIVE_SECONDS;
|
|
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.ASCENT_DISTANCE;
|
|
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.CALORIES_BURNT;
|
|
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.DESCENT_DISTANCE;
|
|
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.DISTANCE_METERS;
|
|
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_KCAL;
|
|
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_METERS;
|
|
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS;
|
|
|
|
import android.content.Context;
|
|
import android.widget.Toast;
|
|
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.util.ArrayList;
|
|
import java.util.Date;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Objects;
|
|
import java.util.SortedMap;
|
|
import java.util.TreeMap;
|
|
|
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
|
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
|
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
|
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminActivitySampleProvider;
|
|
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminEventSampleProvider;
|
|
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminSleepStageSampleProvider;
|
|
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminSpo2SampleProvider;
|
|
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminStressSampleProvider;
|
|
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
|
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
|
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
|
import nodomain.freeyourgadget.gadgetbridge.entities.GarminActivitySample;
|
|
import nodomain.freeyourgadget.gadgetbridge.entities.GarminEventSample;
|
|
import nodomain.freeyourgadget.gadgetbridge.entities.GarminSleepStageSample;
|
|
import nodomain.freeyourgadget.gadgetbridge.entities.GarminSpo2Sample;
|
|
import nodomain.freeyourgadget.gadgetbridge.entities.GarminStressSample;
|
|
import nodomain.freeyourgadget.gadgetbridge.entities.User;
|
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
|
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData;
|
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminTimeUtils;
|
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionSleepStage;
|
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitEvent;
|
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitFileId;
|
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitMonitoring;
|
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitRecord;
|
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitSession;
|
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitSleepStage;
|
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitSpo2;
|
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitSport;
|
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitStressLevel;
|
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitTimeInZone;
|
|
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
|
|
|
public class FitImporter {
|
|
private static final Logger LOG = LoggerFactory.getLogger(FitImporter.class);
|
|
|
|
private final Context context;
|
|
private final GBDevice gbDevice;
|
|
|
|
private final List<GarminActivitySample> activitySamples = new ArrayList<>();
|
|
private final SortedMap<Integer, List<GarminActivitySample>> activitySamplesPerTimestamp = new TreeMap<>();
|
|
private final List<GarminStressSample> stressSamples = new ArrayList<>();
|
|
private final List<GarminSpo2Sample> spo2samples = new ArrayList<>();
|
|
private final List<GarminEventSample> events = new ArrayList<>();
|
|
private final List<GarminSleepStageSample> sleepStageSamples = new ArrayList<>();
|
|
private final List<FitTimeInZone> timesInZone = new ArrayList<>();
|
|
private final List<ActivityPoint> activityPoints = new ArrayList<>();
|
|
private final Map<Integer, Integer> unknownRecords = new HashMap<>();
|
|
private FitFileId fileId = null;
|
|
private FitSession session = null;
|
|
private FitSport sport = null;
|
|
|
|
public FitImporter(final Context context, final GBDevice gbDevice) {
|
|
this.context = context;
|
|
this.gbDevice = gbDevice;
|
|
}
|
|
|
|
public void importFile(final File file) throws IOException {
|
|
reset();
|
|
|
|
final FitFile fitFile = FitFile.parseIncoming(file);
|
|
|
|
for (final RecordData record : fitFile.getRecords()) {
|
|
final Long ts = record.getComputedTimestamp();
|
|
|
|
if (record instanceof FitFileId) {
|
|
final FitFileId newFileId = (FitFileId) record;
|
|
LOG.debug("File ID: {}", newFileId);
|
|
if (fileId != null) {
|
|
// Should not happen
|
|
LOG.warn("Already had a file ID: {}", fileId);
|
|
}
|
|
fileId = newFileId;
|
|
} else if (record instanceof FitStressLevel) {
|
|
final Integer stress = ((FitStressLevel) record).getStressLevelValue();
|
|
if (stress == null || stress < 0) {
|
|
continue;
|
|
}
|
|
LOG.trace("Stress at {}: {}", ts, stress);
|
|
final GarminStressSample sample = new GarminStressSample();
|
|
sample.setTimestamp(ts * 1000L);
|
|
sample.setStress(stress);
|
|
stressSamples.add(sample);
|
|
} else if (record instanceof FitSleepStage) {
|
|
final FieldDefinitionSleepStage.SleepStage stage = ((FitSleepStage) record).getSleepStage();
|
|
if (stage == null) {
|
|
continue;
|
|
}
|
|
LOG.trace("Sleep stage at {}: {}", ts, record);
|
|
final GarminSleepStageSample sample = new GarminSleepStageSample();
|
|
sample.setTimestamp(ts * 1000L);
|
|
sample.setStage(stage.getId());
|
|
sleepStageSamples.add(sample);
|
|
} else if (record instanceof FitMonitoring) {
|
|
final Integer hr = ((FitMonitoring) record).getHeartRate();
|
|
final Long steps = ((FitMonitoring) record).getCycles();
|
|
final Integer activityType = ((FitMonitoring) record).getComputedActivityType();
|
|
final Integer intensity = ((FitMonitoring) record).getComputedIntensity();
|
|
LOG.trace("Monitoring at {}: hr={} steps={} activityType={} intensity={}", ts, hr, steps, activityType, intensity);
|
|
final GarminActivitySample sample = new GarminActivitySample();
|
|
sample.setTimestamp(ts.intValue());
|
|
if (hr != null) {
|
|
sample.setHeartRate(hr);
|
|
}
|
|
if (steps != null) {
|
|
sample.setSteps(steps.intValue());
|
|
}
|
|
if (activityType != null) {
|
|
sample.setRawKind(activityType);
|
|
}
|
|
if (intensity != null) {
|
|
sample.setRawIntensity(intensity);
|
|
}
|
|
activitySamples.add(sample);
|
|
List<GarminActivitySample> samplesForTimestamp = activitySamplesPerTimestamp.get(ts.intValue());
|
|
if (samplesForTimestamp == null) {
|
|
samplesForTimestamp = new ArrayList<>();
|
|
activitySamplesPerTimestamp.put(ts.intValue(), samplesForTimestamp);
|
|
}
|
|
samplesForTimestamp.add(sample);
|
|
} else if (record instanceof FitSpo2) {
|
|
final Integer spo2 = ((FitSpo2) record).getReadingSpo2();
|
|
if (spo2 == null || spo2 <= 0) {
|
|
continue;
|
|
}
|
|
LOG.trace("SpO2 at {}: {}", ts, spo2);
|
|
final GarminSpo2Sample sample = new GarminSpo2Sample();
|
|
sample.setTimestamp(ts * 1000L);
|
|
sample.setSpo2(spo2);
|
|
spo2samples.add(sample);
|
|
} else if (record instanceof FitEvent) {
|
|
final FitEvent event = (FitEvent) record;
|
|
if (event.getEvent() == null) {
|
|
LOG.warn("Event in {} is null", event);
|
|
continue;
|
|
}
|
|
|
|
LOG.trace("Event at {}: {}", ts, event);
|
|
|
|
final GarminEventSample sample = new GarminEventSample();
|
|
sample.setTimestamp(ts * 1000L);
|
|
sample.setEvent(event.getEvent());
|
|
if (event.getEventType() != null) {
|
|
sample.setEventType(event.getEventType());
|
|
}
|
|
if (event.getData() != null) {
|
|
sample.setData(event.getData());
|
|
}
|
|
events.add(sample);
|
|
} else if (record instanceof FitRecord) {
|
|
activityPoints.add(((FitRecord) record).toActivityPoint());
|
|
} else if (record instanceof FitSession) {
|
|
LOG.debug("Session: {}", record);
|
|
if (session != null) {
|
|
LOG.warn("Got multiple sessions - NOT SUPPORTED: {}", record);
|
|
} else {
|
|
// We only support 1 session
|
|
session = (FitSession) record;
|
|
}
|
|
} else if (record instanceof FitSport) {
|
|
LOG.debug("Sport: {}", record);
|
|
if (sport != null) {
|
|
LOG.warn("Got multiple sports - NOT SUPPORTED: {}", record);
|
|
} else {
|
|
// We only support 1 sport
|
|
sport = (FitSport) record;
|
|
}
|
|
} else if (record instanceof FitTimeInZone) {
|
|
LOG.trace("Time in zone: {}", record);
|
|
timesInZone.add((FitTimeInZone) record);
|
|
} else {
|
|
LOG.trace("Unknown record: {}", record);
|
|
|
|
if (!unknownRecords.containsKey(record.getGlobalFITMessage().getNumber())) {
|
|
unknownRecords.put(record.getGlobalFITMessage().getNumber(), 0);
|
|
}
|
|
unknownRecords.put(
|
|
record.getGlobalFITMessage().getNumber(),
|
|
Objects.requireNonNull(unknownRecords.get(record.getGlobalFITMessage().getNumber())) + 1
|
|
);
|
|
}
|
|
}
|
|
|
|
if (fileId == null) {
|
|
LOG.error("Got no file ID");
|
|
return;
|
|
}
|
|
if (fileId.getType() == null) {
|
|
LOG.error("File has no type");
|
|
return;
|
|
}
|
|
|
|
switch (fileId.getType()) {
|
|
case activity:
|
|
persistWorkout(file);
|
|
break;
|
|
case monitor:
|
|
persistActivitySamples();
|
|
persistSpo2Samples();
|
|
persistStressSamples();
|
|
break;
|
|
case sleep:
|
|
persistEvents();
|
|
persistSleepStageSamples();
|
|
break;
|
|
default:
|
|
LOG.warn("Unable to handle fit file of type {}", fileId.getType());
|
|
}
|
|
|
|
for (final Map.Entry<Integer, Integer> e : unknownRecords.entrySet()) {
|
|
LOG.warn("Unknown record of global number {} seen {} times", e.getKey(), e.getValue());
|
|
}
|
|
}
|
|
|
|
private void persistWorkout(final File file) {
|
|
if (session == null) {
|
|
LOG.error("Got workout from {}, but no session", fileId);
|
|
return;
|
|
}
|
|
if (sport == null) {
|
|
LOG.error("Got workout from {}, but no sport", fileId);
|
|
return;
|
|
}
|
|
|
|
LOG.debug("Persisting workout for {}", fileId);
|
|
|
|
final BaseActivitySummary summary = new BaseActivitySummary();
|
|
summary.setActivityKind(ActivityKind.TYPE_UNKNOWN);
|
|
|
|
final ActivitySummaryData summaryData = new ActivitySummaryData();
|
|
|
|
// TODO map all sports
|
|
if (sport.getSport() != null) {
|
|
switch (sport.getSport()) {
|
|
case 2:
|
|
summary.setActivityKind(ActivityKind.TYPE_CYCLING);
|
|
break;
|
|
case 4: // fitness_equipment
|
|
case 10: // training
|
|
if (sport.getSubSport() != null) {
|
|
switch (sport.getSubSport()) {
|
|
case 15:
|
|
summary.setActivityKind(ActivityKind.TYPE_ELLIPTICAL_TRAINER);
|
|
break;
|
|
default:
|
|
LOG.warn("Unknown sub sport {}", sport.getSubSport());
|
|
summaryData.add("Fit Sub Sport", sport.getSubSport(), "");
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
LOG.warn("Unknown sport {}", sport.getSport());
|
|
summaryData.add("Fit Sport", sport.getSport(), "");
|
|
}
|
|
}
|
|
|
|
summary.setName(sport.getName());
|
|
if (session.getStartTime() == null) {
|
|
LOG.error("No session start time for {}", fileId);
|
|
return;
|
|
}
|
|
summary.setStartTime(new Date(GarminTimeUtils.garminTimestampToJavaMillis(session.getStartTime().intValue())));
|
|
|
|
if (session.getTotalElapsedTime() == null) {
|
|
LOG.error("No elapsed time for {}", fileId);
|
|
return;
|
|
}
|
|
summary.setEndTime(new Date(GarminTimeUtils.garminTimestampToJavaMillis(session.getStartTime().intValue() + session.getTotalElapsedTime().intValue() / 1000)));
|
|
|
|
if (session.getTotalTimerTime() != null) {
|
|
summaryData.add(ACTIVE_SECONDS, session.getTotalTimerTime() / 1000f, UNIT_SECONDS);
|
|
}
|
|
if (session.getTotalDistance() != null) {
|
|
summaryData.add(DISTANCE_METERS, session.getTotalDistance() / 100f, UNIT_METERS);
|
|
}
|
|
if (session.getTotalCalories() != null) {
|
|
summaryData.add(CALORIES_BURNT, session.getTotalCalories(), UNIT_KCAL);
|
|
}
|
|
if (session.getTotalAscent() != null) {
|
|
summaryData.add(ASCENT_DISTANCE, session.getTotalAscent(), UNIT_METERS);
|
|
}
|
|
if (session.getTotalDescent() != null) {
|
|
summaryData.add(DESCENT_DISTANCE, session.getTotalDescent(), UNIT_METERS);
|
|
}
|
|
|
|
//FitTimeInZone timeInZone = null;
|
|
//for (final FitTimeInZone fitTimeInZone : timesInZone) {
|
|
// // Find the firt time in zone for the session (assumes single-session)
|
|
// if (fitTimeInZone.getReferenceMessage() != null && fitTimeInZone.getReferenceMessage() == 18) {
|
|
// timeInZone = fitTimeInZone;
|
|
// break;
|
|
// }
|
|
//}
|
|
//if (timeInZone != null) {
|
|
//}
|
|
|
|
summary.setSummaryData(summaryData.toString());
|
|
if (file != null) {
|
|
summary.setRawDetailsPath(file.getAbsolutePath());
|
|
}
|
|
|
|
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
|
final DaoSession session = dbHandler.getDaoSession();
|
|
final Device device = DBHelper.getDevice(gbDevice, session);
|
|
final User user = DBHelper.getUser(session);
|
|
|
|
summary.setDevice(device);
|
|
summary.setUser(user);
|
|
|
|
session.getBaseActivitySummaryDao().insertOrReplace(summary);
|
|
} catch (final Exception e) {
|
|
GB.toast(context, "Error saving workout", Toast.LENGTH_LONG, GB.ERROR, e);
|
|
}
|
|
}
|
|
|
|
private void reset() {
|
|
activitySamples.clear();
|
|
stressSamples.clear();
|
|
spo2samples.clear();
|
|
events.clear();
|
|
sleepStageSamples.clear();
|
|
timesInZone.clear();
|
|
activityPoints.clear();
|
|
unknownRecords.clear();
|
|
fileId = null;
|
|
session = null;
|
|
sport = null;
|
|
}
|
|
|
|
private void persistActivitySamples() {
|
|
if (activitySamples.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
// FIXME prevent overlapping samples in the same timestamp..
|
|
|
|
LOG.debug("Will persist {} activity samples", activitySamples.size());
|
|
|
|
try (DBHandler handler = GBApplication.acquireDB()) {
|
|
final DaoSession session = handler.getDaoSession();
|
|
|
|
final Device device = DBHelper.getDevice(gbDevice, session);
|
|
final User user = DBHelper.getUser(session);
|
|
|
|
final GarminActivitySampleProvider sampleProvider = new GarminActivitySampleProvider(gbDevice, session);
|
|
|
|
for (final GarminActivitySample sample : activitySamples) {
|
|
sample.setDevice(device);
|
|
sample.setUser(user);
|
|
}
|
|
|
|
sampleProvider.addGBActivitySamples(activitySamples.toArray(new GarminActivitySample[0]));
|
|
} catch (final Exception e) {
|
|
GB.toast(context, "Error saving activity samples", Toast.LENGTH_LONG, GB.ERROR, e);
|
|
}
|
|
}
|
|
|
|
private void persistEvents() {
|
|
if (events.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
LOG.debug("Will persist {} event samples", events.size());
|
|
|
|
try (DBHandler handler = GBApplication.acquireDB()) {
|
|
final DaoSession session = handler.getDaoSession();
|
|
|
|
final Device device = DBHelper.getDevice(gbDevice, session);
|
|
final User user = DBHelper.getUser(session);
|
|
|
|
final GarminEventSampleProvider sampleProvider = new GarminEventSampleProvider(gbDevice, session);
|
|
|
|
for (final GarminEventSample sample : events) {
|
|
sample.setDevice(device);
|
|
sample.setUser(user);
|
|
}
|
|
|
|
sampleProvider.addSamples(events);
|
|
} catch (final Exception e) {
|
|
GB.toast(context, "Error saving event samples", Toast.LENGTH_LONG, GB.ERROR, e);
|
|
}
|
|
}
|
|
|
|
private void persistSleepStageSamples() {
|
|
if (sleepStageSamples.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
LOG.debug("Will persist {} sleep stage samples", sleepStageSamples.size());
|
|
|
|
try (DBHandler handler = GBApplication.acquireDB()) {
|
|
final DaoSession session = handler.getDaoSession();
|
|
|
|
final Device device = DBHelper.getDevice(gbDevice, session);
|
|
final User user = DBHelper.getUser(session);
|
|
|
|
final GarminSleepStageSampleProvider sampleProvider = new GarminSleepStageSampleProvider(gbDevice, session);
|
|
|
|
for (final GarminSleepStageSample sample : sleepStageSamples) {
|
|
sample.setDevice(device);
|
|
sample.setUser(user);
|
|
}
|
|
|
|
sampleProvider.addSamples(sleepStageSamples);
|
|
} catch (final Exception e) {
|
|
GB.toast(context, "Error saving sleep stage samples", Toast.LENGTH_LONG, GB.ERROR, e);
|
|
}
|
|
}
|
|
|
|
private void persistSpo2Samples() {
|
|
if (spo2samples.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
LOG.debug("Will persist {} spo2 samples", stressSamples.size());
|
|
|
|
try (DBHandler handler = GBApplication.acquireDB()) {
|
|
final DaoSession session = handler.getDaoSession();
|
|
|
|
final Device device = DBHelper.getDevice(gbDevice, session);
|
|
final User user = DBHelper.getUser(session);
|
|
|
|
final GarminSpo2SampleProvider sampleProvider = new GarminSpo2SampleProvider(gbDevice, session);
|
|
|
|
for (final GarminSpo2Sample sample : spo2samples) {
|
|
sample.setDevice(device);
|
|
sample.setUser(user);
|
|
}
|
|
|
|
sampleProvider.addSamples(spo2samples);
|
|
} catch (final Exception e) {
|
|
GB.toast(context, "Error saving spo2 samples", Toast.LENGTH_LONG, GB.ERROR, e);
|
|
}
|
|
}
|
|
|
|
private void persistStressSamples() {
|
|
if (stressSamples.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
LOG.debug("Will persist {} stress samples", stressSamples.size());
|
|
|
|
try (DBHandler handler = GBApplication.acquireDB()) {
|
|
final DaoSession session = handler.getDaoSession();
|
|
|
|
final Device device = DBHelper.getDevice(gbDevice, session);
|
|
final User user = DBHelper.getUser(session);
|
|
|
|
final GarminStressSampleProvider sampleProvider = new GarminStressSampleProvider(gbDevice, session);
|
|
|
|
for (final GarminStressSample sample : stressSamples) {
|
|
sample.setDevice(device);
|
|
sample.setUser(user);
|
|
}
|
|
|
|
sampleProvider.addSamples(stressSamples);
|
|
} catch (final Exception e) {
|
|
GB.toast(context, "Error saving stress samples", Toast.LENGTH_LONG, GB.ERROR, e);
|
|
}
|
|
}
|
|
}
|