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