1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-06-02 19:36:14 +02:00

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) { } catch (PendingIntent.CanceledException e) {
LOG.warn("replyToLastNotification error: " + e.getLocalizedMessage()); LOG.warn("replyToLastNotification error: " + e.getLocalizedMessage());
} }
} else {
LOG.warn("Received ACTION_REPLY but cannot find the corresponding wearableAction");
} }
break; break;
} }

View File

@ -58,6 +58,20 @@ public class GarminByteBufferReader {
return new String(bytes, StandardCharsets.UTF_8); 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) { public byte[] readBytes(int size) {
byte[] bytes = new byte[size]; byte[] bytes = new byte[size];

View File

@ -172,7 +172,6 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
return; //message cannot be handled return; //message cannot be handled
} }
evaluateGBDeviceEvent(parsedMessage.getGBDeviceEvent());
/* /*
the handler elaborates the followup message but might change the status message since it does 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 communicator.sendMessage(parsedMessage.getAckBytestream()); //send status message
sendOutgoingMessage(parsedMessage); //send reply if any sendOutgoingMessage(parsedMessage); //send reply if any

View File

@ -2,20 +2,27 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin;
import android.util.SparseArray; import android.util.SparseArray;
import org.apache.commons.lang3.EnumUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Queue; 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.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; 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.NotificationDataMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.NotificationUpdateMessage; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.NotificationUpdateMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.NotificationDataStatusMessage; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.NotificationDataStatusMessage;
import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue;
public class NotificationsHandler implements MessageHandler { public class NotificationsHandler implements MessageHandler {
public static final SimpleDateFormat NOTIFICATION_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.ROOT); 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 Queue<NotificationSpec> notificationSpecQueue;
private final Upload upload; private final Upload upload;
private boolean enabled = false; 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() { public NotificationsHandler() {
@ -39,17 +50,12 @@ public class NotificationsHandler implements MessageHandler {
this.upload = new Upload(); this.upload = new Upload();
} }
public NotificationUpdateMessage onNotification(NotificationSpec notificationSpec) { private static void encodeNotificationAttribute(NotificationSpec notificationSpec, Map.Entry<NotificationAttribute, Integer> entry, MessageWriter messageWriter) {
if (!enabled) messageWriter.writeByte(entry.getKey().code);
return null; final byte[] bytes = entry.getKey().getNotificationSpecAttribute(notificationSpec, entry.getValue());
final boolean isUpdate = addNotificationToQueue(notificationSpec); messageWriter.writeShort(bytes.length);
messageWriter.writeBytes(bytes);
NotificationUpdateMessage.NotificationUpdateType notificationUpdateType = isUpdate ? NotificationUpdateMessage.NotificationUpdateType.MODIFY : NotificationUpdateMessage.NotificationUpdateType.ADD; // LOG.info("ATTRIBUTE:{} value:{} length:{}", entry.getKey(), new String(bytes), bytes.length);
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());
} }
@ -86,20 +92,27 @@ public class NotificationsHandler implements MessageHandler {
return null; return null;
} }
public NotificationUpdateMessage onNotification(NotificationSpec notificationSpec) {
public NotificationUpdateMessage onDeleteNotification(int id) {
if (!enabled) if (!enabled)
return null; return null;
final boolean isUpdate = addNotificationToQueue(notificationSpec);
Iterator<NotificationSpec> iterator = notificationSpecQueue.iterator(); NotificationUpdateMessage.NotificationUpdateType notificationUpdateType = isUpdate ? NotificationUpdateMessage.NotificationUpdateType.MODIFY : NotificationUpdateMessage.NotificationUpdateType.ADD;
while (iterator.hasNext()) {
NotificationSpec e = iterator.next(); if (notificationSpecQueue.size() > 10)
if (e.getId() == id) { notificationSpecQueue.poll(); //remove the oldest notification TODO: should send a delete notification message to watch!
iterator.remove();
return new NotificationUpdateMessage(NotificationUpdateMessage.NotificationUpdateType.REMOVE, e.type, getNotificationsCount(e.type), id); 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) { private int getNotificationsCount(NotificationType notificationType) {
@ -119,6 +132,21 @@ public class NotificationsHandler implements MessageHandler {
return null; 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) { public GFDIMessage handle(GFDIMessage message) {
if (!enabled) if (!enabled)
return null; return null;
@ -127,30 +155,16 @@ public class NotificationsHandler implements MessageHandler {
if (notificationSpec != null) { if (notificationSpec != null) {
switch (((NotificationControlMessage) message).getCommand()) { switch (((NotificationControlMessage) message).getCommand()) {
case GET_NOTIFICATION_ATTRIBUTES: case GET_NOTIFICATION_ATTRIBUTES:
final MessageWriter messageWriter = new MessageWriter(); return getNotificationDataMessage((NotificationControlMessage) message, notificationSpec);
messageWriter.writeByte(NotificationCommand.GET_NOTIFICATION_ATTRIBUTES.code); case PERFORM_LEGACY_NOTIFICATION_ACTION:
messageWriter.writeInt(((NotificationControlMessage) message).getNotificationId()); LOG.info("Legacy Notification: {}", ((NotificationControlMessage) message).getLegacyNotificationAction());
for (Map.Entry<NotificationAttribute, Integer> attribute : ((NotificationControlMessage) message).getNotificationAttributesMap().entrySet()) { break;
if (!attribute.getKey().equals(NotificationAttribute.MESSAGE_SIZE)) { //should be last case PERFORM_NOTIFICATION_ACTION:
messageWriter.writeByte(attribute.getKey().code); performNotificationAction((NotificationControlMessage) message, notificationSpec);
final byte[] bytes = attribute.getKey().getNotificationSpecAttribute(notificationSpec, attribute.getValue()); break;
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);
}
NotificationFragment notificationFragment = new NotificationFragment(messageWriter.getBytes());
return upload.setCurrentlyUploading(notificationFragment);
default: default:
LOG.error("NOT SUPPORTED"); LOG.error("NOT SUPPORTED: {}", ((NotificationControlMessage) message).getCommand());
} }
} }
} else if (message instanceof NotificationDataStatusMessage) { } else if (message instanceof NotificationDataStatusMessage) {
@ -160,6 +174,60 @@ public class NotificationsHandler implements MessageHandler {
return null; 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) { public void setEnabled(boolean enable) {
this.enabled = enable; this.enabled = enable;
@ -167,10 +235,9 @@ public class NotificationsHandler implements MessageHandler {
public enum NotificationCommand { //was AncsCommand public enum NotificationCommand { //was AncsCommand
GET_NOTIFICATION_ATTRIBUTES(0), GET_NOTIFICATION_ATTRIBUTES(0),
GET_APP_ATTRIBUTES(1), GET_APP_ATTRIBUTES(1), //unknown/untested
PERFORM_NOTIFICATION_ACTION(2), PERFORM_LEGACY_NOTIFICATION_ACTION(2),
// Garmin extensions PERFORM_NOTIFICATION_ACTION(128);
PERFORM_ANDROID_ACTION(128);
public final int code; 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 public enum NotificationAttribute { //was AncsAttribute
APP_IDENTIFIER(0), APP_IDENTIFIER(0),
TITLE(1, true), TITLE(1, true),
@ -194,8 +266,8 @@ public class NotificationsHandler implements MessageHandler {
MESSAGE(3, true), MESSAGE(3, true),
MESSAGE_SIZE(4), MESSAGE_SIZE(4),
DATE(5), DATE(5),
// POSITIVE_ACTION_LABEL(6), // POSITIVE_ACTION_LABEL(6), //needed only for legacy notification actions
// NEGATIVE_ACTION_LABEL(7), // NEGATIVE_ACTION_LABEL(7), //needed only for legacy notification actions
// Garmin extensions // Garmin extensions
// PHONE_NUMBER(126, true), // PHONE_NUMBER(126, true),
ACTIONS(127, false, true), ACTIONS(127, false, true),
@ -255,13 +327,100 @@ public class NotificationsHandler implements MessageHandler {
toReturn = Integer.toString(notificationSpec.body == null ? "".length() : notificationSpec.body.length()); toReturn = Integer.toString(notificationSpec.body == null ? "".length() : notificationSpec.body.length());
break; break;
case ACTIONS: case ACTIONS:
toReturn = new String(new byte[]{0x00, 0x00, 0x00, 0x00}); toReturn = encodeNotificationActionsString(notificationSpec);
break; break;
} }
if (maxLength == 0)
return toReturn.getBytes(StandardCharsets.UTF_8);
return toReturn.substring(0, Math.min(toReturn.length(), maxLength)).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 { public static class Upload {
private NotificationFragment currentlyUploading; private NotificationFragment currentlyUploading;
@ -272,6 +431,10 @@ public class NotificationsHandler implements MessageHandler {
} }
private GFDIMessage processUploadProgress(NotificationDataStatusMessage notificationDataStatusMessage) { 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()) { if (!currentlyUploading.dataHolder.hasRemaining()) {
this.currentlyUploading = null; this.currentlyUploading = null;
LOG.info("SENT ALL"); 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.HashMap;
import java.util.Map; 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.NotificationsHandler;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.NotificationControlStatusMessage; 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.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 { public class NotificationControlMessage extends GFDIMessage {
private final NotificationsHandler.NotificationCommand command; private final NotificationsHandler.NotificationCommand command;
private final int notificationId; 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) { public NotificationControlMessage(GarminMessage garminMessage, NotificationsHandler.NotificationCommand command, int notificationId, Map<NotificationsHandler.NotificationAttribute, Integer> notificationAttributesMap) {
this.garminMessage = garminMessage; 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); 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) { public static NotificationControlMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {
final NotificationsHandler.NotificationCommand command = NotificationsHandler.NotificationCommand.fromCode(reader.readByte()); 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 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) { private static Map<NotificationsHandler.NotificationAttribute, Integer> createGetNotificationAttributesCommand(MessageReader reader) {
@ -54,7 +90,7 @@ public class NotificationControlMessage extends GFDIMessage {
maxLength = reader.readShort(); maxLength = reader.readShort();
} else if (attribute.hasAdditionalParams) { } else if (attribute.hasAdditionalParams) {
maxLength = reader.readShort(); maxLength = reader.readShort(); //TODO this is wrong
// TODO: What is this?? // TODO: What is this??
reader.readByte(); reader.readByte();
@ -66,6 +102,27 @@ public class NotificationControlMessage extends GFDIMessage {
return notificationAttributesMap; 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() { public NotificationsHandler.NotificationCommand getCommand() {
return command; return command;
} }

View File

@ -2,6 +2,8 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
import org.apache.commons.lang3.EnumUtils; import org.apache.commons.lang3.EnumUtils;
import java.util.EnumSet;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
public class NotificationUpdateMessage extends GFDIMessage { public class NotificationUpdateMessage extends GFDIMessage {
@ -10,13 +12,16 @@ public class NotificationUpdateMessage extends GFDIMessage {
final private NotificationType notificationType; final private NotificationType notificationType;
final private int count; //how many notifications of the same type are present final private int count; //how many notifications of the same type are present
final private int notificationId; 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.garminMessage = GarminMessage.NOTIFICATION_UPDATE;
this.notificationUpdateType = notificationUpdateType; this.notificationUpdateType = notificationUpdateType;
this.notificationType = notificationType; this.notificationType = notificationType;
this.count = count; this.count = count;
this.notificationId = notificationId; this.notificationId = notificationId;
this.hasActions = hasActions;
} }
@Override @Override
@ -29,25 +34,32 @@ public class NotificationUpdateMessage extends GFDIMessage {
writer.writeByte(getCategoryValue(this.notificationType)); writer.writeByte(getCategoryValue(this.notificationType));
writer.writeByte(this.count); writer.writeByte(this.count);
writer.writeInt(this.notificationId); writer.writeInt(this.notificationId);
writer.writeByte(0); //unk (extra flags) writer.writeByte(this.useLegacyActions ? 0x00 : 0x03);
return true; return true;
} }
private int getCategoryFlags(NotificationType notificationType) { 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()) { switch (notificationType.getGenericType()) {
case "generic_phone": case "generic_phone":
case "generic_email": case "generic_email":
case "generic_sms": case "generic_sms":
case "generic_chat": case "generic_chat":
return (int) EnumUtils.generateBitVector(NotificationFlag.class, NotificationFlag.FOREGROUND); flags.add(NotificationFlag.FOREGROUND);
break;
case "generic_navigation": case "generic_navigation":
case "generic_social": case "generic_social":
case "generic_alarm_clock": case "generic_alarm_clock":
case "generic": 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) { private int getCategoryValue(NotificationType notificationType) {
@ -79,6 +91,10 @@ public class NotificationUpdateMessage extends GFDIMessage {
enum NotificationFlag { //was AncsEventFlag enum NotificationFlag { //was AncsEventFlag
BACKGROUND, BACKGROUND,
FOREGROUND, FOREGROUND,
UNK,
ACTION_ACCEPT, //only needed for legacy actions
ACTION_DECLINE, //only needed for legacy actions
} }
enum NotificationCategory { //was AncsCategory enum NotificationCategory { //was AncsCategory