1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-12-27 02:55:50 +01:00

Xiaomi: Watchface upload (wip, does not work)

This commit is contained in:
José Rebelo 2023-10-22 22:41:56 +01:00
parent 82a264cd65
commit c47e830056
7 changed files with 514 additions and 37 deletions

View File

@ -16,40 +16,64 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */ along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.xiaomi; package nodomain.freeyourgadget.gadgetbridge.devices.xiaomi;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiFirmwareType.AGPS_UIHH;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity; import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.miband8.XiaomiFWHelper;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem; import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
public class XiaomiInstallHandler implements InstallHandler { public class XiaomiInstallHandler implements InstallHandler {
protected final Uri mUri; protected final Uri mUri;
protected final Context mContext; protected final Context mContext;
protected final XiaomiFWHelper helper;
public XiaomiInstallHandler(final Uri uri, final Context context) { public XiaomiInstallHandler(final Uri uri, final Context context) {
this.mUri = uri; this.mUri = uri;
this.mContext = context; this.mContext = context;
this.helper = new XiaomiFWHelper(uri, context);
} }
@Override @Override
public boolean isValid() { public boolean isValid() {
// TODO return helper.isValid();
return false;
} }
@Override @Override
public void validateInstallation(final InstallActivity installActivity, final GBDevice device) { 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 @Override
public void onStartInstall(final GBDevice device) { public void onStartInstall(final GBDevice device) {
// nothing to do helper.unsetFwBytes(); // free up memory
} }
} }

View File

@ -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;
}
}

View File

