package com.yeokm1.bleintro;
import android.annotation.TargetApi;
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.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanRecord;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.util.Log;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
/**
* Created by yeokm1 on 29/3/2015.
*/
public class BLEHandler {
private static final String TAG = "BLEHandler";
private static final UUID UUID_SERVICE = UUID.fromString("12345678-9012-3456-7890-123456789012");
private static final UUID UUID_CHAR_LED = UUID.fromString("00000000-0000-0000-0000-000000000010");
private static final UUID UUID_CHAR_BUTTON = UUID.fromString("00000000-0000-0000-0000-000000000020");
//This UUID is a Client Characteristic Configuration descriptor UUID for a Characteristic which has the notify property on the peripheral
//I should not be hard coding this but I cannot find a constant defined anywhere.
private static UUID UUID_CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
private Context context;
private BluetoothAdapter mBluetoothAdapter;
//Android 4.3 - 4.4 uses this
private OldLeScanCallback oldLeScanCallback;
//Android 5.0 and above uses this
private BluetoothLeScanner bleScanner;
private NewLeScanCallback newLeScanCallback;
//We will receive advertisement packets continuously, so we use this HashMap to keep track of what has been found so far.
private HashMap<String, BluetoothDevice> foundDevices = new HashMap<String, BluetoothDevice>();
private BluetoothGatt gattOfCurrentDevice;
private BLEHandlerCallback bleHandlerCallback;
public BLEHandler(Context context, BLEHandlerCallback bleHandlerCallback){
this.context = context;
this.bleHandlerCallback = bleHandlerCallback;
BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothAdapter = bluetoothManager.getAdapter();
}
public void bleScan(boolean start){
if(start){
//Step 1: Start scanning
foundDevices.clear();
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){
bleScanner = mBluetoothAdapter.getBluetoothLeScanner();
newLeScanCallback = new NewLeScanCallback();
//The default is ScanSettings.SCAN_MODE_LOW_POWER which seems too slow for me
//I put the report delay to 0 as I want instant feedback.
ScanSettings settings = new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_BALANCED).setReportDelay(0).build();
//This is usually null as I don't wish to filter but you may want to
List<ScanFilter> filter = null;
// You can also do "bleScanner.startScan(newLeScanCallback);"
bleScanner.startScan(filter, settings, newLeScanCallback);
} else {
oldLeScanCallback = new OldLeScanCallback();
/*
* Samsung phones have a bug when it comes to filtering for UUIDs.
* Nothing will be returned if you use this function for them
UUID[] serviceUUIDs = new UUID[1];
mBluetoothAdapter.startLeScan(serviceUUIDs, oldLeScanCallback);
*/
mBluetoothAdapter.startLeScan(oldLeScanCallback);
}
} else {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if(bleScanner != null){
bleScanner.stopScan(newLeScanCallback);
bleScanner = null;
newLeScanCallback = null;
}
} else {
if(oldLeScanCallback != null) {
mBluetoothAdapter.stopLeScan(oldLeScanCallback);
oldLeScanCallback = null;
}
}
}
}
private void newDeviceScanned(final BluetoothDevice device, int rssi, byte[] scanRecord){
//Step 2 : Received advertisement packet
String macAddress = device.getAddress();
String localName = device.getName();
String logMessage = String.format("Scanning: %s (%s), rssi: %d", localName, macAddress, rssi);
//Log.i(TAG, logMessage);
if(!foundDevices.containsKey(macAddress)){
Log.i(TAG, logMessage);
foundDevices.put(macAddress, device);
bleHandlerCallback.newDeviceScanned(localName, macAddress, rssi, scanRecord);
}
}
//This is used from Android 4.3 to 4.4
private class OldLeScanCallback implements BluetoothAdapter.LeScanCallback{
@Override
public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
newDeviceScanned(device, rssi, scanRecord);
}
}
//This is used from Android 5.0
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private class NewLeScanCallback extends ScanCallback {
@Override
public void onScanResult(int callbackType, ScanResult result) {
BluetoothDevice device = result.getDevice();
int rssi = result.getRssi();
ScanRecord scanRecord = result.getScanRecord();
byte[] record = scanRecord.getBytes();
newDeviceScanned(device, rssi, record);
}
}
public void connectToDevice(String macAddress){
//Step 2.5: Connect to device
final BluetoothDevice deviceToConnect = foundDevices.get(macAddress);
/* Samsung phones require this to be called from the UI thread.
*
* Second param refers to autoconnect(keep connecting in background)
* Initial connection after scan must be set to FALSE
*
* For subsequent connections, it is still preferred to set to false
* to prevent unintended background connection.
*/
if(Build.MANUFACTURER.equalsIgnoreCase("samsung")){
new Handler().post(new Runnable() {
@Override
public void run() {
//Call connect API in UI thread for Samsung phones only
//This will also work non-Samsung phones
deviceToConnect.connectGatt(context, false, mGattCallback);
}
});
} else {
deviceToConnect.connectGatt(context, false, mGattCallback);
}
}
public void disconnectCurrentlyConnectedDevice(){
if(gattOfCurrentDevice != null){
gattOfCurrentDevice.close();
gattOfCurrentDevice.disconnect();
gattOfCurrentDevice = null;
}
}
private void logDeviceMessage(String frontMessage, BluetoothDevice device){
String localName = device.getName();
String macAddress = device.getAddress();
String logMessage = String.format("%s: %s (%s)", frontMessage, localName, macAddress);
Log.i(TAG, logMessage);
}
private BluetoothGattService getCustomService(){
if(gattOfCurrentDevice == null){
return null;
}
BluetoothGattService service = gattOfCurrentDevice.getService(UUID_SERVICE);
return service;
}
//To receive callbacks from a bluetooth device
private BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
BluetoothDevice device = gatt.getDevice();
String localName = device.getName();
String macAddress = device.getAddress();
if (newState == BluetoothProfile.STATE_CONNECTED) {
//Step 3: Connect success
logDeviceMessage("Connected to", gatt.getDevice());
bleHandlerCallback.connectionState(localName, macAddress, true);
gattOfCurrentDevice = gatt;
gatt.discoverServices();
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
logDeviceMessage("Disconnected from", gatt.getDevice());
if(gattOfCurrentDevice != null && gatt.getDevice().getAddress().equals(gatt.getDevice().getAddress())){
gattOfCurrentDevice = null;
}
bleHandlerCallback.connectionState(localName, macAddress, false);
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
BluetoothDevice device = gatt.getDevice();
String localName = device.getName();
String macAddress = device.getAddress();
if (status == BluetoothGatt.GATT_SUCCESS) {
//Step 4 and 5: Services discovered.
//Characteristics and descriptors are automatically discovered with services in Android so we can stop here.
logDeviceMessage("Services discovered for", gatt.getDevice());
//Find the relevant service by UUID
BluetoothGattService customService = getCustomService();
BluetoothGattCharacteristic buttonChar = customService.getCharacteristic(UUID_CHAR_BUTTON);
//Apply to be notified so we can listen to changes in button characteristic
gatt.setCharacteristicNotification(buttonChar, true);
//Extra step for Android, write enable to CCCD
BluetoothGattDescriptor descriptor = buttonChar.getDescriptor(UUID_CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR);
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
gatt.writeDescriptor(descriptor);
bleHandlerCallback.servicesDiscoveredState(localName, macAddress, true);
} else {
logDeviceMessage("Services status " + status + " for", gatt.getDevice());
bleHandlerCallback.servicesDiscoveredState(localName, macAddress, false);
}
}
@Override
public void onCharacteristicChanged (BluetoothGatt gatt, BluetoothGattCharacteristic characteristic){
if(characteristic.getUuid().equals(UUID_CHAR_BUTTON)) {
BluetoothDevice device = gatt.getDevice();
String localName = device.getName();
String macAddress = device.getAddress();
byte[] newValue = characteristic.getValue();
String valueStr = new String(newValue, Charset.forName("US-ASCII"));
bleHandlerCallback.receivedStringValue(localName, macAddress, valueStr);
}
}
};
//Public facing toggle LED methods
public void sendToggleBlueCommand(){
writeThisToLedCharacteristic("b");
}
public void sendToggleYellowCommand(){
writeThisToLedCharacteristic("y");
}
public void writeThisToLedCharacteristic(String stringToWrite){
BluetoothGattService service = getCustomService();
if(service == null){
return;
}
byte[] dataToWrite = stringToWrite.getBytes(Charset.forName("US-ASCII"));
BluetoothGattCharacteristic ledChar = service.getCharacteristic(UUID_CHAR_LED);
ledChar.setValue(dataToWrite);
//Android BLE stack does not allow multiple characteristic writes in quick succession
//You have to wait for the previous characteristic to finish first
gattOfCurrentDevice.writeCharacteristic(ledChar);
}
}