diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminCoordinator.java
index d053daa08..30172d821 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminCoordinator.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminCoordinator.java
@@ -1,6 +1,10 @@
package nodomain.freeyourgadget.gadgetbridge.devices.garmin;
+import android.content.Context;
+import android.net.Uri;
+
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import java.util.Collections;
import java.util.List;
@@ -12,6 +16,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpec
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsScreen;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
+import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.GarminCapability;
@@ -265,4 +270,14 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
return getPrefs(device).getStringSet(GarminPreferences.PREF_GARMIN_CAPABILITIES, Collections.emptySet())
.contains(capability.name());
}
+
+ @Nullable
+ @Override
+ public InstallHandler findInstallHandler(Uri uri, Context context) {
+
+ final GarminGpxRouteInstallHandler garminGpxRouteInstallHandler = new GarminGpxRouteInstallHandler(uri, context);
+ if (garminGpxRouteInstallHandler.isValid())
+ return garminGpxRouteInstallHandler;
+ return null;
+ }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminGpxRouteInstallHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminGpxRouteInstallHandler.java
new file mode 100644
index 000000000..5afb29ef8
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminGpxRouteInstallHandler.java
@@ -0,0 +1,139 @@
+/* Copyright (C) 2023-2024 Daniel Dakhno, 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 . */
+package nodomain.freeyourgadget.gadgetbridge.devices.garmin;
+
+import android.content.Context;
+import android.net.Uri;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity;
+import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
+import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.GpxRouteFileConverter;
+import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
+import nodomain.freeyourgadget.gadgetbridge.util.UriHelper;
+
+import static nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.GarminCapability.COURSE_DOWNLOAD;
+
+public class GarminGpxRouteInstallHandler implements InstallHandler {
+ private static final Logger LOG = LoggerFactory.getLogger(GarminGpxRouteInstallHandler.class);
+
+ protected final Context mContext;
+ public byte[] rawBytes;
+ private GpxRouteFileConverter gpxRouteFileConverter;
+
+ public GarminGpxRouteInstallHandler(final Uri uri, final Context context) {
+ this.mContext = context;
+
+ final UriHelper uriHelper;
+ try {
+ uriHelper = UriHelper.get(uri, context);
+ } catch (final IOException e) {
+ LOG.error("Failed to get uri", e);
+ return;
+ }
+ try (InputStream in = new BufferedInputStream(uriHelper.openInputStream())) {
+ rawBytes = FileUtils.readAll(in, 1024 * 1024); // 1MB
+
+ final GpxRouteFileConverter gpxRouteFileConverter1 = new GpxRouteFileConverter(rawBytes);
+ if (gpxRouteFileConverter1.isValid()) {
+ this.gpxRouteFileConverter = gpxRouteFileConverter1;
+ }
+ } catch (final Exception e) {
+ LOG.error("Failed to read file", e);
+ }
+ }
+
+ @Override
+ public boolean isValid() {
+ return gpxRouteFileConverter != null;
+ }
+
+ @Override
+ public void validateInstallation(final InstallActivity installActivity, final GBDevice device) {
+ if (device.isBusy()) {
+ installActivity.setInfoText(device.getBusyTask());
+ installActivity.setInstallEnabled(false);
+ return;
+ }
+
+ final DeviceCoordinator coordinator = device.getDeviceCoordinator();
+ if (!(coordinator instanceof GarminCoordinator)) {
+ LOG.warn("Coordinator is not a GarminCoordinator: {}", coordinator.getClass());
+ installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_supported));
+ installActivity.setInstallEnabled(false);
+ return;
+ }
+ final GarminCoordinator garminCoordinator = (GarminCoordinator) coordinator;
+ if (!garminCoordinator.supports(device, COURSE_DOWNLOAD)) {
+ installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_supported));
+ installActivity.setInstallEnabled(false);
+ return;
+ }
+
+ if (!device.isInitialized()) {
+ installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_ready));
+ installActivity.setInstallEnabled(false);
+ return;
+ }
+
+ final GenericItem fwItem = createInstallItem(device);
+ fwItem.setIcon(coordinator.getDefaultIconResource());
+
+ if (gpxRouteFileConverter == null) {
+ fwItem.setDetails(mContext.getString(R.string.miband_fwinstaller_incompatible_version));
+ installActivity.setInfoText(mContext.getString(R.string.fwinstaller_firmware_not_compatible_to_device));
+ installActivity.setInstallEnabled(false);
+ return;
+ }
+
+ final StringBuilder builder = new StringBuilder();
+ final String gpxRoute = mContext.getString(R.string.kind_gpx_route);
+ builder.append(mContext.getString(R.string.fw_upgrade_notice, gpxRoute));
+ installActivity.setInfoText(builder.toString());
+ installActivity.setInstallItem(fwItem);
+ installActivity.setInstallEnabled(true);
+ }
+
+ @Override
+ public void onStartInstall(final GBDevice device) {
+ }
+
+ public GpxRouteFileConverter getGpxRouteFileConverter() {
+ return gpxRouteFileConverter;
+ }
+
+ private GenericItem createInstallItem(final GBDevice device) {
+ DeviceCoordinator coordinator = device.getDeviceCoordinator();
+ final String firmwareName = mContext.getString(
+ R.string.installhandler_firmware_name,
+ mContext.getString(coordinator.getDeviceNameResource()),
+ mContext.getString(R.string.kind_gpx_route),
+ gpxRouteFileConverter.getName()
+ );
+ return new GenericItem(firmwareName);
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/GPSCoordinate.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/GPSCoordinate.java
index 6d8ab5b1d..94c310daa 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/GPSCoordinate.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/GPSCoordinate.java
@@ -16,6 +16,8 @@
along with this program. If not, see . */
package nodomain.freeyourgadget.gadgetbridge.model;
+import android.location.Location;
+
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Comparator;
@@ -51,6 +53,34 @@ public class GPSCoordinate {
return altitude;
}
+ public double getDistance(GPSCoordinate source) {
+ final Location end = new Location("end");
+ end.setLatitude(this.getLatitude());
+ end.setLongitude(this.getLongitude());
+
+ final Location start = new Location("start");
+ start.setLatitude(source.getLatitude());
+ start.setLongitude(source.getLongitude());
+
+ return end.distanceTo(start);
+ }
+
+ public double getAltitudeDifference(GPSCoordinate source) {
+ if (this.getAltitude() == UNKNOWN_ALTITUDE)
+ return 0;
+ if (source.getAltitude() == UNKNOWN_ALTITUDE)
+ return 0;
+ return this.getAltitude() - source.getAltitude();
+ }
+
+ public double getAscent(GPSCoordinate source) {
+ return Math.max(0, this.getAltitudeDifference(source));
+ }
+
+ public double getDescent(GPSCoordinate source) {
+ return Math.max(0, -this.getAltitudeDifference(source));
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/FileTransferHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/FileTransferHandler.java
index 6abb94bb4..d60ebc547 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/FileTransferHandler.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/FileTransferHandler.java
@@ -17,6 +17,7 @@ import java.util.Locale;
import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.FileDownloadedDeviceEvent;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.CreateFileMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.DownloadRequestMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.FileTransferDataMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.GFDIMessage;
@@ -87,10 +88,10 @@ public class FileTransferHandler implements MessageHandler {
// return new DownloadRequestMessage(0, 0, DownloadRequestMessage.REQUEST_TYPE.NEW, 0, 0);
// }
//
-// public CreateFileMessage initiateUpload(byte[] fileAsByteArray, FileType.FILETYPE filetype) {
-// upload.setCurrentlyUploading(new FileFragment(new DirectoryEntry(0, filetype, 0, 0, 0, fileAsByteArray.length, null), fileAsByteArray));
-// return new CreateFileMessage(fileAsByteArray.length, filetype);
-// }
+public CreateFileMessage initiateUpload(byte[] fileAsByteArray, FileType.FILETYPE filetype) {
+ upload.setCurrentlyUploading(new FileFragment(new DirectoryEntry(0, filetype, 0, 0, 0, fileAsByteArray.length, null), fileAsByteArray));
+ return new CreateFileMessage(fileAsByteArray.length, filetype);
+}
public class Download {
@@ -296,7 +297,7 @@ public class FileTransferHandler implements MessageHandler {
private FileTransferDataMessage take() {
final int currentOffset = this.dataHolder.position();
- final byte[] chunk = new byte[Math.min(this.dataHolder.remaining(), getMaxBlockSize())];
+ final byte[] chunk = new byte[Math.min(this.dataHolder.remaining(), getMaxBlockSize() - 13)]; //actual payload in FileTransferDataMessage
this.dataHolder.get(chunk);
setRunningCrc(ChecksumCalculator.computeCrc(getRunningCrc(), chunk, 0, chunk.length));
return new FileTransferDataMessage(chunk, currentOffset, getRunningCrc());
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/FileType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/FileType.java
index 4a5f7b49e..670cc78f9 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/FileType.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/FileType.java
@@ -79,6 +79,7 @@ public class FileType {
FBT_PTD_BACKUP(128, 74),
// Other files
+ DOWNLOAD_COURSE(255, 4),
ERROR_SHUTDOWN_REPORTS(255, 245),
IQ_ERROR_REPORTS(255, 244),
ULF_LOGS(255, 247),
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java
index 801ba875f..5afca0659 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java
@@ -37,6 +37,7 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminCoordinator;
+import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminGpxRouteInstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminPreferences;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.GarminCapability;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
@@ -797,6 +798,14 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
}
}
+ @Override
+ public void onInstallApp(Uri uri) {
+ final GarminGpxRouteInstallHandler garminGpxRouteInstallHandler = new GarminGpxRouteInstallHandler(uri, getContext());
+ if (garminGpxRouteInstallHandler.isValid()) {
+ communicator.sendMessage("upload course file", fileTransferHandler.initiateUpload(garminGpxRouteInstallHandler.getGpxRouteFileConverter().getConvertedFile().getOutgoingMessage(), FileType.FILETYPE.DOWNLOAD_COURSE).getOutgoingMessage());
+ }
+ }
+
@Override
public void onTestNewFunction() {
parseAllFitFilesFromStorage();
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminUtils.java
index 75c6e5c65..099a23d75 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminUtils.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminUtils.java
@@ -10,10 +10,6 @@ public final class GarminUtils {
// utility class
}
- public static double semicirclesToDegrees(final long semicircles) {
- return semicircles * (180.0D / 0x80000000L);
- }
-
public static GdiCore.CoreService.LocationData toLocationData(final Location location, final GdiCore.CoreService.DataType dataType) {
final GdiCore.CoreService.LatLon positionForWatch = GdiCore.CoreService.LatLon.newBuilder()
.setLat((int) ((location.getLatitude() * 2.147483648E9d) / 180.0d))
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FieldDefinitionFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FieldDefinitionFactory.java
index 640a95d0c..44426d45f 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FieldDefinitionFactory.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FieldDefinitionFactory.java
@@ -2,6 +2,7 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionAlarm;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionCoordinate;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionDayOfWeek;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionFileType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionGoalSource;
@@ -47,6 +48,8 @@ public class FieldDefinitionFactory {
return new FieldDefinitionSleepStage(localNumber, size, baseType, name);
case WEATHER_AQI:
return new FieldDefinitionWeatherAqi(localNumber, size, baseType, name);
+ case COORDINATE:
+ return new FieldDefinitionCoordinate(localNumber, size, baseType, name);
default:
return new FieldDefinition(localNumber, size, baseType, name);
}
@@ -66,5 +69,6 @@ public class FieldDefinitionFactory {
LANGUAGE,
SLEEP_STAGE,
WEATHER_AQI,
+ COORDINATE
}
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitFile.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitFile.java
index 6579d7d45..4ac3e83d2 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitFile.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitFile.java
@@ -144,7 +144,12 @@ public class FitFile {
this.header.generateOutgoingDataPayload(writer);
writer.writeBytes(temporary.getBytes());
writer.writeShort(ChecksumCalculator.computeCrc(writer.getBytes(), this.header.getHeaderSize(), writer.getBytes().length - this.header.getHeaderSize()));
+ }
+ public byte[] getOutgoingMessage() {
+ final MessageWriter writer = new MessageWriter();
+ this.generateOutgoingDataPayload(writer);
+ return writer.getBytes();
}
@NonNull
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/GlobalFITMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/GlobalFITMessage.java
index 3b5916796..f7d33ce73 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/GlobalFITMessage.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/GlobalFITMessage.java
@@ -91,8 +91,8 @@ public class GlobalFITMessage {
new FieldDefinitionPrimitive(0, BaseType.ENUM, "event"), // 9 lap
new FieldDefinitionPrimitive(1, BaseType.ENUM, "event_type"), // 1 stop
new FieldDefinitionPrimitive(2, BaseType.UINT32, "start_time"),
- new FieldDefinitionPrimitive(3, BaseType.SINT32, "start_latitude"),
- new FieldDefinitionPrimitive(4, BaseType.SINT32, "start_longitude"),
+ new FieldDefinitionPrimitive(3, BaseType.SINT32, "start_latitude", FieldDefinitionFactory.FIELD.COORDINATE),
+ new FieldDefinitionPrimitive(4, BaseType.SINT32, "start_longitude", FieldDefinitionFactory.FIELD.COORDINATE),
new FieldDefinitionPrimitive(5, BaseType.ENUM, "sport"),
new FieldDefinitionPrimitive(6, BaseType.ENUM, "sub_sport"),
new FieldDefinitionPrimitive(7, BaseType.UINT32, "total_elapsed_time"), // with pauses
@@ -106,16 +106,27 @@ public class GlobalFITMessage {
));
public static GlobalFITMessage LAP = new GlobalFITMessage(19, "LAP", Arrays.asList(
+ new FieldDefinitionPrimitive(3, BaseType.SINT32, "start_lat", FieldDefinitionFactory.FIELD.COORDINATE),
+ new FieldDefinitionPrimitive(4, BaseType.SINT32, "start_long", FieldDefinitionFactory.FIELD.COORDINATE),
+ new FieldDefinitionPrimitive(5, BaseType.SINT32, "end_lat", FieldDefinitionFactory.FIELD.COORDINATE),
+ new FieldDefinitionPrimitive(6, BaseType.SINT32, "end_long", FieldDefinitionFactory.FIELD.COORDINATE),
+ new FieldDefinitionPrimitive(7, BaseType.UINT32, "total_elapsed_time", 1000, 0), // s
+ new FieldDefinitionPrimitive(8, BaseType.UINT32, "total_timer_time", 1000, 0), // s
+ new FieldDefinitionPrimitive(9, BaseType.UINT32, "total_distance", 100, 0), // m
+ new FieldDefinitionPrimitive(21, BaseType.UINT16, "total_ascent"), // m
+ new FieldDefinitionPrimitive(22, BaseType.UINT16, "total_descent"), // m
new FieldDefinitionPrimitive(253, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP)
));
public static GlobalFITMessage RECORD = new GlobalFITMessage(20, "RECORD", Arrays.asList(
- new FieldDefinitionPrimitive(0, BaseType.SINT32, "latitude"),
- new FieldDefinitionPrimitive(1, BaseType.SINT32, "longitude"),
+ new FieldDefinitionPrimitive(0, BaseType.SINT32, "latitude", FieldDefinitionFactory.FIELD.COORDINATE),
+ new FieldDefinitionPrimitive(1, BaseType.SINT32, "longitude", FieldDefinitionFactory.FIELD.COORDINATE),
+ new FieldDefinitionPrimitive(2, BaseType.UINT16, "altitude", 5, 500), // m
new FieldDefinitionPrimitive(3, BaseType.UINT8, "heart_rate"),
new FieldDefinitionPrimitive(5, BaseType.UINT32, "distance", 100, 0), // m
+ new FieldDefinitionPrimitive(6, BaseType.UINT16, "speed", 1000, 0), // m/s
new FieldDefinitionPrimitive(73, BaseType.UINT32, "enhanced_speed"), // mm/s
- new FieldDefinitionPrimitive(78, BaseType.UINT32, "enhanced_altitude"), // dm
+ new FieldDefinitionPrimitive(78, BaseType.UINT32, "enhanced_altitude", 5, 500), // m
new FieldDefinitionPrimitive(136, BaseType.UINT8, "wrist_heart_rate"),
new FieldDefinitionPrimitive(143, BaseType.UINT8, "body_battery"),
new FieldDefinitionPrimitive(253, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP)
@@ -137,6 +148,11 @@ public class GlobalFITMessage {
new FieldDefinitionPrimitive(253, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP)
));
+ public static GlobalFITMessage COURSE = new GlobalFITMessage(31, "COURSE", Arrays.asList(
+ new FieldDefinitionPrimitive(4, BaseType.ENUM, "sport"),
+ new FieldDefinitionPrimitive(5, BaseType.STRING, 16, "name")
+ ));
+
public static GlobalFITMessage FILE_CREATOR = new GlobalFITMessage(49, "FILE_CREATOR", Arrays.asList(
new FieldDefinitionPrimitive(0, BaseType.UINT16, "software_version"),
new FieldDefinitionPrimitive(1, BaseType.UINT8, "hardware_version")
@@ -282,6 +298,7 @@ public class GlobalFITMessage {
put(20, RECORD);
put(21, EVENT);
put(23, DEVICE_INFO);
+ put(31, COURSE);
put(49, FILE_CREATOR);
put(55, MONITORING);
put(127, CONNECTIVITY);
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/GpxRouteFileConverter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/GpxRouteFileConverter.java
new file mode 100644
index 000000000..a6804d15e
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/GpxRouteFileConverter.java
@@ -0,0 +1,196 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.FileType;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.enums.GarminSport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitRecordDataFactory;
+import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
+import nodomain.freeyourgadget.gadgetbridge.util.gpx.GpxParser;
+import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxFile;
+import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxTrack;
+import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxTrackPoint;
+import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxTrackSegment;
+
+public class GpxRouteFileConverter {
+ private static final Logger LOG = LoggerFactory.getLogger(GpxRouteFileConverter.class);
+ final double speed = 1.4; // m/s // TODO: make this configurable (and activity dependent?)
+ final int activity = GarminSport.RUN.getType(); //TODO: make this configurable
+ private final long timestamp;
+ private final GpxFile gpxFile;
+ private FitFile convertedFile;
+ private String name;
+
+ public GpxRouteFileConverter(byte[] xmlBytes) {
+ this.timestamp = System.currentTimeMillis() / 1000;
+ this.gpxFile = GpxParser.parseGpx(xmlBytes);
+ try {
+ this.convertedFile = convertGpxToRoute(gpxFile);
+ } catch (Exception e) {
+ LOG.error(e.getMessage());
+ this.convertedFile = null;
+ }
+ }
+
+ private static RecordData getFileCreatorRecordData() {
+ final RecordData fileCreatorRecord = FitRecordDataFactory.create(
+ new RecordDefinition(new RecordHeader((byte) 0x41), ByteOrder.BIG_ENDIAN, GlobalFITMessage.FILE_CREATOR, GlobalFITMessage.FILE_CREATOR.getFieldDefinitions(0), null),
+ new RecordHeader((byte) 0x01));
+ fileCreatorRecord.setFieldByName("software_version", 1);
+ return fileCreatorRecord;
+ }
+
+ public FitFile getConvertedFile() {
+ return convertedFile;
+ }
+
+ public boolean isValid() {
+ return this.convertedFile != null;
+ }
+
+ public String getName() {
+ if (gpxFile == null) {
+ return "";
+ }
+
+ if (!StringUtils.isNullOrEmpty(this.name))
+ return this.name;
+
+ if (!StringUtils.isNullOrEmpty(gpxFile.getName())) {
+ return gpxFile.getName();
+ } else {
+ return String.valueOf(timestamp);
+ }
+ }
+
+ private FitFile convertGpxToRoute(GpxFile gpxFile) {
+ if (gpxFile.getTracks().isEmpty()) {
+ LOG.error("Gpx file contains no Tracks.");
+ return null;
+ }
+ //GPX files may contain multiple tracks, we use only the first one
+ final GpxTrack track = gpxFile.getTracks().get(0);
+
+ if (track.getTrackSegments().isEmpty()) {
+ LOG.error("Gpx track contains no segment.");
+ return null;
+ }
+ //GPX track may contain multiple segments, we use only the first one
+ GpxTrackSegment gpxTrackSegment = track.getTrackSegments().get(0);
+
+ List gpxTrackPointList = gpxTrackSegment.getTrackPoints();
+ if (gpxTrackPointList.isEmpty()) {
+ LOG.error("Gpx track segment contains no point");
+ return null;
+ }
+
+ this.name = track.getName();
+
+ final RecordHeader gpxDataPointRecordHeader = new RecordHeader((byte) 0x05);
+ final RecordDefinition gpxDataPointRecordDefinition = new RecordDefinition(new RecordHeader((byte) 0x45), ByteOrder.BIG_ENDIAN, GlobalFITMessage.RECORD, GlobalFITMessage.RECORD.getFieldDefinitions(0, 1, 2, 5, 253), null);
+ List gpxPointDataRecords = new ArrayList<>();
+
+ double totalAscent = 0;
+ double totalDescent = 0;
+ double totalDistance = 0;
+ long runningTs = timestamp;
+
+ GPSCoordinate prevPoint = gpxTrackPointList.get(0);
+
+ for (GPSCoordinate point :
+ gpxTrackPointList) {
+ totalAscent += point.getAscent(prevPoint);
+ totalDescent += point.getDescent(prevPoint);
+ totalDistance += point.getDistance(prevPoint);
+ runningTs += (long) (point.getDistance(prevPoint) / speed);
+ final RecordData gpxDataPointRecord = FitRecordDataFactory.create(gpxDataPointRecordDefinition, gpxDataPointRecordHeader);
+
+ gpxDataPointRecord.setFieldByName("latitude", point.getLatitude());
+ gpxDataPointRecord.setFieldByName("longitude", point.getLongitude());
+ gpxDataPointRecord.setFieldByName("altitude", point.getAltitude());
+ gpxDataPointRecord.setFieldByName("distance", totalDistance);
+ gpxDataPointRecord.setFieldByName("timestamp", runningTs);
+
+ prevPoint = point;
+ gpxPointDataRecords.add(gpxDataPointRecord);
+ }
+
+ final RecordData lapRecord = getLapRecordData(gpxTrackPointList);
+ lapRecord.setFieldByName("total_distance", totalDistance);
+ lapRecord.setFieldByName("total_ascent", totalAscent);
+ lapRecord.setFieldByName("total_descent", totalDescent);
+ lapRecord.setFieldByName("total_elapsed_time", (runningTs - timestamp));
+ lapRecord.setFieldByName("total_timer_time", (runningTs - timestamp));
+
+ final List courseFileDataRecords = new ArrayList<>();
+ courseFileDataRecords.add(getFileIdRecordData());
+ courseFileDataRecords.add(getFileCreatorRecordData());
+ courseFileDataRecords.add(getCourseRecordData());
+ courseFileDataRecords.add(lapRecord);
+
+ final RecordHeader eventRecordHeader = new RecordHeader((byte) 0x04);
+ final RecordDefinition eventRecordDefinition = new RecordDefinition(new RecordHeader((byte) 0x44), ByteOrder.BIG_ENDIAN, GlobalFITMessage.EVENT, GlobalFITMessage.EVENT.getFieldDefinitions(0, 1, 4, 253), null);
+ courseFileDataRecords.add(getEventRecordData(eventRecordDefinition, eventRecordHeader, timestamp, 0));
+ courseFileDataRecords.add(getEventRecordData(eventRecordDefinition, eventRecordHeader, runningTs, 9));
+
+ courseFileDataRecords.addAll(gpxPointDataRecords);
+
+ return new FitFile(courseFileDataRecords);
+ }
+
+ private RecordData getEventRecordData(RecordDefinition eventRecordDefinition, RecordHeader eventRecordHeader, long timestamp, int eventType) {
+ final RecordData startEvent = FitRecordDataFactory.create(
+ eventRecordDefinition,
+ eventRecordHeader);
+
+ startEvent.setFieldByName("timestamp", timestamp);
+ startEvent.setFieldByName("event", 0);
+ startEvent.setFieldByName("event_group", 0);
+ startEvent.setFieldByName("event_type", eventType);
+ return startEvent;
+ }
+
+ private RecordData getLapRecordData(List gpxTrackPointList) {
+ final GPSCoordinate first = gpxTrackPointList.get(0);
+ final GPSCoordinate last = gpxTrackPointList.get(gpxTrackPointList.size() - 1);
+
+ final RecordData lapRecord = FitRecordDataFactory.create(
+ new RecordDefinition(new RecordHeader((byte) 0x43), ByteOrder.BIG_ENDIAN, GlobalFITMessage.LAP, GlobalFITMessage.LAP.getFieldDefinitions(3, 4, 5, 6, 7, 8, 9, 21, 22, 253), null),
+ new RecordHeader((byte) 0x03));
+ lapRecord.setFieldByName("start_lat", first.getLatitude());
+ lapRecord.setFieldByName("start_long", first.getLongitude());
+ lapRecord.setFieldByName("end_lat", last.getLatitude());
+ lapRecord.setFieldByName("end_long", last.getLongitude());
+ lapRecord.setFieldByName("timestamp", timestamp);
+ return lapRecord;
+ }
+
+ private RecordData getCourseRecordData() {
+ final RecordData courseRecord = FitRecordDataFactory.create(
+ new RecordDefinition(new RecordHeader((byte) 0x42), ByteOrder.BIG_ENDIAN, GlobalFITMessage.COURSE, GlobalFITMessage.COURSE.getFieldDefinitions(4, 5), null),
+ new RecordHeader((byte) 0x02));
+ courseRecord.setFieldByName("sport", activity); //TODO use track.getType()
+ courseRecord.setFieldByName("name", this.getName());
+ return courseRecord;
+ }
+
+ private RecordData getFileIdRecordData() {
+ final RecordData fileIdRecord = FitRecordDataFactory.create(
+ new RecordDefinition(new RecordHeader((byte) 0x40), ByteOrder.BIG_ENDIAN, GlobalFITMessage.FILE_ID, GlobalFITMessage.FILE_ID.getFieldDefinitions(0, 1, 2, 3, 4, 5), null),
+ new RecordHeader((byte) 0x00));
+ fileIdRecord.setFieldByName("type", FileType.FILETYPE.COURSES.getSubType());
+ fileIdRecord.setFieldByName("manufacturer", 1);
+ fileIdRecord.setFieldByName("product", 65534);
+ fileIdRecord.setFieldByName("time_created", timestamp);
+ fileIdRecord.setFieldByName("serial_number", 1);
+ fileIdRecord.setFieldByName("number", 1);
+ return fileIdRecord;
+ }
+
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/RecordHeader.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/RecordHeader.java
index 33eb6ef6d..1440ea97d 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/RecordHeader.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/RecordHeader.java
@@ -69,7 +69,7 @@ public class RecordHeader {
public byte generateOutgoingDataPayload() { //TODO: unclear if correct
if (!definition && !developerData) {
- assert timeOffset != null;
+ if (timeOffset != null)
return (byte) (timeOffset | (((byte) localMessageType) << 5));
}
byte base = (byte) localMessageType;
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/codegen/FitCodeGen.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/codegen/FitCodeGen.java
index e324bea48..83c06ed83 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/codegen/FitCodeGen.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/codegen/FitCodeGen.java
@@ -252,6 +252,8 @@ public class FitCodeGen {
return FieldDefinitionSleepStage.SleepStage.class;
case WEATHER_AQI:
return FieldDefinitionWeatherAqi.AQI_LEVELS.class;
+ case COORDINATE:
+ return Double.class;
}
throw new RuntimeException("Unknown field type " + primitive.getType());
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionCoordinate.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionCoordinate.java
new file mode 100644
index 000000000..dd2705f69
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionCoordinate.java
@@ -0,0 +1,27 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions;
+
+import java.nio.ByteBuffer;
+
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FieldDefinition;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType;
+
+public class FieldDefinitionCoordinate extends FieldDefinition {
+
+ final double conversionFactor = (180.0D / 0x80000000L);
+
+ public FieldDefinitionCoordinate(int localNumber, int size, BaseType baseType, String name) {
+ super(localNumber, size, baseType, name, 1, 0);
+ }
+
+ @Override
+ public Object decode(ByteBuffer byteBuffer) {
+ return ((long) baseType.decode(byteBuffer, 1, 0)) * conversionFactor;
+ }
+
+ @Override
+ public void encode(ByteBuffer byteBuffer, Object o) {
+ baseType.encode(byteBuffer, (int) Math.round((double) o / conversionFactor), 1, 0);
+ }
+
+
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitCourse.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitCourse.java
new file mode 100644
index 000000000..cce2cd0e4
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitCourse.java
@@ -0,0 +1,32 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages;
+
+import androidx.annotation.Nullable;
+
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
+
+//
+// WARNING: This class was auto-generated, please avoid modifying it directly.
+// See nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.codegen.FitCodeGen
+//
+public class FitCourse extends RecordData {
+ public FitCourse(final RecordDefinition recordDefinition, final RecordHeader recordHeader) {
+ super(recordDefinition, recordHeader);
+
+ final int globalNumber = recordDefinition.getGlobalFITMessage().getNumber();
+ if (globalNumber != 31) {
+ throw new IllegalArgumentException("FitCourse expects global messages of " + 31 + ", got " + globalNumber);
+ }
+ }
+
+ @Nullable
+ public Integer getSport() {
+ return (Integer) getFieldByNumber(4);
+ }
+
+ @Nullable
+ public String getName() {
+ return (String) getFieldByNumber(5);
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitLap.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitLap.java
index 43a889892..34dafcee4 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitLap.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitLap.java
@@ -20,6 +20,51 @@ public class FitLap extends RecordData {
}
}
+ @Nullable
+ public Double getStartLat() {
+ return (Double) getFieldByNumber(3);
+ }
+
+ @Nullable
+ public Double getStartLong() {
+ return (Double) getFieldByNumber(4);
+ }
+
+ @Nullable
+ public Double getEndLat() {
+ return (Double) getFieldByNumber(5);
+ }
+
+ @Nullable
+ public Double getEndLong() {
+ return (Double) getFieldByNumber(6);
+ }
+
+ @Nullable
+ public Long getTotalElapsedTime() {
+ return (Long) getFieldByNumber(7);
+ }
+
+ @Nullable
+ public Long getTotalTimerTime() {
+ return (Long) getFieldByNumber(8);
+ }
+
+ @Nullable
+ public Long getTotalDistance() {
+ return (Long) getFieldByNumber(9);
+ }
+
+ @Nullable
+ public Integer getTotalAscent() {
+ return (Integer) getFieldByNumber(21);
+ }
+
+ @Nullable
+ public Integer getTotalDescent() {
+ return (Integer) getFieldByNumber(22);
+ }
+
@Nullable
public Long getTimestamp() {
return (Long) getFieldByNumber(253);
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitRecord.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitRecord.java
index aff3aec2c..4a89bb250 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitRecord.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitRecord.java
@@ -26,13 +26,18 @@ public class FitRecord extends RecordData {
}
@Nullable
- public Long getLatitude() {
- return (Long) getFieldByNumber(0);
+ public Double getLatitude() {
+ return (Double) getFieldByNumber(0);
}
@Nullable
- public Long getLongitude() {
- return (Long) getFieldByNumber(1);
+ public Double getLongitude() {
+ return (Double) getFieldByNumber(1);
+ }
+
+ @Nullable
+ public Integer getAltitude() {
+ return (Integer) getFieldByNumber(2);
}
@Nullable
@@ -45,6 +50,11 @@ public class FitRecord extends RecordData {
return (Long) getFieldByNumber(5);
}
+ @Nullable
+ public Integer getSpeed() {
+ return (Integer) getFieldByNumber(6);
+ }
+
@Nullable
public Long getEnhancedSpeed() {
return (Long) getFieldByNumber(73);
@@ -77,9 +87,9 @@ public class FitRecord extends RecordData {
activityPoint.setTime(new Date(getComputedTimestamp()));
if (getLatitude() != null && getLongitude() != null) {
activityPoint.setLocation(new GPSCoordinate(
- GarminUtils.semicirclesToDegrees(getLongitude().longValue()),
- GarminUtils.semicirclesToDegrees(getLatitude().longValue()),
- getEnhancedAltitude() != null ? getEnhancedAltitude() / 10d : GPSCoordinate.UNKNOWN_ALTITUDE
+ getLongitude(),
+ getLatitude(),
+ getEnhancedAltitude() != null ? getEnhancedAltitude() : GPSCoordinate.UNKNOWN_ALTITUDE
));
}
if (getHeartRate() != null) {
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitRecordDataFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitRecordDataFactory.java
index 0ce3b1821..a7f392df8 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitRecordDataFactory.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitRecordDataFactory.java
@@ -37,6 +37,8 @@ public class FitRecordDataFactory {
return new FitEvent(recordDefinition, recordHeader);
case 23:
return new FitDeviceInfo(recordDefinition, recordHeader);
+ case 31:
+ return new FitCourse(recordDefinition, recordHeader);
case 49:
return new FitFileCreator(recordDefinition, recordHeader);
case 55:
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/operations/ZeppOsGpxRouteFile.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/operations/ZeppOsGpxRouteFile.java
index 0f3267b84..d5d0e4005 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/operations/ZeppOsGpxRouteFile.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/operations/ZeppOsGpxRouteFile.java
@@ -18,12 +18,9 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.operat
import android.location.Location;
-import androidx.annotation.Nullable;
-
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
@@ -31,9 +28,7 @@ import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
-import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
-import nodomain.freeyourgadget.gadgetbridge.util.gpx.GpxParseException;
import nodomain.freeyourgadget.gadgetbridge.util.gpx.GpxParser;
import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxFile;
import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxTrackPoint;
@@ -44,15 +39,6 @@ public class ZeppOsGpxRouteFile {
private static final double COORD_MULTIPLIER = 3000000.0;
- public static final byte[] XML_HEADER = new byte[]{
- '<', '?', 'x', 'm', 'l'
- };
-
- // Some gpx files start with ". */
package nodomain.freeyourgadget.gadgetbridge.util.gpx;
+import androidx.annotation.Nullable;
+
import com.google.gson.internal.bind.util.ISO8601Utils;
import org.slf4j.Logger;
@@ -24,12 +26,14 @@ import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
+import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParsePosition;
import java.util.Date;
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
+import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxFile;
import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxTrack;
import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxTrackPoint;
@@ -39,11 +43,44 @@ import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxWaypoint;
public class GpxParser {
private static final Logger LOG = LoggerFactory.getLogger(GpxParser.class);
+ public static final byte[] XML_HEADER = new byte[]{
+ '<', '?', 'x', 'm', 'l'
+ };
+
+ // Some gpx files start with " trackSegments;
- public GpxTrack(final List trackSegments) {
+ public GpxTrack(String name, String type, final List trackSegments) {
+ this.name = name;
+ this.type = type;
this.trackSegments = trackSegments;
}
+ public String getName() {
+ return name;
+ }
+
+ public String getType() {
+ return type;
+ }
+
public List getTrackSegments() {
return trackSegments;
}
@@ -41,15 +54,27 @@ public class GpxTrack {
}
public static class Builder {
+ private String name;
+ private String type;
private final List trackSegments = new ArrayList<>();
+ public Builder withName(final String name) {
+ this.name = name;
+ return this;
+ }
+
+ public Builder withType(final String type) {
+ this.type = type;
+ return this;
+ }
+
public Builder withTrackSegment(final GpxTrackSegment trackSegment) {
trackSegments.add(trackSegment);
return this;
}
public GpxTrack build() {
- return new GpxTrack(trackSegments);
+ return new GpxTrack(name, type, trackSegments);
}
}
}
diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupportTest.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupportTest.java
index 047f5c96f..1601dc0bb 100644
--- a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupportTest.java
+++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupportTest.java
@@ -364,9 +364,9 @@ public class GarminSupportTest extends TestBase {
"FitFileId{manufacturer=15, type=ACTIVITY, product=9001, serial_number=1701}, " +
"FitDeveloperData{application_id=[1,1,2,3,5,8,13,21,34,55,89,144,233,121,98,219], developer_data_index=0}, " +
"FitFieldDescription{developer_data_index=0, field_definition_number=0, fit_base_type_id=1, field_name=doughnuts_earned, units=doughnuts}, " +
- "FitRecord{heart_rate=140, unknown_4(UINT8/1)=88, distance=510, unknown_6(UINT16/2)=47488, doughnuts_earned=1}, " +
- "FitRecord{heart_rate=143, unknown_4(UINT8/1)=90, distance=2080, unknown_6(UINT16/2)=36416, doughnuts_earned=2}, " +
- "FitRecord{heart_rate=144, unknown_4(UINT8/1)=92, distance=3710, unknown_6(UINT16/2)=35344, doughnuts_earned=3}" +
+ "FitRecord{heart_rate=140, unknown_4(UINT8/1)=88, distance=510, speed=47, doughnuts_earned=1}, " +
+ "FitRecord{heart_rate=143, unknown_4(UINT8/1)=90, distance=2080, speed=36, doughnuts_earned=2}, " +
+ "FitRecord{heart_rate=144, unknown_4(UINT8/1)=92, distance=3710, speed=35, doughnuts_earned=3}" +
"]";
FitFile fitFile = FitFile.parseIncoming(fileContents);