mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-28 04:46:51 +01:00
Garmin: Add support for custom replies (notifications and calls)
To enable custom replies an override must be defined in the devices coordinator that actually support custom replies. The custom preferences allow to: - enable / disable the default message suffix (Instinct 2 appends "sent from my $vendor device" to each reply by default) - define custom messages to reply to calls and incoming messages (leaving those lists empty will enable the default messages to be used) Also adds a new protobuf definition file of mostly unknown values that enable toggling the message suffix on Instinct 2.
This commit is contained in:
parent
15916635e1
commit
698908a589
@ -464,4 +464,6 @@ public class DeviceSettingsPreferenceConst {
|
||||
|
||||
public static final String PREF_CYCLING_SENSOR_PERSISTENCE_INTERVAL = "pref_cycling_persistence_interval";
|
||||
public static final String PREF_CYCLING_SENSOR_WHEEL_DIAMETER = "pref_cycling_wheel_diameter";
|
||||
|
||||
public static final String PREF_GARMIN_DEFAULT_REPLY_SUFFIX = "pref_key_garmin_default_reply_suffix";
|
||||
}
|
||||
|
@ -648,6 +648,8 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
|
||||
|
||||
addPreferenceHandlerFor(PREF_SPEAK_NOTIFICATIONS_FOCUS_EXCLUSIVE);
|
||||
|
||||
addPreferenceHandlerFor(PREF_GARMIN_DEFAULT_REPLY_SUFFIX);
|
||||
|
||||
addPreferenceHandlerFor("lock");
|
||||
|
||||
String sleepTimeState = prefs.getString(PREF_SLEEP_TIME, PREF_DO_NOT_DISTURB_OFF);
|
||||
|
@ -39,6 +39,17 @@ 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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
final List<Integer> developer = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.DEVELOPER);
|
||||
developer.add(R.xml.devicesettings_keep_activity_data_on_device);
|
||||
|
||||
return deviceSpecificSettings;
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ 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
|
||||
@ -11,6 +12,11 @@ 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;
|
||||
|
@ -25,6 +25,7 @@ import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
@ -32,6 +33,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.Weather;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiDeviceStatus;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiFindMyWatch;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiSettingsService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiSmartProto;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
@ -60,6 +62,7 @@ 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;
|
||||
|
||||
|
||||
public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommunicator.Callback {
|
||||
@ -364,6 +367,28 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
|
||||
gbDevice.sendDeviceUpdateIntent(getContext());
|
||||
|
||||
sendOutgoingMessage(new SupportedFileTypesMessage());
|
||||
|
||||
sendOutgoingMessage(toggleDefaultReplySuffix(getDevicePrefs().getBoolean(PREF_GARMIN_DEFAULT_REPLY_SUFFIX, true)));
|
||||
}
|
||||
|
||||
private ProtobufMessage toggleDefaultReplySuffix(boolean value) {
|
||||
final GdiSettingsService.SettingsService.Builder enableSignature = GdiSettingsService.SettingsService.newBuilder()
|
||||
.setChangeRequest(
|
||||
GdiSettingsService.ChangeRequest.newBuilder()
|
||||
.setPointer1(65566) //TODO: this might be device specific, tested on Instinct 2s
|
||||
.setPointer2(3) //TODO: this might be device specific, tested on Instinct 2s
|
||||
.setEnable(GdiSettingsService.ChangeRequest.Switch.newBuilder().setValue(value)));
|
||||
|
||||
return protocolBufferHandler.prepareProtobufRequest(
|
||||
GdiSmartProto.Smart.newBuilder()
|
||||
.setSettingsService(enableSignature).build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendConfiguration(String config) {
|
||||
if (PREF_GARMIN_DEFAULT_REPLY_SUFFIX.equals(config)) {
|
||||
sendOutgoingMessage(toggleDefaultReplySuffix(getDevicePrefs().getBoolean(PREF_GARMIN_DEFAULT_REPLY_SUFFIX, true)));
|
||||
}
|
||||
}
|
||||
|
||||
private void processDownloadQueue() {
|
||||
@ -451,6 +476,11 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni
|
||||
sendOutgoingMessage(findMyWatch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) {
|
||||
sendOutgoingMessage(protocolBufferHandler.setCannedMessages(cannedMessagesSpec));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetMusicInfo(MusicSpec musicSpec) {
|
||||
|
||||
|
@ -179,8 +179,8 @@ public class NotificationsHandler implements MessageHandler {
|
||||
deviceEvtNotificationControl.handle = notificationSpec.getId();
|
||||
final GBDeviceEventCallControl deviceEvtCallControl = new GBDeviceEventCallControl();
|
||||
switch (message.getNotificationAction()) {
|
||||
case REPLY_CUSTOM_MESSAGES:
|
||||
case REPLY_STANDARD_MESSAGES:
|
||||
case REPLY_INCOMING_CALL:
|
||||
case REPLY_MESSAGES:
|
||||
deviceEvtNotificationControl.event = GBDeviceEventNotificationControl.Event.REPLY;
|
||||
deviceEvtNotificationControl.reply = message.getActionString();
|
||||
if (notificationSpec.type.equals(NotificationType.GENERIC_PHONE)) {
|
||||
@ -339,7 +339,7 @@ public class NotificationsHandler implements MessageHandler {
|
||||
|
||||
final List<byte[]> garminActions = new ArrayList<>();
|
||||
if (notificationSpec.type.equals(NotificationType.GENERIC_PHONE)) {
|
||||
garminActions.add(encodeNotificationAction(NotificationAction.REPLY_STANDARD_MESSAGES, " ")); //text is not shown on watch
|
||||
garminActions.add(encodeNotificationAction(NotificationAction.REPLY_INCOMING_CALL, " ")); //text is not shown on watch
|
||||
garminActions.add(encodeNotificationAction(NotificationAction.REJECT_INCOMING_CALL, " ")); //text is not shown on watch
|
||||
garminActions.add(encodeNotificationAction(NotificationAction.ACCEPT_INCOMING_CALL, " ")); //text is not shown on watch
|
||||
}
|
||||
@ -347,7 +347,7 @@ public class NotificationsHandler implements MessageHandler {
|
||||
for (NotificationSpec.Action action : notificationSpec.attachedActions) {
|
||||
switch (action.type) {
|
||||
case NotificationSpec.Action.TYPE_WEARABLE_REPLY:
|
||||
garminActions.add(encodeNotificationAction(NotificationAction.REPLY_STANDARD_MESSAGES, action.title));
|
||||
garminActions.add(encodeNotificationAction(NotificationAction.REPLY_MESSAGES, action.title));
|
||||
break;
|
||||
case NotificationSpec.Action.TYPE_SYNTECTIC_DISMISS:
|
||||
garminActions.add(encodeNotificationAction(NotificationAction.DISMISS_NOTIFICATION, action.title));
|
||||
@ -389,8 +389,8 @@ public class NotificationsHandler implements MessageHandler {
|
||||
}
|
||||
|
||||
public enum NotificationAction {
|
||||
REPLY_CUSTOM_MESSAGES(94, NotificationActionIconPosition.RIGHT), // uses the customized replies (set through protobuf??) does nothing if not available and on watches that do not support custom replies
|
||||
REPLY_STANDARD_MESSAGES(95, NotificationActionIconPosition.BOTTOM), // uses predefined replies
|
||||
REPLY_INCOMING_CALL(94, NotificationActionIconPosition.BOTTOM),
|
||||
REPLY_MESSAGES(95, NotificationActionIconPosition.BOTTOM),
|
||||
ACCEPT_INCOMING_CALL(96, NotificationActionIconPosition.RIGHT),
|
||||
REJECT_INCOMING_CALL(97, NotificationActionIconPosition.LEFT),
|
||||
DISMISS_NOTIFICATION(98, NotificationActionIconPosition.LEFT),
|
||||
|
@ -8,16 +8,19 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiCalendarService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiCore;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiDeviceStatus;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiFindMyWatch;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiSmartProto;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiSmsNotification;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.GFDIMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.ProtobufMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.ProtobufStatusMessage;
|
||||
@ -33,6 +36,8 @@ 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;
|
||||
|
||||
public ProtocolBufferHandler(GarminSupport deviceSupport) {
|
||||
this.deviceSupport = deviceSupport;
|
||||
chunkedFragmentsMap = new HashMap<>();
|
||||
@ -74,6 +79,9 @@ public class ProtocolBufferHandler implements MessageHandler {
|
||||
if (smart.hasCalendarService()) {
|
||||
return prepareProtobufResponse(processProtobufCalendarRequest(smart.getCalendarService()), message.getRequestId());
|
||||
}
|
||||
if (smart.hasSmsNotificationService()) {
|
||||
return prepareProtobufResponse(processProtobufSmsNotificationMessage(smart.getSmsNotificationService()), message.getRequestId());
|
||||
}
|
||||
if (smart.hasDeviceStatusService()) {
|
||||
processed = true;
|
||||
processProtobufDeviceStatusResponse(smart.getDeviceStatusService());
|
||||
@ -220,6 +228,62 @@ public class ProtocolBufferHandler implements MessageHandler {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
private GdiSmartProto.Smart processProtobufSmsNotificationMessage(GdiSmsNotification.SmsNotificationService smsNotificationService) {
|
||||
if (smsNotificationService.hasSmsCannedListRequest()) {
|
||||
if (null == this.cannedListTypeMap || this.cannedListTypeMap.isEmpty()) {
|
||||
this.cannedListTypeMap = new HashMap<>();
|
||||
|
||||
List<GdiSmsNotification.SmsNotificationService.CannedListType> requestedTypes = smsNotificationService.getSmsCannedListRequest().getRequestedTypesList();
|
||||
for (GdiSmsNotification.SmsNotificationService.CannedListType type :
|
||||
requestedTypes) {
|
||||
if (GdiSmsNotification.SmsNotificationService.CannedListType.SMS_MESSAGE_RESPONSE.equals(type)) {
|
||||
final ArrayList<String> messages = new ArrayList<>();
|
||||
for (int i = 1; i <= 16; i++) {
|
||||
String message = deviceSupport.getDevicePrefs().getString("canned_reply_" + i, null);
|
||||
if (message != null && !message.isEmpty()) {
|
||||
messages.add(message);
|
||||
}
|
||||
}
|
||||
if (!messages.isEmpty())
|
||||
this.cannedListTypeMap.put(type, messages.toArray(new String[0]));
|
||||
} else if (GdiSmsNotification.SmsNotificationService.CannedListType.PHONE_CALL_RESPONSE.equals(type)) {
|
||||
final ArrayList<String> messages = new ArrayList<>();
|
||||
for (int i = 1; i <= 16; i++) {
|
||||
String message = deviceSupport.getDevicePrefs().getString("canned_message_dismisscall_" + i, null);
|
||||
if (message != null && !message.isEmpty()) {
|
||||
messages.add(message);
|
||||
}
|
||||
}
|
||||
if (!messages.isEmpty())
|
||||
this.cannedListTypeMap.put(type, messages.toArray(new String[0]));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
List<GdiSmsNotification.SmsNotificationService.CannedListType> requestedTypes = smsNotificationService.getSmsCannedListRequest().getRequestedTypesList();
|
||||
|
||||
GdiSmsNotification.SmsNotificationService.SmsCannedListResponse.Builder builder = GdiSmsNotification.SmsNotificationService.SmsCannedListResponse.newBuilder()
|
||||
.setStatus(GdiSmsNotification.SmsNotificationService.ResponseStatus.SUCCESS);
|
||||
for (GdiSmsNotification.SmsNotificationService.CannedListType requestedType : requestedTypes) {
|
||||
if (this.cannedListTypeMap.containsKey(requestedType)) {
|
||||
builder.addLists(GdiSmsNotification.SmsNotificationService.SmsCannedList.newBuilder()
|
||||
.addAllResponse(Arrays.asList(this.cannedListTypeMap.get(requestedType)))
|
||||
.setType(requestedType)
|
||||
);
|
||||
} else {
|
||||
builder.setStatus(GdiSmsNotification.SmsNotificationService.ResponseStatus.GENERIC_ERROR);
|
||||
LOG.error("Missing canned messages data for type {}", requestedType);
|
||||
}
|
||||
}
|
||||
|
||||
return GdiSmartProto.Smart.newBuilder().setSmsNotificationService(GdiSmsNotification.SmsNotificationService.newBuilder().setSmsCannedListResponse(builder)).build();
|
||||
} else {
|
||||
LOG.warn("Protobuf smsNotificationService request not implemented: {}", smsNotificationService);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void processProtobufFindMyWatchResponse(GdiFindMyWatch.FindMyWatchService findMyWatchService) {
|
||||
if (findMyWatchService.hasCancelRequest()) {
|
||||
LOG.info("Watch found");
|
||||
@ -260,6 +324,36 @@ public class ProtocolBufferHandler implements MessageHandler {
|
||||
return new ProtobufMessage(garminMessage, requestId, 0, bytes.length, bytes.length, bytes);
|
||||
}
|
||||
|
||||
public ProtobufMessage setCannedMessages(CannedMessagesSpec cannedMessagesSpec) {
|
||||
final GdiSmsNotification.SmsNotificationService.CannedListType cannedListType;
|
||||
switch (cannedMessagesSpec.type) {
|
||||
case CannedMessagesSpec.TYPE_REJECTEDCALLS:
|
||||
cannedListType = GdiSmsNotification.SmsNotificationService.CannedListType.PHONE_CALL_RESPONSE;
|
||||
break;
|
||||
case CannedMessagesSpec.TYPE_GENERIC:
|
||||
case CannedMessagesSpec.TYPE_NEWSMS:
|
||||
cannedListType = GdiSmsNotification.SmsNotificationService.CannedListType.SMS_MESSAGE_RESPONSE;
|
||||
break;
|
||||
default:
|
||||
LOG.warn("Unknown canned messages type, ignoring.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (null == this.cannedListTypeMap) {
|
||||
this.cannedListTypeMap = new HashMap<>();
|
||||
}
|
||||
this.cannedListTypeMap.put(cannedListType, cannedMessagesSpec.cannedMessages);
|
||||
|
||||
GdiSmartProto.Smart smart = GdiSmartProto.Smart.newBuilder()
|
||||
.setSmsNotificationService(GdiSmsNotification.SmsNotificationService.newBuilder()
|
||||
.setSmsCannedListChangedNotification(
|
||||
GdiSmsNotification.SmsNotificationService.SmsCannedListChangedNotification.newBuilder().addChangedType(cannedListType)
|
||||
)
|
||||
).build();
|
||||
|
||||
return prepareProtobufRequest(smart);
|
||||
}
|
||||
|
||||
private class ProtobufFragment {
|
||||
private final byte[] fragmentBytes;
|
||||
private final int totalLength;
|
||||
|
@ -0,0 +1,29 @@
|
||||
syntax = "proto2";
|
||||
|
||||
package garmin_vivomovehr;
|
||||
|
||||
option java_package = "nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr";
|
||||
|
||||
message SettingsService {
|
||||
optional ChangeRequest change_request = 5;
|
||||
optional ChangeResponse change_response = 6;
|
||||
}
|
||||
|
||||
message ChangeRequest {
|
||||
optional uint32 pointer1 = 1;
|
||||
optional uint32 pointer2 = 2;
|
||||
optional Switch enable = 3;
|
||||
|
||||
message Switch {
|
||||
required bool value = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message ChangeResponse {
|
||||
optional ResponseStatus status = 1;
|
||||
}
|
||||
|
||||
enum ResponseStatus {
|
||||
SUCCESS = 0;
|
||||
GENERIC_ERROR = 1;
|
||||
}
|
@ -9,6 +9,7 @@ import "garmin_vivomovehr/gdi_find_my_watch.proto";
|
||||
import "garmin_vivomovehr/gdi_core.proto";
|
||||
import "garmin_vivomovehr/gdi_sms_notification.proto";
|
||||
import "garmin_vivomovehr/gdi_calendar_service.proto";
|
||||
import "garmin_vivomovehr/gdi_settings_service.proto";
|
||||
|
||||
message Smart {
|
||||
optional CalendarService calendar_service = 1;
|
||||
@ -16,6 +17,7 @@ message Smart {
|
||||
optional FindMyWatchService find_my_watch_service = 12;
|
||||
optional CoreService core_service = 13;
|
||||
optional SmsNotificationService sms_notification_service = 16;
|
||||
optional SettingsService settings_service = 42;
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -2819,6 +2819,8 @@
|
||||
<string name="pref_title_bottom_navigation_bar">Bottom navigation bar</string>
|
||||
<string name="pref_summary_bottom_navigation_bar_on">Switch between main screens using the navigation bar or horizontal swiping</string>
|
||||
<string name="pref_summary_bottom_navigation_bar_off">Switch between main screens only using horizontal swiping</string>
|
||||
<string name="pref_summary_garmin_default_reply_suffix">Appended in addition to the suffix set in Gadgetbridge</string>
|
||||
<string name="pref_title_garmin_default_reply_suffix">Use predefined reply suffix</string>
|
||||
<string name="pref_dashboard_widget_today_upside_down_title">Midnight at bottom</string>
|
||||
<string name="pref_dashboard_widget_today_upside_down_summary">In 24h mode, draw midnight at the bottom, midday at the top of the chart</string>
|
||||
|
||||
|
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<SwitchPreferenceCompat
|
||||
android:key="pref_key_garmin_default_reply_suffix"
|
||||
android:summary="@string/pref_summary_garmin_default_reply_suffix"
|
||||
android:title="@string/pref_title_garmin_default_reply_suffix">
|
||||
|
||||
</SwitchPreferenceCompat>
|
||||
</androidx.preference.PreferenceScreen>
|
Loading…
Reference in New Issue
Block a user