diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ConfigureContacts.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ConfigureContacts.java index cd287a748..f004f63d6 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ConfigureContacts.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/ConfigureContacts.java @@ -50,7 +50,6 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.entities.User; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; -import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; public class ConfigureContacts extends AbstractGBActivity { 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 ba5a87d85..fe3003f26 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 @@ -107,6 +107,9 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator { notifications.add(R.xml.devicesettings_canned_reply_16); notifications.add(R.xml.devicesettings_canned_dismisscall_16); } + if (getContactsSlotCount(device) > 0) { + notifications.add(R.xml.devicesettings_contacts); + } final List location = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.LOCATION); location.add(R.xml.devicesettings_workout_send_gps_to_band); @@ -197,6 +200,15 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator { return 0; } + @Override + public int getContactsSlotCount(final GBDevice device) { + if (getPrefs(device).getBoolean(GarminPreferences.PREF_FEAT_CONTACTS, false)) { + return 50; + } + + return 0; + } + protected static Prefs getPrefs(final GBDevice device) { return new Prefs(GBApplication.getDeviceSpecificSharedPrefs(device.getAddress())); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminPreferences.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminPreferences.java index 2fb53f5cb..d3f614b54 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminPreferences.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminPreferences.java @@ -5,6 +5,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.CheckSums; public class GarminPreferences { public static final String PREF_GARMIN_CAPABILITIES = "garmin_capabilities"; public static final String PREF_FEAT_CANNED_MESSAGES = "feat_canned_messages"; + public static final String PREF_FEAT_CONTACTS = "feat_contacts"; public static final String PREF_AGPS_KNOWN_URLS = "garmin_agps_known_urls"; public static final String PREF_GARMIN_AGPS_STATUS = "garmin_agps_status_%s"; public static final String PREF_GARMIN_AGPS_UPDATE_TIME = "garmin_agps_update_time_%s"; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/http/ContactsHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/http/ContactsHandler.java new file mode 100644 index 000000000..425571894 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/http/ContactsHandler.java @@ -0,0 +1,111 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.http; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Locale; +import java.util.Random; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminPreferences; +import nodomain.freeyourgadget.gadgetbridge.entities.Contact; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser; +import nodomain.freeyourgadget.gadgetbridge.proto.garmin.GarminContacts; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminSupport; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; + +public class ContactsHandler { + private static final Logger LOG = LoggerFactory.getLogger(ContactsHandler.class); + + private final GarminSupport deviceSupport; + + public ContactsHandler(final GarminSupport deviceSupport) { + this.deviceSupport = deviceSupport; + } + + public GarminHttpResponse handleRequest(final GarminHttpRequest request) { + if (!request.getPath().equals("/device-gateway/usercontact/contacts")) { + LOG.warn("Unknown contacts path {}", request.getPath()); + return null; + } + + if (!"application/octet-stream".equals(request.getHeaders().get("accept"))) { + // We only support protobuf replies + LOG.warn("Requested contacts content type '{}' not supported", request.getHeaders().get("accept")); + return null; + } + + // Mark contacts as supported + final Prefs devicePrefs = deviceSupport.getDevicePrefs(); + devicePrefs.getPreferences().edit() + .putBoolean(GarminPreferences.PREF_FEAT_CONTACTS, true) + .apply(); + + final GarminContacts.Response.Builder responseContacts = GarminContacts.Response.newBuilder(); + + final List contacts = DBHelper.getContacts(deviceSupport.getDevice()); + for (final Contact contact : contacts) { + final GarminContacts.Contact.Builder responseContact = GarminContacts.Contact.newBuilder() + .setId(randomHex(32).toUpperCase(Locale.ROOT)) + .setFullName(contact.getName()) + .setFirstName(contact.getName()) + .setLastName("") + .addPhone( + GarminContacts.Phone.newBuilder() + .setNumber(contact.getNumber()) + .setUnk2(1) + .setUnk3(1) + .setUnk4(0) + .setUnk5(new Random().nextInt(65535) + 65535) + .setUnk6(0) + .setUnk7(0) + .setUnk8("") + .setUnk9(1) + .setId(UUID.randomUUID().toString()) + .setUnk11(1) + .addUnk12(GarminContacts.Unk12.newBuilder().setUnk1(1).setUnk2(5)) + .addUnk12(GarminContacts.Unk12.newBuilder().setUnk1(2).setUnk2(5)) + ) + .setUnk8(0) + .setUnk9(0) + .setUpdateTime(System.currentTimeMillis()) + .setUnk12(0) + .setUnk21(0); + + responseContacts.addContact(responseContact); + } + + final ActivityUser activityUser = new ActivityUser(); + final GarminContacts.Contact.Builder self = GarminContacts.Contact.newBuilder() + .setId("SELF") + .setFullName(activityUser.getName()) + .setFirstName(activityUser.getName()) + .setLastName("") + .setUnk7("") + .setUnk8(0) + .setUnk9(0) + .setUnk10(0) + .setUnk12(0) + .setUnk21(0) + .setUpdateTime(System.currentTimeMillis()); + + responseContacts.setSelf(self); + + final GarminHttpResponse response = new GarminHttpResponse(); + response.setStatus(200); + response.getHeaders().put("Content-Type", "application/octet-stream"); + response.setBody(responseContacts.build().toByteArray()); + return response; + } + + public static String randomHex(final int numChars) { + final Random r = new Random(); + final StringBuilder sb = new StringBuilder(); + while (sb.length() < numChars) { + sb.append(Integer.toHexString(r.nextInt())); + } + return sb.toString().substring(0, numChars); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/http/HttpHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/http/HttpHandler.java index 7838329aa..92e81be1a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/http/HttpHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/http/HttpHandler.java @@ -19,9 +19,11 @@ public class HttpHandler { private static final Logger LOG = LoggerFactory.getLogger(HttpHandler.class); private final AgpsHandler agpsHandler; + private final ContactsHandler contactsHandler; public HttpHandler(GarminSupport deviceSupport) { agpsHandler = new AgpsHandler(deviceSupport); + contactsHandler = new ContactsHandler(deviceSupport); } public GdiHttpService.HttpService handle(final GdiHttpService.HttpService httpService) { @@ -52,6 +54,9 @@ public class HttpHandler { } else if (request.getPath().startsWith("/ephemeris/")) { LOG.info("Got AGPS request for {}", request.getPath()); response = agpsHandler.handleAgpsRequest(request); + } else if (request.getPath().startsWith("/device-gateway/usercontact/")) { + LOG.info("Got contacts request for {}", request.getPath()); + response = contactsHandler.handleRequest(request); } else { LOG.warn("Unhandled path {}", request.getPath()); response = null; diff --git a/app/src/main/proto/garmin/garmin_contacts.proto b/app/src/main/proto/garmin/garmin_contacts.proto new file mode 100644 index 000000000..6f1e79a7d --- /dev/null +++ b/app/src/main/proto/garmin/garmin_contacts.proto @@ -0,0 +1,46 @@ +syntax = "proto2"; + +package garmin; + +option java_package = "nodomain.freeyourgadget.gadgetbridge.proto.garmin"; +option java_outer_classname = "GarminContacts"; + +message Response { + repeated Contact contact = 1; + optional Contact self = 2; +} + +message Contact { + optional string id = 1; // "SELF" on self + optional string fullName = 2; + optional string firstName = 3; + optional string lastName = 4; + repeated Phone phone = 5; + optional string unk7 = 7; // "" + optional uint32 unk8 = 8; // 0 + optional uint32 unk9 = 9; // 0 + optional uint32 unk10 = 10; // 0 + optional uint64 updateTime = 11; + optional uint32 unk12 = 12; // 0 + optional uint32 unk21 = 21; // 0 +} + +message Phone { + optional string number = 1; + optional uint32 unk2 = 2; // 1 + optional uint32 unk3 = 3; // 1 + optional uint32 unk4 = 4; // 0 + optional uint32 unk5 = 5; // random number + optional uint32 unk6 = 6; // 0 + optional uint32 unk7 = 7; // 0 + optional string unk8 = 8; // "" + optional uint32 unk9 = 9; // 1 + optional string id = 10; // uuid + optional uint32 unk11 = 11; // 1 + repeated Unk12 unk12 = 12; // 1/5, 2/5 +} + +message Unk12 { + optional uint32 unk1 = 1; // 1/2 + optional uint32 unk2 = 2; // 5 +}