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; + } + } +}