From 6dfc8953033383e046ba6f89385ff40ddba4c595 Mon Sep 17 00:00:00 2001 From: cpfeiffer Date: Sun, 11 Dec 2016 02:10:07 +0100 Subject: [PATCH] Mi2: Initial work on firmware update #427 --- .../miband/AbstractMiBandFWHelper.java | 100 +++++++ .../AbstractMiBandFWInstallHandler.java | 103 +++++++ .../devices/miband/MiBand2Coordinator.java | 12 +- .../devices/miband/MiBand2Service.java | 8 +- .../devices/miband/MiBandFWHelper.java | 81 ++---- .../miband/MiBandFWInstallHandler.java | 80 +----- .../devices/miband2/MiBand2FWHelper.java | 70 +++++ .../miband2/MiBand2FWInstallHandler.java | 32 +++ .../service/btle/BLETypeConversions.java | 18 ++ .../devices/miband/MiBand2Support.java | 13 +- .../operations/AbstractMiBandOperation.java | 2 +- .../devices/miband2/Mi2FirmwareInfo.java | 87 ++++++ .../operations/UpdateFirmwareOperation.java | 257 ++++++++++++++++++ .../gadgetbridge/util/ArrayUtils.java | 24 +- .../gadgetbridge/test/Tryout.java | 6 +- build.gradle | 2 +- 16 files changed, 746 insertions(+), 149 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/AbstractMiBandFWHelper.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/AbstractMiBandFWInstallHandler.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband2/MiBand2FWHelper.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband2/MiBand2FWInstallHandler.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/Mi2FirmwareInfo.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/UpdateFirmwareOperation.java diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/AbstractMiBandFWHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/AbstractMiBandFWHelper.java new file mode 100644 index 00000000..b328bafa --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/AbstractMiBandFWHelper.java @@ -0,0 +1,100 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.miband; + +import android.content.Context; +import android.net.Uri; +import android.support.annotation.NonNull; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; + +/** + * Also see Mi1SFirmwareInfo. + */ +public abstract class AbstractMiBandFWHelper { + private static final Logger LOG = LoggerFactory.getLogger(AbstractMiBandFWHelper.class); + + @NonNull + private final byte[] fw; + + public AbstractMiBandFWHelper(Uri uri, Context context) throws IOException { + String pebblePattern = ".*\\.(pbw|pbz|pbl)"; + if (uri.getPath().matches(pebblePattern)) { + throw new IOException("Firmware has a filename that looks like a Pebble app/firmware."); + } + + try (InputStream in = new BufferedInputStream(context.getContentResolver().openInputStream(uri))) { + this.fw = FileUtils.readAll(in, 1024 * 1024); // 1 MB + determineFirmwareInfo(fw); + } catch (IOException ex) { + throw ex; // pass through + } catch (IllegalArgumentException ex) { + throw new IOException("This doesn't seem to be a Mi Band firmware: " + ex.getLocalizedMessage(), ex); + } catch (Exception e) { + throw new IOException("Error reading firmware file: " + uri.toString(), e); + } + } + + public abstract int getFirmwareVersion(); + + public abstract int getFirmware2Version(); + + public static String formatFirmwareVersion(int version) { + if (version == -1) + return GBApplication.getContext().getString(R.string._unknown_); + + return String.format("%d.%d.%d.%d", + version >> 24 & 255, + version >> 16 & 255, + version >> 8 & 255, + version & 255); + } + + public String getHumanFirmwareVersion() { + return format(getFirmwareVersion()); + } + + public abstract String getHumanFirmwareVersion2(); + + public String format(int version) { + return formatFirmwareVersion(version); + } + + @NonNull + public byte[] getFw() { + return fw; + } + + public boolean isFirmwareWhitelisted() { + for (int wlf : getWhitelistedFirmwareVersions()) { + if (wlf == getFirmwareVersion()) { + return true; + } + } + return false; + } + + protected abstract int[] getWhitelistedFirmwareVersions(); + + public abstract boolean isFirmwareGenerallyCompatibleWith(GBDevice device); + + public abstract boolean isSingleFirmware(); + + /** + * @param wholeFirmwareBytes + * @return + * @throws IllegalArgumentException when the data is not recognized as firmware data + */ + @NonNull + protected abstract void determineFirmwareInfo(byte[] wholeFirmwareBytes); + + public abstract void checkValid() throws IllegalArgumentException; +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/AbstractMiBandFWInstallHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/AbstractMiBandFWInstallHandler.java new file mode 100644 index 00000000..977ad87f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/AbstractMiBandFWInstallHandler.java @@ -0,0 +1,103 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.miband; + +import android.content.Context; +import android.net.Uri; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity; +import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.GenericItem; + +public abstract class AbstractMiBandFWInstallHandler implements InstallHandler { + private static final Logger LOG = LoggerFactory.getLogger(AbstractMiBandFWInstallHandler.class); + + private final Context mContext; + private AbstractMiBandFWHelper helper; + private String errorMessage; + + public AbstractMiBandFWInstallHandler(Uri uri, Context context) { + mContext = context; + + try { + helper = createHelper(uri, context); + } catch (IOException e) { + errorMessage = e.getMessage(); + LOG.warn(errorMessage, e); + } + } + + protected abstract AbstractMiBandFWHelper createHelper(Uri uri, Context context) throws IOException; + + + @Override + public void validateInstallation(InstallActivity installActivity, GBDevice device) { + if (device.isBusy()) { + installActivity.setInfoText(device.getBusyTask()); + installActivity.setInstallEnabled(false); + return; + } + + if (!isSupportedDeviceType(device) || !device.isInitialized()) { + installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_ready)); + installActivity.setInstallEnabled(false); + return; + } + + try { + helper.checkValid(); + } catch (IllegalArgumentException ex) { + installActivity.setInfoText(ex.getLocalizedMessage()); + installActivity.setInstallEnabled(false); + return; + } + + GenericItem fwItem = new GenericItem(mContext.getString(R.string.miband_installhandler_miband_firmware, helper.getHumanFirmwareVersion())); + fwItem.setIcon(R.drawable.ic_device_miband); + + if (!helper.isFirmwareGenerallyCompatibleWith(device)) { + fwItem.setDetails(mContext.getString(R.string.miband_fwinstaller_incompatible_version)); + installActivity.setInfoText(mContext.getString(R.string.fwinstaller_firmware_not_compatible_to_device)); + installActivity.setInstallEnabled(false); + return; + } + StringBuilder builder = new StringBuilder(); + if (helper.isSingleFirmware()) { + builder.append(mContext.getString(R.string.fw_upgrade_notice, helper.getHumanFirmwareVersion())); + } else { + builder.append(mContext.getString(R.string.fw_multi_upgrade_notice, helper.getHumanFirmwareVersion(), helper.getHumanFirmwareVersion2())); + } + + + if (helper.isFirmwareWhitelisted()) { + builder.append(" ").append(mContext.getString(R.string.miband_firmware_known)); + fwItem.setDetails(mContext.getString(R.string.miband_fwinstaller_compatible_version)); + // TODO: set a CHECK (OKAY) button + } else { + builder.append(" ").append(mContext.getString(R.string.miband_firmware_unknown_warning)).append(" \n\n") + .append(mContext.getString(R.string.miband_firmware_suggest_whitelist, helper.getFirmwareVersion())); + fwItem.setDetails(mContext.getString(R.string.miband_fwinstaller_untested_version)); + // TODO: set a UNKNOWN (question mark) button + } + installActivity.setInfoText(builder.toString()); + installActivity.setInstallItem(fwItem); + installActivity.setInstallEnabled(true); + } + + protected abstract boolean isSupportedDeviceType(GBDevice device); + + @Override + public void onStartInstall(GBDevice device) { + + } + + @Override + public boolean isValid() { + return helper != null; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Coordinator.java index 5d40bfc1..01538e09 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Coordinator.java @@ -19,6 +19,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.miband2.MiBand2FWInstallHandler; import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample; import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; @@ -86,11 +87,6 @@ public class MiBand2Coordinator extends MiBandCoordinator { return new MiBand2SampleProvider(device, session); } - @Override - public InstallHandler findInstallHandler(Uri uri, Context context) { - return null; // not supported at the moment - } - public static DateTimeDisplay getDateDisplay(Context context) throws IllegalArgumentException { Prefs prefs = GBApplication.getPrefs(); String dateFormatTime = context.getString(R.string.p_dateformat_time); @@ -104,4 +100,10 @@ public class MiBand2Coordinator extends MiBandCoordinator { Prefs prefs = GBApplication.getPrefs(); return prefs.getBoolean(MiBandConst.PREF_MI2_ACTIVATE_DISPLAY_ON_LIFT, true); } + + @Override + public InstallHandler findInstallHandler(Uri uri, Context context) { + MiBand2FWInstallHandler handler = new MiBand2FWInstallHandler(uri, context); + return handler.isValid() ? handler : null; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Service.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Service.java index 3681bc49..9412868b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Service.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBand2Service.java @@ -12,7 +12,7 @@ public class MiBand2Service { public static final UUID UUID_SERVICE_MIBAND_SERVICE = UUID.fromString(String.format(BASE_UUID, "FEE0")); public static final UUID UUID_SERVICE_MIBAND2_SERVICE = UUID.fromString(String.format(BASE_UUID, "FEE1")); public static final UUID UUID_SERVICE_HEART_RATE = UUID.fromString(String.format(BASE_UUID, "180D")); - public static final UUID UUID_SERVICE_WEIGHT_SERVICE = UUID.fromString("00001530-0000-3512-2118-0009af100700"); + public static final UUID UUID_SERVICE_FIRMWARE_SERVICE = UUID.fromString("00001530-0000-3512-2118-0009af100700"); public static final UUID UUID_CHARACTERISTIC_FIRMWARE = UUID.fromString("00001531-0000-3512-2118-0009af100700"); public static final UUID UUID_CHARACTERISTIC_FIRMWARE_DATA = UUID.fromString("00001532-0000-3512-2118-0009af100700"); @@ -105,11 +105,11 @@ public class MiBand2Service { public static final byte COMMAND_ACTIVITY_DATA_START_DATE = 0x01; public static final byte COMMAND_ACTIVITY_DATA_XXX_DATE = 0x02; // issued on first connect, followd by COMMAND_XXXX_ACTIVITY_DATA instead of COMMAND_FETCH_ACTIVITY_DATA - public static final byte COMMAND_FIRMWARE_INIT = 0x01; // to UUID_CHARACTERISTIC_FIRMWARE + public static final byte COMMAND_FIRMWARE_INIT = 0x01; // to UUID_CHARACTERISTIC_FIRMWARE, followed by fw file size in bytes public static final byte COMMAND_FIRMWARE_START_DATA = 0x03; // to UUID_CHARACTERISTIC_FIRMWARE public static final byte COMMAND_FIRMWARE_UPDATE_SYNC = 0x00; // to UUID_CHARACTERISTIC_FIRMWARE - public static final byte COMMAND_FIRMWARE_FINISH = 0x04; // to UUID_CHARACTERISTIC_FIRMWARE - public static final byte COMMAND_FIRMWARE_APPLY = 0x05; // or is it REBOOT? to UUID_CHARACTERISTIC_FIRMWARE + public static final byte COMMAND_FIRMWARE_CHECKSUM = 0x04; // to UUID_CHARACTERISTIC_FIRMWARE + public static final byte COMMAND_FIRMWARE_APPLY_REBOOT = 0x05; // or is it REBOOT? to UUID_CHARACTERISTIC_FIRMWARE public static final byte[] RESPONSE_FINISH_SUCCESS = new byte[] {RESPONSE, 2, SUCCESS }; public static final byte[] RESPONSE_FIRMWARE_DATA_SUCCESS = new byte[] {RESPONSE, COMMAND_FIRMWARE_START_DATA, SUCCESS }; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandFWHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandFWHelper.java index 04f87c71..71fd485a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandFWHelper.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandFWHelper.java @@ -7,20 +7,15 @@ import android.support.annotation.NonNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.BufferedInputStream; import java.io.IOException; -import java.io.InputStream; -import nodomain.freeyourgadget.gadgetbridge.GBApplication; -import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.AbstractMiFirmwareInfo; -import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; /** * Also see Mi1SFirmwareInfo. */ -public class MiBandFWHelper { +public class MiBandFWHelper extends AbstractMiBandFWHelper { private static final Logger LOG = LoggerFactory.getLogger(MiBandFWHelper.class); /** @@ -29,9 +24,7 @@ public class MiBandFWHelper { * attempting to flash it. */ @NonNull - private final AbstractMiFirmwareInfo firmwareInfo; - @NonNull - private final byte[] fw; + private AbstractMiFirmwareInfo firmwareInfo; /** * Provides a different notification API which is also used on Mi1A devices. @@ -54,77 +47,55 @@ public class MiBandFWHelper { }; public MiBandFWHelper(Uri uri, Context context) throws IOException { - String pebblePattern = ".*\\.(pbw|pbz|pbl)"; - if (uri.getPath().matches(pebblePattern)) { - throw new IOException("Firmware has a filename that looks like a Pebble app/firmware."); - } - - try (InputStream in = new BufferedInputStream(context.getContentResolver().openInputStream(uri))) { - this.fw = FileUtils.readAll(in, 1024 * 1024); // 1 MB - this.firmwareInfo = determineFirmwareInfoFor(fw); - } catch (IOException ex) { - throw ex; // pass through - } catch (IllegalArgumentException ex) { - throw new IOException("This doesn't seem to be a Mi Band firmware: " + ex.getLocalizedMessage(), ex); - } catch (Exception e) { - throw new IOException("Error reading firmware file: " + uri.toString(), e); - } + super(uri, context); } + @Override public int getFirmwareVersion() { // FIXME: UnsupportedOperationException! return firmwareInfo.getFirst().getFirmwareVersion(); } + @Override public int getFirmware2Version() { return firmwareInfo.getFirst().getFirmwareVersion(); } - public static String formatFirmwareVersion(int version) { - if (version == -1) - return GBApplication.getContext().getString(R.string._unknown_); - - return String.format("%d.%d.%d.%d", - version >> 24 & 255, - version >> 16 & 255, - version >> 8 & 255, - version & 255); - } - - public String getHumanFirmwareVersion() { - return format(getFirmwareVersion()); - } - + @Override public String getHumanFirmwareVersion2() { return format(firmwareInfo.getSecond().getFirmwareVersion()); } - public String format(int version) { - return formatFirmwareVersion(version); - } - - @NonNull - public byte[] getFw() { - return fw; - } - - public boolean isFirmwareWhitelisted() { - for (int wlf : whitelistedFirmwareVersion) { - if (wlf == getFirmwareVersion()) { - return true; - } - } - return false; + @Override + protected int[] getWhitelistedFirmwareVersions() { + return whitelistedFirmwareVersion; } + @Override public boolean isFirmwareGenerallyCompatibleWith(GBDevice device) { return firmwareInfo.isGenerallyCompatibleWith(device); } + @Override public boolean isSingleFirmware() { return firmwareInfo.isSingleMiBandFirmware(); } + /** + * @param wholeFirmwareBytes + * @return + * @throws IllegalArgumentException when the data is not recognized as firmware data + */ + @Override + protected void determineFirmwareInfo(byte[] wholeFirmwareBytes) { + firmwareInfo = AbstractMiFirmwareInfo.determineFirmwareInfoFor(wholeFirmwareBytes); + } + + @Override + public void checkValid() throws IllegalArgumentException { + firmwareInfo.checkValid(); + } + /** * @param wholeFirmwareBytes * @return diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandFWInstallHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandFWInstallHandler.java index 8fcc91a4..ef428efa 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandFWInstallHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband/MiBandFWInstallHandler.java @@ -8,91 +8,23 @@ import org.slf4j.LoggerFactory; import java.io.IOException; -import nodomain.freeyourgadget.gadgetbridge.R; -import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity; -import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; -import nodomain.freeyourgadget.gadgetbridge.model.GenericItem; -public class MiBandFWInstallHandler implements InstallHandler { +public class MiBandFWInstallHandler extends AbstractMiBandFWInstallHandler { private static final Logger LOG = LoggerFactory.getLogger(MiBandFWInstallHandler.class); - private final Context mContext; - private MiBandFWHelper helper; - private String errorMessage; - public MiBandFWInstallHandler(Uri uri, Context context) { - mContext = context; - - try { - helper = new MiBandFWHelper(uri, mContext); - } catch (IOException e) { - errorMessage = e.getMessage(); - LOG.warn(errorMessage, e); - } + super(uri, context); } @Override - public void validateInstallation(InstallActivity installActivity, GBDevice device) { - if (device.isBusy()) { - installActivity.setInfoText(device.getBusyTask()); - installActivity.setInstallEnabled(false); - return; - } - - if (device.getType() != DeviceType.MIBAND || !device.isInitialized()) { - installActivity.setInfoText(mContext.getString(R.string.fwapp_install_device_not_ready)); - installActivity.setInstallEnabled(false); - return; - } - - try { - helper.getFirmwareInfo().checkValid(); - } catch (IllegalArgumentException ex) { - installActivity.setInfoText(ex.getLocalizedMessage()); - installActivity.setInstallEnabled(false); - return; - } - - GenericItem fwItem = new GenericItem(mContext.getString(R.string.miband_installhandler_miband_firmware, helper.getHumanFirmwareVersion())); - fwItem.setIcon(R.drawable.ic_device_miband); - - if (!helper.isFirmwareGenerallyCompatibleWith(device)) { - fwItem.setDetails(mContext.getString(R.string.miband_fwinstaller_incompatible_version)); - installActivity.setInfoText(mContext.getString(R.string.fwinstaller_firmware_not_compatible_to_device)); - installActivity.setInstallEnabled(false); - return; - } - StringBuilder builder = new StringBuilder(); - if (helper.isSingleFirmware()) { - builder.append(mContext.getString(R.string.fw_upgrade_notice, helper.getHumanFirmwareVersion())); - } else { - builder.append(mContext.getString(R.string.fw_multi_upgrade_notice, helper.getHumanFirmwareVersion(), helper.getHumanFirmwareVersion2())); - } - - - if (helper.isFirmwareWhitelisted()) { - builder.append(" ").append(mContext.getString(R.string.miband_firmware_known)); - fwItem.setDetails(mContext.getString(R.string.miband_fwinstaller_compatible_version)); - // TODO: set a CHECK (OKAY) button - } else { - builder.append(" ").append(mContext.getString(R.string.miband_firmware_unknown_warning)).append(" \n\n") - .append(mContext.getString(R.string.miband_firmware_suggest_whitelist, helper.getFirmwareVersion())); - fwItem.setDetails(mContext.getString(R.string.miband_fwinstaller_untested_version)); - // TODO: set a UNKNOWN (question mark) button - } - installActivity.setInfoText(builder.toString()); - installActivity.setInstallItem(fwItem); - installActivity.setInstallEnabled(true); + protected AbstractMiBandFWHelper createHelper(Uri uri, Context context) throws IOException { + return new MiBandFWHelper(uri, context); } @Override - public void onStartInstall(GBDevice device) { - - } - - public boolean isValid() { - return helper != null; + protected boolean isSupportedDeviceType(GBDevice device) { + return device.getType() == DeviceType.MIBAND; } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband2/MiBand2FWHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband2/MiBand2FWHelper.java new file mode 100644 index 00000000..a4beb56d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband2/MiBand2FWHelper.java @@ -0,0 +1,70 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.miband2; + +import android.content.Context; +import android.net.Uri; +import android.support.annotation.NonNull; + +import java.io.IOException; + +import nodomain.freeyourgadget.gadgetbridge.devices.miband.AbstractMiBandFWHelper; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.Mi2FirmwareInfo; + +public class MiBand2FWHelper extends AbstractMiBandFWHelper { + private Mi2FirmwareInfo firmwareInfo; + + public MiBand2FWHelper(Uri uri, Context context) throws IOException { + super(uri, context); + } + + @Override + public String format(int version) { + return Mi2FirmwareInfo.toVersion(version); + } + + @Override + public int getFirmwareVersion() { + return firmwareInfo.getFirmwareVersion(); + } + + @Override + public int getFirmware2Version() { + return 0; + } + + @Override + public String getHumanFirmwareVersion2() { + return ""; + } + + @Override + protected int[] getWhitelistedFirmwareVersions() { + return Mi2FirmwareInfo.getWhitelistedVersions(); + } + + @Override + public boolean isFirmwareGenerallyCompatibleWith(GBDevice device) { + return firmwareInfo.isGenerallyCompatibleWith(device); + } + + @Override + public boolean isSingleFirmware() { + return true; + } + + @NonNull + @Override + protected void determineFirmwareInfo(byte[] wholeFirmwareBytes) { + firmwareInfo = new Mi2FirmwareInfo(wholeFirmwareBytes); + } + + @Override + public void checkValid() throws IllegalArgumentException { + firmwareInfo.checkValid(); + } + + public Mi2FirmwareInfo getFirmwareInfo() { + return firmwareInfo; + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband2/MiBand2FWInstallHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband2/MiBand2FWInstallHandler.java new file mode 100644 index 00000000..ef84dee1 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/miband2/MiBand2FWInstallHandler.java @@ -0,0 +1,32 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.miband2; + +import android.content.Context; +import android.net.Uri; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +import nodomain.freeyourgadget.gadgetbridge.devices.miband.AbstractMiBandFWHelper; +import nodomain.freeyourgadget.gadgetbridge.devices.miband.AbstractMiBandFWInstallHandler; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; + +public class MiBand2FWInstallHandler extends AbstractMiBandFWInstallHandler { + private static final Logger LOG = LoggerFactory.getLogger(MiBand2FWInstallHandler.class); + + public MiBand2FWInstallHandler(Uri uri, Context context) { + super(uri, context); + } + + @Override + protected AbstractMiBandFWHelper createHelper(Uri uri, Context context) throws IOException { + return new MiBand2FWHelper(uri, context); + } + + @Override + protected boolean isSupportedDeviceType(GBDevice device) { + return device.getType() == DeviceType.MIBAND2; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLETypeConversions.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLETypeConversions.java index 1bb4b88f..90ccc4db 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLETypeConversions.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BLETypeConversions.java @@ -148,6 +148,24 @@ public class BLETypeConversions { (byte) ((value >> 8) & 0xff), }; } + + public static byte[] fromUint24(int value) { + return new byte[] { + (byte) (value & 0xff), + (byte) ((value >> 8) & 0xff), + (byte) ((value >> 16) & 0xff), + }; + } + + public static byte[] fromUint32(int value) { + return new byte[] { + (byte) (value & 0xff), + (byte) ((value >> 8) & 0xff), + (byte) ((value >> 16) & 0xff), + (byte) ((value >> 24) & 0xff), + }; + } + public static byte fromUint8(int value) { return (byte) (value & 0xff); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBand2Support.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBand2Support.java index 84bf4c6b..046f9d61 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBand2Support.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/MiBand2Support.java @@ -71,6 +71,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.heartrate.Hear import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.Mi2NotificationStrategy; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations.FetchActivityOperation; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations.InitOperation; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations.UpdateFirmwareOperation; import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; @@ -130,6 +131,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { addSupportedService(MiBandService.UUID_SERVICE_MIBAND_SERVICE); addSupportedService(MiBandService.UUID_SERVICE_MIBAND2_SERVICE); + addSupportedService(MiBand2Service.UUID_SERVICE_FIRMWARE_SERVICE); deviceInfoProfile = new DeviceInfoProfile<>(this); addSupportedProfile(deviceInfoProfile); @@ -768,12 +770,11 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { @Override public void onInstallApp(Uri uri) { -// TODO: onInstallApp (firmware update) -// try { -// new UpdateFirmwareOperation(uri, this).perform(); -// } catch (IOException ex) { -// GB.toast(getContext(), "Firmware cannot be installed: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex); -// } + try { + new UpdateFirmwareOperation(uri, this).perform(); + } catch (IOException ex) { + GB.toast(getContext(), "Firmware cannot be installed: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex); + } } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/AbstractMiBandOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/AbstractMiBandOperation.java index f0b143e3..f30a9268 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/AbstractMiBandOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband/operations/AbstractMiBandOperation.java @@ -17,7 +17,7 @@ public abstract class AbstractMiBandOperation crcToVersion = new HashMap<>(); + static { + crcToVersion.put(41899, "1.0.0.39"); + } + + public static String toVersion(int crc16) { + return crcToVersion.get(crc16); + } + + public static int[] getWhitelistedVersions() { + return ArrayUtils.toIntArray(crcToVersion.keySet()); + } + + private final int crc16; + + private byte[] bytes; + private String firmwareVersion; + + public Mi2FirmwareInfo(byte[] bytes) { + this.bytes = bytes; + crc16 = CheckSums.getCRC16(bytes); + firmwareVersion = crcToVersion.get(crc16); + } + + public boolean isGenerallyCompatibleWith(GBDevice device) { + return isHeaderValid() && device.getType() == DeviceType.MIBAND2; + } + + protected boolean isHeaderValid() { + // TODO: not sure if this is a correct check! + return ArrayUtils.equals(FW_HEADER, bytes, FW_HEADER_OFFSET, FW_HEADER_OFFSET + FW_HEADER.length); + } + + public void checkValid() throws IllegalArgumentException { + } + + /** + * Returns the size of the firmware in number of bytes. + * @return + */ + public int getSize() { + return bytes.length; + } + + public byte[] getBytes() { + return bytes; + } + + public int getCrc16() { + return crc16; + } + + public int getFirmwareVersion() { + return getCrc16(); // HACK until we know how to determine the version from the fw bytes + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/UpdateFirmwareOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/UpdateFirmwareOperation.java new file mode 100644 index 00000000..8cad73dd --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/UpdateFirmwareOperation.java @@ -0,0 +1,257 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.Context; +import android.net.Uri; +import android.widget.Toast; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.Arrays; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventDisplayMessage; +import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBand2Service; +import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceBusyAction; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetProgressAction; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBand2Support; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.AbstractMiBand2Operation; +import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.Mi2FirmwareInfo; +import nodomain.freeyourgadget.gadgetbridge.devices.miband2.MiBand2FWHelper; +import nodomain.freeyourgadget.gadgetbridge.util.GB; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; + +public class UpdateFirmwareOperation extends AbstractMiBand2Operation { + private static final Logger LOG = LoggerFactory.getLogger(UpdateFirmwareOperation.class); + + private final Uri uri; + private final BluetoothGattCharacteristic fwCControlChar; + private final BluetoothGattCharacteristic fwCDataChar; + final Prefs prefs = GBApplication.getPrefs(); + private Mi2FirmwareInfo firmwareInfo; + + public UpdateFirmwareOperation(Uri uri, MiBand2Support support) { + super(support); + this.uri = uri; + fwCControlChar = getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_FIRMWARE); + fwCDataChar = getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_FIRMWARE_DATA); + } + + @Override + protected void enableNeededNotifications(TransactionBuilder builder, boolean enable) { + builder.notify(fwCControlChar, enable); + } + + @Override + protected void doPerform() throws IOException { + MiBand2FWHelper mFwHelper = new MiBand2FWHelper(uri, getContext()); + + firmwareInfo = mFwHelper.getFirmwareInfo(); + if (!firmwareInfo.isGenerallyCompatibleWith(getDevice())) { + throw new IOException("Firmware is not compatible with the given device: " + getDevice().getAddress()); + } + + if (!sendFwInfo()) { + displayMessage(getContext(), "Error sending firmware info, aborting.", Toast.LENGTH_LONG, GB.ERROR); + done(); + } + //the firmware will be sent by the notification listener if the band confirms that the metadata are ok. + } + + private void done() { + LOG.info("Operation done."); + operationFinished(); + unsetBusy(); + } + + @Override + public boolean onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic) { + UUID characteristicUUID = characteristic.getUuid(); + if (fwCControlChar.getUuid().equals(characteristicUUID)) { + handleNotificationNotif(characteristic.getValue()); + } else { + super.onCharacteristicChanged(gatt, characteristic); + } + return false; + } + + /** + * React to messages sent by the Mi Band to the MiBandService.UUID_CHARACTERISTIC_NOTIFICATION + * characteristic, + * These messages appear to be always 1 byte long, with values that are listed in MiBandService. + * It is not excluded that there are further values which are still unknown. + *

