mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2025-01-10 17:11:56 +01:00
Garmin: Upload workout fit files
This commit is contained in:
parent
8b4f1aa1d7
commit
ba82de99d3
@ -34,7 +34,6 @@ import nodomain.freeyourgadget.gadgetbridge.entities.GarminBodyEnergySampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminEventSampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminHrvSummarySampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminHrvValueSampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminRespiratoryRateSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminSleepStageSampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminSpo2SampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminStressSampleDao;
|
||||
@ -343,10 +342,14 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
@Nullable
|
||||
@Override
|
||||
public InstallHandler findInstallHandler(Uri uri, Context context) {
|
||||
final GarminFitFileInstallHandler fitFileInstallHandler = new GarminFitFileInstallHandler(uri, context);
|
||||
if (fitFileInstallHandler.isValid())
|
||||
return fitFileInstallHandler;
|
||||
|
||||
final GarminGpxRouteInstallHandler garminGpxRouteInstallHandler = new GarminGpxRouteInstallHandler(uri, context);
|
||||
if (garminGpxRouteInstallHandler.isValid())
|
||||
return garminGpxRouteInstallHandler;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,231 @@
|
||||
/* 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.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 java.util.Optional;
|
||||
|
||||
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.devices.vivomovehr.GarminCapability;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.FileType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitFile;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitCourse;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitFileId;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitWorkout;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.UriHelper;
|
||||
|
||||
public class GarminFitFileInstallHandler implements InstallHandler {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GarminFitFileInstallHandler.class);
|
||||
|
||||
protected final Context mContext;
|
||||
private byte[] rawBytes;
|
||||
private FitFile fitFile;
|
||||
private FileType.FILETYPE fileType;
|
||||
|
||||
public GarminFitFileInstallHandler(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;
|
||||
}
|
||||
|
||||
// Quickly check whether it's a valid fit file without reading the entire thing
|
||||
try (InputStream in = new BufferedInputStream(uriHelper.openInputStream())) {
|
||||
final byte[] header = new byte[12];
|
||||
final int read = in.read(header);
|
||||
if (read != header.length) {
|
||||
throw new IOException("Not enough bytes for fit header");
|
||||
}
|
||||
if (BLETypeConversions.toUint32(header, 8) != FitFile.Header.MAGIC) {
|
||||
// not fit file
|
||||
return;
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Failed to validate fit file", e);
|
||||
return;
|
||||
}
|
||||
|
||||
try (InputStream in = new BufferedInputStream(uriHelper.openInputStream())) {
|
||||
rawBytes = FileUtils.readAll(in, 10 * 1024 * 1024); // 10MB
|
||||
fitFile = FitFile.parseIncoming(rawBytes);
|
||||
|
||||
final Optional<FitFileId> fitFileIdOpt = fitFile.getRecords().stream()
|
||||
.filter(r -> r instanceof FitFileId)
|
||||
.map(r -> (FitFileId) r)
|
||||
.findFirst();
|
||||
|
||||
if (!fitFileIdOpt.isPresent()) {
|
||||
LOG.error("Fit file has no ID");
|
||||
return;
|
||||
}
|
||||
|
||||
final FitFileId fitFileId = fitFileIdOpt.get();
|
||||
if (fitFileId.getType() == null) {
|
||||
LOG.error("Fit file ID has null type");
|
||||
return;
|
||||
}
|
||||
|
||||
fileType = fitFileId.getType();
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Failed to read fit file", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid() {
|
||||
return fitFile != null && fileType != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateInstallation(final InstallActivity installActivity, final GBDevice device) {
|
||||
if (fitFile == null || fileType == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
final boolean fileSupported = parseFitFile(installActivity, garminCoordinator, device);
|
||||
|
||||
if (!fileSupported) {
|
||||
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;
|
||||
}
|
||||
|
||||
installActivity.setInstallEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartInstall(final GBDevice device) {
|
||||
}
|
||||
|
||||
public byte[] getRawBytes() {
|
||||
return rawBytes;
|
||||
}
|
||||
|
||||
public FitFile getFitFile() {
|
||||
return fitFile;
|
||||
}
|
||||
|
||||
public FileType.FILETYPE getFileType() {
|
||||
return fileType;
|
||||
}
|
||||
|
||||
private boolean parseFitFile(final InstallActivity installActivity, final GarminCoordinator coordinator, final GBDevice device) {
|
||||
final String name;
|
||||
final int kindName;
|
||||
|
||||
switch (fileType) {
|
||||
case COURSES:
|
||||
if (!coordinator.supports(device, GarminCapability.COURSE_DOWNLOAD)) {
|
||||
LOG.warn("Device does not support course download");
|
||||
return false;
|
||||
}
|
||||
final Optional<FitCourse> fitCourseOpt = fitFile.getRecords().stream()
|
||||
.filter(r -> r instanceof FitCourse)
|
||||
.map(r -> (FitCourse) r)
|
||||
.findFirst();
|
||||
|
||||
if (!fitCourseOpt.isPresent()) {
|
||||
LOG.error("Fit file has no course record");
|
||||
return false;
|
||||
}
|
||||
final FitCourse fitCourse = fitCourseOpt.get();
|
||||
|
||||
name = String.valueOf(fitCourse.getName());
|
||||
kindName = R.string.kind_gpx_route;
|
||||
|
||||
break;
|
||||
case WORKOUTS:
|
||||
if (!coordinator.supports(device, GarminCapability.WORKOUT_DOWNLOAD)) {
|
||||
LOG.warn("Device does not support workout download");
|
||||
return false;
|
||||
}
|
||||
final Optional<FitWorkout> fitWorkoutOpt = fitFile.getRecords().stream()
|
||||
.filter(r -> r instanceof FitWorkout)
|
||||
.map(r -> (FitWorkout) r)
|
||||
.findFirst();
|
||||
|
||||
if (!fitWorkoutOpt.isPresent()) {
|
||||
LOG.error("Fit file has no workout record");
|
||||
return false;
|
||||
}
|
||||
final FitWorkout fitWorkout = fitWorkoutOpt.get();
|
||||
|
||||
name = String.valueOf(fitWorkout.getName());
|
||||
kindName = R.string.menuitem_workout;
|
||||
|
||||
break;
|
||||
default:
|
||||
LOG.warn("Unsupported fit file type: {}", fileType);
|
||||
return false;
|
||||
}
|
||||
|
||||
final GenericItem fwItem = new GenericItem(mContext.getString(
|
||||
R.string.installhandler_firmware_name,
|
||||
mContext.getString(coordinator.getDeviceNameResource()),
|
||||
mContext.getString(kindName),
|
||||
name
|
||||
));
|
||||
fwItem.setIcon(coordinator.getDefaultIconResource());
|
||||
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
final String kindNameString = mContext.getString(kindName);
|
||||
builder.append(mContext.getString(R.string.fw_upgrade_notice, kindNameString));
|
||||
installActivity.setInfoText(builder.toString());
|
||||
installActivity.setInstallItem(fwItem);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@ -36,6 +36,7 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.PendingFileProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminFitFileInstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminGpxRouteInstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminPreferences;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.GarminCapability;
|
||||
@ -821,6 +822,17 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
|
||||
|
||||
@Override
|
||||
public void onInstallApp(Uri uri) {
|
||||
final GarminFitFileInstallHandler fitFileInstallHandler = new GarminFitFileInstallHandler(uri, getContext());
|
||||
if (fitFileInstallHandler.isValid()) {
|
||||
communicator.sendMessage(
|
||||
"upload fit file",
|
||||
fileTransferHandler.initiateUpload(
|
||||
fitFileInstallHandler.getRawBytes(),
|
||||
fitFileInstallHandler.getFileType()
|
||||
).getOutgoingMessage()
|
||||
);
|
||||
}
|
||||
|
||||
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());
|
||||
|
@ -179,6 +179,15 @@ public class GlobalFITMessage {
|
||||
new FieldDefinitionPrimitive(253, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP)
|
||||
));
|
||||
|
||||
public static GlobalFITMessage WORKOUT = new GlobalFITMessage(26, "WORKOUT", Arrays.asList(
|
||||
new FieldDefinitionPrimitive(4, BaseType.ENUM, "sport"),
|
||||
new FieldDefinitionPrimitive(5, BaseType.UINT32Z, "capabilities"),
|
||||
new FieldDefinitionPrimitive(6, BaseType.UINT16, "num_valid_steps"),
|
||||
new FieldDefinitionPrimitive(8, BaseType.STRING, "name"),
|
||||
new FieldDefinitionPrimitive(11, BaseType.ENUM, "sub_sport"),
|
||||
new FieldDefinitionPrimitive(17, BaseType.STRING, "notes")
|
||||
));
|
||||
|
||||
public static GlobalFITMessage COURSE = new GlobalFITMessage(31, "COURSE", Arrays.asList(
|
||||
new FieldDefinitionPrimitive(4, BaseType.ENUM, "sport"),
|
||||
new FieldDefinitionPrimitive(5, BaseType.STRING, 16, "name")
|
||||
@ -397,6 +406,7 @@ public class GlobalFITMessage {
|
||||
put(20, RECORD);
|
||||
put(21, EVENT);
|
||||
put(23, DEVICE_INFO);
|
||||
put(26, WORKOUT);
|
||||
put(31, COURSE);
|
||||
put(49, FILE_CREATOR);
|
||||
put(55, MONITORING);
|
||||
|
@ -37,6 +37,8 @@ public class FitRecordDataFactory {
|
||||
return new FitEvent(recordDefinition, recordHeader);
|
||||
case 23:
|
||||
return new FitDeviceInfo(recordDefinition, recordHeader);
|
||||
case 26:
|
||||
return new FitWorkout(recordDefinition, recordHeader);
|
||||
case 31:
|
||||
return new FitCourse(recordDefinition, recordHeader);
|
||||
case 49:
|
||||
|
@ -0,0 +1,52 @@
|
||||
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 FitWorkout extends RecordData {
|
||||
public FitWorkout(final RecordDefinition recordDefinition, final RecordHeader recordHeader) {
|
||||
super(recordDefinition, recordHeader);
|
||||
|
||||
final int globalNumber = recordDefinition.getGlobalFITMessage().getNumber();
|
||||
if (globalNumber != 26) {
|
||||
throw new IllegalArgumentException("FitWorkout expects global messages of " + 26 + ", got " + globalNumber);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Integer getSport() {
|
||||
return (Integer) getFieldByNumber(4);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Long getCapabilities() {
|
||||
return (Long) getFieldByNumber(5);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Integer getNumValidSteps() {
|
||||
return (Integer) getFieldByNumber(6);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getName() {
|
||||
return (String) getFieldByNumber(8);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Integer getSubSport() {
|
||||
return (Integer) getFieldByNumber(11);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getNotes() {
|
||||
return (String) getFieldByNumber(17);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user