igpsport: Install route files as apps

* works with cnx, gpx, fit, tcx and xml roue files
* only extension checked, no real checks for valid file
This commit is contained in:
Vitaliy Tomin
2026-05-13 23:33:24 +02:00
committed by José Rebelo
parent 1d5a59974f
commit 601eca82ef
3 changed files with 275 additions and 25 deletions
@@ -1,16 +1,20 @@
package nodomain.freeyourgadget.gadgetbridge.devices.igpsport;
import android.app.Activity;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import java.util.regex.Pattern;
import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AppManagerActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.MakibesHR3ActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.igpsport.IGPSportDeviceSupport;
@@ -63,4 +67,32 @@ public class IGPSportCoordinator extends AbstractBLEDeviceCoordinator {
return true;
}
@Override
public boolean supportsAppsManagement(final GBDevice device) {
return true;
}
@Override
public Class<? extends Activity> getAppsManagementActivity() {
return AppManagerActivity.class;
}
@Override
public InstallHandler findInstallHandler(final Uri uri, final Context context) {
// final IGPSportAgpsInstallHandler agpsInstallHandler = new IGPSportAgpsInstallHandler(uri, context);
// if (agpsInstallHandler.isValid()) {
// return agpsInstallHandler;
// }
final IGPSportRouteInstallHandler routeInstallHandler = new IGPSportRouteInstallHandler(uri, context);
if (routeInstallHandler.isValid()) {
return routeInstallHandler;
}
return null;
}
}
@@ -0,0 +1,145 @@
package nodomain.freeyourgadget.gadgetbridge.devices.igpsport;
import android.content.Context;
import android.net.Uri;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.UriHelper;
public class IGPSportRouteInstallHandler implements InstallHandler {
private static final Logger LOG = LoggerFactory.getLogger(IGPSportRouteInstallHandler.class);
protected final Context mContext;
private byte[] rawBytes = null;
private String filename;
private String extension;
private List<String> routeExtensions = Arrays.asList("cnx", "gpx", "fit", "tcx", "xml");
public IGPSportRouteInstallHandler(final Uri uri, final Context context) {
this.mContext = context;
final UriHelper uriHelper;
try {
uriHelper = UriHelper.get(uri, context);
filename = uriHelper.getFileName();
int strLength = filename.lastIndexOf(".");
if(strLength > 0)
extension = filename.substring(strLength + 1).toLowerCase();
} 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
try (ByteArrayInputStream bais = new ByteArrayInputStream(rawBytes)) {
} catch (final IOException e) {
LOG.error("Failed to read xml", e);
}
} catch (final Exception e) {
LOG.error("Failed to read file", e);
}
}
@Override
public boolean isValid() {
if (routeExtensions.contains(extension))
return true;
else
return false;
}
@Override
public void validateInstallation(InstallActivity installActivity, GBDevice device) {
if (device.isBusy()) {
installActivity.setInfoText(device.getBusyTask());
installActivity.setInstallEnabled(false);
return;
}
final DeviceCoordinator coordinator = device.getDeviceCoordinator();
if (!(coordinator instanceof IGPSportCoordinator)) {
LOG.warn("Coordinator is not a IGPSportCoordinator: {}", coordinator.getClass());
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 (rawBytes == 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 cnxRoute = mContext.getString(R.string.kind_gpx_route);
builder.append(mContext.getString(R.string.fw_upgrade_notice, filename));
installActivity.setInfoText(builder.toString());
installActivity.setInstallItem(fwItem);
installActivity.setInstallEnabled(true);
}
@Override
public void onStartInstall(GBDevice device) {
}
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),
"test" //FIXME
);
return new GenericItem(firmwareName);
}
public byte[] getBytes() {
return rawBytes;
}
public int getSize() {
return rawBytes.length;
}
public String getFilename() {
return filename;
}
public String getExtension() {
return extension;
}
}
@@ -4,6 +4,7 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.igpsport;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.content.Intent;
import android.net.Uri;
import android.widget.Toast;
import com.google.protobuf.ByteString;
@@ -14,17 +15,18 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Random;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
import nodomain.freeyourgadget.gadgetbridge.devices.igpsport.IGPSportRouteInstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.igpsport.IGPSportConstants;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
@@ -35,7 +37,9 @@ import nodomain.freeyourgadget.gadgetbridge.proto.igpsport.Ble;
import nodomain.freeyourgadget.gadgetbridge.proto.igpsport.Config;
import nodomain.freeyourgadget.gadgetbridge.proto.igpsport.CyclingData;
import nodomain.freeyourgadget.gadgetbridge.proto.igpsport.Firmware;
import nodomain.freeyourgadget.gadgetbridge.proto.igpsport.GeneralFileOperation;
import nodomain.freeyourgadget.gadgetbridge.proto.igpsport.Ins;
import nodomain.freeyourgadget.gadgetbridge.proto.igpsport.RoutePlan;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
@@ -56,6 +60,7 @@ public class IGPSportDeviceSupport extends AbstractBTLEDeviceSupport {
public BluetoothGattCharacteristic readCharacteristic;
public BluetoothGattCharacteristic writeCharacteristic;
public BluetoothGattCharacteristic writeCharacteristicThird;
public BluetoothGattCharacteristic writeCharacteristicFourth;
public final GBDeviceEventBatteryInfo batteryCmd = new GBDeviceEventBatteryInfo();
public final GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo();
public final DeviceInfoProfile<IGPSportDeviceSupport> deviceInfoProfile;
@@ -128,6 +133,7 @@ public class IGPSportDeviceSupport extends AbstractBTLEDeviceSupport {
readCharacteristic = getCharacteristic(IGPSportConstants.UUID_IGPSPORT_CHARACTERISTIC_FIRST_RX);
writeCharacteristic = getCharacteristic(IGPSportConstants.UUID_IGPSPORT_CHARACTERISTIC_FIRST_TX);
writeCharacteristicThird = getCharacteristic(IGPSportConstants.UUID_IGPSPORT_CHARACTERISTIC_THIRD_TX);
writeCharacteristicFourth = getCharacteristic(IGPSportConstants.UUID_IGPSPORT_CHARACTERISTIC_FOURTH_TX);
builder.notify(getCharacteristic(IGPSportConstants.UUID_IGPSPORT_CHARACTERISTIC_FIRST_RX), true);
builder.notify(getCharacteristic(IGPSportConstants.UUID_IGPSPORT_CHARACTERISTIC_SECOND_RX), true);
@@ -187,31 +193,44 @@ public class IGPSportDeviceSupport extends AbstractBTLEDeviceSupport {
LOG.info("Characteristic changed value: " + GB.hexdump(characteristic.getValue()));
byte[] data = characteristic.getValue();
if (data[0] != IGPSportConstants.DATA_HEADER) {
LOG.info("FitPro, packet not starting with 0x01: " + data[0] + "other message types not implemented yet");
return false;
if (data[0] == IGPSportConstants.DATA_HEADER) {
if (data != null && data.length > 20) {
byte mainService = data[1];
byte mainOperation = data[4];
int dataSize = ByteBuffer.wrap(data, 7, 2).getShort();
byte[] pbData = new byte[dataSize];
System.arraycopy(data, 20, pbData, 0, dataSize);
try {
switch (mainService) {
case Common.service_type_index.enum_SERVICE_TYPE_INDEX_FACTORY_VALUE:
handleFactoryData(pbData);
break;
case Common.service_type_index.enum_SERVICE_TYPE_INDEX_FIRMWARE_VALUE:
handleFirmwareData(pbData);
break;
}
} catch (InvalidProtocolBufferException e) {
throw new RuntimeException(e);
}
}
}
if (data != null && data.length > 20) {
if (data[0] == 0x02) {
//0215FFFF03FFFF00FFFFFFFFFFFFFFFFFFFFFF55
byte mainService = data[1];
byte mainOperation = data[4];
int dataSize = ByteBuffer.wrap(data, 7,2).getShort();
byte[] pbData = new byte[dataSize];
System.arraycopy(data, 20, pbData, 0, dataSize);
try {
switch (mainService) {
case Common.service_type_index.enum_SERVICE_TYPE_INDEX_FACTORY_VALUE:
handleFactoryData(pbData);
break;
case Common.service_type_index.enum_SERVICE_TYPE_INDEX_FIRMWARE_VALUE:
handleFirmwareData(pbData);
break;
}
} catch (InvalidProtocolBufferException e) {
throw new RuntimeException(e);
byte result = data[7];
switch (mainService) {
case Common.service_type_index.enum_SERVICE_TYPE_INDEX_ROUTE_PLAN_VALUE:
if(mainOperation == Common.SERVICE_OPERATE_TYPE.enum_SERVICE_OPERATE_TYPE_ADD_VALUE) {
gbDevice.unsetBusyTask();
}
break;
}
}
return false;
@@ -432,4 +451,58 @@ public class IGPSportDeviceSupport extends AbstractBTLEDeviceSupport {
}
@Override
public void onInstallApp(Uri uri) {
//final IGPSportGpxRouteInstallHandler gpxRouteHandler = new IGPSportGpxRouteInstallHandler(uri, getContext());
final IGPSportRouteInstallHandler routeHandler = new IGPSportRouteInstallHandler(uri, getContext());
if (routeHandler.isValid()) {
try {
TransactionBuilder builder = performInitialized("prepare upload gpx");
// RoutePlan.route_plan_data_msg.Builder routePlanBuilder = RoutePlan.route_plan_data_msg.newBuilder();
// routePlanBuilder.setServiceType(Common.service_type_index.enum_SERVICE_TYPE_INDEX_ROUTE_PLAN);
// routePlanBuilder.setRoutePlanOperateType(RoutePlan.ROUTE_PLAN_OPERATE_TYPE.enum_ROUTE_PLAN_OPERATE_TYPE_LIST_NUM_GET);
// byte[] routePlanBytes = craftData(routePlanBuilder.getServiceType().getNumber(), 0xff, routePlanBuilder.getRoutePlanOperateType().getNumber(), routePlanBuilder.build().toByteArray());
// builder.write(writeCharacteristicFourth, routePlanBytes);
// builder.wait(1000);
//
// RoutePlan.route_plan_data_msg.Builder routePlan2ndBuilder = RoutePlan.route_plan_data_msg.newBuilder();
// routePlan2ndBuilder.setServiceType(Common.service_type_index.enum_SERVICE_TYPE_INDEX_ROUTE_PLAN);
// routePlan2ndBuilder.setRoutePlanOperateType(RoutePlan.ROUTE_PLAN_OPERATE_TYPE.enum_ROUTE_PLAN_OPERATE_TYPE_LIST_GET);
// routePlan2ndBuilder.setRouteListGetMsg(Common.file_list_get_message.newBuilder().setFileIndexEnd(0).setFileIndexEnd(0));
// byte[] routePlan2ndBytes = craftData(routePlan2ndBuilder.getServiceType().getNumber(), 0xff, routePlan2ndBuilder.getRoutePlanOperateType().getNumber(), routePlan2ndBuilder.build().toByteArray());
// builder.write(writeCharacteristicFourth, routePlan2ndBytes);
// builder.wait(1000);
//
// builder.write(writeCharacteristicFourth, routePlanBytes);
// builder.wait(1000);
Random random = new Random();
int ran = random.nextInt();
GeneralFileOperation.general_file_operation.Builder fileOperationbuilder = GeneralFileOperation.general_file_operation.newBuilder();
fileOperationbuilder.setServiceType(Common.service_type_index.enum_SERVICE_TYPE_INDEX_FILE_OPERATION)
.setOperateType(Common.SERVICE_OPERATE_TYPE.enum_SERVICE_OPERATE_TYPE_ADD)
.setFileType(GeneralFileOperation.file_operation_type.enum_FILE_TYPE_ROUTE_PLAN)
.setFileId(ran)
.setFileExtension(routeHandler.getExtension())
.setFileName(routeHandler.getFilename())
.setFileSize(routeHandler.getSize());
byte[] fileOperationBytes = craftFileData(fileOperationbuilder.getServiceType().getNumber(),
0xff,
fileOperationbuilder.getOperateType().getNumber(),
fileOperationbuilder.build().toByteArray(),
routeHandler.getBytes());
builder.writeChunkedData(writeCharacteristicFourth, fileOperationBytes, getMTU());
gbDevice.setBusyTask("Installing route");
builder.queue(getQueue());
} catch (final Exception e) {
GB.toast(getContext(), "Gpx install error: " + e.getMessage(), Toast.LENGTH_LONG, GB.ERROR, e);
}
return;
}
}
}