Huawei: new contact sync support added

This commit is contained in:
Me7c7
2025-10-01 13:35:39 +03:00
parent 933ba53a65
commit b37078d309
4 changed files with 400 additions and 11 deletions
@@ -877,6 +877,12 @@ public class HuaweiCoordinator {
return false;
}
public boolean supportsContactsSync() {
if (supportsExpandCapability())
return supportsExpandCapability(271);
return false;
}
public boolean supportsPromptPushMessage () {
// do not ask for capabilities under specific condition
// if (deviceType == 10 && deviceVersion == 73617766697368 && deviceSoftVersion == 372E312E31) -> leo device
@@ -943,6 +949,11 @@ public class HuaweiCoordinator {
}
public int getContactsSlotCount(GBDevice device) {
if(supportsContactsSync()) {
// TODO: Currently I don't know how to obtain contacts limit in runtime, and I don't know is the limit exists,
// set limit to 20 because more items not comfortable to use in the current GB's UI. Can be increased in the future.
return 20;
}
return supportsContacts()?maxContactsCount:0;
}
@@ -292,6 +292,18 @@ public class HuaweiTLV {
return ByteBuffer.wrap(getBytes(tag)).getInt();
}
public Integer getAsInteger(int tag, int def) {
byte[] bytes = getBytes(tag, null);
if (bytes == null || bytes.length == 0 || bytes.length > 4) {
return def;
}
int res = 0;
for (int i = 0; i < bytes.length; i++) {
res |= (bytes[i] & 255) << (((bytes.length - i) - 1) * 8);
}
return res;
}
public Long getAsLong(int tag) throws HuaweiPacket.MissingTagException {
byte[] bytes = getBytes(tag);
if(bytes.length == 1) {
@@ -141,6 +141,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.datasync.Huaw
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.p2p.HuaweiP2PAppIcon;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.p2p.HuaweiP2PCalendarService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.p2p.HuaweiP2PCannedRepliesService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.p2p.HuaweiP2PContactsService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.p2p.HuaweiP2PTrackService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.p2p.HuaweiP2PDataDictionarySyncService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.AcceptAgreementsRequest;
@@ -943,6 +944,12 @@ public class HuaweiSupportProvider {
appIconService.register();
}
}
if(getHuaweiCoordinator().supportsContactsSync()) {
if (HuaweiP2PContactsService.getRegisteredInstance(huaweiP2PManager) == null) {
HuaweiP2PContactsService contactsService = new HuaweiP2PContactsService(huaweiP2PManager);
contactsService.register();
}
}
}
if (getHuaweiCoordinator().supportsThreeCircle() || getHuaweiCoordinator().supportsThreeCircleLite()) {
@@ -2706,18 +2713,27 @@ public class HuaweiSupportProvider {
}
public void onSetContacts(ArrayList<? extends Contact> contacts) {
SendSetContactsRequest sendSetContactsRequest = new SendSetContactsRequest(
this,
contacts,
this.getHuaweiCoordinator().getContactsSlotCount(getDevice())
);
try {
sendSetContactsRequest.doPerform();
} catch (IOException e) {
GB.toast(context, "Failed to set contacts", Toast.LENGTH_SHORT, GB.ERROR, e);
LOG.error("Failed to send set contacts request", e);
}
if(getHuaweiCoordinator().supportsContactsSync()) {
HuaweiP2PContactsService P2PContactsService = HuaweiP2PContactsService.getRegisteredInstance(huaweiP2PManager);
if (P2PContactsService != null) {
P2PContactsService.startSync();
}
} else if(getHuaweiCoordinator().supportsContacts()) {
SendSetContactsRequest sendSetContactsRequest = new SendSetContactsRequest(
this,
contacts,
this.getHuaweiCoordinator().getContactsSlotCount(getDevice())
);
try {
sendSetContactsRequest.doPerform();
} catch (IOException e) {
GB.toast(context, "Failed to set contacts", Toast.LENGTH_SHORT, GB.ERROR, e);
LOG.error("Failed to send set contacts request", e);
}
} else {
LOG.error("Contacts not supported");
}
}
public void onAddCalendarEvent(final CalendarEventSpec calendarEventSpec) {
@@ -0,0 +1,350 @@
/* Copyright (C) 2025 Me7c7
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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.p2p;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicBoolean;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
import nodomain.freeyourgadget.gadgetbridge.entities.Contact;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiP2PManager;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiUploadManager;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendFileUploadInfo;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class HuaweiP2PContactsService extends HuaweiBaseP2PService {
private final Logger LOG = LoggerFactory.getLogger(HuaweiP2PContactsService.class);
private final AtomicBoolean isRegistered = new AtomicBoolean(false);
public static final String MODULE = "hw.unitedevice.contactsapp";
public HuaweiP2PContactsService(HuaweiP2PManager manager) {
super(manager);
LOG.info("HuaweiP2PContactsService");
}
public static HuaweiP2PContactsService getRegisteredInstance(HuaweiP2PManager manager) {
return (HuaweiP2PContactsService) manager.getRegisteredService(MODULE);
}
@Override
public String getModule() {
return MODULE;
}
@Override
public String getPackage() {
return "in.huawei.contacts";
}
@Override
public String getFingerprint() {
return "SystemApp";
}
@Override
public void registered() {
isRegistered.set(true);
}
@Override
public void unregister() {
isRegistered.set(false);
}
private int getUtf8ByteLength(char c) {
if (c <= 127) return 1; // ASCII (1 byte)
if (c <= 2047) return 2; // Latin-1, extended ASCII (2 bytes)
return 3; // unicode characters (3 bytes)
}
private String truncateByLen(String str, int maxBytes) {
if (str == null || str.isEmpty()) {
return str;
}
StringBuilder sb = new StringBuilder();
int byteCount = 0;
for (int i = 0; i < str.length(); i++) {
char currentChar = str.charAt(i);
byteCount += getUtf8ByteLength(currentChar);
if (byteCount > maxBytes) {
break;
}
sb.append(currentChar);
}
return sb.toString();
}
public static <T> List<List<T>> partitionList(List<T> list, int chunkSize) {
if (list == null) {
return new ArrayList<>();
}
ArrayList<List<T>> chunks = new ArrayList<>();
for (int start = 0; start < list.size(); start += chunkSize) {
chunks.add(list.subList(start, Math.min(start + chunkSize, list.size())));
}
return chunks;
}
private List<String> prepareDataWithHeader(int operation, String majorVersion, int minorVersion, TreeMap<String, List<JsonObject>> map) {
List<String> ret = new ArrayList<>();
for (Map.Entry<String, List<JsonObject>> next : map.entrySet()) {
for (List<JsonObject> list : partitionList(next.getValue(), 20)) {
JsonArray contactsList = new JsonArray();
for (JsonObject obj : list) {
contactsList.add(obj);
}
JsonObject syncData = new JsonObject();
syncData.addProperty("operation", operation);
syncData.addProperty("minor", minorVersion + 1);
syncData.addProperty("major", truncateByLen(majorVersion, 32));
syncData.add("contactsList", contactsList); // max 20
syncData.addProperty("sortLetter", next.getKey());
String json = new Gson().toJson(syncData);
LOG.info(json);
ret.add(json);
}
}
return ret;
}
// NOTE: currently watch is properly works only with ASCII symbols.
public static String getLetter(final String str) {
final String tmp = str.substring(0, 1).toUpperCase(Locale.ENGLISH);
return tmp.matches("[A-Z]") ? tmp : "#";
}
public static String removeNonAscii(String str) {
return str.replaceAll("[^a-zA-Z0-9]", "");
}
private JsonObject prepareContact(final Contact con, final String sortLetter) {
JsonArray phoneList = new JsonArray();
JsonObject phone = new JsonObject();
phone.addProperty("label", "Mobile"); // TODO:
phone.addProperty("number", con.getNumber());
phoneList.add(phone);
if (phoneList.size() > 7) {
for (int i = 6; i < phoneList.size(); i++) {
phoneList.remove(i);
}
}
JsonObject contact = new JsonObject();
contact.addProperty("uuid", truncateByLen(con.getContactId(), 32));
contact.addProperty("sortLetter", sortLetter);
contact.addProperty("name", truncateByLen(con.getName(), 28));
contact.add("phoneList", phoneList);
contact.addProperty("sortKey", truncateByLen(removeNonAscii(con.getName()), 56));
return contact;
}
private List<String> prepareContactsByLetters(final List<? extends Contact> contacts, int operation, final String majorVersion, int minorVersion) {
TreeMap<String, List<JsonObject>> map = new TreeMap<>();
for (Contact c : contacts) {
String sortLetter = getLetter(c.getName());
if (!map.containsKey(sortLetter)) {
map.put(sortLetter, new ArrayList<>());
}
map.get(sortLetter).add(prepareContact(c, sortLetter));
}
return prepareDataWithHeader(operation, majorVersion, minorVersion, map);
}
private byte[] getContactsFileContent(String majorVersion, int minorVersion) {
final List<Contact> contacts = DBHelper.getContacts(manager.getSupportProvider().getDevice());
List<String> res = prepareContactsByLetters(contacts, 1, majorVersion, minorVersion);
int len = 1; // flag len, 1 byte
if(res.isEmpty()) {
len += 1;
} else {
for (String s : res) {
len += 4 + s.getBytes(StandardCharsets.UTF_8).length; // length 4 bytes + data length
}
}
ByteBuffer sendData = ByteBuffer.allocate(len);
sendData.put((byte) 0); // sync flag ??
if(res.isEmpty()) {
sendData.put((byte) 0); // no data
} else {
for (String s : res) {
byte[] dataBytes = s.getBytes(StandardCharsets.UTF_8);
sendData.putInt(dataBytes.length);
sendData.put(dataBytes);
}
}
return sendData.array();
}
private void sendContactsFile(String majorVersion, int minorVersion) {
LOG.info("Send contacts file upload info");
if (majorVersion == null || majorVersion.isEmpty()) {
majorVersion = new String(this.manager.getSupportProvider().getAndroidId(), StandardCharsets.UTF_8).toLowerCase();
}
byte[] data = getContactsFileContent(majorVersion, minorVersion);
LOG.info(GB.hexdump(data));
HuaweiUploadManager.FileUploadInfo fileInfo = new HuaweiUploadManager.FileUploadInfo();
fileInfo.setFileType((byte) 7);
fileInfo.setFileName("contacts.json");
fileInfo.setBytes(data);
fileInfo.setSrcPackage(this.getModule());
fileInfo.setDstPackage(this.getPackage());
fileInfo.setSrcFingerprint(this.getLocalFingerprint());
fileInfo.setDstFingerprint(this.getFingerprint());
fileInfo.setFileUploadCallback(new HuaweiUploadManager.FileUploadCallback() {
@Override
public void onUploadStart() {
// TODO: set device as busy in this case. But maybe exists another way to do this. Currently user see text on device card.
// Also text should be changed
manager.getSupportProvider().getDevice().setBusyTask(R.string.updating_firmware, manager.getSupportProvider().getContext());
manager.getSupportProvider().getDevice().sendDeviceUpdateIntent(manager.getSupportProvider().getContext());
}
@Override
public void onUploadProgress(int progress) {
}
@Override
public void onUploadComplete() {
if (manager.getSupportProvider().getDevice().isBusy()) {
manager.getSupportProvider().getDevice().unsetBusyTask();
manager.getSupportProvider().getDevice().sendDeviceUpdateIntent(manager.getSupportProvider().getContext());
}
}
@Override
public void onError(int code) {
if (manager.getSupportProvider().getDevice().isBusy()) {
manager.getSupportProvider().getDevice().unsetBusyTask();
manager.getSupportProvider().getDevice().sendDeviceUpdateIntent(manager.getSupportProvider().getContext());
}
}
});
HuaweiUploadManager huaweiUploadManager = this.manager.getSupportProvider().getUploadManager();
huaweiUploadManager.setFileUploadInfo(fileInfo);
try {
SendFileUploadInfo sendFileUploadInfo = new SendFileUploadInfo(this.manager.getSupportProvider(), huaweiUploadManager);
sendFileUploadInfo.doPerform();
} catch (IOException e) {
LOG.error("Failed to send file upload info", e);
}
}
public void startSync() {
HuaweiTLV tlv = new HuaweiTLV();
tlv.put(0x01, (byte) 0x01);
sendCommand(tlv.serialize(), null);
}
@Override
public void handleData(byte[] data) {
LOG.info("HuaweiP2PContactsService handleData");
try {
HuaweiTLV tlv = new HuaweiTLV();
tlv.parse(data);
LOG.error(tlv.toString());
int operateMode = tlv.getAsInteger(0x01, -1);
if (operateMode == 1) {
String majorVersion = null;
int minorVersion = 0;
int count = -1;
if (tlv.contains(0x2))
majorVersion = tlv.getString(0x2).trim();
if (tlv.contains(0x3))
minorVersion = tlv.getAsInteger(0x3, 0);
if (tlv.contains(0x4))
count = tlv.getAsInteger(0x4, -1);
LOG.info("HuaweiP2PContactsService operateMode: 1, majorVersion: {}, minorVersion: {}, count: {}", majorVersion, minorVersion, count);
sendContactsFile(majorVersion, minorVersion);
} else if (operateMode == 2) {
String majorVersion = null;
int minorVersion = 0;
int count = -1;
if (tlv.contains(0x2))
majorVersion = tlv.getString(0x2).trim();
if (tlv.contains(0x3))
minorVersion = tlv.getAsInteger(0x3, 0);
if (tlv.contains(0x4))
count = tlv.getAsInteger(0x4, -1);
LOG.info("HuaweiP2PContactsService operateMode: 2, majorVersion: {}, minorVersion: {}, count: {}", majorVersion, minorVersion, count);
//status: 0 - ok, 1 - no contacts, 2 - sync in progress, 3 - sync turned off, 4 - no permissions, 5 - not supported
byte status = 3;
final List<Contact> contacts = DBHelper.getContacts(manager.getSupportProvider().getDevice());
if (!contacts.isEmpty()) {
status = 0;
}
HuaweiTLV tlv2 = new HuaweiTLV();
tlv2.put(0x01, (byte) 0x02);
tlv2.put(0x08, status);
sendCommand(tlv2.serialize(), null);
if(status == 0) {
sendContactsFile(majorVersion, minorVersion);
}
} else if (operateMode == 3) {
int status = -1;
if (tlv.contains(0x8))
status = tlv.getAsInteger(0x8);
if (status != 0) {
LOG.error("send contacts fail status: {}", status);
}
} else {
LOG.error("unknown operateMode : {}", operateMode);
}
} catch (HuaweiPacket.MissingTagException e) {
LOG.error("Failed to handle p2p contacts", e);
}
}
}