Garmin: Add support for replying to notifications

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.
This commit is contained in:
Daniele Gobbetti 2024-04-20 16:35:09 +02:00
parent 863c1a5657
commit d6ade723d3
6 changed files with 317 additions and 64 deletions

View File

@ -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;
}

View File

@ -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];

View File

@ -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

View File

@ -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<NotificationSpec> 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<Integer, Long> 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<NotificationAttribute, Integer> 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<NotificationSpec> 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<NotificationSpec> 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<NotificationAttribute, Integer> 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<NotificationAttribute, Integer> lastEntry = null;
for (Map.Entry<NotificationAttribute, Integer> 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<byte[]> 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");

View File

@ -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<NotificationsHandler.NotificationAttribute, Integer> notificationAttributesMap;
private Map<NotificationsHandler.NotificationAttribute, Integer> 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<NotificationsHandler.NotificationAttribute, Integer> 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<NotificationsHandler.NotificationAttribute, Integer> notificationAttributesMap = createGetNotificationAttributesCommand(reader);
if (command == GET_NOTIFICATION_ATTRIBUTES) {
final Map<NotificationsHandler.NotificationAttribute, Integer> 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<NotificationsHandler.NotificationAttribute, Integer> 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;
}

View File

@ -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<NotificationFlag> 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