Compare commits

...

6 Commits

Author SHA1 Message Date
a0z 3ac478e163 Garmin: Initial support of Instinct 2 Solar 2024-04-25 09:25:39 +00:00
Daniele Gobbetti b345e41db0 Garmin: fix notification crashes and handle SMS correctly
It looks like (some) watches really don't like having an empty list of actions, hence enable the legacy "refuse" action in every case, leaving it empty and inactive.
Further display the SMS sender in the notification and enable the correct code path for the reply action to work.
2024-04-25 11:18:51 +02:00
José Rebelo 28984d9152 Garmin: Auto-detect canned messages support 2024-04-25 08:07:21 +00:00
José Rebelo e839a6d197 Garmin: Fix reply to sms 2024-04-25 08:07:21 +00:00
José Rebelo 42fa8ffaed Garmin: Add setting to disable notifications 2024-04-25 08:07:21 +00:00
José Rebelo 5865dc9650 Garmin Venu 3: Enable canned replies 2024-04-25 08:07:21 +00:00
15 changed files with 157 additions and 30 deletions

View File

@ -220,6 +220,12 @@ public class DebugActivity extends AbstractGBActivity {
replyAction.title = "Reply";
replyAction.type = NotificationSpec.Action.TYPE_SYNTECTIC_REPLY_PHONENR;
notificationSpec.attachedActions.add(replyAction);
} else if (notificationSpec.type == NotificationType.CONVERSATIONS) {
// REPLY action
NotificationSpec.Action replyAction = new NotificationSpec.Action();
replyAction.title = "Reply";
replyAction.type = NotificationSpec.Action.TYPE_WEARABLE_REPLY;
notificationSpec.attachedActions.add(replyAction);
}
GBApplication.deviceService().onNotification(notificationSpec);

View File

@ -356,7 +356,7 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
});
}
addPreferenceHandlerFor(PREF_SEND_APP_NOTIFICATIONS);
addPreferenceHandlerFor(PREF_SWIPE_UNLOCK);
addPreferenceHandlerFor(PREF_MI2_DATEFORMAT);
addPreferenceHandlerFor(PREF_DATEFORMAT);

View File

@ -4,6 +4,7 @@ import androidx.annotation.NonNull;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
@ -14,6 +15,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminSupport;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
@Override
@ -39,9 +41,10 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
final List<Integer> connection = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.CONNECTION);
connection.add(R.xml.devicesettings_high_mtu);
if (getCannedRepliesSlotCount(device) > 0) {
final List<Integer> notifications = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS);
final List<Integer> notifications = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS);
notifications.add(R.xml.devicesettings_send_app_notifications);
if (getCannedRepliesSlotCount(device) > 0) {
notifications.add(R.xml.devicesettings_garmin_default_reply_suffix);
notifications.add(R.xml.devicesettings_canned_reply_16);
notifications.add(R.xml.devicesettings_canned_dismisscall_16);
@ -67,4 +70,17 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
public boolean supportsWeather() {
return true;
}
@Override
public int getCannedRepliesSlotCount(final GBDevice device) {
if (getPrefs(device).getBoolean(GarminPreferences.PREF_FEAT_CANNED_MESSAGES, false)) {
return 16;
}
return 0;
}
protected static Prefs getPrefs(final GBDevice device) {
return new Prefs(GBApplication.getDeviceSpecificSharedPrefs(device.getAddress()));
}
}

View File

@ -2,4 +2,5 @@ package nodomain.freeyourgadget.gadgetbridge.devices.garmin;
public class GarminPreferences {
public static final String PREF_GARMIN_CAPABILITIES = "garmin_capabilities";
public static final String PREF_FEAT_CANNED_MESSAGES = "feat_canned_messages";
}

View File

