From 29787d0c9bc48d4012657c7d6de64e73a1fa18ce Mon Sep 17 00:00:00 2001 From: Daniele Gobbetti Date: Fri, 16 Aug 2024 18:40:21 +0200 Subject: [PATCH] Garmin: add gpx import functionality for models that support it Add a Field definition for GPS coordinates and remove the corresponding method from GarminUtils. Add a new message COURSE and some fields to other known messages. Also centralize some utility methods in GpxParser and GpxTrack, adapting ZeppOsGpxRouteFile. Be aware that the capability used to identify the supported watches might be the wrong one. --- .../devices/garmin/GarminCoordinator.java | 15 ++ .../garmin/GarminGpxRouteInstallHandler.java | 139 +++++++++++++ .../gadgetbridge/model/GPSCoordinate.java | 30 +++ .../devices/garmin/FileTransferHandler.java | 11 +- .../service/devices/garmin/FileType.java | 1 + .../service/devices/garmin/GarminSupport.java | 9 + .../service/devices/garmin/GarminUtils.java | 4 - .../garmin/fit/FieldDefinitionFactory.java | 4 + .../service/devices/garmin/fit/FitFile.java | 5 + .../devices/garmin/fit/GlobalFITMessage.java | 27 ++- .../garmin/fit/GpxRouteFileConverter.java | 196 ++++++++++++++++++ .../devices/garmin/fit/RecordHeader.java | 2 +- .../garmin/fit/codegen/FitCodeGen.java | 2 + .../FieldDefinitionCoordinate.java | 27 +++ .../garmin/fit/messages/FitCourse.java | 32 +++ .../devices/garmin/fit/messages/FitLap.java | 45 ++++ .../garmin/fit/messages/FitRecord.java | 24 ++- .../fit/messages/FitRecordDataFactory.java | 2 + .../zeppos/operations/ZeppOsGpxRouteFile.java | 40 +--- .../gadgetbridge/util/gpx/GpxParser.java | 43 ++++ .../gadgetbridge/util/gpx/model/GpxTrack.java | 29 ++- .../devices/garmin/GarminSupportTest.java | 6 +- 22 files changed, 627 insertions(+), 66 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminGpxRouteInstallHandler.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/GpxRouteFileConverter.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/fieldDefinitions/FieldDefinitionCoordinate.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/messages/FitCourse.java 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);