From 45c13675e01f890e8b3988387793a3c419889376 Mon Sep 17 00:00:00 2001 From: Daniele Gobbetti Date: Sat, 20 Apr 2024 16:35:09 +0200 Subject: [PATCH] Garmin: Add support for replying to notifications MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This uses the (assumed) new method of passing multiple actions, instead of the (assumed) legacy accept/decline approach. At the moment the preset messages stored on the watch firmware are used for replying, the code supports using custom messages already but those have to be updated to the watch somehow (probably by protobuf) and this is not supported yet. Using custom messages if they are not set will just do nothing. The NotificationActionIconPosition values have been determined on a vívomove Style and might not work properly on other watches. The evaluation of GBDeviceEvent have been moved in GarminSupport since the notification actions handling uses device events. Also adds a method to read null terminated strings to GarminByteBufferReader. Also adds a warning in NotificationListener if the wrong handle is used for replying to a notification. --- .../externalevents/NotificationListener.java | 2 + .../garmin/GarminByteBufferReader.java | 14 + .../service/devices/garmin/GarminSupport.java | 3 +- .../devices/garmin/NotificationsHandler.java | 261 ++++++++++++++---- .../messages/NotificationControlMessage.java | 75 ++++- .../messages/NotificationUpdateMessage.java | 26 +- 6 files changed, 317 insertions(+), 64 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java index aabdf15ef..47d9e2d25 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java @@ -221,6 +221,8 @@ public class NotificationListener extends NotificationListenerService { } catch (PendingIntent.CanceledException e) { LOG.warn("replyToLastNotification error: " + e.getLocalizedMessage()); } + } else { + LOG.warn("Received ACTION_REPLY but cannot find the corresponding wearableAction"); } break; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminByteBufferReader.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminByteBufferReader.java index c1816439d..2623313a9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminByteBufferReader.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminByteBufferReader.java @@ -58,6 +58,20 @@ public class GarminByteBufferReader { return new String(bytes, StandardCharsets.UTF_8); } + public String readNullTerminatedString() { + int position = byteBuffer.position(); + int size = 0; + while (byteBuffer.hasRemaining()) { + if (byteBuffer.get() == 0) + break; + size++; + } + byteBuffer.position(position); + byte[] bytes = new byte[size]; + byteBuffer.get(bytes); + return new String(bytes, StandardCharsets.UTF_8); + } + public byte[] readBytes(int size) { byte[] bytes = new byte[size]; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java index ee430f864..75b064cd0 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java @@ -172,7 +172,6 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni return; //message cannot be handled } - evaluateGBDeviceEvent(parsedMessage.getGBDeviceEvent()); /* the handler elaborates the followup message but might change the status message since it does @@ -189,6 +188,8 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni } } + evaluateGBDeviceEvent(parsedMessage.getGBDeviceEvent()); + communicator.sendMessage(parsedMessage.getAckBytestream()); //send status message sendOutgoingMessage(parsedMessage); //send reply if any diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/NotificationsHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/NotificationsHandler.java index 7ae5634da..6aec0c866 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/NotificationsHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/NotificationsHandler.java @@ -2,20 +2,27 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin; import android.util.SparseArray; +import org.apache.commons.lang3.EnumUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Date; import java.util.Iterator; import java.util.LinkedList; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Queue; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventNotificationControl; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; @@ -25,6 +32,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.Noti 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; +import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue; public class NotificationsHandler implements MessageHandler { public static final SimpleDateFormat NOTIFICATION_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.ROOT); @@ -32,6 +40,9 @@ public class NotificationsHandler implements MessageHandler { private final Queue notificationSpecQueue; private final Upload upload; private boolean enabled = false; + // Keep track of Notification ID -> action handle, as BangleJSDeviceSupport. + // TODO: This needs to be simplified. + private final LimitedQueue mNotificationReplyAction = new LimitedQueue<>(16); public NotificationsHandler() { @@ -39,17 +50,12 @@ public class NotificationsHandler implements MessageHandler { 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 static void encodeNotificationAttribute(NotificationSpec notificationSpec, Map.Entry entry, MessageWriter messageWriter) { + messageWriter.writeByte(entry.getKey().code); + final byte[] bytes = entry.getKey().getNotificationSpecAttribute(notificationSpec, entry.getValue()); + messageWriter.writeShort(bytes.length); + messageWriter.writeBytes(bytes); +// LOG.info("ATTRIBUTE:{} value:{} length:{}", entry.getKey(), new String(bytes), bytes.length); } @@ -86,20 +92,27 @@ public class NotificationsHandler implements MessageHandler { return null; } - - public NotificationUpdateMessage onDeleteNotification(int id) { + public NotificationUpdateMessage onNotification(NotificationSpec notificationSpec) { if (!enabled) return null; + final boolean isUpdate = addNotificationToQueue(notificationSpec); - Iterator iterator = notificationSpecQueue.iterator(); - while (iterator.hasNext()) { - NotificationSpec e = iterator.next(); - if (e.getId() == id) { - iterator.remove(); - return new NotificationUpdateMessage(NotificationUpdateMessage.NotificationUpdateType.REMOVE, e.type, getNotificationsCount(e.type), id); + 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! + + final boolean hasActions = (null != notificationSpec.attachedActions && !notificationSpec.attachedActions.isEmpty()); + if (hasActions) { + for (int i = 0; i < notificationSpec.attachedActions.size(); i++) { + final NotificationSpec.Action action = notificationSpec.attachedActions.get(i); + + if (action.type == NotificationSpec.Action.TYPE_WEARABLE_REPLY || action.type == NotificationSpec.Action.TYPE_SYNTECTIC_REPLY_PHONENR) { + mNotificationReplyAction.add(notificationSpec.getId(), action.handle); + } } } - return null; + return new NotificationUpdateMessage(notificationUpdateType, notificationSpec.type, getNotificationsCount(notificationSpec.type), notificationSpec.getId(), hasActions); } private int getNotificationsCount(NotificationType notificationType) { @@ -119,6 +132,21 @@ public class NotificationsHandler implements MessageHandler { return null; } + public NotificationUpdateMessage onDeleteNotification(int id) { + if (!enabled) + return null; + + Iterator iterator = notificationSpecQueue.iterator(); + while (iterator.hasNext()) { + NotificationSpec e = iterator.next(); + if (e.getId() == id) { + iterator.remove(); + return new NotificationUpdateMessage(NotificationUpdateMessage.NotificationUpdateType.REMOVE, e.type, getNotificationsCount(e.type), id, false); + } + } + return null; + } + public GFDIMessage handle(GFDIMessage message) { if (!enabled) return null; @@ -127,30 +155,16 @@ public class NotificationsHandler implements MessageHandler { if (notificationSpec != null) { switch (((NotificationControlMessage) message).getCommand()) { case GET_NOTIFICATION_ATTRIBUTES: - final MessageWriter messageWriter = new MessageWriter(); - messageWriter.writeByte(NotificationCommand.GET_NOTIFICATION_ATTRIBUTES.code); - messageWriter.writeInt(((NotificationControlMessage) message).getNotificationId()); - for (Map.Entry attribute : ((NotificationControlMessage) message).getNotificationAttributesMap().entrySet()) { - if (!attribute.getKey().equals(NotificationAttribute.MESSAGE_SIZE)) { //should be last - messageWriter.writeByte(attribute.getKey().code); - final byte[] bytes = attribute.getKey().getNotificationSpecAttribute(notificationSpec, attribute.getValue()); - messageWriter.writeShort(bytes.length); - messageWriter.writeBytes(bytes); - LOG.info("ATTRIBUTE:{} value:{} length:{}", attribute.getKey(), new String(bytes), bytes.length); - } - } - if (((NotificationControlMessage) message).getNotificationAttributesMap().containsKey(NotificationAttribute.MESSAGE_SIZE)) { - messageWriter.writeByte(NotificationAttribute.MESSAGE_SIZE.code); - final byte[] bytes = NotificationAttribute.MESSAGE_SIZE.getNotificationSpecAttribute(notificationSpec, 0); - messageWriter.writeShort(bytes.length); - messageWriter.writeBytes(bytes); - LOG.info("ATTRIBUTE:{} value:{} length:{}", NotificationAttribute.MESSAGE_SIZE, new String(bytes), bytes.length); + return getNotificationDataMessage((NotificationControlMessage) message, notificationSpec); + case PERFORM_LEGACY_NOTIFICATION_ACTION: + LOG.info("Legacy Notification: {}", ((NotificationControlMessage) message).getLegacyNotificationAction()); + break; + case PERFORM_NOTIFICATION_ACTION: + performNotificationAction((NotificationControlMessage) message, notificationSpec); + break; - } - NotificationFragment notificationFragment = new NotificationFragment(messageWriter.getBytes()); - return upload.setCurrentlyUploading(notificationFragment); default: - LOG.error("NOT SUPPORTED"); + LOG.error("NOT SUPPORTED: {}", ((NotificationControlMessage) message).getCommand()); } } } else if (message instanceof NotificationDataStatusMessage) { @@ -160,6 +174,60 @@ public class NotificationsHandler implements MessageHandler { return null; } + private void performNotificationAction(NotificationControlMessage message, NotificationSpec notificationSpec) { + final GBDeviceEventNotificationControl deviceEvtNotificationControl = new GBDeviceEventNotificationControl(); + deviceEvtNotificationControl.handle = notificationSpec.getId(); + final GBDeviceEventCallControl deviceEvtCallControl = new GBDeviceEventCallControl(); + switch (message.getNotificationAction()) { + case REPLY_CUSTOM_MESSAGES: + case REPLY_STANDARD_MESSAGES: + deviceEvtNotificationControl.event = GBDeviceEventNotificationControl.Event.REPLY; + deviceEvtNotificationControl.reply = message.getActionString(); + if (notificationSpec.type.equals(NotificationType.GENERIC_PHONE)) { + deviceEvtNotificationControl.phoneNumber = notificationSpec.phoneNumber; + } else { + deviceEvtNotificationControl.handle = mNotificationReplyAction.lookup(notificationSpec.getId()); //handle of wearable action is needed + } + message.setDeviceEvent(deviceEvtNotificationControl); + break; + case ACCEPT_INCOMING_CALL: + deviceEvtCallControl.event = GBDeviceEventCallControl.Event.ACCEPT; + message.setDeviceEvent(deviceEvtCallControl); + break; + case REJECT_INCOMING_CALL: + deviceEvtCallControl.event = GBDeviceEventCallControl.Event.REJECT; + message.setDeviceEvent(deviceEvtCallControl); + break; + case DISMISS_NOTIFICATION: + deviceEvtNotificationControl.event = GBDeviceEventNotificationControl.Event.DISMISS; + message.setDeviceEvent(deviceEvtNotificationControl); + break; + case BLOCK_APPLICATION: + deviceEvtNotificationControl.event = GBDeviceEventNotificationControl.Event.MUTE; + message.setDeviceEvent(deviceEvtNotificationControl); + break; + } + } + + private NotificationDataMessage getNotificationDataMessage(NotificationControlMessage message, NotificationSpec notificationSpec) { + final MessageWriter messageWriter = new MessageWriter(); + messageWriter.writeByte(NotificationCommand.GET_NOTIFICATION_ATTRIBUTES.code); + messageWriter.writeInt(message.getNotificationId()); + Map.Entry lastEntry = null; + for (Map.Entry entry : message.getNotificationAttributesMap().entrySet()) { + if (!NotificationAttribute.MESSAGE_SIZE.equals(entry.getKey())) { + encodeNotificationAttribute(notificationSpec, entry, messageWriter); + } else { + lastEntry = entry; + } + } + if (lastEntry != null) { + encodeNotificationAttribute(notificationSpec, lastEntry, messageWriter); + } + NotificationFragment notificationFragment = new NotificationFragment(messageWriter.getBytes()); + return upload.setCurrentlyUploading(notificationFragment); + } + public void setEnabled(boolean enable) { this.enabled = enable; @@ -167,10 +235,9 @@ public class NotificationsHandler implements MessageHandler { public enum NotificationCommand { //was AncsCommand GET_NOTIFICATION_ATTRIBUTES(0), - GET_APP_ATTRIBUTES(1), - PERFORM_NOTIFICATION_ACTION(2), - // Garmin extensions - PERFORM_ANDROID_ACTION(128); + GET_APP_ATTRIBUTES(1), //unknown/untested + PERFORM_LEGACY_NOTIFICATION_ACTION(2), + PERFORM_NOTIFICATION_ACTION(128); public final int code; @@ -187,6 +254,11 @@ public class NotificationsHandler implements MessageHandler { } } + public enum LegacyNotificationAction { //was AncsAction + ACCEPT, + REFUSE + + } public enum NotificationAttribute { //was AncsAttribute APP_IDENTIFIER(0), TITLE(1, true), @@ -194,8 +266,8 @@ public class NotificationsHandler implements MessageHandler { MESSAGE(3, true), MESSAGE_SIZE(4), DATE(5), - // POSITIVE_ACTION_LABEL(6), -// NEGATIVE_ACTION_LABEL(7), + // POSITIVE_ACTION_LABEL(6), //needed only for legacy notification actions + // NEGATIVE_ACTION_LABEL(7), //needed only for legacy notification actions // Garmin extensions // PHONE_NUMBER(126, true), ACTIONS(127, false, true), @@ -255,13 +327,100 @@ public class NotificationsHandler implements MessageHandler { toReturn = Integer.toString(notificationSpec.body == null ? "".length() : notificationSpec.body.length()); break; case ACTIONS: - toReturn = new String(new byte[]{0x00, 0x00, 0x00, 0x00}); + toReturn = encodeNotificationActionsString(notificationSpec); break; } + if (maxLength == 0) + return toReturn.getBytes(StandardCharsets.UTF_8); return toReturn.substring(0, Math.min(toReturn.length(), maxLength)).getBytes(StandardCharsets.UTF_8); } + + private String encodeNotificationActionsString(NotificationSpec notificationSpec) { + + final List garminActions = new ArrayList<>(); + if (notificationSpec.type.equals(NotificationType.GENERIC_PHONE)) { + garminActions.add(encodeNotificationAction(NotificationAction.REPLY_STANDARD_MESSAGES, " ")); //text is not shown on watch + garminActions.add(encodeNotificationAction(NotificationAction.REJECT_INCOMING_CALL, " ")); //text is not shown on watch + garminActions.add(encodeNotificationAction(NotificationAction.ACCEPT_INCOMING_CALL, " ")); //text is not shown on watch + } + if (null != notificationSpec.attachedActions) { + for (NotificationSpec.Action action : notificationSpec.attachedActions) { + switch (action.type) { + case NotificationSpec.Action.TYPE_WEARABLE_REPLY: + garminActions.add(encodeNotificationAction(NotificationAction.REPLY_STANDARD_MESSAGES, action.title)); + break; + case NotificationSpec.Action.TYPE_SYNTECTIC_DISMISS: + garminActions.add(encodeNotificationAction(NotificationAction.DISMISS_NOTIFICATION, action.title)); + break; + case NotificationSpec.Action.TYPE_SYNTECTIC_MUTE: + garminActions.add(encodeNotificationAction(NotificationAction.BLOCK_APPLICATION, action.title)); + break; + + } +// LOG.info("Notification has action {} with title {}", action.type, action.title); + } + } + if (garminActions.isEmpty()) + return new String(new byte[]{0x00, 0x00, 0x00, 0x00}); + + try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) { + byteArrayOutputStream.write(garminActions.size()); + for (byte[] item : garminActions) { + byteArrayOutputStream.write(item); + } + return byteArrayOutputStream.toString(); + } catch (IOException e) { + throw new RuntimeException(e); + } + + } + + private byte[] encodeNotificationAction(NotificationAction notificationAction, String description) { + final ByteBuffer action = ByteBuffer.allocate(3 + description.getBytes(StandardCharsets.UTF_8).length); + action.put((byte) notificationAction.code); + if (null == notificationAction.notificationActionIconPosition) + action.put((byte) 0x00); + else + action.put((byte) EnumUtils.generateBitVector(NotificationActionIconPosition.class, notificationAction.notificationActionIconPosition)); + action.put((byte) description.getBytes(StandardCharsets.UTF_8).length); + action.put(description.getBytes()); + return action.array(); + } } + public enum NotificationAction { + REPLY_CUSTOM_MESSAGES(94, NotificationActionIconPosition.RIGHT), // uses the customized replies (set through protobuf??) does nothing if not available and on watches that do not support custom replies + REPLY_STANDARD_MESSAGES(95, NotificationActionIconPosition.BOTTOM), // uses predefined replies + ACCEPT_INCOMING_CALL(96, NotificationActionIconPosition.RIGHT), + REJECT_INCOMING_CALL(97, NotificationActionIconPosition.LEFT), + DISMISS_NOTIFICATION(98, NotificationActionIconPosition.LEFT), + BLOCK_APPLICATION(99, null), + ; + + private final int code; + private final NotificationActionIconPosition notificationActionIconPosition; + + NotificationAction(int code, NotificationActionIconPosition notificationActionIconPosition) { + this.code = code; + this.notificationActionIconPosition = notificationActionIconPosition; + } + + public static NotificationAction fromCode(final int code) { + for (final NotificationAction notificationAction : NotificationAction.values()) { + if (notificationAction.code == code) { + return notificationAction; + } + } + throw new IllegalArgumentException("Unknown notification action code " + code); + } + + } + + enum NotificationActionIconPosition { //educated guesses based on the icons' positions on vívomove style + BOTTOM, //or is it reply? + RIGHT, //or is it accept? + LEFT, //or is it dismiss/refuse? + } public static class Upload { private NotificationFragment currentlyUploading; @@ -272,6 +431,10 @@ public class NotificationsHandler implements MessageHandler { } private GFDIMessage processUploadProgress(NotificationDataStatusMessage notificationDataStatusMessage) { + if (null == currentlyUploading) { + LOG.warn("Received Upload Progress but we are not sending any notification"); + return null; + } if (!currentlyUploading.dataHolder.hasRemaining()) { this.currentlyUploading = null; LOG.info("SENT ALL"); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationControlMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationControlMessage.java index 338012f51..b02e837ee 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationControlMessage.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationControlMessage.java @@ -3,16 +3,43 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages; import java.util.HashMap; import java.util.Map; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; 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; +import static nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.NotificationsHandler.NotificationCommand.PERFORM_LEGACY_NOTIFICATION_ACTION; +import static nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.NotificationsHandler.NotificationCommand.PERFORM_NOTIFICATION_ACTION; public class NotificationControlMessage extends GFDIMessage { private final NotificationsHandler.NotificationCommand command; private final int notificationId; - private final Map notificationAttributesMap; + private Map notificationAttributesMap; + private NotificationsHandler.LegacyNotificationAction legacyNotificationAction; + private NotificationsHandler.NotificationAction notificationAction; + private String actionString; + private GBDeviceEvent deviceEvent; + + public NotificationControlMessage(GarminMessage garminMessage, NotificationsHandler.NotificationCommand command, int notificationId, NotificationsHandler.NotificationAction notificationAction, String actionString) { + this.garminMessage = garminMessage; + this.command = command; + this.notificationId = notificationId; + this.notificationAction = notificationAction; + this.actionString = actionString; + + this.statusMessage = new NotificationControlStatusMessage(garminMessage, GFDIMessage.Status.ACK, NotificationControlStatusMessage.NotificationChunkStatus.OK, NotificationControlStatusMessage.NotificationStatusCode.NO_ERROR); + + } + + public NotificationControlMessage(GarminMessage garminMessage, NotificationsHandler.NotificationCommand command, int notificationId, NotificationsHandler.LegacyNotificationAction legacyNotificationAction) { + this.garminMessage = garminMessage; + this.command = command; + this.notificationId = notificationId; + this.legacyNotificationAction = legacyNotificationAction; + + this.statusMessage = new NotificationControlStatusMessage(garminMessage, GFDIMessage.Status.ACK, NotificationControlStatusMessage.NotificationChunkStatus.OK, NotificationControlStatusMessage.NotificationStatusCode.NO_ERROR); + } public NotificationControlMessage(GarminMessage garminMessage, NotificationsHandler.NotificationCommand command, int notificationId, Map notificationAttributesMap) { this.garminMessage = garminMessage; @@ -23,19 +50,28 @@ public class NotificationControlMessage extends GFDIMessage { this.statusMessage = new NotificationControlStatusMessage(garminMessage, GFDIMessage.Status.ACK, NotificationControlStatusMessage.NotificationChunkStatus.OK, NotificationControlStatusMessage.NotificationStatusCode.NO_ERROR); } + //TODO: the fact that we return three versions of this object is really ugly public static NotificationControlMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) { - final NotificationsHandler.NotificationCommand command = NotificationsHandler.NotificationCommand.fromCode(reader.readByte()); - if (command != GET_NOTIFICATION_ATTRIBUTES) { - LOG.error("NOT SUPPORTED"); - } - LOG.info("COMMAND: {}", command.ordinal()); final int notificationId = reader.readInt(); - final Map notificationAttributesMap = createGetNotificationAttributesCommand(reader); + if (command == GET_NOTIFICATION_ATTRIBUTES) { + final Map notificationAttributesMap = createGetNotificationAttributesCommand(reader); + return new NotificationControlMessage(garminMessage, command, notificationId, notificationAttributesMap); + } else if (command == PERFORM_LEGACY_NOTIFICATION_ACTION) { + NotificationsHandler.LegacyNotificationAction[] values = NotificationsHandler.LegacyNotificationAction.values(); + final NotificationsHandler.LegacyNotificationAction legacyNotificationAction = values[reader.readByte()]; + return new NotificationControlMessage(garminMessage, command, notificationId, legacyNotificationAction); + } else if (command == PERFORM_NOTIFICATION_ACTION) { + final int actionId = reader.readByte(); + final NotificationsHandler.NotificationAction notificationAction = NotificationsHandler.NotificationAction.fromCode(actionId); + final String actionString = reader.readNullTerminatedString(); + return new NotificationControlMessage(garminMessage, command, notificationId, notificationAction, actionString); + } + LOG.warn("Unknown NotificationCommand in NotificationControlMessage"); - return new NotificationControlMessage(garminMessage, command, notificationId, notificationAttributesMap); + return null; } private static Map createGetNotificationAttributesCommand(MessageReader reader) { @@ -54,7 +90,7 @@ public class NotificationControlMessage extends GFDIMessage { maxLength = reader.readShort(); } else if (attribute.hasAdditionalParams) { - maxLength = reader.readShort(); + maxLength = reader.readShort(); //TODO this is wrong // TODO: What is this?? reader.readByte(); @@ -66,6 +102,27 @@ public class NotificationControlMessage extends GFDIMessage { return notificationAttributesMap; } + public String getActionString() { + return actionString; + } + + public void setDeviceEvent(GBDeviceEvent deviceEvent) { + this.deviceEvent = deviceEvent; + } + + @Override + public GBDeviceEvent getGBDeviceEvent() { + return deviceEvent; + } + + public NotificationsHandler.LegacyNotificationAction getLegacyNotificationAction() { + return legacyNotificationAction; + } + + public NotificationsHandler.NotificationAction getNotificationAction() { + return notificationAction; + } + public NotificationsHandler.NotificationCommand getCommand() { return command; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationUpdateMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationUpdateMessage.java index e61c5dbfe..fa245f81e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationUpdateMessage.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationUpdateMessage.java @@ -2,6 +2,8 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages; import org.apache.commons.lang3.EnumUtils; +import java.util.EnumSet; + import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; public class NotificationUpdateMessage extends GFDIMessage { @@ -10,13 +12,16 @@ public class NotificationUpdateMessage extends GFDIMessage { final private NotificationType notificationType; final private int count; //how many notifications of the same type are present final private int notificationId; + final private boolean hasActions; + final private boolean useLegacyActions = false; - public NotificationUpdateMessage(NotificationUpdateType notificationUpdateType, NotificationType notificationType, int count, int notificationId) { + public NotificationUpdateMessage(NotificationUpdateType notificationUpdateType, NotificationType notificationType, int count, int notificationId, boolean hasActions) { this.garminMessage = GarminMessage.NOTIFICATION_UPDATE; this.notificationUpdateType = notificationUpdateType; this.notificationType = notificationType; this.count = count; this.notificationId = notificationId; + this.hasActions = hasActions; } @Override @@ -29,25 +34,32 @@ public class NotificationUpdateMessage extends GFDIMessage { writer.writeByte(getCategoryValue(this.notificationType)); writer.writeByte(this.count); writer.writeInt(this.notificationId); - writer.writeByte(0); //unk (extra flags) + writer.writeByte(this.useLegacyActions ? 0x00 : 0x03); return true; } private int getCategoryFlags(NotificationType notificationType) { + EnumSet flags = EnumSet.noneOf(NotificationFlag.class); + if (this.hasActions && this.useLegacyActions) { //only needed for legacy actions + flags.add(NotificationFlag.ACTION_ACCEPT); + flags.add(NotificationFlag.ACTION_DECLINE); + } + switch (notificationType.getGenericType()) { case "generic_phone": case "generic_email": case "generic_sms": case "generic_chat": - return (int) EnumUtils.generateBitVector(NotificationFlag.class, NotificationFlag.FOREGROUND); + flags.add(NotificationFlag.FOREGROUND); + break; case "generic_navigation": case "generic_social": case "generic_alarm_clock": case "generic": - return (int) EnumUtils.generateBitVector(NotificationFlag.class, NotificationFlag.BACKGROUND); + flags.add(NotificationFlag.BACKGROUND); } - return 1; + return (int) EnumUtils.generateBitVector(NotificationFlag.class, flags); } private int getCategoryValue(NotificationType notificationType) { @@ -79,6 +91,10 @@ public class NotificationUpdateMessage extends GFDIMessage { enum NotificationFlag { //was AncsEventFlag BACKGROUND, FOREGROUND, + UNK, + ACTION_ACCEPT, //only needed for legacy actions + ACTION_DECLINE, //only needed for legacy actions + } enum NotificationCategory { //was AncsCategory