diff --git a/CHANGELOG.md b/CHANGELOG.md index 75796e24..82c8ebdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * Pebble: log pebble app logs if option is enabled in pebble development settings * Pebble: notification icons for more apps * Pebble: Further improve compatibility for watchface configuration +* Mi Band 2: Initial support for firmware update (tested so far: 1.0.0.39) ####Version 0.14.4 * Pebble 2/LE: Fix multiple bugs in reconnection code, honor reconnect tries from settings diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventAppManagement.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventAppManagement.java index 99850c5f..41617f55 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventAppManagement.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventAppManagement.java @@ -12,12 +12,14 @@ public class GBDeviceEventAppManagement extends GBDeviceEvent { UNKNOWN, INSTALL, DELETE, + START, + STOP, } public enum Event { UNKNOWN, SUCCESS, - ACKNOLEDGE, + ACKNOWLEDGE, FAILURE, REQUEST, } 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 a0b04af1..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,13 +12,20 @@ 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"); + public static final UUID UUID_UNKNOWN_CHARACTERISTIC0 = UUID.fromString("00000000-0000-3512-2118-0009af100700"); public static final UUID UUID_UNKNOWN_CHARACTERISTIC1 = UUID.fromString("00000001-0000-3512-2118-0009af100700"); public static final UUID UUID_UNKNOWN_CHARACTERISTIC2 = UUID.fromString("00000002-0000-3512-2118-0009af100700"); - public static final UUID UUID_UNKNOWN_CHARACTERISTIC3 = UUID.fromString("00000003-0000-3512-2118-0009af100700"); // Alarm related + /** + * Alarms, Display and other configuration. + */ + public static final UUID UUID_CHARACTERISTIC_3_CONFIGURATION = UUID.fromString("00000003-0000-3512-2118-0009af100700"); public static final UUID UUID_UNKNOWN_CHARACTERISTIC4 = UUID.fromString("00000004-0000-3512-2118-0009af100700"); - public static final UUID UUID_CHARACTERISTIC_ACTIVITY_DATA = UUID.fromString("00000005-0000-3512-2118-0009af100700"); + public static final UUID UUID_CHARACTERISTIC_5_ACTIVITY_DATA = UUID.fromString("00000005-0000-3512-2118-0009af100700"); public static final UUID UUID_UNKNOWN_CHARACTERISTIC6 = UUID.fromString("00000006-0000-3512-2118-0009af100700"); public static final UUID UUID_UNKNOWN_CHARACTERISTIC7 = UUID.fromString("00000007-0000-3512-2118-0009af100700"); public static final UUID UUID_UNKNOWN_CHARACTERISTIC8 = UUID.fromString("00000008-0000-3512-2118-0009af100700"); @@ -35,206 +42,6 @@ public class MiBand2Service { // set 12 hour time mode -// public static final UUID UUID_CHARACTERISTIC_DEVICE_INFO = UUID.fromString(String.format(BASE_UUID, "FF01")); -// -// public static final UUID UUID_CHARACTERISTIC_DEVICE_NAME = UUID.fromString(String.format(BASE_UUID, "FF02")); -// -// public static final UUID UUID_CHARACTERISTIC_NOTIFICATION = UUID.fromString(String.format(BASE_UUID, "FF03")); -// -// public static final UUID UUID_CHARACTERISTIC_USER_INFO = UUID.fromString(String.format(BASE_UUID, "FF04")); -// -// public static final UUID UUID_CHARACTERISTIC_CONTROL_POINT = UUID.fromString(String.format(BASE_UUID, "FF05")); -// -// public static final UUID UUID_CHARACTERISTIC_REALTIME_STEPS = UUID.fromString(String.format(BASE_UUID, "FF06")); -// -// public static final UUID UUID_CHARACTERISTIC_ACTIVITY_DATA = UUID.fromString(String.format(BASE_UUID, "FF07")); -// -// public static final UUID UUID_CHARACTERISTIC_FIRMWARE_DATA = UUID.fromString(String.format(BASE_UUID, "FF08")); -// -// public static final UUID UUID_CHARACTERISTIC_LE_PARAMS = UUID.fromString(String.format(BASE_UUID, "FF09")); -// -// public static final UUID UUID_CHARACTERISTIC_DATE_TIME = UUID.fromString(String.format(BASE_UUID, "FF0A")); -// -// public static final UUID UUID_CHARACTERISTIC_STATISTICS = UUID.fromString(String.format(BASE_UUID, "FF0B")); -// -// public static final UUID UUID_CHARACTERISTIC_BATTERY = UUID.fromString(String.format(BASE_UUID, "FF0C")); -// -// public static final UUID UUID_CHARACTERISTIC_TEST = UUID.fromString(String.format(BASE_UUID, "FF0D")); -// -// public static final UUID UUID_CHARACTERISTIC_SENSOR_DATA = UUID.fromString(String.format(BASE_UUID, "FF0E")); -// -// public static final UUID UUID_CHARACTERISTIC_PAIR = UUID.fromString(String.format(BASE_UUID, "FF0F")); -// -// public static final UUID UUID_CHARACTERISTIC_HEART_RATE_CONTROL_POINT = UUID.fromString(String.format(BASE_UUID, "2A39")); -// public static final UUID UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT = UUID.fromString(String.format(BASE_UUID, "2A37")); -// -// -// -// /* FURTHER UUIDS that were mixed with the other params below. The base UUID for these is unknown */ -// -// public static final byte ALIAS_LEN = 0xa; -// -// /*NOTIFICATIONS: usually received on the UUID_CHARACTERISTIC_NOTIFICATION characteristic */ -// -// public static final byte NOTIFY_NORMAL = 0x0; -// -// public static final byte NOTIFY_FIRMWARE_UPDATE_FAILED = 0x1; -// -// public static final byte NOTIFY_FIRMWARE_UPDATE_SUCCESS = 0x2; -// -// public static final byte NOTIFY_CONN_PARAM_UPDATE_FAILED = 0x3; -// -// public static final byte NOTIFY_CONN_PARAM_UPDATE_SUCCESS = 0x4; -// -// public static final byte NOTIFY_AUTHENTICATION_SUCCESS = 0x5; -// -// public static final byte NOTIFY_AUTHENTICATION_FAILED = 0x6; -// -// public static final byte NOTIFY_FITNESS_GOAL_ACHIEVED = 0x7; -// -// public static final byte NOTIFY_SET_LATENCY_SUCCESS = 0x8; -// -// public static final byte NOTIFY_RESET_AUTHENTICATION_FAILED = 0x9; -// -// public static final byte NOTIFY_RESET_AUTHENTICATION_SUCCESS = 0xa; -// -// public static final byte NOTIFY_FW_CHECK_FAILED = 0xb; -// -// public static final byte NOTIFY_FW_CHECK_SUCCESS = 0xc; -// -// public static final byte NOTIFY_STATUS_MOTOR_NOTIFY = 0xd; -// -// public static final byte NOTIFY_STATUS_MOTOR_CALL = 0xe; -// -// public static final byte NOTIFY_STATUS_MOTOR_DISCONNECT = 0xf; -// -// public static final byte NOTIFY_STATUS_MOTOR_SMART_ALARM = 0x10; -// -// public static final byte NOTIFY_STATUS_MOTOR_ALARM = 0x11; -// -// public static final byte NOTIFY_STATUS_MOTOR_GOAL = 0x12; -// -// public static final byte NOTIFY_STATUS_MOTOR_AUTH = 0x13; -// -// public static final byte NOTIFY_STATUS_MOTOR_SHUTDOWN = 0x14; -// -// public static final byte NOTIFY_STATUS_MOTOR_AUTH_SUCCESS = 0x15; -// -// public static final byte NOTIFY_STATUS_MOTOR_TEST = 0x16; -// -// // 0x18 is returned when we cancel data sync, perhaps is an ack for this message -// -// public static final byte NOTIFY_UNKNOWN = -0x1; -// -// public static final int NOTIFY_PAIR_CANCEL = 0xef; -// -// public static final int NOTIFY_DEVICE_MALFUNCTION = 0xff; -// -// -// /* MESSAGES: unknown */ -// -// public static final byte MSG_CONNECTED = 0x0; -// -// public static final byte MSG_DISCONNECTED = 0x1; -// -// public static final byte MSG_CONNECTION_FAILED = 0x2; -// -// public static final byte MSG_INITIALIZATION_FAILED = 0x3; -// -// public static final byte MSG_INITIALIZATION_SUCCESS = 0x4; -// -// public static final byte MSG_STEPS_CHANGED = 0x5; -// -// public static final byte MSG_DEVICE_STATUS_CHANGED = 0x6; -// -// public static final byte MSG_BATTERY_STATUS_CHANGED = 0x7; -// -// /* COMMANDS: usually sent to UUID_CHARACTERISTIC_CONTROL_POINT characteristic */ -// -// public static final byte COMMAND_SET_TIMER = 0x4; -// -// public static final byte COMMAND_SET_FITNESS_GOAL = 0x5; -// -// public static final byte COMMAND_FETCH_DATA = 0x6; -// -// public static final byte COMMAND_SEND_FIRMWARE_INFO = 0x7; -// -// public static final byte COMMAND_SEND_NOTIFICATION = 0x8; -// -// public static final byte COMMAND_CONFIRM_ACTIVITY_DATA_TRANSFER_COMPLETE = 0xa; -// -// public static final byte COMMAND_SYNC = 0xb; -// -// public static final byte COMMAND_REBOOT = 0xc; -// -// public static final byte COMMAND_SET_WEAR_LOCATION = 0xf; -// -// public static final byte COMMAND_STOP_SYNC_DATA = 0x11; -// -// public static final byte COMMAND_STOP_MOTOR_VIBRATE = 0x13; -// -// public static final byte COMMAND_SET_REALTIME_STEPS_NOTIFICATION = 0x3; -// -// public static final byte COMMAND_SET_REALTIME_STEP = 0x10; -// -// // Test HR -// public static final byte COMMAND_SET_HR_SLEEP = 0x0; -// public static final byte COMMAND_SET__HR_CONTINUOUS = 0x1; -// public static final byte COMMAND_SET_HR_MANUAL = 0x2; -// -// -// /* FURTHER COMMANDS: unchecked therefore left commented -// -// -// public static final byte COMMAND_FACTORY_RESET = 0x9t; -// -// public static final int COMMAND_SET_COLOR_THEME = et; -// -// public static final byte COMMAND_GET_SENSOR_DATA = 0x12t -// -// */ -// -// /* CONNECTION: unknown -// -// public static final CONNECTION_LATENCY_LEVEL_LOW = 0x0t; -// -// public static final CONNECTION_LATENCY_LEVEL_MEDIUM = 0x1t; -// -// public static final CONNECTION_LATENCY_LEVEL_HIGH = 0x2t; -// -// */ -// -// /* MODES: probably related to the sample data structure -// */ -// -// public static final byte MODE_REGULAR_DATA_LEN_BYTE = 0x0; -// -// // was MODE_REGULAR_DATA_LEN_MINITE -// public static final byte MODE_REGULAR_DATA_LEN_MINUTE = 0x1; -// -// /* PROFILE: unknown -// -// public static final PROFILE_STATE_UNKNOWN:I = 0x0 -// -// public static final PROFILE_STATE_INITIALIZATION_SUCCESS:I = 0x1 -// -// public static final PROFILE_STATE_INITIALIZATION_FAILED:I = 0x2 -// -// public static final PROFILE_STATE_AUTHENTICATION_SUCCESS:I = 0x3 -// -// public static final PROFILE_STATE_AUTHENTICATION_FAILED:I = 0x4 -// -// */ -// -// // TEST_*: sent to UUID_CHARACTERISTIC_TEST characteristic -// -// public static final byte TEST_DISCONNECTED_REMINDER = 0x5; -// -// public static final byte TEST_NOTIFICATION = 0x3; -// -// public static final byte TEST_REMOTE_DISCONNECT = 0x1; -// -// public static final byte TEST_SELFTEST = 0x2; private static final Map MIBAND_DEBUG; @@ -281,6 +88,7 @@ public class MiBand2Service { // maybe not really activity data, but steps? public static final byte COMMAND_FETCH_ACTIVITY_DATA = 0x02; + public static final byte COMMAND_XXXX_ACTIVITY_DATA = 0x03; // maybe delete/drop activity data? public static final byte[] COMMAND_SET_FITNESS_GOAL_START = new byte[] { 0x10, 0x0, 0x0 }; public static final byte[] COMMAND_SET_FITNESS_GOAL_END = new byte[] { 0, 0 }; @@ -295,8 +103,16 @@ public class MiBand2Service { public static final byte SUCCESS = 0x01; 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, 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_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 }; /** * Received in response to any dateformat configuration request (byte 0 in the byte[] value. */ 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 e6466471..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); @@ -264,7 +266,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { // .notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_ACTIVITY_DATA), enable) // .notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_BATTERY), enable) // .notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_SENSOR_DATA), enable); - builder.notify(getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC3), enable); + builder.notify(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), enable); BluetoothGattCharacteristic heartrateCharacteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT); if (heartrateCharacteristic != null) { builder.notify(heartrateCharacteristic, enable); @@ -553,7 +555,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { @Override public void onSetAlarms(ArrayList alarms) { try { - BluetoothGattCharacteristic characteristic = getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC3); + BluetoothGattCharacteristic characteristic = getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION); TransactionBuilder builder = performInitialized("Set alarm"); boolean anyAlarmEnabled = false; for (Alarm alarm : alarms) { @@ -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 @@ -1187,7 +1188,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { * @param builder */ private MiBand2Support sendCalendarEvents(TransactionBuilder builder) { - BluetoothGattCharacteristic characteristic = getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC3); + BluetoothGattCharacteristic characteristic = getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION); Prefs prefs = GBApplication.getPrefs(); int availableSlots = prefs.getInt(MiBandConst.PREF_MIBAND_RESERVE_ALARM_FOR_CALENDAR, 0); @@ -1244,10 +1245,10 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { LOG.info("Setting date display to " + dateTimeDisplay); switch (dateTimeDisplay) { case TIME: - builder.write(getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC3), MiBand2Service.DATEFORMAT_TIME); + builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.DATEFORMAT_TIME); break; case DATE_TIME: - builder.write(getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC3), MiBand2Service.DATEFORMAT_DATE_TIME); + builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.DATEFORMAT_DATE_TIME); break; } return this; @@ -1257,9 +1258,9 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport { boolean enable = MiBand2Coordinator.getActivateDisplayOnLiftWrist(); LOG.info("Setting activate display on lift wrist to " + enable); if (enable) { - builder.write(getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC3), MiBand2Service.COMMAND_ENABLE_DISPLAY_ON_LIFT_WRIST); + builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.COMMAND_ENABLE_DISPLAY_ON_LIFT_WRIST); } else { - builder.write(getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC3), MiBand2Service.COMMAND_DISABLE_DISPLAY_ON_LIFT_WRIST); + builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.COMMAND_DISABLE_DISPLAY_ON_LIFT_WRIST); } return this; } 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/FetchActivityOperation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchActivityOperation.java index 881e5ca5..768f9236 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchActivityOperation.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/miband2/operations/FetchActivityOperation.java @@ -60,7 +60,7 @@ public class FetchActivityOperation extends AbstractMiBand2Operation { protected void enableNeededNotifications(TransactionBuilder builder, boolean enable) { if (!enable) { // dynamically enabled, but always disabled on finish - builder.notify(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_ACTIVITY_DATA), enable); + builder.notify(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_5_ACTIVITY_DATA), enable); } } @@ -71,7 +71,7 @@ public class FetchActivityOperation extends AbstractMiBand2Operation { builder.add(new SetDeviceBusyAction(getDevice(), getContext().getString(R.string.busy_task_fetch_activity_data), getContext())); BluetoothGattCharacteristic characteristicFetch = getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC4); builder.notify(characteristicFetch, true); - BluetoothGattCharacteristic characteristicActivityData = getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_ACTIVITY_DATA); + BluetoothGattCharacteristic characteristicActivityData = getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_5_ACTIVITY_DATA); GregorianCalendar sinceWhen = getLastSuccessfulSynchronizedTime(); builder.write(characteristicFetch, BLETypeConversions.join(new byte[] { MiBand2Service.COMMAND_ACTIVITY_DATA_START_DATE, 0x01 }, getSupport().getTimeBytes(sinceWhen, TimeUnit.MINUTES))); @@ -105,7 +105,7 @@ public class FetchActivityOperation extends AbstractMiBand2Operation { public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { UUID characteristicUUID = characteristic.getUuid(); - if (MiBand2Service.UUID_CHARACTERISTIC_ACTIVITY_DATA.equals(characteristicUUID)) { + if (MiBand2Service.UUID_CHARACTERISTIC_5_ACTIVITY_DATA.equals(characteristicUUID)) { handleActivityNotif(characteristic.getValue()); return true; } else if (MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC4.equals(characteristicUUID)) { 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/service/devices/pebble/PebbleIoThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleIoThread.java index 4e895849..45035477 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleIoThread.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleIoThread.java @@ -562,6 +562,9 @@ class PebbleIoThread extends GBDeviceIoThread { break; } break; + case START: + LOG.info("got GBDeviceEventAppManagement START event for uuid: " + appMgmt.uuid); + break; default: break; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java index 623a4b72..762e7166 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java @@ -1918,7 +1918,13 @@ public class PebbleProtocol extends GBDeviceProtocol { if (handler != null) { return handler.pushMessage(); } - break; + else { + GBDeviceEventAppManagement gbDeviceEventAppManagement = new GBDeviceEventAppManagement(); + gbDeviceEventAppManagement.uuid = uuid; + gbDeviceEventAppManagement.type = GBDeviceEventAppManagement.EventType.START; + gbDeviceEventAppManagement.event = GBDeviceEventAppManagement.Event.SUCCESS; + return new GBDeviceEvent[] {gbDeviceEventAppManagement}; + } case APPRUNSTATE_STOP: LOG.info(ENDPOINT_NAME + ": stopped " + uuid); break; @@ -2249,9 +2255,18 @@ public class PebbleProtocol extends GBDeviceProtocol { } } else { try { - devEvts = decodeDictToJSONAppMessage(uuid, buf); + if (endpoint == ENDPOINT_APPLICATIONMESSAGE) { + devEvts = decodeDictToJSONAppMessage(uuid, buf); + } + else { + GBDeviceEventAppManagement gbDeviceEventAppManagement = new GBDeviceEventAppManagement(); + gbDeviceEventAppManagement.uuid = uuid; + gbDeviceEventAppManagement.type = GBDeviceEventAppManagement.EventType.START; + gbDeviceEventAppManagement.event = GBDeviceEventAppManagement.Event.SUCCESS; + devEvts = new GBDeviceEvent[] {gbDeviceEventAppManagement}; + } } catch (JSONException e) { - e.printStackTrace(); + LOG.error(e.getMessage()); return null; } } 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/main/res/xml/changelog_master.xml b/app/src/main/res/xml/changelog_master.xml index 53f3bef1..4f1ec60e 100644 --- a/app/src/main/res/xml/changelog_master.xml +++ b/app/src/main/res/xml/changelog_master.xml @@ -6,6 +6,7 @@ Pebble: log pebble app logs if option is enabled in pebble development settings Pebble: notification icons for more apps Pebble: Further improve compatibility for watchface configuration + Mi Band 2: Initial support for firmware update (tested so far: 1.0.0.39) Pebble 2/LE: Fix multiple bugs in reconnection code, honor reconnect tries from settings 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