/* * Copyright (C) 2013 jonas.oreland@gmail.com * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.runnerup.hr; import android.annotation.TargetApi; import android.app.Activity; import android.bluetooth.BluetoothAdapter; 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 android.content.pm.PackageManager; import android.os.Build; import android.os.Handler; import java.util.HashSet; import java.util.List; import java.util.UUID; /** * Connects to a Bluetooth Low Energy module for Android versions >= 4.3 * * For BLE in Android versions 4.2, 4.2.1 and 4.2.2, see {@link SamsungBLEHRProvider} * * @author jonas */ @TargetApi(18) public class AndroidBLEHRProvider extends BtHRBase implements HRProvider { public static boolean checkLibrary(Context ctx) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) return false; if (!ctx.getPackageManager().hasSystemFeature( PackageManager.FEATURE_BLUETOOTH_LE)) { return false; } return true; } static final String NAME = "AndroidBLE"; static final String DISPLAY_NAME = "Bluetooth SMART (BLE)"; static final UUID[] SCAN_UUIDS = { HRP_SERVICE }; static boolean AVOID_SCAN_WITH_UUID = false; static boolean CONNECT_IN_OWN_THREAD_FROM_ON_LE_SCAN = false; static { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.JELLY_BEAN_MR2) { // 4.3 AVOID_SCAN_WITH_UUID = true; CONNECT_IN_OWN_THREAD_FROM_ON_LE_SCAN = true; } } private final Context context; private BluetoothAdapter btAdapter = null; private BluetoothGatt btGatt = null; private BluetoothDevice btDevice = null; private int hrValue = 0; private long hrTimestamp = 0; private int batteryLevel = -1; private boolean hasBatteryService = false; private boolean mIsScanning = false; private boolean mIsConnected = false; private boolean mIsConnecting = false; private boolean mIsDisconnecting = false; public AndroidBLEHRProvider(Context ctx) { context = ctx; } @Override public String getName() { return DISPLAY_NAME; } @Override public String getProviderName() { return NAME; } public boolean isEnabled() { return Bt20Base.isEnabledImpl(); } public boolean startEnableIntent(Activity activity, int requestCode) { return Bt20Base.startEnableIntentImpl(activity, requestCode); } @Override public void open(Handler handler, HRClient hrClient) { this.hrClient = hrClient; this.hrClientHandler = handler; if (btAdapter == null) { btAdapter = BluetoothAdapter.getDefaultAdapter(); } if (btAdapter == null) { hrClient.onOpenResult(false); return; } hrClient.onOpenResult(true); } @Override public void close() { stopScan(); disconnect(); if (btGatt != null) { btGatt.close(); btGatt = null; } if (btAdapter == null) { btAdapter = null; } hrClient = null; hrClientHandler = null; } private final BluetoothGattCallback btGattCallbacks = new BluetoothGattCallback() { @Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic arg0) { try { if (!checkBtGattOnlyLogError(gatt)) { return; } if (!arg0.getUuid().equals(HEART_RATE_MEASUREMENT_CHARAC)) { log("onCharacteristicChanged(" + arg0 + ") != HEART_RATE ??"); return; } int length = arg0.getValue().length; if (length == 0) { log("onCharacteristicChanged length = 0"); return; } if (isHeartRateInUINT16(arg0.getValue()[0])) { hrValue = arg0.getIntValue( BluetoothGattCharacteristic.FORMAT_UINT16, 1); } else { hrValue = arg0.getIntValue( BluetoothGattCharacteristic.FORMAT_UINT8, 1); } if (hrValue == 0) { if (mIsConnecting) { reportConnectFailed("got hrValue = 0 => reportConnectFailed"); return; } log("got hrValue == 0 => disconnecting"); reportDisconnected(); return; } hrTimestamp = System.currentTimeMillis(); if (mIsConnecting) { reportConnected(true); } } catch (Exception e) { log("onCharacteristicChanged => " + e); if (mIsConnecting) reportConnectFailed("Exception in onCharacteristicChanged: " + e); else if (mIsConnected) reportDisconnected(); } } @Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic arg0, int status) { try { log("onCharacteristicRead(): " + gatt + ", char: " + arg0.getUuid() + ", status: " + status); if (!checkBtGatt(gatt)) return; UUID charUuid = arg0.getUuid(); if (charUuid.equals(FIRMWARE_REVISON_UUID)) { log("firmware => startHR()"); // triggered from DummyReadForSecLevelCheck startHR(); return; } else if (charUuid.equals(BATTERY_LEVEL_CHARAC)) { log("batterylevel: " + arg0); batteryLevel = arg0.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0); log("Battery level: " + batteryLevel); log(" => startHR()"); // triggered from DummyReadForSecLevelCheck startHR(); return; } else { log("Unknown characteristic received: " + charUuid); } } catch (Exception e) { log("onCharacteristicRead => " + e); reportConnectFailed("Exception in onCharacteristicRead: " + e); } } @Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { super.onCharacteristicWrite(gatt, characteristic, status); } @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { try { log("onConnectionStateChange: " + gatt + ", status: " + status + ", newState: " + newState); log("STATUS_SUCCESS:" + BluetoothGatt.GATT_SUCCESS); log("STATE_CONNECTED: " + BluetoothProfile.STATE_CONNECTED + ", STATE_DISCONNECTED: " + BluetoothProfile.STATE_DISCONNECTED); if (!checkBtGatt(gatt)) { return; } if (mIsConnecting) { if (newState == BluetoothProfile.STATE_CONNECTED) { boolean res = btGatt.discoverServices(); log("discoverServices() => " + res); return; } else { boolean res = btGatt.connect(); log("disconnect while connecting => btGatt.connect() => " + res); return; } } if (mIsDisconnecting) { log("mIsDisconnecting => notify"); synchronized (this) { btGatt.close(); btGatt = null; this.notifyAll(); return; } } if (newState == BluetoothProfile.STATE_DISCONNECTED) { reportDisconnected(); return; } log("onConnectionStateChange => WHAT TO DO??"); } catch (Exception e) { log("onConnectionStateChange => " + e); reportConnectFailed("Exception in onConnectionStateChange: " + e); } } @Override public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor arg0, int status) { BluetoothGattCharacteristic mHRMcharac = arg0.getCharacteristic(); if (!enableNotification(true, mHRMcharac)) { reportConnectFailed("Failed to enable notification in onDescriptorRead"); } } @Override public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { super.onDescriptorWrite(gatt, descriptor, status); } @Override public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) { super.onReadRemoteRssi(gatt, rssi, status); } @Override public void onReliableWriteCompleted(BluetoothGatt gatt, int status) { super.onReliableWriteCompleted(gatt, status); } @Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { log("onServicesDiscoverd(): " + gatt + ", status: " + status); if (!checkBtGatt(gatt)) return; List<BluetoothGattService> list = btGatt.getServices(); for (BluetoothGattService s : list) { log("Found service: " + s.getType() + ", " + s.getInstanceId() + ", " + s.getUuid()); for (BluetoothGattCharacteristic a : s.getCharacteristics()) { log(" char: " + a.getUuid()); } for (BluetoothGattService a : s.getIncludedServices()) { log(" serv: " + a.getUuid()); } if (s.getUuid().equals(BATTERY_SERVICE)) { hasBatteryService = true; } } log(" => DummyRead"); if (status == BluetoothGatt.GATT_SUCCESS) { DummyReadForSecLevelCheck(gatt); // continue in onCharacteristicRead } else { DummyReadForSecLevelCheck(gatt); // reportConnectFailed("onServicesDiscovered(" + gatt + ", " + // status + ")"); } } /* * from Samsung HRPService.java */ private void DummyReadForSecLevelCheck(BluetoothGatt btGatt) { if (btGatt == null) return; if (hasBatteryService && readBatteryLevel()) { return; } BluetoothGattService disService = btGatt.getService(DIS_UUID); if (disService == null) { reportConnectFailed("Dis service not found"); return; } BluetoothGattCharacteristic firmwareIdCharc = disService .getCharacteristic(FIRMWARE_REVISON_UUID); if (firmwareIdCharc == null) { reportConnectFailed("firmware revison charateristic not found!"); return; } if (btGatt.readCharacteristic(firmwareIdCharc) == false) { reportConnectFailed("firmware revison reading is failed!"); } // continue in onCharacteristicRead } private boolean isHeartRateInUINT16(byte b) { if ((b & 1) != 0) return true; return false; } private void startHR() { BluetoothGattService mHRP = btGatt.getService(HRP_SERVICE); if (mHRP == null) { reportConnectFailed("HRP service not found!"); return; } BluetoothGattCharacteristic mHRMcharac = mHRP .getCharacteristic(HEART_RATE_MEASUREMENT_CHARAC); if (mHRMcharac == null) { reportConnectFailed("HEART RATE MEASUREMENT charateristic not found!"); return; } BluetoothGattDescriptor mHRMccc = mHRMcharac.getDescriptor(CCC); if (mHRMccc == null) { reportConnectFailed("CCC for HEART RATE MEASUREMENT charateristic not found!"); return; } if (btGatt.readDescriptor(mHRMccc) == false) { reportConnectFailed("readDescriptor() is failed"); return; } // Continue in onDescriptorRead } private boolean readBatteryLevel() { BluetoothGattService mBS = btGatt.getService(BATTERY_SERVICE); if (mBS == null) { log("Battery service not found."); return false; } BluetoothGattCharacteristic mBLcharac = mBS .getCharacteristic(BATTERY_LEVEL_CHARAC); if (mBLcharac == null) { reportConnectFailed("BATTERY LEVEL charateristic not found!"); return false; } if (!btGatt.readCharacteristic(mBLcharac)) { log("readCharacteristic(" + mBLcharac.getUuid() + ") failed"); return false; } // continue in onCharacteristicRead return true; } }; private boolean enableNotification(boolean onoff, BluetoothGattCharacteristic charac) { if (btGatt == null) return false; if (!btGatt.setCharacteristicNotification(charac, onoff)) { log("btGatt.setCharacteristicNotification() failed"); return false; } BluetoothGattDescriptor clientConfig = charac.getDescriptor(CCC); if (clientConfig == null) { log("clientConfig == null"); return false; } if (onoff) { clientConfig .setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); } else { clientConfig .setValue(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE); } return btGatt.writeDescriptor(clientConfig); } @Override public boolean isScanning() { return mIsScanning; } private final BluetoothAdapter.LeScanCallback mLeScanCallback = new BluetoothAdapter.LeScanCallback() { @Override public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) { if (hrClient == null) return; if (hrClientHandler == null) return; String address = device.getAddress(); if (mIsConnecting && address.equals(btDevice.getAddress())) { stopScan(); if (CONNECT_IN_OWN_THREAD_FROM_ON_LE_SCAN) { log("CONNECT_IN_OWN_THREAD_FROM_ON_LE_SCAN"); hrClientHandler.post(new Runnable() { @Override public void run() { log("before connect"); btGatt = btDevice.connectGatt(context, false, btGattCallbacks); if (btGatt == null) { reportConnectFailed("connectGatt returned null"); } else { log("connectGatt: " + btGatt); } } }); } else { btGatt = btDevice.connectGatt(context, false, btGattCallbacks); if (btGatt == null) { reportConnectFailed("connectGatt returned null"); } else { log("connectGatt: " + btGatt); } } return; } if (mScanDevices.contains(address)) return; mScanDevices.add(address); hrClientHandler.post(new Runnable() { @Override public void run() { if (mIsScanning) { // NOTE: mIsScanning in user-thread hrClient.onScanResult(Bt20Base.createDeviceRef(NAME, device)); } } }); } }; final HashSet<String> mScanDevices = new HashSet<String>(); @Override public void startScan() { if (mIsScanning) return; mIsScanning = true; mScanDevices.clear(); if (AVOID_SCAN_WITH_UUID) btAdapter.startLeScan(mLeScanCallback); else btAdapter.startLeScan(SCAN_UUIDS, mLeScanCallback); } @Override public void stopScan() { if (mIsScanning) { mIsScanning = false; btAdapter.stopLeScan(mLeScanCallback); } } @Override public boolean isConnected() { return mIsConnected; } @Override public boolean isConnecting() { return mIsConnecting; } @Override public void connect(HRDeviceRef ref) { stopScan(); if (!Bt20Base.isEnabledImpl()) { reportConnectFailed("BT is not enabled"); return; } BluetoothDevice dev = BluetoothAdapter.getDefaultAdapter().getRemoteDevice( ref.deviceAddress); if (mIsConnected) return; if (mIsConnecting) return; mIsConnecting = true; btDevice = dev; if (ref.deviceName == null || dev.getName() == null || !dev.getName().contentEquals(ref.deviceName)) { /** * If device doesn't match name, scan for before connecting */ log("Scan before connect"); startScan(); return; } else { log("Skip scan before connect"); } btGatt = btDevice.connectGatt(context, false, btGattCallbacks); if (btGatt == null) { reportConnectFailed("connectGatt returned null"); } else { log("connectGatt: " + btGatt); } } private void reportConnected(final boolean b) { if (hrClientHandler != null) { hrClientHandler.post(new Runnable() { @Override public void run() { if (mIsConnecting && hrClient != null) { mIsConnected = b; mIsConnecting = false; hrClient.onConnectResult(b); } } }); } } private void reportConnectFailed(String string) { log("reportConnectFailed(" + string + ")"); if (btGatt != null) { btGatt.disconnect(); btGatt.close(); btGatt = null; } btDevice = null; reportConnected(false); } @Override public void disconnect() { if (btGatt == null) return; if (btDevice == null) { return; } boolean isConnected = mIsConnected; if (mIsConnecting == false && mIsConnected == false) return; if (mIsDisconnecting == true) return; mIsConnected = false; mIsConnecting = false; mIsDisconnecting = true; do { BluetoothGattService mHRP = btGatt.getService(HRP_SERVICE); if (mHRP == null) { reportDisconnectFailed("HRP service not found!"); break; } BluetoothGattCharacteristic mHRMcharac = mHRP .getCharacteristic(HEART_RATE_MEASUREMENT_CHARAC); if (mHRMcharac == null) { reportDisconnectFailed("HEART RATE MEASUREMENT charateristic not found!"); break; } if (!enableNotification(false, mHRMcharac)) { reportDisconnectFailed("disableNotfication"); break; } } while (false); btGatt.disconnect(); if (isConnected) { log("close btGatt in onConnectionState"); // close btGatt in onConnectionState synchronized (this) { long end = System.currentTimeMillis() + 2000; // wait max 2 // seconds while (btGatt != null && System.currentTimeMillis() < end) { log("waiting for btGatt to become null"); try { this.wait(500); } catch (InterruptedException e) { e.printStackTrace(); } } BluetoothGatt copy = btGatt; if (copy != null) { log("close btGatt in disconnect() after waiting 2 secs"); copy.close(); btGatt = null; } } } else { log("close btGatt here in disconnect()"); BluetoothGatt copy = btGatt; if (copy != null) copy.close(); btGatt = null; } btDevice = null; mIsDisconnecting = false; reportDisconnected(); } private void reportDisconnectFailed(String string) { log("disconnect failed: " + string); hrClient.onDisconnectResult(false); } private void reportDisconnected() { hrClient.onDisconnectResult(true); } @Override public int getHRValue() { return this.hrValue; } @Override public long getHRValueTimestamp() { return this.hrTimestamp; } @Override public HRData getHRData() { if (hrValue <= 0) { return null; } return new HRData().setHeartRate(hrValue).setTimestampEstimate(hrTimestamp); } @Override public boolean isBondingDevice() { return true; } @Override public int getBatteryLevel() { return this.batteryLevel; } private boolean checkBtGatt(BluetoothGatt gatt) { return checkBtGatt(gatt, false); } private boolean checkBtGattOnlyLogError(BluetoothGatt gatt) { return checkBtGatt(gatt, true); } private synchronized boolean checkBtGatt(BluetoothGatt gatt, boolean onlyLogError) { if (btGatt == null) { if (!onlyLogError) log("checkBtGatt, btGatt == null => true"); btGatt = gatt; return true; } if (btGatt == gatt) { if (!onlyLogError) log("checkBtGatt, btGatt == gatt => true"); return true; } log("checkBtGatt, btGatt("+btGatt+") != gatt(" + gatt + ") => false"); return false; } }