Garmin/Zepp OS: Background notifications

This commit is contained in:
José Rebelo 2024-12-24 16:33:48 +00:00
parent f99b43fc56
commit 0a9d08f67b
12 changed files with 92 additions and 26 deletions

View File

@ -875,6 +875,11 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
return R.drawable.ic_device_default_disabled; return R.drawable.ic_device_default_disabled;
} }
@Override
public boolean supportsBackgroundNotifications(final GBDevice device) {
return false;
}
@Override @Override
public boolean supportsNotificationVibrationPatterns() { public boolean supportsNotificationVibrationPatterns() {
return false; return false;

View File

@ -786,6 +786,11 @@ public interface DeviceCoordinator {
@DrawableRes @DrawableRes
int getDisabledIconResource(); int getDisabledIconResource();
/**
* Whether the device supports silent background notifications.
*/
boolean supportsBackgroundNotifications(GBDevice device);
/** /**
* Whether the device supports a variety of vibration patterns for notifications. * Whether the device supports a variety of vibration patterns for notifications.
*/ */

View File

@ -372,6 +372,11 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
return true; return true;
} }
@Override
public boolean supportsBackgroundNotifications(final GBDevice device) {
return true;
}
@Override @Override
public int getCannedRepliesSlotCount(final GBDevice device) { public int getCannedRepliesSlotCount(final GBDevice device) {
if (getPrefs(device).getBoolean(GarminPreferences.PREF_FEAT_CANNED_MESSAGES, false)) { if (getPrefs(device).getBoolean(GarminPreferences.PREF_FEAT_CANNED_MESSAGES, false)) {

View File

@ -68,6 +68,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.service
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsContactsService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsContactsService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsLogsService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsLogsService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsLoyaltyCardService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsLoyaltyCardService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsNotificationService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsRemindersService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsRemindersService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsShortcutCardsService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsShortcutCardsService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsConfigService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsConfigService;
@ -621,6 +622,11 @@ public abstract class ZeppOsCoordinator extends HuamiCoordinator {
return getPrefs(device).getBoolean("zepp_os_experimental_features", false); return getPrefs(device).getBoolean("zepp_os_experimental_features", false);
} }
@Override
public boolean supportsBackgroundNotifications(final GBDevice device) {
return ZeppOsNotificationService.supportsBackgroundNotifications(getPrefs(device));
}
@Override @Override
public boolean validateAuthKey(final String authKey) { public boolean validateAuthKey(final String authKey) {
final byte[] authKeyBytes = authKey.trim().getBytes(); final byte[] authKeyBytes = authKey.trim().getBytes();

View File

@ -54,7 +54,6 @@ 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.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
@ -354,6 +353,8 @@ public class NotificationListener extends NotificationListenerService {
return; return;
} }
NotificationSpec notificationSpec = new NotificationSpec();
// Ignore too frequent notifications, according to user preference // Ignore too frequent notifications, according to user preference
long curTime = System.nanoTime(); long curTime = System.nanoTime();
Long notificationBurstPreventionValue = notificationBurstPrevention.get(source); Long notificationBurstPreventionValue = notificationBurstPrevention.get(source);
@ -361,11 +362,10 @@ public class NotificationListener extends NotificationListenerService {
long diff = curTime - notificationBurstPreventionValue; long diff = curTime - notificationBurstPreventionValue;
if (diff < TimeUnit.SECONDS.toNanos(prefs.getInt("notifications_timeout", 0))) { if (diff < TimeUnit.SECONDS.toNanos(prefs.getInt("notifications_timeout", 0))) {
LOG.info("Ignoring frequent notification, last one was {} ms ago", TimeUnit.NANOSECONDS.toMillis(diff)); LOG.info("Ignoring frequent notification, last one was {} ms ago", TimeUnit.NANOSECONDS.toMillis(diff));
return; notificationSpec.background = true;
} }
} }
NotificationSpec notificationSpec = new NotificationSpec();
notificationSpec.key = sbn.getKey(); notificationSpec.key = sbn.getKey();
notificationSpec.when = notification.when; notificationSpec.when = notification.when;

View File

@ -180,7 +180,8 @@ public class GBDeviceService implements DeviceService {
.putExtra(EXTRA_NOTIFICATION_SOURCEAPPID, notificationSpec.sourceAppId) .putExtra(EXTRA_NOTIFICATION_SOURCEAPPID, notificationSpec.sourceAppId)
.putExtra(EXTRA_NOTIFICATION_ICONID, notificationSpec.iconId) .putExtra(EXTRA_NOTIFICATION_ICONID, notificationSpec.iconId)
.putExtra(NOTIFICATION_PICTURE_PATH, notificationSpec.picturePath) .putExtra(NOTIFICATION_PICTURE_PATH, notificationSpec.picturePath)
.putExtra(EXTRA_NOTIFICATION_DNDSUPPRESSED, notificationSpec.dndSuppressed); .putExtra(EXTRA_NOTIFICATION_DNDSUPPRESSED, notificationSpec.dndSuppressed)
.putExtra(EXTRA_NOTIFICATION_BACKGROUND, notificationSpec.background);
invokeService(intent); invokeService(intent);
} }

View File

@ -101,6 +101,7 @@ public interface DeviceService extends EventHandler {
String EXTRA_NOTIFICATION_ICONID = "notification_iconid"; String EXTRA_NOTIFICATION_ICONID = "notification_iconid";
String NOTIFICATION_PICTURE_PATH = "notification_picture_path"; String NOTIFICATION_PICTURE_PATH = "notification_picture_path";
String EXTRA_NOTIFICATION_DNDSUPPRESSED = "notification_dndsuppressed"; String EXTRA_NOTIFICATION_DNDSUPPRESSED = "notification_dndsuppressed";
String EXTRA_NOTIFICATION_BACKGROUND = "notification_background";
String EXTRA_FIND_START = "find_start"; String EXTRA_FIND_START = "find_start";
String EXTRA_VIBRATION_INTENSITY = "vibration_intensity"; String EXTRA_VIBRATION_INTENSITY = "vibration_intensity";
String EXTRA_CALL_COMMAND = "call_command"; String EXTRA_CALL_COMMAND = "call_command";

View File

@ -56,6 +56,8 @@ public class NotificationSpec {
public int dndSuppressed; public int dndSuppressed;
public boolean background;
public NotificationSpec() { public NotificationSpec() {
this(-1); this(-1);
} }

View File

@ -813,6 +813,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
} }
DeviceSupport deviceSupport = getDeviceSupport(device); DeviceSupport deviceSupport = getDeviceSupport(device);
DeviceCoordinator coordinator = getDeviceCoordinator(device);
Prefs devicePrefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(device.getAddress())); Prefs devicePrefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(device.getAddress()));
@ -861,6 +862,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
notificationSpec.iconId = intentCopy.getIntExtra(EXTRA_NOTIFICATION_ICONID, 0); notificationSpec.iconId = intentCopy.getIntExtra(EXTRA_NOTIFICATION_ICONID, 0);
notificationSpec.picturePath = intent.getStringExtra(NOTIFICATION_PICTURE_PATH); notificationSpec.picturePath = intent.getStringExtra(NOTIFICATION_PICTURE_PATH);
notificationSpec.dndSuppressed = intentCopy.getIntExtra(EXTRA_NOTIFICATION_DNDSUPPRESSED, 0); notificationSpec.dndSuppressed = intentCopy.getIntExtra(EXTRA_NOTIFICATION_DNDSUPPRESSED, 0);
notificationSpec.background = intentCopy.getBooleanExtra(EXTRA_NOTIFICATION_BACKGROUND, false);
if (notificationSpec.type == NotificationType.GENERIC_SMS && notificationSpec.phoneNumber != null) { if (notificationSpec.type == NotificationType.GENERIC_SMS && notificationSpec.phoneNumber != null) {
GBApplication.getIDSenderLookup().add(notificationSpec.getId(), notificationSpec.phoneNumber); GBApplication.getIDSenderLookup().add(notificationSpec.getId(), notificationSpec.phoneNumber);
@ -882,7 +884,9 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
notificationSpec.cannedReplies = replies.toArray(new String[0]); notificationSpec.cannedReplies = replies.toArray(new String[0]);
} }
if (!notificationSpec.background || coordinator.supportsBackgroundNotifications(device)) {
deviceSupport.onNotification(notificationSpec); deviceSupport.onNotification(notificationSpec);
}
break; break;
} }
case ACTION_DELETE_NOTIFICATION: { case ACTION_DELETE_NOTIFICATION: {

View File

@ -118,7 +118,15 @@ public class NotificationsHandler implements MessageHandler {
} }
final boolean hasPicture = !StringUtils.isEmpty(notificationSpec.picturePath); final boolean hasPicture = !StringUtils.isEmpty(notificationSpec.picturePath);
return new NotificationUpdateMessage(notificationUpdateType, notificationSpec.type, getNotificationsCount(notificationSpec.type), notificationSpec.getId(), hasActions, hasPicture); return new NotificationUpdateMessage(
notificationUpdateType,
notificationSpec.type,
getNotificationsCount(notificationSpec.type),
notificationSpec.getId(),
notificationSpec.background,
hasActions,
hasPicture
);
} }
private int getNotificationsCount(NotificationType notificationType) { private int getNotificationsCount(NotificationType notificationType) {
@ -155,7 +163,14 @@ public class NotificationsHandler implements MessageHandler {
NotificationSpec e = iterator.next(); NotificationSpec e = iterator.next();
if (e.getId() == id) { if (e.getId() == id) {
iterator.remove(); iterator.remove();
return new NotificationUpdateMessage(NotificationUpdateMessage.NotificationUpdateType.REMOVE, e.type, getNotificationsCount(e.type), id, false, false); return new NotificationUpdateMessage(
NotificationUpdateMessage.NotificationUpdateType.REMOVE,
e.type, getNotificationsCount(e.type),
id,
false,
false,
false
);
} }
} }
return null; return null;

View File

@ -13,15 +13,23 @@ public class NotificationUpdateMessage extends GFDIMessage {
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 hasActions;
final private boolean background;
final private boolean hasPicture; final private boolean hasPicture;
final private boolean useLegacyActions = false; final private boolean useLegacyActions = false;
public NotificationUpdateMessage(NotificationUpdateType notificationUpdateType, NotificationType notificationType, int count, int notificationId, boolean hasActions, boolean hasPicture) { public NotificationUpdateMessage(NotificationUpdateType notificationUpdateType,
NotificationType notificationType,
int count,
int notificationId,
boolean background,
boolean hasActions,
boolean hasPicture) {
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.background = background;
this.hasActions = hasActions; this.hasActions = hasActions;
this.hasPicture = hasPicture; this.hasPicture = hasPicture;
} }
@ -32,7 +40,7 @@ public class NotificationUpdateMessage extends GFDIMessage {
writer.writeShort(0); // packet size will be filled below writer.writeShort(0); // packet size will be filled below
writer.writeShort(this.garminMessage.getId()); writer.writeShort(this.garminMessage.getId());
writer.writeByte(this.notificationUpdateType.ordinal()); writer.writeByte(this.notificationUpdateType.ordinal());
writer.writeByte(getCategoryFlags(this.notificationType)); writer.writeByte(getCategoryFlags(this.notificationType, this.background));
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);
@ -54,13 +62,16 @@ public class NotificationUpdateMessage extends GFDIMessage {
} }
private int getCategoryFlags(NotificationType notificationType) { private int getCategoryFlags(NotificationType notificationType, boolean background) {
EnumSet<NotificationFlag> flags = EnumSet.noneOf(NotificationFlag.class); EnumSet<NotificationFlag> flags = EnumSet.noneOf(NotificationFlag.class);
if (this.hasActions && this.useLegacyActions) { //only needed for legacy actions if (this.hasActions && this.useLegacyActions) { //only needed for legacy actions
flags.add(NotificationFlag.ACTION_ACCEPT); flags.add(NotificationFlag.ACTION_ACCEPT);
} }
flags.add(NotificationFlag.ACTION_DECLINE); flags.add(NotificationFlag.ACTION_DECLINE);
if (background) {
flags.add(NotificationFlag.BACKGROUND);
} else {
switch (notificationType.getGenericType()) { switch (notificationType.getGenericType()) {
case "generic_phone": case "generic_phone":
case "generic_email": case "generic_email":
@ -76,6 +87,8 @@ public class NotificationUpdateMessage extends GFDIMessage {
// to be foreground, sending them as background was generating bug reports. // to be foreground, sending them as background was generating bug reports.
flags.add(NotificationFlag.FOREGROUND); flags.add(NotificationFlag.FOREGROUND);
} }
}
return (int) EnumUtils.generateBitVector(NotificationFlag.class, flags); return (int) EnumUtils.generateBitVector(NotificationFlag.class, flags);
} }

View File

@ -39,6 +39,7 @@ import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventNotificationControl; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventNotificationControl;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
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;
@ -47,9 +48,9 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.AbstractZeppOsService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.AbstractZeppOsService;
import nodomain.freeyourgadget.gadgetbridge.util.BitmapUtil; import nodomain.freeyourgadget.gadgetbridge.util.BitmapUtil;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue; import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue;
import nodomain.freeyourgadget.gadgetbridge.util.NotificationUtils; import nodomain.freeyourgadget.gadgetbridge.util.NotificationUtils;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
public class ZeppOsNotificationService extends AbstractZeppOsService { public class ZeppOsNotificationService extends AbstractZeppOsService {
@ -78,6 +79,8 @@ public class ZeppOsNotificationService extends AbstractZeppOsService {
public static final byte NOTIFICATION_CALL_STATE_START = 0x00; public static final byte NOTIFICATION_CALL_STATE_START = 0x00;
public static final byte NOTIFICATION_CALL_STATE_END = 0x02; public static final byte NOTIFICATION_CALL_STATE_END = 0x02;
public static final String PREF_VERSION = "zepp_os_notifications_version";
private int version = -1; private int version = -1;
private boolean supportsPictures = false; private boolean supportsPictures = false;
private boolean supportsNotificationKey = false; private boolean supportsNotificationKey = false;
@ -121,6 +124,7 @@ public class ZeppOsNotificationService extends AbstractZeppOsService {
switch (cmd) { switch (cmd) {
case NOTIFICATION_CMD_CAPABILITIES_RESPONSE: { case NOTIFICATION_CMD_CAPABILITIES_RESPONSE: {
version = buf.get() & 0xff; version = buf.get() & 0xff;
getSupport().evaluateGBDeviceEvent(new GBDeviceEventUpdatePreferences(PREF_VERSION, version));
if (version < 4 || version > 5) { if (version < 4 || version > 5) {
// Untested, might work, might not.. // Untested, might work, might not..
LOG.warn("Unsupported notification service version {}", version); LOG.warn("Unsupported notification service version {}", version);
@ -357,7 +361,7 @@ public class ZeppOsNotificationService extends AbstractZeppOsService {
baos.write((byte) (hasReply ? 1 : 0)); baos.write((byte) (hasReply ? 1 : 0));
if (version >= 5) { if (version >= 5) {
baos.write(0); // 1 for silent baos.write(notificationSpec.background ? 1 : 0); // 1 for silent
} }
if (supportsPictures) { if (supportsPictures) {
baos.write((byte) (notificationSpec.picturePath != null ? 1 : 0)); baos.write((byte) (notificationSpec.picturePath != null ? 1 : 0));
@ -597,4 +601,9 @@ public class ZeppOsNotificationService extends AbstractZeppOsService {
return null; return null;
} }
} }
public static boolean supportsBackgroundNotifications(final Prefs devicePrefs) {
final int version = devicePrefs.getInt(PREF_VERSION, 0);
return version >= 5;
}
} }