BLuetooth Gatt Callback not working with new API for Lollipop

Shashank Hebbale picture Shashank Hebbale · Apr 20, 2015 · Viewed 13.4k times · Source

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.

Answer

Alexander Farber picture Alexander Farber · Nov 17, 2015

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.