mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2025-01-25 16:15:55 +01:00
Garmin protocol: install AGPS data as firmware
This commit is contained in:
parent
9f9441ba01
commit
22fafebd91
@ -0,0 +1,122 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.garmin;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import 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.service.devices.garmin.file.GarminAgpsFile;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.UriHelper;
|
||||
|
||||
public class GarminAgpsInstallHandler implements InstallHandler {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GarminAgpsInstallHandler.class);
|
||||
|
||||
protected final Context mContext;
|
||||
private GarminAgpsFile file;
|
||||
|
||||
public GarminAgpsInstallHandler(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, they're usually ~60KB
|
||||
final GarminAgpsFile agpsFile = new GarminAgpsFile(rawBytes);
|
||||
if (agpsFile.isValid()) {
|
||||
this.file = agpsFile;
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
|
||||
final DeviceCoordinator coordinator = device.getDeviceCoordinator();
|
||||
if (!(coordinator instanceof GarminCoordinator)) {
|
||||
LOG.warn("Coordinator is not a GarminCoordinator: {}", coordinator.getClass());
|
||||
installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_supported));
|
||||
installActivity.setInstallEnabled(false);
|
||||
return;
|
||||
}
|
||||
final GarminCoordinator garminCoordinator = (GarminCoordinator) coordinator;
|
||||
if (!garminCoordinator.supportsAgpsUpdates()) {
|
||||
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 agpsBundle = mContext.getString(R.string.kind_agps_bundle);
|
||||
builder.append(mContext.getString(R.string.fw_upgrade_notice, agpsBundle));
|
||||
builder.append("\n\n").append(mContext.getString(R.string.miband_firmware_unknown_warning));
|
||||
fwItem.setDetails(mContext.getString(R.string.miband_fwinstaller_untested_version));
|
||||
installActivity.setInfoText(builder.toString());
|
||||
installActivity.setInstallItem(fwItem);
|
||||
installActivity.setInstallEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartInstall(final GBDevice device) {
|
||||
}
|
||||
|
||||
public GarminAgpsFile 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_agps_bundle),
|
||||
""
|
||||
);
|
||||
return new GenericItem(firmwareName);
|
||||
}
|
||||
}
|
@ -1,5 +1,8 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.garmin;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.List;
|
||||
@ -10,6 +13,7 @@ import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsScreen;
|
||||
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.impl.GBDevice;
|
||||
@ -92,4 +96,20 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
public boolean supportsUnicodeEmojis() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InstallHandler findInstallHandler(final Uri uri, final Context context) {
|
||||
if (supportsAgpsUpdates()) {
|
||||
final GarminAgpsInstallHandler agpsInstallHandler = new GarminAgpsInstallHandler(uri, context);
|
||||
if (agpsInstallHandler.isValid()) {
|
||||
return agpsInstallHandler;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean supportsAgpsUpdates() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -6,13 +6,23 @@ import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminCoordinator;
|
||||
|
||||
public class GarminVivoActive4SCoordinator extends GarminCoordinator {
|
||||
@Override
|
||||
protected Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile("vívoactive 4S");
|
||||
}
|
||||
@Override
|
||||
protected Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile("vívoactive 4S");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_garmin_vivoactive_4s;
|
||||
}
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_garmin_vivoactive_4s;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsFlashing() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsAgpsUpdates() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -3,11 +3,14 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin;
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.location.Location;
|
||||
import android.net.Uri;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.ArrayList;
|
||||
@ -25,6 +28,7 @@ import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminAgpsInstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminPreferences;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.GarminCapability;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService;
|
||||
@ -616,6 +620,25 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInstallApp(final Uri uri) {
|
||||
final GarminAgpsInstallHandler agpsHandler = new GarminAgpsInstallHandler(uri, getContext());
|
||||
if (agpsHandler.isValid()) {
|
||||
try {
|
||||
// Write the AGPS update to a temporary file in cache, so we can load it when requested
|
||||
final File agpsFile = getAgpsFile();
|
||||
try (FileOutputStream outputStream = new FileOutputStream(agpsFile)) {
|
||||
outputStream.write(agpsHandler.getFile().getBytes());
|
||||
LOG.info("AGPS file successfully written to the cache directory.");
|
||||
} catch (final IOException e) {
|
||||
LOG.error("Failed to write AGPS bytes to temporary directory", e);
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
GB.toast(getContext(), "AGPS install error: " + e.getMessage(), Toast.LENGTH_LONG, GB.ERROR, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkFileExists(String fileName) {
|
||||
File dir;
|
||||
try {
|
||||
@ -647,4 +670,20 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
|
||||
);
|
||||
sendOutgoingMessage(locationUpdatedNotificationRequest);
|
||||
}
|
||||
|
||||
public File getAgpsFile() throws IOException {
|
||||
return new File(getAgpsCacheDirectory(), "CPE.BIN");
|
||||
}
|
||||
|
||||
private File getAgpsCacheDirectory() throws IOException {
|
||||
final File cacheDir = getContext().getCacheDir();
|
||||
final File agpsCacheDir = new File(cacheDir, "garmin-agps");
|
||||
if (agpsCacheDir.mkdir()) {
|
||||
LOG.info("AGPS cache directory for Garmin devices successfully created.");
|
||||
} else if (!agpsCacheDir.exists() || !agpsCacheDir.isDirectory()) {
|
||||
throw new IOException("Cannot create/locate AGPS directory for Garmin devices.");
|
||||
}
|
||||
return agpsCacheDir;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,40 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.file;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
|
||||
|
||||
|
||||
public class GarminAgpsFile {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GarminAgpsFile.class);
|
||||
public static final int TAR_MAGIC_BYTES_OFFSET = 257;
|
||||
public static final byte[] TAR_MAGIC_BYTES = new byte[]{
|
||||
'u', 's', 't', 'a', 'r', '\0'
|
||||
};
|
||||
private final byte[] tarBytes;
|
||||
|
||||
public GarminAgpsFile(final byte[] tarBytes) {
|
||||
this.tarBytes = tarBytes;
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
if (!ArrayUtils.equals(tarBytes, TAR_MAGIC_BYTES, TAR_MAGIC_BYTES_OFFSET)) {
|
||||
LOG.debug("Is not TAR file!");
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO Add additional checks.
|
||||
// Archive usually contains following files:
|
||||
// CPE_GLO.BIN
|
||||
// CPE_QZSS.BIN
|
||||
// CPE_GPS.BIN
|
||||
// CPE_GAL.BIN
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public byte[] getBytes() {
|
||||
return tarBytes.clone();
|
||||
}
|
||||
}
|
@ -5,17 +5,22 @@ import com.google.protobuf.ByteString;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Random;
|
||||
import java.util.TreeMap;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiDataTransferService;
|
||||
|
||||
public class DataTransferHandler {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DataTransferHandler.class);
|
||||
private static final AtomicInteger idCounter = new AtomicInteger(0);
|
||||
private static final AtomicInteger idCounter = new AtomicInteger((new Random()).nextInt(Integer.MAX_VALUE / 2));
|
||||
private static final Map<Integer, Data> dataById = new HashMap<>();
|
||||
private static final Map<Integer, ChunkInfo> unprocessedChunksByRequestId = new HashMap<>();
|
||||
|
||||
@ -94,6 +99,13 @@ public class DataTransferHandler {
|
||||
data.onDataChunkSuccessfullyReceived(chunkInfo);
|
||||
if (data.isDataSuccessfullySent()) {
|
||||
LOG.info("Data successfully sent to the device (id: {}, size: {})", chunkInfo.dataId, data.data.length);
|
||||
for (Callable<Void> listener : data.onDataSuccessfullySentListeners) {
|
||||
try {
|
||||
listener.call();
|
||||
} catch (Exception e) {
|
||||
LOG.error("Data listener failed.", e);
|
||||
}
|
||||
}
|
||||
dataById.remove(chunkInfo.dataId);
|
||||
} else {
|
||||
LOG.debug(
|
||||
@ -103,6 +115,10 @@ public class DataTransferHandler {
|
||||
}
|
||||
}
|
||||
|
||||
public static void addOnDataSuccessfullySentListener(final int dataId, final Callable<Void> listener) {
|
||||
Objects.requireNonNull(dataById.get(dataId)).onDataSuccessfullySentListeners.add(listener);
|
||||
}
|
||||
|
||||
private static class ChunkInfo {
|
||||
private final int dataId;
|
||||
private final int start;
|
||||
@ -120,10 +136,12 @@ public class DataTransferHandler {
|
||||
// Because now we have to store the whole data in RAM.
|
||||
private final byte[] data;
|
||||
private final TreeMap<Integer, ChunkInfo> chunksReceivedByDevice;
|
||||
private final List<Callable<Void>> onDataSuccessfullySentListeners;
|
||||
|
||||
private Data(byte[] data) {
|
||||
this.data = data;
|
||||
chunksReceivedByDevice = new TreeMap<>();
|
||||
onDataSuccessfullySentListeners = new ArrayList<>();
|
||||
}
|
||||
|
||||
private byte[] getDataChunk(final int offset, final int maxChunkSize) {
|
||||
|
@ -3,14 +3,15 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.http;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
|
||||
public class EphemerisHandler {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(EphemerisHandler.class);
|
||||
@ -21,21 +22,30 @@ public class EphemerisHandler {
|
||||
}
|
||||
|
||||
public byte[] handleEphemerisRequest(final String path, final Map<String, String> query) {
|
||||
// TODO Return status code 304 (Not Modified) when we don't have newer data and "if-none-match" is set.
|
||||
try {
|
||||
final File exportDirectory = deviceSupport.getWritableExportDirectory();
|
||||
final File ephemerisDataFile = new File(exportDirectory, "CPE.BIN");
|
||||
if (!ephemerisDataFile.exists() || !ephemerisDataFile.isFile()) {
|
||||
throw new IOException("Cannot locate CPE.BIN file in export/import directory.");
|
||||
final File agpsFile = deviceSupport.getAgpsFile();
|
||||
if (!agpsFile.exists() || !agpsFile.isFile()) {
|
||||
LOG.info("File with AGPS data does not exist.");
|
||||
return null;
|
||||
}
|
||||
try(InputStream agpsIn = new FileInputStream(agpsFile)) {
|
||||
final byte[] rawBytes = FileUtils.readAll(agpsIn, 1024 * 1024); // 1MB, they're usually ~60KB
|
||||
LOG.info("Sending new AGPS data to the device.");
|
||||
return rawBytes;
|
||||
}
|
||||
final byte[] bytes = new byte[(int) ephemerisDataFile.length()];
|
||||
final BufferedInputStream bis = new BufferedInputStream(new FileInputStream(ephemerisDataFile));
|
||||
final DataInputStream dis = new DataInputStream(bis);
|
||||
dis.readFully(bytes);
|
||||
return bytes;
|
||||
} catch (IOException e) {
|
||||
LOG.error("Unable to obtain ephemeris data.", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Callable<Void> getOnDataSuccessfullySentListener() {
|
||||
return () -> {
|
||||
LOG.info("AGPS data successfully sent to the device.");
|
||||
if (deviceSupport.getAgpsFile().delete()) {
|
||||
LOG.info("AGPS data was deleted from the cache folder.");
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiHttpService;
|
||||
@ -52,6 +53,7 @@ public class HttpHandler {
|
||||
}
|
||||
|
||||
public GdiHttpService.HttpService.RawResponse handleRawRequest(final GdiHttpService.HttpService.RawRequest rawRequest) {
|
||||
// TODO Return status code 304 (Not Modified) when we don't have newer data and "if-none-match" is set.
|
||||
final String urlString = rawRequest.getUrl();
|
||||
LOG.debug("Got rawRequest: {} - {}", rawRequest.getMethod(), urlString);
|
||||
|
||||
@ -74,15 +76,15 @@ public class HttpHandler {
|
||||
}
|
||||
final String json = GSON.toJson(weatherData);
|
||||
LOG.debug("Weather response: {}", json);
|
||||
return createRawResponse(rawRequest, json.getBytes(StandardCharsets.UTF_8), "application/json");
|
||||
return createRawResponse(rawRequest, json.getBytes(StandardCharsets.UTF_8), "application/json", null);
|
||||
} else if (path.startsWith("/ephemeris/")) {
|
||||
LOG.info("Got ephemeris request for {}", path);
|
||||
byte[] ephemerisData = ephemerisHandler.handleEphemerisRequest(path, query);
|
||||
final byte[] ephemerisData = ephemerisHandler.handleEphemerisRequest(path, query);
|
||||
if (ephemerisData == null) {
|
||||
return null;
|
||||
}
|
||||
LOG.debug("Successfully obtained ephemeris data (length: {})", ephemerisData.length);
|
||||
return createRawResponse(rawRequest, ephemerisData, "application/x-tar");
|
||||
return createRawResponse(rawRequest, ephemerisData, "application/x-tar", ephemerisHandler.getOnDataSuccessfullySentListener());
|
||||
} else {
|
||||
LOG.warn("Unhandled path {}", urlString);
|
||||
return null;
|
||||
@ -92,11 +94,15 @@ public class HttpHandler {
|
||||
private static GdiHttpService.HttpService.RawResponse createRawResponse(
|
||||
final GdiHttpService.HttpService.RawRequest rawRequest,
|
||||
final byte[] data,
|
||||
final String contentType
|
||||
) {
|
||||
final String contentType,
|
||||
final Callable<Void> onDataSuccessfullySentListener
|
||||
) {
|
||||
if (rawRequest.hasUseDataXfer() && rawRequest.getUseDataXfer()) {
|
||||
LOG.debug("Data will be returned using data_xfer");
|
||||
int id = DataTransferHandler.registerData(data);
|
||||
if (onDataSuccessfullySentListener != null) {
|
||||
DataTransferHandler.addOnDataSuccessfullySentListener(id, onDataSuccessfullySentListener);
|
||||
}
|
||||
return GdiHttpService.HttpService.RawResponse.newBuilder()
|
||||
.setStatus(GdiHttpService.HttpService.Status.OK)
|
||||
.setHttpStatus(200)
|
||||
|
Loading…
Reference in New Issue
Block a user