diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021Coordinator.java index 3eeb54a96..1ade75964 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/Huami2021Coordinator.java @@ -36,12 +36,15 @@ import de.greenrobot.dao.query.QueryBuilder; import nodomain.freeyourgadget.gadgetbridge.GBException; import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppos.ZeppOsAgpsInstallHandler; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppos.ZeppOsGpxRouteInstallHandler; import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.entities.HuamiExtendedActivitySampleDao; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuami2021FWInstallHandler; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsShortcutCardsService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsConfigService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiLanguageType; @@ -50,8 +53,27 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.service import nodomain.freeyourgadget.gadgetbridge.util.Prefs; public abstract class Huami2021Coordinator extends HuamiCoordinator { + public abstract AbstractHuami2021FWInstallHandler createFwInstallHandler(final Uri uri, final Context context); + @Override - public abstract InstallHandler findInstallHandler(final Uri uri, final Context context); + public InstallHandler findInstallHandler(final Uri uri, final Context context) { + if (supportsAgpsUpdates()) { + final ZeppOsAgpsInstallHandler agpsInstallHandler = new ZeppOsAgpsInstallHandler(uri, context); + if (agpsInstallHandler.isValid()) { + return agpsInstallHandler; + } + } + + if (supportsGpxUploads()) { + final ZeppOsGpxRouteInstallHandler gpxRouteInstallHandler = new ZeppOsGpxRouteInstallHandler(uri, context); + if (gpxRouteInstallHandler.isValid()) { + return gpxRouteInstallHandler; + } + } + + final AbstractHuami2021FWInstallHandler handler = createFwInstallHandler(uri, context); + return handler.isValid() ? handler : null; + } @Override public boolean supportsHeartRateMeasurement(final GBDevice device) { @@ -339,6 +361,14 @@ public abstract class Huami2021Coordinator extends HuamiCoordinator { return false; } + public boolean supportsAgpsUpdates() { + return false; + } + + public boolean supportsGpxUploads() { + return false; + } + public boolean supportsControlCenter() { // TODO: Auto-detect control center? return false; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitband7/AmazfitBand7Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitband7/AmazfitBand7Coordinator.java index a883072ca..000a906a2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitband7/AmazfitBand7Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitband7/AmazfitBand7Coordinator.java @@ -57,9 +57,8 @@ public class AmazfitBand7Coordinator extends Huami2021Coordinator { } @Override - public AbstractHuami2021FWInstallHandler findInstallHandler(final Uri uri, final Context context) { - final AmazfitBand7FWInstallHandler handler = new AmazfitBand7FWInstallHandler(uri, context); - return handler.isValid() ? handler : null; + public AbstractHuami2021FWInstallHandler createFwInstallHandler(final Uri uri, final Context context) { + return new AmazfitBand7FWInstallHandler(uri, context); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgtr3/AmazfitGTR3Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgtr3/AmazfitGTR3Coordinator.java index becc670e6..7fdf02e35 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgtr3/AmazfitGTR3Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgtr3/AmazfitGTR3Coordinator.java @@ -57,9 +57,8 @@ public class AmazfitGTR3Coordinator extends Huami2021Coordinator { } @Override - public AbstractHuami2021FWInstallHandler findInstallHandler(final Uri uri, final Context context) { - final AmazfitGTR3FWInstallHandler handler = new AmazfitGTR3FWInstallHandler(uri, context); - return handler.isValid() ? handler : null; + public AbstractHuami2021FWInstallHandler createFwInstallHandler(final Uri uri, final Context context) { + return new AmazfitGTR3FWInstallHandler(uri, context); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgtr3pro/AmazfitGTR3ProCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgtr3pro/AmazfitGTR3ProCoordinator.java index aab023598..a377b82ef 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgtr3pro/AmazfitGTR3ProCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgtr3pro/AmazfitGTR3ProCoordinator.java @@ -57,9 +57,8 @@ public class AmazfitGTR3ProCoordinator extends Huami2021Coordinator { } @Override - public AbstractHuami2021FWInstallHandler findInstallHandler(final Uri uri, final Context context) { - final AmazfitGTR3ProFWInstallHandler handler = new AmazfitGTR3ProFWInstallHandler(uri, context); - return handler.isValid() ? handler : null; + public AbstractHuami2021FWInstallHandler createFwInstallHandler(final Uri uri, final Context context) { + return new AmazfitGTR3ProFWInstallHandler(uri, context); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgtr4/AmazfitGTR4Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgtr4/AmazfitGTR4Coordinator.java index 7f2bcbe11..bcfb14020 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgtr4/AmazfitGTR4Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgtr4/AmazfitGTR4Coordinator.java @@ -25,13 +25,12 @@ import androidx.annotation.NonNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; import nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst; -import nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppos.ZeppOsAgpsInstallHandler; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuami2021FWInstallHandler; public class AmazfitGTR4Coordinator extends Huami2021Coordinator { private static final Logger LOG = LoggerFactory.getLogger(AmazfitGTR4Coordinator.class); @@ -58,13 +57,8 @@ public class AmazfitGTR4Coordinator extends Huami2021Coordinator { } @Override - public InstallHandler findInstallHandler(final Uri uri, final Context context) { - final ZeppOsAgpsInstallHandler agpsInstallHandler = new ZeppOsAgpsInstallHandler(uri, context); - if (agpsInstallHandler.isValid()) { - return agpsInstallHandler; - } - final AmazfitGTR4FWInstallHandler handler = new AmazfitGTR4FWInstallHandler(uri, context); - return handler.isValid() ? handler : null; + public AbstractHuami2021FWInstallHandler createFwInstallHandler(final Uri uri, final Context context) { + return new AmazfitGTR4FWInstallHandler(uri, context); } @Override @@ -72,6 +66,16 @@ public class AmazfitGTR4Coordinator extends Huami2021Coordinator { return true; } + @Override + public boolean supportsAgpsUpdates() { + return true; + } + + @Override + public boolean supportsGpxUploads() { + return true; + } + @Override public boolean supportsControlCenter() { return true; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgts3/AmazfitGTS3Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgts3/AmazfitGTS3Coordinator.java index f3fadf3e1..d2266ef2e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgts3/AmazfitGTS3Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgts3/AmazfitGTS3Coordinator.java @@ -57,9 +57,8 @@ public class AmazfitGTS3Coordinator extends Huami2021Coordinator { } @Override - public AbstractHuami2021FWInstallHandler findInstallHandler(final Uri uri, final Context context) { - final AmazfitGTS3FWInstallHandler handler = new AmazfitGTS3FWInstallHandler(uri, context); - return handler.isValid() ? handler : null; + public AbstractHuami2021FWInstallHandler createFwInstallHandler(final Uri uri, final Context context) { + return new AmazfitGTS3FWInstallHandler(uri, context); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgts4/AmazfitGTS4Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgts4/AmazfitGTS4Coordinator.java index 333a508a9..8f96ffa7c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgts4/AmazfitGTS4Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgts4/AmazfitGTS4Coordinator.java @@ -25,13 +25,12 @@ import androidx.annotation.NonNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; import nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst; -import nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppos.ZeppOsAgpsInstallHandler; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuami2021FWInstallHandler; public class AmazfitGTS4Coordinator extends Huami2021Coordinator { private static final Logger LOG = LoggerFactory.getLogger(AmazfitGTS4Coordinator.class); @@ -58,13 +57,8 @@ public class AmazfitGTS4Coordinator extends Huami2021Coordinator { } @Override - public InstallHandler findInstallHandler(final Uri uri, final Context context) { - final ZeppOsAgpsInstallHandler agpsInstallHandler = new ZeppOsAgpsInstallHandler(uri, context); - if (agpsInstallHandler.isValid()) { - return agpsInstallHandler; - } - final AmazfitGTS4FWInstallHandler handler = new AmazfitGTS4FWInstallHandler(uri, context); - return handler.isValid() ? handler : null; + public AbstractHuami2021FWInstallHandler createFwInstallHandler(final Uri uri, final Context context) { + return new AmazfitGTS4FWInstallHandler(uri, context); } @Override @@ -72,6 +66,16 @@ public class AmazfitGTS4Coordinator extends Huami2021Coordinator { return true; } + @Override + public boolean supportsAgpsUpdates() { + return true; + } + + @Override + public boolean supportsGpxUploads() { + return true; + } + @Override public boolean supportsControlCenter() { return true; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgts4mini/AmazfitGTS4MiniCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgts4mini/AmazfitGTS4MiniCoordinator.java index bf11cd31e..4abf0f513 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgts4mini/AmazfitGTS4MiniCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitgts4mini/AmazfitGTS4MiniCoordinator.java @@ -57,9 +57,8 @@ public class AmazfitGTS4MiniCoordinator extends Huami2021Coordinator { } @Override - public AbstractHuami2021FWInstallHandler findInstallHandler(final Uri uri, final Context context) { - final AmazfitGTS4MiniFWInstallHandler handler = new AmazfitGTS4MiniFWInstallHandler(uri, context); - return handler.isValid() ? handler : null; + public AbstractHuami2021FWInstallHandler createFwInstallHandler(final Uri uri, final Context context) { + return new AmazfitGTS4MiniFWInstallHandler(uri, context); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfittrex2/AmazfitTRex2Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfittrex2/AmazfitTRex2Coordinator.java index a9316117a..920ce80d3 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfittrex2/AmazfitTRex2Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfittrex2/AmazfitTRex2Coordinator.java @@ -57,9 +57,8 @@ public class AmazfitTRex2Coordinator extends Huami2021Coordinator { } @Override - public AbstractHuami2021FWInstallHandler findInstallHandler(final Uri uri, final Context context) { - final AmazfitTRex2FWInstallHandler handler = new AmazfitTRex2FWInstallHandler(uri, context); - return handler.isValid() ? handler : null; + public AbstractHuami2021FWInstallHandler createFwInstallHandler(final Uri uri, final Context context) { + return new AmazfitTRex2FWInstallHandler(uri, context); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband7/MiBand7Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband7/MiBand7Coordinator.java index d7ecb8890..97850b316 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband7/MiBand7Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/miband7/MiBand7Coordinator.java @@ -57,9 +57,8 @@ public class MiBand7Coordinator extends Huami2021Coordinator { } @Override - public AbstractHuami2021FWInstallHandler findInstallHandler(final Uri uri, final Context context) { - final MiBand7FWInstallHandler handler = new MiBand7FWInstallHandler(uri, context); - return handler.isValid() ? handler : null; + public AbstractHuami2021FWInstallHandler createFwInstallHandler(final Uri uri, final Context context) { + return new MiBand7FWInstallHandler(uri, context); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/zeppos/ZeppOsGpxRouteInstallHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/zeppos/ZeppOsGpxRouteInstallHandler.java new file mode 100644 index 000000000..bf22362f8 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/zeppos/ZeppOsGpxRouteInstallHandler.java @@ -0,0 +1,119 @@ +/* Copyright (C) 2023 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.huami.zeppos; + +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.InstallHandler; +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 { + private static final Logger LOG = LoggerFactory.getLogger(ZeppOsGpxRouteInstallHandler.class); + + 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; + } + try (InputStream in = new BufferedInputStream(uriHelper.openInputStream())) { + final byte[] rawBytes = FileUtils.readAll(in, 1024 * 1024); // 1MB + final ZeppOsGpxRouteFile gpxFile = new ZeppOsGpxRouteFile(rawBytes); + if (gpxFile.isValid()) { + this.file = gpxFile; + } + } catch (final Exception e) { + LOG.error("Failed to read file", e); + } + } + + @Override + public boolean isValid() { + return file != null; + } + + @Override + public void validateInstallation(final InstallActivity installActivity, final GBDevice device) { + if (device.isBusy()) { + installActivity.setInfoText(device.getBusyTask()); + 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.getType().getIcon()); + + 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) { + final String firmwareName = mContext.getString( + R.string.installhandler_firmware_name, + mContext.getString(device.getType().getName()), + mContext.getString(R.string.kind_gpx_route), + file.getName() + ); + return new GenericItem(firmwareName); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021Support.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021Support.java index c434fb70d..e3349adb6 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021Support.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021Support.java @@ -81,6 +81,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService; import nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppos.ZeppOsAgpsInstallHandler; +import nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppos.ZeppOsGpxRouteInstallHandler; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.miband.VibrationProfile; @@ -106,6 +107,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.Upd import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation2021; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.AbstractZeppOsService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.operations.ZeppOsAgpsUpdateOperation; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.operations.ZeppOsGpxRouteUploadOperation; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsAgpsService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsAlarmsService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsCalendarService; @@ -656,11 +658,28 @@ public abstract class Huami2021Support extends HuamiSupport { configService ).perform(); } catch (final Exception e) { - GB.toast(getContext(), "AGPS File cannot be installed: " + e.getMessage(), Toast.LENGTH_LONG, GB.ERROR, e); + GB.toast(getContext(), "AGPS file cannot be installed: " + e.getMessage(), Toast.LENGTH_LONG, GB.ERROR, e); } - } else { - super.onInstallApp(uri); + + return; } + + final ZeppOsGpxRouteInstallHandler gpxRouteHandler = new ZeppOsGpxRouteInstallHandler(uri, getContext()); + if (gpxRouteHandler.isValid()) { + try { + new ZeppOsGpxRouteUploadOperation( + this, + gpxRouteHandler.getFile(), + fileUploadService + ).perform(); + } catch (final Exception e) { + GB.toast(getContext(), "Gpx route file cannot be installed: " + e.getMessage(), Toast.LENGTH_LONG, GB.ERROR, e); + } + + return; + } + + super.onInstallApp(uri); } @Override 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 new file mode 100644 index 000000000..5eeafe081 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/operations/ZeppOsGpxRouteFile.java @@ -0,0 +1,208 @@ +/* Copyright (C) 2023 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.service.devices.huami.zeppos.operations; + +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; +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; +import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxWaypoint; + +public class ZeppOsGpxRouteFile { + private static final Logger LOG = LoggerFactory.getLogger(ZeppOsGpxRouteFile.class); + + private static final double COORD_MULTIPLIER = 3000000.0; + + public static final byte[] XML_HEADER = new byte[]{ + '<', '?', 'x', 'm', 'l' + }; + + // Some gpx files start with " trackPoints = gpxFile.getPoints(); + final List waypoints = gpxFile.getWaypoints(); + + double minLatitude = 180; + double maxLatitude = -180; + double minLongitude = 180; + double maxLongitude = -180; + double minAltitude = 10000; + double maxAltitude = -10000; + + for (final GPSCoordinate coord : trackPoints) { + minLatitude = Math.min(minLatitude, coord.getLatitude()); + maxLatitude = Math.max(maxLatitude, coord.getLatitude()); + minLongitude = Math.min(minLongitude, coord.getLongitude()); + maxLongitude = Math.max(maxLongitude, coord.getLongitude()); + minAltitude = Math.min(minAltitude, coord.getAltitude()); + maxAltitude = Math.max(maxAltitude, coord.getAltitude()); + } + + try { + baos.write(BLETypeConversions.fromUint32(0)); // ? + baos.write(BLETypeConversions.fromUint32(0x54)); // ? + baos.write(BLETypeConversions.fromUint32(0x01)); // ? + baos.write(BLETypeConversions.fromUint32((int) timestamp)); + baos.write(BLETypeConversions.fromUint32((int) (minLatitude * COORD_MULTIPLIER))); + baos.write(BLETypeConversions.fromUint32((int) (maxLatitude * COORD_MULTIPLIER))); + baos.write(BLETypeConversions.fromUint32((int) (minLongitude * COORD_MULTIPLIER))); + baos.write(BLETypeConversions.fromUint32((int) (maxLongitude * COORD_MULTIPLIER))); + baos.write(BLETypeConversions.fromUint32((int) minAltitude)); + baos.write(BLETypeConversions.fromUint32((int) maxAltitude)); + baos.write(truncatePadString(getName())); + baos.write(BLETypeConversions.fromUint32(0)); // ? + + if (!waypoints.isEmpty()) { + baos.write(BLETypeConversions.fromUint32(2)); + baos.write(BLETypeConversions.fromUint32(waypoints.size() * 68)); + for (final GpxWaypoint waypoint : waypoints) { + baos.write(BLETypeConversions.fromUint32(0x1a)); // ? + baos.write(BLETypeConversions.fromUint32((int) (waypoint.getLatitude() * COORD_MULTIPLIER))); + baos.write(BLETypeConversions.fromUint32((int) (waypoint.getLongitude() * COORD_MULTIPLIER))); + baos.write(BLETypeConversions.fromUint32((int) waypoint.getAltitude())); + baos.write(truncatePadString(waypoint.getName())); + baos.write(BLETypeConversions.fromUint32(0)); // ? + } + } + + baos.write(BLETypeConversions.fromUint32(1)); // ? + baos.write(BLETypeConversions.fromUint32(trackPoints.size() * 14)); + + // Keep track of the total distance + double totalDist = 0; + GPSCoordinate prevPoint = trackPoints.isEmpty() ? null : trackPoints.get(0); + + for (final GPSCoordinate point : trackPoints) { + totalDist += distanceBetween(prevPoint, point); + + baos.write(BLETypeConversions.fromUint32((int) totalDist)); + baos.write(BLETypeConversions.fromUint32((int) (point.getLatitude() * COORD_MULTIPLIER))); + baos.write(BLETypeConversions.fromUint32((int) (point.getLongitude() * COORD_MULTIPLIER))); + baos.write(BLETypeConversions.fromUint16((int) point.getAltitude())); + + prevPoint = point; + } + } catch (final IOException e) { + LOG.error("Failed to encode gpx file", e); + return null; + } + + return baos.toByteArray(); + } + + public static double distanceBetween(final GPSCoordinate a, final GPSCoordinate b) { + final Location start = new Location("start"); + start.setLatitude(a.getLatitude()); + start.setLongitude(a.getLongitude()); + + final Location end = new Location("end"); + end.setLatitude(b.getLatitude()); + end.setLongitude(b.getLongitude()); + + return end.distanceTo(start); + } + + /** + * Truncates / pads a string to 48 bytes (including null terminator). + */ + public static byte[] truncatePadString(final String s) { + final ByteBuffer buf = ByteBuffer.allocate(48); + buf.put(StringUtils.truncateToBytes(s, 47)); + return buf.array(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/operations/ZeppOsGpxRouteUploadOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/operations/ZeppOsGpxRouteUploadOperation.java new file mode 100644 index 000000000..a4dc7aa1b --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/operations/ZeppOsGpxRouteUploadOperation.java @@ -0,0 +1,100 @@ +/* Copyright (C) 2023 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.service.devices.huami.zeppos.operations; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEOperation; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetProgressAction; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021Support; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsFileUploadService; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.OperationStatus; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class ZeppOsGpxRouteUploadOperation extends AbstractBTLEOperation + implements ZeppOsFileUploadService.Callback { + private static final Logger LOG = LoggerFactory.getLogger(ZeppOsGpxRouteUploadOperation.class); + + private final ZeppOsGpxRouteFile file; + private final byte[] fileBytes; + + private final ZeppOsFileUploadService fileUploadService; + + public ZeppOsGpxRouteUploadOperation(final Huami2021Support support, + final ZeppOsGpxRouteFile file, + final ZeppOsFileUploadService fileUploadService) { + super(support); + this.file = file; + this.fileBytes = file.getEncodedBytes(); + this.fileUploadService = fileUploadService; + } + + @Override + protected void doPerform() throws IOException { + fileUploadService.sendFile( + "sport://file_transfer?appId=7073283073¶ms={}", + "track_" + file.getTimestamp() + ".dat", + fileBytes, + this + ); + } + + @Override + protected void operationFinished() { + operationStatus = OperationStatus.FINISHED; + if (getDevice() != null && getDevice().isConnected()) { + unsetBusy(); + getDevice().sendDeviceUpdateIntent(getContext()); + } + } + + @Override + public void onFileUploadFinish(final boolean success) { + LOG.info("Finished gpx route upload operation, success={}", success); + + final String notificationMessage = success ? + getContext().getString(R.string.gpx_route_upload_complete) : + getContext().getString(R.string.gpx_route_upload_failed); + + GB.updateInstallNotification(notificationMessage, false, 100, getContext()); + + operationFinished(); + } + + @Override + public void onFileUploadProgress(final int progress) { + LOG.trace("Gpx route upload operation progress: {}", progress); + + final int progressPercent = (int) ((((float) (progress)) / fileBytes.length) * 100); + updateProgress(progressPercent); + } + + private void updateProgress(final int progressPercent) { + try { + final TransactionBuilder builder = performInitialized("send gpx route upload progress"); + builder.add(new SetProgressAction(getContext().getString(R.string.gpx_route_upload_in_progress), true, progressPercent, getContext())); + builder.queue(getQueue()); + } catch (final Exception e) { + LOG.error("Failed to update progress notification", e); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/StringUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/StringUtils.java index 854f93013..76918c30b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/StringUtils.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/StringUtils.java @@ -46,6 +46,27 @@ public class StringUtils { return s.substring(0, length); } + /** + * Truncate a string to a certain maximum number of bytes, assuming UTF-8 encoding. + * Does not include the null terminator. Due to multi-byte characters, it's possible + * that the resulting array is smaller than len, but never larger. + */ + public static byte[] truncateToBytes(final String s, final int len) { + if (StringUtils.isNullOrEmpty(s)) { + return new byte[]{}; + } + + int i = 0; + while (++i < s.length()) { + final String subString = s.substring(0, i + 1); + if (subString.getBytes(StandardCharsets.UTF_8).length > len) { + break; + } + } + + return s.substring(0, i).getBytes(StandardCharsets.UTF_8); + } + public static int utf8ByteLength(String string, int length) { if (string == null) { return 0; diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1a99dac88..2fff0d7db 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -791,6 +791,9 @@ Firmware installation complete Firmware installation complete, rebooting device… Firmware flashing failed + Gpx route upload failed + Gpx route upload complete + Uploading gpx route Steps Calories Distance @@ -1195,6 +1198,7 @@ GPS Almanac GPS Error Correction AGPS Bundle + GPX Route Resources Watchface App