From b9b91db06ff3a8d75ff9f3121d1fba1d19c2bee9 Mon Sep 17 00:00:00 2001 From: MrYoranimo Date: Wed, 29 Nov 2023 23:46:43 +0100 Subject: [PATCH] Xiaomi: implement phonebook service to respond to contact info requests --- .../service/devices/xiaomi/XiaomiSupport.java | 3 + .../services/XiaomiPhonebookService.java | 136 ++++++++++++++++++ app/src/main/proto/xiaomi.proto | 13 ++ 3 files changed, 152 insertions(+) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiPhonebookService.java diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSupport.java index bbb1bdec9..51303c70c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSupport.java @@ -57,6 +57,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.Xiao import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiHealthService; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiMusicService; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiNotificationService; +import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiPhonebookService; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiScheduleService; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiSystemService; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiWatchfaceService; @@ -82,6 +83,7 @@ public abstract class XiaomiSupport extends AbstractBTLEDeviceSupport { protected final XiaomiCalendarService calendarService = new XiaomiCalendarService(this); protected final XiaomiWatchfaceService watchfaceService = new XiaomiWatchfaceService(this); protected final XiaomiDataUploadService dataUploadService = new XiaomiDataUploadService(this); + protected final XiaomiPhonebookService phonebookService = new XiaomiPhonebookService(this); private String mFirmwareVersion = null; @@ -96,6 +98,7 @@ public abstract class XiaomiSupport extends AbstractBTLEDeviceSupport { put(XiaomiCalendarService.COMMAND_TYPE, calendarService); put(XiaomiWatchfaceService.COMMAND_TYPE, watchfaceService); put(XiaomiDataUploadService.COMMAND_TYPE, dataUploadService); + put(XiaomiPhonebookService.COMMAND_TYPE, phonebookService); }}; public XiaomiSupport() { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiPhonebookService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiPhonebookService.java new file mode 100644 index 000000000..c198d60e0 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiPhonebookService.java @@ -0,0 +1,136 @@ +/* Copyright (C) 2023 Yoran Vulker + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.ContactsContract; +import android.text.TextUtils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto; +import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport; + +public class XiaomiPhonebookService extends AbstractXiaomiService { + + public static final Integer COMMAND_TYPE = 21; + private static final Logger LOG = LoggerFactory.getLogger(XiaomiPhonebookService.class.getSimpleName()); + + private static final int CMD_GET_CONTACT = 2; + private static final int CMD_GET_CONTACT_RESPONSE = 3; + + public XiaomiPhonebookService(final XiaomiSupport support) { + super(support); + } + + @Override + public void handleCommand(XiaomiProto.Command cmd) { + if (cmd.getType() != COMMAND_TYPE) { + throw new IllegalArgumentException("Not a phonebook command"); + } + + XiaomiProto.Phonebook payload = cmd.getPhonebook(); + + if (payload == null) { + LOG.warn("Received phonebook command without phonebook payload"); + } + + switch (cmd.getSubtype()) { + case CMD_GET_CONTACT: + if (payload == null || TextUtils.isEmpty(payload.getRequestedPhoneNumber())) { + LOG.error("Receive request for contact info without payload or requested phone number"); + return; + } + + handleContactRequest(payload.getRequestedPhoneNumber()); + return; + } + + LOG.warn("Unhandled Phonebook command {}", cmd.getSubtype()); + } + + public void handleContactRequest(String phoneNumber) { + LOG.debug("Received request for contact info for {}", phoneNumber); + + XiaomiProto.ContactInfo contact = getContactInfoForPhoneNumber(phoneNumber); + + getSupport().sendCommand( + "send requested contact information", + XiaomiProto.Command.newBuilder() + .setType(COMMAND_TYPE) + .setSubtype(CMD_GET_CONTACT_RESPONSE) + .setPhonebook(XiaomiProto.Phonebook.newBuilder().setContactInfo(contact)) + .build() + ); + } + + /** + * Returns contact information from Android contact list + * + * @param number contact number + * @return the contact display name, if found, otherwise the phone number + */ + private XiaomiProto.ContactInfo getContactInfoForPhoneNumber(String number) { + Context context = getSupport().getContext(); + String currentPrivacyMode = GBApplication.getPrefs().getString("pref_call_privacy_mode", GBApplication.getContext().getString(R.string.p_call_privacy_mode_off)); + + // mask the display name if complete privacy is set in preferences + if (currentPrivacyMode.equals(context.getString(R.string.p_call_privacy_mode_complete))) { + return XiaomiProto.ContactInfo.newBuilder().setDisplayName("********").setPhoneNumber(number).build(); + } + + // send empty contact name if name privacy is set in preferences, as the device will show + // the phone number instead + if (currentPrivacyMode.equals(context.getString(R.string.p_call_privacy_mode_name))) { + return XiaomiProto.ContactInfo.newBuilder().setDisplayName("").setPhoneNumber(number).build(); + } + + String name = ""; + + // prevent lookup of null or empty phone number + if (!TextUtils.isEmpty(number)) { + // search contact's display name in Android contact list + Uri uri = Uri.withAppendedPath(ContactsContract.PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI, Uri.encode(number)); + + try (Cursor contactLookup = getSupport().getContext().getContentResolver().query(uri, new String[] { ContactsContract.Data.DISPLAY_NAME}, null, null, null)) { + if (contactLookup != null && contactLookup.getCount() > 0) { + contactLookup.moveToNext(); + name = contactLookup.getString(0); + } + } catch (SecurityException e) { + // ignore, just return name below + } + } + + XiaomiProto.ContactInfo.Builder contactInfoBuilder = XiaomiProto.ContactInfo.newBuilder(); + + // prevent the number from getting displayed if an empty contact name was retrieved from the + // contact list + if (TextUtils.isEmpty(name) && currentPrivacyMode.equals(context.getString(R.string.p_call_privacy_mode_number))) { + name = "********"; + } + + contactInfoBuilder.setPhoneNumber(number); + contactInfoBuilder.setDisplayName(name); + return contactInfoBuilder.build(); + } +} diff --git a/app/src/main/proto/xiaomi.proto b/app/src/main/proto/xiaomi.proto index 6203ab046..e1c854970 100644 --- a/app/src/main/proto/xiaomi.proto +++ b/app/src/main/proto/xiaomi.proto @@ -19,6 +19,9 @@ message Command { optional Weather weather = 12; optional Schedule schedule = 19; + // command type 21 + optional Phonebook phonebook = 23; + // type 22 optional DataUpload dataUpload = 24; @@ -872,3 +875,13 @@ message DataUploadAck { optional uint32 resumePosition = 4; optional uint32 chunkSize = 5; // 4096 on Redmi Watch 3 Active, Nonexistent on Mi Band 8 } + +message ContactInfo { + optional string displayName = 1; + optional string phoneNumber = 2; +} + +message Phonebook { + optional string requestedPhoneNumber = 2; + optional ContactInfo contactInfo = 3; +}