1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-07-12 00:14:04 +02:00
Gadgetbridge/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/vivomovehr/VivomoveHrSupport.java
José Rebelo 81aef0bf35 Add support for multiple weather locations
Introduce the concept of primary and secondary weathers:

* Primary weather keeps the same behavior as previously across all weather providers, so it's non-breaking. This location is not necessarily the current location, just the primary weather location set by the user.
* The GenericWeatherReceiver now has a new extra WeatherSecondaryJson, that receives a json list with secondary weather locations.

It's guaranteed that the primary weather always exists, so the list of WeatherSpecs provided to devices is never empty. Update all support classes accordingly.
2024-03-29 21:10:40 +00:00

1075 lines
57 KiB
Java

/* Copyright (C) 2023-2024 Petr Kadlec
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
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/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.os.Build;
import android.widget.Toast;
import com.google.protobuf.InvalidProtocolBufferException;
import de.greenrobot.dao.query.Query;
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.GarminCapability;
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveConstants;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminFitFile;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminFitFileDao;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiCore;
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiDeviceStatus;
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiFindMyWatch;
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiSmartProto;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ams.AmsEntity;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ams.AmsEntityAttribute;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ancs.AncsAttribute;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ancs.AncsAttributeRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ancs.AncsEvent;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ancs.AncsGetNotificationAttributeCommand;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ancs.AncsGetNotificationAttributesResponse;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.ancs.GncsDataSourceQueue;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.downloads.DirectoryData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.downloads.DirectoryEntry;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.downloads.FileDownloadListener;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.downloads.FileDownloadQueue;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.fit.FitBool;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.fit.FitDbImporter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.fit.FitMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.fit.FitMessageDefinitions;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.fit.FitParser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.fit.FitWeatherConditions;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.AuthNegotiationMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.AuthNegotiationResponseMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.BatteryStatusMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.ConfigurationMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.CreateFileResponseMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.CurrentTimeRequestMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.CurrentTimeRequestResponseMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.DeviceInformationMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.DeviceInformationResponseMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.DirectoryFileFilterRequestMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.DirectoryFileFilterResponseMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.DownloadRequestResponseMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.FileTransferDataMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.FileTransferDataResponseMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.FindMyPhoneRequestMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.FitDataMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.FitDataResponseMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.FitDefinitionMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.FitDefinitionResponseMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.GenericResponseMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.GncsControlPointMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.GncsControlPointResponseMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.GncsDataSourceResponseMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.GncsNotificationSourceMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.MusicControlCapabilitiesMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.MusicControlCapabilitiesResponseMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.MusicControlEntityUpdateMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.NotificationServiceSubscriptionMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.NotificationServiceSubscriptionResponseMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.ProtobufRequestMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.ProtobufRequestResponseMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.ResponseMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.SetDeviceSettingsMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.SetDeviceSettingsResponseMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.SupportedFileTypesRequestMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.SupportedFileTypesResponseMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.SyncRequestMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.SystemEventMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.SystemEventResponseMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.UploadRequestResponseMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.WeatherRequestMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.messages.WeatherRequestResponseMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.notifications.NotificationData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.notifications.NotificationStorage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.uploads.FileUploadQueue;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class VivomoveHrSupport extends AbstractBTLEDeviceSupport implements FileDownloadListener {
private static final Logger LOG = LoggerFactory.getLogger(VivomoveHrSupport.class);
// Should all FIT data files be fully stored into the database? (Currently, there is no user-friendly way to
// retrieve them, anyway.)
// TODO: Choose what to do with them. Either store them or remove the field from DB.
private static final boolean STORE_FIT_FILES = false;
private final GfdiPacketParser gfdiPacketParser = new GfdiPacketParser();
private Set<GarminCapability> capabilities;
private int lastProtobufRequestId;
private int maxPacketSize;
private final FitParser fitParser = new FitParser(FitMessageDefinitions.ALL_DEFINITIONS);
private final NotificationStorage notificationStorage = new NotificationStorage();
private VivomoveHrCommunicator communicator;
private RealTimeActivityHandler realTimeActivityHandler;
private GncsDataSourceQueue gncsDataSourceQueue;
private FileDownloadQueue fileDownloadQueue;
private FileUploadQueue fileUploadQueue;
private FitDbImporter fitImporter;
private boolean notificationSubscription;
public VivomoveHrSupport() {
super(LOG);
addSupportedService(VivomoveConstants.UUID_SERVICE_GARMIN_GFDI);
addSupportedService(VivomoveConstants.UUID_SERVICE_GARMIN_REALTIME);
}
private int getNextProtobufRequestId() {
lastProtobufRequestId = (lastProtobufRequestId + 1) % 65536;
return lastProtobufRequestId;
}
@Override
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
LOG.info("Initializing");
gbDevice.setState(GBDevice.State.INITIALIZING);
gbDevice.sendDeviceUpdateIntent(getContext());
communicator = new VivomoveHrCommunicator(this);
realTimeActivityHandler = new RealTimeActivityHandler(this);
builder.setCallback(this);
communicator.start(builder);
fileDownloadQueue = new FileDownloadQueue(communicator, this);
fileUploadQueue = new FileUploadQueue(communicator);
LOG.info("Initialization Done");
// OK, this is not perfect: we should not be INITIALIZED until “connected AND all the necessary initialization
// steps have been performed. At the very least, this means that basic information like device name, firmware
// version, hardware revision (as applicable) is available in the GBDevice”. But we cannot send any message
// until we are INITIALIZED. So what can we do…
gbDevice.setState(GBDevice.State.INITIALIZED);
gbDevice.sendDeviceUpdateIntent(getContext());
sendMessage(new AuthNegotiationMessage(AuthNegotiationMessage.LONG_TERM_KEY_AVAILABILITY_NONE, AuthNegotiationMessage.ENCRYPTION_ALGORITHM_NONE).packet);
return builder;
}
@Override
public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
final UUID characteristicUUID = characteristic.getUuid();
if (super.onCharacteristicChanged(gatt, characteristic)) {
LOG.debug("Change of characteristic {} handled by parent", characteristicUUID);
return true;
}
final byte[] data = characteristic.getValue();
if (data.length == 0) {
LOG.debug("No data received on change of characteristic {}", characteristicUUID);
return true;
}
if (VivomoveConstants.UUID_CHARACTERISTIC_GARMIN_GFDI_RECEIVE.equals(characteristicUUID)) {
handleReceivedGfdiBytes(data);
} else if (realTimeActivityHandler.tryHandleChangedCharacteristic(characteristicUUID, data)) {
// handled by real-time activity handler
} else {
if (LOG.isDebugEnabled()) {
LOG.debug("Unknown characteristic {} changed: {}", characteristicUUID, GB.hexdump(data));
}
}
return true;
}
private void sendMessage(byte[] messageBytes) {
communicator.sendMessage(messageBytes);
}
private void handleReceivedGfdiBytes(byte[] data) {
gfdiPacketParser.receivedBytes(data);
LOG.debug("Received {} GFDI bytes", data.length);
byte[] packet;
while ((packet = gfdiPacketParser.retrievePacket()) != null) {
LOG.debug("Processing a {}B GFDI packet", packet.length);
processGfdiPacket(packet);
}
}
private void processGfdiPacket(byte[] packet) {
final int size = BLETypeConversions.toUint16(packet, 0);
if (size != packet.length) {
LOG.error("Received GFDI packet with invalid length: {} vs {}", size, packet.length);
return;
}
final int crc = BLETypeConversions.toUint16(packet, packet.length - 2);
final int correctCrc = ChecksumCalculator.computeCrc(packet, 0, packet.length - 2);
if (crc != correctCrc) {
LOG.error("Received GFDI packet with invalid CRC: {} vs {}", crc, correctCrc);
return;
}
final int messageType = BLETypeConversions.toUint16(packet, 2);
switch (messageType) {
case VivomoveConstants.MESSAGE_RESPONSE:
processResponseMessage(ResponseMessage.parsePacket(packet), packet);
break;
case VivomoveConstants.MESSAGE_FILE_TRANSFER_DATA:
fileDownloadQueue.onFileTransferData(FileTransferDataMessage.parsePacket(packet));
break;
case VivomoveConstants.MESSAGE_DEVICE_INFORMATION:
processDeviceInformationMessage(DeviceInformationMessage.parsePacket(packet));
break;
case VivomoveConstants.MESSAGE_WEATHER_REQUEST:
processWeatherRequest(WeatherRequestMessage.parsePacket(packet));
break;
case VivomoveConstants.MESSAGE_MUSIC_CONTROL_CAPABILITIES:
processMusicControlCapabilities(MusicControlCapabilitiesMessage.parsePacket(packet));
break;
case VivomoveConstants.MESSAGE_CURRENT_TIME_REQUEST:
processCurrentTimeRequest(CurrentTimeRequestMessage.parsePacket(packet));
break;
case VivomoveConstants.MESSAGE_SYNC_REQUEST:
processSyncRequest(SyncRequestMessage.parsePacket(packet));
break;
case VivomoveConstants.MESSAGE_FIND_MY_PHONE:
processFindMyPhoneRequest(FindMyPhoneRequestMessage.parsePacket(packet));
break;
case VivomoveConstants.MESSAGE_CANCEL_FIND_MY_PHONE:
processCancelFindMyPhoneRequest();
break;
case VivomoveConstants.MESSAGE_NOTIFICATION_SERVICE_SUBSCRIPTION:
processNotificationServiceSubscription(NotificationServiceSubscriptionMessage.parsePacket(packet));
break;
case VivomoveConstants.MESSAGE_GNCS_CONTROL_POINT_REQUEST:
processGncsControlPointRequest(GncsControlPointMessage.parsePacket(packet));
break;
case VivomoveConstants.MESSAGE_CONFIGURATION:
processConfigurationMessage(ConfigurationMessage.parsePacket(packet));
break;
case VivomoveConstants.MESSAGE_PROTOBUF_RESPONSE:
processProtobufResponse(ProtobufRequestMessage.parsePacket(packet));
break;
default:
if (LOG.isInfoEnabled()) {
LOG.info("Unknown message type {}: {}", messageType, GB.hexdump(packet, 0, packet.length));
}
break;
}
}
private void processCancelFindMyPhoneRequest() {
LOG.info("Processing request to cancel find-my-phone");
sendMessage(new GenericResponseMessage(VivomoveConstants.MESSAGE_FIND_MY_PHONE, 0).packet);
final GBDeviceEventFindPhone findPhoneEvent = new GBDeviceEventFindPhone();
findPhoneEvent.event = GBDeviceEventFindPhone.Event.STOP;
evaluateGBDeviceEvent(findPhoneEvent);
}
private void processFindMyPhoneRequest(FindMyPhoneRequestMessage requestMessage) {
LOG.info("Processing find-my-phone request ({} s)", requestMessage.duration);
sendMessage(new GenericResponseMessage(VivomoveConstants.MESSAGE_FIND_MY_PHONE, 0).packet);
final GBDeviceEventFindPhone findPhoneEvent = new GBDeviceEventFindPhone();
findPhoneEvent.event = GBDeviceEventFindPhone.Event.START;
evaluateGBDeviceEvent(findPhoneEvent);
}
private void processGncsControlPointRequest(GncsControlPointMessage requestMessage) {
if (requestMessage == null) {
// TODO: Proper error handling with specific error code
sendMessage(new GncsControlPointResponseMessage(VivomoveConstants.STATUS_ACK, GncsControlPointResponseMessage.RESPONSE_ANCS_ERROR_OCCURRED, GncsControlPointResponseMessage.ANCS_ERROR_UNKNOWN_ANCS_COMMAND).packet);
return;
}
switch (requestMessage.command.command) {
case GET_NOTIFICATION_ATTRIBUTES:
final AncsGetNotificationAttributeCommand getNotificationAttributeCommand = (AncsGetNotificationAttributeCommand) requestMessage.command;
LOG.info("Processing ANCS request to get attributes of notification #{}", getNotificationAttributeCommand.notificationUID);
sendMessage(new GncsControlPointResponseMessage(VivomoveConstants.STATUS_ACK, GncsControlPointResponseMessage.RESPONSE_SUCCESSFUL, GncsControlPointResponseMessage.ANCS_ERROR_NO_ERROR).packet);
final NotificationData notificationData = notificationStorage.retrieveNotification(getNotificationAttributeCommand.notificationUID);
if (notificationData == null) {
LOG.warn("Notification #{} not registered", getNotificationAttributeCommand.notificationUID);
}
final Map<AncsAttribute, String> attributes = new LinkedHashMap<>();
for (final AncsAttributeRequest attributeRequest : getNotificationAttributeCommand.attributes) {
final AncsAttribute attribute = attributeRequest.attribute;
final String attributeValue = notificationData == null ? null : notificationData.getAttribute(attributeRequest.attribute);
final String valueShortened = attributeRequest.maxLength > 0 && attributeValue != null && attributeValue.length() > attributeRequest.maxLength ? attributeValue.substring(0, attributeRequest.maxLength) : attributeValue;
LOG.debug("Requested ANCS attribute {}: '{}'", attribute, valueShortened);
attributes.put(attribute, valueShortened == null ? "" : valueShortened);
}
gncsDataSourceQueue.addToQueue(new AncsGetNotificationAttributesResponse(getNotificationAttributeCommand.notificationUID, attributes).packet);
break;
default:
LOG.error("Unknown GNCS control point command {}", requestMessage.command.command);
sendMessage(new GncsControlPointResponseMessage(VivomoveConstants.STATUS_ACK, GncsControlPointResponseMessage.RESPONSE_ANCS_ERROR_OCCURRED, GncsControlPointResponseMessage.ANCS_ERROR_UNKNOWN_ANCS_COMMAND).packet);
break;
}
}
private void processNotificationServiceSubscription(NotificationServiceSubscriptionMessage requestMessage) {
LOG.info("Processing notification service subscription request message: intent={}, flags={}", requestMessage.intentIndicator, requestMessage.featureFlags);
notificationSubscription = requestMessage.intentIndicator == NotificationServiceSubscriptionMessage.INTENT_SUBSCRIBE;
sendMessage(new NotificationServiceSubscriptionResponseMessage(0, 0, requestMessage.intentIndicator, requestMessage.featureFlags).packet);
}
private void processSyncRequest(SyncRequestMessage requestMessage) {
if (LOG.isInfoEnabled()) {
final StringBuilder requestedTypes = new StringBuilder();
for (GarminMessageType type : requestMessage.fileTypes) {
if (requestedTypes.length() > 0) {
requestedTypes.append(", ");
}
requestedTypes.append(type);
}
LOG.info("Processing sync request message: option={}, types: {}", requestMessage.option, requestedTypes);
}
sendMessage(new GenericResponseMessage(VivomoveConstants.MESSAGE_SYNC_REQUEST, 0).packet);
if (requestMessage.option != SyncRequestMessage.OPTION_INVISIBLE) {
doSync();
}
}
private void processProtobufResponse(ProtobufRequestMessage requestMessage) {
LOG.info("Received protobuf response #{}, {}B@{}/{}: {}", requestMessage.requestId, requestMessage.protobufDataLength, requestMessage.dataOffset, requestMessage.totalProtobufLength, GB.hexdump(requestMessage.messageBytes, 0, requestMessage.messageBytes.length));
sendMessage(new GenericResponseMessage(VivomoveConstants.MESSAGE_PROTOBUF_RESPONSE, 0).packet);
final GdiSmartProto.Smart smart;
try {
smart = GdiSmartProto.Smart.parseFrom(requestMessage.messageBytes);
} catch (InvalidProtocolBufferException e) {
LOG.error("Failed to parse protobuf message ({}): {}", e.getLocalizedMessage(), GB.hexdump(requestMessage.messageBytes, 0, requestMessage.messageBytes.length));
return;
}
boolean processed = false;
if (smart.hasDeviceStatusService()) {
processProtobufDeviceStatusResponse(smart.getDeviceStatusService());
processed = true;
}
if (smart.hasFindMyWatchService()) {
processProtobufFindMyWatchResponse(smart.getFindMyWatchService());
processed = true;
}
if (smart.hasCoreService()) {
processProtobufCoreResponse(smart.getCoreService());
processed = true;
}
if (!processed) {
LOG.warn("Unknown protobuf response: {}", smart);
}
}
private void processProtobufCoreResponse(GdiCore.CoreService coreService) {
if (coreService.hasSyncResponse()) {
final GdiCore.CoreService.SyncResponse syncResponse = coreService.getSyncResponse();
LOG.info("Received sync status: {}", syncResponse.getStatus());
}
LOG.warn("Unknown CoreService response: {}", coreService);
}
private void processProtobufDeviceStatusResponse(GdiDeviceStatus.DeviceStatusService deviceStatusService) {
if (deviceStatusService.hasRemoteDeviceBatteryStatusResponse()) {
final GdiDeviceStatus.DeviceStatusService.RemoteDeviceBatteryStatusResponse batteryStatusResponse = deviceStatusService.getRemoteDeviceBatteryStatusResponse();
final int batteryLevel = batteryStatusResponse.getCurrentBatteryLevel();
LOG.info("Received remote battery status {}: level={}", batteryStatusResponse.getStatus(), batteryLevel);
final GBDeviceEventBatteryInfo batteryEvent = new GBDeviceEventBatteryInfo();
batteryEvent.level = (short) batteryLevel;
handleGBDeviceEvent(batteryEvent);
return;
}
if (deviceStatusService.hasActivityStatusResponse()) {
final GdiDeviceStatus.DeviceStatusService.ActivityStatusResponse activityStatusResponse = deviceStatusService.getActivityStatusResponse();
LOG.info("Received activity status: {}", activityStatusResponse.getStatus());
return;
}
LOG.warn("Unknown DeviceStatusService response: {}", deviceStatusService);
}
private void processProtobufFindMyWatchResponse(GdiFindMyWatch.FindMyWatchService findMyWatchService) {
if (findMyWatchService.hasCancelRequest()) {
LOG.info("Watch search cancelled, watch found");
GBApplication.deviceService().onFindDevice(false);
return;
}
if (findMyWatchService.hasCancelResponse() || findMyWatchService.hasFindResponse()) {
LOG.debug("Received findMyWatch response");
return;
}
LOG.warn("Unknown FindMyWatchService response: {}", findMyWatchService);
}
private void processMusicControlCapabilities(MusicControlCapabilitiesMessage capabilitiesMessage) {
LOG.info("Processing music control capabilities request caps={}", capabilitiesMessage.supportedCapabilities);
sendMessage(new MusicControlCapabilitiesResponseMessage(0, GarminMusicControlCommand.values()).packet);
}
private void processWeatherRequest(WeatherRequestMessage requestMessage) {
LOG.info("Processing weather request fmt={}, {} hrs, {}/{}", requestMessage.format, requestMessage.hoursOfForecast, requestMessage.latitude, requestMessage.longitude);
sendMessage(new WeatherRequestResponseMessage(0, 0, 1, 300).packet);
}
private void processCurrentTimeRequest(CurrentTimeRequestMessage requestMessage) {
long now = System.currentTimeMillis();
final TimeZone timeZone = TimeZone.getDefault();
final Calendar calendar = Calendar.getInstance(timeZone);
calendar.setTimeInMillis(now);
int dstOffset = calendar.get(Calendar.DST_OFFSET) / 1000;
int timeZoneOffset = timeZone.getOffset(now) / 1000;
int garminTimestamp = GarminTimeUtils.javaMillisToGarminTimestamp(now);
LOG.info("Processing current time request #{}: time={}, DST={}, ofs={}", requestMessage.referenceID, garminTimestamp, dstOffset, timeZoneOffset);
sendMessage(new CurrentTimeRequestResponseMessage(0, requestMessage.referenceID, garminTimestamp, timeZoneOffset, dstOffset).packet);
}
private void processResponseMessage(ResponseMessage responseMessage, byte[] packet) {
switch (responseMessage.requestID) {
case VivomoveConstants.MESSAGE_DIRECTORY_FILE_FILTER_REQUEST:
processDirectoryFileFilterResponse(DirectoryFileFilterResponseMessage.parsePacket(packet));
break;
case VivomoveConstants.MESSAGE_DOWNLOAD_REQUEST:
fileDownloadQueue.onDownloadRequestResponse(DownloadRequestResponseMessage.parsePacket(packet));
break;
case VivomoveConstants.MESSAGE_UPLOAD_REQUEST:
fileUploadQueue.onUploadRequestResponse(UploadRequestResponseMessage.parsePacket(packet));
break;
case VivomoveConstants.MESSAGE_FILE_TRANSFER_DATA:
fileUploadQueue.onFileTransferResponse(FileTransferDataResponseMessage.parsePacket(packet));
break;
case VivomoveConstants.MESSAGE_CREATE_FILE_REQUEST:
fileUploadQueue.onCreateFileRequestResponse(CreateFileResponseMessage.parsePacket(packet));
break;
case VivomoveConstants.MESSAGE_FIT_DEFINITION:
processFitDefinitionResponse(FitDefinitionResponseMessage.parsePacket(packet));
break;
case VivomoveConstants.MESSAGE_FIT_DATA:
processFitDataResponse(FitDataResponseMessage.parsePacket(packet));
break;
case VivomoveConstants.MESSAGE_PROTOBUF_REQUEST:
processProtobufRequestResponse(ProtobufRequestResponseMessage.parsePacket(packet));
break;
case VivomoveConstants.MESSAGE_DEVICE_SETTINGS:
processDeviceSettingsResponse(SetDeviceSettingsResponseMessage.parsePacket(packet));
break;
case VivomoveConstants.MESSAGE_SYSTEM_EVENT:
processSystemEventResponse(SystemEventResponseMessage.parsePacket(packet));
break;
case VivomoveConstants.MESSAGE_SUPPORTED_FILE_TYPES_REQUEST:
processSupportedFileTypesResponse(SupportedFileTypesResponseMessage.parsePacket(packet));
break;
case VivomoveConstants.MESSAGE_GNCS_DATA_SOURCE:
gncsDataSourceQueue.responseReceived(GncsDataSourceResponseMessage.parsePacket(packet));
break;
case VivomoveConstants.MESSAGE_AUTH_NEGOTIATION:
processAuthNegotiationRequestResponse(AuthNegotiationResponseMessage.parsePacket(packet));
break;
default:
LOG.info("Received response to message {}: {}", responseMessage.requestID, responseMessage.getStatusStr());
break;
}
}
private void processDirectoryFileFilterResponse(DirectoryFileFilterResponseMessage responseMessage) {
if (responseMessage.status == VivomoveConstants.STATUS_ACK && responseMessage.response == DirectoryFileFilterResponseMessage.RESPONSE_DIRECTORY_FILTER_APPLIED) {
LOG.info("Received response to directory file filter request: {}/{}, requesting download of directory data", responseMessage.status, responseMessage.response);
fileDownloadQueue.addToDownloadQueue(0, 0);
} else {
LOG.error("Directory file filter request failed: {}/{}", responseMessage.status, responseMessage.response);
}
}
private void processSupportedFileTypesResponse(SupportedFileTypesResponseMessage responseMessage) {
final StringBuilder supportedTypes = new StringBuilder();
for (SupportedFileTypesResponseMessage.FileTypeInfo type : responseMessage.fileTypes) {
if (supportedTypes.length() > 0) {
supportedTypes.append(", ");
}
supportedTypes.append(String.format(Locale.ROOT, "%d/%d: %s", type.fileDataType, type.fileSubType, type.garminDeviceFileType));
}
LOG.info("Received the list of supported file types (status={}): {}", responseMessage.status, supportedTypes);
}
private void processDeviceSettingsResponse(SetDeviceSettingsResponseMessage responseMessage) {
LOG.info("Received response to device settings message: status={}, response={}", responseMessage.status, responseMessage.response);
}
private void processAuthNegotiationRequestResponse(AuthNegotiationResponseMessage responseMessage) {
LOG.info("Received response to auth negotiation message: status={}, response={}, LTK={}, algorithms={}", responseMessage.status, responseMessage.response, responseMessage.longTermKeyAvailability, responseMessage.supportedEncryptionAlgorithms);
}
private void processSystemEventResponse(SystemEventResponseMessage responseMessage) {
LOG.info("Received response to system event message: status={}, response={}", responseMessage.status, responseMessage.response);
}
private void processFitDefinitionResponse(FitDefinitionResponseMessage responseMessage) {
LOG.info("Received response to FIT definition message: status={}, FIT response={}", responseMessage.status, responseMessage.fitResponse);
}
private void processFitDataResponse(FitDataResponseMessage responseMessage) {
LOG.info("Received response to FIT data message: status={}, FIT response={}", responseMessage.status, responseMessage.fitResponse);
}
private void processProtobufRequestResponse(ProtobufRequestResponseMessage responseMessage) {
LOG.info("Received response to protobuf message #{}@{}: status={}, error={}", responseMessage.requestId, responseMessage.dataOffset, responseMessage.protobufStatus, responseMessage.error);
}
private void processDeviceInformationMessage(DeviceInformationMessage deviceInformationMessage) {
LOG.info("Received device information: protocol {}, product {}, unit {}, SW {}, max packet {}, BT name {}, device name {}, device model {}", deviceInformationMessage.protocolVersion, deviceInformationMessage.productNumber, deviceInformationMessage.unitNumber, deviceInformationMessage.getSoftwareVersionStr(), deviceInformationMessage.maxPacketSize, deviceInformationMessage.bluetoothFriendlyName, deviceInformationMessage.deviceName, deviceInformationMessage.deviceModel);
this.maxPacketSize = deviceInformationMessage.maxPacketSize;
this.gncsDataSourceQueue = new GncsDataSourceQueue(communicator, maxPacketSize);
final GBDeviceEventVersionInfo deviceEventVersionInfo = new GBDeviceEventVersionInfo();
deviceEventVersionInfo.fwVersion = deviceInformationMessage.getSoftwareVersionStr();
handleGBDeviceEvent(deviceEventVersionInfo);
gbDevice.setState(GBDevice.State.INITIALIZED);
gbDevice.sendDeviceUpdateIntent(getContext());
// prepare and send response
final boolean protocolVersionSupported = deviceInformationMessage.protocolVersion / 100 == 1;
if (!protocolVersionSupported) {
LOG.error("Unsupported protocol version {}", deviceInformationMessage.protocolVersion);
}
final int protocolFlags = protocolVersionSupported ? 1 : 0;
final DeviceInformationResponseMessage deviceInformationResponseMessage = new DeviceInformationResponseMessage(VivomoveConstants.STATUS_ACK, 112, -1, VivomoveConstants.GADGETBRIDGE_UNIT_NUMBER, BuildConfig.VERSION_CODE, 16384, getBluetoothAdapter().getName(), Build.MANUFACTURER, Build.DEVICE, protocolFlags);
sendMessage(deviceInformationResponseMessage.packet);
}
private void processConfigurationMessage(ConfigurationMessage configurationMessage) {
this.capabilities = GarminCapability.setFromBinary(configurationMessage.configurationPayload);
if (LOG.isInfoEnabled()) {
LOG.info("Received configuration message; capabilities: {}", GarminCapability.setToString(capabilities));
}
// prepare and send response
sendMessage(new GenericResponseMessage(VivomoveConstants.MESSAGE_CONFIGURATION, VivomoveConstants.STATUS_ACK).packet);
// and report our own configuration/capabilities
final byte[] ourCapabilityFlags = GarminCapability.setToBinary(VivomoveConstants.OUR_CAPABILITIES);
sendMessage(new ConfigurationMessage(ourCapabilityFlags).packet);
// initialize current time and settings
sendCurrentTime();
sendSettings();
// and everything is ready now
sendSyncReady();
requestBatteryStatusUpdate();
sendFitDefinitions();
sendFitConnectivityMessage();
requestSupportedFileTypes();
}
private void sendProtobufRequest(byte[] protobufMessage) {
final int requestId = getNextProtobufRequestId();
if (LOG.isDebugEnabled()) {
LOG.debug("Sending {}B protobuf request #{}: {}", protobufMessage.length, requestId, GB.hexdump(protobufMessage, 0, protobufMessage.length));
}
sendMessage(new ProtobufRequestMessage(requestId, 0, protobufMessage.length, protobufMessage.length, protobufMessage).packet);
}
private void requestBatteryStatusUpdate() {
sendProtobufRequest(
GdiSmartProto.Smart.newBuilder()
.setDeviceStatusService(
GdiDeviceStatus.DeviceStatusService.newBuilder()
.setRemoteDeviceBatteryStatusRequest(
GdiDeviceStatus.DeviceStatusService.RemoteDeviceBatteryStatusRequest.newBuilder()
)
)
.build()
.toByteArray());
}
private void requestActivityStatus() {
sendProtobufRequest(
GdiSmartProto.Smart.newBuilder()
.setDeviceStatusService(
GdiDeviceStatus.DeviceStatusService.newBuilder()
.setActivityStatusRequest(
GdiDeviceStatus.DeviceStatusService.ActivityStatusRequest.newBuilder()
)
)
.build()
.toByteArray());
}
private void sendRequestSync() {
sendProtobufRequest(
GdiSmartProto.Smart.newBuilder()
.setCoreService(
GdiCore.CoreService.newBuilder()
.setSyncRequest(
GdiCore.CoreService.SyncRequest.newBuilder()
)
)
.build()
.toByteArray());
}
private void requestSupportedFileTypes() {
LOG.info("Requesting list of supported file types");
sendMessage(new SupportedFileTypesRequestMessage().packet);
}
private void sendSyncReady() {
sendMessage(new SystemEventMessage(GarminSystemEventType.SYNC_READY, 0).packet);
}
private void sendCurrentTime() {
final Map<GarminDeviceSetting, Object> settings = new LinkedHashMap<>(3);
long now = System.currentTimeMillis();
final TimeZone timeZone = TimeZone.getDefault();
final Calendar calendar = Calendar.getInstance(timeZone);
calendar.setTimeInMillis(now);
int dstOffset = calendar.get(Calendar.DST_OFFSET) / 1000;
int timeZoneOffset = timeZone.getOffset(now) / 1000;
int garminTimestamp = GarminTimeUtils.javaMillisToGarminTimestamp(now);
settings.put(GarminDeviceSetting.CURRENT_TIME, garminTimestamp);
settings.put(GarminDeviceSetting.DAYLIGHT_SAVINGS_TIME_OFFSET, dstOffset);
settings.put(GarminDeviceSetting.TIME_ZONE_OFFSET, timeZoneOffset);
// TODO: NEXT_DAYLIGHT_SAVINGS_START, NEXT_DAYLIGHT_SAVINGS_END
LOG.info("Setting time to {}, dstOffset={}, tzOffset={} (DST={})", garminTimestamp, dstOffset, timeZoneOffset, timeZone.inDaylightTime(new Date(now)) ? 1 : 0);
sendMessage(new SetDeviceSettingsMessage(settings).packet);
}
private void sendSettings() {
final Map<GarminDeviceSetting, Object> settings = new LinkedHashMap<>(3);
settings.put(GarminDeviceSetting.WEATHER_CONDITIONS_ENABLED, true);
settings.put(GarminDeviceSetting.WEATHER_ALERTS_ENABLED, true);
settings.put(GarminDeviceSetting.AUTO_UPLOAD_ENABLED, true);
LOG.info("Sending settings");
sendMessage(new SetDeviceSettingsMessage(settings).packet);
}
private void sendFitDefinitions() {
sendMessage(new FitDefinitionMessage(
FitMessageDefinitions.DEFINITION_CONNECTIVITY,
FitMessageDefinitions.DEFINITION_WEATHER_CONDITIONS,
FitMessageDefinitions.DEFINITION_WEATHER_ALERT,
FitMessageDefinitions.DEFINITION_DEVICE_SETTINGS
).packet);
}
private void sendFitConnectivityMessage() {
final FitMessage connectivityMessage = new FitMessage(FitMessageDefinitions.DEFINITION_CONNECTIVITY);
connectivityMessage.setField(0, FitBool.TRUE);
connectivityMessage.setField(1, FitBool.TRUE);
connectivityMessage.setField(2, FitBool.INVALID);
connectivityMessage.setField(4, FitBool.TRUE);
connectivityMessage.setField(5, FitBool.TRUE);
connectivityMessage.setField(6, FitBool.TRUE);
connectivityMessage.setField(7, FitBool.TRUE);
connectivityMessage.setField(8, FitBool.TRUE);
connectivityMessage.setField(9, FitBool.TRUE);
connectivityMessage.setField(10, FitBool.TRUE);
connectivityMessage.setField(13, FitBool.TRUE);
sendMessage(new FitDataMessage(connectivityMessage).packet);
}
private void sendWeatherConditions(WeatherSpec weather) {
final FitMessage weatherConditionsMessage = new FitMessage(FitMessageDefinitions.DEFINITION_WEATHER_CONDITIONS);
weatherConditionsMessage.setField(253, GarminTimeUtils.unixTimeToGarminTimestamp(weather.timestamp));
weatherConditionsMessage.setField(0, 0); // 0 = current, 2 = hourly_forecast, 3 = daily_forecast
weatherConditionsMessage.setField(1, weather.currentTemp);
weatherConditionsMessage.setField(2, FitWeatherConditions.openWeatherCodeToFitWeatherStatus(weather.currentConditionCode));
weatherConditionsMessage.setField(3, weather.windDirection);
weatherConditionsMessage.setField(4, Math.round(weather.windSpeed * 1000.0 / 3.6));
weatherConditionsMessage.setField(7, weather.currentHumidity);
weatherConditionsMessage.setField(8, weather.location);
final Calendar timestamp = Calendar.getInstance();
timestamp.setTimeInMillis(weather.timestamp * 1000L);
weatherConditionsMessage.setField(12, timestamp.get(Calendar.DAY_OF_WEEK));
weatherConditionsMessage.setField(13, weather.todayMaxTemp);
weatherConditionsMessage.setField(14, weather.todayMinTemp);
sendMessage(new FitDataMessage(weatherConditionsMessage).packet);
}
/*
private void sendWeatherAlert() {
final FitMessage weatherConditionsMessage = new FitMessage(FitMessageDefinitions.DEFINITION_WEATHER_ALERT);
weatherConditionsMessage.setField(253, GarminTimeUtils.javaMillisToGarminTimestamp(System.currentTimeMillis()));
weatherConditionsMessage.setField(0, "TESTRPT");
final Calendar issue = Calendar.getInstance();
issue.set(2019, 8, 27, 0, 0, 0);
final Calendar expiry = Calendar.getInstance();
issue.set(2019, 8, 29, 0, 0, 0);
weatherConditionsMessage.setField(1, GarminTimeUtils.javaMillisToGarminTimestamp(issue.getTimeInMillis()));
weatherConditionsMessage.setField(2, GarminTimeUtils.javaMillisToGarminTimestamp(expiry.getTimeInMillis()));
weatherConditionsMessage.setField(3, FitWeatherConditions.ALERT_SEVERITY_ADVISORY);
weatherConditionsMessage.setField(4, FitWeatherConditions.ALERT_TYPE_SEVERE_THUNDERSTORM);
sendMessage(new FitDataMessage(weatherConditionsMessage).packet);
}
*/
private void sendNotification(AncsEvent event, NotificationData notification) {
if (event == AncsEvent.NOTIFICATION_ADDED) {
notificationStorage.registerNewNotification(notification);
} else {
notificationStorage.deleteNotification(notification.spec.getId());
}
sendMessage(new GncsNotificationSourceMessage(event, notification.flags, notification.category, notificationStorage.getCountInCategory(notification.category), notification.spec.getId()).packet);
}
private void listFiles(int filterType) {
LOG.info("Requesting file list (filter={})", filterType);
sendMessage(new DirectoryFileFilterRequestMessage(filterType).packet);
}
private void downloadFile(int fileIndex) {
LOG.info("Requesting download of file {}", fileIndex);
fileDownloadQueue.addToDownloadQueue(fileIndex, 0);
}
private void downloadGarminDeviceXml() {
LOG.info("Requesting Garmin device XML download");
fileDownloadQueue.addToDownloadQueue(VivomoveConstants.GARMIN_DEVICE_XML_FILE_INDEX, 0);
}
private void sendBatteryStatus(int batteryLevel) {
LOG.info("Sending battery status");
sendMessage(new BatteryStatusMessage(batteryLevel).packet);
}
private void doSync() {
LOG.info("Starting sync");
fitImporter = new FitDbImporter(getDevice());
// sendMessage(new SystemEventMessage(GarminSystemEventType.PAIR_START, 0).packet);
listFiles(DirectoryFileFilterRequestMessage.FILTER_NO_FILTER);
// TODO: Localization
GB.updateTransferNotification(null, "Downloading list of files", true, 0, getContext());
}
@Override
public boolean useAutoConnect() {
return true;
}
@Override
public void onNotification(NotificationSpec notificationSpec) {
if (notificationSubscription) {
sendNotification(AncsEvent.NOTIFICATION_ADDED, new NotificationData(notificationSpec));
} else {
LOG.debug("No notification subscription is active, ignoring notification");
}
}
@Override
public void onDeleteNotification(int id) {
final NotificationData notificationData = notificationStorage.retrieveNotification(id);
if (notificationData != null) {
sendNotification(AncsEvent.NOTIFICATION_REMOVED, notificationData);
}
}
@Override
public void onSetTime() {
sendCurrentTime();
}
@Override
public void onSetMusicInfo(MusicSpec musicSpec) {
sendMessage(new MusicControlEntityUpdateMessage(
new AmsEntityAttribute(AmsEntity.TRACK, AmsEntityAttribute.TRACK_ATTRIBUTE_ARTIST, 0, musicSpec.artist),
new AmsEntityAttribute(AmsEntity.TRACK, AmsEntityAttribute.TRACK_ATTRIBUTE_ALBUM, 0, musicSpec.album),
new AmsEntityAttribute(AmsEntity.TRACK, AmsEntityAttribute.TRACK_ATTRIBUTE_TITLE, 0, musicSpec.track),
new AmsEntityAttribute(AmsEntity.TRACK, AmsEntityAttribute.TRACK_ATTRIBUTE_DURATION, 0, String.valueOf(musicSpec.duration))
).packet);
}
@Override
public void onEnableRealtimeSteps(boolean enable) {
communicator.enableRealtimeSteps(enable);
}
@Override
public void onFetchRecordedData(int dataTypes) {
doSync();
}
@Override
public void onReset(int flags) {
switch (flags) {
case GBDeviceProtocol.RESET_FLAGS_FACTORY_RESET:
LOG.warn("Requesting factory reset");
sendMessage(new SystemEventMessage(GarminSystemEventType.FACTORY_RESET, 1).packet);
break;
default:
GB.toast(getContext(), "This kind of reset not supported for this device", Toast.LENGTH_LONG, GB.ERROR);
break;
}
}
@Override
public void onEnableRealtimeHeartRateMeasurement(boolean enable) {
communicator.enableRealtimeHeartRate(enable);
}
@Override
public void onFindDevice(boolean start) {
if (start) {
sendProtobufRequest(
GdiSmartProto.Smart.newBuilder()
.setFindMyWatchService(
GdiFindMyWatch.FindMyWatchService.newBuilder()
.setFindRequest(
GdiFindMyWatch.FindMyWatchService.FindMyWatchRequest.newBuilder()
.setTimeout(60)
)
)
.build()
.toByteArray());
} else {
sendProtobufRequest(
GdiSmartProto.Smart.newBuilder()
.setFindMyWatchService(
GdiFindMyWatch.FindMyWatchService.newBuilder()
.setCancelRequest(
GdiFindMyWatch.FindMyWatchService.FindMyWatchCancelRequest.newBuilder()
)
)
.build()
.toByteArray());
}
}
private void updateDeviceSettings() {
final FitMessage deviceSettingsMessage = new FitMessage(FitMessageDefinitions.DEFINITION_DEVICE_SETTINGS);
deviceSettingsMessage.setField("bluetooth_connection_alerts_enabled", 0);
deviceSettingsMessage.setField("auto_lock_enabled", 0);
deviceSettingsMessage.setField("activity_tracker_enabled", 1);
deviceSettingsMessage.setField("alarm_time", 0);
deviceSettingsMessage.setField("ble_auto_upload_enabled", 1);
deviceSettingsMessage.setField("autosync_min_steps", 1000);
deviceSettingsMessage.setField("vibration_intensity", 2);
deviceSettingsMessage.setField("screen_timeout", 0);
deviceSettingsMessage.setField("mounting_side", 1);
deviceSettingsMessage.setField("phone_notification_activity_filter", 0);
deviceSettingsMessage.setField("auto_goal_enabled", 1);
deviceSettingsMessage.setField("autosync_min_time", 60);
deviceSettingsMessage.setField("glance_mode_layout", 0);
deviceSettingsMessage.setField("time_offset", 7200);
deviceSettingsMessage.setField("phone_notification_default_filter", 0);
deviceSettingsMessage.setField("alarm_mode", -1);
deviceSettingsMessage.setField("backlight_timeout", 5);
deviceSettingsMessage.setField("sedentary_hr_alert_threshold", 100);
deviceSettingsMessage.setField("backlight_brightness", 0);
deviceSettingsMessage.setField("time_zone", 254);
deviceSettingsMessage.setField("sedentary_hr_alert_state", 0);
deviceSettingsMessage.setField("auto_activity_start_enabled", 0);
deviceSettingsMessage.setField("alarm_days", 0);
deviceSettingsMessage.setField("default_page", 1);
deviceSettingsMessage.setField("message_tones_enabled", 2);
deviceSettingsMessage.setField("key_tones_enabled", 2);
deviceSettingsMessage.setField("date_mode", 0);
deviceSettingsMessage.setField("backlight_gesture", 1);
deviceSettingsMessage.setField("backlight_mode", 3);
deviceSettingsMessage.setField("move_alert_enabled", 1);
deviceSettingsMessage.setField("sleep_do_not_disturb_enabled", 0);
deviceSettingsMessage.setField("display_orientation", 2);
deviceSettingsMessage.setField("time_mode", 1);
deviceSettingsMessage.setField("pages_enabled", 127);
deviceSettingsMessage.setField("smart_notification_display_orientation", 0);
deviceSettingsMessage.setField("display_steps_goal_enabled", 1);
sendMessage(new FitDataMessage(deviceSettingsMessage).packet);
}
private boolean foreground;
@Override
public void onSendWeather(ArrayList<WeatherSpec> weatherSpecs) {
sendWeatherConditions(weatherSpecs.get(0));
}
private final Map<Integer, DirectoryEntry> filesToDownload = new ConcurrentHashMap<>();
private long totalDownloadSize;
private long lastTransferNotificationTimestamp;
private GarminFitFile findDownloadedFitFile(DaoSession session, Device device, User user, int fileNumber, int fileDataType, int fileSubType) {
final GarminFitFileDao fileDao = session.getGarminFitFileDao();
final Query<GarminFitFile> query = fileDao.queryBuilder()
.where(
GarminFitFileDao.Properties.DeviceId.eq(device.getId()),
GarminFitFileDao.Properties.UserId.eq(user.getId()),
GarminFitFileDao.Properties.FileNumber.eq(fileNumber),
GarminFitFileDao.Properties.FileDataType.eq(fileDataType),
GarminFitFileDao.Properties.FileSubType.eq(fileSubType)
)
.build();
final List<GarminFitFile> files = query.list();
return files.size() > 0 ? files.get(0) : null;
}
@Override
public void onDirectoryDownloaded(DirectoryData directoryData) {
if (filesToDownload.size() != 0) {
throw new IllegalStateException("File download already in progress!");
}
long totalSize = 0;
try {
try (final DBHandler dbHandler = GBApplication.acquireDB()) {
final DaoSession session = dbHandler.getDaoSession();
final GBDevice gbDevice = getDevice();
final Device device = DBHelper.getDevice(gbDevice, session);
final User user = DBHelper.getUser(session);
for (final DirectoryEntry entry : directoryData.entries) {
LOG.info("File #{}: type {}/{} #{}, {}B, flags {}/{}, timestamp {}", entry.fileIndex, entry.fileDataType, entry.fileSubType, entry.fileNumber, entry.fileSize, entry.specificFlags, entry.fileFlags, entry.fileDate);
if (entry.fileIndex == 0) {
// ?
LOG.warn("File #0 reported?");
continue;
}
final long timestamp = entry.fileDate.getTime();
final GarminFitFile alreadyDownloadedFile = findDownloadedFitFile(session, device, user, entry.fileNumber, entry.fileDataType, entry.fileSubType);
if (alreadyDownloadedFile == null) {
LOG.debug("File not yet downloaded");
} else {
if (alreadyDownloadedFile.getFileTimestamp() == timestamp && alreadyDownloadedFile.getFileSize() == entry.fileSize) {
LOG.debug("File already downloaded, skipping");
continue;
} else {
LOG.info("File #{} modified after previous download, removing previous version and re-downloading", entry.fileIndex);
alreadyDownloadedFile.delete();
}
}
filesToDownload.put(entry.fileIndex, entry);
fileDownloadQueue.addToDownloadQueue(entry.fileIndex, entry.fileSize);
totalSize += entry.fileSize;
}
}
} catch (Exception e) {
LOG.error("Error storing data to DB", e);
}
totalDownloadSize = totalSize;
}
@Override
public void onFileDownloadComplete(int fileIndex, byte[] data) {
LOG.info("Downloaded file {}: {} bytes", fileIndex, data.length);
final DirectoryEntry downloadedDirectoryEntry = filesToDownload.get(fileIndex);
if (downloadedDirectoryEntry == null) {
LOG.warn("Unexpected file {} downloaded", fileIndex);
} else {
try (final DBHandler dbHandler = GBApplication.acquireDB()) {
final DaoSession session = dbHandler.getDaoSession();
final GBDevice gbDevice = getDevice();
final Device device = DBHelper.getDevice(gbDevice, session);
final User user = DBHelper.getUser(session);
final int ts = (int) (System.currentTimeMillis() / 1000);
final GarminFitFile garminFitFile = new GarminFitFile(null, ts, device.getId(), user.getId(), downloadedDirectoryEntry.fileNumber, downloadedDirectoryEntry.fileDataType, downloadedDirectoryEntry.fileSubType, downloadedDirectoryEntry.fileDate.getTime(), downloadedDirectoryEntry.specificFlags, downloadedDirectoryEntry.fileSize, STORE_FIT_FILES ? data : null);
session.getGarminFitFileDao().insert(garminFitFile);
} catch (Exception e) {
LOG.error("Error saving downloaded file to database", e);
}
}
if (fileIndex <= 0x8000) {
fitImporter.processFitFile(fitParser.parseFitFile(data));
} else {
LOG.debug("Not importing file {} as FIT", fileIndex);
}
}
@Override
public void onFileDownloadError(int fileIndex) {
LOG.error("Failed to download file {}", fileIndex);
}
@Override
public void onDownloadProgress(long remainingBytes) {
LOG.debug("{}B/{} remaining to download", remainingBytes, totalDownloadSize);
if (remainingBytes == 0) {
GB.updateTransferNotification(null, null, false, 100, getContext());
} else if (totalDownloadSize > 0) {
final long now = System.currentTimeMillis();
if (now - lastTransferNotificationTimestamp < 1000) {
// do not issue updates too often
return;
}
lastTransferNotificationTimestamp = now;
// TODO: Localization
GB.updateTransferNotification(null, "Downloading data", true, Math.round(100.0f * (totalDownloadSize - remainingBytes) / totalDownloadSize), getContext());
}
}
@Override
public void onAllDownloadsCompleted() {
LOG.info("All downloads completed");
GB.updateTransferNotification(null, null, false, 100, getContext());
sendMessage(new SystemEventMessage(GarminSystemEventType.SYNC_COMPLETE, 0).packet);
if (fitImporter != null) {
fitImporter.processData();
GB.signalActivityDataFinish();
}
fitImporter = null;
}
}