/* 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 . */
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 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 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 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;
}
}