@ -35,15 +35,16 @@ import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class XiaomiCharacteristic { public class XiaomiCharacteristic {
private final Logger LOG = LoggerFactory.getLogger(XiaomiCharacteristic.class);
public static final byte[] PAYLOAD_ACK = new byte[]{0, 0, 3, 0}; public static final byte[] PAYLOAD_ACK = new byte[]{0, 0, 3, 0};
// max chunk size, including headers // max chunk size, including headers
public static final int MAX_WRITE_SIZE = 242; public static final int MAX_WRITE_SIZE = 242;
private final Logger LOG;
private final XiaomiSupport mSupport; private final XiaomiSupport mSupport;
private final BluetoothGattCharacteristic bluetoothGattCharacteristic; private final BluetoothGattCharacteristic bluetoothGattCharacteristic;
@ -52,6 +53,7 @@ public class XiaomiCharacteristic {
// Encryption // Encryption
private final XiaomiAuthService authService; private final XiaomiAuthService authService;
private boolean isEncrypted; private boolean isEncrypted;
public boolean incrementNonce = true;
private short encryptedIndex = 0; private short encryptedIndex = 0;
// Chunking // Chunking
@ -68,6 +70,8 @@ public class XiaomiCharacteristic {
private Handler handler = null; private Handler handler = null;
private SendCallback callback;
public XiaomiCharacteristic(final XiaomiSupport support, public XiaomiCharacteristic(final XiaomiSupport support,
final BluetoothGattCharacteristic bluetoothGattCharacteristic, final BluetoothGattCharacteristic bluetoothGattCharacteristic,
@Nullable final XiaomiAuthService authService) { @Nullable final XiaomiAuthService authService) {
@ -75,7 +79,6 @@ public class XiaomiCharacteristic {
this.bluetoothGattCharacteristic = bluetoothGattCharacteristic; this.bluetoothGattCharacteristic = bluetoothGattCharacteristic;
this.authService = authService; this.authService = authService;
this.isEncrypted = authService != null; this.isEncrypted = authService != null;
this.LOG = LoggerFactory.getLogger("XiaomiCharacteristic [" + bluetoothGattCharacteristic.getUuid().toString() + "]");
this.characteristicUUID = bluetoothGattCharacteristic.getUuid(); this.characteristicUUID = bluetoothGattCharacteristic.getUuid();
} }
@ -87,10 +90,18 @@ public class XiaomiCharacteristic {
this.handler = handler; this.handler = handler;
} }
public void setCallback(final SendCallback callback) {
this.callback = callback;
}
public void setEncrypted(final boolean encrypted) { public void setEncrypted(final boolean encrypted) {
this.isEncrypted = encrypted; this.isEncrypted = encrypted;
} }
public void setIncrementNonce(final boolean incrementNonce) {
this.incrementNonce = incrementNonce;
}
public void reset() { public void reset() {
this.numChunks = 0; this.numChunks = 0;
this.currentChunk = 0; this.currentChunk = 0;
@ -127,6 +138,9 @@ public class XiaomiCharacteristic {
LOG.debug("Got ack"); LOG.debug("Got ack");
currentPayload = null; currentPayload = null;
waitingAck = false; waitingAck = false;
if (callback != null) {
callback.onSend(payloadQueue.size());
}
sendNext(null); sendNext(null);
return; return;
} }
@ -185,6 +199,9 @@ public class XiaomiCharacteristic {
LOG.debug("Got chunked ack end"); LOG.debug("Got chunked ack end");
currentPayload = null; currentPayload = null;
sendingChunked = false; sendingChunked = false;
if (callback != null) {
callback.onSend(payloadQueue.size());
}
sendNext(null); sendNext(null);
return; return;
case 1: case 1:
@ -207,6 +224,9 @@ public class XiaomiCharacteristic {
LOG.warn("Got chunked nack for {}", currentPayload.getTaskName()); LOG.warn("Got chunked nack for {}", currentPayload.getTaskName());
currentPayload = null; currentPayload = null;
sendingChunked = false; sendingChunked = false;
if (callback != null) {
callback.onSend(payloadQueue.size());
}
sendNext(null); sendNext(null);
return; return;
} }
@ -251,6 +271,8 @@ public class XiaomiCharacteristic {
return; return;
} }
LOG.debug("Will send {}", GB.hexdump(currentPayload.getBytesToSend()));
final boolean encrypt = isEncrypted && authService.isEncryptionInitialized(); final boolean encrypt = isEncrypted && authService.isEncryptionInitialized();
if (encrypt) { if (encrypt) {
@ -262,10 +284,13 @@ public class XiaomiCharacteristic {
// Prepend encrypted index for the nonce // Prepend encrypted index for the nonce
currentPayload.setBytesToSend( currentPayload.setBytesToSend(
ByteBuffer.allocate(2 + currentPayload.getBytesToSend().length).order(ByteOrder.LITTLE_ENDIAN) ByteBuffer.allocate(2 + currentPayload.getBytesToSend().length).order(ByteOrder.LITTLE_ENDIAN)
.putShort(encryptedIndex++) .putShort(encryptedIndex)
.put(currentPayload.getBytesToSend()) .put(currentPayload.getBytesToSend())
.array() .array()
); );
if (incrementNonce) {
encryptedIndex++;
}
} }
LOG.debug("Sending {} - chunked", currentPayload.getTaskName()); LOG.debug("Sending {} - chunked", currentPayload.getTaskName());
@ -294,7 +319,10 @@ public class XiaomiCharacteristic {
buf.put((byte) 2); // 2 for command buf.put((byte) 2); // 2 for command
buf.put((byte) (encrypt ? 1 : 2)); buf.put((byte) (encrypt ? 1 : 2));
if (encrypt) { if (encrypt) {
buf.putShort(encryptedIndex++); buf.putShort(encryptedIndex);
if (incrementNonce) {
encryptedIndex++;
}
} }
buf.put(currentPayload.getBytesToSend()); // it's already encrypted buf.put(currentPayload.getBytesToSend()); // it's already encrypted
@ -364,4 +392,8 @@ public class XiaomiCharacteristic {
return bytesToSend != null ? bytesToSend : bytes; return bytesToSend != null ? bytesToSend : bytes;
} }
} }
public interface SendCallback {
void onSend(int remaining);
}
} }

View File

@ -52,6 +52,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; 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.AbstractXiaomiService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiCalendarService; 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.XiaomiHealthService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiMusicService; import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiMusicService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiNotificationService; 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 XiaomiSystemService systemService = new XiaomiSystemService(this);
protected final XiaomiCalendarService calendarService = new XiaomiCalendarService(this); protected final XiaomiCalendarService calendarService = new XiaomiCalendarService(this);
protected final XiaomiWatchfaceService watchfaceService = new XiaomiWatchfaceService(this); protected final XiaomiWatchfaceService watchfaceService = new XiaomiWatchfaceService(this);
protected final XiaomiDataUploadService dataUploadService = new XiaomiDataUploadService(this);
private String mFirmwareVersion = null; private String mFirmwareVersion = null;
@ -92,6 +94,7 @@ public abstract class XiaomiSupport extends AbstractBTLEDeviceSupport {
put(XiaomiSystemService.COMMAND_TYPE, systemService); put(XiaomiSystemService.COMMAND_TYPE, systemService);
put(XiaomiCalendarService.COMMAND_TYPE, calendarService); put(XiaomiCalendarService.COMMAND_TYPE, calendarService);
put(XiaomiWatchfaceService.COMMAND_TYPE, watchfaceService); put(XiaomiWatchfaceService.COMMAND_TYPE, watchfaceService);
put(XiaomiDataUploadService.COMMAND_TYPE, dataUploadService);
}}; }};
public XiaomiSupport() { public XiaomiSupport() {
@ -133,6 +136,8 @@ public abstract class XiaomiSupport extends AbstractBTLEDeviceSupport {
this.characteristicActivityData.setEncrypted(isEncrypted()); this.characteristicActivityData.setEncrypted(isEncrypted());
this.characteristicDataUpload = new XiaomiCharacteristic(this, btCharacteristicDataUpload, authService); this.characteristicDataUpload = new XiaomiCharacteristic(this, btCharacteristicDataUpload, authService);
this.characteristicDataUpload.setEncrypted(isEncrypted()); this.characteristicDataUpload.setEncrypted(isEncrypted());
this.characteristicDataUpload.setIncrementNonce(false);
this.dataUploadService.setDataUploadCharacteristic(this.characteristicDataUpload);
builder.requestMtu(247); builder.requestMtu(247);
@ -314,6 +319,7 @@ public abstract class XiaomiSupport extends AbstractBTLEDeviceSupport {
@Override @Override
public void onInstallApp(final Uri uri) { public void onInstallApp(final Uri uri) {
// TODO distinguish between fw and watchface
watchfaceService.installWatchface(uri); watchfaceService.installWatchface(uri);
} }
@ -442,4 +448,8 @@ public abstract class XiaomiSupport extends AbstractBTLEDeviceSupport {
.build() .build()
); );
} }
public XiaomiDataUploadService getDataUploader() {
return this.dataUploadService;
}
} }