@ -4,7 +4,6 @@ import java.util.regex.Pattern;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class GarminInstinct2SCoordinator extends GarminCoordinator {
@Override
@ -12,11 +11,6 @@ public class GarminInstinct2SCoordinator extends GarminCoordinator {
return Pattern.compile("Instinct 2S");
}
@Override
public int getCannedRepliesSlotCount(final GBDevice device) {
return 16;
}
@Override
public int getDeviceNameResource() {
return R.string.devicetype_garmin_instinct_2s;

View File

@ -0,0 +1,19 @@
package nodomain.freeyourgadget.gadgetbridge.devices.garmin.instinct2solar;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import java.util.regex.Pattern;
public class GarminInstinct2SolarCoordinator extends GarminCoordinator {
@Override
protected Pattern getSupportedDeviceName() {
return Pattern.compile("Instinct 2 Solar");
}
@Override
public int getDeviceNameResource() {
return R.string.devicetype_garmin_instinct_2_solar;
}
}

View File

@ -51,6 +51,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.galaxy_buds.GalaxyBudsLiveDe
import nodomain.freeyourgadget.gadgetbridge.devices.galaxy_buds.GalaxyBudsProDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.forerunner245.GarminForerunner245Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.instinct2s.GarminInstinct2SCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.instinct2solar.GarminInstinct2SolarCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.venu3.GarminVenu3Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.vivoactive4s.GarminVivoActive4SCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.vivoactive5.GarminVivoActive5Coordinator;
@ -329,6 +330,7 @@ public enum DeviceType {
VIVOMOVE_HR(VivomoveHrCoordinator.class),
GARMIN_FORERUNNER_245(GarminForerunner245Coordinator.class),
GARMIN_INSTINCT_2S(GarminInstinct2SCoordinator.class),
GARMIN_INSTINCT_2_SOLAR(GarminInstinct2SolarCoordinator.class),
GARMIN_VIVOMOVE_STYLE(GarminVivomoveStyleCoordinator.class),
GARMIN_VENU_3(GarminVenu3Coordinator.class),
GARMIN_VIVOACTIVE_4S(GarminVivoActive4SCoordinator.class),

View File

@ -60,12 +60,14 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.SetD
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.SetFileFlagsMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.SupportedFileTypesMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.SystemEventMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.NotificationSubscriptionStatusMessage;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_ALLOW_HIGH_MTU;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_GARMIN_DEFAULT_REPLY_SUFFIX;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SEND_APP_NOTIFICATIONS;
public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommunicator.Callback {
@ -230,7 +232,22 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
} else if (deviceEvent instanceof NotificationSubscriptionDeviceEvent) {
final boolean enable = ((NotificationSubscriptionDeviceEvent) deviceEvent).enable;
notificationsHandler.setEnabled(enable);
LOG.info("NOTIFICATIONS ARE NOW {}", enable ? "ON" : "OFF");
final NotificationSubscriptionStatusMessage.NotificationStatus finalStatus;
if (getDevicePrefs().getBoolean(PREF_SEND_APP_NOTIFICATIONS, true)) {
finalStatus = NotificationSubscriptionStatusMessage.NotificationStatus.ENABLED;
} else {
finalStatus = NotificationSubscriptionStatusMessage.NotificationStatus.DISABLED;
}
LOG.info("NOTIFICATIONS ARE NOW enabled={}, status={}", enable, finalStatus);
sendOutgoingMessage(new NotificationSubscriptionStatusMessage(
GFDIMessage.Status.ACK,
finalStatus,
enable,
0
));
} else if (deviceEvent instanceof SupportedFileTypesDeviceEvent) {
this.supportedFileTypeList.clear();
this.supportedFileTypeList.addAll(((SupportedFileTypesDeviceEvent) deviceEvent).getSupportedFileTypes());
@ -261,7 +278,7 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
}
@Override
public void onNotification(NotificationSpec notificationSpec) {
public void onNotification(final NotificationSpec notificationSpec) {
sendOutgoingMessage(notificationsHandler.onNotification(notificationSpec));
}
@ -406,8 +423,15 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
@Override
public void onSendConfiguration(String config) {
if (PREF_GARMIN_DEFAULT_REPLY_SUFFIX.equals(config)) {
sendOutgoingMessage(toggleDefaultReplySuffix(getDevicePrefs().getBoolean(PREF_GARMIN_DEFAULT_REPLY_SUFFIX, true)));
switch (config) {
case PREF_GARMIN_DEFAULT_REPLY_SUFFIX:
sendOutgoingMessage(toggleDefaultReplySuffix(getDevicePrefs().getBoolean(PREF_GARMIN_DEFAULT_REPLY_SUFFIX, true)));
break;
case PREF_SEND_APP_NOTIFICATIONS:
NotificationSubscriptionDeviceEvent notificationSubscriptionDeviceEvent = new NotificationSubscriptionDeviceEvent();
notificationSubscriptionDeviceEvent.enable = true; // actual status is fetched from preferences
evaluateGBDeviceEvent(notificationSubscriptionDeviceEvent);
break;
}
}

View File

@ -55,7 +55,7 @@ public class NotificationsHandler implements MessageHandler {
final byte[] bytes = entry.getKey().getNotificationSpecAttribute(notificationSpec, entry.getValue());
messageWriter.writeShort(bytes.length);
messageWriter.writeBytes(bytes);
// LOG.info("ATTRIBUTE:{} value:{} length:{}", entry.getKey(), new String(bytes), bytes.length);
// LOG.info("ATTRIBUTE:{} value:{}/{} length:{}", entry.getKey(), new String(bytes), GB.hexdump(bytes), bytes.length);
}
@ -183,7 +183,7 @@ public class NotificationsHandler implements MessageHandler {
case REPLY_MESSAGES:
deviceEvtNotificationControl.event = GBDeviceEventNotificationControl.Event.REPLY;
deviceEvtNotificationControl.reply = message.getActionString();
if (notificationSpec.type.equals(NotificationType.GENERIC_PHONE)) {
if (notificationSpec.type.equals(NotificationType.GENERIC_PHONE) || notificationSpec.type.equals(NotificationType.GENERIC_SMS)) {
deviceEvtNotificationControl.phoneNumber = notificationSpec.phoneNumber;
} else {
deviceEvtNotificationControl.handle = mNotificationReplyAction.lookup(notificationSpec.getId()); //handle of wearable action is needed
@ -267,7 +267,7 @@ public class NotificationsHandler implements MessageHandler {
MESSAGE_SIZE(4),
DATE(5),
// POSITIVE_ACTION_LABEL(6), //needed only for legacy notification actions
// NEGATIVE_ACTION_LABEL(7), //needed only for legacy notification actions
NEGATIVE_ACTION_LABEL(7), //needed only for legacy notification actions
// Garmin extensions
// PHONE_NUMBER(126, true),
ACTIONS(127, false, true),
@ -312,7 +312,10 @@ public class NotificationsHandler implements MessageHandler {
toReturn = NOTIFICATION_DATE_FORMAT.format(new Date(notificationTimestamp));
break;
case TITLE:
toReturn = notificationSpec.title == null ? "" : notificationSpec.title;
if (NotificationType.GENERIC_SMS.equals(notificationSpec.type))
toReturn = notificationSpec.sender == null ? "" : notificationSpec.sender;
else
toReturn = notificationSpec.title == null ? "" : notificationSpec.title;
break;
case SUBTITLE:
toReturn = notificationSpec.subject == null ? "" : notificationSpec.subject;
@ -347,6 +350,7 @@ public class NotificationsHandler implements MessageHandler {
for (NotificationSpec.Action action : notificationSpec.attachedActions) {
switch (action.type) {
case NotificationSpec.Action.TYPE_WEARABLE_REPLY:
case NotificationSpec.Action.TYPE_SYNTECTIC_REPLY_PHONENR:
garminActions.add(encodeNotificationAction(NotificationAction.REPLY_MESSAGES, action.title));
break;
case NotificationSpec.Action.TYPE_SYNTECTIC_DISMISS:

View File

@ -12,8 +12,11 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminPreferences;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiCalendarService;
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiCore;
@ -38,7 +41,7 @@ public class ProtocolBufferHandler implements MessageHandler {
private final int maxChunkSize = 375; //tested on Vívomove Style
private int lastProtobufRequestId;
private Map<GdiSmsNotification.SmsNotificationService.CannedListType, String[]> cannedListTypeMap;
private final Map<GdiSmsNotification.SmsNotificationService.CannedListType, String[]> cannedListTypeMap = new HashMap<>();
public ProtocolBufferHandler(GarminSupport deviceSupport) {
this.deviceSupport = deviceSupport;
@ -239,9 +242,12 @@ public class ProtocolBufferHandler implements MessageHandler {
private GdiSmartProto.Smart processProtobufSmsNotificationMessage(GdiSmsNotification.SmsNotificationService smsNotificationService) {
if (smsNotificationService.hasSmsCannedListRequest()) {
if (null == this.cannedListTypeMap || this.cannedListTypeMap.isEmpty()) {
this.cannedListTypeMap = new HashMap<>();
LOG.debug("Got request for sms canned list");
// Mark canned messages as supported
deviceSupport.evaluateGBDeviceEvent(new GBDeviceEventUpdatePreferences(GarminPreferences.PREF_FEAT_CANNED_MESSAGES, true));
if (this.cannedListTypeMap.isEmpty()) {
List<GdiSmsNotification.SmsNotificationService.CannedListType> requestedTypes = smsNotificationService.getSmsCannedListRequest().getRequestedTypesList();
for (GdiSmsNotification.SmsNotificationService.CannedListType type :
requestedTypes) {
@ -277,7 +283,7 @@ public class ProtocolBufferHandler implements MessageHandler {
for (GdiSmsNotification.SmsNotificationService.CannedListType requestedType : requestedTypes) {
if (this.cannedListTypeMap.containsKey(requestedType)) {
builder.addLists(GdiSmsNotification.SmsNotificationService.SmsCannedList.newBuilder()
.addAllResponse(Arrays.asList(this.cannedListTypeMap.get(requestedType)))
.addAllResponse(Arrays.asList(Objects.requireNonNull(this.cannedListTypeMap.get(requestedType))))
.setType(requestedType)
);
} else {
@ -348,9 +354,6 @@ public class ProtocolBufferHandler implements MessageHandler {
return null;
}
if (null == this.cannedListTypeMap) {
this.cannedListTypeMap = new HashMap<>();
}
this.cannedListTypeMap.put(cannedListType, cannedMessagesSpec.cannedMessages);
GdiSmartProto.Smart smart = GdiSmartProto.Smart.newBuilder()

View File

@ -1,7 +1,7 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@ -77,7 +77,7 @@ public class NotificationControlMessage extends GFDIMessage {
}
private static Map<NotificationsHandler.NotificationAttribute, Integer> createGetNotificationAttributesCommand(MessageReader reader) {
final Map<NotificationsHandler.NotificationAttribute, Integer> notificationAttributesMap = new HashMap<>();
final Map<NotificationsHandler.NotificationAttribute, Integer> notificationAttributesMap = new LinkedHashMap<>();
while (reader.remaining() > 0) {
final int attributeID = reader.readByte();

View File

@ -17,7 +17,8 @@ public class NotificationSubscriptionMessage extends GFDIMessage {
this.enable = enable;
this.unk = unk;
this.statusMessage = new NotificationSubscriptionStatusMessage(Status.ACK, NotificationSubscriptionStatusMessage.NotificationStatus.OK, enable, unk);
// We do not set the status message here so we can reply with the proper notifications status
// from the device event
}
public static NotificationSubscriptionMessage parseIncoming(MessageReader reader, GarminMessage garminMessage) {

View File

@ -34,17 +34,64 @@ public class NotificationUpdateMessage extends GFDIMessage {
writer.writeByte(getCategoryValue(this.notificationType));
writer.writeByte(this.count);
writer.writeInt(this.notificationId);
writer.writeByte(this.useLegacyActions ? 0x00 : 0x03);
writer.writeByte(getNotificationPhoneFlags());
return true;
}
private int getNotificationPhoneFlags() {
EnumSet<NotificationPhoneFlags> flags = EnumSet.noneOf(NotificationPhoneFlags.class);
if (this.hasActions)
flags.add(NotificationPhoneFlags.NEW_ACTIONS);
if (this.useLegacyActions)
flags.add(NotificationPhoneFlags.LEGACY_ACTIONS);
return (int) EnumUtils.generateBitVector(NotificationPhoneFlags.class, flags);
}
//no image
//00 updatetype
// 12 flags
// 00 notif type
// 00 count
// 03000000
// 02
//image
//00
// 12
// 00
// 00
// 04000000
// 06
//0F00
// A913
// 00
// 12
// 0C
// 00
// 471D2A66
// 02
// BC14
//0F00
// A913
// 00
// 11
// 00
// 00
// 461D2A66
// 00
// 8C00
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);
}
flags.add(NotificationFlag.ACTION_DECLINE);
switch (notificationType.getGenericType()) {
case "generic_phone":
@ -111,5 +158,13 @@ public class NotificationUpdateMessage extends GFDIMessage {
LOCATION,
ENTERTAINMENT,
SMS
}
enum NotificationPhoneFlags {
LEGACY_ACTIONS,
NEW_ACTIONS,
HAS_ATTACHMENTS,
;
}
}

View File

@ -33,7 +33,8 @@ public class NotificationSubscriptionStatusMessage extends GFDIStatusMessage {
}
public enum NotificationStatus {
OK,
ENABLED,
DISABLED
;
public static NotificationStatus fromId(int id) {

View File

@ -1483,6 +1483,7 @@
<string name="devicetype_garmin_vivomove_hr">Garmin Vivomove HR</string>
<string name="devicetype_garmin_vivomove_style">Vívomove Style</string>
<string name="devicetype_garmin_instinct_2s">Garmin Instinct 2S</string>
<string name="devicetype_garmin_instinct_2_solar">Garmin Instinct 2 Solar</string>
<string name="devicetype_garmin_forerunner_245">Garmin Forerunner 245</string>
<string name="devicetype_garmin_vivoactive_4s">Vívoactive 4S</string>
<string name="devicetype_garmin_vivoactive_5">Vivoactive 5</string>