BLuetooth Gatt Callback not working with new API for Lollipop

14,095

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.

Share:
14,095
Shashank Hebbale
Author by

Shashank Hebbale

Updated on June 24, 2022

Comments

  • Shashank Hebbale
    Shashank Hebbale about 2 years

    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.

  • swooby
    swooby over 7 years
    Is there any guarantee that calling disconnect() will result in onConnectionStateChange(..., newState=BluetoothProfile.STATE_DISCONNECTED) being called?