From 0a9d08f67b9af8d76ddade531f514a3e6965144a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Tue, 24 Dec 2024 16:33:48 +0000 Subject: [PATCH] Garmin/Zepp OS: Background notifications --- .../devices/AbstractDeviceCoordinator.java | 5 ++ .../devices/DeviceCoordinator.java | 5 ++ .../devices/garmin/GarminCoordinator.java | 5 ++ .../huami/zeppos/ZeppOsCoordinator.java | 6 +++ .../externalevents/NotificationListener.java | 6 +-- .../gadgetbridge/impl/GBDeviceService.java | 3 +- .../gadgetbridge/model/DeviceService.java | 1 + .../gadgetbridge/model/NotificationSpec.java | 2 + .../service/DeviceCommunicationService.java | 6 ++- .../devices/garmin/NotificationsHandler.java | 19 +++++++- .../messages/NotificationUpdateMessage.java | 47 ++++++++++++------- .../services/ZeppOsNotificationService.java | 13 ++++- 12 files changed, 92 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java index eff63dc78..79076baa8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinator.java @@ -875,6 +875,11 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator { return R.drawable.ic_device_default_disabled; } + @Override + public boolean supportsBackgroundNotifications(final GBDevice device) { + return false; + } + @Override public boolean supportsNotificationVibrationPatterns() { return false; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java index 5b1d7fee6..7fa908dba 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/DeviceCoordinator.java @@ -786,6 +786,11 @@ public interface DeviceCoordinator { @DrawableRes int getDisabledIconResource(); + /** + * Whether the device supports silent background notifications. + */ + boolean supportsBackgroundNotifications(GBDevice device); + /** * Whether the device supports a variety of vibration patterns for notifications. */ diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminCoordinator.java index c2dd946ac..082d5d4eb 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminCoordinator.java @@ -372,6 +372,11 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator { return true; } + @Override + public boolean supportsBackgroundNotifications(final GBDevice device) { + return true; + } + @Override public int getCannedRepliesSlotCount(final GBDevice device) { if (getPrefs(device).getBoolean(GarminPreferences.PREF_FEAT_CANNED_MESSAGES, false)) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/zeppos/ZeppOsCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/zeppos/ZeppOsCoordinator.java index 5fa92ea10..854014992 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/zeppos/ZeppOsCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/zeppos/ZeppOsCoordinator.java @@ -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.ZeppOsLogsService; 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.ZeppOsShortcutCardsService; 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); } + @Override + public boolean supportsBackgroundNotifications(final GBDevice device) { + return ZeppOsNotificationService.supportsBackgroundNotifications(getPrefs(device)); + } + @Override public boolean validateAuthKey(final String authKey) { final byte[] authKeyBytes = authKey.trim().getBytes(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java index cb2754f90..36219c245 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java @@ -54,7 +54,6 @@ import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -354,6 +353,8 @@ public class NotificationListener extends NotificationListenerService { return; } + NotificationSpec notificationSpec = new NotificationSpec(); + // Ignore too frequent notifications, according to user preference long curTime = System.nanoTime(); Long notificationBurstPreventionValue = notificationBurstPrevention.get(source); @@ -361,11 +362,10 @@ public class NotificationListener extends NotificationListenerService { long diff = curTime - notificationBurstPreventionValue; if (diff < TimeUnit.SECONDS.toNanos(prefs.getInt("notifications_timeout", 0))) { 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.when = notification.when; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java index b88bd98ed..5578b660e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java @@ -180,7 +180,8 @@ public class GBDeviceService implements DeviceService { .putExtra(EXTRA_NOTIFICATION_SOURCEAPPID, notificationSpec.sourceAppId) .putExtra(EXTRA_NOTIFICATION_ICONID, notificationSpec.iconId) .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); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java index 1379eee48..3c45c7b2f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java @@ -101,6 +101,7 @@ public interface DeviceService extends EventHandler { String EXTRA_NOTIFICATION_ICONID = "notification_iconid"; String NOTIFICATION_PICTURE_PATH = "notification_picture_path"; String EXTRA_NOTIFICATION_DNDSUPPRESSED = "notification_dndsuppressed"; + String EXTRA_NOTIFICATION_BACKGROUND = "notification_background"; String EXTRA_FIND_START = "find_start"; String EXTRA_VIBRATION_INTENSITY = "vibration_intensity"; String EXTRA_CALL_COMMAND = "call_command"; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/NotificationSpec.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/NotificationSpec.java index 778d4c7f6..8bd7569d5 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/NotificationSpec.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/NotificationSpec.java @@ -56,6 +56,8 @@ public class NotificationSpec { public int dndSuppressed; + public boolean background; + public NotificationSpec() { this(-1); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java index bcdf3c7db..2e1812332 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java @@ -813,6 +813,7 @@ public class DeviceCommunicationService extends Service implements SharedPrefere } DeviceSupport deviceSupport = getDeviceSupport(device); + DeviceCoordinator coordinator = getDeviceCoordinator(device); 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.picturePath = intent.getStringExtra(NOTIFICATION_PICTURE_PATH); 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) { 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]); } - deviceSupport.onNotification(notificationSpec); + if (!notificationSpec.background || coordinator.supportsBackgroundNotifications(device)) { + deviceSupport.onNotification(notificationSpec); + } break; } case ACTION_DELETE_NOTIFICATION: { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/NotificationsHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/NotificationsHandler.java index de2aebeed..899825cde 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/NotificationsHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/NotificationsHandler.java @@ -118,7 +118,15 @@ public class NotificationsHandler implements MessageHandler { } 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) { @@ -155,7 +163,14 @@ public class NotificationsHandler implements MessageHandler { NotificationSpec e = iterator.next(); if (e.getId() == id) { 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; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationUpdateMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationUpdateMessage.java index 159666022..a97ac2c0a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationUpdateMessage.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationUpdateMessage.java @@ -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 notificationId; final private boolean hasActions; + final private boolean background; final private boolean hasPicture; 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.notificationUpdateType = notificationUpdateType; this.notificationType = notificationType; this.count = count; this.notificationId = notificationId; + this.background = background; this.hasActions = hasActions; this.hasPicture = hasPicture; } @@ -32,7 +40,7 @@ public class NotificationUpdateMessage extends GFDIMessage { writer.writeShort(0); // packet size will be filled below writer.writeShort(this.garminMessage.getId()); writer.writeByte(this.notificationUpdateType.ordinal()); - writer.writeByte(getCategoryFlags(this.notificationType)); + writer.writeByte(getCategoryFlags(this.notificationType, this.background)); writer.writeByte(getCategoryValue(this.notificationType)); writer.writeByte(this.count); writer.writeInt(this.notificationId); @@ -54,28 +62,33 @@ public class NotificationUpdateMessage extends GFDIMessage { } - private int getCategoryFlags(NotificationType notificationType) { + private int getCategoryFlags(NotificationType notificationType, boolean background) { EnumSet 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": - flags.add(NotificationFlag.FOREGROUND); - break; - case "generic_navigation": - case "generic_social": - case "generic_alarm_clock": - case "generic": - // TODO: Maybe make this configurable, but most users expect all notifications - // to be foreground, sending them as background was generating bug reports. - flags.add(NotificationFlag.FOREGROUND); + if (background) { + flags.add(NotificationFlag.BACKGROUND); + } else { + switch (notificationType.getGenericType()) { + case "generic_phone": + case "generic_email": + case "generic_sms": + case "generic_chat": + flags.add(NotificationFlag.FOREGROUND); + break; + case "generic_navigation": + case "generic_social": + case "generic_alarm_clock": + case "generic": + // TODO: Maybe make this configurable, but most users expect all notifications + // to be foreground, sending them as background was generating bug reports. + flags.add(NotificationFlag.FOREGROUND); + } } + return (int) EnumUtils.generateBitVector(NotificationFlag.class, flags); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsNotificationService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsNotificationService.java index 15d658f3c..fcb6dc9bd 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsNotificationService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsNotificationService.java @@ -39,6 +39,7 @@ import nodomain.freeyourgadget.gadgetbridge.BuildConfig; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventNotificationControl; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; 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.AbstractZeppOsService; import nodomain.freeyourgadget.gadgetbridge.util.BitmapUtil; -import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue; import nodomain.freeyourgadget.gadgetbridge.util.NotificationUtils; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; 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_END = 0x02; + public static final String PREF_VERSION = "zepp_os_notifications_version"; + private int version = -1; private boolean supportsPictures = false; private boolean supportsNotificationKey = false; @@ -121,6 +124,7 @@ public class ZeppOsNotificationService extends AbstractZeppOsService { switch (cmd) { case NOTIFICATION_CMD_CAPABILITIES_RESPONSE: { version = buf.get() & 0xff; + getSupport().evaluateGBDeviceEvent(new GBDeviceEventUpdatePreferences(PREF_VERSION, version)); if (version < 4 || version > 5) { // Untested, might work, might not.. LOG.warn("Unsupported notification service version {}", version); @@ -357,7 +361,7 @@ public class ZeppOsNotificationService extends AbstractZeppOsService { baos.write((byte) (hasReply ? 1 : 0)); if (version >= 5) { - baos.write(0); // 1 for silent + baos.write(notificationSpec.background ? 1 : 0); // 1 for silent } if (supportsPictures) { baos.write((byte) (notificationSpec.picturePath != null ? 1 : 0)); @@ -597,4 +601,9 @@ public class ZeppOsNotificationService extends AbstractZeppOsService { return null; } } + + public static boolean supportsBackgroundNotifications(final Prefs devicePrefs) { + final int version = devicePrefs.getInt(PREF_VERSION, 0); + return version >= 5; + } }