package nodomain.freeyourgadget.gadgetbridge.miband; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; import android.content.SharedPreferences; import android.preference.PreferenceManager; import java.io.IOException; import java.util.Arrays; import java.util.Calendar; import java.util.UUID; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.*; 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 onAppDelete(int id, int index) { // 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); } }