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 663b9d7a2..5f0eb41b9 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 @@ -649,6 +649,9 @@ public class HuaweiCoordinator { return supportsCommandForService(0x09, 0x0f); } + public boolean supportDefaultSwitch() { + return supportsCommandForService(0x01, 0x21); + } public boolean supportsExternalCalendarService() { if (supportsExpandCapability()) @@ -780,7 +783,13 @@ public class HuaweiCoordinator { return supportsExpandCapability(147); return false; } - + + public boolean supportsReverseCapabilities() { + if (supportsExpandCapability()) + return supportsExpandCapability(182); + return false; + } + public boolean supportsPromptPushMessage () { // do not ask for capabilities under specific condition // if (deviceType == 10 && deviceVersion == 73617766697368 && deviceSoftVersion == 372E312E31) -> leo device 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 263a9f571..03f1c6d95 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 @@ -19,6 +19,8 @@ package nodomain.freeyourgadget.gadgetbridge.devices.huawei; import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants.HUAWEI_MAGIC; +import androidx.annotation.NonNull; + import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; @@ -455,6 +457,8 @@ public class HuaweiPacket { return new DeviceConfig.DeviceStatus.Response(paramsProvider).fromPacket(this); case DeviceConfig.DndLiftWristType.id: return new DeviceConfig.DndLiftWristType.Response(paramsProvider).fromPacket(this); + case DeviceConfig.GetDefaultSwitch.id: + return new DeviceConfig.GetDefaultSwitch.Response(paramsProvider).fromPacket(this); case DeviceConfig.HiChain.id: return new DeviceConfig.HiChain.Response(paramsProvider).fromPacket(this); case DeviceConfig.PinCode.id: @@ -471,6 +475,8 @@ public class HuaweiPacket { return new DeviceConfig.SecurityNegotiation.Response(paramsProvider).fromPacket(this); case DeviceConfig.WearStatus.id: return new DeviceConfig.WearStatus.Response(paramsProvider).fromPacket(this); + case DeviceConfig.ReverseCapabilities.id: + return new DeviceConfig.ReverseCapabilities.Response(paramsProvider).fromPacket(this); // Camera remote has same ID as DeviceConfig case CameraRemote.CameraRemoteStatus.id: @@ -677,10 +683,14 @@ public class HuaweiPacket { switch (this.commandId) { case FileDownloadService2C.FileDownloadInit.id: return new FileDownloadService2C.FileDownloadInit.Response(paramsProvider).fromPacket(this); + case FileDownloadService2C.FileRequestHash.id: + return new FileDownloadService2C.FileRequestHash.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); + case FileDownloadService2C.IncomingInitRequest.id: + return new FileDownloadService2C.IncomingInitRequest.Response(paramsProvider).fromPacket(this); default: this.isEncrypted = this.attemptDecrypt(); // Helps with debugging return this; @@ -1090,6 +1100,7 @@ public class HuaweiPacket { return Objects.equals(tlv, that.tlv); } + @NonNull @Override public String toString() { return "HuaweiPacket{" + 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 536e4216c..da53ff07d 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 @@ -96,7 +96,7 @@ public class DeviceConfig { this.interval = this.tlv.getShort(0x04); System.arraycopy(this.tlv.getBytes(0x05), 2, this.serverNonce, 0, 16); - this.authVersion = (byte)this.tlv.getBytes(0x05)[1]; + this.authVersion = this.tlv.getBytes(0x05)[1]; if (this.tlv.contains(0x07)) this.deviceSupportType = this.tlv.getByte(0x07); @@ -116,15 +116,17 @@ public class DeviceConfig { public static class SupportedServices { public static final byte id = 0x02; + // device should always support service 0x01 // notDeviceCapabilities = 0x1C, 0x1E, 0x1F, 0x28, 0x29, 0x2C, 0x2F, 0x31 // but services = 0x1E, 0x28, 0x2C, 0x31 // service 0x21 depends on MiddleWear support + public static final byte[] knownSupportedServices = new byte[] { - 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, - 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, - 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1D, 0x20, - 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x2A, 0x2B, 0x2D, 0x2E, - 0x30, 0x32, 0x33, 0x34, 0x35 + 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, + 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, + 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1D, 0x20, + 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x2A, 0x2B, 0x2D, 0x2E, + 0x30, 0x32, 0x33, 0x34, 0x35 }; public static class Request extends HuaweiPacket { @@ -803,6 +805,15 @@ public class DeviceConfig { switch (b) { case 0x0: break; + case 0x2: + this.tlv.put(b); // Force phone manufactures to "" + break; + case 0x4: + this.tlv.put(b); // Force phone model to "" + break; + case 0x8: + this.tlv.put(b, "14"); // Force android version to "14" + break; case 0xf: break; case 0x11: @@ -964,6 +975,35 @@ public class DeviceConfig { // TODO: set (earphone) double tap action 0x1f // TODO: get (earphone) double tap action 0x20 + public static class GetDefaultSwitch { + public static final int id = 0x21; + + public static class Request extends HuaweiPacket { + public Request(HuaweiPacket.ParamsProvider paramsProvider) { + super(paramsProvider); + + this.serviceId = DeviceConfig.id; + this.commandId = id; + + this.tlv = new HuaweiTLV() + .put(0x01); + this.complete = true; + } + } + + public static class Response extends HuaweiPacket { + + public Response(ParamsProvider paramsProvider) { + super(paramsProvider); + } + + @Override + public void parseTlv() throws ParseException { + + } + } + } + public static class HiChain { public static final int id = 0x28; @@ -1467,28 +1507,27 @@ public class DeviceConfig { this.serviceId = DeviceConfig.id; this.commandId = id; - int timestamp = (int) (System.currentTimeMillis() / 1000); + long timestamp = System.currentTimeMillis(); HuaweiTLV software = new HuaweiTLV() .put(0x03, "software_update_service_statement") - .put(0x04, 0x01) + .put(0x04, (byte)0x01) .put(0x05, "20230508-20230508-0-0") - .put(0x06, timestamp); + .put(0x06, String.valueOf(timestamp)); HuaweiTLV device_information = new HuaweiTLV() .put(0x03, "device_information_management") - .put(0x04,0x01) + .put(0x04,(byte)0x01) .put(0x05, "20230508-20230508-0-0") - .put(0x06,timestamp); - + .put(0x06,String.valueOf(timestamp)); HuaweiTLV user_license = new HuaweiTLV() .put(0x03, "user_license_agreement") - .put(0x04,0x01) + .put(0x04,(byte)0x01) .put(0x05, "20230508-20230508-0-0") - .put(0x06,timestamp); + .put(0x06,String.valueOf(timestamp)); HuaweiTLV tlvList = new HuaweiTLV() .put(0x82, software) - .put(0x82,device_information) - .put(0x82,user_license); + .put(0x82, device_information) + .put(0x82, user_license); this.tlv = new HuaweiTLV() .put(0x81, tlvList); } @@ -1776,6 +1815,39 @@ public class DeviceConfig { } } + public static class ReverseCapabilities { + public static final int id = 0x3f; + + public static class Request extends HuaweiPacket { + public Request(ParamsProvider paramsProvider) { + super(paramsProvider); + + this.serviceId = DeviceConfig.id; + this.commandId = id; + + // Bits like ext capabilities + byte[] capabilities = {(byte) 0xFD, 0x17}; + this.tlv = new HuaweiTLV() + .put(0x01, capabilities); + + this.complete = true; + } + } + + public static class Response extends HuaweiPacket { + + public Response(ParamsProvider paramsProvider) { + super(paramsProvider); + this.serviceId = DeviceConfig.id; + this.commandId = id; + } + + @Override + public void parseTlv() throws ParseException { + } + } + } + // TODO: wear location enum? public static class Date { 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 index bbbea5332..940e22b90 100644 --- 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 @@ -104,6 +104,40 @@ public class FileDownloadService2C { } } + public static class FileRequestHash { + public static final int id = 0x02; + + 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, (byte) 0x01); + + this.complete = true; + } + } + + public static class Response extends HuaweiPacket { + public byte fileId; + public byte[] fileHash; + + public Response(ParamsProvider paramsProvider) { + super(paramsProvider); + } + + @Override + public void parseTlv() throws ParseException { + fileId = this.tlv.getByte(0x01); + fileHash = this.tlv.getBytes(0x03); + } + } + } + public static class FileInfo { public static final int id = 0x03; @@ -207,7 +241,7 @@ public class FileDownloadService2C { public static class FileDownloadCompleteRequest extends HuaweiPacket { public static final int id = 0x06; - public FileDownloadCompleteRequest(ParamsProvider paramsProvider, byte fileId) { + public FileDownloadCompleteRequest(ParamsProvider paramsProvider, byte fileId, byte status) { super(paramsProvider); this.serviceId = FileDownloadService2C.id; @@ -215,9 +249,72 @@ public class FileDownloadService2C { this.tlv = new HuaweiTLV() .put(0x01, fileId) - .put(0x02, (byte) 1); + .put(0x02, status); this.complete = true; } } + + public static class IncomingInitRequest { + public static final int id = 0x07; + + public static class Request extends HuaweiPacket { + public Request(ParamsProvider paramsProvider, String filename, byte fileType, byte fileId, int fileSize, String srcPackage, String dstPackage, String srcFingerprint, String dstFingerprint, byte status) { + super(paramsProvider); + + this.serviceId = FileDownloadService2C.id; + this.commandId = id; + + this.tlv = new HuaweiTLV() + .put(0x01, filename) + .put(0x02, fileType) + .put(0x03, fileId) + .put(0x04, fileSize); + + if (srcPackage != null && dstPackage != null) { + this.tlv.put(0x08, srcPackage) + .put(0x09, dstPackage) + .put(0x0a, srcFingerprint) + .put(0x0b, dstFingerprint); + } + + this.tlv.put(0x0d, status); + + this.complete = true; + } + } + + public static class Response extends HuaweiPacket { + public String filename; + public byte fileType; + public byte fileId; + public int fileSize; + public String description; + public String srcPackage; + public String dstPackage; + public String srcFingerprint = null; + public String dstFingerprint = null; + + public Response(ParamsProvider paramsProvider) { + super(paramsProvider); + } + + @Override + public void parseTlv() throws ParseException { + filename = this.tlv.getString(0x01); + fileType = this.tlv.getByte(0x02); + fileId = this.tlv.getByte(0x03); + fileSize = this.tlv.getAsInteger(0x4); + + description = this.tlv.getString(0x07); + srcPackage = this.tlv.getString(0x08); + dstPackage = this.tlv.getString(0x09); + if (this.tlv.contains(0x0a)) + srcFingerprint = this.tlv.getString(0x0a); + if (this.tlv.contains(0x0b)) + dstFingerprint = this.tlv.getString(0x0b); + + } + } + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/AsynchronousResponse.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/AsynchronousResponse.java index 620ba03eb..0d6ca437a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/AsynchronousResponse.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/AsynchronousResponse.java @@ -53,6 +53,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DataSync; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Ephemeris; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.EphemerisFileUpload; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileDownloadService2C; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FindPhone; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.GpsAndTime; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Menstrual; @@ -135,6 +136,7 @@ public class AsynchronousResponse { handlePermissionCheck(response); handleDataSyncCommands(response); handleOTA(response); + handleFileDownload(response); } catch (Request.ResponseParseException e) { LOG.error("Response parse exception", e); @@ -831,4 +833,16 @@ public class AsynchronousResponse { support.getHuaweiOTAManager().handleDeviceError(((OTA.DeviceError.Response) response).errorCode); } } + + void handleFileDownload(HuaweiPacket response) throws Request.ResponseTypeMismatchException { + if (response.serviceId != FileDownloadService2C.id) + return; + if (response.commandId == FileDownloadService2C.IncomingInitRequest.id) { + if (!(response instanceof FileDownloadService2C.IncomingInitRequest.Response)) { + throw new Request.ResponseTypeMismatchException(response, FileDownloadService2C.IncomingInitRequest.class); + } + FileDownloadService2C.IncomingInitRequest.Response resp = (FileDownloadService2C.IncomingInitRequest.Response) response; + support.deviceFileDownloadRequest(resp.filename, resp.fileType, resp.fileId, resp.fileSize, resp.srcPackage, resp.dstPackage, resp.srcFingerprint, resp.dstFingerprint); + } + } } 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 index 8c855ec5d..791f31f1c 100644 --- 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 @@ -28,6 +28,7 @@ import org.slf4j.LoggerFactory; import java.io.IOException; import java.nio.ByteBuffer; +import java.security.MessageDigest; import java.util.ArrayList; import java.util.Arrays; import java.util.Locale; @@ -38,6 +39,8 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileDownloadS 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.GetFileHashRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetFileIncomingAck; 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; @@ -79,6 +82,12 @@ public class HuaweiFileDownloadManager { } } + public static class HuaweiFileDownloadVerifyException extends HuaweiFileDownloadException { + HuaweiFileDownloadVerifyException(@Nullable FileRequest fileRequest) { + super(fileRequest, "Verify failed"); + } + } + 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); @@ -96,9 +105,9 @@ public class HuaweiFileDownloadManager { super( fileRequest, "Data for wrong file received. Expected " + - (newSync ? - "id " + fileRequest.fileId + ", got id " + number : - "packet number " + (fileRequest.lastPacketNumber + 1) + ", got " + number) + (newSync ? + "id " + fileRequest.fileId + ", got id " + number : + "packet number " + (fileRequest.lastPacketNumber + 1) + ", got " + number) ); } } @@ -112,7 +121,8 @@ public class HuaweiFileDownloadManager { } public static class FileDownloadCallback { - public void downloadComplete(FileRequest fileRequest) { } + public void downloadComplete(FileRequest fileRequest) { + } public void downloadException(HuaweiFileDownloadException e) { if (e.fileRequest != null) @@ -129,7 +139,7 @@ public class HuaweiFileDownloadManager { private final FileType fileType; private final boolean newSync; - FileDownloadCallback fileDownloadCallback = null; + private final FileDownloadCallback fileDownloadCallback; // Sleep type only - for 2C GPS they are set to zero private int startTime = 0; @@ -139,6 +149,9 @@ public class HuaweiFileDownloadManager { private short workoutId; private Long databaseId; + + private boolean initFormDevice = false; + private FileRequest(String filename, FileType fileType, boolean newSync, int startTime, int endTime, FileDownloadCallback fileDownloadCallback) { this.filename = filename; this.fileType = fileType; @@ -148,6 +161,10 @@ public class HuaweiFileDownloadManager { this.endTime = endTime; } + public static FileRequest IncomingFileRequest(String filename, FileDownloadCallback fileDownloadCallback) { + return new FileRequest(filename, FileType.UNKNOWN, true, fileDownloadCallback); + } + public static FileRequest sleepStateFileRequest(boolean supportsTruSleepNewSync, int startTime, int endTime, FileDownloadCallback fileDownloadCallback) { return new FileRequest("sleep_state.bin", FileType.SLEEP_STATE, supportsTruSleepNewSync, startTime, endTime, fileDownloadCallback); } @@ -200,6 +217,15 @@ public class HuaweiFileDownloadManager { // New sync only private byte fileId; private boolean noEncrypt; + private boolean needVerify = false; + private byte[] fileHash = null; + + // Incoming P2P request + private byte inFileType; + private String srcPackage = null; + private String dstPackage = null; + private String srcFingerprint = null; + private String dstFingerprint = null; public byte getFileId() { return fileId; @@ -215,7 +241,7 @@ public class HuaweiFileDownloadManager { public byte[] getData() { if (buffer == null) - return new byte[] {}; + return new byte[]{}; return buffer.array(); } @@ -250,6 +276,74 @@ public class HuaweiFileDownloadManager { public boolean isNoEncrypt() { return noEncrypt; } + + public boolean isInitFormDevice() { + return initFormDevice; + } + + public void setInitFormDevice(boolean initFormDevice) { + this.initFormDevice = initFormDevice; + } + + public String getSrcPackage() { + return srcPackage; + } + + public void setSrcPackage(String srcPackage) { + this.srcPackage = srcPackage; + } + + public String getDstPackage() { + return dstPackage; + } + + public void setDstPackage(String dstPackage) { + this.dstPackage = dstPackage; + } + + public String getSrcFingerprint() { + return srcFingerprint; + } + + public void setSrcFingerprint(String srcFingerprint) { + this.srcFingerprint = srcFingerprint; + } + + public String getDstFingerprint() { + return dstFingerprint; + } + + public void setDstFingerprint(String dstFingerprint) { + this.dstFingerprint = dstFingerprint; + } + + public void setFileId(byte fileId) { + this.fileId = fileId; + } + + public int getFileSize() { + return fileSize; + } + + public void setFileSize(int fileSize) { + this.fileSize = fileSize; + } + + public byte getInFileType() { + return inFileType; + } + + public void setInFileType(byte inFileType) { + this.inFileType = inFileType; + } + + public boolean isNeedVerify() { + return needVerify; + } + + public void setNeedVerify(boolean needVerify) { + this.needVerify = needVerify; + } } /** @@ -272,9 +366,9 @@ public class HuaweiFileDownloadManager { public boolean handleResponse(HuaweiPacket response) { if ( (response.serviceId == FileDownloadService0A.id && - response.commandId == FileDownloadService0A.BlockResponse.id) || - (response.serviceId == FileDownloadService2C.id && - response.commandId == FileDownloadService2C.BlockResponse.id) + response.commandId == FileDownloadService0A.BlockResponse.id) || + (response.serviceId == FileDownloadService2C.id && + response.commandId == FileDownloadService2C.BlockResponse.id) ) { receivedPacket = response; return true; @@ -350,6 +444,14 @@ public class HuaweiFileDownloadManager { if (needSync) this.needSync = true; } + if (fileRequest.isInitFormDevice()) { + GetFileIncomingAck getFileIncomingAck = new GetFileIncomingAck(supportProvider, fileRequest, (byte) 0); + try { + getFileIncomingAck.doPerform(); + } catch (IOException e) { + LOG.error("Error execute", e); + } + } startDownload(); } @@ -400,6 +502,11 @@ public class HuaweiFileDownloadManager { this.currentFileRequest = this.fileRequests.remove(0); + if (this.currentFileRequest.isInitFormDevice()) { + getFileHash(); + return; + } + GetFileDownloadInitRequest getFileDownloadInitRequest = new GetFileDownloadInitRequest(supportProvider, currentFileRequest); getFileDownloadInitRequest.setFinalizeReq(new Request.RequestCallback() { @Override @@ -514,6 +621,39 @@ public class HuaweiFileDownloadManager { } } + private void getFileHash() { + if (!currentFileRequest.isNeedVerify() || !currentFileRequest.isNewSync()) { + getFileInfo(); + return; + } + GetFileHashRequest getFileHashRequest = new GetFileHashRequest(supportProvider, currentFileRequest); + getFileHashRequest.setFinalizeReq(new Request.RequestCallback() { + @Override + public void call(Request request) { + GetFileHashRequest r = (GetFileHashRequest) request; + if (currentFileRequest.fileId != r.fileId) { + currentFileRequest.fileDownloadCallback.downloadException(new HuaweiFileDownloadFileMismatchException(currentFileRequest, r.fileId, true)); + reset(); + return; + } + currentFileRequest.fileHash = r.fileHash; + getFileInfo(); + } + + @Override + public void handleException(Request.ResponseParseException e) { + currentFileRequest.fileDownloadCallback.downloadException(new HuaweiFileDownloadRequestException(null, this.getClass(), e)); + reset(); + } + }); + try { + getFileHashRequest.doPerform(); + } catch (IOException e) { + currentFileRequest.fileDownloadCallback.downloadException(new HuaweiFileDownloadSendException(currentFileRequest, getFileHashRequest, e)); + reset(); + } + } + private void getFileInfo() { GetFileInfoRequest getFileInfoRequest = new GetFileInfoRequest(supportProvider, currentFileRequest); getFileInfoRequest.setFinalizeReq(new Request.RequestCallback() { @@ -624,24 +764,52 @@ public class HuaweiFileDownloadManager { // Stop timeout from hitting this.handler.removeCallbacks(this.timeout); + + byte status = 1; + if (currentFileRequest.isNeedVerify() && currentFileRequest.isNewSync()) { + status = 2; + try { + if (currentFileRequest.fileHash != null) { + MessageDigest m = MessageDigest.getInstance("SHA256"); + m.update(currentFileRequest.getData(), 0, currentFileRequest.fileSize); + byte[] sha256 = m.digest(); + LOG.info("SHA256: {} {}", GB.hexdump(sha256), GB.hexdump(currentFileRequest.fileHash)); + if (Arrays.equals(sha256, currentFileRequest.fileHash)) { + status = 1; + } + } + } catch (Exception e) { + LOG.error("Error verify SHA256", e); + } + } + // File complete request - GetFileDownloadCompleteRequest getFileDownloadCompleteRequest = new GetFileDownloadCompleteRequest(supportProvider, currentFileRequest); + GetFileDownloadCompleteRequest getFileDownloadCompleteRequest = new GetFileDownloadCompleteRequest(supportProvider, currentFileRequest, status); try { getFileDownloadCompleteRequest.doPerform(); } catch (IOException e) { currentFileRequest.fileDownloadCallback.downloadException(new HuaweiFileDownloadSendException(currentFileRequest, getFileDownloadCompleteRequest, e)); reset(); + return; + } + + if (status != 1) { + currentFileRequest.fileDownloadCallback.downloadException(new HuaweiFileDownloadVerifyException(currentFileRequest)); + reset(); + return; } // Handle file data + try { currentFileRequest.fileDownloadCallback.downloadComplete(currentFileRequest); } catch (Exception e) { LOG.error("Download complete callback exception.", e); LOG.warn("File contents: {}", GB.hexdump(currentFileRequest.getData())); - GB.toast("Workout GPX file could not be parsed.",Toast.LENGTH_SHORT, GB.ERROR, e); + GB.toast("Workout GPX file could not be parsed.", Toast.LENGTH_SHORT, GB.ERROR, e); } + 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)) { @@ -660,7 +828,6 @@ public class HuaweiFileDownloadManager { return; } } - reset(); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiP2PManager.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiP2PManager.java index 3710e29e4..62096e1a5 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiP2PManager.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiP2PManager.java @@ -1,21 +1,37 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei; +import android.net.Uri; +import android.text.TextUtils; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.P2P; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.p2p.HuaweiBaseP2PService; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.p2p.HuaweiP2PWakeAppScreenshot; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendP2PCommand; public class HuaweiP2PManager { private final Logger LOG = LoggerFactory.getLogger(HuaweiP2PManager.class); + public interface HuaweiWakeApp { + boolean onWakeApp(HuaweiP2PManager manager, Uri uri); + } + private final HuaweiSupportProvider support; private final List registeredServices; + private final Map supportedWakeApp; + private Short sequence = 1; public synchronized Short getNextSequence() { @@ -25,6 +41,8 @@ public class HuaweiP2PManager { public HuaweiP2PManager(HuaweiSupportProvider support) { this.support = support; this.registeredServices = new ArrayList<>(); + this.supportedWakeApp = new HashMap<>(); + this.supportedWakeApp.put("/router/device/screenshot",new HuaweiP2PWakeAppScreenshot()); } public HuaweiSupportProvider getSupportProvider() { @@ -59,9 +77,71 @@ public class HuaweiP2PManager { registeredServices.clear(); } + public void sendAck(short sequence, String srcPackage, String dstPackage, int code) { + try { + SendP2PCommand test = new SendP2PCommand(this.getSupportProvider(), (byte) 3, sequence, srcPackage, dstPackage, null, null, null, code); + test.doPerform(); + } catch (IOException e) { + LOG.error("P2P Service error send ACK", e); + } + } + + public int handleLink(String link) { + if (TextUtils.isEmpty(link) || (!link.startsWith("huaweischeme://healthapp/router/") && !link.startsWith("huaweischeme://healthapp/home/"))) { + return 0xd2; + } + Uri uri = Uri.parse(link); + LOG.info("Path: {}", uri.getPath()); + HuaweiWakeApp svr = supportedWakeApp.get(uri.getPath()); + if(svr != null && svr.onWakeApp(this, uri)) { + return 0xd1; //success + } + return 0xd2; + } + + public int handleWakeApp(P2P.P2PCommand.Response packet) { + if (packet.respData == null || packet.respData.length == 0) { + return 0xcc; + } + + HuaweiTLV tlv = new HuaweiTLV(); + tlv.parse(packet.respData); + String link = ""; + if(tlv.contains(0x04)) { + try { + link = tlv.getString(0x04); + } catch (HuaweiPacket.MissingTagException e) { + LOG.error("P2P Service error get link", e); + } + } + + if (!TextUtils.isEmpty(link)) { + return handleLink(link); + } + // TODO: support other TLV. + return 0xcd; + } + + + public void handleFile(String srcPackage, String dstPackage, String srcFingerprint, String dstFingerprint, String filename, byte[] data) { + // NOTE: Maybe packet should be found by dstPacket or as a pair package + fingerprint + for (HuaweiBaseP2PService service : registeredServices) { + if (service.getPackage().equals(srcPackage)) { + service.handleFile(filename, data); + } + } + } public void handlePacket(P2P.P2PCommand.Response packet) { LOG.info("P2P Service message: Src: {} Dst: {} Seq: {}", packet.srcPackage, packet.dstPackage, packet.sequenceId); + if(packet.cmdId == 1) { + String[] split = packet.dstPackage.split("\\."); + if (split.length > 2 && split[2].equals("wakeapp")) { + int code = handleWakeApp(packet); + sendAck(packet.sequenceId, packet.dstPackage, packet.srcPackage, code); + return; + } + } for (HuaweiBaseP2PService service : registeredServices) { if (service.getPackage().equals(packet.srcPackage)) { service.handlePacket(packet); 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 ea4c2a306..a10473f7b 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 @@ -130,15 +130,16 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetN import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetOTAChangeLog; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetSmartAlarmList; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetWatchfaceParams; -import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetWorkoutCapability; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendCameraRemoteSetupEvent; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendDeviceReportThreshold; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendExtendedAccountRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendFitnessUserInfoRequest; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendGetDefaultSwitch; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendGpsDataRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendFileUploadInfo; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendHeartRateZonesConfig; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendOTASetAutoUpdate; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendReverseCapabilitiesRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendRunPaceConfigRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendSetContactsRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendNotifyHeartRateCapabilityRequest; @@ -823,13 +824,14 @@ public class HuaweiSupportProvider { initRequestQueue.add(new SendExtendedAccountRequest(this)); initRequestQueue.add(new GetSettingRelatedRequest(this)); initRequestQueue.add(new AcceptAgreementsRequest(this)); + initRequestQueue.add(new SendReverseCapabilitiesRequest(this)); + initRequestQueue.add(new SendSetUpDeviceStatusRequest(this)); initRequestQueue.add(new GetActivityTypeRequest(this)); + initRequestQueue.add(new GetWearStatusRequest(this)); initRequestQueue.add(new GetConnectStatusRequest(this)); initRequestQueue.add(new GetDndLiftWristTypeRequest(this)); initRequestQueue.add(new SendDndDeleteRequest(this)); initRequestQueue.add(new SendDndAddRequest(this)); - initRequestQueue.add(new SendSetUpDeviceStatusRequest(this)); - initRequestQueue.add(new GetWearStatusRequest(this)); initRequestQueue.add(new SendMenstrualCapabilityRequest(this)); initRequestQueue.add(new SendNotifyHeartRateCapabilityRequest(this)); initRequestQueue.add(new SendNotifyRestHeartRateCapabilityRequest(this)); @@ -844,6 +846,7 @@ public class HuaweiSupportProvider { initRequestQueue.add(new GetWatchfaceParams(this)); initRequestQueue.add(new SendCameraRemoteSetupEvent(this, CameraRemote.CameraRemoteSetup.Request.Event.ENABLE_CAMERA)); initRequestQueue.add(new GetAppInfoParams(this)); + initRequestQueue.add(new SendGetDefaultSwitch(this)); initRequestQueue.add(new GetMusicInfoParams(this)); initRequestQueue.add(new GetExtendedMusicInfoParams(this)); initRequestQueue.add(new SetActivateOnLiftRequest(this)); @@ -2506,6 +2509,35 @@ public class HuaweiSupportProvider { this.huaweiDataSyncTreeCircleGoals = null; } + public void deviceFileDownloadRequest(String filename, byte fileType, byte fileId, int fileSize, String srcPackage, String dstPackage, String srcFingerprint, String dstFingerprint) { + HuaweiFileDownloadManager.FileRequest request = HuaweiFileDownloadManager.FileRequest.IncomingFileRequest(filename, new HuaweiFileDownloadManager.FileDownloadCallback() { + @Override + public void downloadComplete(HuaweiFileDownloadManager.FileRequest fileRequest) { + LOG.info("Download file: {}", fileRequest.getFilename()); + huaweiP2PManager.handleFile(fileRequest.getSrcPackage(), fileRequest.getDstPackage(), fileRequest.getSrcFingerprint(), fileRequest.getDstFingerprint(), fileRequest.getFilename(), fileRequest.getData()); + } + + @Override + public void downloadException(HuaweiFileDownloadManager.HuaweiFileDownloadException e) { + super.downloadException(e); + LOG.debug("Download exception"); + } + }); + + request.setInitFormDevice(true); + request.setInFileType(fileType); + request.setFileId(fileId); + request.setFileSize(fileSize); + request.setSrcPackage(srcPackage); + request.setDstPackage(dstPackage); + request.setSrcFingerprint(srcFingerprint); + request.setDstFingerprint(dstFingerprint); + request.setNeedVerify(true); + + huaweiFileDownloadManager.addToQueue(request, false); + + } + public boolean downloadTruSleepData(int start, int end) { // We only get the data if TruSleep is supported if (!getHuaweiCoordinator().supportsTruSleep()) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/p2p/HuaweiBaseP2PService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/p2p/HuaweiBaseP2PService.java index d66be7dd3..efe56d7aa 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/p2p/HuaweiBaseP2PService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/p2p/HuaweiBaseP2PService.java @@ -81,15 +81,6 @@ public abstract class HuaweiBaseP2PService { } } - public void sendAck(short sequence, String srcPackage, String dstPackage, int code) { - try { - SendP2PCommand test = new SendP2PCommand(this.manager.getSupportProvider(), (byte) 3, sequence, srcPackage, dstPackage, null, null, null, code); - test.doPerform(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - public void handlePacket(P2P.P2PCommand.Response packet) { LOG.info("HuaweiBaseP2PService handlePacket: {} Code: {}", packet.cmdId, packet.respCode); if (waitPackets.containsKey(packet.sequenceId)) { @@ -101,14 +92,17 @@ public abstract class HuaweiBaseP2PService { LOG.error("HuaweiBaseP2PService handler is null"); } } else { - if (packet.cmdId == 1) { //Ping - sendAck(packet.sequenceId, packet.dstPackage, packet.srcPackage, 0xca); + manager.sendAck(packet.sequenceId, packet.dstPackage, packet.srcPackage, 0xcf); } else if (packet.cmdId == 2) { + manager.sendAck(packet.sequenceId, packet.dstPackage, packet.srcPackage, 0xcf); handleData(packet.respData); - sendAck(packet.sequenceId, packet.dstPackage, packet.srcPackage, 0xca); } } } + public void handleFile(String filename, byte[] data) { + + } + } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/p2p/HuaweiP2PScreenshotService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/p2p/HuaweiP2PScreenshotService.java new file mode 100644 index 000000000..f3565a18a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/p2p/HuaweiP2PScreenshotService.java @@ -0,0 +1,348 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.p2p; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PaintFlagsDrawFilter; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.provider.MediaStore; +import android.text.TextUtils; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import com.google.gson.annotations.SerializedName; + +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiP2PManager; +import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; + +public class HuaweiP2PScreenshotService extends HuaweiBaseP2PService { + private final Logger LOG = LoggerFactory.getLogger(HuaweiP2PScreenshotService.class); + + public static final String MODULE = "hw.unitedevice.screenshot"; + + public static class DeviceInfo { + @SerializedName("bezierCurvePoint") + public BezierCurvePoint bezierCurvePoint; + @SerializedName("cutout") + public int cutout; + @SerializedName("compressionAlgorithm") + public int compressionAlgorithm; + @SerializedName("photoHeight") + public int photoHeight; + @SerializedName("radius") + public float[] radius; + @SerializedName("photoWidth") + public int photoWidth; + @SerializedName("roundedCutType") + public int roundedCutType; + @SerializedName("startPointX") + public int startPointX; + @SerializedName("startPointY") + public int startPointY; + @SerializedName("watchType") + public int watchType; + + public static class BezierCurvePoint { + @SerializedName("inPoint") + public float[][] inPoint; + @SerializedName("outPoint") + public float[][] outPoint; + } + } + + // Crop related constants. + private final int PIXELS_TO_CROP = 1; + private final int CROP_BORDER_WIDTH = 2; + private final int CROP_BORDER_COLOR = Color.BLACK; + private final int CROP_BACKGROUND_COLOR = Color.WHITE; + + private DeviceInfo deviceInfo = null; + + public HuaweiP2PScreenshotService(HuaweiP2PManager manager) { + super(manager); + LOG.info("HuaweiP2PScreenshotService"); + } + + @Override + public String getModule() { + return HuaweiP2PScreenshotService.MODULE; + } + + @Override + public String getPackage() { + return "com.huawei.watch.screenshot"; + } + + @Override + public String getFingerprint() { + return "SystemApp"; + } + + private String startServiceData() { + JSONObject data = new JSONObject(); + try { + data.put("operateType", 1); + data.put("statusCode", 1); + data.put("hasPermission", 1); + } catch (JSONException e) { + LOG.error("startServiceData: Failed to prepare JSOM Object", e); + } + return data.toString(); + } + + private String createResponse(int i) { + JSONObject data = new JSONObject(); + try { + data.put("operateType", 3); + data.put("statusCode", i); + } catch (JSONException e) { + LOG.error("createResponse: Failed to prepare JSOM Object", e); + } + return data.toString(); + } + + private String createDownloadResponse(String filename, int i) { + JSONObject data = new JSONObject(); + try { + data.put("operateType", 2); + data.put("fileName", filename); + data.put("statusCode", i); + } catch (JSONException e) { + LOG.error("createResponse: Failed to prepare JSOM Object", e); + } + return data.toString(); + } + + public void sendNegotiateConfig() { + String packet = startServiceData(); + LOG.info("HuaweiP2PScreenshotService sendNegotiateConfig"); + sendCommand(packet.getBytes(StandardCharsets.UTF_8), null); + } + + @Override + public void registered() { + } + + @Override + public void unregister() { + + } + + @Override + public void handleData(byte[] data) { + if (data == null) { + LOG.error("HuaweiP2PScreenshotService data is null"); + return; + } + String config = new String(data, StandardCharsets.UTF_8); + if (TextUtils.isEmpty(config)) { + LOG.error("HuaweiP2PScreenshotService config is empty"); + return; + } + LOG.info("HuaweiP2PScreenshotService handleData: {}", config); + DeviceInfo deviceInfo = null; + try { + deviceInfo = new Gson().fromJson(config, DeviceInfo.class); + } catch (JsonSyntaxException e) { + LOG.error("HuaweiP2PScreenshotService error parse config", e); + } + + int status = 2; + if (deviceInfo == null) { + LOG.error("HuaweiP2PScreenshotService deviceInfo is null"); + } else { + if (deviceInfo.compressionAlgorithm == 1) { + status = 1; + } + } + + if (status == 1) { + this.deviceInfo = deviceInfo; + } + sendCommand(createResponse(status).getBytes(StandardCharsets.UTF_8), null); + } + + private int getWightWithoutCropPixels(int width) { + return width - PIXELS_TO_CROP * 2; // crop from both sides. + } + + private int getHeightWithoutCropPixels(int height) { + return height - PIXELS_TO_CROP * 2; // crop from both sides. + } + + private Bitmap prepareCroppedImageCircle(Bitmap image) { + int cropWidth = getWightWithoutCropPixels(this.deviceInfo.photoWidth); + int cropHeight = getHeightWithoutCropPixels(this.deviceInfo.photoHeight); + int width = getWightWithoutCropPixels(this.deviceInfo.photoWidth) + CROP_BORDER_WIDTH * 2; // we have border on both sides + int height = getHeightWithoutCropPixels(this.deviceInfo.photoHeight) + CROP_BORDER_WIDTH * 2; // we have border on both sides + Bitmap createBitmap = Bitmap.createBitmap(width, height, image.getConfig()); + Canvas canvas = new Canvas(createBitmap); + canvas.drawColor(CROP_BACKGROUND_COLOR); + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(CROP_BORDER_WIDTH); + paint.setColor(CROP_BORDER_COLOR); + canvas.drawCircle(((float) width) / 2.0f, ((float) height) / 2.0f, ((float) Math.min(width - CROP_BORDER_WIDTH, height - CROP_BORDER_WIDTH)) / 2.0f, paint); + Bitmap croppedImage = cropImageCircle(image, cropWidth, cropHeight); + canvas.drawBitmap(croppedImage, ((float) (width - cropWidth)) / 2.0f, ((float) (height - cropHeight)) / 2.0f, new Paint(Paint.ANTI_ALIAS_FLAG)); + return createBitmap; + } + + private Bitmap cropImageCircle(Bitmap srcImage, int width, int height) { + Bitmap resultImage = Bitmap.createBitmap(width, height, srcImage.getConfig()); + Canvas canvas = new Canvas(resultImage); + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); + paint.setStyle(Paint.Style.FILL); + canvas.drawCircle(((float) width) / 2.0f, ((float) height) / 2.0f, ((float) Math.min(width, height)) / 2.0f, paint); + paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); + canvas.setDrawFilter(new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG)); + canvas.drawBitmap(srcImage, (float) (this.deviceInfo.startPointX - PIXELS_TO_CROP), (float) (this.deviceInfo.startPointY - PIXELS_TO_CROP), paint); + return resultImage; + } + + private Bitmap prepareCroppedImageRounded(Bitmap image) { + //TODO: implement crop for device with rounded corners + return image; + } + + private Bitmap prepareCroppedImage(Bitmap bitmap) { + if (bitmap == null) + return null; + if (this.deviceInfo.watchType == 1) { + return prepareCroppedImageCircle(bitmap); + } else if (this.deviceInfo.watchType == 2) { + return prepareCroppedImageRounded(bitmap); + } + LOG.info("HuaweiP2PScreenshotService unknown watchType: {}", this.deviceInfo.watchType); + // I don't know what to do here. So save image as is. + return bitmap; + } + + private boolean saveBitmapOld(String filename, Bitmap bitmap) { + LOG.info("HuaweiP2PScreenshotService saveBitmapOld: {}", filename); + File targetFile; + try { + targetFile = new File(FileUtils.getExternalFilesDir() + File.separator + filename); + } catch (IOException e) { + LOG.error("Could not open Screenshot file to write to", e); + return false; + } + try { + FileOutputStream fileOutputStream = new FileOutputStream(targetFile); + if (!bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream)) { + fileOutputStream.close(); + return false; + } + + fileOutputStream.close(); + } catch (IOException e) { + LOG.error("Error save bitmap", e); + return false; + } + return true; + } + + private boolean saveMediaStore(String filename, Bitmap bitmap) { + LOG.info("HuaweiP2PScreenshotService saveMediaStore: {}", filename); + String relativePath = Environment.DIRECTORY_PICTURES + File.separator + "Screenshots"; + ContentValues contentValues = new ContentValues(); + contentValues.put(MediaStore.Images.ImageColumns.RELATIVE_PATH, relativePath); + contentValues.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, filename); + contentValues.put(MediaStore.Images.ImageColumns.MIME_TYPE, "image/png"); + contentValues.put(MediaStore.Images.ImageColumns.IS_PENDING, 1); + ContentResolver contentResolver = GBApplication.getContext().getContentResolver(); + Uri uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues); + + if (uri == null) { + contentValues.clear(); + contentValues.put(MediaStore.MediaColumns.IS_PENDING, 0); + return false; + } + try { + OutputStream stream = contentResolver.openOutputStream(uri); + if (stream != null && bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)) { + contentValues.put(MediaStore.MediaColumns.IS_PENDING, 0); + contentResolver.update(uri, contentValues, null, null); + stream.close(); + return true; + } + if (stream != null) { + stream.close(); + } + } catch (Exception e) { + LOG.error("Error save screenshot", e); + } + + contentValues.clear(); + contentValues.put(MediaStore.Images.ImageColumns.IS_PENDING, 0); + contentResolver.update(uri, contentValues, null, null); + return false; + } + + private boolean saveBitmap(String filename, Bitmap bitmap) { + if (Build.VERSION.SDK_INT < 29) + return saveBitmapOld(filename, bitmap); + return saveMediaStore(filename, bitmap); + } + + + @Override + public void handleFile(String filename, byte[] data) { + LOG.info("HuaweiP2PScreenshotService handleFile: {}", filename); + if (data == null) { + LOG.error("HuaweiP2PScreenshotService empty file data"); + sendCommand(createDownloadResponse(filename, 2).getBytes(StandardCharsets.UTF_8), null); + return; + } + if (this.deviceInfo == null) { + LOG.error("HuaweiP2PScreenshotService no device config"); + sendCommand(createDownloadResponse(filename, 2).getBytes(StandardCharsets.UTF_8), null); + return; + } + + Bitmap image = null; + if (this.deviceInfo.compressionAlgorithm == 1) { + image = BitmapFactory.decodeByteArray(data, 0, data.length); + } + + if (this.deviceInfo.cutout == 1) { + image = this.prepareCroppedImage(image); + } + if (image == null) { + LOG.error("HuaweiP2PScreenshotService no image"); + sendCommand(createDownloadResponse(filename, 2).getBytes(StandardCharsets.UTF_8), null); + return; + } + + if (saveBitmap(filename, image)) { + sendCommand(createDownloadResponse(filename, 1).getBytes(StandardCharsets.UTF_8), null); + } else { + LOG.error("HuaweiP2PScreenshotService error to save image"); + sendCommand(createDownloadResponse(filename, 2).getBytes(StandardCharsets.UTF_8), null); + } + } + + public static HuaweiP2PScreenshotService getRegisteredInstance(HuaweiP2PManager manager) { + return (HuaweiP2PScreenshotService) manager.getRegisteredService(HuaweiP2PScreenshotService.MODULE); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/p2p/HuaweiP2PWakeAppScreenshot.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/p2p/HuaweiP2PWakeAppScreenshot.java new file mode 100644 index 000000000..3c371501d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/p2p/HuaweiP2PWakeAppScreenshot.java @@ -0,0 +1,20 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.p2p; + +import android.net.Uri; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiP2PManager; + +public class HuaweiP2PWakeAppScreenshot implements HuaweiP2PManager.HuaweiWakeApp { + @Override + public boolean onWakeApp(HuaweiP2PManager manager, Uri uri) { + if (HuaweiP2PScreenshotService.getRegisteredInstance(manager) == null) { + new HuaweiP2PScreenshotService(manager).register(); + } + + HuaweiP2PScreenshotService screenshotService = HuaweiP2PScreenshotService.getRegisteredInstance(manager); + + screenshotService.sendNegotiateConfig(); + + return true; + } +} 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 index a0168863b..63ce0ada5 100644 --- 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 @@ -27,8 +27,9 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupport public class GetFileDownloadCompleteRequest extends Request { private final HuaweiFileDownloadManager.FileRequest request; + private final byte status; - public GetFileDownloadCompleteRequest(HuaweiSupportProvider support, HuaweiFileDownloadManager.FileRequest request) { + public GetFileDownloadCompleteRequest(HuaweiSupportProvider support, HuaweiFileDownloadManager.FileRequest request, byte status) { super(support); if (request.isNewSync()) { this.serviceId = FileDownloadService2C.id; @@ -38,13 +39,14 @@ public class GetFileDownloadCompleteRequest extends Request { this.commandId = FileDownloadService0A.FileDownloadCompleteRequest.id; } this.request = request; + this.status = status; } @Override protected List createRequest() throws RequestCreationException { try { if (request.isNewSync()) - return new FileDownloadService2C.FileDownloadCompleteRequest(paramsProvider, this.request.getFileId()).serialize(); + return new FileDownloadService2C.FileDownloadCompleteRequest(paramsProvider, this.request.getFileId(), status).serialize(); else return new FileDownloadService0A.FileDownloadCompleteRequest(paramsProvider).serialize(); } catch (HuaweiPacket.CryptoException e) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetFileHashRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetFileHashRequest.java new file mode 100644 index 000000000..a404aa43b --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetFileHashRequest.java @@ -0,0 +1,45 @@ +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.FileDownloadService2C; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiFileDownloadManager; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider; + +public class GetFileHashRequest extends Request { + private final HuaweiFileDownloadManager.FileRequest request; + + public boolean newSync; + public byte fileId; + public byte[] fileHash; + + + public GetFileHashRequest(HuaweiSupportProvider support, HuaweiFileDownloadManager.FileRequest request) { + super(support); + this.serviceId = FileDownloadService2C.id; + this.commandId = FileDownloadService2C.FileRequestHash.id; + this.request = request; + } + + @Override + protected List createRequest() throws RequestCreationException { + try { + return new FileDownloadService2C.FileRequestHash.Request(paramsProvider, this.request.getFileId()).serialize(); + } catch (HuaweiPacket.CryptoException e) { + throw new RequestCreationException(e); + } + } + + @Override + protected void processResponse() throws ResponseParseException { + if (this.receivedPacket instanceof FileDownloadService2C.FileRequestHash.Response) { + this.newSync = true; + FileDownloadService2C.FileRequestHash.Response packet = (FileDownloadService2C.FileRequestHash.Response) this.receivedPacket; + this.fileId = packet.fileId; + this.fileHash = packet.fileHash; + } else { + throw new ResponseTypeMismatchException(this.receivedPacket, FileDownloadService2C.FileInfo.Response.class); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetFileIncomingAck.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetFileIncomingAck.java new file mode 100644 index 000000000..f0ee1b9fe --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetFileIncomingAck.java @@ -0,0 +1,32 @@ +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.FileDownloadService2C; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiFileDownloadManager; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider; + +public class GetFileIncomingAck extends Request { + private final HuaweiFileDownloadManager.FileRequest request; + private final byte status; + + public GetFileIncomingAck(HuaweiSupportProvider support, HuaweiFileDownloadManager.FileRequest request, byte status) { + super(support); + this.serviceId = FileDownloadService2C.id; + this.commandId = FileDownloadService2C.IncomingInitRequest.id; + this.request = request; + this.status = status; + + this.addToResponse = false; + } + + @Override + protected List createRequest() throws RequestCreationException { + try { + return new FileDownloadService2C.IncomingInitRequest.Request(this.paramsProvider, this.request.getFilename(), this.request.getInFileType(), this.request.getFileId(), this.request.getFileSize(), this.request.getSrcPackage(), this.request.getDstPackage(), this.request.getSrcFingerprint(), this.request.getDstFingerprint(), this.status).serialize(); + } catch (HuaweiPacket.CryptoException e) { + throw new RequestCreationException(e); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSupportedCommandsRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSupportedCommandsRequest.java index 93061bb9d..734266859 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSupportedCommandsRequest.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSupportedCommandsRequest.java @@ -54,7 +54,7 @@ public class GetSupportedCommandsRequest extends Request { DeviceConfig.SupportedCommands.Request commandsRequest = new DeviceConfig.SupportedCommands.Request(paramsProvider); byte nextService = activatedServices.remove(0); boolean fits = commandsRequest.addCommandsForService(nextService, this.commandsPerService.get((int) nextService)); - while (fits && activatedServices.size() > 0) { + while (fits && !activatedServices.isEmpty()) { nextService = activatedServices.remove(0); fits = commandsRequest.addCommandsForService(nextService, this.commandsPerService.get((int) nextService)); } @@ -88,18 +88,12 @@ public class GetSupportedCommandsRequest extends Request { ); } - if (activatedServices.size() > 0) { + if (!activatedServices.isEmpty()) { GetSupportedCommandsRequest nextRequest = new GetSupportedCommandsRequest(supportProvider, activatedServices); this.nextRequest(nextRequest); } else { supportProvider.getHuaweiCoordinator().printCommandsPerService(); - if (supportProvider.getHuaweiCoordinator().supportsExpandCapability()) { - GetExpandCapabilityRequest nextRequest = new GetExpandCapabilityRequest(supportProvider); - nextRequest.setFinalizeReq(dynamicServicesReq); - this.nextRequest(nextRequest); - } else { - dynamicServicesReq.call(); - } + dynamicServicesReq.call(); } } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSupportedServicesRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSupportedServicesRequest.java index f7c967711..ef5d3a6b8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSupportedServicesRequest.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetSupportedServicesRequest.java @@ -56,6 +56,7 @@ public class GetSupportedServicesRequest extends Request { byte[] supportedServices = ((DeviceConfig.SupportedServices.Response) receivedPacket).supportedServices; List activatedServices = new ArrayList<>(); + activatedServices.add((byte) 0x01); // device always support 1 service. add it for (int i = 0; i < supportedServices.length; i++) { if (supportedServices[i] == 1) { activatedServices.add(knownSupportedServices[i]); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendGetDefaultSwitch.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendGetDefaultSwitch.java new file mode 100644 index 000000000..4693c82c7 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendGetDefaultSwitch.java @@ -0,0 +1,39 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider; + +public class SendGetDefaultSwitch extends Request { + private static final Logger LOG = LoggerFactory.getLogger(SendGetDefaultSwitch.class); + + public SendGetDefaultSwitch(HuaweiSupportProvider support) { + super(support); + this.serviceId = DeviceConfig.id; + this.commandId = DeviceConfig.GetDefaultSwitch.id; + } + + @Override + protected boolean requestSupported() { + return supportProvider.getHuaweiCoordinator().supportDefaultSwitch(); + } + + @Override + protected List createRequest() throws Request.RequestCreationException { + try { + return new DeviceConfig.GetDefaultSwitch.Request(paramsProvider).serialize(); + } catch (HuaweiPacket.CryptoException e) { + throw new Request.RequestCreationException(e); + } + } + + @Override + protected void processResponse() throws Request.ResponseParseException { + LOG.debug("handle GetDefaultSwitch"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendReverseCapabilitiesRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendReverseCapabilitiesRequest.java new file mode 100644 index 000000000..545960d2f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendReverseCapabilitiesRequest.java @@ -0,0 +1,40 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; +import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider; + +public class SendReverseCapabilitiesRequest extends Request { + private static final Logger LOG = LoggerFactory.getLogger(SendReverseCapabilitiesRequest.class); + + public SendReverseCapabilitiesRequest(HuaweiSupportProvider support) { + super(support); + this.serviceId = DeviceConfig.id; + this.commandId = DeviceConfig.ReverseCapabilities.id; + } + + @Override + protected boolean requestSupported() { + return supportProvider.getHuaweiCoordinator().supportsReverseCapabilities(); + } + + @Override + protected List createRequest() throws RequestCreationException { + DeviceConfig.ReverseCapabilities.Request reverseCapabilitiesRequest = new DeviceConfig.ReverseCapabilities.Request(paramsProvider); + try { + return reverseCapabilitiesRequest.serialize(); + } catch (HuaweiPacket.CryptoException e) { + throw new RequestCreationException(e); + } + } + + @Override + protected void processResponse() throws ResponseParseException { + LOG.debug("handle ReverseCapabilities"); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendSetUpDeviceStatusRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendSetUpDeviceStatusRequest.java index 6bcd397e3..9d338b0a1 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendSetUpDeviceStatusRequest.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/SendSetUpDeviceStatusRequest.java @@ -27,7 +27,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig. import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider; public class SendSetUpDeviceStatusRequest extends Request { - private static final Logger LOG = LoggerFactory.getLogger(SetUpDeviceStatusRequest.class); + private static final Logger LOG = LoggerFactory.getLogger(SendSetUpDeviceStatusRequest.class); public SendSetUpDeviceStatusRequest(HuaweiSupportProvider support) { super(support);