mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-28 21:06:50 +01:00
Merge branch 'master' into feature-weather
This commit is contained in:
commit
825f2bf2e8
@ -6,6 +6,7 @@
|
|||||||
* Pebble: log pebble app logs if option is enabled in pebble development settings
|
* Pebble: log pebble app logs if option is enabled in pebble development settings
|
||||||
* Pebble: notification icons for more apps
|
* Pebble: notification icons for more apps
|
||||||
* Pebble: Further improve compatibility for watchface configuration
|
* 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
|
####Version 0.14.4
|
||||||
* Pebble 2/LE: Fix multiple bugs in reconnection code, honor reconnect tries from settings
|
* Pebble 2/LE: Fix multiple bugs in reconnection code, honor reconnect tries from settings
|
||||||
|
@ -12,12 +12,14 @@ public class GBDeviceEventAppManagement extends GBDeviceEvent {
|
|||||||
UNKNOWN,
|
UNKNOWN,
|
||||||
INSTALL,
|
INSTALL,
|
||||||
DELETE,
|
DELETE,
|
||||||
|
START,
|
||||||
|
STOP,
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Event {
|
public enum Event {
|
||||||
UNKNOWN,
|
UNKNOWN,
|
||||||
SUCCESS,
|
SUCCESS,
|
||||||
ACKNOLEDGE,
|
ACKNOWLEDGE,
|
||||||
FAILURE,
|
FAILURE,
|
||||||
REQUEST,
|
REQUEST,
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -19,6 +19,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.miband2.MiBand2FWInstallHandler;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
|
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
@ -86,11 +87,6 @@ public class MiBand2Coordinator extends MiBandCoordinator {
|
|||||||
return new MiBand2SampleProvider(device, session);
|
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 {
|
public static DateTimeDisplay getDateDisplay(Context context) throws IllegalArgumentException {
|
||||||
Prefs prefs = GBApplication.getPrefs();
|
Prefs prefs = GBApplication.getPrefs();
|
||||||
String dateFormatTime = context.getString(R.string.p_dateformat_time);
|
String dateFormatTime = context.getString(R.string.p_dateformat_time);
|
||||||
@ -104,4 +100,10 @@ public class MiBand2Coordinator extends MiBandCoordinator {
|
|||||||
Prefs prefs = GBApplication.getPrefs();
|
Prefs prefs = GBApplication.getPrefs();
|
||||||
return prefs.getBoolean(MiBandConst.PREF_MI2_ACTIVATE_DISPLAY_ON_LIFT, true);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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_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_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_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_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_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_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_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_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_CHARACTERISTIC7 = UUID.fromString("00000007-0000-3512-2118-0009af100700");
|
||||||
public static final UUID UUID_UNKNOWN_CHARACTERISTIC8 = UUID.fromString("00000008-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
|
// 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<UUID, String> MIBAND_DEBUG;
|
private static final Map<UUID, String> MIBAND_DEBUG;
|
||||||
|
|
||||||
@ -281,6 +88,7 @@ public class MiBand2Service {
|
|||||||
|
|
||||||
// maybe not really activity data, but steps?
|
// maybe not really activity data, but steps?
|
||||||
public static final byte COMMAND_FETCH_ACTIVITY_DATA = 0x02;
|
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_START = new byte[] { 0x10, 0x0, 0x0 };
|
||||||
public static final byte[] COMMAND_SET_FITNESS_GOAL_END = new byte[] { 0, 0 };
|
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 SUCCESS = 0x01;
|
||||||
public static final byte COMMAND_ACTIVITY_DATA_START_DATE = 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_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.
|
* Received in response to any dateformat configuration request (byte 0 in the byte[] value.
|
||||||
*/
|
*/
|
||||||
|
@ -7,20 +7,15 @@ import android.support.annotation.NonNull;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
|
||||||
import java.io.IOException;
|
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.impl.GBDevice;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.AbstractMiFirmwareInfo;
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.AbstractMiFirmwareInfo;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Also see Mi1SFirmwareInfo.
|
* Also see Mi1SFirmwareInfo.
|
||||||
*/
|
*/
|
||||||
public class MiBandFWHelper {
|
public class MiBandFWHelper extends AbstractMiBandFWHelper {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(MiBandFWHelper.class);
|
private static final Logger LOG = LoggerFactory.getLogger(MiBandFWHelper.class);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -29,9 +24,7 @@ public class MiBandFWHelper {
|
|||||||
* attempting to flash it.
|
* attempting to flash it.
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
private final AbstractMiFirmwareInfo firmwareInfo;
|
private AbstractMiFirmwareInfo firmwareInfo;
|
||||||
@NonNull
|
|
||||||
private final byte[] fw;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides a different notification API which is also used on Mi1A devices.
|
* 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 {
|
public MiBandFWHelper(Uri uri, Context context) throws IOException {
|
||||||
String pebblePattern = ".*\\.(pbw|pbz|pbl)";
|
super(uri, context);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public int getFirmwareVersion() {
|
public int getFirmwareVersion() {
|
||||||
// FIXME: UnsupportedOperationException!
|
// FIXME: UnsupportedOperationException!
|
||||||
return firmwareInfo.getFirst().getFirmwareVersion();
|
return firmwareInfo.getFirst().getFirmwareVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public int getFirmware2Version() {
|
public int getFirmware2Version() {
|
||||||
return firmwareInfo.getFirst().getFirmwareVersion();
|
return firmwareInfo.getFirst().getFirmwareVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String formatFirmwareVersion(int version) {
|
@Override
|
||||||
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 String getHumanFirmwareVersion2() {
|
public String getHumanFirmwareVersion2() {
|
||||||
return format(firmwareInfo.getSecond().getFirmwareVersion());
|
return format(firmwareInfo.getSecond().getFirmwareVersion());
|
||||||
}
|
}
|
||||||
|
|
||||||
public String format(int version) {
|
@Override
|
||||||
return formatFirmwareVersion(version);
|
protected int[] getWhitelistedFirmwareVersions() {
|
||||||
}
|
return whitelistedFirmwareVersion;
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public byte[] getFw() {
|
|
||||||
return fw;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isFirmwareWhitelisted() {
|
|
||||||
for (int wlf : whitelistedFirmwareVersion) {
|
|
||||||
if (wlf == getFirmwareVersion()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public boolean isFirmwareGenerallyCompatibleWith(GBDevice device) {
|
public boolean isFirmwareGenerallyCompatibleWith(GBDevice device) {
|
||||||
return firmwareInfo.isGenerallyCompatibleWith(device);
|
return firmwareInfo.isGenerallyCompatibleWith(device);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public boolean isSingleFirmware() {
|
public boolean isSingleFirmware() {
|
||||||
return firmwareInfo.isSingleMiBandFirmware();
|
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
|
* @param wholeFirmwareBytes
|
||||||
* @return
|
* @return
|
||||||
|
@ -8,91 +8,23 @@ import org.slf4j.LoggerFactory;
|
|||||||
|
|
||||||
import java.io.IOException;
|
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.impl.GBDevice;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
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 static final Logger LOG = LoggerFactory.getLogger(MiBandFWInstallHandler.class);
|
||||||
|
|
||||||
private final Context mContext;
|
|
||||||
private MiBandFWHelper helper;
|
|
||||||
private String errorMessage;
|
|
||||||
|
|
||||||
public MiBandFWInstallHandler(Uri uri, Context context) {
|
public MiBandFWInstallHandler(Uri uri, Context context) {
|
||||||
mContext = context;
|
super(uri, context);
|
||||||
|
|
||||||
try {
|
|
||||||
helper = new MiBandFWHelper(uri, mContext);
|
|
||||||
} catch (IOException e) {
|
|
||||||
errorMessage = e.getMessage();
|
|
||||||
LOG.warn(errorMessage, e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void validateInstallation(InstallActivity installActivity, GBDevice device) {
|
protected AbstractMiBandFWHelper createHelper(Uri uri, Context context) throws IOException {
|
||||||
if (device.isBusy()) {
|
return new MiBandFWHelper(uri, context);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onStartInstall(GBDevice device) {
|
protected boolean isSupportedDeviceType(GBDevice device) {
|
||||||
|
return device.getType() == DeviceType.MIBAND;
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isValid() {
|
|
||||||
return helper != null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -148,6 +148,24 @@ public class BLETypeConversions {
|
|||||||
(byte) ((value >> 8) & 0xff),
|
(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) {
|
public static byte fromUint8(int value) {
|
||||||
return (byte) (value & 0xff);
|
return (byte) (value & 0xff);
|
||||||
}
|
}
|
||||||
|
@ -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.Mi2NotificationStrategy;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations.FetchActivityOperation;
|
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.InitOperation;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.miband2.operations.UpdateFirmwareOperation;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
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_MIBAND_SERVICE);
|
||||||
addSupportedService(MiBandService.UUID_SERVICE_MIBAND2_SERVICE);
|
addSupportedService(MiBandService.UUID_SERVICE_MIBAND2_SERVICE);
|
||||||
|
addSupportedService(MiBand2Service.UUID_SERVICE_FIRMWARE_SERVICE);
|
||||||
|
|
||||||
deviceInfoProfile = new DeviceInfoProfile<>(this);
|
deviceInfoProfile = new DeviceInfoProfile<>(this);
|
||||||
addSupportedProfile(deviceInfoProfile);
|
addSupportedProfile(deviceInfoProfile);
|
||||||
@ -264,7 +266,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
|
|||||||
// .notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_ACTIVITY_DATA), enable)
|
// .notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_ACTIVITY_DATA), enable)
|
||||||
// .notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_BATTERY), enable)
|
// .notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_BATTERY), enable)
|
||||||
// .notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_SENSOR_DATA), 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);
|
BluetoothGattCharacteristic heartrateCharacteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT);
|
||||||
if (heartrateCharacteristic != null) {
|
if (heartrateCharacteristic != null) {
|
||||||
builder.notify(heartrateCharacteristic, enable);
|
builder.notify(heartrateCharacteristic, enable);
|
||||||
@ -553,7 +555,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
|
|||||||
@Override
|
@Override
|
||||||
public void onSetAlarms(ArrayList<? extends Alarm> alarms) {
|
public void onSetAlarms(ArrayList<? extends Alarm> alarms) {
|
||||||
try {
|
try {
|
||||||
BluetoothGattCharacteristic characteristic = getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC3);
|
BluetoothGattCharacteristic characteristic = getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION);
|
||||||
TransactionBuilder builder = performInitialized("Set alarm");
|
TransactionBuilder builder = performInitialized("Set alarm");
|
||||||
boolean anyAlarmEnabled = false;
|
boolean anyAlarmEnabled = false;
|
||||||
for (Alarm alarm : alarms) {
|
for (Alarm alarm : alarms) {
|
||||||
@ -768,12 +770,11 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onInstallApp(Uri uri) {
|
public void onInstallApp(Uri uri) {
|
||||||
// TODO: onInstallApp (firmware update)
|
try {
|
||||||
// try {
|
new UpdateFirmwareOperation(uri, this).perform();
|
||||||
// new UpdateFirmwareOperation(uri, this).perform();
|
} catch (IOException ex) {
|
||||||
// } catch (IOException ex) {
|
GB.toast(getContext(), "Firmware cannot be installed: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
|
||||||
// GB.toast(getContext(), "Firmware cannot be installed: " + ex.getMessage(), Toast.LENGTH_LONG, GB.ERROR, ex);
|
}
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -1187,7 +1188,7 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
|
|||||||
* @param builder
|
* @param builder
|
||||||
*/
|
*/
|
||||||
private MiBand2Support sendCalendarEvents(TransactionBuilder 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();
|
Prefs prefs = GBApplication.getPrefs();
|
||||||
int availableSlots = prefs.getInt(MiBandConst.PREF_MIBAND_RESERVE_ALARM_FOR_CALENDAR, 0);
|
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);
|
LOG.info("Setting date display to " + dateTimeDisplay);
|
||||||
switch (dateTimeDisplay) {
|
switch (dateTimeDisplay) {
|
||||||
case TIME:
|
case TIME:
|
||||||
builder.write(getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC3), MiBand2Service.DATEFORMAT_TIME);
|
builder.write(getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_3_CONFIGURATION), MiBand2Service.DATEFORMAT_TIME);
|
||||||
break;
|
break;
|
||||||
case DATE_TIME:
|
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;
|
break;
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
@ -1257,9 +1258,9 @@ public class MiBand2Support extends AbstractBTLEDeviceSupport {
|
|||||||
boolean enable = MiBand2Coordinator.getActivateDisplayOnLiftWrist();
|
boolean enable = MiBand2Coordinator.getActivateDisplayOnLiftWrist();
|
||||||
LOG.info("Setting activate display on lift wrist to " + enable);
|
LOG.info("Setting activate display on lift wrist to " + enable);
|
||||||
if (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 {
|
} 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;
|
return this;
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ public abstract class AbstractMiBandOperation<T extends AbstractBTLEDeviceSuppor
|
|||||||
@Override
|
@Override
|
||||||
protected void prePerform() throws IOException {
|
protected void prePerform() throws IOException {
|
||||||
super.prePerform();
|
super.prePerform();
|
||||||
getDevice().setBusyTask("fetch activity data"); // mark as busy quickly to avoid interruptions from the outside
|
getDevice().setBusyTask("Operation starting..."); // mark as busy quickly to avoid interruptions from the outside
|
||||||
TransactionBuilder builder = performInitialized("disabling some notifications");
|
TransactionBuilder builder = performInitialized("disabling some notifications");
|
||||||
enableOtherNotifications(builder, false);
|
enableOtherNotifications(builder, false);
|
||||||
enableNeededNotifications(builder, true);
|
enableNeededNotifications(builder, true);
|
||||||
|
@ -0,0 +1,87 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.miband2;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
|
||||||
|
|
||||||
|
public class Mi2FirmwareInfo {
|
||||||
|
private static final byte[] FW_HEADER = new byte[]{
|
||||||
|
(byte) 0xa3,
|
||||||
|
(byte) 0x68,
|
||||||
|
(byte) 0x04,
|
||||||
|
(byte) 0x3b,
|
||||||
|
(byte) 0x02,
|
||||||
|
(byte) 0xdb,
|
||||||
|
(byte) 0xc8,
|
||||||
|
(byte) 0x58,
|
||||||
|
(byte) 0xd0,
|
||||||
|
(byte) 0x50,
|
||||||
|
(byte) 0xfa,
|
||||||
|
(byte) 0xe7,
|
||||||
|
(byte) 0x0c,
|
||||||
|
(byte) 0x34,
|
||||||
|
(byte) 0xf3,
|
||||||
|
(byte) 0xe7,
|
||||||
|
};
|
||||||
|
private static final int FW_HEADER_OFFSET = 0x150;
|
||||||
|
|
||||||
|
private static Map<Integer,String> 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
|
||||||
|
}
|
||||||
|
}
|
@ -60,7 +60,7 @@ public class FetchActivityOperation extends AbstractMiBand2Operation {
|
|||||||
protected void enableNeededNotifications(TransactionBuilder builder, boolean enable) {
|
protected void enableNeededNotifications(TransactionBuilder builder, boolean enable) {
|
||||||
if (!enable) {
|
if (!enable) {
|
||||||
// dynamically enabled, but always disabled on finish
|
// 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()));
|
builder.add(new SetDeviceBusyAction(getDevice(), getContext().getString(R.string.busy_task_fetch_activity_data), getContext()));
|
||||||
BluetoothGattCharacteristic characteristicFetch = getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC4);
|
BluetoothGattCharacteristic characteristicFetch = getCharacteristic(MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC4);
|
||||||
builder.notify(characteristicFetch, true);
|
builder.notify(characteristicFetch, true);
|
||||||
BluetoothGattCharacteristic characteristicActivityData = getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_ACTIVITY_DATA);
|
BluetoothGattCharacteristic characteristicActivityData = getCharacteristic(MiBand2Service.UUID_CHARACTERISTIC_5_ACTIVITY_DATA);
|
||||||
|
|
||||||
GregorianCalendar sinceWhen = getLastSuccessfulSynchronizedTime();
|
GregorianCalendar sinceWhen = getLastSuccessfulSynchronizedTime();
|
||||||
builder.write(characteristicFetch, BLETypeConversions.join(new byte[] { MiBand2Service.COMMAND_ACTIVITY_DATA_START_DATE, 0x01 }, getSupport().getTimeBytes(sinceWhen, TimeUnit.MINUTES)));
|
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,
|
public boolean onCharacteristicChanged(BluetoothGatt gatt,
|
||||||
BluetoothGattCharacteristic characteristic) {
|
BluetoothGattCharacteristic characteristic) {
|
||||||
UUID characteristicUUID = characteristic.getUuid();
|
UUID characteristicUUID = characteristic.getUuid();
|
||||||
if (MiBand2Service.UUID_CHARACTERISTIC_ACTIVITY_DATA.equals(characteristicUUID)) {
|
if (MiBand2Service.UUID_CHARACTERISTIC_5_ACTIVITY_DATA.equals(characteristicUUID)) {
|
||||||
handleActivityNotif(characteristic.getValue());
|
handleActivityNotif(characteristic.getValue());
|
||||||
return true;
|
return true;
|
||||||
} else if (MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC4.equals(characteristicUUID)) {
|
} else if (MiBand2Service.UUID_UNKNOWN_CHARACTERISTIC4.equals(characteristicUUID)) {
|
||||||
|
@ -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.
|
||||||
|
* <p/>
|
||||||
|
* 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.
|
||||||
|
* <p/>
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
}
|
@ -562,6 +562,9 @@ class PebbleIoThread extends GBDeviceIoThread {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case START:
|
||||||
|
LOG.info("got GBDeviceEventAppManagement START event for uuid: " + appMgmt.uuid);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -1918,7 +1918,13 @@ public class PebbleProtocol extends GBDeviceProtocol {
|
|||||||
if (handler != null) {
|
if (handler != null) {
|
||||||
return handler.pushMessage();
|
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:
|
case APPRUNSTATE_STOP:
|
||||||
LOG.info(ENDPOINT_NAME + ": stopped " + uuid);
|
LOG.info(ENDPOINT_NAME + ": stopped " + uuid);
|
||||||
break;
|
break;
|
||||||
@ -2249,9 +2255,18 @@ public class PebbleProtocol extends GBDeviceProtocol {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
try {
|
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) {
|
} catch (JSONException e) {
|
||||||
e.printStackTrace();
|
LOG.error(e.getMessage());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package nodomain.freeyourgadget.gadgetbridge.util;
|
package nodomain.freeyourgadget.gadgetbridge.util;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
public class ArrayUtils {
|
public class ArrayUtils {
|
||||||
/**
|
/**
|
||||||
* Checks the two given arrays for equality, but comparing only a subset of the second
|
* 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) {
|
if (second.length < secondEndIndex) {
|
||||||
throw new IllegalArgumentException("secondStartIndex must be smaller than secondEndIndex");
|
throw new IllegalArgumentException("secondStartIndex must be smaller than secondEndIndex");
|
||||||
}
|
}
|
||||||
if (first.length < secondEndIndex) {
|
int len = secondEndIndex - secondStartIndex;
|
||||||
|
if (first.length != len) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
int len = secondEndIndex - secondStartIndex;
|
|
||||||
for (int i = 0; i < len; i++) {
|
for (int i = 0; i < len; i++) {
|
||||||
if (first[i] != second[secondStartIndex + i]) {
|
if (first[i] != second[secondStartIndex + i]) {
|
||||||
return false;
|
return false;
|
||||||
@ -36,4 +38,22 @@ public class ArrayUtils {
|
|||||||
}
|
}
|
||||||
return true;
|
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<Integer> values) {
|
||||||
|
if (values == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int i = 0;
|
||||||
|
int[] result = new int[values.size()];
|
||||||
|
for (Integer value : values) {
|
||||||
|
result[i++] = value;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
<change>Pebble: log pebble app logs if option is enabled in pebble development settings</change>
|
<change>Pebble: log pebble app logs if option is enabled in pebble development settings</change>
|
||||||
<change>Pebble: notification icons for more apps</change>
|
<change>Pebble: notification icons for more apps</change>
|
||||||
<change>Pebble: Further improve compatibility for watchface configuration</change>
|
<change>Pebble: Further improve compatibility for watchface configuration</change>
|
||||||
|
<change>Mi Band 2: Initial support for firmware update (tested so far: 1.0.0.39)</change>
|
||||||
</release>
|
</release>
|
||||||
<release version="0.14.4" versioncode="76">
|
<release version="0.14.4" versioncode="76">
|
||||||
<change>Pebble 2/LE: Fix multiple bugs in reconnection code, honor reconnect tries from settings</change>
|
<change>Pebble 2/LE: Fix multiple bugs in reconnection code, honor reconnect tries from settings</change>
|
||||||
|
@ -4,12 +4,17 @@ import org.junit.Test;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
import java.util.GregorianCalendar;
|
import java.util.GregorianCalendar;
|
||||||
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.Logging;
|
import nodomain.freeyourgadget.gadgetbridge.Logging;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandDateConverter;
|
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.service.devices.miband.MiBand2Support;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.CheckSums;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple class for trying out things, not actually testing something.
|
* 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()));
|
LOG.info("Calender: " + DateTimeUtils.formatDateTime(calendar.getTime()));
|
||||||
Logging.logBytes(LOG, bytes);
|
Logging.logBytes(LOG, bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ buildscript {
|
|||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
dependencies {
|
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
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
|
Loading…
Reference in New Issue
Block a user