Introduce GpxRouteInstallHandler

This commit is contained in:
José Rebelo
2025-08-02 21:34:25 +01:00
parent b750f60349
commit 721acb5952
8 changed files with 182 additions and 242 deletions
@@ -0,0 +1,151 @@
package nodomain.freeyourgadget.gadgetbridge.devices;
import android.app.Activity;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.apache.commons.lang3.StringUtils;
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.install.FwAppInstallerActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.install.InstallActivity;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.UriHelper;
import nodomain.freeyourgadget.gadgetbridge.util.gpx.GpxParser;
import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxFile;
import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxTrack;
public abstract class GpxRouteInstallHandler implements InstallHandler {
private static final Logger LOG = LoggerFactory.getLogger(GpxRouteInstallHandler.class);
private static final int MAX_EXPECTED_SIZE = 1024 * 1024; // 1MB, they're usually ~128KB
protected final Context mContext;
private GpxFile gpxFile;
public GpxRouteInstallHandler(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;
}
if (uriHelper.getFileSize() > MAX_EXPECTED_SIZE) {
LOG.debug("Not gpx - file too large");
return;
}
try (InputStream in = new BufferedInputStream(uriHelper.openInputStream())) {
final byte[] rawBytes = FileUtils.readAll(in, MAX_EXPECTED_SIZE);
this.gpxFile = GpxParser.parseGpx(rawBytes);
} catch (final Exception e) {
LOG.error("Failed to read gpx file", e);
}
}
protected abstract boolean isCompatible(final GBDevice device);
@NonNull
@Override
public Class<? extends Activity> getInstallActivity() {
return FwAppInstallerActivity.class;
}
@Override
public boolean isValid() {
return gpxFile != null;
}
@Override
public void validateInstallation(final InstallActivity installActivity, final GBDevice device) {
if (device.isBusy()) {
installActivity.setInfoText(device.getBusyTask());
installActivity.setInstallEnabled(false);
return;
}
if (!isCompatible(device)) {
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(device.getDeviceCoordinator().getDefaultIconResource());
if (gpxFile == 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) {
}
@Nullable
public GpxFile getGpxFile() {
return gpxFile;
}
public String getName() {
if (gpxFile == null) {
return "";
}
// Prioritize metadata name
if (StringUtils.isNotBlank(gpxFile.getName())) {
return gpxFile.getName();
}
// Fallback to the first track that has a name
for (final GpxTrack track : gpxFile.getTracks()) {
if (StringUtils.isNotBlank(track.getName())) {
return track.getName();
}
}
return "";
}
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),
getName()
);
return new GenericItem(firmwareName);
}
}
@@ -1,4 +1,4 @@
/* Copyright (C) 2023-2024 Daniel Dakhno, José Rebelo
/* Copyright (C) 2023-2025 Daniel Dakhno, José Rebelo
This file is part of Gadgetbridge.
@@ -18,131 +18,30 @@ package nodomain.freeyourgadget.gadgetbridge.devices.garmin;
import static nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.GarminCapability.COURSE_DOWNLOAD;
import android.app.Activity;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
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.install.FwAppInstallerActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.install.InstallActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.GpxRouteInstallHandler;
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;
public class GarminGpxRouteInstallHandler implements InstallHandler {
public class GarminGpxRouteInstallHandler extends GpxRouteInstallHandler {
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);
}
}
@NonNull
@Override
public Class<? extends Activity> getInstallActivity() {
return FwAppInstallerActivity.class;
super(uri, context);
}
@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;
}
protected boolean isCompatible(GBDevice device) {
final DeviceCoordinator coordinator = device.getDeviceCoordinator();
if (!(coordinator instanceof GarminCoordinator 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;
return false;
}
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);
return garminCoordinator.supports(device, COURSE_DOWNLOAD);
}
}
@@ -1,4 +1,4 @@
/* Copyright (C) 2023-2024 Daniel Dakhno, José Rebelo
/* Copyright (C) 2023-2025 Daniel Dakhno, José Rebelo
This file is part of Gadgetbridge.
@@ -16,137 +16,30 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppos;
import android.app.Activity;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
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.install.FwAppInstallerActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.install.InstallActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.GpxRouteInstallHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.operations.ZeppOsGpxRouteFile;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.UriHelper;
public class ZeppOsGpxRouteInstallHandler implements InstallHandler {
public class ZeppOsGpxRouteInstallHandler extends GpxRouteInstallHandler {
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsGpxRouteInstallHandler.class);
private static final int MAX_EXPECTED_SIZE = 1024 * 1024; // 1MB, they're usually ~128KB
protected final Context mContext;
private ZeppOsGpxRouteFile file;
public ZeppOsGpxRouteInstallHandler(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;
}
if (uriHelper.getFileSize() > MAX_EXPECTED_SIZE) {
LOG.debug("Not gpx - file too large");
return;
}
try (InputStream in = new BufferedInputStream(uriHelper.openInputStream())) {
final byte[] rawBytes = FileUtils.readAll(in, MAX_EXPECTED_SIZE);
final ZeppOsGpxRouteFile gpxFile = new ZeppOsGpxRouteFile(rawBytes);
if (gpxFile.isValid()) {
this.file = gpxFile;
}
} catch (final Exception e) {
LOG.error("Failed to read file", e);
}
}
@NonNull
@Override
public Class<? extends Activity> getInstallActivity() {
return FwAppInstallerActivity.class;
super(uri, context);
}
@Override
public boolean isValid() {
return file != null && file.isValid();
}
@Override
public void validateInstallation(final InstallActivity installActivity, final GBDevice device) {
if (device.isBusy()) {
installActivity.setInfoText(device.getBusyTask());
installActivity.setInstallEnabled(false);
return;
}
protected boolean isCompatible(GBDevice device) {
final DeviceCoordinator coordinator = device.getDeviceCoordinator();
if (!(coordinator instanceof ZeppOsCoordinator zeppOsCoordinator)) {
LOG.warn("Coordinator is not a ZeppOsCoordinator: {}", coordinator.getClass());
installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_supported));
installActivity.setInstallEnabled(false);
return;
return false;
}
if (!zeppOsCoordinator.supportsGpxUploads(device)) {
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 (file == 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 ZeppOsGpxRouteFile getFile() {
return file;
}
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),
file.getName()
);
return new GenericItem(firmwareName);
return zeppOsCoordinator.supportsGpxUploads(device);
}
}
@@ -76,6 +76,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.SupportedFileTypesDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.WeatherRequestDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FitAsyncProcessor;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.GpxRouteFileConverter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.PredefinedLocalMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
@@ -996,7 +997,8 @@ public class GarminSupport extends AbstractBTLESingleDeviceSupport implements IC
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());
final GpxRouteFileConverter gpxRouteFileConverter = new GpxRouteFileConverter(garminGpxRouteInstallHandler.getGpxFile());
communicator.sendMessage("upload course file", fileTransferHandler.initiateUpload(gpxRouteFileConverter.getConvertedFile().getOutgoingMessage(), FileType.FILETYPE.DOWNLOAD_COURSE).getOutgoingMessage());
}
final GarminPrgFileInstallHandler prgFileInstallHandler = new GarminPrgFileInstallHandler(uri, getContext());
@@ -12,8 +12,6 @@ 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.GpxTrackPoint;
@@ -26,16 +24,14 @@ public class GpxRouteFileConverter {
private FitFile convertedFile;
private String name;
public GpxRouteFileConverter(byte[] xmlBytes) {
public GpxRouteFileConverter(final GpxFile gpxFile) {
this.timestamp = System.currentTimeMillis() / 1000;
this.gpxFile = GpxParser.parseGpx(xmlBytes);
if (this.gpxFile != null) {
try {
this.convertedFile = convertGpxToRoute(gpxFile);
} catch (final Exception e) {
LOG.error("Failed to convert gpx to route", e);
this.convertedFile = null;
}
this.gpxFile = gpxFile;
try {
this.convertedFile = convertGpxToRoute(gpxFile);
} catch (final Exception e) {
LOG.error("Failed to convert gpx to route", e);
this.convertedFile = null;
}
}
@@ -97,6 +97,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiDevicePre
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiFetcher;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.operations.ZeppOsAgpsUpdateOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.operations.ZeppOsFirmwareUpdateOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.operations.ZeppOsGpxRouteFile;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.operations.ZeppOsGpxRouteUploadOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.operations.ZeppOsMusicUploadOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsActivityFetchService;
@@ -526,7 +527,7 @@ public class ZeppOsSupport extends AbstractDeviceSupport
try {
new ZeppOsGpxRouteUploadOperation(
this,
gpxRouteHandler.getFile(),
gpxRouteHandler.getGpxFile(),
fileTransferService
).perform();
} catch (final Exception e) {
@@ -29,7 +29,6 @@ import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
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.GpxTrackPoint;
import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxWaypoint;
@@ -39,14 +38,12 @@ public class ZeppOsGpxRouteFile {
private static final double COORD_MULTIPLIER = 3000000.0;
private final byte[] xmlBytes;
private final long timestamp;
private final GpxFile gpxFile;
public ZeppOsGpxRouteFile(final byte[] xmlBytes) {
this.xmlBytes = xmlBytes;
public ZeppOsGpxRouteFile(final GpxFile gpxFile) {
this.timestamp = System.currentTimeMillis() / 1000;
this.gpxFile = GpxParser.parseGpx(xmlBytes);
this.gpxFile = gpxFile;
}
public boolean isValid() {
@@ -27,6 +27,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsT
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsFileTransferService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.OperationStatus;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxFile;
public class ZeppOsGpxRouteUploadOperation extends AbstractZeppOsOperation<ZeppOsSupport>
implements ZeppOsFileTransferService.UploadCallback {
@@ -38,10 +39,10 @@ public class ZeppOsGpxRouteUploadOperation extends AbstractZeppOsOperation<ZeppO
private final ZeppOsFileTransferService fileTransferService;
public ZeppOsGpxRouteUploadOperation(final ZeppOsSupport support,
final ZeppOsGpxRouteFile file,
final GpxFile gpxFile,
final ZeppOsFileTransferService fileTransferService) {
super(support);
this.file = file;
this.file = new ZeppOsGpxRouteFile(gpxFile);
this.fileBytes = file.getEncodedBytes();
this.fileTransferService = fileTransferService;
}