diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/AbstractBTLEDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/AbstractBTLEDeviceSupport.java index 30f408deb..b65a6d5e5 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/AbstractBTLEDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/AbstractBTLEDeviceSupport.java @@ -53,6 +53,10 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im } } + protected TransactionBuilder createTransactionBuilder(String taskName) { + return new TransactionBuilder(taskName); + } + /** * Send commands like this to the device: *

@@ -71,10 +75,10 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im } if (!isInitialized()) { // first, add a transaction that performs device initialization - TransactionBuilder builder = new TransactionBuilder("Initialize device"); + TransactionBuilder builder = createTransactionBuilder("Initialize device"); initializeDevice(builder).queue(getQueue()); } - return new TransactionBuilder(taskName); + return createTransactionBuilder(taskName); } /** @@ -156,7 +160,7 @@ public abstract class AbstractBTLEDeviceSupport extends AbstractDeviceSupport im @Override public void onServicesDiscovered(BluetoothGatt gatt) { gattServicesDiscovered(getQueue().getSupportedGattServices()); - initializeDevice(new TransactionBuilder("Initializing device")).queue(getQueue()); + initializeDevice(createTransactionBuilder("Initializing device")).queue(getQueue()); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/NotifyAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/NotifyAction.java new file mode 100644 index 000000000..37d87b7d2 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/NotifyAction.java @@ -0,0 +1,37 @@ +package nodomain.freeyourgadget.gadgetbridge.btle; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCallback; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.miband.MiBandService; + +/** + * Enables or disables notifications for a given GATT characteristic. + * The result will be made available asynchronously through the + * {@link BluetoothGattCallback}. + */ +public class NotifyAction extends BtLEAction { + + private static final Logger LOG = LoggerFactory.getLogger(TransactionBuilder.class); + protected final boolean enableFlag; + + public NotifyAction(BluetoothGattCharacteristic characteristic, boolean enable) { + super(characteristic); + enableFlag = enable; + } + + @Override + public boolean run(BluetoothGatt gatt) { + return gatt.setCharacteristicNotification(getCharacteristic(), enableFlag); + } + + @Override + public boolean expectsResult() { + return false; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/TransactionBuilder.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/TransactionBuilder.java index dcb97f5eb..5db0e68ca 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/TransactionBuilder.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/btle/TransactionBuilder.java @@ -5,6 +5,8 @@ import android.bluetooth.BluetoothGattCharacteristic; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import nodomain.freeyourgadget.gadgetbridge.miband.MiBandNotifyAction; + public class TransactionBuilder { private static final Logger LOG = LoggerFactory.getLogger(TransactionBuilder.class); @@ -32,6 +34,19 @@ public class TransactionBuilder { return add(action); } + public TransactionBuilder notify(BluetoothGattCharacteristic characteristic, boolean enable) { + if (characteristic == null) { + LOG.warn("Unable to notify characteristic: null"); + return this; + } + NotifyAction action = createNotifyAction(characteristic, enable); + return add(action); + } + + protected NotifyAction createNotifyAction(BluetoothGattCharacteristic characteristic, boolean enable) { + return new NotifyAction(characteristic, enable); + } + public TransactionBuilder wait(int millis) { WaitAction action = new WaitAction(millis); return add(action); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandNotifyAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandNotifyAction.java new file mode 100644 index 000000000..486718fe1 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandNotifyAction.java @@ -0,0 +1,55 @@ +package nodomain.freeyourgadget.gadgetbridge.miband; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCallback; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.btle.BtLEAction; +import nodomain.freeyourgadget.gadgetbridge.btle.NotifyAction; +import nodomain.freeyourgadget.gadgetbridge.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.miband.MiBandService; + +/** + * Enables or disables notifications for a given GATT characteristic. + * The result will be made available asynchronously through the + * {@link BluetoothGattCallback}. + * + * This class is Mi Band specific. + */ +public class MiBandNotifyAction extends NotifyAction { + + private static final Logger LOG = LoggerFactory.getLogger(TransactionBuilder.class); + + public MiBandNotifyAction(BluetoothGattCharacteristic characteristic, boolean enable) { + super(characteristic, enable); + } + + @Override + public boolean run(BluetoothGatt gatt) { + boolean result = super.run(gatt); + if (result) { + BluetoothGattDescriptor sleepDescriptor = getCharacteristic().getDescriptor(MiBandService.UUID_DESCRIPTOR_CLIENT_CHARACTERISTIC_CONFIGURATION); + if (sleepDescriptor != null) { + int properties = getCharacteristic().getProperties(); + if ((properties & 0x10) > 0) { + LOG.info("properties & 0x10 > 0"); + sleepDescriptor.setValue(enableFlag ? BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE); + result = gatt.writeDescriptor(sleepDescriptor); + } else if ((properties & 0x20) > 0){ + LOG.info("properties & 0x20 > 0"); + sleepDescriptor.setValue(enableFlag ? BluetoothGattDescriptor.ENABLE_INDICATION_VALUE : BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE); + result = gatt.writeDescriptor(sleepDescriptor); + } + } else { + LOG.warn("sleep descriptor null"); + } + } else { + LOG.error("Unable to enable notification for " + getCharacteristic().getUuid()); + } + return result; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandService.java index 6a3cee96d..633be8ead 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandService.java @@ -6,6 +6,7 @@ import java.util.UUID; public class MiBandService { + public static final String MAC_ADDRESS_FILTER = "88:0F:10"; public static final String BASE_UUID = "0000%s-0000-1000-8000-00805f9b34fb"; @@ -42,6 +43,10 @@ public class MiBandService { public static final UUID UUID_CHARACTERISTIC_PAIR = UUID.fromString(String.format(BASE_UUID, "FF0F")); + public static final UUID UUID_DESCRIPTOR_CHARACTERISTIC_USER_CONFIGURATION = UUID.fromString(String.format(BASE_UUID, "2901")); + + public static final UUID UUID_DESCRIPTOR_CLIENT_CHARACTERISTIC_CONFIGURATION = UUID.fromString(String.format(BASE_UUID, "2902")); + public static final byte ALIAS_LEN = 0xa; public static final byte NOTIFY_AUTHENTICATION_FAILED = 0x6; @@ -126,9 +131,9 @@ public class MiBandService { public static final byte COMMAND_STOP_MOTOR_VIBRATE = 0x13; + public static final byte COMMAND_CONFIRM_ACTIVITY_DATA_TRANSFER_COMPLETE = 0xa; /* - public static final COMMAND_CONFIRM_ACTIVITY_DATA_TRANSFER_COMPLETE = 0xat; public static final byte COMMAND_FACTORY_RESET = 0x9t; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandSupport.java index 007d88fe5..7bf967dc9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandSupport.java @@ -11,8 +11,11 @@ import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.Arrays; import java.util.Calendar; +import java.util.GregorianCalendar; import java.util.UUID; +import java.text.DateFormat; + import nodomain.freeyourgadget.gadgetbridge.GBCommand; import nodomain.freeyourgadget.gadgetbridge.GBDevice.State; import nodomain.freeyourgadget.gadgetbridge.btle.AbstractBTLEDeviceSupport; @@ -47,10 +50,22 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { @Override protected TransactionBuilder initializeDevice(TransactionBuilder builder) { - pair(builder).sendUserInfo(builder).setCurrentTime(builder).requestBatteryInfo(builder); + pair(builder).sendUserInfo(builder).setCurrentTime(builder).requestBatteryInfo(builder).enableNotifications(builder, true); + return builder; } + // TODO: tear down the notifications on quit + private MiBandSupport enableNotifications(TransactionBuilder builder, boolean enable) { + builder.notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_NOTIFICATION), enable) + .notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_REALTIME_STEPS), enable) + .notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_ACTIVITY_DATA),enable) + .notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_BATTERY), enable) + .notify(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_SENSOR_DATA), enable); + + return this; + } + @Override public boolean useAutoConnect() { return true; @@ -101,6 +116,7 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { private static final byte[] startVibrate = new byte[]{MiBandService.COMMAND_SEND_NOTIFICATION, 1}; private static final byte[] stopVibrate = new byte[]{MiBandService.COMMAND_STOP_MOTOR_VIBRATE}; private static final byte[] reboot = new byte[]{MiBandService.COMMAND_REBOOT}; + private static final byte[] fetch = new byte[]{6}; private byte[] getNotification(long vibrateDuration, int vibrateTimes, int flashTimes, int flashColour, int originalColour, long flashDuration) { byte[] vibrate = new byte[]{MiBandService.COMMAND_SEND_NOTIFICATION, (byte) 1}; @@ -308,13 +324,20 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { @Override public void onReboot() { try { - TransactionBuilder builder = performInitialized("Reboot"); - builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT), reboot); + TransactionBuilder builder = performInitialized("send command"); + builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT), fetch); builder.queue(getQueue()); } catch (IOException ex) { - LOG.error("Unable to reboot MI", ex); + LOG.error("Unable to fetch MI", ex); } - } +// try { +// TransactionBuilder builder = performInitialized("Read Activity data"); +// BluetoothGattCharacteristic characteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_ACTIVITY_DATA); +// builder.read(characteristic).queue(getQueue()); +// } catch (IOException ex) { +// LOG.error("Unable to read activity data from MI", ex); +// } + } @Override public void onAppInfoReq() { @@ -336,6 +359,17 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { // not supported } + @Override + public void onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic) { + LOG.info("Notification characteristic changed: " + characteristic.getUuid()); + super.onCharacteristicChanged(gatt, characteristic); + + UUID characteristicUUID = characteristic.getUuid(); + if (MiBandService.UUID_CHARACTERISTIC_ACTIVITY_DATA.equals(characteristicUUID)) { + handleActivityData(characteristic.getValue(), BluetoothGatt.GATT_SUCCESS); + } + } @Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { @@ -346,6 +380,8 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { handleDeviceInfo(characteristic.getValue(), status); } else if (MiBandService.UUID_CHARACTERISTIC_BATTERY.equals(characteristicUUID)) { handleBatteryInfo(characteristic.getValue(), status); + } else if (MiBandService.UUID_CHARACTERISTIC_ACTIVITY_DATA.equals(characteristicUUID)) { + handleActivityData(characteristic.getValue(), status); } } @@ -357,6 +393,8 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { handlePairResult(characteristic.getValue(), status); } else if (MiBandService.UUID_CHARACTERISTIC_USER_INFO.equals(characteristicUUID)) { handleUserInfoResult(characteristic.getValue(), status); + } else if (MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT.equals(characteristicUUID)) { + handleControlPointResult(characteristic.getValue(), status); } } @@ -368,6 +406,89 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { } } + private void handleActivityData(byte[] value, int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + for (byte b: value){ + LOG.info("2GOT DATA:" + String.format("0x%6x", b)); + } + + if ( value.length == 11 ) { + //I know what to do: + // byte 0 is the data type + int dataType = value[0]; + // byte 1 to 6 represent a timestamp + GregorianCalendar timestamp = new GregorianCalendar(value[1]+2000, + value[2], + value[3], + value[4], + value[5], + value[6]); + // no idea about the following two bytes + int i = value[7] & 0xff; + int j = value[8] & 0xff; + // but combined they tell us how to proceed: + int k = i | (j << 8); + if (dataType == 1) { + k *= 3; + } + int totalDataToRead = k; + + // no idea about the following two bytes + int l = value[9] & 0xff ; + int i1 = value[10] & 0xff; + + int j1 = l | (i1 << 8); + + int k1; + if (dataType == 1) + { + k1 = j1 * 3; + } else + { + k1 = j1; + } + LOG.info("totaldatatoread: "+ totalDataToRead +" len: " + (k1 / 3) + " minute(s)"); + + LOG.info("TIMESTAMP: " + DateFormat.getDateTimeInstance().format(timestamp.getTime()).toString() + " magic byte: " + j1); + //sendAckDataTransfer(timestamp, j1); + sendAckDataTransfer(timestamp, 0); + } + } + } + + private void handleControlPointResult(byte[] value, int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + for (byte b: value){ + LOG.info("3GOT DATA:" + String.format("0x%20x", b)); + } + handleActivityData(value, status); + } else { + LOG.info("BOOOOOOOOO"); + } + + } + private void sendAckDataTransfer(Calendar time, int unknown_code) { + byte[] ack = new byte[]{ + MiBandService.COMMAND_CONFIRM_ACTIVITY_DATA_TRANSFER_COMPLETE, + (byte) (time.get(Calendar.YEAR) - 2000), + (byte) time.get(Calendar.MONTH), + (byte) time.get(Calendar.DATE), + (byte) time.get(Calendar.HOUR_OF_DAY), + (byte) time.get(Calendar.MINUTE), + (byte) time.get(Calendar.SECOND), + (byte) (unknown_code & 0xff), + (byte) (0xff & (unknown_code >> 8)) + }; + LOG.info("WRITING: " + DateFormat.getDateTimeInstance().format(time.getTime()).toString()); + try { + TransactionBuilder builder = performInitialized("send acknowledge"); + builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT), ack); + builder.queue(getQueue()); + } catch (IOException ex) { + LOG.error("Unable to send ack to MI", ex); + } + } + private void handleBatteryInfo(byte[] value, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { BatteryInfo info = new BatteryInfo(value); @@ -412,4 +533,9 @@ public class MiBandSupport extends AbstractBTLEDeviceSupport { } LOG.info("MI Band pairing result: " + value); } + + @Override + protected TransactionBuilder createTransactionBuilder(String taskName) { + return new MiBandTransactionBuilder(taskName); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandSupport.java.orig b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandSupport.java.orig new file mode 100644 index 000000000..ad317bc3a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandSupport.java.orig @@ -0,0 +1,415 @@ +package nodomain.freeyourgadget.gadgetbridge.miband; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Calendar; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.GBCommand; +import nodomain.freeyourgadget.gadgetbridge.GBDevice.State; +import nodomain.freeyourgadget.gadgetbridge.btle.AbstractBTLEDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.btle.TransactionBuilder; + +import static nodomain.freeyourgadget.gadgetbridge.miband.MiBandConst.DEFAULT_VALUE_FLASH_COLOUR; +import static nodomain.freeyourgadget.gadgetbridge.miband.MiBandConst.DEFAULT_VALUE_FLASH_COUNT; +import static nodomain.freeyourgadget.gadgetbridge.miband.MiBandConst.DEFAULT_VALUE_FLASH_DURATION; +import static nodomain.freeyourgadget.gadgetbridge.miband.MiBandConst.DEFAULT_VALUE_FLASH_ORIGINAL_COLOUR; +import static nodomain.freeyourgadget.gadgetbridge.miband.MiBandConst.DEFAULT_VALUE_VIBRATION_COUNT; +import static nodomain.freeyourgadget.gadgetbridge.miband.MiBandConst.DEFAULT_VALUE_VIBRATION_DURATION; +import static nodomain.freeyourgadget.gadgetbridge.miband.MiBandConst.DEFAULT_VALUE_VIBRATION_PAUSE; +import static nodomain.freeyourgadget.gadgetbridge.miband.MiBandConst.FLASH_COLOUR; +import static nodomain.freeyourgadget.gadgetbridge.miband.MiBandConst.FLASH_COUNT; +import static nodomain.freeyourgadget.gadgetbridge.miband.MiBandConst.FLASH_DURATION; +import static nodomain.freeyourgadget.gadgetbridge.miband.MiBandConst.FLASH_ORIGINAL_COLOUR; +import static nodomain.freeyourgadget.gadgetbridge.miband.MiBandConst.ORIGIN_GENERIC; +import static nodomain.freeyourgadget.gadgetbridge.miband.MiBandConst.ORIGIN_K9MAIL; +import static nodomain.freeyourgadget.gadgetbridge.miband.MiBandConst.ORIGIN_SMS; +import static nodomain.freeyourgadget.gadgetbridge.miband.MiBandConst.VIBRATION_COUNT; +import static nodomain.freeyourgadget.gadgetbridge.miband.MiBandConst.VIBRATION_DURATION; +import static nodomain.freeyourgadget.gadgetbridge.miband.MiBandConst.VIBRATION_PAUSE; +import static nodomain.freeyourgadget.gadgetbridge.miband.MiBandConst.getNotificationPrefIntValue; + +public class MiBandSupport extends AbstractBTLEDeviceSupport { + + private static final Logger LOG = LoggerFactory.getLogger(MiBandSupport.class); + + public MiBandSupport() { + addSupportedService(MiBandService.UUID_SERVICE_MIBAND_SERVICE); + } + + @Override + protected TransactionBuilder initializeDevice(TransactionBuilder builder) { + pair(builder).sendUserInfo(builder).setCurrentTime(builder).requestBatteryInfo(builder); + return builder; + } + + @Override + public boolean useAutoConnect() { + return true; + } + + @Override + public void pair() { + for (int i = 0; i < 5; i++) { + if (connect()) { + return; + } + } + } + + private byte[] getDefaultNotification() { + final int vibrateTimes = 1; + final long vibrateDuration = 250l; + final int flashTimes = 1; + final int flashColour = 0xFFFFFFFF; + final int originalColour = 0xFFFFFFFF; + final long flashDuration = 250l; + + return getNotification(vibrateDuration, vibrateTimes, flashTimes, flashColour, originalColour, flashDuration); + } + + private void sendDefaultNotification(TransactionBuilder builder) { + BluetoothGattCharacteristic characteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT); + LOG.info("Sending notification to MiBand: " + characteristic); + builder.write(characteristic, getDefaultNotification()).queue(getQueue()); + } + + private void sendCustomNotification(int vibrateDuration, int vibrateTimes, int pause, int flashTimes, int flashColour, int originalColour, long flashDuration, TransactionBuilder builder) { + BluetoothGattCharacteristic controlPoint = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT); + int vDuration = Math.min(500, vibrateDuration); // longer than 500ms is not possible + for (int i = 0; i < vibrateTimes; i++) { + builder.write(controlPoint, startVibrate); + builder.wait(vDuration); + builder.write(controlPoint, stopVibrate); + if (pause > 0) { + builder.wait(pause); + } + } + + LOG.info("Sending notification to MiBand: " + controlPoint); + builder.queue(getQueue()); + } + + private static final byte[] startVibrate = new byte[]{8, 1}; + private static final byte[] stopVibrate = new byte[]{19}; + private static final byte[] reboot = new byte[]{12}; + + private byte[] getNotification(long vibrateDuration, int vibrateTimes, int flashTimes, int flashColour, int originalColour, long flashDuration) { + byte[] vibrate = new byte[]{(byte) 8, (byte) 1}; + byte r = 6; + byte g = 0; + byte b = 6; + boolean display = true; + // byte[] flashColor = new byte[]{ 14, r, g, b, display ? (byte) 1 : (byte) 0 }; + return vibrate; + } + + /** + * Part of device initialization process. Do not call manually. + * + * @param builder + * @return + */ + private MiBandSupport sendUserInfo(TransactionBuilder builder) { + LOG.debug("Writing User Info!"); + BluetoothGattCharacteristic characteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_USER_INFO); + builder.write(characteristic, MiBandCoordinator.getAnyUserInfo(getDevice().getAddress()).getData()); + return this; + } + + private MiBandSupport requestBatteryInfo(TransactionBuilder builder) { + LOG.debug("Requesting Battery Info!"); + BluetoothGattCharacteristic characteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_BATTERY); + builder.read(characteristic); + return this; + } + + /** + * Part of device initialization process. Do not call manually. + * + * @param transaction + * @return + */ + private MiBandSupport pair(TransactionBuilder transaction) { + LOG.info("Attempting to pair MI device..."); + BluetoothGattCharacteristic characteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_PAIR); + if (characteristic != null) { + transaction.write(characteristic, new byte[]{2}); + } else { + LOG.info("Unable to pair MI device -- characteristic not available"); + } + return this; + } + + private void performDefaultNotification(String task) { + try { + TransactionBuilder builder = performInitialized(task); + sendDefaultNotification(builder); + } catch (IOException ex) { + LOG.error("Unable to send notification to MI device", ex); + } + } + +// private void performCustomNotification(String task, int vibrateDuration, int vibrateTimes, int pause, int flashTimes, int flashColour, int originalColour, long flashDuration) { +// try { +// TransactionBuilder builder = performInitialized(task); +// sendCustomNotification(vibrateDuration, vibrateTimes, pause, flashTimes, flashColour, originalColour, flashDuration, builder); +// } catch (IOException ex) { +// LOG.error("Unable to send notification to MI device", ex); +// } +// } + + private void performPreferredNotification(String task, String notificationOrigin) { + try { + TransactionBuilder builder = performInitialized(task); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + int vibrateDuration = getPreferredVibrateDuration(notificationOrigin, prefs); + int vibrateTimes = getPreferredVibrateCount(notificationOrigin, prefs); + int vibratePause = getPreferredVibratePause(notificationOrigin, prefs); + + int flashTimes = getPreferredFlashCount(notificationOrigin, prefs); + int flashColour = getPreferredFlashColour(notificationOrigin, prefs); + int originalColour = getPreferredOriginalColour(notificationOrigin, prefs); + int flashDuration = getPreferredFlashDuration(notificationOrigin, prefs); + + sendCustomNotification(vibrateDuration, vibrateTimes, vibratePause, flashTimes, flashColour, originalColour, flashDuration, builder); + } catch (IOException ex) { + LOG.error("Unable to send notification to MI device", ex); + } + } + + private int getPreferredFlashDuration(String notificationOrigin, SharedPreferences prefs) { + return getNotificationPrefIntValue(FLASH_DURATION, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_DURATION); + } + + private int getPreferredOriginalColour(String notificationOrigin, SharedPreferences prefs) { + return getNotificationPrefIntValue(FLASH_ORIGINAL_COLOUR, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_ORIGINAL_COLOUR); + } + + private int getPreferredFlashColour(String notificationOrigin, SharedPreferences prefs) { + return getNotificationPrefIntValue(FLASH_COLOUR, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_COLOUR); + } + + private int getPreferredFlashCount(String notificationOrigin, SharedPreferences prefs) { + return getNotificationPrefIntValue(FLASH_COUNT, notificationOrigin, prefs, DEFAULT_VALUE_FLASH_COUNT); + } + + private int getPreferredVibratePause(String notificationOrigin, SharedPreferences prefs) { + return getNotificationPrefIntValue(VIBRATION_PAUSE, notificationOrigin, prefs, DEFAULT_VALUE_VIBRATION_PAUSE); + } + + private int getPreferredVibrateCount(String notificationOrigin, SharedPreferences prefs) { + return getNotificationPrefIntValue(VIBRATION_COUNT, notificationOrigin, prefs, DEFAULT_VALUE_VIBRATION_COUNT); + } + + private int getPreferredVibrateDuration(String notificationOrigin, SharedPreferences prefs) { + return getNotificationPrefIntValue(VIBRATION_DURATION, notificationOrigin, prefs, DEFAULT_VALUE_VIBRATION_DURATION); + } + + @Override + public void onSMS(String from, String body) { +// performCustomNotification("sms received", 500, 3, 2000, 0, 0, 0, 0); + performPreferredNotification("sms received", ORIGIN_SMS); + } + + @Override + public void onEmail(String from, String subject, String body) { + performPreferredNotification("email received", ORIGIN_K9MAIL); + } + + @Override + public void onGenericNotification(String title, String details) { + performPreferredNotification("generic notification received", ORIGIN_GENERIC); + } + + @Override + public void onSetTime(long ts) { + try { + TransactionBuilder builder = performInitialized("Set date and time"); + setCurrentTime(builder); + builder.queue(getQueue()); + } catch (IOException ex) { + LOG.error("Unable to set time on MI device", ex); + } + } + + /** + * Sets the current time to the Mi device using the given builder. + * + * @param builder + */ + private MiBandSupport setCurrentTime(TransactionBuilder builder) { + Calendar now = Calendar.getInstance(); + byte[] time = new byte[]{ + (byte) (now.get(Calendar.YEAR) - 2000), + (byte) now.get(Calendar.MONTH), + (byte) now.get(Calendar.DATE), + (byte) now.get(Calendar.HOUR_OF_DAY), + (byte) now.get(Calendar.MINUTE), + (byte) now.get(Calendar.SECOND), + (byte) 0x0f, + (byte) 0x0f, + (byte) 0x0f, + (byte) 0x0f, + (byte) 0x0f, + (byte) 0x0f + }; + BluetoothGattCharacteristic characteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_DATE_TIME); + if (characteristic != null) { + builder.write(characteristic, time); + } else { + LOG.info("Unable to set time -- characteristic not available"); + } + return this; + } + + @Override + public void onSetCallState(String number, String name, GBCommand command) { + if (GBCommand.CALL_INCOMING.equals(command)) { + performDefaultNotification("incoming call"); + } + } + + @Override + public void onSetMusicInfo(String artist, String album, String track) { + // not supported + } + + @Override + public void onFirmwareVersionReq() { + try { + TransactionBuilder builder = performInitialized("Get MI Band device info"); + BluetoothGattCharacteristic characteristic = getCharacteristic(MiBandService.UUID_CHARACTERISTIC_DEVICE_INFO); + builder.read(characteristic).queue(getQueue()); + } catch (IOException ex) { + LOG.error("Unable to read device info from MI", ex); + } + } + + @Override + public void onBatteryInfoReq() { + try { + TransactionBuilder builder = performInitialized("Get MI Band battery info"); + requestBatteryInfo(builder); + builder.queue(getQueue()); + } catch (IOException ex) { + LOG.error("Unable to read battery info from MI", ex); + } + } + + @Override + public void onReboot() { + try { + TransactionBuilder builder = performInitialized("Reboot"); + builder.write(getCharacteristic(MiBandService.UUID_CHARACTERISTIC_CONTROL_POINT), reboot); + builder.queue(getQueue()); + } catch (IOException ex) { + LOG.error("Unable to reboot MI", ex); + } + } + + @Override + public void onAppInfoReq() { + // not supported + } + + @Override + public void onAppStart(UUID uuid) { + // not supported + } + + @Override + public void onAppDelete(UUID uuid) { + // not supported + } + + @Override + public void onPhoneVersion(byte os) { + // not supported + } + + @Override + public void onCharacteristicRead(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic, int status) { + super.onCharacteristicRead(gatt, characteristic, status); + + UUID characteristicUUID = characteristic.getUuid(); + if (MiBandService.UUID_CHARACTERISTIC_DEVICE_INFO.equals(characteristicUUID)) { + handleDeviceInfo(characteristic.getValue(), status); + } else if (MiBandService.UUID_CHARACTERISTIC_BATTERY.equals(characteristicUUID)) { + handleBatteryInfo(characteristic.getValue(), status); + } + } + + @Override + public void onCharacteristicWrite(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic, int status) { + UUID characteristicUUID = characteristic.getUuid(); + if (MiBandService.UUID_CHARACTERISTIC_PAIR.equals(characteristicUUID)) { + handlePairResult(characteristic.getValue(), status); + } else if (MiBandService.UUID_CHARACTERISTIC_USER_INFO.equals(characteristicUUID)) { + handleUserInfoResult(characteristic.getValue(), status); + } + } + + private void handleDeviceInfo(byte[] value, int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + DeviceInfo info = new DeviceInfo(value); + getDevice().setFirmwareVersion(info.getFirmwareVersion()); + getDevice().sendDeviceUpdateIntent(getContext()); + } + } + + private void handleBatteryInfo(byte[] value, int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + BatteryInfo info = new BatteryInfo(value); + getDevice().setBatteryLevel((short) info.getLevelInPercent()); + getDevice().setBatteryState(info.getStatus()); + getDevice().sendDeviceUpdateIntent(getContext()); + } + } + + private void handleUserInfoResult(byte[] value, int status) { + // successfully transfered user info means we're initialized + if (status == BluetoothGatt.GATT_SUCCESS) { + setConnectionState(State.INITIALIZED); + } + } + + private void setConnectionState(State newState) { + getDevice().setState(newState); + getDevice().sendDeviceUpdateIntent(getContext()); + } + + private void handlePairResult(byte[] pairResult, int status) { + if (status != BluetoothGatt.GATT_SUCCESS) { + LOG.info("Pairing MI device failed: " + status); + return; + } + + String value = null; + if (pairResult != null) { + if (pairResult.length == 1) { + try { + if (pairResult[0] == 2) { + LOG.info("Successfully paired MI device"); + return; + } + } catch (Exception ex) { + LOG.warn("Error identifying pairing result", ex); + return; + } + } + value = Arrays.toString(pairResult); + } + LOG.info("MI Band pairing result: " + value); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandTransactionBuilder.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandTransactionBuilder.java new file mode 100644 index 000000000..b20916956 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/miband/MiBandTransactionBuilder.java @@ -0,0 +1,22 @@ +package nodomain.freeyourgadget.gadgetbridge.miband; + +import android.bluetooth.BluetoothGattCharacteristic; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import nodomain.freeyourgadget.gadgetbridge.btle.NotifyAction; +import nodomain.freeyourgadget.gadgetbridge.btle.TransactionBuilder; + +public class MiBandTransactionBuilder extends TransactionBuilder { + private static final Logger LOG = LoggerFactory.getLogger(MiBandTransactionBuilder.class); + + public MiBandTransactionBuilder(String taskName) { + super(taskName); + } + + @Override + protected NotifyAction createNotifyAction(BluetoothGattCharacteristic characteristic, boolean enable) { + return new MiBandNotifyAction(characteristic, enable); + } +}