I currently have a method which writes to the BLE devices to beep it. My Bluetooth Callback goes as follows :
ReadCharacteristic rc = new ReadCharacteristic(context, ds.getMacAddress(), serviceUUID, UUID.fromString(myUUID), "") {
@Override
public void onRead() {
Log.w(TAG, "callDevice onRead");
try{Thread.sleep(1000);}catch(InterruptedException ex){}
WriteCharacteristic wc = new WriteCharacteristic(activity, context, getMacAddress(), serviceUUID, UUID.fromString(myUUID), ""){
@Override
public void onWrite(){
Log.w(TAG, "callDevice onWrite");
}
@Override
public void onError(){
Log.w(TAG, "callDevice onWrite-onError");
}
};
// Store data in writeBuffer
wc.writeCharacteristic(writeBuffer);
}
@Override
public void onError(){
Log.w(TAG, "callDevice onRead-onError");
}
};
rc.readCharacteristic();
My ReadCharacteristic implementation is as follows :
public class ReadCharacteristic extends BluetoothGattCallback {
public ReadCharacteristic(Context context, String macAddress, UUID service, UUID characteristic, Object tag) {
mMacAddress = macAddress;
mService = service;
mCharacteristic = characteristic;
mTag = tag;
mContext = context;
this.activity =activity;
final BluetoothManager bluetoothManager =
(BluetoothManager) mContext.getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothAdapter = bluetoothManager.getAdapter();
}
final private static String TAG = "ReadCharacteristic";
private Object mTag;
private String mMacAddress;
private UUID mService;
private UUID mCharacteristic;
private byte[] mValue;
private Activity activity;
private BluetoothAdapter mBluetoothAdapter;
private Context mContext;
private int retry = 5;
public String getMacAddress() {
return mMacAddress;
}
public UUID getService() {
return mService;
}
public UUID getCharacteristic() {
return mCharacteristic;
}
public byte[] getValue() { return mValue; }
public void onRead() {
Log.w(TAG, "onRead: " + getDataHex(getValue()));
}
public void onError() {
Log.w(TAG, "onError");
}
public void readCharacteristic(){
if (retry == 0)
{
onError();
return;
}
retry--;
final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(getMacAddress());
if (device != null) {
Log.w(TAG, "Starting Read [" + getService() + "|" + getCharacteristic() + "]");
final ReadCharacteristic rc = ReadCharacteristic.this;
device.connectGatt(mContext, false, rc);
}
}
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
Log.w(TAG,"onConnectionStateChange [" + status + "|" + newState + "]");
if ((newState == 2)&&(status ==0)) {
gatt.discoverServices();
}
else{
Log.w(TAG, "[" + status + "]");
// gatt.disconnect();
gatt.close();
try
{
Thread.sleep(2000);
}
catch(Exception e)
{
}
readCharacteristic();
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
Log.w(TAG,"onServicesDiscovered [" + status + "]");
BluetoothGattService bgs = gatt.getService(getService());
if (bgs != null) {
BluetoothGattCharacteristic bgc = bgs.getCharacteristic(getCharacteristic());
gatt.readCharacteristic(bgc);
}
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic,
int status) {
Log.w(TAG,"onCharacteristicRead [" + status + "]");
if (status == BluetoothGatt.GATT_SUCCESS) {
mValue = characteristic.getValue();
Log.w(TAG,"onCharacteristicRead [" + mValue + "]");
gatt.disconnect();
gatt.close();
onRead();
}
else {
gatt.disconnect();
gatt.close();
}
}
}
This current method works perfectly fine for devices running KitKat and below. But when I run the same function on Lollipop, it beeps the device a couple of times and then stops working. From then on wards, whenever I try to connect, it says the device is disconnected and gives me an error code of 257 in OnConnectionStateChanged method.
I also get this error whenever I call this method -
04-20 14:14:23.503 12329-12384/com.webble.xy W/BluetoothGatt﹕ Unhandled exception in callback
java.lang.NullPointerException: Attempt to invoke virtual method 'void android.bluetooth.BluetoothGattCallback.onConnectionStateChange(android.bluetooth.BluetoothGatt, int, int)' on a null object reference
at android.bluetooth.BluetoothGatt$1.onClientConnectionState(BluetoothGatt.java:181)
at android.bluetooth.IBluetoothGattCallback$Stub.onTransact(IBluetoothGattCallback.java:70)
at android.os.Binder.execTransact(Binder.java:446)
IS there anyone who has faced the same problem? I never encountered the object to be null when ever I tried debugging.
The problem has been reported to Google as Issue 183108: NullPointerException in BluetoothGatt.java when disconnecting and closing.
A workaround is to call disconnect()
when you want to close BLE connection - and then only call close()
in the onConnectionStateChange
callback:
public void shutdown() {
try {
mBluetoothGatt.disconnect();
} catch (Exception e) {
Log.d(TAG, "disconnect ignoring: " + e);
}
}
private final BluetoothGattCallback mGattCallback =
new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt,
int status, int newState) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
try {
gatt.close();
} catch (Exception e) {
Log.d(TAG, "close ignoring: " + e);
}
}
}
Here is my full source code (a class doing normal scan, directed scan - and discovering services):
public class BleObject {
public static final String ACTION_BLUETOOTH_ENABLED = "action.bluetooth.enabled";
public static final String ACTION_BLUETOOTH_DISABLED = "action.bluetooth.disabled";
public static final String ACTION_DEVICE_FOUND = "action.device.found";
public static final String ACTION_DEVICE_BONDED = "action.device.bonded";
public static final String ACTION_DEVICE_CONNECTED = "action.device.connected";
public static final String ACTION_DEVICE_DISCONNECTED = "action.device.disconnected";
public static final String ACTION_POSITION_READ = "action.position.read";
public static final String EXTRA_BLUETOOTH_DEVICE = "extra.bluetooth.device";
public static final String EXTRA_BLUETOOTH_RSSI = "extra.bluetooth.rssi";
private Context mContext;
private IntentFilter mIntentFilter;
private LocalBroadcastManager mBroadcastManager;
private BluetoothAdapter mBluetoothAdapter;
private BluetoothGatt mBluetoothGatt;
private BluetoothLeScanner mScanner;
private ScanSettings mSettings;
private List<ScanFilter> mScanFilters;
private Handler mConnectHandler;
public BleObject(Context context) {
mContext = context;
if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
Log.d(TAG, "BLE not supported");
return;
}
BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothAdapter = bluetoothManager.getAdapter();
if (mBluetoothAdapter == null) {
Log.d(TAG, "BLE not accessible");
return;
}
mIntentFilter = new IntentFilter();
mIntentFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
mIntentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
mSettings = new ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build();
mScanFilters = new ArrayList<ScanFilter>();
mConnectHandler = new Handler();
mBroadcastManager = LocalBroadcastManager.getInstance(context);
}
public boolean isEnabled() {
return (mBluetoothAdapter != null && mBluetoothAdapter.isEnabled());
}
private ScanCallback mScanCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
processResult(result);
}
@Override
public void onBatchScanResults(List<ScanResult> results) {
for (ScanResult result: results) {
processResult(result);
}
}
private void processResult(ScanResult result) {
if (result == null)
return;
BluetoothDevice device = result.getDevice();
if (device == null)
return;
Intent i = new Intent(Utils.ACTION_DEVICE_FOUND);
i.putExtra(Utils.EXTRA_BLUETOOTH_DEVICE, device);
i.putExtra(Utils.EXTRA_BLUETOOTH_RSSI, result.getRssi());
mBroadcastManager.sendBroadcast(i);
}
};
private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
if (gatt == null)
return;
BluetoothDevice device = gatt.getDevice();
if (device == null)
return;
Log.d(TAG, "BluetoothProfile.STATE_CONNECTED: " + device);
Intent i = new Intent(Utils.ACTION_DEVICE_CONNECTED);
i.putExtra(Utils.EXTRA_BLUETOOTH_DEVICE, device);
mBroadcastManager.sendBroadcast(i);
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
Log.d(TAG, "BluetoothProfile.STATE_DISCONNECTED");
Intent i = new Intent(Utils.ACTION_DEVICE_DISCONNECTED);
mBroadcastManager.sendBroadcast(i);
// Issue 183108: https://code.google.com/p/android/issues/detail?id=183108
try {
gatt.close();
} catch (Exception e) {
Log.d(TAG, "close ignoring: " + e);
}
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (gatt == null)
return;
for (BluetoothGattService service: gatt.getServices()) {
Log.d(TAG, "service: " + service.getUuid());
for (BluetoothGattCharacteristic chr: service.getCharacteristics()) {
Log.d(TAG, "char: " + chr.getUuid());
}
}
}
};
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) {
final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
switch (state) {
case BluetoothAdapter.STATE_TURNING_OFF: {
Log.d(TAG, "BluetoothAdapter.STATE_TURNING_OFF");
break;
}
case BluetoothAdapter.STATE_OFF: {
Log.d(TAG, "BluetoothAdapter.STATE_OFF");
Intent i = new Intent(Utils.ACTION_BLUETOOTH_DISABLED);
mBroadcastManager.sendBroadcast(i);
break;
}
case BluetoothAdapter.STATE_TURNING_ON: {
Log.d(TAG, "BluetoothAdapter.STATE_TURNING_ON");
break;
}
case BluetoothAdapter.STATE_ON: {
Log.d(TAG, "BluetoothAdapter.STATE_ON");
Intent i = new Intent(Utils.ACTION_BLUETOOTH_ENABLED);
mBroadcastManager.sendBroadcast(i);
break;
}
}
} else if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)) {
final int state = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.ERROR);
final int prevState = intent.getIntExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, BluetoothDevice.ERROR);
if (state == BluetoothDevice.BOND_BONDED &&
prevState == BluetoothDevice.BOND_BONDING) {
if (mBluetoothGatt != null) {
BluetoothDevice device = mBluetoothGatt.getDevice();
if (device == null)
return;
Intent i = new Intent(Utils.ACTION_DEVICE_BONDED);
i.putExtra(Utils.EXTRA_BLUETOOTH_DEVICE, device);
mBroadcastManager.sendBroadcast(i);
}
}
}
}
};
// scan for all BLE devices nearby
public void startScanning() {
Log.d(TAG, "startScanning");
mScanFilters.clear();
// create the scanner here, rather than in init() -
// because otherwise app crashes when Bluetooth is switched on
mScanner = mBluetoothAdapter.getBluetoothLeScanner();
mScanner.startScan(mScanFilters, mSettings, mScanCallback);
}
// scan for a certain BLE device and after delay
public void startScanning(final String address) {
Log.d(TAG, "startScanning for " + address);
mScanFilters.clear();
mScanFilters.add(new ScanFilter.Builder().setDeviceAddress(address).build());
// create the scanner here, rather than in init() -
// because otherwise app crashes when Bluetooth is switched on
mScanner = mBluetoothAdapter.getBluetoothLeScanner();
mScanner.startScan(mScanFilters, mSettings, mScanCallback);
}
public void stopScanning() {
Log.d(TAG, "stopScanning");
if (mScanner != null) {
mScanner.stopScan(mScanCallback);
mScanner = null;
}
mScanFilters.clear();
}
public void connect(final BluetoothDevice device) {
Log.d(TAG, "connect: " + device.getAddress() + ", mBluetoothGatt: " + mBluetoothGatt);
mConnectHandler.post(new Runnable() {
@Override
public void run() {
setPin(device, Utils.PIN);
mBluetoothGatt = device.connectGatt(mContext, true, mGattCallback);
}
});
}
private void setPin(BluetoothDevice device, String pin) {
if (device == null || pin == null || pin.length() < 4)
return;
try {
device.setPin(pin.getBytes("UTF8"));
} catch (Exception e) {
Utils.logw("setPin ignoring: " + e);
}
}
// called on successful device connection and will toggle reading coordinates
public void discoverServices() {
if (mBluetoothGatt != null)
mBluetoothGatt.discoverServices();
}
public boolean isBonded(BluetoothDevice device) {
Set<BluetoothDevice> bondedDevices = mBluetoothAdapter.getBondedDevices();
if (bondedDevices == null || bondedDevices.size() == 0)
return false;
for (BluetoothDevice bondedDevice: bondedDevices) {
Log.d(TAG, "isBonded bondedDevice: " + bondedDevice);
if (bondedDevice.equals(device)) {
Log.d(TAG, "Found bonded device: " + device);
return true;
}
}
return false;
}
public void startup() {
try {
mContext.registerReceiver(mReceiver, mIntentFilter);
} catch (Exception e) {
Log.d(TAG, "registerReceiver ignoring: " + e);
}
}
public void shutdown() {
Log.d(TAG, "BleObject shutdown");
try {
mContext.unregisterReceiver(mReceiver);
} catch (Exception e) {
Log.d(TAG, "unregisterReceiver ignoring: " + e);
}
try {
stopScanning();
} catch (Exception e) {
Log.d(TAG, "stopScanning ignoring: " + e);
}
try {
mBluetoothGatt.disconnect();
} catch (Exception e) {
Log.d(TAG, "disconnect ignoring: " + e);
}
mConnectHandler.removeCallbacksAndMessages(null);
}
}
On each BLE event it broadcasts intents via LocalBroadcastManager
.
Thanks for pointing the problem Shashank.
I have looked at the Google example and followed what they recommend, it works like a charm with my device.
You should call the close() function in your onUnbind and the disconnect() when you need to, for example when you quit your application.