/* * 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.BluetoothProfile; import android.bluetooth.BluetoothProfile.ServiceListener; import android.content.Context; import android.os.Build; import android.os.Handler; import android.os.Looper; import com.samsung.android.sdk.bt.gatt.BluetoothGatt; import com.samsung.android.sdk.bt.gatt.BluetoothGattAdapter; import com.samsung.android.sdk.bt.gatt.BluetoothGattCallback; import com.samsung.android.sdk.bt.gatt.BluetoothGattCharacteristic; import com.samsung.android.sdk.bt.gatt.BluetoothGattDescriptor; import com.samsung.android.sdk.bt.gatt.BluetoothGattService; import java.util.List; import java.util.UUID; /** * A HRProvider for BLE on Android versions 4.2, 4.2.1 and 4.2.2. For later versions, see * {@link AndroidBLEHRProvider} * * The Samsung BLE SDK for Bluetooth Low Energy connectivity. On later versions, Android supports BLE natively */ @TargetApi(Build.VERSION_CODES.HONEYCOMB) public class SamsungBLEHRProvider extends BtHRBase implements HRProvider { static final String NAME = "SamsungBLE"; static final String DISPLAY_NAME = "Bluetooth SMART (BLE)"; 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 boolean hasBattery = false; private int batteryLevel = -1; public SamsungBLEHRProvider(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); } private boolean mIsScanning = false; private boolean mIsConnected = false; private boolean mIsConnecting = false; @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; } BluetoothGattAdapter.getProfileProxy(context, profileServiceListener, BluetoothGattAdapter.GATT); } @Override public void close() { do { if (btAdapter == null) break; if (btGatt == null) break; stopScan(); disconnect(); BluetoothGattAdapter.closeProfileProxy(BluetoothGattAdapter.GATT, btGatt); btAdapter = null; } while (false); hrClient = null; hrClientHandler = null; } private final ServiceListener profileServiceListener = new ServiceListener() { @Override public void onServiceConnected(int profile, BluetoothProfile proxy) { if (profile == BluetoothGattAdapter.GATT) { btGatt = (BluetoothGatt) proxy; if (hrClient == null) { log("hrClient == null => skip register in onServiceConnected => closeProfileProxy"); BluetoothGattAdapter.closeProfileProxy(BluetoothGattAdapter.GATT, btGatt); btGatt = null; return; } btGatt.registerApp(btGattCallbacks); } } @Override public void onServiceDisconnected(int profile) { if (profile == BluetoothGattAdapter.GATT) { if (btGatt != null) { btGatt.unregisterApp(); } btGatt = null; } } }; private final BluetoothGattCallback btGattCallbacks = new BluetoothGattCallback() { @Override public void onAppRegistered(final int arg0) { if (hrClientHandler == null) { /* * let's hope that unregister has been called...and that it * works to call it before this callback is called */ log("onAppRegistered: hrClientHandler == null => return"); return; } hrClientHandler.post(new Runnable() { @Override public void run() { if (hrClient == null) { log("onAppRegistered: hrClient == null => return"); return; } if (arg0 == BluetoothGatt.GATT_SUCCESS) { hrClient.onOpenResult(true); } else { hrClient.onOpenResult(false); } } }); } @Override public void onCharacteristicChanged(BluetoothGattCharacteristic arg0) { if (!arg0.getUuid().equals(HEART_RATE_MEASUREMENT_CHARAC)) { log("onCharacteristicChanged(" + arg0 + ") != HEART_RATE ??"); return; } int length = arg0.getValue().length; if (length == 0) { log("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); } } private boolean isHeartRateInUINT16(byte b) { if ((b & 1) != 0) return true; return false; } @Override public void onCharacteristicRead(BluetoothGattCharacteristic arg0, int arg1) { // triggered from DummyReadForSecLevelCheck UUID charUuid = arg0.getUuid(); if (charUuid.equals(FIRMWARE_REVISON_UUID)) { } else if (charUuid.equals(BATTERY_LEVEL_CHARAC)) { batteryLevel = arg0.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0); log("Battery level: " + batteryLevel); } log(" => startHR()"); // triggered from DummyReadForSecLevelCheck startHR(); return; } private void startHR() { BluetoothGattService mHRP = btGatt.getService(btDevice, 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 } @Override public void onCharacteristicWrite(BluetoothGattCharacteristic arg0, int arg1) { } @Override public void onConnectionStateChange(BluetoothDevice arg0, int status, int newState) { if (btDevice != null && arg0 != null && btDevice.getAddress().contentEquals(arg0.getAddress())) { } if (btGatt == null) { log("onConnectionStateChange: btGatt == null"); return; } if (newState == BluetoothProfile.STATE_CONNECTED) { btGatt.discoverServices(arg0); } if (newState == BluetoothProfile.STATE_DISCONNECTED) { reportDisconnected(); } } @Override public void onDescriptorRead(BluetoothGattDescriptor arg0, int arg1) { BluetoothGattCharacteristic mHRMcharac = arg0.getCharacteristic(); if (!enableNotification(true, mHRMcharac)) { reportConnectFailed("Failed to enable notification in onDescriptorRead"); } } @Override public void onDescriptorWrite(BluetoothGattDescriptor arg0, int arg1) { } @Override public void onReadRemoteRssi(BluetoothDevice arg0, int arg1, int arg2) { } @Override public void onReliableWriteCompleted(BluetoothDevice arg0, int arg1) { } @Override public void onScanResult(final BluetoothDevice arg0, int arg1, byte[] scanRecord) { final boolean broadcast = checkIfBroadcastMode(scanRecord); log("onScanResult(" + arg0.getName() + "), hrClient:" + hrClient + ", broadcast: " + broadcast + ", mIsConnecting: " + mIsConnecting + ", mIsScanning: " + mIsScanning); if (!broadcast) { /** * If connect was called and btDevice was unknown, we scan for * it before btGatt.connect() */ if (mIsConnecting && arg0.getAddress().equals(btDevice.getAddress())) { btGatt.stopScan(); btGatt.connect(btDevice, false); return; } hrClientHandler.post(new Runnable() { @Override public void run() { if (mIsScanning && hrClient != null) { // NOTE: // mIsScanning in // user-thread hrClient.onScanResult(Bt20Base.createDeviceRef(NAME, arg0)); } } }); } else { log("checkIfBroadcastMode(" + arg0 + ") => FAIL"); } } @Override public void onServicesDiscovered(BluetoothDevice device, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { List list = btGatt.getServices(device); for (Object _s : list) { BluetoothGattService s = (BluetoothGattService) _s; if (BtHRBase.BATTERY_SERVICE.equals(s.getUuid())) { hasBattery = true; break; } } DummyReadForSecLevelCheck(device); // continue in onCharacteristicRead } else { reportConnectFailed("onServicesDiscovered(" + device + ", " + status + ")"); } } /* * from Samsung HRPService.java */ private boolean checkIfBroadcastMode(byte[] scanRecord) { final int ADV_DATA_FLAG = 0x01; final int LIMITED_AND_GENERAL_DISC_MASK = 0x03; int offset = 0; while (offset < (scanRecord.length - 2)) { int len = scanRecord[offset++]; if (len == 0) break; // Length == 0 , we ignore rest of the packet // TOD Check the rest of the packet if get len = 0 int type = scanRecord[offset++]; switch (type) { case ADV_DATA_FLAG: if (len >= 2) { // The usual scenario(2) and More that 2 octets // scenario. // Since this data will be in Little endian format, // we // are interested in first 2 bits of first byte byte flag = scanRecord[offset++]; /* * 00000011(0x03) - LE Limited Discoverable Mode and * LE General Discoverable Mode */ if ((flag & LIMITED_AND_GENERAL_DISC_MASK) > 0) return false; else return true; } else if (len == 1) { continue;// ignore that packet and continue with the // rest } default: offset += (len - 1); break; } } return false; } /* * from Samsung HRPService.java */ private void DummyReadForSecLevelCheck(BluetoothDevice device) { if (btGatt == null) { log("DummyReadForSecLevelCheck: btGatt == null"); return; } if (device == null) { log("DummyReadForSecLevelCheck: device == null"); return; } if (hasBattery && readBatteryLevel(device)) { return; } BluetoothGattService disService = btGatt.getService(device, 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 readBatteryLevel(BluetoothDevice device) { BluetoothGattService service = btGatt.getService(device, BATTERY_SERVICE); if (service == null) { log("Battery service not found."); return false; } BluetoothGattCharacteristic characteristic = service.getCharacteristic(BATTERY_LEVEL_CHARAC); if (characteristic == null) { log("Battery characteristic not found."); return false; } if (!btGatt.readCharacteristic(characteristic)) { log("readCharacteristic(" + characteristic.getUuid() + ") failed"); return false; } return true; // continue in onCharacteristicRead } private boolean enableNotification(boolean onoff, BluetoothGattCharacteristic charac) { if (btGatt == null) { log("enableNotfication("+onoff+ ", " + charac + "): btGatt == null"); return false; } if (!btGatt.setCharacteristicNotification(charac, onoff)) { log("enableNotfication("+onoff+ ", " + charac + "): setCharacteristicNotification => false"); return false; } BluetoothGattDescriptor clientConfig = charac.getDescriptor(CCC); if (clientConfig == null) { log("enableNotfication("+onoff+ ", " + charac + "): 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; } @Override public void startScan() { if (btGatt == null) return; if (mIsScanning) return; mIsScanning = true; btGatt.startScan(); } @Override public void stopScan() { if (mIsScanning) { mIsScanning = false; btGatt.stopScan(); } } @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.getAddress()); if (mIsConnected) return; if (mIsConnecting) return; mIsConnecting = true; btDevice = dev; if (ref.deviceName == null || dev.getName() == null || !ref.deviceName.contentEquals(dev.getName())) { /** * If device doesn't match name, scan for before connecting */ log("Scan before connect"); startScan(); return; } btGatt.connect(btDevice, false); } private void reportConnected(final boolean b) { hrClientHandler.post(new Runnable() { @Override public void run() { if (mIsConnecting) { mIsConnected = b; mIsConnecting = false; hrClient.onConnectResult(b); } } }); } private void reportConnectFailed(String string) { log("reportConnectFailed(" + string + ")"); if (btGatt != null && btDevice != null) { btGatt.cancelConnection(btDevice); } btDevice = null; reportConnected(false); } @Override public void disconnect() { if (btGatt == null) return; if (btDevice == null) { return; } if (mIsConnecting == false && mIsConnected == false) return; mIsConnected = false; mIsConnecting = false; do { BluetoothGattService mHRP = btGatt.getService(btDevice, 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.cancelConnection(btDevice); btDevice = null; } private void reportDisconnectFailed(String string) { log("disconnect failed: " + string); reportDisconnected(false); } private void reportDisconnected() { reportDisconnected(true); } private void reportDisconnected(final boolean ok) { if (hrClient != null) { if(Looper.myLooper() == Looper.getMainLooper()) { hrClient.onDisconnectResult(ok); } else { hrClientHandler.post(new Runnable() { @Override public void run() { if (hrClient != null) hrClient.onDisconnectResult(ok); } }); } } } @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 int getBatteryLevel() { return batteryLevel; } @Override public boolean isBondingDevice() { return true; } }