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();
+ }
}