/* HeartRateConnector Copyright (c) 2015 NTT DOCOMO,INC. Released under the MIT license http://opensource.org/licenses/mit-license.php */ package org.deviceconnect.android.deviceplugin.heartrate; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCallback; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattService; import android.bluetooth.BluetoothProfile; import android.content.Context; import org.deviceconnect.android.deviceplugin.heartrate.ble.BleDeviceDetector; import org.deviceconnect.android.deviceplugin.heartrate.ble.BleUtils; import org.deviceconnect.android.deviceplugin.heartrate.data.HeartRateDevice; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; import static android.bluetooth.BluetoothGattCharacteristic.FORMAT_UINT16; import static android.bluetooth.BluetoothGattCharacteristic.FORMAT_UINT8; /** * This class manages a BLE device that have Heart Rate Service. * <p> * This class provides the following functions: * <li>Connect a GATT of Heart Rate Service</li> * <li>Disconnect a GATT of Heart Rate Service</li> * <li>Get heart rate</li> * </p> * @author NTT DOCOMO, INC. */ public class HeartRateConnector { /** Logger. */ private final Logger mLogger = Logger.getLogger("heartrate.dplugin"); /** * Define the time to delay first execution.(ms) */ private static final int CHK_FIRST_WAIT_PERIOD = 1000; /** * Define the period between successive executions.(ms) */ private static final int CHK_WAIT_PERIOD = 20 * 1000; /** * application context. */ private Context mContext; /** * Instance of HeartRateConnectEventListener. */ private HeartRateConnectEventListener mListener; /** * Map of Device state. */ private final Map<BluetoothGatt, DeviceState> mHRDevices = new ConcurrentHashMap<>(); /** * List of address of device that registered. */ private final List<String> mRegisterDevices = Collections.synchronizedList( new ArrayList<String>()); /** * Instance of ScheduledExecutorService. */ private ScheduledExecutorService mExecutor = Executors.newSingleThreadScheduledExecutor(); /** * ScheduledFuture of automatic connection timer. */ private ScheduledFuture<?> mAutoConnectTimerFuture; /** * Instance of BleDeviceDetector. */ private BleDeviceDetector mBleDeviceDetector; /** * Constructor. * * @param context application context * @param devices HeartRateDevice list */ public HeartRateConnector(final Context context, final List<HeartRateDevice> devices) { mContext = context; for (HeartRateDevice device : devices) { mRegisterDevices.add(device.getAddress()); } } /** * Sets a instance of BleDeviceDetector. * @param detector instance of BleDeviceDetector */ public void setBleDeviceDetector(final BleDeviceDetector detector) { mBleDeviceDetector = detector; } /** * Sets a listener. * * @param listener listener */ public void setListener(final HeartRateConnectEventListener listener) { mListener = listener; } /** * Connect to the bluetooth device. * * @param device bluetooth device */ public void connectDevice(final BluetoothDevice device) { if (device == null) { throw new IllegalArgumentException("device is null"); } if (containGattMap(device.getAddress())) { return; } try { device.connectGatt(mContext, false, mBluetoothGattCallback); } catch (Exception e) { // Exception occurred when the BLE state is invalid. mLogger.warning("Exception occurred."); } } /** * Disconnect to the bluetooth device. * * @param device bluetooth device */ public void disconnectDevice(final BluetoothDevice device) { if (device == null) { throw new IllegalArgumentException("device is null"); } String address = device.getAddress(); synchronized (mHRDevices) { for (BluetoothGatt gatt : mHRDevices.keySet()) { if (gatt.getDevice().getAddress().equalsIgnoreCase(address)) { gatt.disconnect(); } } } mRegisterDevices.remove(device.getAddress()); } /** * Gets a BluetoothDevice from device list. * @param list list * @param address address * @return Instance of BluetoothDevice, null if not found address */ private BluetoothDevice getBluetoothDeviceFromDeviceList( final List<BluetoothDevice> list, final String address) { for (BluetoothDevice device : list) { if (address.equalsIgnoreCase(device.getAddress())) { return device; } } return null; } /** * Starts timer for automatic connection of BLE device. * <p> * If timer has already started, this method do nothing. * </p> * <p> * NOTE: The automatic connection was implemented on one's own, * because the autoConnect flag of BluetoothDevice#connectGatt did not work as expected. * </p> * @throws IllegalStateException if {@link BleDeviceDetector} has not been set, this exception occur. */ public synchronized void start() { if (mAutoConnectTimerFuture != null) { // timer has already started. return; } if (mBleDeviceDetector == null) { throw new IllegalStateException("BleDeviceDetector has not been set."); } mAutoConnectTimerFuture = mExecutor.scheduleAtFixedRate(new Runnable() { @Override public void run() { mLogger.info("AutoConnect "); boolean foundOfflineDevice = false; for (String address : mRegisterDevices) { if (!containGattMap(address)) { // Found the offline device. foundOfflineDevice = true; } } if (foundOfflineDevice) { mLogger.info("Found an offline device."); mBleDeviceDetector.scanLeDeviceOnce(new BleDeviceDetector.BleDeviceDiscoveryListener() { @Override public void onDiscovery(final List<BluetoothDevice> devices) { synchronized (mRegisterDevices) { for (String address : mRegisterDevices) { if (!containGattMap(address)) { BluetoothDevice device = getBluetoothDeviceFromDeviceList(devices, address); if (device != null) { connectDevice(device); } } } } } }); } } }, CHK_FIRST_WAIT_PERIOD, CHK_WAIT_PERIOD, TimeUnit.MILLISECONDS); } /** * Stops timer for automatic connection of BLE device. */ public synchronized void stop() { if (mAutoConnectTimerFuture != null) { mAutoConnectTimerFuture.cancel(true); mAutoConnectTimerFuture = null; } for (BluetoothGatt gatt : mHRDevices.keySet()) { gatt.close(); } mHRDevices.clear(); } /** * Tests whether this mHRDevices contains address. * @param address BLE device address * @return true if address is an element of mHRDevices, false otherwise */ private boolean containGattMap(final String address) { synchronized (mHRDevices) { for (BluetoothGatt gatt : mHRDevices.keySet()) { if (gatt.getDevice().getAddress().equalsIgnoreCase(address)) { return true; } } } return false; } /** * Tests whether BLE Device has Heart Rate Service. * @param gatt GATT Service * @return true BLE device has Heart Rate Service */ private boolean hasHeartRateService(final BluetoothGatt gatt) { BluetoothGattService service = gatt.getService(UUID.fromString( BleUtils.SERVICE_HEART_RATE_SERVICE)); return service != null; } /** * Checks whether characteristic's uuid and checkUuid is same. * @param characteristic uuid * @param checkUuid uuid * @return true uuid is same, false otherwise */ private boolean isCharacteristic(final BluetoothGattCharacteristic characteristic, final String checkUuid) { String uuid = characteristic.getUuid().toString(); return checkUuid.equalsIgnoreCase(uuid); } /** * Checks whether characteristic is body sensor location. * @param characteristic uuid * @return true uuid is same, false otherwise */ private boolean isBodySensorLocation(final BluetoothGattCharacteristic characteristic) { return isCharacteristic(characteristic, BleUtils.CHAR_BODY_SENSOR_LOCATION); } /** * Checks whether characteristic is Heart Rate Measurement * @param characteristic uuid * @return true uuid is same, false otherwise */ private boolean isHeartRateMeasurement(final BluetoothGattCharacteristic characteristic) { return isCharacteristic(characteristic, BleUtils.CHAR_HEART_RATE_MEASUREMENT); } /** * Register a state of GATT Service and connects GATT Service. * @param gatt GATT */ private void registerHeartRateDeviceState(final BluetoothGatt gatt) { mHRDevices.put(gatt, DeviceState.GET_LOCATION); if (mListener != null) { mListener.onConnected(gatt.getDevice()); } if (!mRegisterDevices.contains(gatt.getDevice().getAddress())) { mRegisterDevices.add(gatt.getDevice().getAddress()); } } /** * Get a body sensor location from GATT Service. * * @param gatt GATT Service * @return true if gatt has Generic Access Service, false if gatt has no service. */ private boolean callGetBodySensorLocation(final BluetoothGatt gatt) { boolean result = false; BluetoothGattService service = gatt.getService(UUID.fromString( BleUtils.SERVICE_HEART_RATE_SERVICE)); if (service != null) { BluetoothGattCharacteristic c = service.getCharacteristic( UUID.fromString(BleUtils.CHAR_BODY_SENSOR_LOCATION)); if (c != null) { result = gatt.readCharacteristic(c); } } return result; } /** * Register notification of HeartRateMeasurement Characteristic. * * @param gatt GATT Service * @return true if successful in notification of registration */ private boolean callRegisterHeartRateMeasurement(final BluetoothGatt gatt) { boolean registered = false; BluetoothGattService service = gatt.getService(UUID.fromString( BleUtils.SERVICE_HEART_RATE_SERVICE)); if (service != null) { BluetoothGattCharacteristic c = service.getCharacteristic( UUID.fromString(BleUtils.CHAR_HEART_RATE_MEASUREMENT)); if (c != null) { registered = gatt.setCharacteristicNotification(c, true); if (registered) { for (BluetoothGattDescriptor descriptor : c.getDescriptors()) { descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); gatt.writeDescriptor(descriptor); } mHRDevices.put(gatt, DeviceState.REGISTER_NOTIFY); } } } return registered; } /** * Shift to the next state on GATT Service. * @param gatt GATT Service */ private void next(final BluetoothGatt gatt) { if (!mHRDevices.containsKey(gatt)) { registerHeartRateDeviceState(gatt); } DeviceState state = mHRDevices.get(gatt); switch (state) { case GET_LOCATION: if (!callGetBodySensorLocation(gatt)) { mHRDevices.put(gatt, DeviceState.REGISTER_NOTIFY); gatt.discoverServices(); } break; case REGISTER_NOTIFY: if (!callRegisterHeartRateMeasurement(gatt)) { mHRDevices.put(gatt, DeviceState.ERROR); } break; case CONNECTED: mLogger.fine("@@@@@@ GATT Service is connected."); break; default: mLogger.warning("Illegal state. state=" + state); break; } } /** * Notify heart rate to {@link HeartRateConnectEventListener}. * @param gatt GATT Service * @param characteristic BluetoothGattCharacteristic */ private void notifyHeartRateMeasurement(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) { int heartRate = 0; int energyExpended = 0; double rrInterval = 0; int offset = 1; byte[] buf = characteristic.getValue(); if (buf.length > 1) { // Heart Rate Value Format bit if ((buf[0] & 0x01) != 0) { Integer v = characteristic.getIntValue(FORMAT_UINT16, offset); if (v != null) { heartRate = v; } offset += 2; } else { Integer v = characteristic.getIntValue(FORMAT_UINT8, offset); if (v != null) { heartRate = v; } offset += 1; } // Sensor Contact Status bits if ((buf[0] & 0x06) != 0) { // MEMO: not implements yet } // Energy Expended Status bit if ((buf[0] & 0x08) != 0) { Integer v = characteristic.getIntValue(FORMAT_UINT16, offset); if (v != null) { energyExpended = v; } offset += 2; } // RR-Interval bit if ((buf[0] & 0x10) != 0) { Integer v = characteristic.getIntValue(FORMAT_UINT16, offset); if (v != null) { rrInterval = ((double) v / 1024.0) * 1000.0; } } } mLogger.warning("@@@@@@ HEART RATE[" + heartRate + ", " + energyExpended + ", " + rrInterval + "]"); BluetoothDevice device = gatt.getDevice(); if (mListener != null) { mListener.onReceivedData(device, heartRate, energyExpended, rrInterval); } } /** * This class is the implement of BluetoothGattCallback. */ private final BluetoothGattCallback mBluetoothGattCallback = new BluetoothGattCallback() { @Override public void onConnectionStateChange(final BluetoothGatt gatt, final int status, final int newState) { mLogger.fine("@@@@@@ onConnectionStateChange: [" + gatt.getDevice() + "]: " + status + " -> " + newState); if (newState == BluetoothProfile.STATE_CONNECTED) { gatt.discoverServices(); } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { mHRDevices.remove(gatt); gatt.close(); if (mListener != null) { mListener.onDisconnected(gatt.getDevice()); } } } @Override public void onServicesDiscovered(final BluetoothGatt gatt, final int status) { mLogger.fine("@@@@@@ onServicesDiscovered: [" + gatt.getDevice() + "]"); if (status == BluetoothGatt.GATT_SUCCESS) { if (!hasHeartRateService(gatt)) { // ble device has no heart rate service. gatt.close(); if (mListener != null) { mListener.onConnectFailed(gatt.getDevice()); } } else { next(gatt); } } else { // connect error gatt.close(); if (mListener != null) { mListener.onConnectFailed(gatt.getDevice()); } } } @Override public void onCharacteristicRead(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int status) { mLogger.fine("@@@@@@ onCharacteristicRead: [" + gatt.getDevice() + "]"); if (status == BluetoothGatt.GATT_SUCCESS) { if (isBodySensorLocation(characteristic)) { Integer location = characteristic.getIntValue(FORMAT_UINT8, 0); if (mListener != null && location != null) { mListener.onReadSensorLocation(gatt.getDevice(), location); } } } mHRDevices.put(gatt, DeviceState.REGISTER_NOTIFY); gatt.discoverServices(); } @Override public void onCharacteristicChanged(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) { mLogger.fine("@@@@@@ onCharacteristicChanged: [" + gatt.getDevice() + "]"); if (isHeartRateMeasurement(characteristic)) { notifyHeartRateMeasurement(gatt, characteristic); } } }; private enum DeviceState { GET_LOCATION, REGISTER_NOTIFY, CONNECTED, DISCONNECT, ERROR, } /** * This interface is used to implement {@link HeartRateConnector} callbacks. */ public static interface HeartRateConnectEventListener { void onConnected(BluetoothDevice device); void onDisconnected(BluetoothDevice device); void onConnectFailed(BluetoothDevice device); void onReadSensorLocation(BluetoothDevice device, int location); void onReceivedData(BluetoothDevice device, int heartRate, int energyExpended, double rrInterval); } }