mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-12-26 10:35:50 +01:00
Xiaomi: Watchface upload (wip, does not work)
This commit is contained in:
parent
82a264cd65
commit
c47e830056
@ -16,40 +16,64 @@
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -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 <http://www.gnu.org/licenses/>. */
|
||||
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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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 <http://www.gnu.org/licenses/>. */
|
||||
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);
|
||||
}
|
||||
}
|
@ -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<UUID> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,13 +16,22 @@
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user