1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-11-28 12:56:49 +01:00

Garmin protocol: basic file transfer and notification handling

adds synchronization of supported files from watch to external directory
adds support for Activity and Monitoring files (workouts and activity samples), but those are not integrated yet
adds upload functionality (not used ATM and not tested)
adds notification support without actions
introduces centralized processing of "messageHandlers" (protobuf, file transfer, notifications)

also properly dispose of the music timer when disconnecting
This commit is contained in:
Daniele Gobbetti 2024-04-12 18:14:18 +02:00
parent 9dee71df6f
commit e6365638d4
31 changed files with 1998 additions and 72 deletions

View File

@ -14,7 +14,7 @@
You should have received a copy of the GNU Affero General Public License You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */ along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages; package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;

View File

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

View File

@ -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<Integer, Integer> type;
FILETYPE(Pair<Integer, Integer> 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;
}
}
}

View File

@ -6,20 +6,27 @@ import android.bluetooth.BluetoothGattCharacteristic;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.text.DecimalFormat; import java.text.DecimalFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Queue;
import java.util.Timer; import java.util.Timer;
import java.util.TimerTask; import java.util.TimerTask;
import java.util.UUID; import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.Weather; import nodomain.freeyourgadget.gadgetbridge.model.Weather;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiDeviceStatus; 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.ICommunicator;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.communicator.v1.CommunicatorV1; 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.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.deviceevents.WeatherRequestDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.LocalMessage; 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.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition; 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.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.GFDIMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MusicControlEntityUpdateMessage; 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.ProtobufMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.SetDeviceSettingsMessage; 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.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; import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommunicator.Callback { public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommunicator.Callback {
private static final Logger LOG = LoggerFactory.getLogger(GarminSupport.class); private static final Logger LOG = LoggerFactory.getLogger(GarminSupport.class);
private final ProtocolBufferHandler protocolBufferHandler; private final ProtocolBufferHandler protocolBufferHandler;
private final NotificationsHandler notificationsHandler;
private final FileTransferHandler fileTransferHandler;
private final Queue<FileTransferHandler.DirectoryEntry> filesToDownload;
private final List<MessageHandler> messageHandlers;
private ICommunicator communicator; private ICommunicator communicator;
private MusicStateSpec musicStateSpec; private MusicStateSpec musicStateSpec;
private Timer musicStateTimer; private Timer musicStateTimer;
private List<FileType> supportedFileTypeList;
public GarminSupport() { public GarminSupport() {
super(LOG); super(LOG);
addSupportedService(CommunicatorV1.UUID_SERVICE_GARMIN_GFDI); addSupportedService(CommunicatorV1.UUID_SERVICE_GARMIN_GFDI);
addSupportedService(CommunicatorV2.UUID_SERVICE_GARMIN_ML_GFDI); addSupportedService(CommunicatorV2.UUID_SERVICE_GARMIN_ML_GFDI);
protocolBufferHandler = new ProtocolBufferHandler(this); 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 @Override
@ -109,33 +151,40 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
evaluateGBDeviceEvent(parsedMessage.getGBDeviceEvent()); evaluateGBDeviceEvent(parsedMessage.getGBDeviceEvent());
if (parsedMessage instanceof ProtobufMessage) { /*
ProtobufMessage protobufMessage = protocolBufferHandler.processIncoming((ProtobufMessage) parsedMessage); the handler elaborates the followup message but might change the status message since it does
if (protobufMessage != null) { check the integrity of the incoming message payload. Hence we let the handlers elaborate the
communicator.sendMessage(protobufMessage.getOutgoingMessage()); incoming message, then we send the status message of the incoming message, then the response
communicator.sendMessage(protobufMessage.getAckBytestream()); 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(); sendOutgoingMessage(parsedMessage); //send reply if any
if (null != response) {
// LOG.debug("sending response {}", GB.hexdump(response)); sendOutgoingMessage(followup); //send followup message if any
communicator.sendMessage(response);
}
if (parsedMessage instanceof ConfigurationMessage) { //the last forced message exchange if (parsedMessage instanceof ConfigurationMessage) { //the last forced message exchange
completeInitialization(); completeInitialization();
} }
if (parsedMessage instanceof ProtobufStatusMessage) { processDownloadQueue();
ProtobufMessage protobufMessage = protocolBufferHandler.processIncoming((ProtobufStatusMessage) parsedMessage);
if (protobufMessage != null) {
communicator.sendMessage(protobufMessage.getOutgoingMessage());
communicator.sendMessage(protobufMessage.getAckBytestream());
}
} }
@Override
public void onSetCallState(CallSpec callSpec) {
LOG.info("INCOMING CALLSPEC: {}", callSpec.command);
sendOutgoingMessage(notificationsHandler.onSetCallState(callSpec));
} }
@Override @Override
@ -145,16 +194,40 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
if (weather != null) { if (weather != null) {
sendWeatherConditions(weather); 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); super.evaluateGBDeviceEvent(deviceEvent);
} }
@Override @Override
public void onSendWeather(final ArrayList<WeatherSpec> 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<WeatherSpec> weatherSpecs) { //todo: find the closest one relative to the requested lat/long
sendWeatherConditions(weatherSpecs.get(0)); sendWeatherConditions(weatherSpecs.get(0));
} }
private void sendOutgoingMessage(GFDIMessage message) {
if (message == null)
return;
communicator.sendMessage(message.getOutgoingMessage());
}
private void sendWeatherConditions(WeatherSpec weather) { private void sendWeatherConditions(WeatherSpec weather) {
List<RecordData> weatherData = new ArrayList<>(); List<RecordData> weatherData = new ArrayList<>();
@ -163,7 +236,7 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
weatherDefinitions.add(LocalMessage.HOURLY_WEATHER_FORECAST.getRecordDefinition()); weatherDefinitions.add(LocalMessage.HOURLY_WEATHER_FORECAST.getRecordDefinition());
weatherDefinitions.add(LocalMessage.DAILY_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 { try {
RecordData today = new RecordData(LocalMessage.TODAY_WEATHER_CONDITIONS.getRecordDefinition()); 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(); sendOutgoingMessage(new nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.FitDataMessage(weatherData));
communicator.sendMessage(message);
} catch (Exception e) { } catch (Exception e) {
LOG.error(e.getMessage()); LOG.error(e.getMessage());
} }
@ -239,19 +311,38 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
} }
private void completeInitialization() { private void completeInitialization() {
onSetTime(); onSetTime();
enableWeather(); enableWeather();
//following is needed for vivomove style //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(); enableBatteryLevelUpdate();
gbDevice.setState(GBDevice.State.INITIALIZED); gbDevice.setState(GBDevice.State.INITIALIZED);
gbDevice.sendDeviceUpdateIntent(getContext()); 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() { private void enableBatteryLevelUpdate() {
@ -263,46 +354,40 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
) )
) )
.build()); .build());
communicator.sendMessage(batteryLevelProtobufRequest.getOutgoingMessage()); sendOutgoingMessage(batteryLevelProtobufRequest);
} }
private void enableWeather() { private void enableWeather() {
final Map<SetDeviceSettingsMessage.GarminDeviceSetting, Object> settings = new LinkedHashMap<>(1); final Map<SetDeviceSettingsMessage.GarminDeviceSetting, Object> settings = new LinkedHashMap<>(3);
settings.put(SetDeviceSettingsMessage.GarminDeviceSetting.AUTO_UPLOAD_ENABLED, false);
settings.put(SetDeviceSettingsMessage.GarminDeviceSetting.WEATHER_CONDITIONS_ENABLED, true); 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 @Override
public void onSetTime() { public void onSetTime() {
communicator.sendMessage(new SystemEventMessage(SystemEventMessage.GarminSystemEventType.TIME_UPDATED, 0).getOutgoingMessage()); sendOutgoingMessage(new SystemEventMessage(SystemEventMessage.GarminSystemEventType.TIME_UPDATED, 0));
} }
@Override @Override
public void onFindDevice(boolean start) { public void onFindDevice(boolean start) {
final GdiFindMyWatch.FindMyWatchService.Builder a = GdiFindMyWatch.FindMyWatchService.newBuilder();
if (start) { if (start) {
final ProtobufMessage findMyWatch = protocolBufferHandler.prepareProtobufRequest( a.setFindRequest(
GdiSmartProto.Smart.newBuilder()
.setFindMyWatchService(
GdiFindMyWatch.FindMyWatchService.newBuilder()
.setFindRequest(
GdiFindMyWatch.FindMyWatchService.FindMyWatchRequest.newBuilder() GdiFindMyWatch.FindMyWatchService.FindMyWatchRequest.newBuilder()
.setTimeout(60) .setTimeout(60)
) );
)
.build());
communicator.sendMessage(findMyWatch.getOutgoingMessage());
} else { } else {
final ProtobufMessage cancelFindMyWatch = protocolBufferHandler.prepareProtobufRequest( a.setCancelRequest(
GdiSmartProto.Smart.newBuilder()
.setFindMyWatchService(
GdiFindMyWatch.FindMyWatchService.newBuilder()
.setCancelRequest(
GdiFindMyWatch.FindMyWatchService.FindMyWatchCancelRequest.newBuilder() GdiFindMyWatch.FindMyWatchService.FindMyWatchCancelRequest.newBuilder()
) );
)
.build());
communicator.sendMessage(cancelFindMyWatch.getOutgoingMessage());
} }
final ProtobufMessage findMyWatch = protocolBufferHandler.prepareProtobufRequest(
GdiSmartProto.Smart.newBuilder()
.setFindMyWatchService(a).build());
sendOutgoingMessage(findMyWatch);
} }
@Override @Override
@ -315,18 +400,14 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
attributes.put(MusicControlEntityUpdateMessage.TRACK.TITLE, musicSpec.track); attributes.put(MusicControlEntityUpdateMessage.TRACK.TITLE, musicSpec.track);
attributes.put(MusicControlEntityUpdateMessage.TRACK.DURATION, String.valueOf(musicSpec.duration)); attributes.put(MusicControlEntityUpdateMessage.TRACK.DURATION, String.valueOf(musicSpec.duration));
communicator.sendMessage(new MusicControlEntityUpdateMessage(attributes).getOutgoingMessage()); sendOutgoingMessage(new MusicControlEntityUpdateMessage(attributes));
} }
@Override @Override
public void onSetMusicState(MusicStateSpec stateSpec) { public void onSetMusicState(MusicStateSpec stateSpec) {
musicStateSpec = stateSpec; musicStateSpec = stateSpec;
if (musicStateTimer != null) { stopMusicTimer();
musicStateTimer.cancel();
musicStateTimer.purge();
musicStateTimer = null;
}
musicStateTimer = new Timer(); musicStateTimer = new Timer();
int updatePeriod = 4000; //milliseconds int updatePeriod = 4000; //milliseconds
@ -343,7 +424,7 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
Map<MusicControlEntityUpdateMessage.MusicEntity, String> attributes = new HashMap<>(); Map<MusicControlEntityUpdateMessage.MusicEntity, String> attributes = new HashMap<>();
attributes.put(MusicControlEntityUpdateMessage.PLAYER.PLAYBACK_INFO, StringUtils.join(",", playing, playRate, position).toString()); attributes.put(MusicControlEntityUpdateMessage.PLAYER.PLAYBACK_INFO, StringUtils.join(",", playing, playRate, position).toString());
communicator.sendMessage(new MusicControlEntityUpdateMessage(attributes).getOutgoingMessage()); sendOutgoingMessage(new MusicControlEntityUpdateMessage(attributes));
} }
}, 0, updatePeriod); }, 0, updatePeriod);
@ -354,8 +435,33 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
Map<MusicControlEntityUpdateMessage.MusicEntity, String> attributes = new HashMap<>(); Map<MusicControlEntityUpdateMessage.MusicEntity, String> attributes = new HashMap<>();
attributes.put(MusicControlEntityUpdateMessage.PLAYER.PLAYBACK_INFO, StringUtils.join(",", playing, playRate, position).toString()); 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;
}
} }

View File

@ -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);
}

View File

@ -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<NotificationSpec> 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<NotificationSpec> 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<NotificationSpec> 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<NotificationAttribute, Integer> 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<NotificationAttribute> 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;
}
}
}

View File

@ -25,7 +25,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.calendar.CalendarEvent; import nodomain.freeyourgadget.gadgetbridge.util.calendar.CalendarEvent;
import nodomain.freeyourgadget.gadgetbridge.util.calendar.CalendarManager; 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 static final Logger LOG = LoggerFactory.getLogger(ProtocolBufferHandler.class);
private final GarminSupport deviceSupport; private final GarminSupport deviceSupport;
@ -43,7 +43,16 @@ public class ProtocolBufferHandler {
return lastProtobufRequestId; 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); ProtobufFragment protobufFragment = processChunkedMessage(message);
if (protobufFragment.isComplete()) { //message is now complete if (protobufFragment.isComplete()) { //message is now complete
@ -81,7 +90,7 @@ public class ProtocolBufferHandler {
return null; 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()); 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 //TODO: check status and react accordingly, right now we blindly proceed to next chunk
if (chunkedFragmentsMap.containsKey(statusMessage.getRequestId()) && statusMessage.isOK()) { 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); return new ProtobufMessage(garminMessage, requestId, 0, bytes.length, bytes.length, bytes);
} }
class ProtobufFragment { private class ProtobufFragment {
private final byte[] fragmentBytes; private final byte[] fragmentBytes;
private final int totalLength; private final int totalLength;

View File

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

View File

@ -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<FileType> supportedFileTypes;
public SupportedFileTypesDeviceEvent(List<FileType> fileTypes) {
this.supportedFileTypes = fileTypes;
}
public List<FileType> getSupportedFileTypes() {
return supportedFileTypes;
}
}

View File

@ -15,8 +15,8 @@ import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; 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.GarminByteBufferReader;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.ChecksumCalculator;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter;
public class FitFile { public class FitFile {

View File

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

View File

@ -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,
}
}

View File

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

View File

@ -10,6 +10,7 @@ import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; 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.GarminByteBufferReader;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.GFDIStatusMessage; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.GFDIStatusMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.GenericStatusMessage; 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 abstract class GFDIMessage {
public static final int MESSAGE_REQUEST = 5001; 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_DIRECTORY_FILE_FILTER_REQUEST = 5007;
public static final int MESSAGE_FILE_READY = 5009; public static final int MESSAGE_FILE_READY = 5009;
public static final int MESSAGE_BATTERY_STATUS = 5023; 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_NOTIFICATION_SOURCE = 5033;
public static final int MESSAGE_GNCS_CONTROL_POINT_REQUEST = 5034; public static final int MESSAGE_GNCS_CONTROL_POINT_REQUEST = 5034;
public static final int MESSAGE_GNCS_DATA_SOURCE = 5035; public static final int MESSAGE_GNCS_DATA_SOURCE = 5035;
@ -99,12 +95,21 @@ public abstract class GFDIMessage {
public enum GarminMessage { public enum GarminMessage {
RESPONSE(5000, GFDIStatusMessage.class), //TODO: STATUS is a better name? 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_DEFINITION(5011, FitDefinitionMessage.class),
FIT_DATA(5012, FitDataMessage.class), FIT_DATA(5012, FitDataMessage.class),
WEATHER_REQUEST(5014, WeatherMessage.class), WEATHER_REQUEST(5014, WeatherMessage.class),
DEVICE_INFORMATION(5024, DeviceInformationMessage.class), DEVICE_INFORMATION(5024, DeviceInformationMessage.class),
DEVICE_SETTINGS(5026, SetDeviceSettingsMessage.class), DEVICE_SETTINGS(5026, SetDeviceSettingsMessage.class),
SYSTEM_EVENT(5030, SystemEventMessage.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), FIND_MY_PHONE(5039, FindMyPhoneRequestMessage.class),
CANCEL_FIND_MY_PHONE(5040, FindMyPhoneRequestMessage.class), CANCEL_FIND_MY_PHONE(5040, FindMyPhoneRequestMessage.class),
MUSIC_CONTROL(5041, MusicControlMessage.class), MUSIC_CONTROL(5041, MusicControlMessage.class),

View File

@ -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<NotificationsHandler.NotificationAttribute, Integer> notificationAttributesMap;
public NotificationControlMessage(GarminMessage garminMessage, NotificationsHandler.NotificationCommand command, int notificationId, Map<NotificationsHandler.NotificationAttribute, Integer> 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<NotificationsHandler.NotificationAttribute, Integer> notificationAttributesMap = createGetNotificationAttributesCommand(reader);
return new NotificationControlMessage(garminMessage, command, notificationId, notificationAttributesMap);
}
private static Map<NotificationsHandler.NotificationAttribute, Integer> createGetNotificationAttributesCommand(MessageReader reader) {
final Map<NotificationsHandler.NotificationAttribute, Integer> 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<NotificationsHandler.NotificationAttribute, Integer> getNotificationAttributesMap() {
return notificationAttributesMap;
}
@Override
protected boolean generateOutgoing() {
return false;
}
}

View File

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

View File

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

View File

@ -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
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,6 +11,20 @@ public abstract class GFDIStatusMessage extends GFDIMessage {
final GarminMessage originalGarminMessage = GFDIMessage.GarminMessage.fromId(originalMessageType); final GarminMessage originalGarminMessage = GFDIMessage.GarminMessage.fromId(originalMessageType);
if (GarminMessage.PROTOBUF_REQUEST.equals(originalGarminMessage) || GarminMessage.PROTOBUF_RESPONSE.equals(originalGarminMessage)) { if (GarminMessage.PROTOBUF_REQUEST.equals(originalGarminMessage) || GarminMessage.PROTOBUF_RESPONSE.equals(originalGarminMessage)) {
return ProtobufStatusMessage.parseIncoming(reader, 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)) { } else if (GarminMessage.FIT_DEFINITION.equals(originalGarminMessage)) {
return FitDefinitionStatusMessage.parseIncoming(reader, originalGarminMessage); return FitDefinitionStatusMessage.parseIncoming(reader, originalGarminMessage);
} else if (GarminMessage.FIT_DATA.equals(originalGarminMessage)) { } else if (GarminMessage.FIT_DATA.equals(originalGarminMessage)) {

View File

@ -4,8 +4,8 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.Mess
public class GenericStatusMessage extends GFDIStatusMessage { public class GenericStatusMessage extends GFDIStatusMessage {
private int messageType; // for unsupported message types
private final Status status; private final Status status;
private int messageType; // for unsupported message types
public GenericStatusMessage(GarminMessage originalMessage, Status status) { public GenericStatusMessage(GarminMessage originalMessage, Status status) {
this.garminMessage = originalMessage; this.garminMessage = originalMessage;
@ -16,6 +16,7 @@ public class GenericStatusMessage extends GFDIStatusMessage {
this.messageType = messageType; this.messageType = messageType;
this.status = status; this.status = status;
} }
@Override @Override
protected boolean generateOutgoing() { protected boolean generateOutgoing() {
final MessageWriter writer = new MessageWriter(response); final MessageWriter writer = new MessageWriter(response);

View File

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

View File

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

View File

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

View File

@ -105,6 +105,7 @@ public class ProtobufStatusMessage extends GFDIStatusMessage {
MISSING_PACKET(102), MISSING_PACKET(102),
EXCEEDED_TOTAL_PROTOBUF_LENGTH(103), EXCEEDED_TOTAL_PROTOBUF_LENGTH(103),
PROTOBUF_PARSE_ERROR(200), PROTOBUF_PARSE_ERROR(200),
UNKNOWN(201),
; ;
private final int code; private final int code;

View File

@ -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<FileType> fileTypeInfoList;
public SupportedFileTypesStatusMessage(GarminMessage garminMessage, Status status, List<FileType> 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<FileType> 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);
}
}

View File

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