View File

@ -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);
}
}

View File

@ -27,14 +27,18 @@ import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppInfo; 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.impl.GBDeviceApp;
import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto; import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; 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.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); private static final Logger LOG = LoggerFactory.getLogger(XiaomiWatchfaceService.class);
public static final int COMMAND_TYPE = 4; public static final int COMMAND_TYPE = 4;
@ -48,6 +52,9 @@ public class XiaomiWatchfaceService extends AbstractXiaomiService {
private final Set<UUID> userWatchfaces = new HashSet<>(); private final Set<UUID> userWatchfaces = new HashSet<>();
private UUID activeWatchface = null; private UUID activeWatchface = null;
// Not null if we're installing a firmware
private XiaomiFWHelper fwHelper = null;
public XiaomiWatchfaceService(final XiaomiSupport support) { public XiaomiWatchfaceService(final XiaomiSupport support) {
super(support); super(support);
} }
@ -69,18 +76,22 @@ public class XiaomiWatchfaceService extends AbstractXiaomiService {
requestWatchfaceList(); requestWatchfaceList();
return; return;
case CMD_WATCHFACE_INSTALL: 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; return;
} }
LOG.warn("Unknown watchface command {}", cmd.getSubtype()); 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() { public void requestWatchfaceList() {
getSupport().sendCommand("request watchface list", COMMAND_TYPE, CMD_WATCHFACE_LIST); 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) { public void setWatchface(final UUID uuid) {
if (!allWatchfaces.contains(uuid)) { final String id = toWatchfaceId(uuid);
LOG.warn("Unknown watchface {}", uuid);
return; // TODO for now we need to allow when installing a watchface
} //if (!allWatchfaces.contains(uuid)) {
// LOG.warn("Unknown watchface {}", uuid);
// return;
//}
activeWatchface = uuid; activeWatchface = uuid;
LOG.debug("Set watchface to {}", uuid); LOG.debug("Set watchface to {}", id);
getSupport().sendCommand( getSupport().sendCommand(
"set watchface to " + uuid, "set watchface to " + uuid,
XiaomiProto.Command.newBuilder() XiaomiProto.Command.newBuilder()
.setType(COMMAND_TYPE) .setType(COMMAND_TYPE)
.setSubtype(CMD_WATCHFACE_SET) .setSubtype(CMD_WATCHFACE_SET)
.setWatchface(XiaomiProto.Watchface.newBuilder().setWatchfaceId(toWatchfaceId(uuid))) .setWatchface(XiaomiProto.Watchface.newBuilder().setWatchfaceId(id))
.build() .build()
); );
} }
@ -144,42 +158,58 @@ public class XiaomiWatchfaceService extends AbstractXiaomiService {
} }
public void deleteWatchface(final UUID uuid) { public void deleteWatchface(final UUID uuid) {
final String id = toWatchfaceId(uuid);
if (!userWatchfaces.contains(uuid)) { if (!userWatchfaces.contains(uuid)) {
LOG.warn("Refusing to delete non-user watchface {}", uuid); LOG.warn("Refusing to delete non-user watchface {}", id);
return; return;
} }
if (!allWatchfaces.contains(uuid)) { if (!allWatchfaces.contains(uuid)) {
LOG.warn("Refusing to delete unknown watchface {}", uuid); LOG.warn("Refusing to delete unknown watchface {}", id);
return; return;
} }
if (uuid.equals(activeWatchface)) { if (uuid.equals(activeWatchface)) {
LOG.warn("Refusing to delete active watchface {}", uuid); LOG.warn("Refusing to delete active watchface {}", id);
return; return;
} }
LOG.debug("Delete watchface {}", uuid); LOG.debug("Delete watchface {}", id);
allWatchfaces.remove(uuid); allWatchfaces.remove(uuid);
userWatchfaces.remove(uuid); userWatchfaces.remove(uuid);
getSupport().sendCommand( getSupport().sendCommand(
"delete watchface " + uuid, "delete watchface " + id,
XiaomiProto.Command.newBuilder() XiaomiProto.Command.newBuilder()
.setType(COMMAND_TYPE) .setType(COMMAND_TYPE)
.setSubtype(CMD_WATCHFACE_DELETE) .setSubtype(CMD_WATCHFACE_DELETE)
.setWatchface(XiaomiProto.Watchface.newBuilder().setWatchfaceId(toWatchfaceId(uuid))) .setWatchface(XiaomiProto.Watchface.newBuilder().setWatchfaceId(id))
.build() .build()
); );
} }
public void deleteWatchface(final String watchfaceId) { public void installWatchface(final Uri uri) {
deleteWatchface(toWatchfaceUUID(watchfaceId)); fwHelper = new XiaomiFWHelper(uri, getSupport().getContext());
if (!fwHelper.isValid()) {
fwHelper = null;
LOG.warn("watchface is not valid");
return;
} }
public void installWatchface(final Uri uri) { getSupport().sendCommand(
// TODO "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) { public static UUID toWatchfaceUUID(final String id) {
@ -201,4 +231,58 @@ public class XiaomiWatchfaceService extends AbstractXiaomiService {
.replaceAll("f", "") .replaceAll("f", "")
.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());
}
}
} }

View File

@ -16,13 +16,22 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */ along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.util; package nodomain.freeyourgadget.gadgetbridge.util;
import androidx.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.zip.CRC32; import java.util.zip.CRC32;
public class CheckSums { public class CheckSums {
private static final Logger LOG = LoggerFactory.getLogger(CheckSums.class);
public static int getCRC8(byte[] seq) { public static int getCRC8(byte[] seq) {
int len = seq.length; int len = seq.length;
int i = 0; int i = 0;
@ -152,4 +161,17 @@ public class CheckSums {
return 65535 & i2; 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();
}
} }