diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/ChecksumCalculator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/ChecksumCalculator.java
similarity index 99%
rename from app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/ChecksumCalculator.java
rename to app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/ChecksumCalculator.java
index a46fa76e8..d061bede4 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/ChecksumCalculator.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/ChecksumCalculator.java
@@ -14,7 +14,7 @@
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.garmin.messages;
+package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin;
import java.nio.ByteBuffer;
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/FileTransferHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/FileTransferHandler.java
new file mode 100644
index 000000000..4fec390c3
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/FileTransferHandler.java
@@ -0,0 +1,337 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Date;
+
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.DownloadRequestMessage;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.FileTransferDataMessage;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.GFDIMessage;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.SystemEventMessage;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.UploadRequestMessage;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.CreateFileStatusMessage;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.DownloadRequestStatusMessage;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.FileTransferDataStatusMessage;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.UploadRequestStatusMessage;
+import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
+
+public class FileTransferHandler implements MessageHandler {
+ private static final Logger LOG = LoggerFactory.getLogger(FileTransferHandler.class);
+ private final GarminSupport deviceSupport;
+ private final Download download;
+ private final Upload upload;
+
+ public FileTransferHandler(GarminSupport deviceSupport) {
+ this.deviceSupport = deviceSupport;
+ this.download = new Download();
+ this.upload = new Upload();
+ }
+
+ public boolean isDownloading() {
+ return download.getCurrentlyDownloading() != null;
+ }
+
+ public boolean isUploading() {
+ return upload.getCurrentlyUploading() != null;
+ }
+
+ public GFDIMessage handle(GFDIMessage message) {
+ if (message instanceof DownloadRequestStatusMessage)
+ download.processDownloadRequestStatusMessage((DownloadRequestStatusMessage) message);
+ else if (message instanceof FileTransferDataMessage)
+ download.processDownloadChunkedMessage((FileTransferDataMessage) message);
+ else if (message instanceof CreateFileStatusMessage)
+ return upload.setCreateFileStatusMessage((CreateFileStatusMessage) message);
+ else if (message instanceof UploadRequestStatusMessage)
+ return upload.setUploadRequestStatusMessage((UploadRequestStatusMessage) message);
+ else if (message instanceof FileTransferDataStatusMessage)
+ return upload.processUploadProgress((FileTransferDataStatusMessage) message);
+
+ return null;
+ }
+
+ public DownloadRequestMessage downloadDirectoryEntry(DirectoryEntry directoryEntry) {
+ download.setCurrentlyDownloading(new FileFragment(directoryEntry));
+ return new DownloadRequestMessage(directoryEntry.getFileIndex(), 0, DownloadRequestMessage.REQUEST_TYPE.NEW, 0, 0);
+ }
+
+ public DownloadRequestMessage initiateDownload() {
+ download.setCurrentlyDownloading(new FileFragment(new DirectoryEntry(0, FileType.FILETYPE.DIRECTORY, 0, 0, 0, 0, null)));
+ return new DownloadRequestMessage(0, 0, DownloadRequestMessage.REQUEST_TYPE.NEW, 0, 0);
+ }
+// public DownloadRequestMessage downloadSettings() {
+// download.setCurrentlyDownloading(new FileFragment(new DirectoryEntry(0, FileType.FILETYPE.SETTINGS, 0, 0, 0, 0, null)));
+// return new DownloadRequestMessage(0, 0, DownloadRequestMessage.REQUEST_TYPE.NEW, 0, 0);
+// }
+//
+// public CreateFileMessage initiateUpload(byte[] fileAsByteArray, FileType.FILETYPE filetype) {
+// upload.setCurrentlyUploading(new FileFragment(new DirectoryEntry(0, filetype, 0, 0, 0, fileAsByteArray.length, null), fileAsByteArray));
+// return new CreateFileMessage(fileAsByteArray.length, filetype);
+// }
+
+
+ class Download {
+ private FileFragment currentlyDownloading;
+
+ public FileFragment getCurrentlyDownloading() {
+ return currentlyDownloading;
+ }
+
+ public void setCurrentlyDownloading(FileFragment currentlyDownloading) {
+ this.currentlyDownloading = currentlyDownloading;
+ }
+
+ private void processDownloadChunkedMessage(FileTransferDataMessage fileTransferDataMessage) {
+ if (!isDownloading())
+ throw new IllegalStateException("Received file transfer of unknown file");
+
+ currentlyDownloading.append(fileTransferDataMessage);
+ if (!currentlyDownloading.dataHolder.hasRemaining())
+ processCompleteDownload();
+ }
+
+ private void processCompleteDownload() {
+ currentlyDownloading.dataHolder.flip();
+
+ if (FileType.FILETYPE.DIRECTORY.equals(currentlyDownloading.directoryEntry.filetype)) { //is a directory
+ parseDirectoryEntries();
+ } else {
+ saveFileToExternalStorage();
+ }
+
+ currentlyDownloading = null;
+ }
+
+ public void processDownloadRequestStatusMessage(DownloadRequestStatusMessage downloadRequestStatusMessage) {
+ if (null == currentlyDownloading)
+ throw new IllegalStateException("Received file transfer of unknown file");
+ if (downloadRequestStatusMessage.canProceed())
+ currentlyDownloading.setSize(downloadRequestStatusMessage);
+ else
+ currentlyDownloading = null;
+ }
+
+ private void saveFileToExternalStorage() {
+ File dir;
+ try {
+ dir = deviceSupport.getWritableExportDirectory();
+ File outputFile = new File(dir, currentlyDownloading.getFileName());
+ FileUtils.copyStreamToFile(new ByteArrayInputStream(currentlyDownloading.dataHolder.array()), outputFile);
+ outputFile.setLastModified(currentlyDownloading.directoryEntry.fileDate.getTime());
+
+ } catch (IOException e) {
+ LOG.error("IOException: " + e);
+ }
+
+ LOG.debug("FILE DOWNLOAD COMPLETE {}", currentlyDownloading.getFileName());
+ }
+
+ private void parseDirectoryEntries() {
+ if ((currentlyDownloading.getDataSize() % 16) != 0)
+ throw new IllegalArgumentException("Invalid directory data length");
+ final GarminByteBufferReader reader = new GarminByteBufferReader(currentlyDownloading.dataHolder.array());
+ reader.setByteOrder(ByteOrder.LITTLE_ENDIAN);
+ while (reader.remaining() > 0) {
+ final int fileIndex = reader.readShort();//2
+ final int fileDataType = reader.readByte();//3
+ final int fileSubType = reader.readByte();//4
+ final FileType.FILETYPE filetype = FileType.FILETYPE.fromDataTypeSubType(fileDataType, fileSubType);
+ final int fileNumber = reader.readShort();//6
+ final int specificFlags = reader.readByte();//7
+ final int fileFlags = reader.readByte();//8
+ final int fileSize = reader.readInt();//12
+ final Date fileDate = new Date(GarminTimeUtils.garminTimestampToJavaMillis(reader.readInt()));//16
+ final DirectoryEntry directoryEntry = new DirectoryEntry(fileIndex, filetype, fileNumber, specificFlags, fileFlags, fileSize, fileDate);
+ if (directoryEntry.filetype == null) //silently discard unsupported files
+ continue;
+ deviceSupport.addFileToDownloadList(directoryEntry);
+ }
+ currentlyDownloading = null;
+ }
+ }
+
+ class Upload {
+
+ private FileFragment currentlyUploading;
+
+ private UploadRequestMessage setCreateFileStatusMessage(CreateFileStatusMessage createFileStatusMessage) {
+ if (createFileStatusMessage.canProceed()) {
+ LOG.info("SENDING UPLOAD FILE");
+ return new UploadRequestMessage(createFileStatusMessage.getFileIndex(), currentlyUploading.getDataSize());
+ } else {
+ LOG.warn("Cannot proceed with upload");
+ this.currentlyUploading = null;
+ }
+ return null;
+ }
+
+ private FileTransferDataMessage setUploadRequestStatusMessage(UploadRequestStatusMessage uploadRequestStatusMessage) {
+ if (null == currentlyUploading)
+ throw new IllegalStateException("Received upload request status transfer of unknown file");
+ if (uploadRequestStatusMessage.canProceed()) {
+ if (uploadRequestStatusMessage.getDataOffset() != currentlyUploading.dataHolder.position())
+ throw new IllegalStateException("Received upload request with unaligned offset");
+ return currentlyUploading.take();
+ } else {
+ LOG.warn("Cannot proceed with upload");
+ this.currentlyUploading = null;
+ }
+ return null;
+ }
+
+ private GFDIMessage processUploadProgress(FileTransferDataStatusMessage fileTransferDataStatusMessage) {
+ if (currentlyUploading.getDataSize() <= fileTransferDataStatusMessage.getDataOffset()) {
+ this.currentlyUploading = null;
+ LOG.info("SENDING SYNC COMPLETE!!!");
+
+ return new SystemEventMessage(SystemEventMessage.GarminSystemEventType.SYNC_COMPLETE, 0);
+ } else {
+ if (fileTransferDataStatusMessage.canProceed()) {
+ LOG.info("SENDING NEXT CHUNK!!!");
+ if (fileTransferDataStatusMessage.getDataOffset() != currentlyUploading.dataHolder.position())
+ throw new IllegalStateException("Received file transfer status with unaligned offset");
+ return currentlyUploading.take();
+ } else {
+ LOG.warn("Cannot proceed with upload");
+ this.currentlyUploading = null;
+ }
+
+ }
+ return null;
+ }
+
+ public FileFragment getCurrentlyUploading() {
+ return this.currentlyUploading;
+ }
+
+ public void setCurrentlyUploading(FileFragment currentlyUploading) {
+ this.currentlyUploading = currentlyUploading;
+ }
+
+ }
+
+ class FileFragment {
+ private final DirectoryEntry directoryEntry;
+ private final int maxBlockSize = 500;
+ private int dataSize;
+ private ByteBuffer dataHolder;
+ private int runningCrc;
+
+ FileFragment(DirectoryEntry directoryEntry) {
+ this.directoryEntry = directoryEntry;
+ this.setRunningCrc(0);
+ }
+
+ FileFragment(DirectoryEntry directoryEntry, byte[] contents) {
+ this.directoryEntry = directoryEntry;
+ this.setDataSize(contents.length);
+ this.dataHolder = ByteBuffer.wrap(contents);
+ this.dataHolder.flip(); //we'll be only reading from here on
+ this.dataHolder.compact();
+ this.setRunningCrc(0);
+ }
+
+ private int getMaxBlockSize() {
+ return Math.max(maxBlockSize, GFDIMessage.getMaxPacketSize());
+ }
+
+ public String getFileName() {
+ return directoryEntry.getFileName();
+ }
+
+ private void setSize(DownloadRequestStatusMessage downloadRequestStatusMessage) {
+ if (0 != getDataSize())
+ throw new IllegalStateException("Data size already set");
+
+ this.setDataSize(downloadRequestStatusMessage.getMaxFileSize());
+ this.dataHolder = ByteBuffer.allocate(getDataSize());
+ }
+
+ private void append(FileTransferDataMessage fileTransferDataMessage) {
+ if (fileTransferDataMessage.getDataOffset() != dataHolder.position())
+ throw new IllegalStateException("Received message that was already received");
+
+ final int dataCrc = ChecksumCalculator.computeCrc(getRunningCrc(), fileTransferDataMessage.getMessage(), 0, fileTransferDataMessage.getMessage().length);
+ if (fileTransferDataMessage.getCrc() != dataCrc)
+ throw new IllegalStateException("Received message with invalid CRC");
+ setRunningCrc(dataCrc);
+
+ this.dataHolder.put(fileTransferDataMessage.getMessage());
+ }
+
+ private FileTransferDataMessage take() {
+ final int currentOffset = this.dataHolder.position();
+ final byte[] chunk = new byte[Math.min(this.dataHolder.remaining(), getMaxBlockSize())];
+ this.dataHolder.get(chunk);
+ setRunningCrc(ChecksumCalculator.computeCrc(getRunningCrc(), chunk, 0, chunk.length));
+ return new FileTransferDataMessage(chunk, currentOffset, getRunningCrc());
+ }
+
+ private int getDataSize() {
+ return dataSize;
+ }
+
+ private void setDataSize(int dataSize) {
+ this.dataSize = dataSize;
+ }
+
+ private int getRunningCrc() {
+ return runningCrc;
+ }
+
+ private void setRunningCrc(int runningCrc) {
+ this.runningCrc = runningCrc;
+ }
+ }
+
+ class DirectoryEntry {
+ private final int fileIndex;
+ private final FileType.FILETYPE filetype;
+ private final int fileNumber;
+ private final int specificFlags;
+ private final int fileFlags;
+ private final int fileSize;
+ private final Date fileDate;
+
+ public DirectoryEntry(int fileIndex, FileType.FILETYPE filetype, int fileNumber, int specificFlags, int fileFlags, int fileSize, Date fileDate) {
+ this.fileIndex = fileIndex;
+ this.filetype = filetype;
+ this.fileNumber = fileNumber;
+ this.specificFlags = specificFlags;
+ this.fileFlags = fileFlags;
+ this.fileSize = fileSize;
+ this.fileDate = fileDate;
+ }
+
+ public int getFileIndex() {
+ return fileIndex;
+ }
+
+ public FileType.FILETYPE getFiletype() {
+ return filetype;
+ }
+
+ public String getFileName() {
+ return getFiletype().name() + "_" + getFileIndex() + (getFiletype().isFitFile() ? ".fit" : "");
+ }
+
+ @Override
+ public String toString() {
+ return "DirectoryEntry{" +
+ "fileIndex=" + fileIndex +
+ ", fileType=" + filetype.name() +
+ ", fileNumber=" + fileNumber +
+ ", specificFlags=" + specificFlags +
+ ", fileFlags=" + fileFlags +
+ ", fileSize=" + fileSize +
+ ", fileDate=" + fileDate +
+ '}';
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/FileType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/FileType.java
new file mode 100644
index 000000000..352b89332
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/FileType.java
@@ -0,0 +1,79 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin;
+
+import android.util.Pair;
+
+import androidx.annotation.Nullable;
+
+public class FileType {
+ //common
+ //128/4: FIT_TYPE_4, -> garmin/activity
+ //128/32: FIT_TYPE_32, -> garmin/monitor
+ //128/44: FIT_TYPE_44, ->garmin/metrics
+ //128/41: FIT_TYPE_41, ->garmin/chnglog
+ //128/49: FIT_TYPE_49, -> garmin/sleep
+ //255/245: ErrorShutdownReports,
+
+ //Specific Instinct 2S:
+ //128/38: FIT_TYPE_38, -> garmin/SCORCRDS
+ //255/248: KPI,
+ //128/58: FIT_TYPE_58, -> outputFromUnit garmin/device????
+ //255/247: ULFLogs,
+ //128/68: FIT_TYPE_68, -> garmin/HRVSTATUS
+ //128/70: FIT_TYPE_70, -> garmin/HSA
+ //128/72: FIT_TYPE_72, -> garmin/FBTBACKUP
+ //128/74: FIT_TYPE_74
+
+
+ private final FILETYPE fileType;
+ private final String garminDeviceFileType;
+
+ public FileType(int fileDataType, int fileSubType, String garminDeviceFileType) {
+ this.fileType = FILETYPE.fromDataTypeSubType(fileDataType, fileSubType);
+ this.garminDeviceFileType = garminDeviceFileType;
+ }
+
+ public FILETYPE getFileType() {
+ return fileType;
+ }
+
+ public enum FILETYPE { //TODO: add specialized method to parse each file type to the enum?
+ ACTIVITY(Pair.create(128, 4)),
+ MONITOR(Pair.create(128, 32)),
+ CHANGELOG(Pair.create(128, 41)),
+ METRICS(Pair.create(128, 44)),
+ SLEEP(Pair.create(128, 49)),
+
+ //"virtual" and/or undocumented file types
+ DIRECTORY(Pair.create(0, 0)),
+// SETTINGS(Pair.create(128,2)),
+ ;
+
+ private final Pair type;
+
+ FILETYPE(Pair pair) {
+ this.type = pair;
+ }
+
+ @Nullable
+ public static FILETYPE fromDataTypeSubType(int dataType, int subType) {
+ for (FILETYPE ft :
+ FILETYPE.values()) {
+ if (ft.type.first == dataType && ft.type.second == subType)
+ return ft;
+ }
+ return null;
+ }
+
+ public int getType() {
+ return type.first;
+ }
+
+ public int getSubType() {
+ return type.second;
+ }
+
+ public boolean isFitFile() {
+ return type.first == 128;
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java
index 0b9ba3e1e..3ded65dbb 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java
@@ -6,20 +6,27 @@ import android.bluetooth.BluetoothGattCharacteristic;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.io.File;
+import java.io.IOException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
+import java.util.LinkedList;
import java.util.List;
import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Queue;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiDeviceStatus;
@@ -31,32 +38,67 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateA
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.communicator.ICommunicator;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.communicator.v1.CommunicatorV1;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.communicator.v2.CommunicatorV2;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.NotificationSubscriptionDeviceEvent;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.SupportedFileTypesDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.WeatherRequestDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.LocalMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.ConfigurationMessage;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.DownloadRequestMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.GFDIMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MusicControlEntityUpdateMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.ProtobufMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.SetDeviceSettingsMessage;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.SupportedFileTypesMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.SystemEventMessage;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.ProtobufStatusMessage;
+import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommunicator.Callback {
private static final Logger LOG = LoggerFactory.getLogger(GarminSupport.class);
private final ProtocolBufferHandler protocolBufferHandler;
+ private final NotificationsHandler notificationsHandler;
+ private final FileTransferHandler fileTransferHandler;
+ private final Queue filesToDownload;
+ private final List messageHandlers;
private ICommunicator communicator;
private MusicStateSpec musicStateSpec;
private Timer musicStateTimer;
+ private List supportedFileTypeList;
public GarminSupport() {
super(LOG);
addSupportedService(CommunicatorV1.UUID_SERVICE_GARMIN_GFDI);
addSupportedService(CommunicatorV2.UUID_SERVICE_GARMIN_ML_GFDI);
protocolBufferHandler = new ProtocolBufferHandler(this);
+ fileTransferHandler = new FileTransferHandler(this);
+ filesToDownload = new LinkedList<>();
+ messageHandlers = new ArrayList<>();
+ notificationsHandler = new NotificationsHandler();
+ messageHandlers.add(fileTransferHandler);
+ messageHandlers.add(protocolBufferHandler);
+ messageHandlers.add(notificationsHandler);
+ }
+
+ @Override
+ public void dispose() {
+ super.dispose();
+ stopMusicTimer();
+ }
+
+ private void stopMusicTimer() {
+ if (musicStateTimer != null) {
+ musicStateTimer.cancel();
+ musicStateTimer.purge();
+ musicStateTimer = null;
+ }
+ }
+
+ public void addFileToDownloadList(FileTransferHandler.DirectoryEntry directoryEntry) {
+ filesToDownload.add(directoryEntry);
}
@Override
@@ -109,33 +151,40 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
evaluateGBDeviceEvent(parsedMessage.getGBDeviceEvent());
- if (parsedMessage instanceof ProtobufMessage) {
- ProtobufMessage protobufMessage = protocolBufferHandler.processIncoming((ProtobufMessage) parsedMessage);
- if (protobufMessage != null) {
- communicator.sendMessage(protobufMessage.getOutgoingMessage());
- communicator.sendMessage(protobufMessage.getAckBytestream());
+ /*
+ the handler elaborates the followup message but might change the status message since it does
+ check the integrity of the incoming message payload. Hence we let the handlers elaborate the
+ incoming message, then we send the status message of the incoming message, then the response
+ and finally we send the followup.
+ */
+
+ GFDIMessage followup = null;
+ for (MessageHandler han : messageHandlers) {
+ followup = han.handle(parsedMessage);
+ if (followup != null) {
+ break;
}
}
- communicator.sendMessage(parsedMessage.getAckBytestream());
+ communicator.sendMessage(parsedMessage.getAckBytestream()); //send status message
- byte[] response = parsedMessage.getOutgoingMessage();
- if (null != response) {
-// LOG.debug("sending response {}", GB.hexdump(response));
- communicator.sendMessage(response);
- }
+ sendOutgoingMessage(parsedMessage); //send reply if any
+
+ sendOutgoingMessage(followup); //send followup message if any
if (parsedMessage instanceof ConfigurationMessage) { //the last forced message exchange
completeInitialization();
}
- if (parsedMessage instanceof ProtobufStatusMessage) {
- ProtobufMessage protobufMessage = protocolBufferHandler.processIncoming((ProtobufStatusMessage) parsedMessage);
- if (protobufMessage != null) {
- communicator.sendMessage(protobufMessage.getOutgoingMessage());
- communicator.sendMessage(protobufMessage.getAckBytestream());
- }
- }
+ processDownloadQueue();
+
+ }
+
+
+ @Override
+ public void onSetCallState(CallSpec callSpec) {
+ LOG.info("INCOMING CALLSPEC: {}", callSpec.command);
+ sendOutgoingMessage(notificationsHandler.onSetCallState(callSpec));
}
@Override
@@ -145,16 +194,40 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
if (weather != null) {
sendWeatherConditions(weather);
}
-
+ } else if (deviceEvent instanceof NotificationSubscriptionDeviceEvent) {
+ final boolean enable = ((NotificationSubscriptionDeviceEvent) deviceEvent).enable;
+ notificationsHandler.setEnabled(enable);
+ LOG.info("NOTIFICATIONS ARE NOW {}", enable ? "ON" : "OFF");
+ } else if (deviceEvent instanceof SupportedFileTypesDeviceEvent) {
+ this.supportedFileTypeList = ((SupportedFileTypesDeviceEvent) deviceEvent).getSupportedFileTypes();
+ sendOutgoingMessage(fileTransferHandler.initiateDownload());
}
+
super.evaluateGBDeviceEvent(deviceEvent);
}
@Override
- public void onSendWeather(final ArrayList weatherSpecs) {
+ public void onNotification(NotificationSpec notificationSpec) {
+ sendOutgoingMessage(notificationsHandler.onNotification(notificationSpec));
+ }
+
+ @Override
+ public void onDeleteNotification(int id) {
+ sendOutgoingMessage(notificationsHandler.onDeleteNotification(id));
+ }
+
+
+ @Override
+ public void onSendWeather(final ArrayList weatherSpecs) { //todo: find the closest one relative to the requested lat/long
sendWeatherConditions(weatherSpecs.get(0));
}
+ private void sendOutgoingMessage(GFDIMessage message) {
+ if (message == null)
+ return;
+ communicator.sendMessage(message.getOutgoingMessage());
+ }
+
private void sendWeatherConditions(WeatherSpec weather) {
List weatherData = new ArrayList<>();
@@ -163,7 +236,7 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
weatherDefinitions.add(LocalMessage.HOURLY_WEATHER_FORECAST.getRecordDefinition());
weatherDefinitions.add(LocalMessage.DAILY_WEATHER_FORECAST.getRecordDefinition());
- communicator.sendMessage(new nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.FitDefinitionMessage(weatherDefinitions).getOutgoingMessage());
+ sendOutgoingMessage(new nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.FitDefinitionMessage(weatherDefinitions));
try {
RecordData today = new RecordData(LocalMessage.TODAY_WEATHER_CONDITIONS.getRecordDefinition());
@@ -230,8 +303,7 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
}
}
- byte[] message = new nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.FitDataMessage(weatherData).getOutgoingMessage();
- communicator.sendMessage(message);
+ sendOutgoingMessage(new nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.FitDataMessage(weatherData));
} catch (Exception e) {
LOG.error(e.getMessage());
}
@@ -239,19 +311,38 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
}
private void completeInitialization() {
-
-
onSetTime();
enableWeather();
//following is needed for vivomove style
- communicator.sendMessage(new SystemEventMessage(SystemEventMessage.GarminSystemEventType.SYNC_READY, 0).getOutgoingMessage());
+ sendOutgoingMessage(new SystemEventMessage(SystemEventMessage.GarminSystemEventType.SYNC_READY, 0));
enableBatteryLevelUpdate();
gbDevice.setState(GBDevice.State.INITIALIZED);
gbDevice.sendDeviceUpdateIntent(getContext());
+ sendOutgoingMessage(new SupportedFileTypesMessage());
+ }
+
+ private void processDownloadQueue() {
+ if (!filesToDownload.isEmpty() && !fileTransferHandler.isDownloading()) {
+ try {
+ FileTransferHandler.DirectoryEntry directoryEntry = filesToDownload.remove();
+ while (checkFileExists(directoryEntry.getFileName())) {
+ directoryEntry = filesToDownload.remove();
+ LOG.debug("File: {} already downloaded, not downloading again.", directoryEntry.getFileName());
+ }
+ DownloadRequestMessage downloadRequestMessage = fileTransferHandler.downloadDirectoryEntry(directoryEntry);
+ if (downloadRequestMessage != null) {
+ sendOutgoingMessage(downloadRequestMessage);
+ } else {
+ LOG.debug("File: {} already downloaded, not downloading again, from inside.", directoryEntry.getFileName());
+ }
+ } catch (NoSuchElementException e) {
+ //ignore
+ }
+ }
}
private void enableBatteryLevelUpdate() {
@@ -263,46 +354,40 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
)
)
.build());
- communicator.sendMessage(batteryLevelProtobufRequest.getOutgoingMessage());
+ sendOutgoingMessage(batteryLevelProtobufRequest);
}
private void enableWeather() {
- final Map settings = new LinkedHashMap<>(1);
+ final Map settings = new LinkedHashMap<>(3);
+ settings.put(SetDeviceSettingsMessage.GarminDeviceSetting.AUTO_UPLOAD_ENABLED, false);
settings.put(SetDeviceSettingsMessage.GarminDeviceSetting.WEATHER_CONDITIONS_ENABLED, true);
- communicator.sendMessage(new SetDeviceSettingsMessage(settings).getOutgoingMessage());
+ settings.put(SetDeviceSettingsMessage.GarminDeviceSetting.WEATHER_ALERTS_ENABLED, false);
+ sendOutgoingMessage(new SetDeviceSettingsMessage(settings));
}
@Override
public void onSetTime() {
- communicator.sendMessage(new SystemEventMessage(SystemEventMessage.GarminSystemEventType.TIME_UPDATED, 0).getOutgoingMessage());
+ sendOutgoingMessage(new SystemEventMessage(SystemEventMessage.GarminSystemEventType.TIME_UPDATED, 0));
}
@Override
public void onFindDevice(boolean start) {
+ final GdiFindMyWatch.FindMyWatchService.Builder a = GdiFindMyWatch.FindMyWatchService.newBuilder();
if (start) {
- final ProtobufMessage findMyWatch = protocolBufferHandler.prepareProtobufRequest(
- GdiSmartProto.Smart.newBuilder()
- .setFindMyWatchService(
- GdiFindMyWatch.FindMyWatchService.newBuilder()
- .setFindRequest(
- GdiFindMyWatch.FindMyWatchService.FindMyWatchRequest.newBuilder()
- .setTimeout(60)
- )
- )
- .build());
- communicator.sendMessage(findMyWatch.getOutgoingMessage());
+ a.setFindRequest(
+ GdiFindMyWatch.FindMyWatchService.FindMyWatchRequest.newBuilder()
+ .setTimeout(60)
+ );
} else {
- final ProtobufMessage cancelFindMyWatch = protocolBufferHandler.prepareProtobufRequest(
- GdiSmartProto.Smart.newBuilder()
- .setFindMyWatchService(
- GdiFindMyWatch.FindMyWatchService.newBuilder()
- .setCancelRequest(
- GdiFindMyWatch.FindMyWatchService.FindMyWatchCancelRequest.newBuilder()
- )
- )
- .build());
- communicator.sendMessage(cancelFindMyWatch.getOutgoingMessage());
+ a.setCancelRequest(
+ GdiFindMyWatch.FindMyWatchService.FindMyWatchCancelRequest.newBuilder()
+ );
}
+ final ProtobufMessage findMyWatch = protocolBufferHandler.prepareProtobufRequest(
+ GdiSmartProto.Smart.newBuilder()
+ .setFindMyWatchService(a).build());
+
+ sendOutgoingMessage(findMyWatch);
}
@Override
@@ -315,18 +400,14 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
attributes.put(MusicControlEntityUpdateMessage.TRACK.TITLE, musicSpec.track);
attributes.put(MusicControlEntityUpdateMessage.TRACK.DURATION, String.valueOf(musicSpec.duration));
- communicator.sendMessage(new MusicControlEntityUpdateMessage(attributes).getOutgoingMessage());
+ sendOutgoingMessage(new MusicControlEntityUpdateMessage(attributes));
}
@Override
public void onSetMusicState(MusicStateSpec stateSpec) {
musicStateSpec = stateSpec;
- if (musicStateTimer != null) {
- musicStateTimer.cancel();
- musicStateTimer.purge();
- musicStateTimer = null;
- }
+ stopMusicTimer();
musicStateTimer = new Timer();
int updatePeriod = 4000; //milliseconds
@@ -343,7 +424,7 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
Map attributes = new HashMap<>();
attributes.put(MusicControlEntityUpdateMessage.PLAYER.PLAYBACK_INFO, StringUtils.join(",", playing, playRate, position).toString());
- communicator.sendMessage(new MusicControlEntityUpdateMessage(attributes).getOutgoingMessage());
+ sendOutgoingMessage(new MusicControlEntityUpdateMessage(attributes));
}
}, 0, updatePeriod);
@@ -354,8 +435,33 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
Map attributes = new HashMap<>();
attributes.put(MusicControlEntityUpdateMessage.PLAYER.PLAYBACK_INFO, StringUtils.join(",", playing, playRate, position).toString());
- communicator.sendMessage(new MusicControlEntityUpdateMessage(attributes).getOutgoingMessage());
+ sendOutgoingMessage(new MusicControlEntityUpdateMessage(attributes));
}
}
+ private boolean checkFileExists(String fileName) {
+ File dir;
+ try {
+ dir = getWritableExportDirectory();
+ File outputFile = new File(dir, fileName);
+ if (outputFile.exists()) //do not download again already downloaded file
+ return true;
+ } catch (IOException e) {
+ LOG.error("IOException: " + e);
+ }
+ return false;
+ }
+
+ public File getWritableExportDirectory() throws IOException {
+ File dir;
+ dir = new File(FileUtils.getExternalFilesDir() + "/" + FileUtils.makeValidFileName(getDevice().getName() + "_" + getDevice().getAddress()));
+ if (!dir.isDirectory()) {
+ if (!dir.mkdir()) {
+ throw new IOException("Cannot create device specific directory for " + getDevice().getName());
+ }
+ }
+ return dir;
+ }
+
+
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/MessageHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/MessageHandler.java
new file mode 100644
index 000000000..191772203
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/MessageHandler.java
@@ -0,0 +1,7 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin;
+
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.GFDIMessage;
+
+public interface MessageHandler {
+ public GFDIMessage handle(GFDIMessage message);
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/NotificationsHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/NotificationsHandler.java
new file mode 100644
index 000000000..cc25b57fb
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/NotificationsHandler.java
@@ -0,0 +1,334 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin;
+
+import android.util.SparseArray;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Queue;
+
+import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.GFDIMessage;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.NotificationControlMessage;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.NotificationDataMessage;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.NotificationUpdateMessage;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.NotificationDataStatusMessage;
+
+public class NotificationsHandler implements MessageHandler {
+ public static final SimpleDateFormat NOTIFICATION_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.ROOT);
+ private static final Logger LOG = LoggerFactory.getLogger(NotificationsHandler.class);
+ private final Queue notificationSpecQueue;
+ private final Upload upload;
+ private boolean enabled = false;
+
+
+ public NotificationsHandler() {
+ this.notificationSpecQueue = new LinkedList<>();
+ this.upload = new Upload();
+ }
+
+ public NotificationUpdateMessage onNotification(NotificationSpec notificationSpec) {
+ if (!enabled)
+ return null;
+ final boolean isUpdate = addNotificationToQueue(notificationSpec);
+
+ NotificationUpdateMessage.NotificationUpdateType notificationUpdateType = isUpdate ? NotificationUpdateMessage.NotificationUpdateType.MODIFY : NotificationUpdateMessage.NotificationUpdateType.ADD;
+
+ if (notificationSpecQueue.size() > 10)
+ notificationSpecQueue.poll(); //remove the oldest notification TODO: should send a delete notification message to watch!
+
+ return new NotificationUpdateMessage(notificationUpdateType, notificationSpec.type, getNotificationsCount(notificationSpec.type), notificationSpec.getId());
+ }
+
+
+ private boolean addNotificationToQueue(NotificationSpec notificationSpec) {
+ boolean found = false;
+ Iterator iterator = notificationSpecQueue.iterator();
+ while (iterator.hasNext()) {
+ NotificationSpec e = iterator.next();
+ if (e.getId() == notificationSpec.getId()) {
+ found = true;
+ iterator.remove();
+ }
+ }
+ notificationSpecQueue.offer(notificationSpec); // Add the notificationSpec to the front of the queue
+ return found;
+ }
+
+ public NotificationUpdateMessage onSetCallState(CallSpec callSpec) {
+ if (!enabled)
+ return null;
+ if (callSpec.command == CallSpec.CALL_INCOMING) {
+ NotificationSpec callNotificationSpec = new NotificationSpec(callSpec.number.hashCode());
+ callNotificationSpec.phoneNumber = callSpec.number;
+ callNotificationSpec.sourceAppId = callSpec.sourceAppId;
+ callNotificationSpec.title = StringUtils.isEmpty(callSpec.name) ? callSpec.number : callSpec.name;
+ callNotificationSpec.type = NotificationType.GENERIC_PHONE;
+ callNotificationSpec.body = StringUtils.isEmpty(callSpec.name) ? callSpec.number : callSpec.name;
+
+ return onNotification(callNotificationSpec);
+ } else {
+ if (callSpec.number != null) // this happens in debug screen
+ return onDeleteNotification(callSpec.number.hashCode());
+ }
+ return null;
+ }
+
+
+ public NotificationUpdateMessage onDeleteNotification(int id) {
+ if (!enabled)
+ return null;
+
+ Iterator iterator = notificationSpecQueue.iterator();
+ while (iterator.hasNext()) {
+ NotificationSpec e = iterator.next();
+ if (e.getId() == id) {
+ iterator.remove();
+ return new NotificationUpdateMessage(NotificationUpdateMessage.NotificationUpdateType.REMOVE, e.type, getNotificationsCount(e.type), id);
+ }
+ }
+ return null;
+ }
+
+ private int getNotificationsCount(NotificationType notificationType) {
+ int count = 0;
+ for (NotificationSpec e : notificationSpecQueue) {
+ count += e.type == notificationType ? 1 : 0;
+ }
+ return count;
+ }
+
+ private NotificationSpec getNotificationSpecFromQueue(int id) {
+ for (NotificationSpec e : notificationSpecQueue) {
+ if (e.getId() == id) {
+ return e;
+ }
+ }
+ return null;
+ }
+
+ public GFDIMessage handle(GFDIMessage message) {
+ if (!enabled)
+ return null;
+ if (message instanceof NotificationControlMessage) {
+ final NotificationSpec notificationSpec = getNotificationSpecFromQueue(((NotificationControlMessage) message).getNotificationId());
+ if (notificationSpec != null) {
+ switch (((NotificationControlMessage) message).getCommand()) {
+ case GET_NOTIFICATION_ATTRIBUTES:
+ final MessageWriter messageWriter = new MessageWriter();
+ messageWriter.writeByte(NotificationCommand.GET_NOTIFICATION_ATTRIBUTES.code);
+ messageWriter.writeInt(((NotificationControlMessage) message).getNotificationId());
+ for (Map.Entry attribute : ((NotificationControlMessage) message).getNotificationAttributesMap().entrySet()) {
+ if (!attribute.getKey().equals(NotificationAttribute.MESSAGE_SIZE)) { //should be last
+ messageWriter.writeByte(attribute.getKey().code);
+ final byte[] bytes = attribute.getKey().getNotificationSpecAttribute(notificationSpec, attribute.getValue());
+ messageWriter.writeShort(bytes.length);
+ messageWriter.writeBytes(bytes);
+ LOG.info("ATTRIBUTE:{} value:{} length:{}", attribute.getKey(), new String(bytes), bytes.length);
+ }
+ }
+ if (((NotificationControlMessage) message).getNotificationAttributesMap().containsKey(NotificationAttribute.MESSAGE_SIZE)) {
+ messageWriter.writeByte(NotificationAttribute.MESSAGE_SIZE.code);
+ final byte[] bytes = NotificationAttribute.MESSAGE_SIZE.getNotificationSpecAttribute(notificationSpec, 0);
+ messageWriter.writeShort(bytes.length);
+ messageWriter.writeBytes(bytes);
+ LOG.info("ATTRIBUTE:{} value:{} length:{}", NotificationAttribute.MESSAGE_SIZE, new String(bytes), bytes.length);
+
+ }
+ NotificationFragment notificationFragment = new NotificationFragment(messageWriter.getBytes());
+ return upload.setCurrentlyUploading(notificationFragment);
+ default:
+ LOG.error("NOT SUPPORTED");
+ }
+ }
+ } else if (message instanceof NotificationDataStatusMessage) {
+ return upload.processUploadProgress((NotificationDataStatusMessage) message);
+ }
+
+ return null;
+ }
+
+
+ public void setEnabled(boolean enable) {
+ this.enabled = enable;
+ }
+
+ public enum NotificationCommand { //was AncsCommand
+ GET_NOTIFICATION_ATTRIBUTES(0),
+ GET_APP_ATTRIBUTES(1),
+ PERFORM_NOTIFICATION_ACTION(2),
+ // Garmin extensions
+ PERFORM_ANDROID_ACTION(128);
+
+ public final int code;
+
+ NotificationCommand(int code) {
+ this.code = code;
+ }
+
+ public static NotificationCommand fromCode(int code) {
+ for (NotificationCommand value : values()) {
+ if (value.code == code)
+ return value;
+ }
+ throw new IllegalArgumentException("Unknown notification command " + code);
+ }
+ }
+
+ public enum NotificationAttribute { //was AncsAttribute
+ APP_IDENTIFIER(0),
+ TITLE(1, true),
+ SUBTITLE(2, true),
+ MESSAGE(3, true),
+ MESSAGE_SIZE(4),
+ DATE(5),
+ // POSITIVE_ACTION_LABEL(6),
+// NEGATIVE_ACTION_LABEL(7),
+ // Garmin extensions
+// PHONE_NUMBER(126, true),
+ ACTIONS(127, false, true),
+ ;
+ private static final SparseArray valueByCode;
+
+ static {
+ final NotificationAttribute[] values = values();
+ valueByCode = new SparseArray<>(values.length);
+ for (NotificationAttribute value : values) {
+ valueByCode.append(value.code, value);
+ }
+ }
+
+ public final int code;
+ public final boolean hasLengthParam;
+ public final boolean hasAdditionalParams;
+
+ NotificationAttribute(int code) {
+ this(code, false, false);
+ }
+
+ NotificationAttribute(int code, boolean hasLengthParam) {
+ this(code, hasLengthParam, false);
+ }
+
+ NotificationAttribute(int code, boolean hasLengthParam, boolean hasAdditionalParams) {
+ this.code = code;
+ this.hasLengthParam = hasLengthParam;
+ this.hasAdditionalParams = hasAdditionalParams;
+ }
+
+ public static NotificationAttribute getByCode(int code) {
+ return valueByCode.get(code);
+ }
+
+ public byte[] getNotificationSpecAttribute(NotificationSpec notificationSpec, int maxLength) {
+ String toReturn = "";
+ switch (this) {
+ case DATE:
+ final long notificationTimestamp = notificationSpec.when == 0 ? System.currentTimeMillis() : notificationSpec.when;
+ toReturn = NOTIFICATION_DATE_FORMAT.format(new Date(notificationTimestamp));
+ break;
+ case TITLE:
+ toReturn = notificationSpec.title == null ? "" : notificationSpec.title;
+ break;
+ case SUBTITLE:
+ toReturn = notificationSpec.subject == null ? "" : notificationSpec.subject;
+ break;
+ case APP_IDENTIFIER:
+ toReturn = notificationSpec.sourceAppId == null ? "" : notificationSpec.sourceAppId;
+ break;
+ case MESSAGE:
+ toReturn = notificationSpec.body == null ? "" : notificationSpec.body;
+ break;
+ case MESSAGE_SIZE:
+ toReturn = Integer.toString(notificationSpec.body == null ? "".length() : notificationSpec.body.length());
+ break;
+ case ACTIONS:
+ toReturn = new String(new byte[]{0x00, 0x00, 0x00, 0x00});
+ break;
+ }
+ return toReturn.substring(0, Math.min(toReturn.length(), maxLength)).getBytes(StandardCharsets.UTF_8);
+ }
+ }
+
+ class Upload {
+
+ private NotificationFragment currentlyUploading;
+
+ public NotificationDataMessage setCurrentlyUploading(NotificationFragment currentlyUploading) {
+ this.currentlyUploading = currentlyUploading;
+ return currentlyUploading.take();
+ }
+
+ private GFDIMessage processUploadProgress(NotificationDataStatusMessage notificationDataStatusMessage) {
+ if (!currentlyUploading.dataHolder.hasRemaining()) {
+ this.currentlyUploading = null;
+ LOG.info("SENT ALL");
+
+ return new NotificationDataStatusMessage(GFDIMessage.GarminMessage.NOTIFICATION_DATA, GFDIMessage.Status.ACK, NotificationDataStatusMessage.TransferStatus.OK);
+ } else {
+ if (notificationDataStatusMessage.canProceed()) {
+ LOG.info("SENDING NEXT CHUNK!!!");
+ return currentlyUploading.take();
+ } else {
+ LOG.warn("Cannot proceed with upload"); //TODO: send the correct status message
+ this.currentlyUploading = null;
+ }
+
+ }
+ return null;
+ }
+
+ }
+
+ class NotificationFragment {
+ private final int dataSize;
+ private final ByteBuffer dataHolder;
+ private final int maxBlockSize = 300;
+ private int runningCrc;
+
+ NotificationFragment(byte[] contents) {
+ this.dataHolder = ByteBuffer.wrap(contents);
+ this.dataSize = contents.length;
+ this.dataHolder.flip();
+ this.dataHolder.compact();
+ this.setRunningCrc(0);
+ }
+
+ public int getDataSize() {
+ return dataSize;
+ }
+
+ private int getMaxBlockSize() {
+ return maxBlockSize;
+ }
+
+ private NotificationDataMessage take() {
+ final int currentOffset = this.dataHolder.position();
+ final byte[] chunk = new byte[Math.min(this.dataHolder.remaining(), getMaxBlockSize())];
+ this.dataHolder.get(chunk);
+ setRunningCrc(ChecksumCalculator.computeCrc(getRunningCrc(), chunk, 0, chunk.length));
+ return new NotificationDataMessage(chunk, getDataSize(), currentOffset, getRunningCrc());
+ }
+
+ private int getRunningCrc() {
+ return runningCrc;
+ }
+
+ private void setRunningCrc(int runningCrc) {
+ this.runningCrc = runningCrc;
+ }
+ }
+
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/ProtocolBufferHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/ProtocolBufferHandler.java
index f03373a4a..f25d39c39 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/ProtocolBufferHandler.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/ProtocolBufferHandler.java
@@ -25,7 +25,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.calendar.CalendarEvent;
import nodomain.freeyourgadget.gadgetbridge.util.calendar.CalendarManager;
-public class ProtocolBufferHandler {
+public class ProtocolBufferHandler implements MessageHandler {
private static final Logger LOG = LoggerFactory.getLogger(ProtocolBufferHandler.class);
private final GarminSupport deviceSupport;
@@ -43,7 +43,16 @@ public class ProtocolBufferHandler {
return lastProtobufRequestId;
}
- ProtobufMessage processIncoming(ProtobufMessage message) {
+ public ProtobufMessage handle(GFDIMessage protobufMessage) {
+ if (protobufMessage instanceof ProtobufMessage) {
+ return processIncoming((ProtobufMessage) protobufMessage);
+ } else if (protobufMessage instanceof ProtobufStatusMessage) {
+ return processIncoming((ProtobufStatusMessage) protobufMessage);
+ }
+ return null;
+ }
+
+ private ProtobufMessage processIncoming(ProtobufMessage message) {
ProtobufFragment protobufFragment = processChunkedMessage(message);
if (protobufFragment.isComplete()) { //message is now complete
@@ -81,7 +90,7 @@ public class ProtocolBufferHandler {
return null;
}
- public ProtobufMessage processIncoming(ProtobufStatusMessage statusMessage) {
+ private ProtobufMessage processIncoming(ProtobufStatusMessage statusMessage) {
LOG.info("Processing protobuf status message #{}@{}: status={}, error={}", statusMessage.getRequestId(), statusMessage.getDataOffset(), statusMessage.getProtobufChunkStatus(), statusMessage.getProtobufStatusCode());
//TODO: check status and react accordingly, right now we blindly proceed to next chunk
if (chunkedFragmentsMap.containsKey(statusMessage.getRequestId()) && statusMessage.isOK()) {
@@ -251,7 +260,7 @@ public class ProtocolBufferHandler {
return new ProtobufMessage(garminMessage, requestId, 0, bytes.length, bytes.length, bytes);
}
- class ProtobufFragment {
+ private class ProtobufFragment {
private final byte[] fragmentBytes;
private final int totalLength;
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/deviceevents/NotificationSubscriptionDeviceEvent.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/deviceevents/NotificationSubscriptionDeviceEvent.java
new file mode 100644
index 000000000..76ecc92d3
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/deviceevents/NotificationSubscriptionDeviceEvent.java
@@ -0,0 +1,9 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents;
+
+import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
+
+public class NotificationSubscriptionDeviceEvent extends GBDeviceEvent {
+
+ public boolean enable;
+
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/deviceevents/SupportedFileTypesDeviceEvent.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/deviceevents/SupportedFileTypesDeviceEvent.java
new file mode 100644
index 000000000..979a49636
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/deviceevents/SupportedFileTypesDeviceEvent.java
@@ -0,0 +1,19 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents;
+
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.FileType;
+
+public class SupportedFileTypesDeviceEvent extends GBDeviceEvent {
+
+ private final List supportedFileTypes;
+
+ public SupportedFileTypesDeviceEvent(List fileTypes) {
+ this.supportedFileTypes = fileTypes;
+ }
+
+ public List getSupportedFileTypes() {
+ return supportedFileTypes;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitFile.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitFile.java
index 01ae3f60a..9fb17ab2e 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitFile.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/fit/FitFile.java
@@ -15,8 +15,8 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.ChecksumCalculator;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminByteBufferReader;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.ChecksumCalculator;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter;
public class FitFile {
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/CreateFileMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/CreateFileMessage.java
new file mode 100644
index 000000000..3db6ae9ac
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/CreateFileMessage.java
@@ -0,0 +1,62 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
+
+import java.util.Random;
+
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.FileType;
+
+public class CreateFileMessage extends GFDIMessage {
+
+ private final int fileSize;
+ private final FileType.FILETYPE filetype;
+ private final boolean generateOutgoing;
+
+ public CreateFileMessage(GarminMessage garminMessage, int fileSize, FileType.FILETYPE filetype) {
+ this.fileSize = fileSize;
+ this.filetype = filetype;
+ this.garminMessage = garminMessage;
+ this.statusMessage = this.getStatusMessage();
+ this.generateOutgoing = false;
+ }
+
+ public CreateFileMessage(int fileSize, FileType.FILETYPE filetype) {
+ this.garminMessage = GarminMessage.CREATE_FILE;
+ this.fileSize = fileSize;
+ this.filetype = filetype;
+ this.statusMessage = this.getStatusMessage();
+ this.generateOutgoing = true;
+ }
+
+ public static CreateFileMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
+
+ final int fileSize = reader.readInt();
+ final int dataType = reader.readByte(); //SupportedFileTypesStatusMessage.FileTypeInfo.type
+ final int subType = reader.readByte();//SupportedFileTypesStatusMessage.FileTypeInfo.subtypetype
+ final FileType.FILETYPE filetype = FileType.FILETYPE.fromDataTypeSubType(dataType, subType);
+ final int fileIndex = reader.readShort(); //???
+ reader.readByte(); //unk
+ final int subTypeMask = reader.readByte(); //???
+ final int numberMask = reader.readShort(); //???
+
+ return new CreateFileMessage(garminMessage, fileSize, filetype);
+ }
+
+ @Override
+ protected boolean generateOutgoing() { //TODO: adjust variables
+ Random random = new Random();
+ final MessageWriter writer = new MessageWriter(response);
+ writer.writeShort(0); // packet size will be filled below
+ writer.writeShort(this.garminMessage.getId());
+ writer.writeInt(this.fileSize);
+ writer.writeByte(this.filetype.getType());
+ writer.writeByte(this.filetype.getSubType());
+ writer.writeShort(0); //fileIndex
+ writer.writeByte(0); //reserved
+ writer.writeByte(0); //subtypemask
+ writer.writeShort(65535); //numbermask
+ writer.writeShort(0); ///???
+ writer.writeLong(random.nextLong());
+
+ return generateOutgoing;
+ }
+
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/DownloadRequestMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/DownloadRequestMessage.java
new file mode 100644
index 000000000..225d1c283
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/DownloadRequestMessage.java
@@ -0,0 +1,45 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
+
+public class DownloadRequestMessage extends GFDIMessage {
+
+ private final int fileIndex;
+ private final REQUEST_TYPE requestType;
+ private final int crcSeed;
+ private final int dataSize;
+
+ private final int dataOffset;
+
+ public DownloadRequestMessage(GarminMessage garminMessage, int fileIndex, int size, REQUEST_TYPE requestType, int crcSeed, int dataSize, int dataOffset) {
+ this.requestType = requestType;
+ this.crcSeed = crcSeed;
+ this.dataSize = dataSize;
+ this.dataOffset = dataOffset;
+ this.garminMessage = garminMessage;
+ this.fileIndex = fileIndex;
+ this.statusMessage = this.getStatusMessage();
+ }
+
+ public DownloadRequestMessage(int fileIndex, int dataSize, REQUEST_TYPE requestType, int crcSeed, int dataOffset) {
+ this(GarminMessage.DOWNLOAD_REQUEST, fileIndex, dataSize, requestType, crcSeed, dataSize, dataOffset);
+ }
+
+ @Override
+ protected boolean generateOutgoing() {
+ final MessageWriter writer = new MessageWriter(response);
+ writer.writeShort(0); // packet size will be filled below
+ writer.writeShort(this.garminMessage.getId());
+ writer.writeShort(this.fileIndex);
+ writer.writeInt(this.dataOffset);
+ writer.writeByte(this.requestType.ordinal());
+ writer.writeShort(this.crcSeed);
+ writer.writeInt(this.dataSize);
+
+ return true;
+ }
+
+ public enum REQUEST_TYPE {
+ CONTINUE,
+ NEW,
+ }
+
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/FileTransferDataMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/FileTransferDataMessage.java
new file mode 100644
index 000000000..2aff933c1
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/FileTransferDataMessage.java
@@ -0,0 +1,60 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
+
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.FileTransferDataStatusMessage;
+
+public class FileTransferDataMessage extends GFDIMessage {
+
+ private final byte[] message;
+ private final int dataOffset;
+ private final boolean sendOutgoing;
+ private final int crc;
+
+ public FileTransferDataMessage(byte[] message, int dataOffset, int crc) {
+ this(message, dataOffset, crc, true);
+ }
+
+ public FileTransferDataMessage(byte[] message, int dataOffset, int crc, boolean sendOutgoing) {
+ this.garminMessage = GarminMessage.FILE_TRANSFER_DATA;
+ this.dataOffset = dataOffset;
+ this.crc = crc;
+ this.message = message;
+
+ this.statusMessage = new FileTransferDataStatusMessage(GarminMessage.FILE_TRANSFER_DATA, Status.ACK, FileTransferDataStatusMessage.TransferStatus.OK, dataOffset);
+ this.sendOutgoing = sendOutgoing;
+ }
+
+ public static FileTransferDataMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
+
+ final int flags = reader.readByte();
+ final int crc = reader.readShort();
+ final int dataOffset = reader.readInt();
+ final byte[] message = reader.readBytes(reader.remaining());
+
+ return new FileTransferDataMessage(message, dataOffset, crc, false);
+ }
+
+ public byte[] getMessage() {
+ return message;
+ }
+
+ public int getCrc() {
+ return crc;
+ }
+
+ public int getDataOffset() {
+ return dataOffset;
+ }
+
+ @Override
+ protected boolean generateOutgoing() {
+ final MessageWriter writer = new MessageWriter(response);
+ writer.writeShort(0); // packet size will be filled below
+ writer.writeShort(garminMessage.getId());
+ writer.writeByte(0); //flags?
+ writer.writeShort(crc);
+ writer.writeInt(dataOffset);
+ writer.writeBytes(message);
+ return sendOutgoing;
+ }
+
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/GFDIMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/GFDIMessage.java
index 1047c6b8e..72b01608c 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/GFDIMessage.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/GFDIMessage.java
@@ -10,6 +10,7 @@ import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.ChecksumCalculator;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminByteBufferReader;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.GFDIStatusMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.GenericStatusMessage;
@@ -17,14 +18,9 @@ import nodomain.freeyourgadget.gadgetbridge.util.GB;
public abstract class GFDIMessage {
public static final int MESSAGE_REQUEST = 5001;
- public static final int MESSAGE_DOWNLOAD_REQUEST = 5002;
- public static final int MESSAGE_UPLOAD_REQUEST = 5003;
- public static final int MESSAGE_FILE_TRANSFER_DATA = 5004;
- public static final int MESSAGE_CREATE_FILE_REQUEST = 5005;
public static final int MESSAGE_DIRECTORY_FILE_FILTER_REQUEST = 5007;
public static final int MESSAGE_FILE_READY = 5009;
public static final int MESSAGE_BATTERY_STATUS = 5023;
- public static final int MESSAGE_SUPPORTED_FILE_TYPES_REQUEST = 5031;
public static final int MESSAGE_NOTIFICATION_SOURCE = 5033;
public static final int MESSAGE_GNCS_CONTROL_POINT_REQUEST = 5034;
public static final int MESSAGE_GNCS_DATA_SOURCE = 5035;
@@ -99,12 +95,21 @@ public abstract class GFDIMessage {
public enum GarminMessage {
RESPONSE(5000, GFDIStatusMessage.class), //TODO: STATUS is a better name?
+ DOWNLOAD_REQUEST(5002, DownloadRequestMessage.class),
+ UPLOAD_REQUEST(5003, UploadRequestMessage.class),
+ FILE_TRANSFER_DATA(5004, FileTransferDataMessage.class),
+ CREATE_FILE(5005, CreateFileMessage.class),
FIT_DEFINITION(5011, FitDefinitionMessage.class),
FIT_DATA(5012, FitDataMessage.class),
WEATHER_REQUEST(5014, WeatherMessage.class),
DEVICE_INFORMATION(5024, DeviceInformationMessage.class),
DEVICE_SETTINGS(5026, SetDeviceSettingsMessage.class),
SYSTEM_EVENT(5030, SystemEventMessage.class),
+ SUPPORTED_FILE_TYPES_REQUEST(5031, SupportedFileTypesMessage.class),
+ NOTIFICATION_UPDATE(5033, NotificationUpdateMessage.class),
+ NOTIFICATION_CONTROL(5034, NotificationControlMessage.class),
+ NOTIFICATION_DATA(5035, NotificationDataMessage.class),
+ NOTIFICATION_SUBSCRIPTION(5036, NotificationSubscriptionMessage.class),
FIND_MY_PHONE(5039, FindMyPhoneRequestMessage.class),
CANCEL_FIND_MY_PHONE(5040, FindMyPhoneRequestMessage.class),
MUSIC_CONTROL(5041, MusicControlMessage.class),
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationControlMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationControlMessage.java
new file mode 100644
index 000000000..338012f51
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationControlMessage.java
@@ -0,0 +1,86 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.NotificationsHandler;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.NotificationControlStatusMessage;
+
+import static nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.NotificationsHandler.NotificationCommand.GET_NOTIFICATION_ATTRIBUTES;
+
+public class NotificationControlMessage extends GFDIMessage {
+
+ private final NotificationsHandler.NotificationCommand command;
+ private final int notificationId;
+ private final Map notificationAttributesMap;
+
+ public NotificationControlMessage(GarminMessage garminMessage, NotificationsHandler.NotificationCommand command, int notificationId, Map notificationAttributesMap) {
+ this.garminMessage = garminMessage;
+ this.command = command;
+ this.notificationId = notificationId;
+ this.notificationAttributesMap = notificationAttributesMap;
+
+ this.statusMessage = new NotificationControlStatusMessage(garminMessage, GFDIMessage.Status.ACK, NotificationControlStatusMessage.NotificationChunkStatus.OK, NotificationControlStatusMessage.NotificationStatusCode.NO_ERROR);
+ }
+
+ public static NotificationControlMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
+
+
+ final NotificationsHandler.NotificationCommand command = NotificationsHandler.NotificationCommand.fromCode(reader.readByte());
+ if (command != GET_NOTIFICATION_ATTRIBUTES) {
+ LOG.error("NOT SUPPORTED");
+
+ }
+ LOG.info("COMMAND: {}", command.ordinal());
+ final int notificationId = reader.readInt();
+ final Map notificationAttributesMap = createGetNotificationAttributesCommand(reader);
+
+ return new NotificationControlMessage(garminMessage, command, notificationId, notificationAttributesMap);
+ }
+
+ private static Map createGetNotificationAttributesCommand(MessageReader reader) {
+ final Map notificationAttributesMap = new HashMap<>();
+ while (reader.remaining() > 0) {
+ final int attributeID = reader.readByte();
+
+ final NotificationsHandler.NotificationAttribute attribute = NotificationsHandler.NotificationAttribute.getByCode(attributeID);
+ LOG.info("Requested attribute: {}", attribute);
+ if (attribute == null) {
+ LOG.error("Unknown notification attribute {}", attributeID);
+ return null;
+ }
+ final int maxLength;
+ if (attribute.hasLengthParam) {
+ maxLength = reader.readShort();
+
+ } else if (attribute.hasAdditionalParams) {
+ maxLength = reader.readShort();
+ // TODO: What is this??
+ reader.readByte();
+
+ } else {
+ maxLength = 0;
+ }
+ notificationAttributesMap.put(attribute, maxLength);
+ }
+ return notificationAttributesMap;
+ }
+
+ public NotificationsHandler.NotificationCommand getCommand() {
+ return command;
+ }
+
+ public int getNotificationId() {
+ return notificationId;
+ }
+
+ public Map getNotificationAttributesMap() {
+ return notificationAttributesMap;
+ }
+
+ @Override
+ protected boolean generateOutgoing() {
+ return false;
+ }
+
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationDataMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationDataMessage.java
new file mode 100644
index 000000000..2c6beea22
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationDataMessage.java
@@ -0,0 +1,39 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
+
+public class NotificationDataMessage extends GFDIMessage {
+ private final byte[] chunk;
+ private final int dataOffset;
+ private final int messageSize;
+ private final boolean sendOutgoing;
+ private final int crc;
+
+ public NotificationDataMessage(byte[] chunk, int messageSize, int dataOffset, int crc) {
+ this(chunk, messageSize, dataOffset, crc, true);
+ }
+
+ public NotificationDataMessage(byte[] chunk, int messageSize, int dataOffset, int crc, boolean sendOutgoing) {
+ this.garminMessage = GarminMessage.NOTIFICATION_DATA;
+ this.dataOffset = dataOffset;
+ this.crc = crc;
+ this.chunk = chunk;
+ this.messageSize = messageSize;
+
+ this.sendOutgoing = sendOutgoing;
+ }
+
+ public int getCrc() {
+ return crc;
+ }
+
+ @Override
+ protected boolean generateOutgoing() {
+ final MessageWriter writer = new MessageWriter(response);
+ writer.writeShort(0); // packet size will be filled below
+ writer.writeShort(garminMessage.getId());
+ writer.writeShort(messageSize);
+ writer.writeShort(crc);
+ writer.writeShort(dataOffset);
+ writer.writeBytes(chunk);
+ return sendOutgoing;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationSubscriptionMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationSubscriptionMessage.java
new file mode 100644
index 000000000..d0b2f1a8b
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationSubscriptionMessage.java
@@ -0,0 +1,38 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
+
+import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.NotificationSubscriptionDeviceEvent;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.NotificationSubscriptionStatusMessage;
+
+public class NotificationSubscriptionMessage extends GFDIMessage {
+
+ private final boolean enable;
+ private final int unk;
+
+ public NotificationSubscriptionMessage(GarminMessage garminMessage, boolean enable, int unk) {
+ this.garminMessage = garminMessage;
+ this.enable = enable;
+ this.unk = unk;
+
+ this.statusMessage = new NotificationSubscriptionStatusMessage(Status.ACK, NotificationSubscriptionStatusMessage.NotificationStatus.OK, enable, unk);
+ }
+
+ public static NotificationSubscriptionMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
+ final boolean enable = reader.readByte() == 1;
+ final int unk = reader.readByte();
+
+ return new NotificationSubscriptionMessage(garminMessage, enable, unk);
+ }
+
+ @Override
+ public GBDeviceEvent getGBDeviceEvent() {
+ NotificationSubscriptionDeviceEvent notificationSubscriptionDeviceEvent = new NotificationSubscriptionDeviceEvent();
+ notificationSubscriptionDeviceEvent.enable = this.enable;
+ return notificationSubscriptionDeviceEvent;
+ }
+
+ @Override
+ protected boolean generateOutgoing() {
+ return false;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationUpdateMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationUpdateMessage.java
new file mode 100644
index 000000000..e61c5dbfe
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationUpdateMessage.java
@@ -0,0 +1,99 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
+
+import org.apache.commons.lang3.EnumUtils;
+
+import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
+
+public class NotificationUpdateMessage extends GFDIMessage {
+
+ final private NotificationUpdateType notificationUpdateType;
+ final private NotificationType notificationType;
+ final private int count; //how many notifications of the same type are present
+ final private int notificationId;
+
+ public NotificationUpdateMessage(NotificationUpdateType notificationUpdateType, NotificationType notificationType, int count, int notificationId) {
+ this.garminMessage = GarminMessage.NOTIFICATION_UPDATE;
+ this.notificationUpdateType = notificationUpdateType;
+ this.notificationType = notificationType;
+ this.count = count;
+ this.notificationId = notificationId;
+ }
+
+ @Override
+ protected boolean generateOutgoing() {
+ final MessageWriter writer = new MessageWriter(response);
+ writer.writeShort(0); // packet size will be filled below
+ writer.writeShort(this.garminMessage.getId());
+ writer.writeByte(this.notificationUpdateType.ordinal());
+ writer.writeByte(getCategoryFlags(this.notificationType));
+ writer.writeByte(getCategoryValue(this.notificationType));
+ writer.writeByte(this.count);
+ writer.writeInt(this.notificationId);
+ writer.writeByte(0); //unk (extra flags)
+
+ return true;
+ }
+
+ private int getCategoryFlags(NotificationType notificationType) {
+ switch (notificationType.getGenericType()) {
+ case "generic_phone":
+ case "generic_email":
+ case "generic_sms":
+ case "generic_chat":
+ return (int) EnumUtils.generateBitVector(NotificationFlag.class, NotificationFlag.FOREGROUND);
+ case "generic_navigation":
+ case "generic_social":
+ case "generic_alarm_clock":
+ case "generic":
+ return (int) EnumUtils.generateBitVector(NotificationFlag.class, NotificationFlag.BACKGROUND);
+ }
+ return 1;
+ }
+
+ private int getCategoryValue(NotificationType notificationType) {
+ switch (notificationType.getGenericType()) {
+ case "generic_phone":
+ return NotificationCategory.INCOMING_CALL.ordinal();
+ case "generic_email":
+ return NotificationCategory.EMAIL.ordinal();
+ case "generic_sms":
+ case "generic_chat":
+ return NotificationCategory.SMS.ordinal();
+ case "generic_navigation":
+ return NotificationCategory.LOCATION.ordinal();
+ case "generic_social":
+ return NotificationCategory.SOCIAL.ordinal();
+ case "generic_alarm_clock":
+ case "generic":
+ return NotificationCategory.OTHER.ordinal();
+ }
+ return NotificationCategory.OTHER.ordinal();
+ }
+
+ public enum NotificationUpdateType {
+ ADD,
+ MODIFY,
+ REMOVE,
+ }
+
+ enum NotificationFlag { //was AncsEventFlag
+ BACKGROUND,
+ FOREGROUND,
+ }
+
+ enum NotificationCategory { //was AncsCategory
+ OTHER,
+ INCOMING_CALL,
+ MISSED_CALL,
+ VOICEMAIL,
+ SOCIAL,
+ SCHEDULE,
+ EMAIL,
+ NEWS,
+ HEALTH_AND_FITNESS,
+ BUSINESS_AND_FINANCE,
+ LOCATION,
+ ENTERTAINMENT,
+ SMS
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/SupportedFileTypesMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/SupportedFileTypesMessage.java
new file mode 100644
index 000000000..2018aeeab
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/SupportedFileTypesMessage.java
@@ -0,0 +1,16 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
+
+public class SupportedFileTypesMessage extends GFDIMessage {
+
+ public SupportedFileTypesMessage() {
+ this.garminMessage = GarminMessage.SUPPORTED_FILE_TYPES_REQUEST;
+ }
+
+ @Override
+ protected boolean generateOutgoing() {
+ final MessageWriter writer = new MessageWriter(response);
+ writer.writeShort(0); // packet size will be filled below
+ writer.writeShort(this.garminMessage.getId());
+ return true;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/UploadRequestMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/UploadRequestMessage.java
new file mode 100644
index 000000000..c10588dc6
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/UploadRequestMessage.java
@@ -0,0 +1,45 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
+
+public class UploadRequestMessage extends GFDIMessage {
+
+ private final int fileIndex;
+ private final int size;
+ private final boolean generateOutgoing;
+ private final int dataOffset;
+ private final int crcSeed;
+
+
+ public UploadRequestMessage(GarminMessage garminMessage, int fileIndex, int size, int dataOffset, int crcSeed) {
+ this.garminMessage = garminMessage;
+ this.fileIndex = fileIndex;
+ this.size = size;
+ this.dataOffset = dataOffset;
+ this.crcSeed = crcSeed;
+ this.statusMessage = this.getStatusMessage();
+ this.generateOutgoing = false;
+ }
+
+ public UploadRequestMessage(int fileIndex, int size) {
+ this.garminMessage = GarminMessage.UPLOAD_REQUEST;
+ this.fileIndex = fileIndex;
+ this.size = size;
+ this.dataOffset = 0;
+ this.crcSeed = 0;
+ this.statusMessage = this.getStatusMessage();
+ this.generateOutgoing = true;
+ }
+
+ @Override
+ protected boolean generateOutgoing() {
+ final MessageWriter writer = new MessageWriter(response);
+ writer.writeShort(0); // packet size will be filled below
+ writer.writeShort(this.garminMessage.getId());
+ writer.writeShort(this.fileIndex);
+ writer.writeInt(this.size);
+ writer.writeInt(this.dataOffset);
+ writer.writeShort(this.crcSeed);
+
+ return generateOutgoing;
+ }
+
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/CreateFileStatusMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/CreateFileStatusMessage.java
new file mode 100644
index 000000000..4062c7c69
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/CreateFileStatusMessage.java
@@ -0,0 +1,73 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status;
+
+
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.FileType;
+
+public class CreateFileStatusMessage extends GFDIStatusMessage {
+ private final Status status;
+ private final CreateStatus createStatus;
+ private final FileType.FILETYPE filetype;
+ private final int fileIndex;
+ private final int fileNumber;
+
+ public CreateFileStatusMessage(GarminMessage garminMessage, Status status, CreateStatus createStatus, int fileIndex, FileType.FILETYPE filetype, int fileNumber) {
+ this.garminMessage = garminMessage;
+ this.status = status;
+ this.createStatus = createStatus;
+ this.fileIndex = fileIndex;
+ this.filetype = filetype;
+ this.fileNumber = fileNumber;
+ }
+
+ public static CreateFileStatusMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
+ final Status status = Status.fromCode(reader.readByte());
+
+ if (!status.equals(Status.ACK)) {
+ return null;
+ }
+ final CreateStatus createStatus = CreateStatus.fromId(reader.readByte());
+ int fileIndex = reader.readShort();
+ final int dataType = reader.readByte();
+ final int subType = reader.readByte();
+ final FileType.FILETYPE filetype = FileType.FILETYPE.fromDataTypeSubType(dataType, subType);
+ final int fileNumber = reader.readShort();
+ if (!createStatus.equals(CreateStatus.OK)) {
+ LOG.warn("Received {} / {} for message {}", status, createStatus, garminMessage);
+ } else {
+ LOG.info("Received {} / {} for message {}", status, createStatus, garminMessage);
+ }
+ return new CreateFileStatusMessage(garminMessage, status, createStatus, fileIndex, filetype, fileNumber);
+ }
+
+ public int getFileIndex() {
+ return fileIndex;
+ }
+
+ public int getFileNumber() {
+ return fileNumber;
+ }
+
+ public boolean canProceed() {
+ return status.equals(Status.ACK) && createStatus.equals(CreateStatus.OK);
+ }
+
+ enum CreateStatus {
+ OK,
+ DUPLICATE,
+ NO_SPACE,
+ UNSUPPORTED,
+ NO_SLOTS,
+ NO_SPACE_FOR_TYPE,
+ ;
+
+ public static CreateStatus fromId(int id) {
+ for (CreateStatus createStatus :
+ CreateStatus.values()) {
+ if (createStatus.ordinal() == id) {
+ return createStatus;
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/DownloadRequestStatusMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/DownloadRequestStatusMessage.java
new file mode 100644
index 000000000..868f49b72
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/DownloadRequestStatusMessage.java
@@ -0,0 +1,61 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status;
+
+
+public class DownloadRequestStatusMessage extends GFDIStatusMessage {
+ private final Status status;
+ private final DownloadStatus downloadStatus;
+ private final int maxFileSize;
+
+ public DownloadRequestStatusMessage(GarminMessage garminMessage, Status status, DownloadStatus downloadStatus, int maxFileSize) {
+ this.garminMessage = garminMessage;
+ this.status = status;
+ this.downloadStatus = downloadStatus;
+ this.maxFileSize = maxFileSize;
+ }
+
+ public static DownloadRequestStatusMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
+ final Status status = Status.fromCode(reader.readByte());
+
+ if (!status.equals(Status.ACK)) {
+ return null;
+ }
+ final DownloadStatus downloadStatus = DownloadStatus.fromId(reader.readByte());
+ final int maxFileSize = reader.readInt();
+
+ if (!downloadStatus.equals(DownloadStatus.OK)) {
+ LOG.warn("Received {} / {} for message {}", status, downloadStatus, garminMessage);
+ } else {
+ LOG.info("Received {} / {} for message {}", status, downloadStatus, garminMessage);
+ }
+ return new DownloadRequestStatusMessage(garminMessage, status, downloadStatus, maxFileSize);
+ }
+
+ public int getMaxFileSize() {
+ return maxFileSize;
+ }
+
+ public boolean canProceed() {
+ return status.equals(Status.ACK) && downloadStatus.equals(DownloadStatus.OK);
+ }
+
+ enum DownloadStatus { //was DownloadRequestResponseMessage
+ OK,
+ INDEX_UNKNOWN,
+ INDEX_NOT_READABLE,
+ NO_SPACE_LEFT,
+ INVALID,
+ NOT_READY,
+ CRC_INCORRECT,
+ ;
+
+ public static DownloadStatus fromId(int id) {
+ for (DownloadStatus downloadStatus :
+ DownloadStatus.values()) {
+ if (downloadStatus.ordinal() == id) {
+ return downloadStatus;
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/FileTransferDataStatusMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/FileTransferDataStatusMessage.java
new file mode 100644
index 000000000..a8907d1b8
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/FileTransferDataStatusMessage.java
@@ -0,0 +1,81 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status;
+
+
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter;
+
+public class FileTransferDataStatusMessage extends GFDIStatusMessage {
+ private final Status status;
+ private final TransferStatus transferStatus;
+ private final int dataOffset;
+ private final boolean sendOutgoing;
+ public FileTransferDataStatusMessage(GarminMessage garminMessage, Status status, TransferStatus transferStatus, int dataOffset) {
+ this(garminMessage, status, transferStatus, dataOffset, true);
+ }
+
+
+ public FileTransferDataStatusMessage(GarminMessage garminMessage, Status status, TransferStatus transferStatus, int dataOffset, boolean sendOutgoing) {
+ this.garminMessage = garminMessage;
+ this.status = status;
+ this.transferStatus = transferStatus;
+ this.dataOffset = dataOffset;
+ this.sendOutgoing = sendOutgoing;
+ }
+
+ public static FileTransferDataStatusMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
+ final Status status = Status.fromCode(reader.readByte());
+
+ if (!status.equals(Status.ACK)) {
+ return null;
+ }
+ final TransferStatus transferStatus = TransferStatus.fromId(reader.readByte());
+ final int dataOffset = reader.readInt();
+
+ if (!transferStatus.equals(TransferStatus.OK)) {
+ LOG.warn("Received {} / {} for message {}", status, transferStatus, garminMessage);
+ } else {
+ LOG.info("Received {} / {} for message {}", status, transferStatus, garminMessage);
+ }
+ return new FileTransferDataStatusMessage(garminMessage, status, transferStatus, dataOffset, false);
+ }
+
+ public int getDataOffset() {
+ return dataOffset;
+ }
+
+ public boolean canProceed() {
+ return status.equals(Status.ACK) && transferStatus.equals(TransferStatus.OK);
+ }
+
+ @Override
+ protected boolean generateOutgoing() {
+ final MessageWriter writer = new MessageWriter(response);
+ writer.writeShort(0); // packet size will be filled below
+ writer.writeShort(GarminMessage.RESPONSE.getId());
+ writer.writeShort(garminMessage.getId());
+ writer.writeByte(status.ordinal());
+ writer.writeByte(transferStatus.ordinal());
+ writer.writeInt(dataOffset);
+
+ return sendOutgoing;
+ }
+
+ public enum TransferStatus {
+ OK,
+ RESEND,
+ ABORT,
+ CRC_MISMATCH,
+ OFFSET_MISMATCH,
+ SYNC_PAUSED,
+ ;
+
+ public static TransferStatus fromId(int id) {
+ for (TransferStatus transferStatus :
+ TransferStatus.values()) {
+ if (transferStatus.ordinal() == id) {
+ return transferStatus;
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/GFDIStatusMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/GFDIStatusMessage.java
index a2fcf7fef..34bfcd5ab 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/GFDIStatusMessage.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/GFDIStatusMessage.java
@@ -11,6 +11,20 @@ public abstract class GFDIStatusMessage extends GFDIMessage {
final GarminMessage originalGarminMessage = GFDIMessage.GarminMessage.fromId(originalMessageType);
if (GarminMessage.PROTOBUF_REQUEST.equals(originalGarminMessage) || GarminMessage.PROTOBUF_RESPONSE.equals(originalGarminMessage)) {
return ProtobufStatusMessage.parseIncoming(reader, originalGarminMessage);
+ } else if (GarminMessage.NOTIFICATION_DATA.equals(originalGarminMessage)) {
+ return NotificationDataStatusMessage.parseIncoming(reader, originalGarminMessage);
+ } else if (GarminMessage.UPLOAD_REQUEST.equals(originalGarminMessage)) {
+ return UploadRequestStatusMessage.parseIncoming(reader, originalGarminMessage);
+ } else if (GarminMessage.DOWNLOAD_REQUEST.equals(originalGarminMessage)) {
+ return DownloadRequestStatusMessage.parseIncoming(reader, originalGarminMessage);
+ } else if (GarminMessage.FILE_TRANSFER_DATA.equals(originalGarminMessage)) {
+ return FileTransferDataStatusMessage.parseIncoming(reader, originalGarminMessage);
+ } else if (GarminMessage.CREATE_FILE.equals(originalGarminMessage)) {
+ return CreateFileStatusMessage.parseIncoming(reader, originalGarminMessage);
+ } else if (GarminMessage.SUPPORTED_FILE_TYPES_REQUEST.equals(originalGarminMessage)) {
+ SupportedFileTypesStatusMessage supportedFileTypesStatusMessage = SupportedFileTypesStatusMessage.parseIncoming(reader, garminMessage);
+ LOG.info(supportedFileTypesStatusMessage.toString());
+ return supportedFileTypesStatusMessage;
} else if (GarminMessage.FIT_DEFINITION.equals(originalGarminMessage)) {
return FitDefinitionStatusMessage.parseIncoming(reader, originalGarminMessage);
} else if (GarminMessage.FIT_DATA.equals(originalGarminMessage)) {
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/GenericStatusMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/GenericStatusMessage.java
index 7ec377773..95dad72f2 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/GenericStatusMessage.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/GenericStatusMessage.java
@@ -4,8 +4,8 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.Mess
public class GenericStatusMessage extends GFDIStatusMessage {
- private int messageType; // for unsupported message types
private final Status status;
+ private int messageType; // for unsupported message types
public GenericStatusMessage(GarminMessage originalMessage, Status status) {
this.garminMessage = originalMessage;
@@ -16,6 +16,7 @@ public class GenericStatusMessage extends GFDIStatusMessage {
this.messageType = messageType;
this.status = status;
}
+
@Override
protected boolean generateOutgoing() {
final MessageWriter writer = new MessageWriter(response);
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/NotificationControlStatusMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/NotificationControlStatusMessage.java
new file mode 100644
index 000000000..bb9e735cd
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/NotificationControlStatusMessage.java
@@ -0,0 +1,76 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status;
+
+import androidx.annotation.Nullable;
+
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter;
+
+public class NotificationControlStatusMessage extends GFDIStatusMessage {
+ private final Status status;
+ private final NotificationChunkStatus notificationChunkStatus;
+ private final NotificationStatusCode notificationStatusCode;
+ private final boolean sendOutgoing;
+
+ public NotificationControlStatusMessage(GarminMessage garminMessage, Status status, NotificationChunkStatus notificationChunkStatus, NotificationStatusCode notificationStatusCode) {
+ this.garminMessage = garminMessage;
+ this.status = status;
+ this.notificationChunkStatus = notificationChunkStatus;
+ this.notificationStatusCode = notificationStatusCode;
+ this.sendOutgoing = true;
+ }
+
+
+ @Override
+ protected boolean generateOutgoing() {
+ final MessageWriter writer = new MessageWriter(response);
+ writer.writeShort(0); // packet size will be filled below
+ writer.writeShort(GarminMessage.RESPONSE.getId());
+ writer.writeShort(garminMessage.getId());
+ writer.writeByte(status.ordinal());
+ writer.writeByte(notificationChunkStatus.ordinal());
+ writer.writeByte(notificationStatusCode.getCode());
+
+ return this.sendOutgoing;
+ }
+
+ public enum NotificationChunkStatus {
+ OK,
+ ERROR,
+ ;
+
+ public static NotificationChunkStatus fromId(int id) {
+ for (NotificationChunkStatus notificationChunkStatus :
+ NotificationChunkStatus.values()) {
+ if (notificationChunkStatus.ordinal() == id) {
+ return notificationChunkStatus;
+ }
+ }
+ return null;
+ }
+ }
+
+ public enum NotificationStatusCode {
+ NO_ERROR(0),
+ UNKNOWN_COMMAND(160),
+ ;
+
+ private final int code;
+
+ NotificationStatusCode(final int code) {
+ this.code = code;
+ }
+
+ @Nullable
+ public static NotificationStatusCode fromCode(final int code) {
+ for (final NotificationStatusCode notificationStatusCode : NotificationStatusCode.values()) {
+ if (notificationStatusCode.getCode() == code) {
+ return notificationStatusCode;
+ }
+ }
+ return null;
+ }
+
+ public int getCode() {
+ return code;
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/NotificationDataStatusMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/NotificationDataStatusMessage.java
new file mode 100644
index 000000000..547362f7b
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/NotificationDataStatusMessage.java
@@ -0,0 +1,57 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status;
+
+
+public class NotificationDataStatusMessage extends GFDIStatusMessage {
+ private final Status status;
+ private final TransferStatus transferStatus;
+
+ public NotificationDataStatusMessage(GarminMessage garminMessage, Status status, TransferStatus transferStatus) {
+ this.garminMessage = garminMessage;
+ this.status = status;
+ this.transferStatus = transferStatus;
+ }
+
+ public static NotificationDataStatusMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
+ final Status status = Status.fromCode(reader.readByte());
+
+ if (!status.equals(Status.ACK)) {
+ return null;
+ }
+ final TransferStatus transferStatus = TransferStatus.fromId(reader.readByte());
+
+ if (!transferStatus.equals(TransferStatus.OK)) {
+ LOG.warn("Received {} / {} for message {}", status, transferStatus, garminMessage);
+ } else {
+ LOG.info("Received {} / {} for message {}", status, transferStatus, garminMessage);
+ }
+ return new NotificationDataStatusMessage(garminMessage, status, transferStatus);
+ }
+
+ public boolean canProceed() {
+ return status.equals(Status.ACK) && transferStatus.equals(TransferStatus.OK);
+ }
+
+ @Override
+ protected boolean generateOutgoing() {
+ return false;
+ }
+
+ public enum TransferStatus {
+ OK,
+ RESEND,
+ ABORT,
+ CRC_MISMATCH,
+ OFFSET_MISMATCH,
+ ;
+
+ public static TransferStatus fromId(int id) {
+ for (TransferStatus transferStatus :
+ TransferStatus.values()) {
+ if (transferStatus.ordinal() == id) {
+ return transferStatus;
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/NotificationSubscriptionStatusMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/NotificationSubscriptionStatusMessage.java
new file mode 100644
index 000000000..ffb78b4f0
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/NotificationSubscriptionStatusMessage.java
@@ -0,0 +1,49 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status;
+
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter;
+
+public class NotificationSubscriptionStatusMessage extends GFDIStatusMessage {
+ private final Status status;
+ private final NotificationStatus notificationStatus;
+ private final int enableRaw;
+ private final int unk;
+ private final boolean sendOutgoing;
+
+ public NotificationSubscriptionStatusMessage(Status status, NotificationStatus notificationStatus, boolean enable, int unk) {
+ this.garminMessage = GarminMessage.NOTIFICATION_SUBSCRIPTION;
+ this.status = status;
+ this.notificationStatus = notificationStatus;
+ this.enableRaw = enable ? 1 : 0;
+ this.unk = unk;
+ this.sendOutgoing = true;
+ }
+
+ @Override
+ protected boolean generateOutgoing() {
+ final MessageWriter writer = new MessageWriter(response);
+ writer.writeShort(0); // packet size will be filled below
+ writer.writeShort(GarminMessage.RESPONSE.getId());
+ writer.writeShort(garminMessage.getId());
+ writer.writeByte(status.ordinal());
+ writer.writeByte(notificationStatus.ordinal());
+ writer.writeByte(this.enableRaw);
+ writer.writeByte(this.unk);
+
+ return this.sendOutgoing;
+ }
+
+ public enum NotificationStatus {
+ OK,
+ ;
+
+ public static NotificationStatus fromId(int id) {
+ for (NotificationStatus notificationStatus :
+ NotificationStatus.values()) {
+ if (notificationStatus.ordinal() == id) {
+ return notificationStatus;
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/ProtobufStatusMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/ProtobufStatusMessage.java
index 92b039e2e..f212c1ed4 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/ProtobufStatusMessage.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/ProtobufStatusMessage.java
@@ -105,6 +105,7 @@ public class ProtobufStatusMessage extends GFDIStatusMessage {
MISSING_PACKET(102),
EXCEEDED_TOTAL_PROTOBUF_LENGTH(103),
PROTOBUF_PARSE_ERROR(200),
+ UNKNOWN(201),
;
private final int code;
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/SupportedFileTypesStatusMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/SupportedFileTypesStatusMessage.java
new file mode 100644
index 000000000..e5316568d
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/SupportedFileTypesStatusMessage.java
@@ -0,0 +1,47 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status;
+
+
+import java.util.ArrayList;
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.FileType;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.SupportedFileTypesDeviceEvent;
+
+public class SupportedFileTypesStatusMessage extends GFDIStatusMessage {
+ private final Status status;
+ private final List fileTypeInfoList;
+
+ public SupportedFileTypesStatusMessage(GarminMessage garminMessage, Status status, List fileTypeInfoList) {
+ this.garminMessage = garminMessage;
+ this.status = status;
+ this.fileTypeInfoList = fileTypeInfoList;
+ }
+
+ public static SupportedFileTypesStatusMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
+ final Status status = Status.fromCode(reader.readByte());
+
+ if (!status.equals(Status.ACK)) {
+ return null;
+ }
+ final int typeCount = reader.readByte();
+ final List types = new ArrayList<>(typeCount);
+ for (int i = 0; i < typeCount; ++i) {
+ final int fileDataType = reader.readByte();
+ final int fileSubType = reader.readByte();
+ final String garminDeviceFileType = reader.readString();
+ FileType fileType = new FileType(fileDataType, fileSubType, garminDeviceFileType);
+ if (fileType.getFileType() == null) {
+ LOG.warn("Watch supports a filetype that we do not support: {}", garminDeviceFileType);
+ continue;
+ }
+ types.add(fileType);
+ }
+
+ return new SupportedFileTypesStatusMessage(garminMessage, status, types);
+ }
+
+ @Override
+ public SupportedFileTypesDeviceEvent getGBDeviceEvent() {
+ return new SupportedFileTypesDeviceEvent(fileTypeInfoList);
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/UploadRequestStatusMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/UploadRequestStatusMessage.java
new file mode 100644
index 000000000..7237b21d1
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/UploadRequestStatusMessage.java
@@ -0,0 +1,71 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status;
+
+
+public class UploadRequestStatusMessage extends GFDIStatusMessage {
+ private final Status status;
+ private final UploadStatus uploadStatus;
+ private final int dataOffset;
+ private final int maxFileSize;
+ private final int crcSeed;
+
+ public UploadRequestStatusMessage(GarminMessage garminMessage, Status status, UploadStatus uploadStatus, int dataOffset, int maxFileSize, int crcSeed) {
+ this.garminMessage = garminMessage;
+ this.status = status;
+ this.uploadStatus = uploadStatus;
+ this.dataOffset = dataOffset;
+ this.maxFileSize = maxFileSize;
+ this.crcSeed = crcSeed;
+ }
+
+ public static UploadRequestStatusMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
+ final Status status = Status.fromCode(reader.readByte());
+
+ if (!status.equals(Status.ACK)) {
+ return null;
+ }
+ final UploadStatus uploadStatus = UploadStatus.fromId(reader.readByte());
+ final int dataOffset = reader.readInt();
+ final int maxFileSize = reader.readInt();
+ final int crcSeed = reader.readShort();
+
+ if (!uploadStatus.equals(UploadStatus.OK)) {
+ LOG.warn("Received {} / {} for message {}", status, uploadStatus, garminMessage);
+ } else {
+ LOG.info("Received {} / {} for message {}", status, uploadStatus, garminMessage);
+ }
+ return new UploadRequestStatusMessage(garminMessage, status, uploadStatus, dataOffset, maxFileSize, crcSeed);
+ }
+
+ public int getDataOffset() {
+ return dataOffset;
+ }
+
+ public int getCrcSeed() {
+ return crcSeed;
+ }
+
+ public boolean canProceed() {
+ return status.equals(Status.ACK) && uploadStatus.equals(UploadStatus.OK);
+ }
+
+ enum UploadStatus {
+ OK,
+ INDEX_UNKNOWN,
+ INDEX_NOT_WRITEABLE,
+ NO_SPACE_LEFT,
+ INVALID,
+ NOT_READY,
+ CRC_INCORRECT,
+ ;
+
+ public static UploadStatus fromId(int id) {
+ for (UploadStatus uploadStatus :
+ UploadStatus.values()) {
+ if (uploadStatus.ordinal() == id) {
+ return uploadStatus;
+ }
+ }
+ return null;
+ }
+ }
+}