Huawei: notifications: remove from watch and replies support

This commit is contained in:
Me7c7 2024-12-31 21:41:09 +02:00 committed by José Rebelo
parent 9ca2cd4e8f
commit 559ccda676
11 changed files with 427 additions and 30 deletions

View File

@ -654,6 +654,36 @@ public class HuaweiCoordinator {
return false;
}
public boolean supportsNotificationsTimestamp() {
if (supportsExpandCapability())
return supportsExpandCapability(77);
return false;
}
public boolean supportsNotificationsReply() {
if (supportsExpandCapability())
return supportsExpandCapability(73);
return false;
}
public boolean supportsNotificationsRepeatedNotify() {
if (supportsExpandCapability())
return supportsExpandCapability(94);
return false;
}
public boolean supportsNotificationsSyncKey() {
if (supportsExpandCapability())
return supportsExpandCapability(89);
return false;
}
public boolean supportsNotificationsRemoveSingle() {
if (supportsExpandCapability())
return supportsExpandCapability(120);
return false;
}
public boolean supportsPromptPushMessage () {
// do not ask for capabilities under specific condition

View File

@ -482,6 +482,8 @@ public class HuaweiPacket {
return new Notifications.NotificationConstraints.Response(paramsProvider).fromPacket(this);
case Notifications.NotificationCapabilities.id:
return new Notifications.NotificationCapabilities.Response(paramsProvider).fromPacket(this);
case Notifications.NotificationReply.id:
return new Notifications.NotificationReply.ReplyResponse(paramsProvider).fromPacket(this);
default:
return this;
}

View File

@ -16,6 +16,8 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets;
import android.text.TextUtils;
import java.nio.ByteBuffer;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
@ -24,15 +26,32 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
public class Notifications {
public static final byte id = 0x02;
public static final byte[] defaultConstraints = new byte[]{
0x00, 0x02, 0x00, 0x0F,
0x00, 0x00, 0x00, 0x02, 0x00, 0x1E,
0x00, 0x00, 0x00, 0x02, 0x00, 0x1E,
0x00, 0x00, 0x00, 0x02, 0x00, 0x1E
0x00, 0x02, 0x00, 0x0F,
0x00, 0x00, 0x00, 0x02, 0x00, 0x1E,
0x00, 0x00, 0x00, 0x02, 0x00, 0x1E,
0x00, 0x00, 0x00, 0x02, 0x00, 0x1E
};
public static class NotificationActionRequest extends HuaweiPacket {
public static final byte id = 0x01;
public static class AdditionalParams {
public boolean supportsSyncKey = false;
public boolean supportsRepeatedNotify = false;
public boolean supportsRemoveSingle = false;
public boolean supportsReply = false;
public boolean supportsTimestamp = false;
public String notificationKey = "";
public int notificationId = -1;
public String channelId = "";
public byte subscriptionId = 0;
public String address = "";
}
// TODO: support other types of notifications
// public static final int send = 0x01;
// public static final int notificationId = 0x01;
@ -54,13 +73,14 @@ public class Notifications {
public NotificationActionRequest(
ParamsProvider paramsProvider,
short notificationId,
short msgId,
byte notificationType,
int encoding,
String titleContent,
String senderContent,
String bodyContent,
String sourceAppId
String sourceAppId,
AdditionalParams addParams
) {
super(paramsProvider);
@ -70,7 +90,7 @@ public class Notifications {
// TODO: Add notification information per type if necessary
this.tlv = new HuaweiTLV()
.put(0x01, notificationId)
.put(0x01, msgId)
.put(0x02, notificationType)
.put(0x03, true); // This used to be vibrate, but doesn't work
@ -105,6 +125,32 @@ public class Notifications {
if (sourceAppId != null)
this.tlv.put(0x11, sourceAppId);
if(addParams != null) {
if (addParams.supportsSyncKey)
this.tlv.put(0x18, (addParams.notificationKey != null) ? addParams.notificationKey : "");
//this.tlv.put(0x12, "msg"); //"msg" or "imcall", maybe other - category, if not empty and productType>=34
//if(addParams.repeatedNotifySupports) {
// this.tlv.put(0x13, 0); // 0x13 - reminder 15 = vibrate, 0 - default
//}
if (addParams.supportsReply && notificationType == NotificationType.sms) {
this.tlv.put(0x14, addParams.subscriptionId);
this.tlv.put(0x17, addParams.address);
}
if (addParams.supportsRepeatedNotify || addParams.supportsRemoveSingle) {
this.tlv.put(0x19, (addParams.notificationKey != null) ? addParams.notificationKey : "");
this.tlv.put(0x20, addParams.notificationId);
this.tlv.put(0x1d, (addParams.channelId != null) ? addParams.channelId : "");
}
if (addParams.supportsTimestamp) {
this.tlv.put(0x15, (int) (System.currentTimeMillis() / 1000));
}
}
this.complete = true;
}
}
@ -118,7 +164,7 @@ public class Notifications {
this.serviceId = Notifications.id;
this.commandId = id;
this.tlv = new HuaweiTLV()
.put(0x01);
.put(0x01);
this.complete = true;
}
}
@ -158,23 +204,23 @@ public class Notifications {
.getObject(0x90);
for (HuaweiTLV subContainer : container.getObjects(0x91)) {
if (subContainer.getByte(0x12) == 0x01) {
putByteBuffer(constraints, NotificationConstraintsType.contentFormat, new byte[] {0x02}); //Always 0x02 even if gadget report 0x03
putByteBuffer(constraints, NotificationConstraintsType.contentFormat, new byte[]{0x02}); //Always 0x02 even if gadget report 0x03
putByteBuffer(constraints, NotificationConstraintsType.contentLength, subContainer.getBytes(0x14));
}
if (subContainer.getByte(0x12) == 0x05) {
constraints.putShort(NotificationConstraintsType.yellowPagesSupport,(short)0x01);
putByteBuffer(constraints, NotificationConstraintsType.yellowPagesFormat,subContainer.getBytes(0x13));
putByteBuffer(constraints, NotificationConstraintsType.yellowPagesLength,subContainer.getBytes(0x14));
constraints.putShort(NotificationConstraintsType.yellowPagesSupport, (short) 0x01);
putByteBuffer(constraints, NotificationConstraintsType.yellowPagesFormat, subContainer.getBytes(0x13));
putByteBuffer(constraints, NotificationConstraintsType.yellowPagesLength, subContainer.getBytes(0x14));
}
if (subContainer.getByte(0x12) == 0x06) {
constraints.putShort(NotificationConstraintsType.contentSignSupport,(short)0x01);
putByteBuffer(constraints, NotificationConstraintsType.contentSignFormat,subContainer.getBytes(0x13));
putByteBuffer(constraints, NotificationConstraintsType.contentSignLength,subContainer.getBytes(0x14));
constraints.putShort(NotificationConstraintsType.contentSignSupport, (short) 0x01);
putByteBuffer(constraints, NotificationConstraintsType.contentSignFormat, subContainer.getBytes(0x13));
putByteBuffer(constraints, NotificationConstraintsType.contentSignLength, subContainer.getBytes(0x14));
}
if (subContainer.getByte(0x12) == 0x07 ) {
constraints.putShort(NotificationConstraintsType.incomingNumberSupport,(short)0x01);
putByteBuffer(constraints, NotificationConstraintsType.incomingNumberFormat,subContainer.getBytes(0x13));
putByteBuffer(constraints, NotificationConstraintsType.incomingNumberLength,subContainer.getBytes(0x14));
if (subContainer.getByte(0x12) == 0x07) {
constraints.putShort(NotificationConstraintsType.incomingNumberSupport, (short) 0x01);
putByteBuffer(constraints, NotificationConstraintsType.incomingNumberFormat, subContainer.getBytes(0x13));
putByteBuffer(constraints, NotificationConstraintsType.incomingNumberLength, subContainer.getBytes(0x14));
}
}
constraints.rewind();
@ -250,7 +296,7 @@ public class Notifications {
public static class Request extends HuaweiPacket {
public Request(
ParamsProvider paramsProvider
){
) {
super(paramsProvider);
this.serviceId = Notifications.id;
this.commandId = id;
@ -278,6 +324,36 @@ public class Notifications {
}
}
public static class NotificationRemoveAction extends HuaweiPacket {
public static final byte id = 0x06;
public NotificationRemoveAction(
ParamsProvider paramsProvider,
byte msgType,
String sourceAppId,
String notificationKey,
int notificationId,
String notificationChannelId,
String notificationCategory
) {
super(paramsProvider);
this.serviceId = Notifications.id;
this.commandId = id;
this.tlv = new HuaweiTLV()
.put(0x01, msgType)
.put(0x02, sourceAppId)
.put(0x03, notificationKey)
.put(0x04, notificationId)
.put(0x05, notificationChannelId);
if (notificationCategory != null && !TextUtils.isEmpty(notificationCategory))
this.tlv.put(0x06, notificationCategory); // category
this.complete = true;
}
}
public static class WearMessagePushRequest extends HuaweiPacket {
public static final byte id = 0x08;
@ -297,4 +373,62 @@ public class Notifications {
this.complete = true;
}
}
public static class NotificationReply {
public static final byte id = 0x10;
public static class ReplyResponse extends HuaweiPacket {
public int type = 0;
public int encoding = 0; // 3 - "utf-16"
public int subId = 0;
public String sender;
public String key;
public String text;
public ReplyResponse(ParamsProvider paramsProvider) {
super(paramsProvider);
this.serviceId = MusicControl.id;
this.commandId = id;
}
@Override
public void parseTlv() throws ParseException {
if (this.tlv.contains(0x01))
this.type = this.tlv.getAsInteger(0x01);
if (this.tlv.contains(0x02))
this.encoding = this.tlv.getAsInteger(0x02);
if (this.tlv.contains(0x03))
this.subId = this.tlv.getAsInteger(0x03);
if (this.tlv.contains(0x04))
this.key = this.tlv.getString(0x04);
if (this.tlv.contains(0x05))
this.sender = this.tlv.getString(0x05);
if (this.tlv.contains(0x06))
this.text = this.tlv.getString(0x06);
}
}
//TODO: send ack if required, 7f on error.
public static class ReplyAck extends HuaweiPacket {
public ReplyAck(
ParamsProvider paramsProvider,
byte code
) {
super(paramsProvider);
this.serviceId = Notifications.id;
this.commandId = id;
this.tlv = new HuaweiTLV()
.put(0x07, code);
this.complete = true;
}
}
}
}

View File

@ -57,6 +57,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.GpsAndTime;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Menstrual;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.MusicControl;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileUpload;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Notifications;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.P2P;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Watchface;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Weather;
@ -127,6 +128,7 @@ public class AsynchronousResponse {
handleEphemeris(response);
handleEphemerisUploadService(response);
handleAsyncBattery(response);
handleNotifications(response);
} catch (Request.ResponseParseException e) {
LOG.error("Response parse exception", e);
}
@ -725,4 +727,14 @@ public class AsynchronousResponse {
}
}
}
private void handleNotifications(HuaweiPacket response) {
if (response.serviceId == Notifications.id && response.commandId == Notifications.NotificationReply.id) {
if (!(response instanceof Notifications.NotificationReply.ReplyResponse)) {
return;
}
LOG.info("Notification response");
support.getHuaweiNotificationsManager().onReplyResponse((Notifications.NotificationReply.ReplyResponse) response);
}
}
}

View File

@ -102,6 +102,11 @@ public class HuaweiBRSupport extends AbstractBTBRDeviceSupport {
supportProvider.onNotification(notificationSpec);
}
@Override
public void onDeleteNotification(int id) {
supportProvider.onDeleteNotification(id);
}
@Override
public void onSetTime() {
supportProvider.onSetTime();

View File

@ -0,0 +1,140 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei;
import android.text.TextUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Queue;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventNotificationControl;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Notifications;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendNotificationRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendNotificationRemoveRequest;
public class HuaweiNotificationsManager {
private static final Logger LOG = LoggerFactory.getLogger(HuaweiNotificationsManager.class);
private final HuaweiSupportProvider support;
private final Queue<NotificationSpec> notificationSpecCache = new LinkedList<>();
public HuaweiNotificationsManager(HuaweiSupportProvider support) {
this.support = support;
}
private void addNotificationToCache(NotificationSpec notificationSpec) {
// TODO: rewrite this
if (notificationSpecCache.size() > 10)
notificationSpecCache.poll();
Iterator<NotificationSpec> iterator = notificationSpecCache.iterator();
while (iterator.hasNext()) {
NotificationSpec e = iterator.next();
if (e.getId() == notificationSpec.getId()) {
iterator.remove();
}
}
notificationSpecCache.offer(notificationSpec);
}
public void onNotification(NotificationSpec notificationSpec) {
addNotificationToCache(notificationSpec);
SendNotificationRequest sendNotificationReq = new SendNotificationRequest(this.support);
try {
sendNotificationReq.buildNotificationTLVFromNotificationSpec(notificationSpec);
sendNotificationReq.doPerform();
} catch (IOException e) {
LOG.error("Sending notification failed", e);
}
}
public void onDeleteNotification(int id) {
if (!support.getHuaweiCoordinator().supportsNotificationsRepeatedNotify() && !support.getHuaweiCoordinator().supportsNotificationsRemoveSingle()) {
LOG.info("Delete notification is not supported");
return;
}
NotificationSpec notificationSpec = null;
Iterator<NotificationSpec> iterator = notificationSpecCache.iterator();
while (iterator.hasNext()) {
notificationSpec = iterator.next();
if (notificationSpec.getId() == id) {
iterator.remove();
break;
}
}
if (notificationSpec == null) {
LOG.info("Notification is not found");
return;
}
try {
SendNotificationRemoveRequest sendNotificationReq = new SendNotificationRemoveRequest(this.support,
SendNotificationRequest.getNotificationType(notificationSpec.type), // notificationType
notificationSpec.sourceAppId,
notificationSpec.key,
id,
"", // TODO:
null);
sendNotificationReq.doPerform();
} catch (IOException e) {
LOG.error("Sending notification remove failed", e);
}
}
void onReplyResponse(Notifications.NotificationReply.ReplyResponse response) {
LOG.info(" KEY: {}, Text: {}", response.key, response.text);
if(!this.support.getHuaweiCoordinator().supportsNotificationsReply()) {
LOG.info("Reply is not supported");
return;
}
if (TextUtils.isEmpty(response.key) || TextUtils.isEmpty(response.text)) {
LOG.info("Reply is empty");
return;
}
if(response.type != 1 && response.type != 2) {
LOG.info("Reply: only type 1 and 2 supported");
return;
}
NotificationSpec notificationSpec = null;
for (NotificationSpec spec : notificationSpecCache) {
notificationSpec = spec;
if (notificationSpec.key.equals(response.key)) {
break;
}
}
if (notificationSpec == null) {
LOG.info("Notification for reply is not found");
return;
}
final GBDeviceEventNotificationControl deviceEvtNotificationControl = new GBDeviceEventNotificationControl();
deviceEvtNotificationControl.handle = notificationSpec.getId();
deviceEvtNotificationControl.event = GBDeviceEventNotificationControl.Event.REPLY;
deviceEvtNotificationControl.reply = response.text;
if (notificationSpec.type.equals(NotificationType.GENERIC_PHONE) || notificationSpec.type.equals(NotificationType.GENERIC_SMS)) {
deviceEvtNotificationControl.phoneNumber = notificationSpec.phoneNumber;
} else {
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) {
deviceEvtNotificationControl.handle = action.handle; //handle of wearable action is needed
break;
}
}
}
}
this.support.evaluateGBDeviceEvent(deviceEvtNotificationControl);
//TODO: maybe should be send reply. Service: 0x2, command: 0x10, tlv 7 and/or 1, type byte, 7f on error
}
}

View File

@ -245,9 +245,12 @@ public class HuaweiSupportProvider {
protected HuaweiMusicManager huaweiMusicManager = new HuaweiMusicManager(this);
protected HuaweiNotificationsManager huaweiNotificationsManager = new HuaweiNotificationsManager(this);
//TODO: we need only one instance of manager and all it services.
protected HuaweiP2PManager huaweiP2PManager = new HuaweiP2PManager(this);
public HuaweiCoordinatorSupplier getCoordinator() {
return ((HuaweiCoordinatorSupplier) this.gbDevice.getDeviceCoordinator());
}
@ -276,6 +279,10 @@ public class HuaweiSupportProvider {
return huaweiEphemerisManager;
}
public HuaweiNotificationsManager getHuaweiNotificationsManager() {
return huaweiNotificationsManager;
}
public HuaweiMusicManager getHuaweiMusicManager() {
return huaweiMusicManager;
}
@ -1408,21 +1415,17 @@ public class HuaweiSupportProvider {
}
return msgId;
}
public void onNotification(NotificationSpec notificationSpec) {
if (!GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()).getBoolean(DeviceSettingsPreferenceConst.PREF_NOTIFICATION_ENABLE, false)) {
// Don't send notifications when they are disabled
LOG.info("Stopped notification as they are disabled.");
return;
}
huaweiNotificationsManager.onNotification(notificationSpec);
}
SendNotificationRequest sendNotificationReq = new SendNotificationRequest(this);
try {
sendNotificationReq.buildNotificationTLVFromNotificationSpec(notificationSpec);
sendNotificationReq.doPerform();
} catch (IOException e) {
LOG.error("Sending notification failed", e);
}
public void onDeleteNotification(int id) {
huaweiNotificationsManager.onDeleteNotification(id);
}
public void setDateFormat() {

View File

@ -0,0 +1,55 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Notifications;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
public class SendNotificationRemoveRequest extends Request {
private static final Logger LOG = LoggerFactory.getLogger(SendNotificationRemoveRequest.class);
private final byte notificationType;
private final String sourceAppId;
private final String notificationKey;
private final int notificationId;
private final String notificationChannelId;
private final String notificationCategory;
public SendNotificationRemoveRequest(HuaweiSupportProvider support, byte notificationType, String sourceAppId, String notificationKey, int notificationId, String notificationChannelId, String notificationCategory) {
super(support);
this.serviceId = Notifications.id;
this.commandId = Notifications.NotificationRemoveAction.id;
this.notificationType = notificationType;
this.sourceAppId = sourceAppId;
this.notificationKey = notificationKey;
this.notificationId = notificationId;
this.notificationChannelId = notificationChannelId;
this.notificationCategory = notificationCategory;
}
@Override
protected List<byte[]> createRequest() throws RequestCreationException {
try {
return new Notifications.NotificationRemoveAction(
paramsProvider,
this.notificationType,
this.sourceAppId,
this.notificationKey,
this.notificationId,
this.notificationChannelId,
this.notificationCategory).serialize();
} catch (HuaweiPacket.CryptoException e) {
throw new RequestCreationException(e);
}
}
@Override
protected void processResponse() {
LOG.debug("handle NotificationRemove");
}
}

View File

@ -66,6 +66,18 @@ public class SendNotificationRequest extends Request {
body = notificationSpec.body.substring(0x0, supportProvider.getHuaweiCoordinator().getContentLength() - 0xD);
body += "...";
}
Notifications.NotificationActionRequest.AdditionalParams params = new Notifications.NotificationActionRequest.AdditionalParams();
params.supportsSyncKey = supportProvider.getHuaweiCoordinator().supportsNotificationsSyncKey();
params.supportsRepeatedNotify = supportProvider.getHuaweiCoordinator().supportsNotificationsRepeatedNotify();
params.supportsRemoveSingle = supportProvider.getHuaweiCoordinator().supportsNotificationsRemoveSingle();
params.supportsReply = supportProvider.getHuaweiCoordinator().supportsNotificationsReply();
params.supportsTimestamp = supportProvider.getHuaweiCoordinator().supportsNotificationsTimestamp();
params.notificationId = notificationSpec.getId();
params.notificationKey = notificationSpec.key;
this.packet = new Notifications.NotificationActionRequest(
paramsProvider,
supportProvider.getNotificationId(),
@ -74,7 +86,8 @@ public class SendNotificationRequest extends Request {
title,
notificationSpec.sender,
body,
notificationSpec.sourceAppId
notificationSpec.sourceAppId,
params
);
}
@ -88,6 +101,7 @@ public class SendNotificationRequest extends Request {
callSpec.name,
callSpec.name,
callSpec.name,
null,
null
);
}

View File

@ -41,6 +41,7 @@ public class StopNotificationRequest extends Request {
null,
null,
null,
null,
null
).serialize();
} catch (HuaweiPacket.CryptoException e) {

View File

@ -106,7 +106,8 @@ public class TestNotifications {
titleContent,
senderContent,
bodyContent,
sourceAppId
sourceAppId,
null
);
Assert.assertEquals(0x02, request.serviceId);