Gadgetbridge/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/ProtocolBufferHandler.java

330 lines
17 KiB
Java

package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin;
import android.location.Location;
import com.google.protobuf.InvalidProtocolBufferException;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiCalendarService;
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.GdiHttpService;
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiSmartProto;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.http.HttpHandler;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.GFDIMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.ProtobufMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.ProtobufStatusMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.CurrentPosition;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.calendar.CalendarEvent;
import nodomain.freeyourgadget.gadgetbridge.util.calendar.CalendarManager;
public class ProtocolBufferHandler implements MessageHandler {
private static final Logger LOG = LoggerFactory.getLogger(ProtocolBufferHandler.class);
private final GarminSupport deviceSupport;
private final Map<Integer, ProtobufFragment> chunkedFragmentsMap;
private final int maxChunkSize = 375; //tested on VĂ­vomove Style
private int lastProtobufRequestId;
public ProtocolBufferHandler(GarminSupport deviceSupport) {
this.deviceSupport = deviceSupport;
chunkedFragmentsMap = new HashMap<>();
}
private int getNextProtobufRequestId() {
lastProtobufRequestId = (lastProtobufRequestId + 1) % 65536;
return lastProtobufRequestId;
}
public ProtobufMessage handle(GFDIMessage protobufMessage) {
if (protobufMessage instanceof ProtobufMessage) {
return processIncoming((ProtobufMessage) protobufMessage);
} else if (protobufMessage instanceof ProtobufStatusMessage) {
return processIncoming((ProtobufStatusMessage) protobufMessage);
}
return null;
}
private ProtobufMessage processIncoming(ProtobufMessage message) {
ProtobufFragment protobufFragment = processChunkedMessage(message);
if (protobufFragment.isComplete()) { //message is now complete
LOG.info("Received protobuf message #{}, {}B: {}", message.getRequestId(), protobufFragment.totalLength, GB.hexdump(protobufFragment.fragmentBytes, 0, protobufFragment.totalLength));
final GdiSmartProto.Smart smart;
try {
smart = GdiSmartProto.Smart.parseFrom(protobufFragment.fragmentBytes);
} catch (InvalidProtocolBufferException e) {
LOG.error("Failed to parse protobuf message ({}): {}", e.getLocalizedMessage(), GB.hexdump(protobufFragment.fragmentBytes));
return null;
}
boolean processed = false;
if (smart.hasCoreService()) { //TODO: unify request and response???
return prepareProtobufResponse(processProtobufCoreRequest(smart.getCoreService()), message.getRequestId());
}
if (smart.hasCalendarService()) {
return prepareProtobufResponse(processProtobufCalendarRequest(smart.getCalendarService()), message.getRequestId());
}
if (smart.hasHttpService()) {
final GdiHttpService.HttpService response = HttpHandler.handle(smart.getHttpService());
if (response == null) {
return null;
}
return prepareProtobufResponse(GdiSmartProto.Smart.newBuilder().setHttpService(response).build(), message.getRequestId());
}
if (smart.hasDeviceStatusService()) {
processed = true;
processProtobufDeviceStatusResponse(smart.getDeviceStatusService());
}
if (smart.hasFindMyWatchService()) {
processed = true;
processProtobufFindMyWatchResponse(smart.getFindMyWatchService());
}
if (!processed) {
LOG.warn("Unknown protobuf request: {}", smart);
message.setStatusMessage(new ProtobufStatusMessage(message.getMessageType(), GFDIMessage.Status.ACK, message.getRequestId(), message.getDataOffset(), ProtobufStatusMessage.ProtobufChunkStatus.DISCARDED, ProtobufStatusMessage.ProtobufStatusCode.UNKNOWN_REQUEST_ID));
}
}
return null;
}
private ProtobufMessage processIncoming(ProtobufStatusMessage statusMessage) {
LOG.info("Processing protobuf status message #{}@{}: status={}, error={}", statusMessage.getRequestId(), statusMessage.getDataOffset(), statusMessage.getProtobufChunkStatus(), statusMessage.getProtobufStatusCode());
//TODO: check status and react accordingly, right now we blindly proceed to next chunk
if (chunkedFragmentsMap.containsKey(statusMessage.getRequestId()) && statusMessage.isOK()) {
final ProtobufFragment protobufFragment = chunkedFragmentsMap.get(statusMessage.getRequestId());
LOG.debug("Protobuf message #{} found in queue: {}", statusMessage.getRequestId(), GB.hexdump(protobufFragment.fragmentBytes));
if (protobufFragment.totalLength <= (statusMessage.getDataOffset() + maxChunkSize)) {
chunkedFragmentsMap.remove(protobufFragment);
}
return protobufFragment.getNextChunk(statusMessage);
}
return null;
}
private ProtobufFragment processChunkedMessage(ProtobufMessage message) {
if (message.isComplete()) //comment this out if for any reason also smaller messages should end up in the map
return new ProtobufFragment(message.getMessageBytes());
if (message.getDataOffset() == 0) { //store new messages beginning at 0, overwrite old messages
chunkedFragmentsMap.put(message.getRequestId(), new ProtobufFragment(message));
LOG.info("Protobuf request put in queue: #{} , {}", message.getRequestId(), GB.hexdump(message.getMessageBytes()));
} else {
if (chunkedFragmentsMap.containsKey(message.getRequestId())) {
ProtobufFragment oldFragment = chunkedFragmentsMap.get(message.getRequestId());
chunkedFragmentsMap.put(message.getRequestId(),
new ProtobufFragment(oldFragment, message));
}
}
return chunkedFragmentsMap.get(message.getRequestId());
}
private GdiSmartProto.Smart processProtobufCalendarRequest(GdiCalendarService.CalendarService calendarService) {
if (calendarService.hasCalendarRequest()) {
GdiCalendarService.CalendarService.CalendarServiceRequest calendarServiceRequest = calendarService.getCalendarRequest();
CalendarManager upcomingEvents = new CalendarManager(deviceSupport.getContext(), deviceSupport.getDevice().getAddress());
List<CalendarEvent> mEvents = upcomingEvents.getCalendarEventList();
List<GdiCalendarService.CalendarService.CalendarEvent> watchEvents = new ArrayList<>();
for (CalendarEvent mEvt : mEvents) {
if (mEvt.getEndSeconds() < calendarServiceRequest.getBegin() ||
mEvt.getBeginSeconds() > calendarServiceRequest.getEnd()) {
LOG.debug("CalendarService Skipping event {} that is out of requested time range", mEvt.getTitle());
continue;
}
watchEvents.add(GdiCalendarService.CalendarService.CalendarEvent.newBuilder()
.setTitle(mEvt.getTitle())
.setAllDay(mEvt.isAllDay())
.setBegin(mEvt.getBeginSeconds())
.setEnd(mEvt.getEndSeconds())
.setLocation(StringUtils.defaultString(mEvt.getLocation()))
.setDescription(StringUtils.defaultString(mEvt.getDescription()))
.build()
);
}
LOG.debug("CalendarService Sending {} events to watch", watchEvents.size());
return GdiSmartProto.Smart.newBuilder().setCalendarService(
GdiCalendarService.CalendarService.newBuilder().setCalendarResponse(
GdiCalendarService.CalendarService.CalendarServiceResponse.newBuilder()
.addAllCalendarEvent(watchEvents)
.setUnknown(1)
)
).build();
}
LOG.warn("Unknown CalendarService request: {}", calendarService);
return GdiSmartProto.Smart.newBuilder().setCalendarService(
GdiCalendarService.CalendarService.newBuilder().setCalendarResponse(
GdiCalendarService.CalendarService.CalendarServiceResponse.newBuilder()
.setUnknown(0)
)
).build();
}
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;
deviceSupport.evaluateGBDeviceEvent(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 GdiSmartProto.Smart processProtobufCoreRequest(GdiCore.CoreService coreService) {
if (coreService.hasSyncResponse()) {
final GdiCore.CoreService.SyncResponse syncResponse = coreService.getSyncResponse();
LOG.info("Received sync status: {}", syncResponse.getStatus());
return null;
}
if (coreService.hasGetLocationRequest()) {
final Location location = new CurrentPosition().getLastKnownLocation();
final GdiCore.CoreService.GetLocationResponse.Builder response = GdiCore.CoreService.GetLocationResponse.newBuilder();
if (location.getLatitude() == 0 && location.getLongitude() == 0) {
response.setStatus(GdiCore.CoreService.GetLocationResponse.Status.NO_VALID_LOCATION);
} else {
response.setStatus(GdiCore.CoreService.GetLocationResponse.Status.OK)
.setLocationData(GarminUtils.toLocationData(location, GdiCore.CoreService.DataType.GENERAL_LOCATION));
}
return GdiSmartProto.Smart.newBuilder().setCoreService(
GdiCore.CoreService.newBuilder().setGetLocationResponse(response)).build();
}
if (coreService.hasLocationUpdatedSetEnabledRequest()) {
LOG.debug("Location CoreService: {}", coreService);
final GdiCore.CoreService.LocationUpdatedSetEnabledRequest locationUpdatedSetEnabledRequest = coreService.getLocationUpdatedSetEnabledRequest();
LOG.info("Received locationUpdatedSetEnabledRequest status: {}", locationUpdatedSetEnabledRequest.getEnabled());
GdiCore.CoreService.LocationUpdatedSetEnabledResponse.Builder response = GdiCore.CoreService.LocationUpdatedSetEnabledResponse.newBuilder();
final boolean sendGpsPref = deviceSupport.getDevicePrefs().getBoolean(DeviceSettingsPreferenceConst.PREF_WORKOUT_SEND_GPS_TO_BAND, false);
if (locationUpdatedSetEnabledRequest.getEnabled() && sendGpsPref) {
response.setStatus(GdiCore.CoreService.LocationUpdatedSetEnabledResponse.Status.OK)
.addRequests(GdiCore.CoreService.LocationUpdatedSetEnabledResponse.Requested.newBuilder()
.setRequested(locationUpdatedSetEnabledRequest.getRequests(0).getRequested())
.setStatus(GdiCore.CoreService.LocationUpdatedSetEnabledResponse.Requested.RequestedStatus.OK));
} else {
response.setStatus(GdiCore.CoreService.LocationUpdatedSetEnabledResponse.Status.UNAVAILABLE);
}
deviceSupport.processLocationUpdateRequest(locationUpdatedSetEnabledRequest.getEnabled(), locationUpdatedSetEnabledRequest.getRequestsList());
return GdiSmartProto.Smart.newBuilder().setCoreService(
GdiCore.CoreService.newBuilder().setLocationUpdatedSetEnabledResponse(response)).build();
}
LOG.warn("Unknown CoreService request: {}", coreService);
return null;
}
private void processProtobufFindMyWatchResponse(GdiFindMyWatch.FindMyWatchService findMyWatchService) {
if (findMyWatchService.hasCancelRequest()) {
LOG.info("Watch found");
}
if (findMyWatchService.hasCancelResponse() || findMyWatchService.hasFindResponse()) {
LOG.debug("Received findMyWatch response");
}
LOG.warn("Unknown FindMyWatchService response: {}", findMyWatchService);
}
public ProtobufMessage prepareProtobufRequest(GdiSmartProto.Smart protobufPayload) {
if (null == protobufPayload)
return null;
final int requestId = getNextProtobufRequestId();
return prepareProtobufMessage(protobufPayload.toByteArray(), GFDIMessage.GarminMessage.PROTOBUF_REQUEST, requestId);
}
private ProtobufMessage prepareProtobufResponse(GdiSmartProto.Smart protobufPayload, int requestId) {
if (null == protobufPayload)
return null;
return prepareProtobufMessage(protobufPayload.toByteArray(), GFDIMessage.GarminMessage.PROTOBUF_RESPONSE, requestId);
}
private ProtobufMessage prepareProtobufMessage(byte[] bytes, GFDIMessage.GarminMessage garminMessage, int requestId) {
if (bytes == null || bytes.length == 0)
return null;
LOG.info("Preparing protobuf message. Type{}, #{}, {}B: {}", garminMessage, requestId, bytes.length, GB.hexdump(bytes, 0, bytes.length));
if (bytes.length > maxChunkSize) {
chunkedFragmentsMap.put(requestId, new ProtobufFragment(bytes));
return new ProtobufMessage(garminMessage,
requestId,
0,
bytes.length,
maxChunkSize,
ArrayUtils.subarray(bytes, 0, maxChunkSize));
}
return new ProtobufMessage(garminMessage, requestId, 0, bytes.length, bytes.length, bytes);
}
private class ProtobufFragment {
private final byte[] fragmentBytes;
private final int totalLength;
public ProtobufFragment(byte[] fragmentBytes) {
this.fragmentBytes = fragmentBytes;
this.totalLength = fragmentBytes.length;
}
public ProtobufFragment(ProtobufMessage message) {
if (message.getDataOffset() != 0)
throw new IllegalArgumentException("Cannot create fragment if message is not the first of the sequence");
this.fragmentBytes = message.getMessageBytes();
this.totalLength = message.getTotalProtobufLength();
}
public ProtobufFragment(ProtobufFragment existing, ProtobufMessage toMerge) {
if (toMerge.getDataOffset() != existing.fragmentBytes.length)
throw new IllegalArgumentException("Cannot merge fragment: incoming message has different offset than needed");
this.fragmentBytes = ArrayUtils.addAll(existing.fragmentBytes, toMerge.getMessageBytes());
this.totalLength = existing.totalLength;
}
public ProtobufMessage getNextChunk(ProtobufStatusMessage protobufStatusMessage) {
int start = protobufStatusMessage.getDataOffset() + maxChunkSize;
int length = Math.min(maxChunkSize, this.fragmentBytes.length - start);
return new ProtobufMessage(protobufStatusMessage.getMessageType(),
protobufStatusMessage.getRequestId(),
start,
this.totalLength,
length,
ArrayUtils.subarray(this.fragmentBytes, start, start + length));
}
public boolean isComplete() {
return totalLength == fragmentBytes.length;
}
}
}