1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-12-26 02:25:50 +01:00

Huawei: Simple TruSleep support

Only supports start and end time of sleep periods.
This commit is contained in:
Martin.JM 2024-07-14 23:11:12 +02:00
parent fbe727644e
commit 6f83fc815f
26 changed files with 1752 additions and 67 deletions

View File

@ -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;
}
}

View File

@ -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;

View File

@ -55,6 +55,7 @@ public class HuaweiSampleProvider extends AbstractSampleProvider<HuaweiActivityS
* The timestamp of the other marker, if it's larger this is the begin, otherwise the end
* - `source`
* The source of the data, which Huawei Band message the data came from
* 0x0d for sleep data from activity, 0x0a for TruSleep data
*/
private static class RawTypes {
@ -155,7 +156,16 @@ public class HuaweiSampleProvider extends AbstractSampleProvider<HuaweiActivityS
Property sourceProperty = HuaweiActivitySampleDao.Properties.Source;
Property activityTypeProperty = HuaweiActivitySampleDao.Properties.RawKind;
qb.where(sourceProperty.eq(0x0d), activityTypeProperty.eq(0x01));
qb.where(
qb.or(
sourceProperty.eq(0x0d),
sourceProperty.eq(0x0a)
),
qb.or(
activityTypeProperty.eq(RawTypes.LIGHT_SLEEP),
activityTypeProperty.eq(RawTypes.DEEP_SLEEP)
)
);
return getLastFetchTimestamp(qb);
}
@ -365,7 +375,7 @@ public class HuaweiSampleProvider extends AbstractSampleProvider<HuaweiActivityS
private List<HuaweiActivitySample> interpolate(List<HuaweiActivitySample> processedSamples) {
List<HuaweiActivitySample> 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<HuaweiActivityS
if (sample.getTimestamp() > 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<HuaweiActivityS
processedSamples.add(sample);
}
if (sample.getSource() == FitnessData.MessageData.sleepId && (sample.getRawKind() == RawTypes.LIGHT_SLEEP || sample.getRawKind() == RawTypes.DEEP_SLEEP)) {
if (
(sample.getSource() == FitnessData.MessageData.sleepId || sample.getSource() == 0x0a) // Sleep sources
&& (sample.getRawKind() == RawTypes.LIGHT_SLEEP || sample.getRawKind() == RawTypes.DEEP_SLEEP) // Sleep types
) {
if (isStartMarker)
state.sleepModifier = sample.getRawKind();
else

View File

@ -307,13 +307,17 @@ public class HuaweiTLV {
.put(CryptoTags.cipherText, encryptedTLV);
}
public void decrypt(ParamsProvider paramsProvider) throws CryptoException, HuaweiPacket.MissingTagException {
public byte[] decryptRaw(ParamsProvider paramsProvider) throws CryptoException, HuaweiPacket.MissingTagException {
byte[] key = paramsProvider.getSecretKey();
byte[] decryptedTLV = HuaweiCrypto.decrypt(
return HuaweiCrypto.decrypt(
paramsProvider.getEncryptMethod() == 0x01 || paramsProvider.getDeviceSupportType() == 0x04,
getBytes(CryptoTags.cipherText),
key,
getBytes(CryptoTags.initVector));
}
public void decrypt(ParamsProvider paramsProvider) throws CryptoException, HuaweiPacket.MissingTagException {
byte[] decryptedTLV = decryptRaw(paramsProvider);
this.valueMap = new ArrayList<>();
parse(decryptedTLV);
}

View File

@ -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 <https://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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
}

View File

@ -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 <https://www.gnu.org/licenses/>. */
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;
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>. */
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;
}
}
}

View File

@ -175,4 +175,8 @@ public class HuaweiBRSupport extends AbstractBTBRDeviceSupport {
supportProvider.dispose();
super.dispose();
}
public void onTestNewFunction() {
supportProvider.onTestNewFunction();
}
}

View File

@ -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 <https://www.gnu.org/licenses/>. */
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<FileRequest> 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);
}
}

View File

@ -183,4 +183,8 @@ public class HuaweiLESupport extends AbstractBTLEDeviceSupport {
supportProvider.dispose();
super.dispose();
}
public void onTestNewFunction() {
supportProvider.onTestNewFunction();
}
}

View File

@ -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)
);
}
}

View File

@ -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();

View File

@ -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 <https://www.gnu.org/licenses/>. */
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<byte[]> 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?
}
}

View File

@ -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 <https://www.gnu.org/licenses/>. */
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<byte[]> 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);
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>. */
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<byte[]> 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);
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>. */
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<byte[]> 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);
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>. */
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<byte[]> 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;
}
}

View File

@ -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);
}
}

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -39,8 +39,8 @@ public class SetTruSleepRequest extends Request {
@Override
protected List<byte[]> 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) {

View File

@ -2468,7 +2468,7 @@
<string name="huawei_trusleep_title">HUAWEI TruSleep &#8482;</string>
<string name="huawei_trusleep_summary">Monitor your sleep quality and breathing pattern in real time.\nAnalyze your sleep patterns and accurately diagnose 6 types of sleeping problems.</string>
<string name="huawei_trusleep_summary_light">Improved sleep monitoring</string>
<string name="huawei_trusleep_warning">Warning: enabling this will stop sleep from showing up in Gadgetbridge! Click here if you accept this.</string>
<string name="huawei_trusleep_warning">Warning: enabling this will make all sleep show up as light sleep in Gadgetbridge! Click here if you accept this.</string>
<string name="prefs_activity_recognition">Activity recognition settings</string>
<string name="pref_activity_recognize_running">recognize running</string>
<string name="pref_activity_recognize_biking">recognize biking</string>

View File

@ -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 <https://www.gnu.org/licenses/>. */
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);
}
}

View File

@ -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) {

View File

@ -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<Request> inputHandlers = Collections.synchronizedList(new ArrayList<Request>());
inputHandlers.add(extra);
inputHandlers.add(input1);
inputHandlers.add(extra);
inputHandlers.add(input2);
List<Request> expectedHandlers = Collections.synchronizedList(new ArrayList<Request>());
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<Request> inputHandlers = Collections.synchronizedList(new ArrayList<Request>());
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<Request> inputHandlers = Collections.synchronizedList(new ArrayList<Request>());
inputHandlers.add(request1);