mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-07-23 23:32:23 +02:00
664 lines
26 KiB
Java
664 lines
26 KiB
Java
/* Copyright (C) 2024 José Rebelo
|
|
|
|
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.service.devices.cmfwatchpro;
|
|
|
|
import android.content.Context;
|
|
import android.widget.Toast;
|
|
|
|
import androidx.annotation.Nullable;
|
|
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.nio.ByteBuffer;
|
|
import java.nio.ByteOrder;
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.Date;
|
|
import java.util.List;
|
|
|
|
import de.greenrobot.dao.query.QueryBuilder;
|
|
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
|
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
|
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
|
import nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.samples.CmfActivitySampleProvider;
|
|
import nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.samples.CmfHeartRateSampleProvider;
|
|
import nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.samples.CmfSleepSessionSampleProvider;
|
|
import nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.samples.CmfSleepStageSampleProvider;
|
|
import nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.samples.CmfSpo2SampleProvider;
|
|
import nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.samples.CmfStressSampleProvider;
|
|
import nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.samples.CmfWorkoutGpsSampleProvider;
|
|
import nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.workout.CmfWorkoutSummaryParser;
|
|
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
|
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao;
|
|
import nodomain.freeyourgadget.gadgetbridge.entities.CmfActivitySample;
|
|
import nodomain.freeyourgadget.gadgetbridge.entities.CmfHeartRateSample;
|
|
import nodomain.freeyourgadget.gadgetbridge.entities.CmfSleepSessionSample;
|
|
import nodomain.freeyourgadget.gadgetbridge.entities.CmfSleepStageSample;
|
|
import nodomain.freeyourgadget.gadgetbridge.entities.CmfSpo2Sample;
|
|
import nodomain.freeyourgadget.gadgetbridge.entities.CmfStressSample;
|
|
import nodomain.freeyourgadget.gadgetbridge.entities.CmfWorkoutGpsSample;
|
|
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
|
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
|
import nodomain.freeyourgadget.gadgetbridge.entities.User;
|
|
import nodomain.freeyourgadget.gadgetbridge.export.ActivityTrackExporter;
|
|
import nodomain.freeyourgadget.gadgetbridge.export.GPXExporter;
|
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
|
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
|
|
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
|
|
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
|
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
|
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
|
|
|
public class CmfActivitySync {
|
|
private static final Logger LOG = LoggerFactory.getLogger(CmfActivitySync.class);
|
|
|
|
private final CmfWatchProSupport mSupport;
|
|
|
|
private final List<BaseActivitySummary> activitiesWithGps = new ArrayList<>();
|
|
|
|
protected CmfActivitySync(final CmfWatchProSupport support) {
|
|
this.mSupport = support;
|
|
}
|
|
|
|
protected boolean onCommand(final CmfCommand cmd, final byte[] payload) {
|
|
switch (cmd) {
|
|
case ACTIVITY_FETCH_ACK_1:
|
|
handleActivityFetchAck1(payload);
|
|
return true;
|
|
case ACTIVITY_FETCH_ACK_2:
|
|
handleActivityFetchAck2(payload);
|
|
return true;
|
|
case ACTIVITY_DATA:
|
|
handleActivityData(payload);
|
|
return true;
|
|
case HEART_RATE_MANUAL_AUTO:
|
|
case HEART_RATE_WORKOUT:
|
|
handleHeartRate(payload);
|
|
return true;
|
|
case HEART_RATE_RESTING:
|
|
handleHeartRateResting(payload);
|
|
return true;
|
|
case SLEEP_DATA:
|
|
handleSleepData(payload);
|
|
return true;
|
|
case STRESS:
|
|
handleStress(payload);
|
|
return true;
|
|
case SPO2:
|
|
handleSpo2(payload);
|
|
return true;
|
|
case WORKOUT_SUMMARY:
|
|
handleWorkoutSummary(payload);
|
|
return true;
|
|
case WORKOUT_GPS:
|
|
handleWorkoutGps(payload);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void handleActivityFetchAck1(final byte[] payload) {
|
|
switch (payload[0]) {
|
|
case 0x01:
|
|
LOG.debug("Got activity fetch ack 1, starting step 2");
|
|
GB.updateTransferNotification(getContext().getString(R.string.busy_task_fetch_activity_data), "", true, 0, getContext());
|
|
getDevice().setBusyTask(getContext().getString(R.string.busy_task_fetch_activity_data));
|
|
mSupport.sendCommand("fetch recorded data step 2", CmfCommand.ACTIVITY_FETCH_2, CmfWatchProSupport.A5);
|
|
break;
|
|
case 0x02:
|
|
LOG.debug("Got activity fetch finish");
|
|
// Process activities with GPS before unsetting device as busy
|
|
processActivitiesWithGps();
|
|
break;
|
|
default:
|
|
LOG.warn("Unknown activity fetch ack code {}", payload[0]);
|
|
return;
|
|
}
|
|
|
|
getDevice().sendDeviceUpdateIntent(getContext());
|
|
}
|
|
|
|
private static void handleActivityFetchAck2(final byte[] payload) {
|
|
final ByteBuffer buf = ByteBuffer.wrap(payload).order(ByteOrder.LITTLE_ENDIAN);
|
|
|
|
final int activityTs = buf.getInt();
|
|
final byte[] activityFlags = new byte[4]; // TODO what do they mean?
|
|
buf.order(ByteOrder.BIG_ENDIAN).get(activityFlags);
|
|
LOG.debug("Getting activity since {}, flags={}", activityTs, GB.hexdump(activityFlags));
|
|
}
|
|
|
|
private void handleActivityData(final byte[] payload) {
|
|
if (payload.length % 32 != 0) {
|
|
LOG.error("Activity data payload size {} not divisible by 32", payload.length);
|
|
return;
|
|
}
|
|
|
|
LOG.debug("Got {} activity samples", payload.length / 32);
|
|
|
|
final ByteBuffer buf = ByteBuffer.wrap(payload).order(ByteOrder.LITTLE_ENDIAN);
|
|
|
|
final List<CmfActivitySample> samples = new ArrayList<>();
|
|
|
|
while (buf.remaining() > 0) {
|
|
final CmfActivitySample sample = new CmfActivitySample();
|
|
sample.setTimestamp(buf.getInt());
|
|
sample.setSteps(buf.getInt());
|
|
sample.setDistance(buf.getInt());
|
|
sample.setCalories(buf.getInt());
|
|
|
|
final byte[] unk = new byte[16];
|
|
buf.get(unk);
|
|
|
|
samples.add(sample);
|
|
}
|
|
|
|
try (DBHandler handler = GBApplication.acquireDB()) {
|
|
final DaoSession session = handler.getDaoSession();
|
|
|
|
final Device device = DBHelper.getDevice(getDevice(), session);
|
|
final User user = DBHelper.getUser(session);
|
|
|
|
final CmfActivitySampleProvider sampleProvider = new CmfActivitySampleProvider(getDevice(), session);
|
|
|
|
for (final CmfActivitySample sample : samples) {
|
|
sample.setDevice(device);
|
|
sample.setUser(user);
|
|
sample.setProvider(sampleProvider);
|
|
}
|
|
|
|
LOG.debug("Will persist {} activity samples", samples.size());
|
|
sampleProvider.addGBActivitySamples(samples.toArray(new CmfActivitySample[0]));
|
|
} catch (final Exception e) {
|
|
GB.toast(getContext(), "Error saving activity samples", Toast.LENGTH_LONG, GB.ERROR, e);
|
|
}
|
|
}
|
|
|
|
private void handleHeartRate(final byte[] payload) {
|
|
if (payload.length % 8 != 0) {
|
|
LOG.error("Heart rate payload size {} not divisible by 8", payload.length);
|
|
return;
|
|
}
|
|
|
|
LOG.debug("Got {} heart rate samples", payload.length / 8);
|
|
|
|
final ByteBuffer buf = ByteBuffer.wrap(payload).order(ByteOrder.LITTLE_ENDIAN);
|
|
|
|
final List<CmfHeartRateSample> samples = new ArrayList<>();
|
|
|
|
while (buf.remaining() > 0) {
|
|
final CmfHeartRateSample sample = new CmfHeartRateSample();
|
|
sample.setTimestamp(buf.getInt() * 1000L);
|
|
sample.setHeartRate(buf.getInt());
|
|
|
|
samples.add(sample);
|
|
}
|
|
|
|
try (DBHandler handler = GBApplication.acquireDB()) {
|
|
final DaoSession session = handler.getDaoSession();
|
|
|
|
final Device device = DBHelper.getDevice(getDevice(), session);
|
|
final User user = DBHelper.getUser(session);
|
|
|
|
final CmfHeartRateSampleProvider sampleProvider = new CmfHeartRateSampleProvider(getDevice(), session);
|
|
|
|
for (final CmfHeartRateSample sample : samples) {
|
|
sample.setDevice(device);
|
|
sample.setUser(user);
|
|
}
|
|
|
|
LOG.debug("Will persist {} heart rate samples", samples.size());
|
|
sampleProvider.addSamples(samples);
|
|
} catch (final Exception e) {
|
|
GB.toast(getContext(), "Error saving heart rate samples", Toast.LENGTH_LONG, GB.ERROR, e);
|
|
}
|
|
}
|
|
|
|
private static void handleHeartRateResting(final byte[] payload) {
|
|
// TODO persist resting HR samples;
|
|
LOG.warn("Persisting resting HR samples is not implemented");
|
|
}
|
|
|
|
private void handleSleepData(final byte[] payload) {
|
|
final ByteBuffer buf = ByteBuffer.wrap(payload).order(ByteOrder.LITTLE_ENDIAN);
|
|
|
|
LOG.debug("Got sleep data samples");
|
|
|
|
final int sessionTimestamp = buf.getInt();
|
|
final int wakeupTime = buf.getInt();
|
|
final byte[] metadata = new byte[10];
|
|
buf.get(metadata);
|
|
|
|
final CmfSleepSessionSample sessionSample = new CmfSleepSessionSample();
|
|
sessionSample.setTimestamp(sessionTimestamp * 1000L);
|
|
sessionSample.setWakeupTime(wakeupTime * 1000L);
|
|
sessionSample.setMetadata(metadata);
|
|
|
|
final List<CmfSleepStageSample> stageSamples = new ArrayList<>();
|
|
|
|
while (buf.remaining() > 0) {
|
|
final CmfSleepStageSample sample = new CmfSleepStageSample();
|
|
sample.setTimestamp(buf.getInt() * 1000L);
|
|
sample.setDuration(buf.getShort());
|
|
sample.setStage(buf.getShort());
|
|
stageSamples.add(sample);
|
|
}
|
|
|
|
try (DBHandler handler = GBApplication.acquireDB()) {
|
|
final DaoSession session = handler.getDaoSession();
|
|
|
|
final Device device = DBHelper.getDevice(getDevice(), session);
|
|
final User user = DBHelper.getUser(session);
|
|
|
|
final CmfSleepSessionSampleProvider sampleProvider = new CmfSleepSessionSampleProvider(getDevice(), session);
|
|
|
|
sessionSample.setDevice(device);
|
|
sessionSample.setUser(user);
|
|
|
|
LOG.debug("Will persist 1 sleep session sample from {} to {}", sessionSample.getTimestamp(), sessionSample.getWakeupTime());
|
|
sampleProvider.addSample(sessionSample);
|
|
} catch (final Exception e) {
|
|
GB.toast(getContext(), "Error saving sleep session sample", Toast.LENGTH_LONG, GB.ERROR, e);
|
|
}
|
|
|
|
try (DBHandler handler = GBApplication.acquireDB()) {
|
|
final DaoSession session = handler.getDaoSession();
|
|
|
|
final Device device = DBHelper.getDevice(getDevice(), session);
|
|
final User user = DBHelper.getUser(session);
|
|
|
|
final CmfSleepStageSampleProvider sampleProvider = new CmfSleepStageSampleProvider(getDevice(), session);
|
|
|
|
for (final CmfSleepStageSample sample : stageSamples) {
|
|
sample.setDevice(device);
|
|
sample.setUser(user);
|
|
}
|
|
|
|
LOG.debug("Will persist {} sleep stage samples", stageSamples.size());
|
|
sampleProvider.addSamples(stageSamples);
|
|
} catch (final Exception e) {
|
|
GB.toast(getContext(), "Error saving sleep samples", Toast.LENGTH_LONG, GB.ERROR, e);
|
|
}
|
|
}
|
|
|
|
private void handleStress(final byte[] payload) {
|
|
if (payload.length % 8 != 0) {
|
|
LOG.error("Stress payload size {} not divisible by 8", payload.length);
|
|
return;
|
|
}
|
|
|
|
LOG.debug("Got {} stress samples", payload.length / 8);
|
|
|
|
final ByteBuffer buf = ByteBuffer.wrap(payload).order(ByteOrder.LITTLE_ENDIAN);
|
|
|
|
final List<CmfStressSample> samples = new ArrayList<>();
|
|
|
|
while (buf.remaining() > 0) {
|
|
final CmfStressSample sample = new CmfStressSample();
|
|
sample.setTimestamp(buf.getInt() * 1000L);
|
|
sample.setStress(buf.getInt());
|
|
|
|
samples.add(sample);
|
|
}
|
|
|
|
try (DBHandler handler = GBApplication.acquireDB()) {
|
|
final DaoSession session = handler.getDaoSession();
|
|
|
|
final Device device = DBHelper.getDevice(getDevice(), session);
|
|
final User user = DBHelper.getUser(session);
|
|
|
|
final CmfStressSampleProvider sampleProvider = new CmfStressSampleProvider(getDevice(), session);
|
|
|
|
for (final CmfStressSample sample : samples) {
|
|
sample.setDevice(device);
|
|
sample.setUser(user);
|
|
}
|
|
|
|
LOG.debug("Will persist {} stress samples", samples.size());
|
|
sampleProvider.addSamples(samples);
|
|
} catch (final Exception e) {
|
|
GB.toast(getContext(), "Error saving stress samples", Toast.LENGTH_LONG, GB.ERROR, e);
|
|
}
|
|
}
|
|
|
|
private void handleSpo2(final byte[] payload) {
|
|
if (payload.length % 8 != 0) {
|
|
LOG.error("Spo2 payload size {} not divisible by 8", payload.length);
|
|
return;
|
|
}
|
|
|
|
LOG.debug("Got {} spo2 samples", payload.length / 8);
|
|
|
|
final ByteBuffer buf = ByteBuffer.wrap(payload).order(ByteOrder.LITTLE_ENDIAN);
|
|
|
|
final List<CmfSpo2Sample> samples = new ArrayList<>();
|
|
|
|
while (buf.remaining() > 0) {
|
|
final CmfSpo2Sample sample = new CmfSpo2Sample();
|
|
sample.setTimestamp(buf.getInt() * 1000L);
|
|
sample.setSpo2(buf.getInt());
|
|
|
|
samples.add(sample);
|
|
}
|
|
|
|
try (DBHandler handler = GBApplication.acquireDB()) {
|
|
final DaoSession session = handler.getDaoSession();
|
|
|
|
final Device device = DBHelper.getDevice(getDevice(), session);
|
|
final User user = DBHelper.getUser(session);
|
|
|
|
final CmfSpo2SampleProvider sampleProvider = new CmfSpo2SampleProvider(getDevice(), session);
|
|
|
|
for (final CmfSpo2Sample sample : samples) {
|
|
sample.setDevice(device);
|
|
sample.setUser(user);
|
|
}
|
|
|
|
LOG.debug("Will persist {} spo2 samples", samples.size());
|
|
sampleProvider.addSamples(samples);
|
|
} catch (final Exception e) {
|
|
GB.toast(getContext(), "Error saving spo2 samples", Toast.LENGTH_LONG, GB.ERROR, e);
|
|
}
|
|
}
|
|
|
|
private void handleWorkoutSummary(final byte[] payload) {
|
|
if (payload.length % 32 != 0) {
|
|
LOG.error("Workout summary payload size {} not divisible by 32", payload.length);
|
|
return;
|
|
}
|
|
|
|
LOG.debug("Got {} workout summary samples", payload.length / 32);
|
|
|
|
final ByteBuffer buf = ByteBuffer.wrap(payload).order(ByteOrder.LITTLE_ENDIAN);
|
|
|
|
final CmfWorkoutSummaryParser summaryParser = new CmfWorkoutSummaryParser(getDevice());
|
|
|
|
while (buf.remaining() > 0) {
|
|
final byte[] summaryBytes = new byte[32];
|
|
buf.get(summaryBytes);
|
|
|
|
BaseActivitySummary summary = new BaseActivitySummary();
|
|
summary.setRawSummaryData(summaryBytes);
|
|
summary.setActivityKind(ActivityKind.TYPE_UNKNOWN);
|
|
|
|
try {
|
|
summary = summaryParser.parseBinaryData(summary);
|
|
} catch (final Exception e) {
|
|
LOG.error("Failed to parse workout summary", e);
|
|
GB.toast(getContext(), "Failed to parse workout summary", Toast.LENGTH_LONG, GB.ERROR, e);
|
|
return;
|
|
}
|
|
|
|
if (summary == null) {
|
|
LOG.error("Workout summary is null");
|
|
return;
|
|
}
|
|
|
|
summary.setSummaryData(null); // remove json before saving to database
|
|
|
|
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
|
final DaoSession session = dbHandler.getDaoSession();
|
|
final Device device = DBHelper.getDevice(getDevice(), session);
|
|
final User user = DBHelper.getUser(session);
|
|
|
|
summary.setDevice(device);
|
|
summary.setUser(user);
|
|
|
|
LOG.debug("Persisting workout summary for {}", summary.getStartTime());
|
|
|
|
session.getBaseActivitySummaryDao().insertOrReplace(summary);
|
|
} catch (final Exception e) {
|
|
GB.toast(getContext(), "Error saving activity summary", Toast.LENGTH_LONG, GB.ERROR, e);
|
|
return;
|
|
}
|
|
|
|
// Previous to last byte indicates if it has gps
|
|
if (summaryBytes[summaryBytes.length - 2] == 1) {
|
|
activitiesWithGps.add(summary);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void handleWorkoutGps(final byte[] payload) {
|
|
if (payload.length % 12 != 0) {
|
|
LOG.error("Workout gps payload size {} not divisible by 12", payload.length);
|
|
return;
|
|
}
|
|
|
|
LOG.debug("Got {} workout gps samples", payload.length / 12);
|
|
|
|
final ByteBuffer buf = ByteBuffer.wrap(payload).order(ByteOrder.LITTLE_ENDIAN);
|
|
|
|
final List<CmfWorkoutGpsSample> samples = new ArrayList<>();
|
|
|
|
while (buf.remaining() > 0) {
|
|
final CmfWorkoutGpsSample sample = new CmfWorkoutGpsSample();
|
|
sample.setTimestamp(buf.getInt() * 1000L);
|
|
sample.setLongitude(buf.getInt());
|
|
sample.setLatitude(buf.getInt());
|
|
|
|
samples.add(sample);
|
|
}
|
|
|
|
try (DBHandler handler = GBApplication.acquireDB()) {
|
|
final DaoSession session = handler.getDaoSession();
|
|
|
|
final Device device = DBHelper.getDevice(getDevice(), session);
|
|
final User user = DBHelper.getUser(session);
|
|
|
|
final CmfWorkoutGpsSampleProvider sampleProvider = new CmfWorkoutGpsSampleProvider(getDevice(), session);
|
|
|
|
for (final CmfWorkoutGpsSample sample : samples) {
|
|
sample.setDevice(device);
|
|
sample.setUser(user);
|
|
}
|
|
|
|
LOG.debug("Will persist {} workout gps samples", samples.size());
|
|
sampleProvider.addSamples(samples);
|
|
} catch (final Exception e) {
|
|
GB.toast(getContext(), "Error saving workout gps samples", Toast.LENGTH_LONG, GB.ERROR, e);
|
|
}
|
|
}
|
|
|
|
private void processActivitiesWithGps() {
|
|
LOG.debug("There are {} activities with gps to process", activitiesWithGps.size());
|
|
|
|
for (final BaseActivitySummary summary : activitiesWithGps) {
|
|
processGps(summary);
|
|
}
|
|
|
|
activitiesWithGps.clear();
|
|
|
|
getDevice().unsetBusyTask();
|
|
GB.updateTransferNotification(null, "", false, 100, getContext());
|
|
}
|
|
|
|
private void processGps(final BaseActivitySummary summary) {
|
|
final ActivityTrack activityTrack = buildActivityTrack(summary);
|
|
if (activityTrack == null) {
|
|
return;
|
|
}
|
|
|
|
// Save the gpx file
|
|
final File gpxFile = exportGpx(summary, activityTrack);
|
|
if (gpxFile == null) {
|
|
return;
|
|
}
|
|
|
|
// Update the summary in the db with the gpx path
|
|
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
|
final DaoSession session = dbHandler.getDaoSession();
|
|
final Device device = DBHelper.getDevice(mSupport.getDevice(), session);
|
|
final User user = DBHelper.getUser(session);
|
|
|
|
final BaseActivitySummaryDao summaryDao = session.getBaseActivitySummaryDao();
|
|
final QueryBuilder<BaseActivitySummary> qb = summaryDao.queryBuilder();
|
|
qb.where(BaseActivitySummaryDao.Properties.StartTime.eq(summary.getStartTime()));
|
|
qb.where(BaseActivitySummaryDao.Properties.DeviceId.eq(device.getId()));
|
|
qb.where(BaseActivitySummaryDao.Properties.UserId.eq(user.getId()));
|
|
final List<BaseActivitySummary> summaries = qb.build().list();
|
|
|
|
if (summaries.isEmpty()) {
|
|
LOG.warn("Failed to find existing summary in db - this should never happen");
|
|
return;
|
|
}
|
|
if (summaries.size() > 1) {
|
|
LOG.warn("Found multiple summaries in db - this should never happen");
|
|
}
|
|
|
|
final BaseActivitySummary summaryToUpdate = summaries.get(0);
|
|
summaryToUpdate.setGpxTrack(gpxFile.getAbsolutePath());
|
|
session.getBaseActivitySummaryDao().insertOrReplace(summaryToUpdate);
|
|
} catch (final Exception e) {
|
|
LOG.error("Failed to update summary with gpx path", e);
|
|
}
|
|
}
|
|
|
|
@Nullable
|
|
private File exportGpx(final BaseActivitySummary summary, final ActivityTrack activityTrack) {
|
|
final GPXExporter exporter = new GPXExporter();
|
|
exporter.setCreator(GBApplication.app().getNameAndVersion());
|
|
|
|
final String gpxFileName = FileUtils.makeValidFileName("gadgetbridge-" + DateTimeUtils.formatIso8601(summary.getStartTime()) + ".gpx");
|
|
final File gpxTargetFile;
|
|
try {
|
|
gpxTargetFile = new File(FileUtils.getExternalFilesDir(), gpxFileName);
|
|
} catch (final IOException e) {
|
|
LOG.error("Failed to get external files dir", e);
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
exporter.performExport(activityTrack, gpxTargetFile);
|
|
} catch (final ActivityTrackExporter.GPXTrackEmptyException e) {
|
|
LOG.warn("Gpx is empty");
|
|
return null;
|
|
} catch (IOException e) {
|
|
LOG.error("Failed to write gpx", e);
|
|
return null;
|
|
}
|
|
|
|
return gpxTargetFile;
|
|
}
|
|
|
|
@Nullable
|
|
private ActivityTrack buildActivityTrack(final BaseActivitySummary summary) {
|
|
final ActivityTrack track = new ActivityTrack();
|
|
track.setUser(summary.getUser());
|
|
track.setDevice(summary.getDevice());
|
|
track.setName(createActivityName(summary));
|
|
|
|
final List<CmfWorkoutGpsSample> gpsSamples;
|
|
final List<CmfHeartRateSample> hrSamples;
|
|
try (DBHandler handler = GBApplication.acquireDB()) {
|
|
final DaoSession session = handler.getDaoSession();
|
|
|
|
final CmfWorkoutGpsSampleProvider gpsSampleProvider = new CmfWorkoutGpsSampleProvider(getDevice(), session);
|
|
gpsSamples = gpsSampleProvider.getAllSamples(summary.getStartTime().getTime(), summary.getEndTime().getTime());
|
|
|
|
final CmfHeartRateSampleProvider hrSampleProvider = new CmfHeartRateSampleProvider(getDevice(), session);
|
|
hrSamples = new ArrayList<>(hrSampleProvider.getAllSamples(summary.getStartTime().getTime(), summary.getEndTime().getTime()));
|
|
} catch (final Exception e) {
|
|
LOG.error("Error while building activity track", e);
|
|
return null;
|
|
}
|
|
|
|
Collections.sort(hrSamples, (a, b) -> Long.compare(a.getTimestamp(), b.getTimestamp()));
|
|
|
|
for (final CmfWorkoutGpsSample gpsSample : gpsSamples) {
|
|
final ActivityPoint ap = new ActivityPoint(new Date(gpsSample.getTimestamp()));
|
|
final GPSCoordinate coordinate = new GPSCoordinate(
|
|
gpsSample.getLongitude() / 10000000d,
|
|
gpsSample.getLatitude() / 10000000d,
|
|
-20000
|
|
);
|
|
ap.setLocation(coordinate);
|
|
|
|
final CmfHeartRateSample hrSample = findNearestSample(hrSamples, gpsSample.getTimestamp());
|
|
if (hrSample != null) {
|
|
ap.setHeartRate(hrSample.getHeartRate());
|
|
}
|
|
|
|
track.addTrackPoint(ap);
|
|
}
|
|
|
|
return track;
|
|
}
|
|
|
|
@Nullable
|
|
private CmfHeartRateSample findNearestSample(final List<CmfHeartRateSample> samples, final long timestamp) {
|
|
if (samples.isEmpty()) {
|
|
return null;
|
|
}
|
|
|
|
if (timestamp < samples.get(0).getTimestamp()) {
|
|
return samples.get(0);
|
|
}
|
|
|
|
if (timestamp > samples.get(samples.size() - 1).getTimestamp()) {
|
|
return samples.get(samples.size() - 1);
|
|
}
|
|
|
|
int start = 0;
|
|
int end = samples.size() - 1;
|
|
|
|
while (start <= end) {
|
|
final int mid = (start + end) / 2;
|
|
|
|
if (timestamp < samples.get(mid).getTimestamp()) {
|
|
end = mid - 1;
|
|
} else if (timestamp > samples.get(mid).getTimestamp()) {
|
|
start = mid + 1;
|
|
} else {
|
|
return samples.get(mid);
|
|
}
|
|
}
|
|
|
|
// FIXME return null if too far?
|
|
|
|
if (samples.get(start).getTimestamp() - timestamp < timestamp - samples.get(end).getTimestamp()) {
|
|
return samples.get(start);
|
|
}
|
|
|
|
return samples.get(end);
|
|
}
|
|
|
|
protected static String createActivityName(final BaseActivitySummary summary) {
|
|
String name = summary.getName();
|
|
String nameText = "";
|
|
Long id = summary.getId();
|
|
if (name != null) {
|
|
nameText = name + " - ";
|
|
}
|
|
return nameText + id;
|
|
}
|
|
|
|
private Context getContext() {
|
|
return mSupport.getContext();
|
|
}
|
|
|
|
private GBDevice getDevice() {
|
|
return mSupport.getDevice();
|
|
}
|
|
}
|