1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-06-07 05:38:13 +02:00
Gadgetbridge/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/NotificationsHandler.java
2024-05-01 23:35:16 +01:00

334 lines
14 KiB
Java

package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin;
import android.util.SparseArray;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Locale;
import java.util.Map;
import java.util.Queue;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.GFDIMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.MessageWriter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.NotificationControlMessage;
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;
public class NotificationsHandler implements MessageHandler {
public static final SimpleDateFormat NOTIFICATION_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.ROOT);
private static final Logger LOG = LoggerFactory.getLogger(NotificationsHandler.class);
private final Queue<NotificationSpec> notificationSpecQueue;
private final Upload upload;
private boolean enabled = false;
public NotificationsHandler() {
this.notificationSpecQueue = new LinkedList<>();
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 boolean addNotificationToQueue(NotificationSpec notificationSpec) {
boolean found = false;
Iterator<NotificationSpec> iterator = notificationSpecQueue.iterator();
while (iterator.hasNext()) {
NotificationSpec e = iterator.next();
if (e.getId() == notificationSpec.getId()) {
found = true;
iterator.remove();
}
}
notificationSpecQueue.offer(notificationSpec); // Add the notificationSpec to the front of the queue
return found;
}
public NotificationUpdateMessage onSetCallState(CallSpec callSpec) {
if (!enabled)
return null;
if (callSpec.command == CallSpec.CALL_INCOMING) {
NotificationSpec callNotificationSpec = new NotificationSpec(callSpec.number.hashCode());
callNotificationSpec.phoneNumber = callSpec.number;
callNotificationSpec.sourceAppId = callSpec.sourceAppId;
callNotificationSpec.title = StringUtils.isEmpty(callSpec.name) ? callSpec.number : callSpec.name;
callNotificationSpec.type = NotificationType.GENERIC_PHONE;
callNotificationSpec.body = StringUtils.isEmpty(callSpec.name) ? callSpec.number : callSpec.name;
return onNotification(callNotificationSpec);
} else {
if (callSpec.number != null) // this happens in debug screen
return onDeleteNotification(callSpec.number.hashCode());
}
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);
}
}
return null;
}
private int getNotificationsCount(NotificationType notificationType) {
int count = 0;
for (NotificationSpec e : notificationSpecQueue) {
count += e.type == notificationType ? 1 : 0;
}
return count;
}
private NotificationSpec getNotificationSpecFromQueue(int id) {
for (NotificationSpec e : notificationSpecQueue) {
if (e.getId() == id) {
return e;
}
}
return null;
}
public GFDIMessage handle(GFDIMessage message) {
if (!enabled)
return null;
if (message instanceof NotificationControlMessage) {
final NotificationSpec notificationSpec = getNotificationSpecFromQueue(((NotificationControlMessage) message).getNotificationId());
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);
}
NotificationFragment notificationFragment = new NotificationFragment(messageWriter.getBytes());
return upload.setCurrentlyUploading(notificationFragment);
default:
LOG.error("NOT SUPPORTED");
}
}
} else if (message instanceof NotificationDataStatusMessage) {
return upload.processUploadProgress((NotificationDataStatusMessage) message);
}
return null;
}
public void setEnabled(boolean enable) {
this.enabled = enable;
}
public enum NotificationCommand { //was AncsCommand
GET_NOTIFICATION_ATTRIBUTES(0),
GET_APP_ATTRIBUTES(1),
PERFORM_NOTIFICATION_ACTION(2),
// Garmin extensions
PERFORM_ANDROID_ACTION(128);
public final int code;
NotificationCommand(int code) {
this.code = code;
}
public static NotificationCommand fromCode(int code) {
for (NotificationCommand value : values()) {
if (value.code == code)
return value;
}
throw new IllegalArgumentException("Unknown notification command " + code);
}
}
public enum NotificationAttribute { //was AncsAttribute
APP_IDENTIFIER(0),
TITLE(1, true),
SUBTITLE(2, true),
MESSAGE(3, true),
MESSAGE_SIZE(4),
DATE(5),
// POSITIVE_ACTION_LABEL(6),
// NEGATIVE_ACTION_LABEL(7),
// Garmin extensions
// PHONE_NUMBER(126, true),
ACTIONS(127, false, true),
;
private static final SparseArray<NotificationAttribute> valueByCode;
static {
final NotificationAttribute[] values = values();
valueByCode = new SparseArray<>(values.length);
for (NotificationAttribute value : values) {
valueByCode.append(value.code, value);
}
}
public final int code;
public final boolean hasLengthParam;
public final boolean hasAdditionalParams;
NotificationAttribute(int code) {
this(code, false, false);
}
NotificationAttribute(int code, boolean hasLengthParam) {
this(code, hasLengthParam, false);
}
NotificationAttribute(int code, boolean hasLengthParam, boolean hasAdditionalParams) {
this.code = code;
this.hasLengthParam = hasLengthParam;
this.hasAdditionalParams = hasAdditionalParams;
}
public static NotificationAttribute getByCode(int code) {
return valueByCode.get(code);
}
public byte[] getNotificationSpecAttribute(NotificationSpec notificationSpec, int maxLength) {
String toReturn = "";
switch (this) {
case DATE:
final long notificationTimestamp = notificationSpec.when == 0 ? System.currentTimeMillis() : notificationSpec.when;
toReturn = NOTIFICATION_DATE_FORMAT.format(new Date(notificationTimestamp));
break;
case TITLE:
toReturn = notificationSpec.title == null ? "" : notificationSpec.title;
break;
case SUBTITLE:
toReturn = notificationSpec.subject == null ? "" : notificationSpec.subject;
break;
case APP_IDENTIFIER:
toReturn = notificationSpec.sourceAppId == null ? "" : notificationSpec.sourceAppId;
break;
case MESSAGE:
toReturn = notificationSpec.body == null ? "" : notificationSpec.body;
break;
case MESSAGE_SIZE:
toReturn = Integer.toString(notificationSpec.body == null ? "".length() : notificationSpec.body.length());
break;
case ACTIONS:
toReturn = new String(new byte[]{0x00, 0x00, 0x00, 0x00});
break;
}
return toReturn.substring(0, Math.min(toReturn.length(), maxLength)).getBytes(StandardCharsets.UTF_8);
}
}
public static class Upload {
private NotificationFragment currentlyUploading;
public NotificationDataMessage setCurrentlyUploading(NotificationFragment currentlyUploading) {
this.currentlyUploading = currentlyUploading;
return currentlyUploading.take();
}
private GFDIMessage processUploadProgress(NotificationDataStatusMessage notificationDataStatusMessage) {
if (!currentlyUploading.dataHolder.hasRemaining()) {
this.currentlyUploading = null;
LOG.info("SENT ALL");
return new NotificationDataStatusMessage(GFDIMessage.GarminMessage.NOTIFICATION_DATA, GFDIMessage.Status.ACK, NotificationDataStatusMessage.TransferStatus.OK);
} else {
if (notificationDataStatusMessage.canProceed()) {
LOG.info("SENDING NEXT CHUNK!!!");
return currentlyUploading.take();
} else {
LOG.warn("Cannot proceed with upload"); //TODO: send the correct status message
this.currentlyUploading = null;
}
}
return null;
}
}
public static class NotificationFragment {
private final int dataSize;
private final ByteBuffer dataHolder;
private final int maxBlockSize = 300;
private int runningCrc;
NotificationFragment(byte[] contents) {
this.dataHolder = ByteBuffer.wrap(contents);
this.dataSize = contents.length;
this.dataHolder.flip();
this.dataHolder.compact();
this.setRunningCrc(0);
}
public int getDataSize() {
return dataSize;
}
private int getMaxBlockSize() {
return maxBlockSize;
}
private NotificationDataMessage take() {
final int currentOffset = this.dataHolder.position();
final byte[] chunk = new byte[Math.min(this.dataHolder.remaining(), getMaxBlockSize())];
this.dataHolder.get(chunk);
setRunningCrc(ChecksumCalculator.computeCrc(getRunningCrc(), chunk, 0, chunk.length));
return new NotificationDataMessage(chunk, getDataSize(), currentOffset, getRunningCrc());
}
private int getRunningCrc() {
return runningCrc;
}
private void setRunningCrc(int runningCrc) {
this.runningCrc = runningCrc;
}
}
}