mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2025-02-09 00:16:48 +01:00
Zepp OS: Add gpx route file upload
This commit is contained in:
parent
01ec74602a
commit
9851493cf1
@ -36,12 +36,15 @@ import de.greenrobot.dao.query.QueryBuilder;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
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.AbstractActivitySample;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuamiExtendedActivitySampleDao;
|
import nodomain.freeyourgadget.gadgetbridge.entities.HuamiExtendedActivitySampleDao;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
|
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.ZeppOsShortcutCardsService;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsConfigService;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsConfigService;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiLanguageType;
|
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;
|
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||||
|
|
||||||
public abstract class Huami2021Coordinator extends HuamiCoordinator {
|
public abstract class Huami2021Coordinator extends HuamiCoordinator {
|
||||||
|
public abstract AbstractHuami2021FWInstallHandler createFwInstallHandler(final Uri uri, final Context context);
|
||||||
|
|
||||||
@Override
|
@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
|
@Override
|
||||||
public boolean supportsHeartRateMeasurement(final GBDevice device) {
|
public boolean supportsHeartRateMeasurement(final GBDevice device) {
|
||||||
@ -339,6 +361,14 @@ public abstract class Huami2021Coordinator extends HuamiCoordinator {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean supportsAgpsUpdates() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean supportsGpxUploads() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean supportsControlCenter() {
|
public boolean supportsControlCenter() {
|
||||||
// TODO: Auto-detect control center?
|
// TODO: Auto-detect control center?
|
||||||
return false;
|
return false;
|
||||||
|
@ -57,9 +57,8 @@ public class AmazfitBand7Coordinator extends Huami2021Coordinator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AbstractHuami2021FWInstallHandler findInstallHandler(final Uri uri, final Context context) {
|
public AbstractHuami2021FWInstallHandler createFwInstallHandler(final Uri uri, final Context context) {
|
||||||
final AmazfitBand7FWInstallHandler handler = new AmazfitBand7FWInstallHandler(uri, context);
|
return new AmazfitBand7FWInstallHandler(uri, context);
|
||||||
return handler.isValid() ? handler : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -57,9 +57,8 @@ public class AmazfitGTR3Coordinator extends Huami2021Coordinator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AbstractHuami2021FWInstallHandler findInstallHandler(final Uri uri, final Context context) {
|
public AbstractHuami2021FWInstallHandler createFwInstallHandler(final Uri uri, final Context context) {
|
||||||
final AmazfitGTR3FWInstallHandler handler = new AmazfitGTR3FWInstallHandler(uri, context);
|
return new AmazfitGTR3FWInstallHandler(uri, context);
|
||||||
return handler.isValid() ? handler : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -57,9 +57,8 @@ public class AmazfitGTR3ProCoordinator extends Huami2021Coordinator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AbstractHuami2021FWInstallHandler findInstallHandler(final Uri uri, final Context context) {
|
public AbstractHuami2021FWInstallHandler createFwInstallHandler(final Uri uri, final Context context) {
|
||||||
final AmazfitGTR3ProFWInstallHandler handler = new AmazfitGTR3ProFWInstallHandler(uri, context);
|
return new AmazfitGTR3ProFWInstallHandler(uri, context);
|
||||||
return handler.isValid() ? handler : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -25,13 +25,12 @@ import androidx.annotation.NonNull;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Coordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Coordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst;
|
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.GBDevice;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuami2021FWInstallHandler;
|
||||||
|
|
||||||
public class AmazfitGTR4Coordinator extends Huami2021Coordinator {
|
public class AmazfitGTR4Coordinator extends Huami2021Coordinator {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(AmazfitGTR4Coordinator.class);
|
private static final Logger LOG = LoggerFactory.getLogger(AmazfitGTR4Coordinator.class);
|
||||||
@ -58,13 +57,8 @@ public class AmazfitGTR4Coordinator extends Huami2021Coordinator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InstallHandler findInstallHandler(final Uri uri, final Context context) {
|
public AbstractHuami2021FWInstallHandler createFwInstallHandler(final Uri uri, final Context context) {
|
||||||
final ZeppOsAgpsInstallHandler agpsInstallHandler = new ZeppOsAgpsInstallHandler(uri, context);
|
return new AmazfitGTR4FWInstallHandler(uri, context);
|
||||||
if (agpsInstallHandler.isValid()) {
|
|
||||||
return agpsInstallHandler;
|
|
||||||
}
|
|
||||||
final AmazfitGTR4FWInstallHandler handler = new AmazfitGTR4FWInstallHandler(uri, context);
|
|
||||||
return handler.isValid() ? handler : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -72,6 +66,16 @@ public class AmazfitGTR4Coordinator extends Huami2021Coordinator {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsAgpsUpdates() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsGpxUploads() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean supportsControlCenter() {
|
public boolean supportsControlCenter() {
|
||||||
return true;
|
return true;
|
||||||
|
@ -57,9 +57,8 @@ public class AmazfitGTS3Coordinator extends Huami2021Coordinator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AbstractHuami2021FWInstallHandler findInstallHandler(final Uri uri, final Context context) {
|
public AbstractHuami2021FWInstallHandler createFwInstallHandler(final Uri uri, final Context context) {
|
||||||
final AmazfitGTS3FWInstallHandler handler = new AmazfitGTS3FWInstallHandler(uri, context);
|
return new AmazfitGTS3FWInstallHandler(uri, context);
|
||||||
return handler.isValid() ? handler : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -25,13 +25,12 @@ import androidx.annotation.NonNull;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Coordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Coordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst;
|
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.GBDevice;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuami2021FWInstallHandler;
|
||||||
|
|
||||||
public class AmazfitGTS4Coordinator extends Huami2021Coordinator {
|
public class AmazfitGTS4Coordinator extends Huami2021Coordinator {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(AmazfitGTS4Coordinator.class);
|
private static final Logger LOG = LoggerFactory.getLogger(AmazfitGTS4Coordinator.class);
|
||||||
@ -58,13 +57,8 @@ public class AmazfitGTS4Coordinator extends Huami2021Coordinator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public InstallHandler findInstallHandler(final Uri uri, final Context context) {
|
public AbstractHuami2021FWInstallHandler createFwInstallHandler(final Uri uri, final Context context) {
|
||||||
final ZeppOsAgpsInstallHandler agpsInstallHandler = new ZeppOsAgpsInstallHandler(uri, context);
|
return new AmazfitGTS4FWInstallHandler(uri, context);
|
||||||
if (agpsInstallHandler.isValid()) {
|
|
||||||
return agpsInstallHandler;
|
|
||||||
}
|
|
||||||
final AmazfitGTS4FWInstallHandler handler = new AmazfitGTS4FWInstallHandler(uri, context);
|
|
||||||
return handler.isValid() ? handler : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -72,6 +66,16 @@ public class AmazfitGTS4Coordinator extends Huami2021Coordinator {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsAgpsUpdates() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsGpxUploads() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean supportsControlCenter() {
|
public boolean supportsControlCenter() {
|
||||||
return true;
|
return true;
|
||||||
|
@ -57,9 +57,8 @@ public class AmazfitGTS4MiniCoordinator extends Huami2021Coordinator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AbstractHuami2021FWInstallHandler findInstallHandler(final Uri uri, final Context context) {
|
public AbstractHuami2021FWInstallHandler createFwInstallHandler(final Uri uri, final Context context) {
|
||||||
final AmazfitGTS4MiniFWInstallHandler handler = new AmazfitGTS4MiniFWInstallHandler(uri, context);
|
return new AmazfitGTS4MiniFWInstallHandler(uri, context);
|
||||||
return handler.isValid() ? handler : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -57,9 +57,8 @@ public class AmazfitTRex2Coordinator extends Huami2021Coordinator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AbstractHuami2021FWInstallHandler findInstallHandler(final Uri uri, final Context context) {
|
public AbstractHuami2021FWInstallHandler createFwInstallHandler(final Uri uri, final Context context) {
|
||||||
final AmazfitTRex2FWInstallHandler handler = new AmazfitTRex2FWInstallHandler(uri, context);
|
return new AmazfitTRex2FWInstallHandler(uri, context);
|
||||||
return handler.isValid() ? handler : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -57,9 +57,8 @@ public class MiBand7Coordinator extends Huami2021Coordinator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AbstractHuami2021FWInstallHandler findInstallHandler(final Uri uri, final Context context) {
|
public AbstractHuami2021FWInstallHandler createFwInstallHandler(final Uri uri, final Context context) {
|
||||||
final MiBand7FWInstallHandler handler = new MiBand7FWInstallHandler(uri, context);
|
return new MiBand7FWInstallHandler(uri, context);
|
||||||
return handler.isValid() ? handler : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -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 <http://www.gnu.org/licenses/>. */
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -81,6 +81,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService;
|
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiService;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.zeppos.ZeppOsAgpsInstallHandler;
|
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.MiBandConst;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.VibrationProfile;
|
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.operations.UpdateFirmwareOperation2021;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.AbstractZeppOsService;
|
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.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.ZeppOsAgpsService;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsAlarmsService;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsAlarmsService;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsCalendarService;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsCalendarService;
|
||||||
@ -656,11 +658,28 @@ public abstract class Huami2021Support extends HuamiSupport {
|
|||||||
configService
|
configService
|
||||||
).perform();
|
).perform();
|
||||||
} catch (final Exception e) {
|
} 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
|
@Override
|
||||||
|
@ -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 <http://www.gnu.org/licenses/>. */
|
||||||
|
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 "<gpx" directly.. this needs to be improved
|
||||||
|
public static final byte[] GPX_START = new byte[]{
|
||||||
|
'<', 'g', 'p', 'x'
|
||||||
|
};
|
||||||
|
|
||||||
|
private final byte[] xmlBytes;
|
||||||
|
private final long timestamp;
|
||||||
|
private final GpxFile gpxFile;
|
||||||
|
|
||||||
|
public ZeppOsGpxRouteFile(final byte[] xmlBytes) {
|
||||||
|
this.xmlBytes = xmlBytes;
|
||||||
|
this.timestamp = System.currentTimeMillis() / 1000;
|
||||||
|
this.gpxFile = parseGpx(xmlBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isValid() {
|
||||||
|
return this.gpxFile != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static GpxFile parseGpx(final byte[] xmlBytes) {
|
||||||
|
if (!isGpxFile(xmlBytes)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try (ByteArrayInputStream bais = new ByteArrayInputStream(xmlBytes)) {
|
||||||
|
final GpxParser gpxParser = new GpxParser(bais);
|
||||||
|
return gpxParser.getGpxFile();
|
||||||
|
} catch (final IOException e) {
|
||||||
|
LOG.error("Failed to read xml", e);
|
||||||
|
} catch (final GpxParseException e) {
|
||||||
|
LOG.error("Failed to parse gpx", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isGpxFile(final byte[] data) {
|
||||||
|
// TODO improve this
|
||||||
|
return ArrayUtils.equals(data, XML_HEADER, 0) || ArrayUtils.equals(data, GPX_START, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTimestamp() {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
if (gpxFile == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!StringUtils.isNullOrEmpty(gpxFile.getName())) {
|
||||||
|
return gpxFile.getName();
|
||||||
|
} else {
|
||||||
|
return String.valueOf(getTimestamp());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getEncodedBytes() {
|
||||||
|
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
final GpxFile gpxFile = parseGpx(xmlBytes);
|
||||||
|
if (gpxFile == null) {
|
||||||
|
LOG.error("Failed to read gpx file - this should never happen");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<GpxTrackPoint> trackPoints = gpxFile.getPoints();
|
||||||
|
final List<GpxWaypoint> 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();
|
||||||
|
}
|
||||||
|
}
|
@ -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 <http://www.gnu.org/licenses/>. */
|
||||||
|
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<Huami2021Support>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -46,6 +46,27 @@ public class StringUtils {
|
|||||||
return s.substring(0, length);
|
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) {
|
public static int utf8ByteLength(String string, int length) {
|
||||||
if (string == null) {
|
if (string == null) {
|
||||||
return 0;
|
return 0;
|
||||||
|
@ -791,6 +791,9 @@
|
|||||||
<string name="updatefirmwareoperation_update_complete">Firmware installation complete</string>
|
<string name="updatefirmwareoperation_update_complete">Firmware installation complete</string>
|
||||||
<string name="updatefirmwareoperation_update_complete_rebooting">Firmware installation complete, rebooting device…</string>
|
<string name="updatefirmwareoperation_update_complete_rebooting">Firmware installation complete, rebooting device…</string>
|
||||||
<string name="updatefirmwareoperation_write_failed">Firmware flashing failed</string>
|
<string name="updatefirmwareoperation_write_failed">Firmware flashing failed</string>
|
||||||
|
<string name="gpx_route_upload_failed">Gpx route upload failed</string>
|
||||||
|
<string name="gpx_route_upload_complete">Gpx route upload complete</string>
|
||||||
|
<string name="gpx_route_upload_in_progress">Uploading gpx route</string>
|
||||||
<string name="chart_steps">Steps</string>
|
<string name="chart_steps">Steps</string>
|
||||||
<string name="calories">Calories</string>
|
<string name="calories">Calories</string>
|
||||||
<string name="distance">Distance</string>
|
<string name="distance">Distance</string>
|
||||||
@ -1195,6 +1198,7 @@
|
|||||||
<string name="kind_gps_almanac">GPS Almanac</string>
|
<string name="kind_gps_almanac">GPS Almanac</string>
|
||||||
<string name="kind_gps_cep">GPS Error Correction</string>
|
<string name="kind_gps_cep">GPS Error Correction</string>
|
||||||
<string name="kind_agps_bundle">AGPS Bundle</string>
|
<string name="kind_agps_bundle">AGPS Bundle</string>
|
||||||
|
<string name="kind_gpx_route">GPX Route</string>
|
||||||
<string name="kind_resources">Resources</string>
|
<string name="kind_resources">Resources</string>
|
||||||
<string name="kind_watchface">Watchface</string>
|
<string name="kind_watchface">Watchface</string>
|
||||||
<string name="kind_app">App</string>
|
<string name="kind_app">App</string>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user