From c47e8300566b39531b3bc4732e6343cf08980509 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Sun, 22 Oct 2023 22:41:56 +0100 Subject: [PATCH] Xiaomi: Watchface upload (wip, does not work) --- .../devices/xiaomi/XiaomiInstallHandler.java | 38 +++- .../xiaomi/miband8/XiaomiFWHelper.java | 123 ++++++++++++ .../devices/xiaomi/XiaomiCharacteristic.java | 42 +++- .../service/devices/xiaomi/XiaomiSupport.java | 10 + .../services/XiaomiDataUploadService.java | 182 ++++++++++++++++++ .../services/XiaomiWatchfaceService.java | 134 ++++++++++--- .../gadgetbridge/util/CheckSums.java | 22 +++ 7 files changed, 514 insertions(+), 37 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/miband8/XiaomiFWHelper.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiDataUploadService.java diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiInstallHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiInstallHandler.java index 79ca4cb44..de76f58a3 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiInstallHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/XiaomiInstallHandler.java @@ -16,40 +16,64 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.devices.xiaomi; -import static nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiFirmwareType.AGPS_UIHH; - import android.content.Context; import android.net.Uri; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity; -import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; +import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.miband8.XiaomiFWHelper; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.GenericItem; public class XiaomiInstallHandler implements InstallHandler { protected final Uri mUri; protected final Context mContext; + protected final XiaomiFWHelper helper; public XiaomiInstallHandler(final Uri uri, final Context context) { this.mUri = uri; this.mContext = context; + this.helper = new XiaomiFWHelper(uri, context); } @Override public boolean isValid() { - // TODO - return false; + return helper.isValid(); } @Override public void validateInstallation(final InstallActivity installActivity, final GBDevice device) { - // TODO + if (device.isBusy()) { + installActivity.setInfoText(device.getBusyTask()); + installActivity.setInstallEnabled(false); + return; + } + + if (!device.isInitialized()) { + installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_ready)); + installActivity.setInstallEnabled(false); + return; + } + + if (!helper.isValid()) { + installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_supported)); + installActivity.setInstallEnabled(false); + return; + } + + final GenericItem installItem = new GenericItem(); + installItem.setIcon(R.drawable.ic_watchface); + installItem.setName(mContext.getString(R.string.kind_watchface)); + installItem.setDetails(helper.getDetails()); + + installActivity.setInfoText(mContext.getString(R.string.firmware_install_warning, "(unknown)")); + installActivity.setInstallItem(installItem); + installActivity.setInstallEnabled(true); } @Override public void onStartInstall(final GBDevice device) { - // nothing to do + helper.unsetFwBytes(); // free up memory } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/miband8/XiaomiFWHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/miband8/XiaomiFWHelper.java new file mode 100644 index 000000000..bf5d7aa7a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/xiaomi/miband8/XiaomiFWHelper.java @@ -0,0 +1,123 @@ +/* Copyright (C) 2023 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.miband8; + +import android.content.Context; +import android.net.Uri; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; + +import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; +import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; +import nodomain.freeyourgadget.gadgetbridge.util.UriHelper; + +public class XiaomiFWHelper { + private static final Logger LOG = LoggerFactory.getLogger(XiaomiFWHelper.class); + + private final Uri uri; + private byte[] fw; + private boolean valid; + + private String id; + private String name; + + public XiaomiFWHelper(final Uri uri, final Context context) { + this.uri = uri; + + final UriHelper uriHelper; + try { + uriHelper = UriHelper.get(uri, context); + } catch (final IOException e) { + LOG.error("Failed to get uri helper for {}", uri, e); + return; + } + + final int maxExpectedFileSize = 1024 * 1024 * 128; // 64MB + + if (uriHelper.getFileSize() > maxExpectedFileSize) { + LOG.warn("Firmware size is larger than the maximum expected file size of {}", maxExpectedFileSize); + return; + } + + try (final InputStream in = new BufferedInputStream(uriHelper.openInputStream())) { + this.fw = FileUtils.readAll(in, maxExpectedFileSize); + } catch (final IOException e) { + LOG.error("Failed to read bytes from {}", uri, e); + return; + } + + valid = parseFirmware(); + } + + public boolean isValid() { + return valid; + } + + public String getDetails() { + return name != null ? name : "UNKNOWN WATCHFACE"; + } + + public byte[] getBytes() { + return fw; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public void unsetFwBytes() { + this.fw = null; + } + + private boolean parseFirmware() { + if (fw[0] != (byte) 0x5A || fw[1] != (byte) 0xA5) { + LOG.warn("File header not a watchface"); + return false; + } + + id = StringUtils.untilNullTerminator(fw, 0x28); + name = StringUtils.untilNullTerminator(fw, 0x68); + + if (id == null) { + LOG.warn("id not found in {}", uri); + return false; + } + + if (name == null) { + LOG.warn("name not found in {}", uri); + return false; + } + + try { + Integer.parseInt(id); + } catch (final Exception e) { + LOG.warn("Id {} not a number", id); + return false; + } + + return true; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiCharacteristic.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiCharacteristic.java index 5780a99ce..fe7bcc1ed 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiCharacteristic.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiCharacteristic.java @@ -35,15 +35,16 @@ import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.util.GB; public class XiaomiCharacteristic { + private final Logger LOG = LoggerFactory.getLogger(XiaomiCharacteristic.class); + public static final byte[] PAYLOAD_ACK = new byte[]{0, 0, 3, 0}; // max chunk size, including headers public static final int MAX_WRITE_SIZE = 242; - private final Logger LOG; - private final XiaomiSupport mSupport; private final BluetoothGattCharacteristic bluetoothGattCharacteristic; @@ -52,6 +53,7 @@ public class XiaomiCharacteristic { // Encryption private final XiaomiAuthService authService; private boolean isEncrypted; + public boolean incrementNonce = true; private short encryptedIndex = 0; // Chunking @@ -68,6 +70,8 @@ public class XiaomiCharacteristic { private Handler handler = null; + private SendCallback callback; + public XiaomiCharacteristic(final XiaomiSupport support, final BluetoothGattCharacteristic bluetoothGattCharacteristic, @Nullable final XiaomiAuthService authService) { @@ -75,7 +79,6 @@ public class XiaomiCharacteristic { this.bluetoothGattCharacteristic = bluetoothGattCharacteristic; this.authService = authService; this.isEncrypted = authService != null; - this.LOG = LoggerFactory.getLogger("XiaomiCharacteristic [" + bluetoothGattCharacteristic.getUuid().toString() + "]"); this.characteristicUUID = bluetoothGattCharacteristic.getUuid(); } @@ -87,10 +90,18 @@ public class XiaomiCharacteristic { this.handler = handler; } + public void setCallback(final SendCallback callback) { + this.callback = callback; + } + public void setEncrypted(final boolean encrypted) { this.isEncrypted = encrypted; } + public void setIncrementNonce(final boolean incrementNonce) { + this.incrementNonce = incrementNonce; + } + public void reset() { this.numChunks = 0; this.currentChunk = 0; @@ -127,6 +138,9 @@ public class XiaomiCharacteristic { LOG.debug("Got ack"); currentPayload = null; waitingAck = false; + if (callback != null) { + callback.onSend(payloadQueue.size()); + } sendNext(null); return; } @@ -185,6 +199,9 @@ public class XiaomiCharacteristic { LOG.debug("Got chunked ack end"); currentPayload = null; sendingChunked = false; + if (callback != null) { + callback.onSend(payloadQueue.size()); + } sendNext(null); return; case 1: @@ -207,6 +224,9 @@ public class XiaomiCharacteristic { LOG.warn("Got chunked nack for {}", currentPayload.getTaskName()); currentPayload = null; sendingChunked = false; + if (callback != null) { + callback.onSend(payloadQueue.size()); + } sendNext(null); return; } @@ -251,6 +271,8 @@ public class XiaomiCharacteristic { return; } + LOG.debug("Will send {}", GB.hexdump(currentPayload.getBytesToSend())); + final boolean encrypt = isEncrypted && authService.isEncryptionInitialized(); if (encrypt) { @@ -262,10 +284,13 @@ public class XiaomiCharacteristic { // Prepend encrypted index for the nonce currentPayload.setBytesToSend( ByteBuffer.allocate(2 + currentPayload.getBytesToSend().length).order(ByteOrder.LITTLE_ENDIAN) - .putShort(encryptedIndex++) + .putShort(encryptedIndex) .put(currentPayload.getBytesToSend()) .array() ); + if (incrementNonce) { + encryptedIndex++; + } } LOG.debug("Sending {} - chunked", currentPayload.getTaskName()); @@ -294,7 +319,10 @@ public class XiaomiCharacteristic { buf.put((byte) 2); // 2 for command buf.put((byte) (encrypt ? 1 : 2)); if (encrypt) { - buf.putShort(encryptedIndex++); + buf.putShort(encryptedIndex); + if (incrementNonce) { + encryptedIndex++; + } } buf.put(currentPayload.getBytesToSend()); // it's already encrypted @@ -364,4 +392,8 @@ public class XiaomiCharacteristic { return bytesToSend != null ? bytesToSend : bytes; } } + + public interface SendCallback { + void onSend(int remaining); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSupport.java index d590b689e..3a2b52562 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSupport.java @@ -52,6 +52,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.AbstractXiaomiService; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiCalendarService; +import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiDataUploadService; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiHealthService; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiMusicService; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiNotificationService; @@ -79,6 +80,7 @@ public abstract class XiaomiSupport extends AbstractBTLEDeviceSupport { protected final XiaomiSystemService systemService = new XiaomiSystemService(this); protected final XiaomiCalendarService calendarService = new XiaomiCalendarService(this); protected final XiaomiWatchfaceService watchfaceService = new XiaomiWatchfaceService(this); + protected final XiaomiDataUploadService dataUploadService = new XiaomiDataUploadService(this); private String mFirmwareVersion = null; @@ -92,6 +94,7 @@ public abstract class XiaomiSupport extends AbstractBTLEDeviceSupport { put(XiaomiSystemService.COMMAND_TYPE, systemService); put(XiaomiCalendarService.COMMAND_TYPE, calendarService); put(XiaomiWatchfaceService.COMMAND_TYPE, watchfaceService); + put(XiaomiDataUploadService.COMMAND_TYPE, dataUploadService); }}; public XiaomiSupport() { @@ -133,6 +136,8 @@ public abstract class XiaomiSupport extends AbstractBTLEDeviceSupport { this.characteristicActivityData.setEncrypted(isEncrypted()); this.characteristicDataUpload = new XiaomiCharacteristic(this, btCharacteristicDataUpload, authService); this.characteristicDataUpload.setEncrypted(isEncrypted()); + this.characteristicDataUpload.setIncrementNonce(false); + this.dataUploadService.setDataUploadCharacteristic(this.characteristicDataUpload); builder.requestMtu(247); @@ -314,6 +319,7 @@ public abstract class XiaomiSupport extends AbstractBTLEDeviceSupport { @Override public void onInstallApp(final Uri uri) { + // TODO distinguish between fw and watchface watchfaceService.installWatchface(uri); } @@ -442,4 +448,8 @@ public abstract class XiaomiSupport extends AbstractBTLEDeviceSupport { .build() ); } + + public XiaomiDataUploadService getDataUploader() { + return this.dataUploadService; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiDataUploadService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiDataUploadService.java new file mode 100644 index 000000000..8be61474c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiDataUploadService.java @@ -0,0 +1,182 @@ +/* Copyright (C) 2023 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services; + +import androidx.annotation.Nullable; + +import com.google.protobuf.ByteString; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Objects; + +import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto; +import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; +import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiCharacteristic; +import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport; +import nodomain.freeyourgadget.gadgetbridge.util.CheckSums; + +public class XiaomiDataUploadService extends AbstractXiaomiService { + private static final Logger LOG = LoggerFactory.getLogger(XiaomiDataUploadService.class); + + public static final int COMMAND_TYPE = 22; + + public static final int CMD_UPLOAD_START = 0; + + public static final byte TYPE_WATCHFACE = 16; + public static final byte TYPE_FIRMWARE = 32; + public static final byte TYPE_NOTIFICATION_ICON = 50; + + private XiaomiCharacteristic characteristic; + private Callback callback; + + private byte currentType; + private byte[] currentBytes; + + public XiaomiDataUploadService(final XiaomiSupport support) { + super(support); + } + + @Override + public void handleCommand(final XiaomiProto.Command cmd) { + switch (cmd.getSubtype()) { + case CMD_UPLOAD_START: + final XiaomiProto.DataUploadAck dataUploadAck = cmd.getDataUpload().getDataUploadAck(); + LOG.debug("Got upload start, unknown2={}, unknown4={}", dataUploadAck.getUnknown2(), dataUploadAck.getUnknown4()); + + if (dataUploadAck.getUnknown2() != 0 || dataUploadAck.getUnknown4() != 0) { + LOG.warn("Unexpected response"); + this.currentType = 0; + this.currentBytes = null; + return; + } + doUpload(currentType, currentBytes); + return; + } + + LOG.warn("Unknown data upload command {}", cmd.getSubtype()); + } + + public void setCallback(@Nullable final Callback callback) { + this.callback = callback; + } + + public void requestUpload(final byte type, final byte[] bytes) { + LOG.debug("Requesting upload for {} bytes of type {}", bytes.length, type); + + this.currentType = type; + this.currentBytes = bytes; + + getSupport().sendCommand( + "request upload", + XiaomiProto.Command.newBuilder() + .setType(COMMAND_TYPE) + .setSubtype(CMD_UPLOAD_START) + .setDataUpload(XiaomiProto.DataUpload.newBuilder().setDataUploadRequest( + XiaomiProto.DataUploadRequest.newBuilder() + .setType(type) + .setMd5Sum(ByteString.copyFrom(Objects.requireNonNull(CheckSums.md5(bytes)))) + .setSize(bytes.length) + )) + .build() + ); + } + + private void doUpload(final short type, final byte[] bytes) { + LOG.debug("Doing upload for {} bytes of type {}", bytes.length, type); + + // type + md5 + size + bytes + crc32 + final ByteBuffer buf1 = ByteBuffer.allocate(2 + 16 + 4 + bytes.length).order(ByteOrder.LITTLE_ENDIAN); + final byte[] md5 = CheckSums.md5(bytes); + if (md5 == null) { + onUploadFinish(false); + return; + } + + buf1.put((byte) 0); + buf1.put((byte) type); + buf1.put(md5); + buf1.putInt(bytes.length); + buf1.put(bytes); + + final ByteBuffer buf2 = ByteBuffer.allocate(buf1.capacity() + 4).order(ByteOrder.LITTLE_ENDIAN); + buf2.put(buf1.array()); + buf2.putInt(CheckSums.getCRC32(buf1.array())); + + final byte[] payload = buf2.array(); + final int partSize = 2044; // 2 + 2 at beginning of each for total and progress + final int totalParts = (int) Math.ceil(payload.length / (float) partSize); + + characteristic.setCallback(remainingParts -> { + final int totalBytes = totalParts * 4 + payload.length; + int progressBytes = totalParts * 4 + payload.length; + if (remainingParts > 1) { + progressBytes -= (remainingParts - 1) * partSize; + } + if (remainingParts > 0) { + progressBytes -= (payload.length % partSize); + } + + final int progressPercent = Math.round((100.0f * progressBytes) / totalBytes); + + LOG.debug("Data upload progress: {} parts remaining ({}%)", remainingParts, progressPercent); + + if (remainingParts > 0) { + if (callback != null) { + callback.onUploadProgress(progressPercent); + } + } else { + onUploadFinish(true); + } + }); + + for (int i = 0; i * partSize < payload.length; i++) { + final int startIndex = i * partSize; + final int endIndex = Math.min((i + 1) * partSize, payload.length); + LOG.debug("Uploading part {} of {}, from {} to {}", (i + 1), totalParts, startIndex, endIndex); + final byte[] chunkToSend = new byte[4 + endIndex - startIndex]; + BLETypeConversions.writeUint16(chunkToSend, 0, totalParts); + BLETypeConversions.writeUint16(chunkToSend, 2, i + 1); + System.arraycopy(payload, startIndex, chunkToSend, 4, endIndex - startIndex); + characteristic.write("upload part " + (i + 1) + " of " + totalParts, chunkToSend); + } + } + + public void setDataUploadCharacteristic(final XiaomiCharacteristic characteristic) { + this.characteristic = characteristic; + } + + private void onUploadFinish(final boolean success) { + this.currentType = 0; + this.currentBytes = null; + + if (callback != null) { + callback.onUploadFinish(success); + } + + characteristic.setCallback(null); + } + + public interface Callback { + void onUploadFinish(boolean success); + + void onUploadProgress(int progress); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiWatchfaceService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiWatchfaceService.java index 5c41dcdd3..537afd4c3 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiWatchfaceService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/services/XiaomiWatchfaceService.java @@ -27,14 +27,18 @@ import java.util.List; import java.util.Set; import java.util.UUID; +import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppInfo; +import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.miband8.XiaomiFWHelper; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp; import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetProgressAction; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport; -import nodomain.freeyourgadget.gadgetbridge.util.Prefs; +import nodomain.freeyourgadget.gadgetbridge.util.GB; -public class XiaomiWatchfaceService extends AbstractXiaomiService { +public class XiaomiWatchfaceService extends AbstractXiaomiService implements XiaomiDataUploadService.Callback { private static final Logger LOG = LoggerFactory.getLogger(XiaomiWatchfaceService.class); public static final int COMMAND_TYPE = 4; @@ -48,6 +52,9 @@ public class XiaomiWatchfaceService extends AbstractXiaomiService { private final Set userWatchfaces = new HashSet<>(); private UUID activeWatchface = null; + // Not null if we're installing a firmware + private XiaomiFWHelper fwHelper = null; + public XiaomiWatchfaceService(final XiaomiSupport support) { super(support); } @@ -69,18 +76,22 @@ public class XiaomiWatchfaceService extends AbstractXiaomiService { requestWatchfaceList(); return; case CMD_WATCHFACE_INSTALL: + final int installStatus = cmd.getWatchface().getInstallStatus(); + if (installStatus != 0) { + LOG.warn("Invalid watchface install status {} for {}", installStatus, fwHelper.getId()); + return; + } + + LOG.debug("Watchface install status 0, uploading"); + setDeviceBusy(); + getSupport().getDataUploader().setCallback(this); + getSupport().getDataUploader().requestUpload(XiaomiDataUploadService.TYPE_WATCHFACE, fwHelper.getBytes()); return; } LOG.warn("Unknown watchface command {}", cmd.getSubtype()); } - @Override - public boolean onSendConfiguration(final String config, final Prefs prefs) { - // TODO set watchface - return super.onSendConfiguration(config, prefs); - } - public void requestWatchfaceList() { getSupport().sendCommand("request watchface list", COMMAND_TYPE, CMD_WATCHFACE_LIST); } @@ -120,21 +131,24 @@ public class XiaomiWatchfaceService extends AbstractXiaomiService { } public void setWatchface(final UUID uuid) { - if (!allWatchfaces.contains(uuid)) { - LOG.warn("Unknown watchface {}", uuid); - return; - } + final String id = toWatchfaceId(uuid); + + // TODO for now we need to allow when installing a watchface + //if (!allWatchfaces.contains(uuid)) { + // LOG.warn("Unknown watchface {}", uuid); + // return; + //} activeWatchface = uuid; - LOG.debug("Set watchface to {}", uuid); + LOG.debug("Set watchface to {}", id); getSupport().sendCommand( "set watchface to " + uuid, XiaomiProto.Command.newBuilder() .setType(COMMAND_TYPE) .setSubtype(CMD_WATCHFACE_SET) - .setWatchface(XiaomiProto.Watchface.newBuilder().setWatchfaceId(toWatchfaceId(uuid))) + .setWatchface(XiaomiProto.Watchface.newBuilder().setWatchfaceId(id)) .build() ); } @@ -144,42 +158,58 @@ public class XiaomiWatchfaceService extends AbstractXiaomiService { } public void deleteWatchface(final UUID uuid) { + final String id = toWatchfaceId(uuid); + if (!userWatchfaces.contains(uuid)) { - LOG.warn("Refusing to delete non-user watchface {}", uuid); + LOG.warn("Refusing to delete non-user watchface {}", id); return; } if (!allWatchfaces.contains(uuid)) { - LOG.warn("Refusing to delete unknown watchface {}", uuid); + LOG.warn("Refusing to delete unknown watchface {}", id); return; } if (uuid.equals(activeWatchface)) { - LOG.warn("Refusing to delete active watchface {}", uuid); + LOG.warn("Refusing to delete active watchface {}", id); return; } - LOG.debug("Delete watchface {}", uuid); + LOG.debug("Delete watchface {}", id); allWatchfaces.remove(uuid); userWatchfaces.remove(uuid); getSupport().sendCommand( - "delete watchface " + uuid, + "delete watchface " + id, XiaomiProto.Command.newBuilder() .setType(COMMAND_TYPE) .setSubtype(CMD_WATCHFACE_DELETE) - .setWatchface(XiaomiProto.Watchface.newBuilder().setWatchfaceId(toWatchfaceId(uuid))) + .setWatchface(XiaomiProto.Watchface.newBuilder().setWatchfaceId(id)) .build() ); } - public void deleteWatchface(final String watchfaceId) { - deleteWatchface(toWatchfaceUUID(watchfaceId)); - } - public void installWatchface(final Uri uri) { - // TODO + fwHelper = new XiaomiFWHelper(uri, getSupport().getContext()); + if (!fwHelper.isValid()) { + fwHelper = null; + LOG.warn("watchface is not valid"); + return; + } + + getSupport().sendCommand( + "install watchface " + fwHelper.getId(), + XiaomiProto.Command.newBuilder() + .setType(COMMAND_TYPE) + .setSubtype(CMD_WATCHFACE_INSTALL) + .setWatchface(XiaomiProto.Watchface.newBuilder().setWatchfaceInstallStart( + XiaomiProto.WatchfaceInstallStart.newBuilder() + .setId(fwHelper.getId()) + .setSize(fwHelper.getBytes().length) + )) + .build() + ); } public static UUID toWatchfaceUUID(final String id) { @@ -201,4 +231,58 @@ public class XiaomiWatchfaceService extends AbstractXiaomiService { .replaceAll("f", "") .replaceAll("F", ""); } + + @Override + public void onUploadFinish(final boolean success) { + LOG.debug("Watchface upload finished: {}", success); + + getSupport().getDataUploader().setCallback(null); + + final String notificationMessage = success ? + getSupport().getContext().getString(R.string.updatefirmwareoperation_update_complete) : + getSupport().getContext().getString(R.string.updatefirmwareoperation_write_failed); + + GB.updateInstallNotification(notificationMessage, false, 100, getSupport().getContext()); + + unsetDeviceBusy(); + + if (success) { + setWatchface(fwHelper.getId()); + } + + fwHelper = null; + } + + @Override + public void onUploadProgress(final int progressPercent) { + try { + final TransactionBuilder builder = getSupport().createTransactionBuilder("send data upload progress"); + builder.add(new SetProgressAction( + getSupport().getContext().getString(R.string.updatefirmwareoperation_update_in_progress), + true, + progressPercent, + getSupport().getContext() + )); + builder.queue(getSupport().getQueue()); + } catch (final Exception e) { + LOG.error("Failed to update progress notification", e); + } + } + + private void setDeviceBusy() { + final GBDevice device = getSupport().getDevice(); + device.setBusyTask(getSupport().getContext().getString(R.string.updating_firmware)); + device.sendDeviceUpdateIntent(getSupport().getContext()); + } + + private void unsetDeviceBusy() { + final GBDevice device = getSupport().getDevice(); + if (device != null && device.isConnected()) { + if (device.isBusy()) { + device.unsetBusyTask(); + device.sendDeviceUpdateIntent(getSupport().getContext()); + } + device.sendDeviceUpdateIntent(getSupport().getContext()); + } + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CheckSums.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CheckSums.java index c7455beda..4299eca1c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CheckSums.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CheckSums.java @@ -16,13 +16,22 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.util; +import androidx.annotation.Nullable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.zip.CRC32; public class CheckSums { + private static final Logger LOG = LoggerFactory.getLogger(CheckSums.class); + public static int getCRC8(byte[] seq) { int len = seq.length; int i = 0; @@ -152,4 +161,17 @@ public class CheckSums { return 65535 & i2; } + + @Nullable + public static byte[] md5(final byte[] data) { + final MessageDigest md; + try { + md = MessageDigest.getInstance("MD5"); + } catch (final NoSuchAlgorithmException e) { + LOG.error("Failed to get md5 digest", e); + return null; + } + md.update(data); + return md.digest(); + } }