From b3da377b343227687a0b19f576b49f0e1a01080f Mon Sep 17 00:00:00 2001 From: Daniele Gobbetti Date: Fri, 12 Apr 2024 18:14:18 +0200 Subject: [PATCH] Garmin protocol: basic file transfer and notification handling adds synchronization of supported files from watch to external directory adds support for Activity and Monitoring files (workouts and activity samples), but those are not integrated yet adds upload functionality (not used ATM and not tested) adds notification support without actions introduces centralized processing of "messageHandlers" (protobuf, file transfer, notifications) also properly dispose of the music timer when disconnecting --- .../{messages => }/ChecksumCalculator.java | 2 +- .../devices/garmin/FileTransferHandler.java | 337 ++++++++++++++++++ .../service/devices/garmin/FileType.java | 79 ++++ .../service/devices/garmin/GarminSupport.java | 226 ++++++++---- .../devices/garmin/MessageHandler.java | 7 + .../devices/garmin/NotificationsHandler.java | 334 +++++++++++++++++ .../devices/garmin/ProtocolBufferHandler.java | 17 +- .../NotificationSubscriptionDeviceEvent.java | 9 + .../SupportedFileTypesDeviceEvent.java | 19 + .../service/devices/garmin/fit/FitFile.java | 2 +- .../garmin/messages/CreateFileMessage.java | 62 ++++ .../messages/DownloadRequestMessage.java | 45 +++ .../messages/FileTransferDataMessage.java | 60 ++++ .../devices/garmin/messages/GFDIMessage.java | 15 +- .../messages/NotificationControlMessage.java | 86 +++++ .../messages/NotificationDataMessage.java | 39 ++ .../NotificationSubscriptionMessage.java | 38 ++ .../messages/NotificationUpdateMessage.java | 99 +++++ .../messages/SupportedFileTypesMessage.java | 16 + .../garmin/messages/UploadRequestMessage.java | 45 +++ .../status/CreateFileStatusMessage.java | 73 ++++ .../status/DownloadRequestStatusMessage.java | 61 ++++ .../status/FileTransferDataStatusMessage.java | 81 +++++ .../messages/status/GFDIStatusMessage.java | 14 + .../messages/status/GenericStatusMessage.java | 3 +- .../NotificationControlStatusMessage.java | 76 ++++ .../status/NotificationDataStatusMessage.java | 57 +++ ...NotificationSubscriptionStatusMessage.java | 49 +++ .../status/ProtobufStatusMessage.java | 1 + .../SupportedFileTypesStatusMessage.java | 47 +++ .../status/UploadRequestStatusMessage.java | 71 ++++ 31 files changed, 1998 insertions(+), 72 deletions(-) rename app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/{messages => }/ChecksumCalculator.java (99%) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/FileTransferHandler.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/FileType.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/MessageHandler.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/NotificationsHandler.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/deviceevents/NotificationSubscriptionDeviceEvent.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/deviceevents/SupportedFileTypesDeviceEvent.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/CreateFileMessage.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/DownloadRequestMessage.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/FileTransferDataMessage.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationControlMessage.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationDataMessage.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationSubscriptionMessage.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationUpdateMessage.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/SupportedFileTypesMessage.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/UploadRequestMessage.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/CreateFileStatusMessage.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/DownloadRequestStatusMessage.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/FileTransferDataStatusMessage.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/NotificationControlStatusMessage.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/NotificationDataStatusMessage.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/NotificationSubscriptionStatusMessage.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/SupportedFileTypesStatusMessage.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/UploadRequestStatusMessage.java 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; + } + } +}