diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiCoordinator.java index b6c6053fc..2c1673079 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiCoordinator.java @@ -55,6 +55,8 @@ public class HuaweiCoordinator { byte notificationCapabilities = -0x01; ByteBuffer notificationConstraints = null; + private boolean supportsTruSleepNewSync = false; + private Watchface.WatchfaceDeviceParams watchfaceDeviceParams; private final HuaweiCoordinatorSupplier parent; @@ -603,4 +605,11 @@ public class HuaweiCoordinator { return handler.isValid() ? handler : null; } + public boolean getSupportsTruSleepNewSync() { + return supportsTruSleepNewSync; + } + + public void setSupportsTruSleepNewSync(boolean supportsTruSleepNewSync) { + this.supportsTruSleepNewSync = supportsTruSleepNewSync; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiPacket.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiPacket.java index bd83395e4..d12c5debb 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiPacket.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiPacket.java @@ -33,6 +33,8 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Alarms; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.AccountRelated; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Calls; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.CameraRemote; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileDownloadService0A; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileDownloadService2C; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.GpsAndTime; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Watchface; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Weather; @@ -241,7 +243,7 @@ public class HuaweiPacket { protected HuaweiTLV tlv = null; private byte[] partialPacket = null; - private byte[] payload = null; + protected byte[] payload = null; public boolean complete = false; @@ -384,9 +386,11 @@ public class HuaweiPacket { if ( (serviceId == 0x0a && commandId == 0x05) || - (serviceId == 0x28 && commandId == 0x06) + (serviceId == 0x28 && commandId == 0x06) || + (serviceId == 0x2c && commandId == 0x05) ) { // TODO: this doesn't seem to be TLV + this.payload = newPayload; return; } @@ -487,6 +491,22 @@ public class HuaweiPacket { this.isEncrypted = this.attemptDecrypt(); // Helps with debugging return this; } + case FileDownloadService0A.id: + switch (this.commandId) { + case FileDownloadService0A.FileDownloadInit.id: + return new FileDownloadService0A.FileDownloadInit.Response(paramsProvider).fromPacket(this); + case FileDownloadService0A.FileParameters.id: + return new FileDownloadService0A.FileParameters.Response(paramsProvider).fromPacket(this); + case FileDownloadService0A.FileInfo.id: + return new FileDownloadService0A.FileInfo.Response(paramsProvider).fromPacket(this); + case FileDownloadService0A.RequestBlock.id: + return new FileDownloadService0A.RequestBlock.Response(paramsProvider).fromPacket(this); + case FileDownloadService0A.BlockResponse.id: + return new FileDownloadService0A.BlockResponse(paramsProvider).fromPacket(this); + default: + this.isEncrypted = this.attemptDecrypt(); + return this; + } case FindPhone.id: if (this.commandId == FindPhone.Response.id) return new FindPhone.Response(paramsProvider).fromPacket(this); @@ -580,6 +600,18 @@ public class HuaweiPacket { this.isEncrypted = this.attemptDecrypt(); // Helps with debugging return this; } + case FileDownloadService2C.id: + switch (this.commandId) { + case FileDownloadService2C.FileDownloadInit.id: + return new FileDownloadService2C.FileDownloadInit.Response(paramsProvider).fromPacket(this); + case FileDownloadService2C.FileInfo.id: + return new FileDownloadService2C.FileInfo.Response(paramsProvider).fromPacket(this); + case FileDownloadService2C.BlockResponse.id: + return new FileDownloadService2C.BlockResponse(paramsProvider).fromPacket(this); + default: + this.isEncrypted = this.attemptDecrypt(); // Helps with debugging + return this; + } default: this.isEncrypted = this.attemptDecrypt(); // Helps with debugging return this; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiSampleProvider.java index 0653145c4..fcd9585a7 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiSampleProvider.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiSampleProvider.java @@ -55,6 +55,7 @@ public class HuaweiSampleProvider extends AbstractSampleProvider interpolate(List processedSamples) { List retv = new ArrayList<>(); - if (processedSamples.size() == 0) + if (processedSamples.isEmpty()) return retv; HuaweiActivitySample lastSample = processedSamples.get(0); @@ -463,7 +473,7 @@ public class HuaweiSampleProvider extends AbstractSampleProvider sample.getOtherTimestamp()) sample.setTimestamp(sample.getTimestamp() - 1); - if (processedSamples.size() > 0) + if (!processedSamples.isEmpty()) lastSample = processedSamples.get(processedSamples.size() - 1); if (lastSample != null && lastSample.getTimestamp() == sample.getTimestamp()) { // Merge the samples - only if there isn't any data yet, except the kind @@ -496,7 +506,10 @@ public class HuaweiSampleProvider extends AbstractSampleProvider(); parse(decryptedTLV); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiTruSleepParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiTruSleepParser.java new file mode 100644 index 000000000..1f9f46840 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiTruSleepParser.java @@ -0,0 +1,74 @@ +/* Copyright (C) 2024 Martin.JM + + 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.devices.huawei; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +public class HuaweiTruSleepParser { + + public static class TruSleepStatus { + public final int startTime; + public final int endTime; + + public TruSleepStatus(int startTime, int endTime) { + this.startTime = startTime; + this.endTime = endTime; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + TruSleepStatus that = (TruSleepStatus) o; + return startTime == that.startTime && endTime == that.endTime; + } + + @Override + public String toString() { + return "TruSleepStatus{" + + "endTime=" + endTime + + ", startTime=" + startTime + + '}'; + } + } + + public static TruSleepStatus[] parseState(byte[] stateData) { + /* + Format: + - Start time (int) + - End time (int) + - Unknown (short) + - Unknown (byte) + - Padding (5 bytes) + Could be multiple available + */ + ByteBuffer buffer = ByteBuffer.wrap(stateData); + buffer.order(ByteOrder.LITTLE_ENDIAN); + TruSleepStatus[] retv = new TruSleepStatus[buffer.remaining() / 0x10]; + int c = 0; + while (stateData.length - buffer.position() >= 0x10) { + int startTime = buffer.getInt(); + int endTime = buffer.getInt(); + // Throw away for now because we don't know what it means, and we don't think we can implement this soon + buffer.get(); buffer.get(); buffer.get(); + buffer.get(); buffer.get(); buffer.get(); buffer.get(); buffer.get(); + + retv[c++] = new TruSleepStatus(startTime, endTime); + } + return retv; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/DeviceConfig.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/DeviceConfig.java index e614a959d..30f6b98aa 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/DeviceConfig.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/DeviceConfig.java @@ -950,7 +950,7 @@ public class DeviceConfig { private final byte[] selfAuthId; private final String groupId; private JSONObject version = null; - private JSONObject payload = null; + private JSONObject jsonPayload = null; private JSONObject value = null; public Request (int operationCode, long requestId, byte[] selfAuthId, String groupId) { @@ -969,7 +969,7 @@ public class DeviceConfig { this.isEncrypted = false; this.complete = true; version = new JSONObject(); - payload = new JSONObject(); + jsonPayload = new JSONObject(); value = new JSONObject(); createJson(messageId); } @@ -986,14 +986,14 @@ public class DeviceConfig { super(paramsProvider, messageId); // createJson(1); //messageId); try { - payload + jsonPayload .put("isoSalt", StringUtils.bytesToHex(isoSalt)) .put("peerAuthId", StringUtils.bytesToHex(selfAuthId)) .put("operationCode", operationCode) .put("seed", StringUtils.bytesToHex(seed)) .put("peerUserType", 0x00); if (operationCode == 0x02) { - payload + jsonPayload .put("pkgName", "com.huawei.devicegroupmanage") .put("serviceType", groupId) .put("keyLength", 0x20); @@ -1020,7 +1020,7 @@ public class DeviceConfig { super(paramsProvider, messageId); // createJson(2); //messageId); try { - payload + jsonPayload .put("peerAuthId", StringUtils.bytesToHex(selfAuthId)) .put("token", StringUtils.bytesToHex(token)); if (operationCode == 0x02) value.put("isDeviceLevel", false); @@ -1044,7 +1044,7 @@ public class DeviceConfig { super(paramsProvider, messageId); // createJson(3); try { - payload + jsonPayload .put("nonce", StringUtils.bytesToHex(nonce)) .put("encData", StringUtils.bytesToHex(encData)); this.tlv = new HuaweiTLV() @@ -1071,7 +1071,7 @@ public class DeviceConfig { // createJson(3); // } try { - payload + jsonPayload .put("nonce", StringUtils.bytesToHex(nonce)) //generateRandom .put("encResult", StringUtils.bytesToHex(encResult)) .put("operationCode", operationCode); @@ -1093,11 +1093,11 @@ public class DeviceConfig { version .put("minVersion", "1.0.0") .put("currentVersion", "2.0.16"); - payload + jsonPayload .put("version", version); value .put("authForm", 0x00) - .put("payload", payload) + .put("payload", jsonPayload) .put("groupAndModuleVersion", "2.0.1") .put("message", messageId); if (operationCode == 0x01) { @@ -1201,7 +1201,7 @@ public class DeviceConfig { public byte type; public JSONObject value; - public JSONObject payload; + public JSONObject jsonPayload; public byte step; // public int operationCode; // TODO @@ -1225,18 +1225,18 @@ public class DeviceConfig { if (this.type == 0x00) { try { this.value = new JSONObject(this.tlv.getString(0x01)); - this.payload = value.getJSONObject("payload"); + this.jsonPayload = value.getJSONObject("payload"); // Ugly, but should work - if (payload.has("isoSalt")) { + if (jsonPayload.has("isoSalt")) { this.step = 0x01; - this.step1Data = new Step1Data(payload); - } else if (payload.has("returnCodeMac")) { + this.step1Data = new Step1Data(jsonPayload); + } else if (jsonPayload.has("returnCodeMac")) { this.step = 0x02; - this.step2Data = new Step2Data(payload); - } else if (payload.has("encAuthToken")) { + this.step2Data = new Step2Data(jsonPayload); + } else if (jsonPayload.has("encAuthToken")) { this.step = 0x03; - this.step3Data = new Step3Data(payload); + this.step3Data = new Step3Data(jsonPayload); } } catch (JSONException e) { throw new JsonException("", e); @@ -1344,7 +1344,7 @@ public class DeviceConfig { public long requestId; public byte[] selfAuthId; public String groupId; - public JSONObject payload = null; + public JSONObject jsonPayload = null; public JSONObject value = null; public Step1Data step1Data; @@ -1361,20 +1361,20 @@ public class DeviceConfig { public void parseTlv() throws ParseException { try { value = new JSONObject(this.tlv.getString(0x01)); - payload = value.getJSONObject("payload"); + jsonPayload = value.getJSONObject("payload"); - if (payload.has("isoSalt")) { + if (jsonPayload.has("isoSalt")) { this.step = 1; - this.step1Data = new Step1Data(payload); - } else if (payload.has("token")) { + this.step1Data = new Step1Data(jsonPayload); + } else if (jsonPayload.has("token")) { this.step = 2; - this.step2Data = new Step2Data(payload); - } else if (payload.has("encData")) { + this.step2Data = new Step2Data(jsonPayload); + } else if (jsonPayload.has("encData")) { this.step = 3; - this.step3Data = new Step3Data(payload); - } else if (payload.has("encResult")) { + this.step3Data = new Step3Data(jsonPayload); + } else if (jsonPayload.has("encResult")) { this.step = 4; - this.step4Data = new Step4Data(payload); + this.step4Data = new Step4Data(jsonPayload); } } catch (JSONException e) { throw new JsonException("Cannot parse JSON", e); @@ -1493,6 +1493,8 @@ public class DeviceConfig { } public static class Response extends HuaweiPacket { + public boolean truSleepNewSync = false; + public Response(ParamsProvider paramsProvider) { super(paramsProvider); this.serviceId = DeviceConfig.id; @@ -1501,8 +1503,16 @@ public class DeviceConfig { @Override public void parseTlv() throws ParseException { + // Works with bitmaps + // Tag 1 -> LegalStuff - // Tag 2 -> File support + + if (this.tlv.contains(0x02)) { + // Tag 2 -> File support + byte value = this.tlv.getByte(0x02); + truSleepNewSync = (value & 2) != 0; + } + // Tag 3 -> SmartWatchVersion // Tag 4 to 6 are HMS related } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/FileDownloadService0A.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/FileDownloadService0A.java new file mode 100644 index 000000000..482b9317d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/FileDownloadService0A.java @@ -0,0 +1,237 @@ +/* Copyright (C) 2024 Martin.JM + + 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.devices.huawei.packets; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV; + +/** + * File downloading for "older" devices/implementations + * Newer ones might be using service id 0x2c + * Which one is used is reported by the band in 0x01 0x31 + */ +public class FileDownloadService0A { + public static final int id = 0x0a; + + /* + Type of files that can be downloaded through here: + - debug files + - sleep files + - rrisqi file + */ + + public static class FileDownloadInit { + public static final int id = 0x01; + + public static class DebugFilesRequest extends HuaweiPacket { + public DebugFilesRequest(ParamsProvider paramsProvider) { + super(paramsProvider); + + this.serviceId = FileDownloadService0A.id; + this.commandId = id; + + this.tlv = new HuaweiTLV(); // Empty TLV + + this.complete = true; + } + } + + public static class SleepFilesRequest extends HuaweiPacket { + public SleepFilesRequest(ParamsProvider paramsProvider, int startTime, int endTime) { + super(paramsProvider); + + this.serviceId = FileDownloadService0A.id; + this.commandId = id; + + this.tlv = new HuaweiTLV() + .put(0x02, (byte) 0x01) + .put(0x83, new HuaweiTLV() + .put(0x04, startTime) + .put(0x05, endTime) + ); + + this.complete = true; + } + } + + public static class Response extends HuaweiPacket { + public String[] fileNames; + + public Response(ParamsProvider paramsProvider) { + super(paramsProvider); + } + + @Override + public void parseTlv() throws ParseException { + String possibleNames = this.tlv.getString(0x01); + fileNames = possibleNames.split(";"); + } + } + } + + public static class FileParameters { + public static final int id = 0x02; + + public static class Request extends HuaweiPacket { + public Request(ParamsProvider paramsProvider) { + super(paramsProvider); + + this.serviceId = FileDownloadService0A.id; + this.commandId = id; + + this.tlv = new HuaweiTLV().put(0x06, (byte) 1); + + this.complete = true; + } + } + + public static class Response extends HuaweiPacket { + public String version; + public boolean unknown; + public short packetSize; + public short maxBlockSize; + public short timeout; + + public Response(ParamsProvider paramsProvider) { + super(paramsProvider); + } + + @Override + public void parseTlv() throws ParseException { + // TODO: below could be different for AW70? + + this.version = this.tlv.getString(0x01); + this.unknown = this.tlv.getBoolean(0x02); + this.packetSize = this.tlv.getShort(0x03); + this.maxBlockSize = this.tlv.getShort(0x04); + this.timeout = this.tlv.getShort(0x05); + } + } + } + + public static class FileInfo { + public static final int id = 0x03; + + public static class Request extends HuaweiPacket { + public Request(ParamsProvider paramsProvider, String fileName) { + super(paramsProvider); + + this.serviceId = FileDownloadService0A.id; + this.commandId = id; + + this.tlv = new HuaweiTLV().put(0x01, fileName); + + this.complete = true; + } + } + + public static class Response extends HuaweiPacket { + public int fileLength; + public byte transferType = -1; + public int fileCreateTime = -1; + public byte unknown = -1; + + public Response(ParamsProvider paramsProvider) { + super(paramsProvider); + } + + @Override + public void parseTlv() throws ParseException { + this.fileLength = this.tlv.getInteger(0x02); + if (this.tlv.contains(0x04)) + this.transferType = this.tlv.getByte(0x04); + if (this.tlv.contains(0x05)) + this.fileCreateTime = this.tlv.getInteger(0x05); + if (this.tlv.contains(0x06)) + this.unknown = this.tlv.getByte(0x06); + } + } + } + + public static class RequestBlock { + public static final int id = 0x04; + + public static class Request extends HuaweiPacket { + public Request(ParamsProvider paramsProvider, String fileName, int offset, int size) { + super(paramsProvider); + + this.serviceId = FileDownloadService0A.id; + this.commandId = id; + + this.tlv = new HuaweiTLV() + .put(0x01, fileName) + .put(0x02, offset) // TODO: not for AW70 + .put(0x03, size); // TODO: not for AW70 + + this.complete = true; + } + } + + public static class Response extends HuaweiPacket { + public boolean isOk; + public String filename; + public int offset; + + public Response(ParamsProvider paramsProvider) { + super(paramsProvider); + } + + public void parseTlv() throws ParseException { + isOk = this.tlv.getInteger(0x7f) == 0x000186A0; + if (isOk) { + if (this.tlv.contains(0x01)) + filename = this.tlv.getString(0x01); + offset = this.tlv.getInteger(0x02); + } + } + } + } + + public static class BlockResponse extends HuaweiPacket { + public static final int id = 0x05; + + public byte number; + public byte[] data; + + public BlockResponse(ParamsProvider paramsProvider) { + super(paramsProvider); + } + + @Override + public void parseTlv() throws ParseException { + // Note that this packet does not contain TLV data + this.number = this.payload[2]; + this.data = new byte[this.payload.length - 3]; + System.arraycopy(this.payload, 3, this.data, 0, this.payload.length - 3); + } + } + + public static class FileDownloadCompleteRequest extends HuaweiPacket { + public static final int id = 0x06; + + public FileDownloadCompleteRequest(ParamsProvider paramsProvider) { + super(paramsProvider); + + this.serviceId = FileDownloadService0A.id; + this.commandId = id; + + this.tlv = new HuaweiTLV().put(0x06, true); + + this.complete = true; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/FileDownloadService2C.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/FileDownloadService2C.java new file mode 100644 index 000000000..dba51c29f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/FileDownloadService2C.java @@ -0,0 +1,213 @@ +/* Copyright (C) 2024 Martin.JM + + 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.devices.huawei.packets; + +import java.nio.ByteBuffer; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCrypto; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV; + +/** + * File downloading for "older" devices/implementations + * Newer ones might be using service id 0x2c + * Which one is used is reported by the band in 0x01 0x31 + */ +public class FileDownloadService2C { + public static final int id = 0x2c; + + public enum FileType { + SLEEP, + UNKNOWN; // Never use this as input + + static byte fileTypeToByte(FileType fileType) { + switch (fileType) { + case SLEEP: + return (byte) 0x0e; + default: + throw new RuntimeException(); + } + } + + static FileType byteToFileType(byte b) { + switch (b) { + case 0x0e: + return FileType.SLEEP; + default: + return FileType.UNKNOWN; + } + } + } + + public static class FileDownloadInit { + public static final int id = 0x01; + + public static class Request extends HuaweiPacket { + public Request(ParamsProvider paramsProvider, String filename, FileType filetype, int startTime, int endTime) { + super(paramsProvider); + + this.serviceId = FileDownloadService2C.id; + this.commandId = id; + + // TODO: start and end time might be optional? + this.tlv = new HuaweiTLV() + .put(0x01, filename) + .put(0x02, FileType.fileTypeToByte(filetype)) + .put(0x05, startTime) + .put(0x06, endTime); + + this.complete = true; + } + } + + public static class Response extends HuaweiPacket { + public String fileName; + public FileType fileType; + public byte fileId; + public int fileSize; + + public Response(ParamsProvider paramsProvider) { + super(paramsProvider); + } + + @Override + public void parseTlv() throws ParseException { + fileName = this.tlv.getString(0x01); + fileType = FileType.byteToFileType(this.tlv.getByte(0x02)); + fileId = this.tlv.getByte(0x03); + fileSize = this.tlv.getInteger(0x04); + } + } + } + + public static class FileInfo { + public static final int id = 0x03; + + public static class Request extends HuaweiPacket { + public Request(ParamsProvider paramsProvider, byte fileId) { + super(paramsProvider); + + this.serviceId = FileDownloadService2C.id; + this.commandId = id; + + this.tlv = new HuaweiTLV() + .put(0x01, fileId) + .put(0x02) + .put(0x03) + .put(0x04) + .put(0x05); + + this.complete = true; + } + } + + public static class Response extends HuaweiPacket { + public byte fileId; + public byte timeout; // TODO: not sure about unit here - maybe seconds? + // TODO: following two might not have the best names... + public short burstSize; // How large each 0x2c 0x05 will be + public int maxBlockSize; // How much we can ask for before needing another 0x2c 0x04 + public boolean noEncrypt; + + public Response(ParamsProvider paramsProvider) { + super(paramsProvider); + } + + @Override + public void parseTlv() throws ParseException { + fileId = this.tlv.getByte(0x01); + timeout = this.tlv.getByte(0x02); + burstSize = this.tlv.getShort(0x03); + maxBlockSize = this.tlv.getInteger(0x04); + noEncrypt = this.tlv.getBoolean(0x05); // True if command 0x04 cannot be encrypted + } + } + } + + public static class RequestBlock extends HuaweiPacket { + public static final int id = 0x04; + + public RequestBlock(ParamsProvider paramsProvider, byte fileId, int offset, int size, boolean noEncrypt) { + super(paramsProvider); + + this.serviceId = FileDownloadService2C.id; + this.commandId = id; + + this.tlv = new HuaweiTLV() + .put(0x01, fileId) + .put(0x02, offset) + .put(0x03, size); + + this.complete = true; + this.isEncrypted = !noEncrypt; + } + } + + public static class BlockResponse extends HuaweiPacket { + public static final int id = 0x05; + + public byte fileId; + public int offset; + public byte unknown; + public byte[] data; + + public BlockResponse(ParamsProvider paramsProvider) { + super(paramsProvider); + } + + @Override + public void parseTlv() throws ParseException { + ByteBuffer byteBuffer; + if (this.payload[2] == 0x7c && this.payload[3] == 0x01 && this.payload[4] == 0x01) { + // Encrypted TLV, so we decrypt first + this.tlv = new HuaweiTLV(); + this.tlv.parse(this.payload, 2, this.payload.length - 2); + try { + byteBuffer = ByteBuffer.wrap(this.tlv.decryptRaw(paramsProvider)); + } catch (HuaweiCrypto.CryptoException e) { + throw new CryptoException("File download decryption exception", e); + } + this.tlv = null; // Prevent using it accidentally + } else { + byteBuffer = ByteBuffer.wrap(this.payload, 2, this.payload.length - 2); + } + + fileId = byteBuffer.get(); + offset = byteBuffer.getInt(); + unknown = byteBuffer.get(); + data = new byte[byteBuffer.remaining()]; + System.arraycopy(byteBuffer.array(), byteBuffer.position(), data, 0, byteBuffer.remaining()); + } + } + + public static class FileDownloadCompleteRequest extends HuaweiPacket { + public static final int id = 0x06; + + public FileDownloadCompleteRequest(ParamsProvider paramsProvider, byte fileId) { + super(paramsProvider); + + this.serviceId = FileDownloadService2C.id; + this.commandId = id; + + this.tlv = new HuaweiTLV() + .put(0x01, fileId) + .put(0x02, (byte) 1); + + this.complete = true; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiBRSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiBRSupport.java index 59bca5e62..0a4a287cd 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiBRSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiBRSupport.java @@ -175,4 +175,8 @@ public class HuaweiBRSupport extends AbstractBTBRDeviceSupport { supportProvider.dispose(); super.dispose(); } + + public void onTestNewFunction() { + supportProvider.onTestNewFunction(); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiFileDownloadManager.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiFileDownloadManager.java new file mode 100644 index 000000000..d3146aa90 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiFileDownloadManager.java @@ -0,0 +1,547 @@ +/* Copyright (C) 2024 Martin.JM + + 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.huawei; + +import android.os.Handler; +import android.os.Looper; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileDownloadService0A; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileDownloadService2C; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetFileBlockRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetFileDownloadCompleteRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetFileDownloadInitRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetFileInfoRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetFileParametersRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.Request; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class HuaweiFileDownloadManager { + private static final Logger LOG = LoggerFactory.getLogger(HuaweiFileDownloadManager.class); + + public static class HuaweiFileDownloadException extends Exception { + @Nullable + public final FileRequest fileRequest; + + HuaweiFileDownloadException(@Nullable FileRequest fileRequest, String message) { + super(message); + this.fileRequest = fileRequest; + } + + HuaweiFileDownloadException(@Nullable FileRequest fileRequest, String message, Exception e) { + super(message, e); + this.fileRequest = fileRequest; + } + } + + public static class HuaweiFileDownloadSendException extends HuaweiFileDownloadException { + HuaweiFileDownloadSendException(@Nullable FileRequest fileRequest, Request request, Exception e) { + super(fileRequest, "Error sending request " + request.getName(), e); + } + } + + public static class HuaweiFileDownloadRequestException extends HuaweiFileDownloadException { + HuaweiFileDownloadRequestException(@Nullable FileRequest fileRequest, Class requestClass, Exception e) { + super(fileRequest, "Error in request of class " + requestClass, e); + } + } + + public static class HuaweiFileDownloadTimeoutException extends HuaweiFileDownloadException { + HuaweiFileDownloadTimeoutException(@Nullable FileRequest fileRequest) { + super(fileRequest, "Timeout was hit!"); + } + } + + public static class HuaweiFileDownloadFileMismatchException extends HuaweiFileDownloadException { + HuaweiFileDownloadFileMismatchException(@NonNull FileRequest fileRequest, String filename) { + super(fileRequest, "Data for wrong file received. Expected name " + fileRequest.filename + ", got name " + filename); + } + + HuaweiFileDownloadFileMismatchException(@NonNull FileRequest fileRequest, FileType fileType) { + super(fileRequest, "Data for wrong file received. Expected type " + fileRequest.fileType + ", got type " + fileType); + } + + HuaweiFileDownloadFileMismatchException(@NonNull FileRequest fileRequest, String[] filenames) { + super(fileRequest, "File " + fileRequest.filename + " cannot be downloaded using this method. Only the following files are supported: " + Arrays.toString(filenames)); + } + + HuaweiFileDownloadFileMismatchException(@NonNull FileRequest fileRequest, int number, boolean newSync) { + super( + fileRequest, + "Data for wrong file received. Expected " + + (newSync ? + "id " + fileRequest.fileId + ", got id " + number : + "packet number " + (fileRequest.lastPacketNumber + 1) + ", got " + number) + ); + } + } + + /** + * Only for internal use + */ + public enum FileType { + DEBUG, + SLEEP, + UNKNOWN // Never for input! + } + + /** + * Only for internal use, though also used in exception + */ + public static class FileRequest { + // Inputs + + public String filename; + public FileType fileType; + public boolean newSync; + + // Sleep type only + public int startTime; + public int endTime; + + + // Retrieved + + public int fileSize; + public int maxBlockSize; + public int timeout; // TODO: unit? + public ByteBuffer buffer; + + public int startOfBlockOffset; + public int currentBlockSize; + + // Old sync only + public String[] filenames; + public byte lastPacketNumber; + + // New sync only + public byte fileId; + public boolean noEncrypt; + } + + /** + * Actually receives the file data + */ + private static class FileDataReceiver extends Request { + + public boolean newSync; + public byte[] data; + + // Packet number for old sync + // fileId for new sync + public byte number; + + public FileDataReceiver(HuaweiSupportProvider supportProvider) { + super(supportProvider); + } + + @Override + public boolean handleResponse(HuaweiPacket response) { + if ( + (response.serviceId == FileDownloadService0A.id && + response.commandId == FileDownloadService0A.BlockResponse.id) || + (response.serviceId == FileDownloadService2C.id && + response.commandId == FileDownloadService2C.BlockResponse.id) + ) { + receivedPacket = response; + return true; + } + return false; + } + + @Override + public boolean autoRemoveFromResponseHandler() { + // This needs to be removed manually + return false; + } + + @Override + protected void processResponse() throws ResponseParseException { + if (this.receivedPacket instanceof FileDownloadService0A.BlockResponse) { + this.newSync = false; + this.number = ((FileDownloadService0A.BlockResponse) this.receivedPacket).number; + this.data = ((FileDownloadService0A.BlockResponse) this.receivedPacket).data; + } else if (this.receivedPacket instanceof FileDownloadService2C.BlockResponse) { + this.newSync = true; + this.number = ((FileDownloadService2C.BlockResponse) this.receivedPacket).fileId; + this.data = ((FileDownloadService2C.BlockResponse) this.receivedPacket).data; + } else { + throw new ResponseTypeMismatchException( + this.receivedPacket, + FileDownloadService0A.BlockResponse.class, + FileDownloadService2C.BlockResponse.class + ); + } + } + } + + private final HuaweiSupportProvider supportProvider; + private final Handler handler; + private final Runnable timeout; + + // Cannot be final as we need the device to be connected before creating this + private FileDataReceiver fileDataReceiver; + + // For old sync we cannot download multiple files at the same time, for new sync we don't want + // to, so we limit that. We also do not allow old and new sync at the same time. + // Note that old and new sync are already split by the serviceID and commandID that are used for + // all the requests. + // Note that the timeout is not ready for concurrent downloads + private boolean isBusy; + + private final ArrayList fileRequests; + private FileRequest currentFileRequest; + + public HuaweiFileDownloadManager(HuaweiSupportProvider supportProvider) { + this.supportProvider = supportProvider; + handler = new Handler(Looper.getMainLooper()); + isBusy = false; + fileRequests = new ArrayList<>(); + timeout = () -> { + this.supportProvider.downloadException(new HuaweiFileDownloadTimeoutException(currentFileRequest)); + reset(); + }; + } + + public void downloadDebug(String filename) { + FileRequest request = new FileRequest(); + request.filename = filename; + request.fileType = FileType.DEBUG; + request.newSync = false; + synchronized (supportProvider) { + fileRequests.add(request); + } + startDownload(); + } + + public void downloadSleep(boolean supportsTruSleepNewSync, String filename, int startTime, int endTime) { + FileRequest request = new FileRequest(); + request.filename = filename; + request.fileType = FileType.SLEEP; + request.newSync = supportsTruSleepNewSync; + request.startTime = startTime; + request.endTime = endTime; + synchronized (supportProvider) { + fileRequests.add(request); + } + startDownload(); + } + + private boolean arrayContains(String[] haystack, String needle) { + return Arrays.stream(haystack).anyMatch(s -> s.equals(needle)); + } + + private void initFileDataReceiver() { + if (fileDataReceiver == null) { + // We can only init fileDataReceiver if the device is already connected + fileDataReceiver = new FileDataReceiver(supportProvider); + fileDataReceiver.setFinalizeReq(new Request.RequestCallback() { + @Override + public void call() { + // Reset timeout + handler.removeCallbacks(HuaweiFileDownloadManager.this.timeout); + handler.postDelayed(HuaweiFileDownloadManager.this.timeout, currentFileRequest.timeout * 1000L); + + // Handle data + handleFileData(fileDataReceiver.newSync, fileDataReceiver.number, fileDataReceiver.data); + } + + @Override + public void handleException(Request.ResponseParseException e) { + supportProvider.downloadException(new HuaweiFileDownloadRequestException(null, this.getClass(), e)); + } + }); + } + } + + public void startDownload() { + initFileDataReceiver(); // Make sure the fileDataReceiver is ready + + synchronized (this.supportProvider) { + if (this.isBusy) + return; // Already downloading, this file will come eventually + if (this.fileRequests.isEmpty()) { + // No more files to download + supportProvider.downloadQueueEmpty(); + return; + } + this.isBusy = true; + } + + this.currentFileRequest = this.fileRequests.remove(0); + + GetFileDownloadInitRequest getFileDownloadInitRequest = new GetFileDownloadInitRequest(supportProvider, currentFileRequest); + getFileDownloadInitRequest.setFinalizeReq(new Request.RequestCallback() { + @Override + public void call(Request request) { + // For multi-download, match to file instead of assuming current + GetFileDownloadInitRequest r = (GetFileDownloadInitRequest) request; + if (r.newSync) { + if (!currentFileRequest.filename.equals(r.filename)) { + supportProvider.downloadException(new HuaweiFileDownloadFileMismatchException( + currentFileRequest, + r.filename + )); + reset(); + return; + } + if (currentFileRequest.fileType != r.fileType) { + supportProvider.downloadException(new HuaweiFileDownloadFileMismatchException( + currentFileRequest, + r.fileType + )); + reset(); + return; + } + currentFileRequest.fileId = r.fileId; + currentFileRequest.fileSize = r.fileSize; + if (r.fileSize == 0) { + // Nothing to download, go to end + fileComplete(); + return; + } + getFileInfo(); + } else { + if (!arrayContains(r.filenames, currentFileRequest.filename)) { + supportProvider.downloadException(new HuaweiFileDownloadFileMismatchException( + currentFileRequest, + r.filenames + )); + reset(); + return; + } + currentFileRequest.filenames = r.filenames; + getDownloadParameters(); + } + } + + @Override + public void handleException(Request.ResponseParseException e) { + supportProvider.downloadException(new HuaweiFileDownloadRequestException(currentFileRequest, this.getClass(), e)); + } + }); + try { + getFileDownloadInitRequest.doPerform(); + } catch (IOException e) { + supportProvider.downloadException(new HuaweiFileDownloadSendException(currentFileRequest, getFileDownloadInitRequest, e)); + reset(); + } + } + + private void getDownloadParameters() { + // Old sync only, can never be multiple at the same time + // Assuming currentRequest is the correct one the entire time + // Which may no longer be the case when we implement multi-download for new sync + GetFileParametersRequest getFileParametersRequest = new GetFileParametersRequest(supportProvider); + getFileParametersRequest.setFinalizeReq(new Request.RequestCallback() { + @Override + public void call() { + currentFileRequest.maxBlockSize = getFileParametersRequest.getMaxBlockSize(); + currentFileRequest.timeout = getFileParametersRequest.getTimeout(); + getFileInfo(); + } + + @Override + public void handleException(Request.ResponseParseException e) { + supportProvider.downloadException(new HuaweiFileDownloadRequestException(null, this.getClass(), e)); + reset(); + } + }); + try { + getFileParametersRequest.doPerform(); + } catch (IOException e) { + supportProvider.downloadException(new HuaweiFileDownloadSendException(currentFileRequest, getFileParametersRequest, e)); + reset(); + } + } + + private void getFileInfo() { + GetFileInfoRequest getFileInfoRequest = new GetFileInfoRequest(supportProvider, currentFileRequest); + getFileInfoRequest.setFinalizeReq(new Request.RequestCallback() { + @Override + public void call(Request request) { + GetFileInfoRequest r = (GetFileInfoRequest) request; + if (r.newSync) { + if (currentFileRequest.fileId != r.fileId) { + supportProvider.downloadException(new HuaweiFileDownloadFileMismatchException(currentFileRequest, r.fileId, true)); + reset(); + return; + } + currentFileRequest.timeout = r.timeout; + currentFileRequest.maxBlockSize = r.maxBlockSize; + currentFileRequest.noEncrypt = r.noEncrypt; + } else { + // currentFileRequest MUST BE correct here + currentFileRequest.fileSize = r.fileLength; + + if (currentFileRequest.fileSize == 0) { + // Nothing to download, go to complete + fileComplete(); + return; + } + } + downloadNextFileBlock(); + } + + @Override + public void handleException(Request.ResponseParseException e) { + supportProvider.downloadException(new HuaweiFileDownloadRequestException(null, this.getClass(), e)); + reset(); + } + }); + try { + getFileInfoRequest.doPerform(); + } catch (IOException e) { + supportProvider.downloadException(new HuaweiFileDownloadSendException(currentFileRequest, getFileInfoRequest, e)); + reset(); + } + } + + private void downloadNextFileBlock() { + handler.removeCallbacks(this.timeout); + + if (currentFileRequest.buffer == null) // New file + currentFileRequest.buffer = ByteBuffer.allocate(currentFileRequest.fileSize); + currentFileRequest.lastPacketNumber = -1; // Counts per block + currentFileRequest.startOfBlockOffset = currentFileRequest.buffer.position(); + currentFileRequest.currentBlockSize = Math.min( + currentFileRequest.fileSize - currentFileRequest.buffer.position(), // Remaining file size + currentFileRequest.maxBlockSize // Max we can ask for + ); + + // Start listening for file data + this.supportProvider.addInProgressRequest(fileDataReceiver); + + GetFileBlockRequest getFileBlockRequest = new GetFileBlockRequest(supportProvider, currentFileRequest); + getFileBlockRequest.setFinalizeReq(new Request.RequestCallback() { + @Override + public void call() { + // Reset timeout + handler.removeCallbacks(HuaweiFileDownloadManager.this.timeout); + handler.postDelayed(HuaweiFileDownloadManager.this.timeout, currentFileRequest.timeout * 1000L); + } + }); + try { + getFileBlockRequest.doPerform(); + } catch (IOException e) { + supportProvider.downloadException(new HuaweiFileDownloadSendException(currentFileRequest, getFileBlockRequest, e)); + reset(); + } + } + + private void handleFileData(boolean newSync, byte number, byte[] data) { + if (newSync && currentFileRequest.fileId != number) { + supportProvider.downloadException(new HuaweiFileDownloadFileMismatchException(currentFileRequest, number, true)); + reset(); + return; + } + if (!newSync) { + if (currentFileRequest.lastPacketNumber != number - 1) { + supportProvider.downloadException(new HuaweiFileDownloadFileMismatchException(currentFileRequest, number, false)); + reset(); + return; + } + currentFileRequest.lastPacketNumber = number; + } + + currentFileRequest.buffer.put(data); + + if (currentFileRequest.buffer.position() >= currentFileRequest.fileSize) { + // File complete + LOG.info("Download complete for file {}", currentFileRequest.filename); + if (currentFileRequest.buffer.position() != currentFileRequest.fileSize) { + GB.toast("Downloaded file is larger than expected", Toast.LENGTH_SHORT, GB.ERROR); + LOG.error("Downloaded file is larger than expected: {}", currentFileRequest.filename); + } + fileComplete(); + } else if (currentFileRequest.buffer.position() - currentFileRequest.startOfBlockOffset >= currentFileRequest.maxBlockSize) { + // Block complete, need to request a new file block + downloadNextFileBlock(); + } // Else we're expecting more data to arrive automatically + } + + private void fileComplete() { + // Stop timeout from hitting + this.handler.removeCallbacks(this.timeout); + + // File complete request + GetFileDownloadCompleteRequest getFileDownloadCompleteRequest = new GetFileDownloadCompleteRequest(supportProvider, currentFileRequest); + try { + getFileDownloadCompleteRequest.doPerform(); + } catch (IOException e) { + supportProvider.downloadException(new HuaweiFileDownloadSendException(currentFileRequest, getFileDownloadCompleteRequest, e)); + reset(); + } + + // Handle file data + if (currentFileRequest.buffer != null) // File size was zero + supportProvider.downloadComplete(currentFileRequest.filename, currentFileRequest.buffer.array()); + + if (!this.currentFileRequest.newSync && !this.fileRequests.isEmpty() && !this.fileRequests.get(0).newSync) { + // Old sync can potentially take a shortcut + if (arrayContains(this.currentFileRequest.filenames, this.fileRequests.get(0).filename)) { + // Shortcut to next download + // - No init + // - No getting parameters + // Just copy over the info and go directly to GetFileInfo + FileRequest nextRequest = this.fileRequests.remove(0); + + nextRequest.filenames = this.currentFileRequest.filenames; + nextRequest.maxBlockSize = this.currentFileRequest.maxBlockSize; + nextRequest.timeout = this.currentFileRequest.timeout; + + this.currentFileRequest = nextRequest; + getFileInfo(); + return; + } + } + + reset(); + } + + private void reset() { + // Stop listening for file data, if we were doing that + this.supportProvider.removeInProgressRequests(this.fileDataReceiver); + + // Reset current request + this.currentFileRequest = null; + + // Unset busy, otherwise the next download will never start + synchronized (this.supportProvider) { + this.isBusy = false; + } + // Try to download next file + startDownload(); + } + + public void dispose() { + // Stop timeout from hitting, nothing else to really do + this.handler.removeCallbacks(this.timeout); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiLESupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiLESupport.java index e12260a83..4d30cfd75 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiLESupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiLESupport.java @@ -183,4 +183,8 @@ public class HuaweiLESupport extends AbstractBTLEDeviceSupport { supportProvider.dispose(); super.dispose(); } + + public void onTestNewFunction() { + supportProvider.onTestNewFunction(); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java index 5a37b22aa..d373f1cc0 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java @@ -55,6 +55,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCoordinatorSupp import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCrypto; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTruSleepParser; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.CameraRemote; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.GpsAndTime; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Weather; @@ -200,6 +201,8 @@ public class HuaweiSupportProvider { protected HuaweiWatchfaceManager huaweiWatchfaceManager = new HuaweiWatchfaceManager(this); + protected HuaweiFileDownloadManager huaweiFileDownloadManager = new HuaweiFileDownloadManager(this); + public HuaweiCoordinatorSupplier getCoordinator() { return ((HuaweiCoordinatorSupplier) this.gbDevice.getDeviceCoordinator()); } @@ -1092,7 +1095,7 @@ public class HuaweiSupportProvider { private void fetchActivityData() { int sleepStart = 0; int stepStart = 0; - int end = (int) (System.currentTimeMillis() / 1000); + final int end = (int) (System.currentTimeMillis() / 1000); SharedPreferences sharedPreferences = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()); long prefLastSyncTime = sharedPreferences.getLong("lastSyncTimeMillis", 0); @@ -1129,12 +1132,15 @@ public class HuaweiSupportProvider { } final GetStepDataCountRequest getStepDataCountRequest = new GetStepDataCountRequest(this, stepStart, end); + //noinspection ExtractMethodRecommender final GetFitnessTotalsRequest getFitnessTotalsRequest = new GetFitnessTotalsRequest(this); + final int start = sleepStart; getFitnessTotalsRequest.setFinalizeReq(new RequestCallback() { @Override public void call() { - handleSyncFinished(); + if (!downloadTruSleepData(start, end)) + handleSyncFinished(); } @Override @@ -1455,18 +1461,20 @@ public class HuaweiSupportProvider { responseManager.addHandler(request); } - public void addSleepActivity(int timestamp, short duration, byte type) { + public void addSleepActivity(int timestamp_start, int timestamp_end, byte type, byte source) { + LOG.debug("Adding sleep activity between {} and {}", timestamp_start, timestamp_end); + try (DBHandler db = GBApplication.acquireDB()) { Long userId = DBHelper.getUser(db.getDaoSession()).getId(); Long deviceId = DBHelper.getDevice(gbDevice, db.getDaoSession()).getId(); HuaweiSampleProvider sampleProvider = new HuaweiSampleProvider(gbDevice, db.getDaoSession()); HuaweiActivitySample activitySample = new HuaweiActivitySample( - timestamp, + timestamp_start, deviceId, userId, - timestamp + duration, - FitnessData.MessageData.sleepId, + timestamp_end, + source, type, 1, ActivitySample.NOT_MEASURED, @@ -2071,5 +2079,78 @@ public class HuaweiSupportProvider { public void dispose() { stopBatteryRunnerDelayed(); + huaweiFileDownloadManager.dispose(); + } + + public boolean downloadTruSleepData(int start, int end) { + // We only get the data if TruSleep is supported + if (!getHuaweiCoordinator().supportsTruSleep()) + return false; + + huaweiFileDownloadManager.downloadSleep( + getHuaweiCoordinator().getSupportsTruSleepNewSync(), + "sleep_state.bin", // new String[] {"sleep_state.bin"}, // Later also "sleep_data.bin", but we don't use it right now + start, + end + ); + return true; + } + + /** + * Called when a file download is complete + * @param fileName Filename of the file + * @param fileContents Contents of the file + */ + public void downloadComplete(String fileName, byte[] fileContents) { + LOG.debug("File download complete: {}: {}", fileName, GB.hexdump(fileContents)); + + if (fileName.equals("sleep_state.bin")) { + HuaweiTruSleepParser.TruSleepStatus[] results = HuaweiTruSleepParser.parseState(fileContents); + for (HuaweiTruSleepParser.TruSleepStatus status : results) + addSleepActivity(status.startTime, status.endTime, (byte) 0x06, (byte) 0x0a); + // This should only be called once per sync - also if we start downloading more sleep data + GB.signalActivityDataFinish(); + // Unsetting busy is done at the end of all downloads + } // "sleep_data.bin" later as well + } + + /** + * Called when there are no more files left to download + */ + public void downloadQueueEmpty() { + if (gbDevice.isBusy()) { + gbDevice.unsetBusyTask(); + gbDevice.sendDeviceUpdateIntent(context); + } + } + + public void downloadException(HuaweiFileDownloadManager.HuaweiFileDownloadException e) { + GB.toast("Error downloading file", Toast.LENGTH_SHORT, GB.ERROR, e); + if (e.fileRequest != null) + LOG.error("Error downloading file: {}{}", e.fileRequest.filename, e.fileRequest.newSync ? " (newsync)" : "", e); + else + LOG.error("Error in file download", e); + + // We also reset the sync state, just to get back to working as nicely as possible + handleSyncFinished(); + } + + public void onTestNewFunction() { + // Show to user + gbDevice.setBusyTask("Downloading file..."); + gbDevice.sendDeviceUpdateIntent(getContext()); + + huaweiFileDownloadManager.downloadSleep( + getHuaweiCoordinator().getSupportsTruSleepNewSync(), + "sleep_state.bin", + 0, + (int) (System.currentTimeMillis() / 1000) + ); + huaweiFileDownloadManager.downloadSleep( + getHuaweiCoordinator().getSupportsTruSleepNewSync(), + "sleep_data.bin", + 0, + (int) (System.currentTimeMillis() / 1000) + ); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/ResponseManager.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/ResponseManager.java index 18f03c29b..618b5e7ea 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/ResponseManager.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/ResponseManager.java @@ -62,6 +62,16 @@ public class ResponseManager { } } + /** + * Remove all requests with specified class from the response handler list + * @param handlerClass The class of which the requests are removed + */ + public void removeHandler(Class handlerClass) { + synchronized (handlers) { + handlers.removeIf(request -> request.getClass() == handlerClass); + } + } + /** * Parses the data into a Huawei Packet. * If the packet is complete, it will be handled by the first request that accepts it, @@ -102,8 +112,10 @@ public class ResponseManager { } else { LOG.debug("Service: " + Integer.toHexString(receivedPacket.serviceId & 0xff) + ", command: " + Integer.toHexString(receivedPacket.commandId & 0xff) + ", handled by: " + handler.getClass()); - synchronized (handlers) { - handlers.remove(handler); + if (handler.autoRemoveFromResponseHandler()) { + synchronized (handlers) { + handlers.remove(handler); + } } handler.handleResponse(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetFileBlockRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetFileBlockRequest.java new file mode 100644 index 000000000..fcc4389bb --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetFileBlockRequest.java @@ -0,0 +1,69 @@ +/* Copyright (C) 2024 Martin.JM + + 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.huawei.requests; + +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileDownloadService0A; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileDownloadService2C; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiFileDownloadManager; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider; + +public class GetFileBlockRequest extends Request { + private final HuaweiFileDownloadManager.FileRequest request; + + public GetFileBlockRequest(HuaweiSupportProvider support, HuaweiFileDownloadManager.FileRequest request) { + super(support); + if (request.newSync) { + this.serviceId = FileDownloadService2C.id; + this.commandId = FileDownloadService2C.RequestBlock.id; + } else { + this.serviceId = FileDownloadService0A.id; + this.commandId = FileDownloadService0A.RequestBlock.id; + } + this.request = request; + } + + @Override + protected List createRequest() throws Request.RequestCreationException { + try { + if (this.request.newSync) + return new FileDownloadService2C.RequestBlock( + paramsProvider, + this.request.fileId, + this.request.buffer.position(), + this.request.currentBlockSize, + this.request.noEncrypt + ).serialize(); + else + return new FileDownloadService0A.RequestBlock.Request( + paramsProvider, + this.request.filename, + this.request.buffer.position(), + this.request.currentBlockSize + ).serialize(); + } catch (HuaweiPacket.CryptoException e) { + throw new Request.RequestCreationException(e); + } + } + + @Override + protected void processResponse() throws Request.ResponseParseException { + // TODO: handle data? + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetFileDownloadCompleteRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetFileDownloadCompleteRequest.java new file mode 100644 index 000000000..425c14e70 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetFileDownloadCompleteRequest.java @@ -0,0 +1,54 @@ +/* Copyright (C) 2024 Martin.JM + + 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.huawei.requests; + +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileDownloadService0A; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileDownloadService2C; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiFileDownloadManager; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider; + +public class GetFileDownloadCompleteRequest extends Request { + + private final HuaweiFileDownloadManager.FileRequest request; + + public GetFileDownloadCompleteRequest(HuaweiSupportProvider support, HuaweiFileDownloadManager.FileRequest request) { + super(support); + if (request.newSync) { + this.serviceId = FileDownloadService2C.id; + this.commandId = FileDownloadService2C.FileDownloadCompleteRequest.id; + } else { + this.serviceId = FileDownloadService0A.id; + this.commandId = FileDownloadService0A.FileDownloadCompleteRequest.id; + } + this.request = request; + } + + @Override + protected List createRequest() throws RequestCreationException { + try { + if (request.newSync) + return new FileDownloadService2C.FileDownloadCompleteRequest(paramsProvider, this.request.fileId).serialize(); + else + return new FileDownloadService0A.FileDownloadCompleteRequest(paramsProvider).serialize(); + } catch (HuaweiPacket.CryptoException e) { + throw new RequestCreationException(e); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetFileDownloadInitRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetFileDownloadInitRequest.java new file mode 100644 index 000000000..9009acf5d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetFileDownloadInitRequest.java @@ -0,0 +1,112 @@ +/* Copyright (C) 2024 Martin.JM + + 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.huawei.requests; + +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileDownloadService0A; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileDownloadService2C; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiFileDownloadManager; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider; + +public class GetFileDownloadInitRequest extends Request { + + final HuaweiFileDownloadManager.FileRequest request; + + public boolean newSync; + + // Old sync + public String[] filenames; + + // New sync + public String filename; + public HuaweiFileDownloadManager.FileType fileType; + public byte fileId; + public int fileSize; + + public GetFileDownloadInitRequest(HuaweiSupportProvider support, HuaweiFileDownloadManager.FileRequest request) { + super(support); + if (request.newSync) { + this.serviceId = FileDownloadService2C.id; + this.commandId = FileDownloadService2C.FileDownloadInit.id; + } else { + this.serviceId = FileDownloadService0A.id; + this.commandId = FileDownloadService0A.FileDownloadInit.id; + } + this.request = request; + } + + private FileDownloadService2C.FileType convertFileTypeTo2C(HuaweiFileDownloadManager.FileType type) { + switch (type) { + case SLEEP: + return FileDownloadService2C.FileType.SLEEP; + default: + return FileDownloadService2C.FileType.UNKNOWN; + } + } + + private HuaweiFileDownloadManager.FileType convertFileTypeFrom2C(FileDownloadService2C.FileType type) { + switch (type) { + case SLEEP: + return HuaweiFileDownloadManager.FileType.SLEEP; + default: + return HuaweiFileDownloadManager.FileType.UNKNOWN; + } + } + + @Override + protected List createRequest() throws RequestCreationException { + try { + if (this.request.newSync) { + FileDownloadService2C.FileType type = convertFileTypeTo2C(request.fileType); + if (type == FileDownloadService2C.FileType.UNKNOWN) + throw new RequestCreationException("Cannot convert type " + request.fileType); + return new FileDownloadService2C.FileDownloadInit.Request(paramsProvider, request.filename, type, request.startTime, request.endTime).serialize(); + } else { + if (this.request.fileType == HuaweiFileDownloadManager.FileType.DEBUG) + return new FileDownloadService0A.FileDownloadInit.DebugFilesRequest(paramsProvider).serialize(); + else if (this.request.fileType == HuaweiFileDownloadManager.FileType.SLEEP) + return new FileDownloadService0A.FileDownloadInit.SleepFilesRequest(paramsProvider, request.startTime, request.endTime).serialize(); + else + throw new RequestCreationException("Unknown file type"); + } + } catch (HuaweiPacket.CryptoException e) { + throw new RequestCreationException(e); + } + } + + @Override + protected void processResponse() throws ResponseParseException { + // In case of multiple downloads, the response here does not need to match the original request + // So we cannot use the request at all! + + if (this.receivedPacket instanceof FileDownloadService0A.FileDownloadInit.Response) { + this.newSync = false; + this.filenames = ((FileDownloadService0A.FileDownloadInit.Response) this.receivedPacket).fileNames; + } else if (this.receivedPacket instanceof FileDownloadService2C.FileDownloadInit.Response) { + this.newSync = true; + FileDownloadService2C.FileDownloadInit.Response packet = (FileDownloadService2C.FileDownloadInit.Response) this.receivedPacket; + this.filename = packet.fileName; + this.fileType = convertFileTypeFrom2C(packet.fileType); + this.fileId = packet.fileId; + this.fileSize = packet.fileSize; + } else { + throw new ResponseTypeMismatchException(receivedPacket, FileDownloadService2C.FileDownloadInit.Response.class); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetFileInfoRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetFileInfoRequest.java new file mode 100644 index 000000000..92965a356 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetFileInfoRequest.java @@ -0,0 +1,81 @@ +/* Copyright (C) 2024 Martin.JM + + 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.huawei.requests; + +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileDownloadService0A; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileDownloadService2C; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiFileDownloadManager; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider; + +public class GetFileInfoRequest extends Request { + private final HuaweiFileDownloadManager.FileRequest request; + + public boolean newSync; + + // Old sync + public int fileLength; + + // New sync + public byte fileId; + public byte timeout; + public int maxBlockSize; + public boolean noEncrypt; + + public GetFileInfoRequest(HuaweiSupportProvider support, HuaweiFileDownloadManager.FileRequest request) { + super(support); + if (request.newSync) { + this.serviceId = FileDownloadService2C.id; + this.commandId = FileDownloadService2C.FileInfo.id; + } else { + this.serviceId = FileDownloadService0A.id; + this.commandId = FileDownloadService0A.FileInfo.id; + } + this.request = request; + } + + @Override + protected List createRequest() throws RequestCreationException { + try { + if (this.request.newSync) + return new FileDownloadService2C.FileInfo.Request(paramsProvider, this.request.fileId).serialize(); + else + return new FileDownloadService0A.FileInfo.Request(paramsProvider, this.request.filename).serialize(); + } catch (HuaweiPacket.CryptoException e) { + throw new RequestCreationException(e); + } + } + + @Override + protected void processResponse() throws ResponseParseException { + if (this.receivedPacket instanceof FileDownloadService0A.FileInfo.Response) { + this.newSync = false; + this.fileLength = ((FileDownloadService0A.FileInfo.Response) this.receivedPacket).fileLength; + } else if (this.receivedPacket instanceof FileDownloadService2C.FileInfo.Response) { + this.newSync = true; + FileDownloadService2C.FileInfo.Response packet = (FileDownloadService2C.FileInfo.Response) this.receivedPacket; + this.fileId = packet.fileId; + this.timeout = packet.timeout; + this.maxBlockSize = packet.maxBlockSize; + this.noEncrypt = packet.noEncrypt; + } else { + throw new ResponseTypeMismatchException(this.receivedPacket, FileDownloadService2C.FileInfo.Response.class); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetFileParametersRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetFileParametersRequest.java new file mode 100644 index 000000000..8002aeb6f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetFileParametersRequest.java @@ -0,0 +1,60 @@ +/* Copyright (C) 2024 Martin.JM + + 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.huawei.requests; + +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileDownloadService0A; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider; + +public class GetFileParametersRequest extends Request { + + private int maxBlockSize; + private int timeout; + + public GetFileParametersRequest(HuaweiSupportProvider support) { + super(support); + this.serviceId = FileDownloadService0A.id; + this.commandId = FileDownloadService0A.FileParameters.id; + } + + @Override + protected List createRequest() throws RequestCreationException { + try { + return new FileDownloadService0A.FileParameters.Request(paramsProvider).serialize(); + } catch (HuaweiPacket.CryptoException e) { + throw new RequestCreationException(e); + } + } + + @Override + protected void processResponse() throws ResponseParseException { + if (!(this.receivedPacket instanceof FileDownloadService0A.FileParameters.Response)) + throw new ResponseTypeMismatchException(this.receivedPacket, FileDownloadService0A.FileParameters.Response.class); + this.maxBlockSize = ((FileDownloadService0A.FileParameters.Response) this.receivedPacket).maxBlockSize; + this.timeout = ((FileDownloadService0A.FileParameters.Response) this.receivedPacket).timeout; + } + + public int getMaxBlockSize() { + return maxBlockSize; + } + + public int getTimeout() { + return timeout; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSettingRelatedRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSettingRelatedRequest.java index 440ac0911..5fbc716a1 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSettingRelatedRequest.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSettingRelatedRequest.java @@ -46,6 +46,11 @@ public class GetSettingRelatedRequest extends Request { @Override protected void processResponse() throws ResponseParseException { + if (!(receivedPacket instanceof DeviceConfig.SettingRelated.Response)) + throw new ResponseTypeMismatchException(receivedPacket, DeviceConfig.SettingRelated.Response.class); + LOG.debug("handle Setting Related"); + + supportProvider.getHuaweiCoordinator().setSupportsTruSleepNewSync(((DeviceConfig.SettingRelated.Response) receivedPacket).truSleepNewSync); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSleepDataRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSleepDataRequest.java index 345b142ca..0779a566c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSleepDataRequest.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSleepDataRequest.java @@ -85,7 +85,7 @@ public class GetSleepDataRequest extends Request { (timestampInts[5]); short duration = (short) (durationInt * 60); - this.supportProvider.addSleepActivity(timestamp, duration, subContainer.type); + this.supportProvider.addSleepActivity(timestamp, timestamp + duration, subContainer.type, FitnessData.MessageData.sleepId); } if (count + 1 < maxCount) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/Request.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/Request.java index cb418abbe..c341b8e5a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/Request.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/Request.java @@ -79,6 +79,10 @@ public class Request { public ResponseTypeMismatchException(HuaweiPacket a, Class b) { super("Response type mismatch, packet is of type " + a.getClass() + " but expected " + b); } + + public ResponseTypeMismatchException(HuaweiPacket a, Class b, Class c) { + super("Response type mismatch, packet is of type " + a.getClass() + " but expected " + b + " or " + c); + } } public static class WorkoutParseException extends ResponseParseException { @@ -111,10 +115,13 @@ public class Request { public RequestCallback(HuaweiSupportProvider supportProvider) { support = supportProvider; } - public void call() {}; + public void call() {} + public void call(Request request) { + call(); // To keep everything working as it was as well + } public void handleException(ResponseParseException e) { LOG.error("Callback request exception", e); - }; + } } public Request(HuaweiSupportProvider supportProvider, nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder builder) { @@ -213,7 +220,7 @@ public class Request { if (nextRequest == null || stopChain) { operationStatus = OperationStatus.FINISHED; if (finalizeReq != null) { - finalizeReq.call(); + finalizeReq.call(this); } } } @@ -300,4 +307,8 @@ public class Request { this.supportProvider.performConnected(transaction); } } + + public boolean autoRemoveFromResponseHandler() { + return true; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetTruSleepRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetTruSleepRequest.java index 2cb5ad92b..e46f18acb 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetTruSleepRequest.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SetTruSleepRequest.java @@ -39,8 +39,8 @@ public class SetTruSleepRequest extends Request { @Override protected List createRequest() throws RequestCreationException { boolean truSleepSwitch = GBApplication - .getDeviceSpecificSharedPrefs(supportProvider.getDevice().getAddress()) - .getBoolean(HuaweiConstants.PREF_HUAWEI_TRUSLEEP, false); + .getDeviceSpecificSharedPrefs(this.getDevice().getAddress()) + .getBoolean(HuaweiConstants.PREF_HUAWEI_TRUSLEEP, false); try { return new FitnessData.TruSleep.Request(paramsProvider, truSleepSwitch).serialize(); } catch (HuaweiPacket.CryptoException e) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1d7c06c12..bfdd7a4d9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2468,7 +2468,7 @@ HUAWEI TruSleep ™ Monitor your sleep quality and breathing pattern in real time.\nAnalyze your sleep patterns and accurately diagnose 6 types of sleeping problems. Improved sleep monitoring - Warning: enabling this will stop sleep from showing up in Gadgetbridge! Click here if you accept this. + Warning: enabling this will make all sleep show up as light sleep in Gadgetbridge! Click here if you accept this. Activity recognition settings recognize running recognize biking diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/TestHuaweiTruSleepParser.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/TestHuaweiTruSleepParser.java new file mode 100644 index 000000000..5aafe650b --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/TestHuaweiTruSleepParser.java @@ -0,0 +1,39 @@ +/* Copyright (C) 2024 Martin.JM + + 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.devices.huawei; + +import org.junit.Assert; +import org.junit.Test; + +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class TestHuaweiTruSleepParser { + + @Test + public void testParseState() { + byte[] test = GB.hexStringToByteArray("0100000002000000000000000000000003000000050000000000000000000000"); + + HuaweiTruSleepParser.TruSleepStatus[] expected = new HuaweiTruSleepParser.TruSleepStatus[] { + new HuaweiTruSleepParser.TruSleepStatus(1, 2), + new HuaweiTruSleepParser.TruSleepStatus(3, 5) + }; + + HuaweiTruSleepParser.TruSleepStatus[] result = HuaweiTruSleepParser.parseState(test); + + Assert.assertArrayEquals(expected, result); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/TestDebugRequestParser.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/TestDebugRequestParser.java index d3e0f53e8..853acc196 100644 --- a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/TestDebugRequestParser.java +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/TestDebugRequestParser.java @@ -140,11 +140,6 @@ public class TestDebugRequestParser { } - @Override - public void addSleepActivity(int timestamp, short duration, byte type) { - - } - @Override public void addStepData(int timestamp, short steps, short calories, short distance, byte spo, byte heartrate) { diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/TestResponseManager.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/TestResponseManager.java index 745e04697..b26a7bdbe 100644 --- a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/TestResponseManager.java +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/TestResponseManager.java @@ -36,6 +36,7 @@ import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; @@ -44,6 +45,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.service.btbr.Transaction; import nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetEventAlarmList; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.Request; @RunWith(MockitoJUnitRunner.class) @@ -151,11 +153,6 @@ public class TestResponseManager { } - @Override - public void addSleepActivity(int timestamp, short duration, byte type) { - - } - @Override public void addStepData(int timestamp, short steps, short calories, short distance, byte spo, byte heartrate) { @@ -233,6 +230,30 @@ public class TestResponseManager { Assert.assertEquals(expectedHandlers, handlersField.get(responseManager)); } + @Test + public void testRemoveHandlerClass() throws IllegalAccessException { + Request input1 = new GetEventAlarmList(supportProvider); + Request input2 = new GetEventAlarmList(supportProvider); + Request extra = new Request(supportProvider); + + List inputHandlers = Collections.synchronizedList(new ArrayList()); + inputHandlers.add(extra); + inputHandlers.add(input1); + inputHandlers.add(extra); + inputHandlers.add(input2); + + List expectedHandlers = Collections.synchronizedList(new ArrayList()); + expectedHandlers.add(extra); + expectedHandlers.add(extra); + + ResponseManager responseManager = new ResponseManager(supportProvider); + handlersField.set(responseManager, inputHandlers); + + responseManager.removeHandler(GetEventAlarmList.class); + + Assert.assertEquals(expectedHandlers, handlersField.get(responseManager)); + } + @Test public void testHandleDataCompletePacketSynchronous() throws Exception { // Note that this is not a proper packet, but that doesn't matter as we're not testing @@ -249,10 +270,9 @@ public class TestResponseManager { Request request1 = Mockito.mock(Request.class); when(request1.handleResponse((HuaweiPacket) any())) .thenReturn(true); + when(request1.autoRemoveFromResponseHandler()) + .thenReturn(true); Request request2 = Mockito.mock(Request.class); - // FIXME: Removed due to UnnecessaryStubbingException after mockito-core update - //when(request2.handleResponse((HuaweiPacket) any())) - // .thenReturn(false); List inputHandlers = Collections.synchronizedList(new ArrayList()); inputHandlers.add(request1); @@ -342,10 +362,9 @@ public class TestResponseManager { Request request1 = Mockito.mock(Request.class); when(request1.handleResponse((HuaweiPacket) any())) .thenReturn(true); + when(request1.autoRemoveFromResponseHandler()) + .thenReturn(true); Request request2 = Mockito.mock(Request.class); - // FIXME: Removed due to UnnecessaryStubbingException after mockito-core update - //when(request2.handleResponse((HuaweiPacket) any())) - // .thenReturn(false); List inputHandlers = Collections.synchronizedList(new ArrayList()); inputHandlers.add(request1);