1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-06-29 00:10:26 +02:00
Gadgetbridge/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/ProtocolBufferHandler.java
Daniele Gobbetti 36781e6958 Garmin: fix regression in call handling
Add a fictitious action to the notification to enable reply/hangup/reject from the watch.
Also fixes the behavior on sms reply, which should also reject the incoming call.

Change the log level in case some of the canned messages types are left as default to info, as this is a supported scenario.
2024-05-03 20:28:12 +02:00

472 lines
26 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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminPreferences;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationProviderType;
import nodomain.freeyourgadget.gadgetbridge.externalevents.gps.GBLocationService;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
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.proto.vivomovehr.GdiSmsNotification;
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;
private final Map<GdiSmsNotification.SmsNotificationService.CannedListType, String[]> cannedListTypeMap = new HashMap<>();
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.hasSmsNotificationService()) {
return prepareProtobufResponse(processProtobufSmsNotificationMessage(smart.getSmsNotificationService()), 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;
}
if (!calendarServiceRequest.getIncludeAllDay() && mEvt.isAllDay()) {
LOG.debug("CalendarService Skipping event {} that is AllDay", mEvt.getTitle());
continue;
}
if (watchEvents.size() >= calendarServiceRequest.getMaxEvents() * 2) { //NOTE: Tested with values higher than double of the reported max without issues
LOG.debug("Reached the maximum number of events supported by the watch");
break;
}
final GdiCalendarService.CalendarService.CalendarEvent.Builder event = GdiCalendarService.CalendarService.CalendarEvent.newBuilder()
.setTitle(mEvt.getTitle().substring(0, Math.min(mEvt.getTitle().length(), calendarServiceRequest.getMaxTitleLength())))
.setAllDay(mEvt.isAllDay())
.setStartDate(mEvt.getBeginSeconds())
.setEndDate(mEvt.getEndSeconds());
if (calendarServiceRequest.getIncludeLocation() && mEvt.getLocation() != null) {
event.setLocation(mEvt.getLocation().substring(0, Math.min(mEvt.getLocation().length(), calendarServiceRequest.getMaxLocationLength())));
}
if (calendarServiceRequest.getIncludeDescription() && mEvt.getDescription() != null) {
event.setDescription(mEvt.getDescription().substring(0, Math.min(mEvt.getDescription().length(), calendarServiceRequest.getMaxDescriptionLength())));
}
if (calendarServiceRequest.getIncludeOrganizer() && mEvt.getOrganizer() != null) {
event.setDescription(mEvt.getOrganizer().substring(0, Math.min(mEvt.getOrganizer().length(), calendarServiceRequest.getMaxOrganizerLength())));
}
watchEvents.add(event.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)
.setStatus(GdiCalendarService.CalendarService.CalendarServiceResponse.ResponseStatus.OK)
)
).build();
}
LOG.warn("Unknown CalendarService request: {}", calendarService);
return GdiSmartProto.Smart.newBuilder().setCalendarService(
GdiCalendarService.CalendarService.newBuilder().setCalendarResponse(
GdiCalendarService.CalendarService.CalendarServiceResponse.newBuilder()
.setStatus(GdiCalendarService.CalendarService.CalendarServiceResponse.ResponseStatus.UNKNOWN_RESPONSE_STATUS)
)
).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()) {
LOG.info("Got location request");
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()) {
final GdiCore.CoreService.LocationUpdatedSetEnabledRequest locationUpdatedSetEnabledRequest = coreService.getLocationUpdatedSetEnabledRequest();
LOG.info("Received locationUpdatedSetEnabledRequest status: {}", locationUpdatedSetEnabledRequest.getEnabled());
GdiCore.CoreService.LocationUpdatedSetEnabledResponse.Builder response = GdiCore.CoreService.LocationUpdatedSetEnabledResponse.newBuilder()
.setStatus(GdiCore.CoreService.LocationUpdatedSetEnabledResponse.Status.OK);
final boolean sendGpsPref = deviceSupport.getDevicePrefs().getBoolean(DeviceSettingsPreferenceConst.PREF_WORKOUT_SEND_GPS_TO_BAND, false);
GdiCore.CoreService.Request realtimeRequest = null;
if (locationUpdatedSetEnabledRequest.getEnabled()) {
for (final GdiCore.CoreService.Request request : locationUpdatedSetEnabledRequest.getRequestsList()) {
final GdiCore.CoreService.LocationUpdatedSetEnabledResponse.Requested.RequestedStatus requestedStatus;
if (GdiCore.CoreService.DataType.REALTIME_TRACKING.equals(request.getRequested())) {
realtimeRequest = request;
if (sendGpsPref) {
requestedStatus = GdiCore.CoreService.LocationUpdatedSetEnabledResponse.Requested.RequestedStatus.OK;
} else {
requestedStatus = GdiCore.CoreService.LocationUpdatedSetEnabledResponse.Requested.RequestedStatus.KO;
}
} else {
requestedStatus = GdiCore.CoreService.LocationUpdatedSetEnabledResponse.Requested.RequestedStatus.KO;
}
response.addRequests(
GdiCore.CoreService.LocationUpdatedSetEnabledResponse.Requested.newBuilder()
.setRequested(request.getRequested())
.setStatus(requestedStatus)
);
}
}
if (sendGpsPref) {
if (realtimeRequest != null) {
GBLocationService.start(
deviceSupport.getContext(),
deviceSupport.getDevice(),
GBLocationProviderType.GPS,
1000 // TODO from realtimeRequest
);
} else {
GBLocationService.stop(deviceSupport.getContext(), deviceSupport.getDevice());
}
}
return GdiSmartProto.Smart.newBuilder().setCoreService(
GdiCore.CoreService.newBuilder().setLocationUpdatedSetEnabledResponse(response)).build();
}
LOG.warn("Unknown CoreService request: {}", coreService);
return null;
}
private GdiSmartProto.Smart processProtobufSmsNotificationMessage(GdiSmsNotification.SmsNotificationService smsNotificationService) {
if (smsNotificationService.hasSmsCannedListRequest()) {
LOG.debug("Got request for sms canned list");
// Mark canned messages as supported
deviceSupport.evaluateGBDeviceEvent(new GBDeviceEventUpdatePreferences(GarminPreferences.PREF_FEAT_CANNED_MESSAGES, true));
if (this.cannedListTypeMap.isEmpty()) {
List<GdiSmsNotification.SmsNotificationService.CannedListType> requestedTypes = smsNotificationService.getSmsCannedListRequest().getRequestedTypesList();
for (GdiSmsNotification.SmsNotificationService.CannedListType type :
requestedTypes) {
if (GdiSmsNotification.SmsNotificationService.CannedListType.SMS_MESSAGE_RESPONSE.equals(type)) {
final ArrayList<String> messages = new ArrayList<>();
for (int i = 1; i <= 16; i++) {
String message = deviceSupport.getDevicePrefs().getString("canned_reply_" + i, null);
if (message != null && !message.isEmpty()) {
messages.add(message);
}
}
if (!messages.isEmpty())
this.cannedListTypeMap.put(type, messages.toArray(new String[0]));
} else if (GdiSmsNotification.SmsNotificationService.CannedListType.PHONE_CALL_RESPONSE.equals(type)) {
final ArrayList<String> messages = new ArrayList<>();
for (int i = 1; i <= 16; i++) {
String message = deviceSupport.getDevicePrefs().getString("canned_message_dismisscall_" + i, null);
if (message != null && !message.isEmpty()) {
messages.add(message);
}
}
if (!messages.isEmpty())
this.cannedListTypeMap.put(type, messages.toArray(new String[0]));
}
}
}
List<GdiSmsNotification.SmsNotificationService.CannedListType> requestedTypes = smsNotificationService.getSmsCannedListRequest().getRequestedTypesList();
GdiSmsNotification.SmsNotificationService.SmsCannedListResponse.Builder builder = GdiSmsNotification.SmsNotificationService.SmsCannedListResponse.newBuilder()
.setStatus(GdiSmsNotification.SmsNotificationService.ResponseStatus.SUCCESS);
for (GdiSmsNotification.SmsNotificationService.CannedListType requestedType : requestedTypes) {
if (this.cannedListTypeMap.containsKey(requestedType)) {
builder.addLists(GdiSmsNotification.SmsNotificationService.SmsCannedList.newBuilder()
.addAllResponse(Arrays.asList(Objects.requireNonNull(this.cannedListTypeMap.get(requestedType))))
.setType(requestedType)
);
} else {
builder.setStatus(GdiSmsNotification.SmsNotificationService.ResponseStatus.GENERIC_ERROR);
LOG.info("Missing canned messages data for type {}", requestedType);
}
}
return GdiSmartProto.Smart.newBuilder().setSmsNotificationService(GdiSmsNotification.SmsNotificationService.newBuilder().setSmsCannedListResponse(builder)).build();
} else {
LOG.warn("Protobuf smsNotificationService request not implemented: {}", smsNotificationService);
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);
}
public ProtobufMessage setCannedMessages(CannedMessagesSpec cannedMessagesSpec) {
final GdiSmsNotification.SmsNotificationService.CannedListType cannedListType;
switch (cannedMessagesSpec.type) {
case CannedMessagesSpec.TYPE_REJECTEDCALLS:
cannedListType = GdiSmsNotification.SmsNotificationService.CannedListType.PHONE_CALL_RESPONSE;
break;
case CannedMessagesSpec.TYPE_GENERIC:
case CannedMessagesSpec.TYPE_NEWSMS:
cannedListType = GdiSmsNotification.SmsNotificationService.CannedListType.SMS_MESSAGE_RESPONSE;
break;
default:
LOG.warn("Unknown canned messages type, ignoring.");
return null;
}
this.cannedListTypeMap.put(cannedListType, cannedMessagesSpec.cannedMessages);
GdiSmartProto.Smart smart = GdiSmartProto.Smart.newBuilder()
.setSmsNotificationService(GdiSmsNotification.SmsNotificationService.newBuilder()
.setSmsCannedListChangedNotification(
GdiSmsNotification.SmsNotificationService.SmsCannedListChangedNotification.newBuilder().addChangedType(cannedListType)
)
).build();
return prepareProtobufRequest(smart);
}
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;
}
}
}