From d38afe60c2d195ee71bb833b8969f423f0be02e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Sun, 11 Jun 2023 15:21:39 +0100 Subject: [PATCH] Zepp OS: Add incoming file support to file transfer service --- .../devices/huami/Huami2021Support.java | 33 ++-- .../operations/ZeppOsAgpsUpdateOperation.java | 19 ++- .../ZeppOsGpxRouteUploadOperation.java | 17 +- ...ce.java => ZeppOsFileTransferService.java} | 160 +++++++++++++++--- .../services/ZeppOsNotificationService.java | 15 +- 5 files changed, 189 insertions(+), 55 deletions(-) rename app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/{ZeppOsFileUploadService.java => ZeppOsFileTransferService.java} (57%) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021Support.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021Support.java index 5f53945b5..17efc874f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021Support.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021Support.java @@ -96,15 +96,11 @@ import nodomain.freeyourgadget.gadgetbridge.model.Contact; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; -import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes; import nodomain.freeyourgadget.gadgetbridge.model.Reminder; import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; -import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchActivityOperation; -import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchSportsSummaryOperation; -import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.HuamiFetchDebugLogsOperation; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.UpdateFirmwareOperation2021; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.AbstractZeppOsService; @@ -120,7 +116,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.service import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsShortcutCardsService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsConfigService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsContactsService; -import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsFileUploadService; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsFileTransferService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsFtpServerService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsMorningUpdatesService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsPhoneService; @@ -133,7 +129,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.MapUtils; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; -public abstract class Huami2021Support extends HuamiSupport { +public abstract class Huami2021Support extends HuamiSupport implements ZeppOsFileTransferService.Callback { private static final Logger LOG = LoggerFactory.getLogger(Huami2021Support.class); // Tracks whether realtime HR monitoring is already started, so we can just @@ -141,7 +137,7 @@ public abstract class Huami2021Support extends HuamiSupport { private boolean heartRateRealtimeStarted; // Services - private final ZeppOsFileUploadService fileUploadService = new ZeppOsFileUploadService(this); + private final ZeppOsFileTransferService fileTransferService = new ZeppOsFileTransferService(this); private final ZeppOsConfigService configService = new ZeppOsConfigService(this); private final ZeppOsAgpsService agpsService = new ZeppOsAgpsService(this); private final ZeppOsWifiService wifiService = new ZeppOsWifiService(this); @@ -154,12 +150,12 @@ public abstract class Huami2021Support extends HuamiSupport { private final ZeppOsAlarmsService alarmsService = new ZeppOsAlarmsService(this); private final ZeppOsCalendarService calendarService = new ZeppOsCalendarService(this); private final ZeppOsCannedMessagesService cannedMessagesService = new ZeppOsCannedMessagesService(this); - private final ZeppOsNotificationService notificationService = new ZeppOsNotificationService(this, fileUploadService); + private final ZeppOsNotificationService notificationService = new ZeppOsNotificationService(this, fileTransferService); private final ZeppOsAlexaService alexaService = new ZeppOsAlexaService(this); private final ZeppOsAppsService appsService = new ZeppOsAppsService(this); private final Map mServiceMap = new LinkedHashMap() {{ - put(fileUploadService.getEndpoint(), fileUploadService); + put(fileTransferService.getEndpoint(), fileTransferService); put(configService.getEndpoint(), configService); put(agpsService.getEndpoint(), agpsService); put(wifiService.getEndpoint(), wifiService); @@ -645,7 +641,7 @@ public abstract class Huami2021Support extends HuamiSupport { this, agpsHandler.getFile(), agpsService, - fileUploadService, + fileTransferService, configService ).perform(); } catch (final Exception e) { @@ -661,7 +657,7 @@ public abstract class Huami2021Support extends HuamiSupport { new ZeppOsGpxRouteUploadOperation( this, gpxRouteHandler.getFile(), - fileUploadService + fileTransferService ).perform(); } catch (final Exception e) { GB.toast(getContext(), "Gpx route file cannot be installed: " + e.getMessage(), Toast.LENGTH_LONG, GB.ERROR, e); @@ -1868,6 +1864,21 @@ public abstract class Huami2021Support extends HuamiSupport { } } + @Override + public void onFileUploadFinish(final boolean success) { + LOG.warn("Unexpected file upload finish: {}", success); + } + + @Override + public void onFileUploadProgress(final int progress) { + LOG.warn("Unexpected file upload progress: {}", progress); + } + + @Override + public void onFileDownloadFinish(final String url, final String filename, final byte[] data) { + LOG.info("File received: url={} filename={} length={}", url, filename, data.length); + } + private byte bool(final boolean b) { return (byte) (b ? 1 : 0); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/operations/ZeppOsAgpsUpdateOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/operations/ZeppOsAgpsUpdateOperation.java index 4671a298c..f5c574caa 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/operations/ZeppOsAgpsUpdateOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/operations/ZeppOsAgpsUpdateOperation.java @@ -28,20 +28,20 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetProgressActi import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021Support; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsAgpsService; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsConfigService; -import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsFileUploadService; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsFileTransferService; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.operations.OperationStatus; import nodomain.freeyourgadget.gadgetbridge.util.GB; /** * Updates the AGPS EPO on a Zepp OS device. Update goes as follows: * 1. Request an upload start from {@link ZeppOsAgpsService} - * 2. After successful ack from 1, upload the file to agps://upgrade using {@link ZeppOsFileUploadService} + * 2. After successful ack from 1, upload the file to agps://upgrade using {@link ZeppOsFileTransferService} * 3. After successful ack from 2, trigger the actual update with {@link ZeppOsAgpsService} * 4. After successful ack from 3, update is finished. Trigger an AGPS config request from {@link ZeppOsConfigService} * to reload the AGPS update and expiration timestamps. */ public class ZeppOsAgpsUpdateOperation extends AbstractBTLEOperation - implements ZeppOsFileUploadService.Callback, ZeppOsAgpsService.Callback { + implements ZeppOsFileTransferService.Callback, ZeppOsAgpsService.Callback { private static final Logger LOG = LoggerFactory.getLogger(ZeppOsAgpsUpdateOperation.class); private static final String AGPS_UPDATE_URL = "agps://upgrade"; @@ -51,19 +51,19 @@ public class ZeppOsAgpsUpdateOperation extends AbstractBTLEOperation - implements ZeppOsFileUploadService.Callback { + implements ZeppOsFileTransferService.Callback { private static final Logger LOG = LoggerFactory.getLogger(ZeppOsGpxRouteUploadOperation.class); private final ZeppOsGpxRouteFile file; private final byte[] fileBytes; - private final ZeppOsFileUploadService fileUploadService; + private final ZeppOsFileTransferService fileTransferService; public ZeppOsGpxRouteUploadOperation(final Huami2021Support support, final ZeppOsGpxRouteFile file, - final ZeppOsFileUploadService fileUploadService) { + final ZeppOsFileTransferService fileTransferService) { super(support); this.file = file; this.fileBytes = file.getEncodedBytes(); - this.fileUploadService = fileUploadService; + this.fileTransferService = fileTransferService; } @Override protected void doPerform() throws IOException { - fileUploadService.sendFile( + fileTransferService.sendFile( "sport://file_transfer?appId=7073283073¶ms={}", "track_" + file.getTimestamp() + ".dat", fileBytes, @@ -88,6 +88,11 @@ public class ZeppOsGpxRouteUploadOperation extends AbstractBTLEOperation mSessionRequests = new HashMap<>(); + private final Map mSessionRequests = new HashMap<>(); private int mChunkSize = -1; - public ZeppOsFileUploadService(final Huami2021Support support) { + public ZeppOsFileTransferService(final Huami2021Support support) { super(support); } @@ -75,25 +75,28 @@ public class ZeppOsFileUploadService extends AbstractZeppOsService { case CMD_CAPABILITIES_RESPONSE: final int version = payload[1] & 0xff; if (version != 1 && version != 2) { - LOG.error("Unsupported file upload service version: {}", version); + LOG.error("Unsupported file transfer service version: {}", version); return; } mChunkSize = BLETypeConversions.toUint16(payload, 2); - LOG.info("Got file upload service: version={}, chunkSize={}", version, mChunkSize); + LOG.info("Got file transfer service: version={}, chunkSize={}", version, mChunkSize); return; - case CMD_UPLOAD_RESPONSE: + case CMD_TRANSFER_REQUEST: + handleFileTransferRequest(payload); + return; + case CMD_TRANSFER_RESPONSE: session = payload[1]; status = payload[2]; final int existingProgress = BLETypeConversions.toUint32(payload, 3); - LOG.info("Band acknowledged file upload request: session={}, status={}, existingProgress={}", session, status, existingProgress); + LOG.info("Band acknowledged file transfer request: session={}, status={}, existingProgress={}", session, status, existingProgress); if (status != 0) { LOG.error("Unexpected status from band for session {}, aborting", session); - onFinish(session, false); + onUploadFinish(session, false); return; } if (existingProgress != 0) { LOG.info("Updating existing progress for session {} to {}", session, existingProgress); - final FileSendRequest request = mSessionRequests.get(session); + final FileTransferRequest request = mSessionRequests.get(session); if (request == null) { LOG.error("No request found for session {}", session); return; @@ -102,19 +105,22 @@ public class ZeppOsFileUploadService extends AbstractZeppOsService { } sendNextQueuedData(session); return; + case CMD_DATA_SEND: + handleFileTransferData(payload); + return; case CMD_DATA_ACK: session = payload[1]; status = payload[2]; - LOG.info("Band acknowledged file upload data: session={}, status={}", session, status); + LOG.info("Band acknowledged file transfer data: session={}, status={}", session, status); if (status != 0) { LOG.error("Unexpected status from band, aborting session {}", session); - onFinish(session, false); + onUploadFinish(session, false); return; } sendNextQueuedData(session); return; default: - LOG.warn("Unexpected file upload byte {}", String.format("0x%02x", payload[0])); + LOG.warn("Unexpected file transfer byte {}", String.format("0x%02x", payload[0])); } } @@ -127,6 +133,93 @@ public class ZeppOsFileUploadService extends AbstractZeppOsService { write(builder, new byte[]{CMD_CAPABILITIES_REQUEST}); } + private void handleFileTransferRequest(final byte[] payload) { + // File transfer request initialized from watch + int pos = 1; + final byte session = payload[pos++]; + final String url = StringUtils.untilNullTerminator(payload, pos); + if (url == null) { + LOG.error("Unable to parse url from transfer request"); + return; + } + pos += url.length() + 1; + final String filename = StringUtils.untilNullTerminator(payload, pos); + if (filename == null) { + LOG.error("Unable to parse filename from transfer request"); + return; + } + pos += filename.length() + 1; + final int length = BLETypeConversions.toUint32(payload, pos); + pos += 4; + final int crc32 = BLETypeConversions.toUint32(payload, pos); + + LOG.info("Got transfer request: session={}, url={}, filename={}, length={}", session, url, filename, length); + + final FileTransferRequest request = new FileTransferRequest(url, filename, new byte[length], getSupport()); + request.setCrc32(crc32); + + final ByteBuffer buf = ByteBuffer.allocate(7).order(ByteOrder.LITTLE_ENDIAN); + buf.order(ByteOrder.LITTLE_ENDIAN); + buf.put(CMD_TRANSFER_RESPONSE); + buf.put(session); + buf.put((byte) 0x00); + buf.putInt(0); + + mSessionRequests.put(session, request); + + write("send file transfer response", buf.array()); + } + + private void handleFileTransferData(final byte[] payload) { + final ByteBuffer buf = ByteBuffer.wrap(payload).order(ByteOrder.LITTLE_ENDIAN); + buf.get(); // Discard first byte + final byte secondByte = buf.get(); + final boolean firstPacket = (secondByte == 1); + final boolean lastPacket = (secondByte == 2); + final byte session = buf.get(); + final byte index = buf.get(); + final short size = buf.getShort(); + + final FileTransferRequest request = mSessionRequests.get(session); + if (request == null) { + LOG.error("No request found for session {}", session); + return; + } + + if (index != request.index) { + LOG.warn("Unexpected index {}, expected {}", index, request.index); + return; + } + + if (firstPacket && request.getProgress() != 0) { + LOG.warn("Got first packet, but progress is {}", request.getProgress()); + return; + } + + buf.get(request.getBytes(), request.getProgress(), size); + request.setIndex((byte) (index + 1)); + request.setProgress(request.getProgress() + size); + + LOG.debug("Got data for session={}, progress={}/{}", session, request.getProgress(), request.getSize()); + + if (lastPacket) { + mSessionRequests.remove(session); + + if (request.getProgress() != request.getSize()) { + LOG.warn("Request not finished: {}/{}", request.getProgress(), request.getSize()); + return; + } + + final int checksum = CheckSums.getCRC32(request.getBytes()); + if (checksum != request.getCrc32()) { + LOG.warn("Checksum mismatch: expected {}, got {}", request.getCrc32(), checksum); + return; + } + + request.getCallback().onFileDownloadFinish(request.getUrl(), request.getFilename(), request.getBytes()); + } + } + public void sendFile(final String url, final String filename, final byte[] bytes, final Callback callback) { if (mChunkSize < 0) { LOG.error("Service not initialized, refusing to send {}", url); @@ -136,20 +229,23 @@ public class ZeppOsFileUploadService extends AbstractZeppOsService { LOG.info("Sending {} bytes to {}", bytes.length, url); - final FileSendRequest request = new FileSendRequest(url, filename, bytes, callback); + final FileTransferRequest request = new FileTransferRequest(url, filename, bytes, callback); - final byte session = (byte) mSessionRequests.size(); + byte session = (byte) mSessionRequests.size(); + while (mSessionRequests.containsKey(session)) { + session++; + } final ByteBuffer buf = ByteBuffer.allocate(2 + url.length() + 1 + filename.length() + 1 + 4 + 4); buf.order(ByteOrder.LITTLE_ENDIAN); - buf.put(CMD_UPLOAD_REQUEST); + buf.put(CMD_TRANSFER_REQUEST); buf.put(session); buf.put(url.getBytes(StandardCharsets.UTF_8)); buf.put((byte) 0x00); buf.put(filename.getBytes(StandardCharsets.UTF_8)); buf.put((byte) 0x00); buf.putInt(bytes.length); - buf.putInt(CheckSums.getCRC32(bytes)); + buf.putInt(request.getCrc32()); write("send file upload request", buf.array()); @@ -157,7 +253,7 @@ public class ZeppOsFileUploadService extends AbstractZeppOsService { } private void sendNextQueuedData(final byte session) { - final FileSendRequest request = mSessionRequests.get(session); + final FileTransferRequest request = mSessionRequests.get(session); if (request == null) { LOG.error("No request found for session {}", session); return; @@ -165,7 +261,7 @@ public class ZeppOsFileUploadService extends AbstractZeppOsService { if (request.getProgress() >= request.getSize()) { LOG.info("Sending {} finished", request.getUrl()); - onFinish(session, true); + onUploadFinish(session, true); return; } @@ -209,8 +305,8 @@ public class ZeppOsFileUploadService extends AbstractZeppOsService { write("send file data", buf.array()); } - private void onFinish(final byte session, final boolean success) { - final FileSendRequest request = mSessionRequests.get(session); + private void onUploadFinish(final byte session, final boolean success) { + final FileTransferRequest request = mSessionRequests.get(session); if (request == null) { LOG.error("No request found for session {}", session); return; @@ -224,19 +320,21 @@ public class ZeppOsFileUploadService extends AbstractZeppOsService { /** * Wrapper class to keep track of ongoing file send requests and their progress. */ - public static class FileSendRequest { + public static class FileTransferRequest { private final String url; private final String filename; private final byte[] bytes; private final Callback callback; private int progress = 0; private byte index = 0; + private int crc32; - public FileSendRequest(final String url, final String filename, final byte[] bytes, final Callback callback) { + public FileTransferRequest(final String url, final String filename, final byte[] bytes, final Callback callback) { this.url = url; this.filename = filename; this.bytes = bytes; this.callback = callback; + this.crc32 = CheckSums.getCRC32(bytes); } public String getUrl() { @@ -274,11 +372,21 @@ public class ZeppOsFileUploadService extends AbstractZeppOsService { public void setIndex(final byte index) { this.index = index; } + + public int getCrc32() { + return crc32; + } + + public void setCrc32(final int crc32) { + this.crc32 = crc32; + } } public interface Callback { void onFileUploadFinish(boolean success); void onFileUploadProgress(int progress); + + void onFileDownloadFinish(String url, String filename, byte[] data); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsNotificationService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsNotificationService.java index 4539c00b2..b0e55b83f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsNotificationService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsNotificationService.java @@ -71,11 +71,11 @@ public class ZeppOsNotificationService extends AbstractZeppOsService { // This needs to be simplified. private final LimitedQueue mNotificationReplyAction = new LimitedQueue(16); - private final ZeppOsFileUploadService fileUploadService; + private final ZeppOsFileTransferService fileTransferService; - public ZeppOsNotificationService(final Huami2021Support support, final ZeppOsFileUploadService fileUploadService) { + public ZeppOsNotificationService(final Huami2021Support support, final ZeppOsFileTransferService fileTransferService) { super(support); - this.fileUploadService = fileUploadService; + this.fileTransferService = fileTransferService; } @Override @@ -380,11 +380,11 @@ public class ZeppOsNotificationService extends AbstractZeppOsService { ); final String filename = String.format("logo_%s.tga", packageName.replace(".", "_")); - fileUploadService.sendFile( + fileTransferService.sendFile( url, filename, tga565, - new ZeppOsFileUploadService.Callback() { + new ZeppOsFileTransferService.Callback() { @Override public void onFileUploadFinish(final boolean success) { LOG.info("Finished sending icon, success={}", success); @@ -397,6 +397,11 @@ public class ZeppOsNotificationService extends AbstractZeppOsService { public void onFileUploadProgress(final int progress) { LOG.trace("Icon send progress: {}", progress); } + + @Override + public void onFileDownloadFinish(final String url, final String filename, final byte[] data) { + LOG.warn("Receiver unexpected file: url={} filename={} length={}", url, filename, data.length); + } } );