+ * Upon receiving known values that request further action by GB, the appropriate method is called. + * + * @param value + */ + private void handleNotificationNotif(byte[] value) { + if (value.length != 3) { + LOG.error("Notifications should be 3 bytes long."); + getSupport().logMessageContent(value); + return; + } + boolean success = value[2] == MiBand2Service.SUCCESS; + + if (value[0] == MiBand2Service.RESPONSE && success) { + try { + switch (value[1]) { + case MiBand2Service.COMMAND_FIRMWARE_INIT: { + sendFirmwareData(getFirmwareInfo()); + break; + } + case MiBand2Service.COMMAND_FIRMWARE_START_DATA: { + sendChecksum(getFirmwareInfo()); + break; + } + case MiBand2Service.COMMAND_FIRMWARE_CHECKSUM: { + sendApplyReboot(getFirmwareInfo()); + break; + } + case MiBand2Service.COMMAND_FIRMWARE_APPLY_REBOOT: { + GB.updateInstallNotification(getContext().getString(R.string.updatefirmwareoperation_update_complete), false, 100, getContext()); +// getSupport().onReboot(); + done(); + break; + } + default: { + LOG.error("Unexpected response during firmware update: "); + getSupport().logMessageContent(value); + displayMessage(getContext(), getContext().getString(R.string.updatefirmwareoperation_updateproblem_do_not_reboot), Toast.LENGTH_LONG, GB.ERROR); + done(); + return; + } + } + } catch (Exception ex) { + displayMessage(getContext(), getContext().getString(R.string.updatefirmwareoperation_updateproblem_do_not_reboot), Toast.LENGTH_LONG, GB.ERROR); + done(); + } + } else { + LOG.error("Unexpected notification during firmware update: "); + getSupport().logMessageContent(value); + displayMessage(getContext(), getContext().getString(R.string.updatefirmwareoperation_metadata_updateproblem), Toast.LENGTH_LONG, GB.ERROR); + done(); + } + } + private void displayMessage(Context context, String message, int duration, int severity) { + getSupport().handleGBDeviceEvent(new GBDeviceEventDisplayMessage(message, duration, severity)); + } + + public boolean sendFwInfo() { + try { + TransactionBuilder builder = performInitialized("send firmware info"); +// getSupport().setLowLatency(builder); + builder.add(new SetDeviceBusyAction(getDevice(), getContext().getString(R.string.updating_firmware), getContext())); + int fwSize = getFirmwareInfo().getSize(); + byte[] sizeBytes = BLETypeConversions.fromUint24(fwSize); + byte[] bytes = new byte[]{ + MiBand2Service.COMMAND_FIRMWARE_INIT, + sizeBytes[0], + sizeBytes[1], + sizeBytes[2], + }; + + builder.write(fwCControlChar, bytes); + builder.queue(getQueue()); + return true; + } catch (IOException e) { + LOG.error("Error sending firmware info: " + e.getLocalizedMessage(), e); + return false; + } + } + + /** + * Method that uploads a firmware (fwbytes) to the Mi Band. + * The firmware has to be split into chunks of 20 bytes each, and periodically a COMMAND_SYNC command has to be issued to the Mi Band. + *

+ * The Mi Band will send a notification after receiving this data to confirm if the firmware looks good to it. + * + * @param info + * @return whether the transfer succeeded or not. Only a BT layer exception will cause the transmission to fail. + * @see MiBand2Support#handleNotificationNotif + */ + private boolean sendFirmwareData(Mi2FirmwareInfo info) { + byte[] fwbytes = info.getBytes(); + int len = fwbytes.length; + final int packetLength = 20; + int packets = len / packetLength; + + try { + // going from 0 to len + int firmwareProgress = 0; + + TransactionBuilder builder = performInitialized("send firmware packet"); + if (prefs.getBoolean("mi_low_latency_fw_update", true)) { + getSupport().setLowLatency(builder); + } + builder.write(fwCControlChar, new byte[] { MiBand2Service.COMMAND_FIRMWARE_START_DATA }); + + for (int i = 0; i < packets; i++) { + byte[] fwChunk = Arrays.copyOfRange(fwbytes, i * packetLength, i * packetLength + packetLength); + + builder.write(fwCDataChar, fwChunk); + firmwareProgress += packetLength; + + int progressPercent = (int) ((((float) firmwareProgress) / len) * 100); + if ((i > 0) && (i % 100 == 0)) { + builder.write(fwCControlChar, new byte[]{MiBand2Service.COMMAND_FIRMWARE_UPDATE_SYNC}); + builder.add(new SetProgressAction(getContext().getString(R.string.updatefirmwareoperation_update_in_progress), true, progressPercent, getContext())); + } + } + + if (firmwareProgress < len) { + byte[] lastChunk = Arrays.copyOfRange(fwbytes, packets * packetLength, len); + builder.write(fwCDataChar, lastChunk); + firmwareProgress = len; + } + + builder.write(fwCControlChar, new byte[]{MiBand2Service.COMMAND_FIRMWARE_UPDATE_SYNC}); + builder.queue(getQueue()); + + } catch (IOException ex) { + LOG.error("Unable to send fw to MI 2", ex); + GB.updateInstallNotification(getContext().getString(R.string.updatefirmwareoperation_firmware_not_sent), false, 0, getContext()); + return false; + } + return true; + } + + + private void sendChecksum(Mi2FirmwareInfo firmwareInfo) throws IOException { + TransactionBuilder builder = performInitialized("send firmware checksum"); + int crc16 = firmwareInfo.getCrc16(); + byte[] bytes = BLETypeConversions.fromUint16(crc16); + builder.write(fwCControlChar, new byte[] { + MiBand2Service.COMMAND_FIRMWARE_CHECKSUM, + bytes[0], + bytes[1], + }); + builder.queue(getQueue()); + } + + private void sendApplyReboot(Mi2FirmwareInfo firmwareInfo) throws IOException { + TransactionBuilder builder = performInitialized("send firmware apply/reboot"); + builder.write(fwCControlChar, new byte[] { MiBand2Service.COMMAND_FIRMWARE_APPLY_REBOOT }); + builder.queue(getQueue()); + } + + private Mi2FirmwareInfo getFirmwareInfo() { + return firmwareInfo; + } + + enum State { + INITIAL, + SEND_FW2, + SEND_FW1, + FINISHED, + UNKNOWN + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/ArrayUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/ArrayUtils.java index 7c767cc1..a7fbbf3e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/ArrayUtils.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/ArrayUtils.java @@ -1,5 +1,7 @@ package nodomain.freeyourgadget.gadgetbridge.util; +import java.util.Collection; + public class ArrayUtils { /** * Checks the two given arrays for equality, but comparing only a subset of the second @@ -25,10 +27,10 @@ public class ArrayUtils { if (second.length < secondEndIndex) { throw new IllegalArgumentException("secondStartIndex must be smaller than secondEndIndex"); } - if (first.length < secondEndIndex) { + int len = secondEndIndex - secondStartIndex; + if (first.length != len) { return false; } - int len = secondEndIndex - secondStartIndex; for (int i = 0; i < len; i++) { if (first[i] != second[secondStartIndex + i]) { return false; @@ -36,4 +38,22 @@ public class ArrayUtils { } return true; } + + /** + * Converts a collection of Integer values to an int[] array. + * @param values + * @return null if the given collection is null, otherwise an array of the same size as the collection + * @throws NullPointerException when an element of the collection is null + */ + public static int[] toIntArray(Collection values) { + if (values == null) { + return null; + } + int i = 0; + int[] result = new int[values.size()]; + for (Integer value : values) { + result[i++] = value; + } + return result; + } } diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/Tryout.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/Tryout.java index 0f36f268..1ac4316c 100644 --- a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/Tryout.java +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/test/Tryout.java @@ -4,12 +4,17 @@ import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.File; +import java.io.FileInputStream; import java.util.GregorianCalendar; import nodomain.freeyourgadget.gadgetbridge.Logging; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandDateConverter; +import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBand2Support; +import nodomain.freeyourgadget.gadgetbridge.util.CheckSums; import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; +import nodomain.freeyourgadget.gadgetbridge.util.FileUtils; /** * A simple class for trying out things, not actually testing something. @@ -32,5 +37,4 @@ public class Tryout extends TestBase { LOG.info("Calender: " + DateTimeUtils.formatDateTime(calendar.getTime())); Logging.logBytes(LOG, bytes); } - } diff --git a/build.gradle b/build.gradle index 29d2de8a..7a65513e 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.2.2' + classpath 'com.android.tools.build:gradle:2.2.3' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files