Making Android BLE work — part 2

Martijn van Welie
14 min readApr 6, 2019

--

In my previous article I extensively discussed the topic of scanning. In this article we’ll look at connecting, disconnecting and discovering services.

Connecting to a device

After you have found your device by scanning for it, you must connect to it by calling connectGatt(). It returns a BluetoothGatt object that you will then use for all GATT related operations like reading and writing characteristics. However, there are 2 versions of the connectGatt() method! Later versions of Android have added even more variations but since we want to be Android 6 compatible we only look at these two:

BluetoothGatt connectGatt(Context context, boolean autoConnect,
BluetoothGattCallback callback)
BluetoothGatt connectGatt(Context context, boolean autoConnect,
BluetoothGattCallback callback, int transport)

The internal implementation of the first call is that Android calls the second one with the value TRANSPORT_AUTO for the transport parameter. If you want to connect over BLE this is not the right value. TRANSPORT_AUTO is for devices that support both BLE and classic Bluetooth. It means you have ‘no preference’ for the type of connection that is made and want Android to choose. It is totally unclear how Android chooses so this may lead to rather unpredictable results and many people have reported issues. That is why you should always use the second version and pass TRANSPORT_LE for the transport parameter:

BluetoothGatt gatt = device.connectGatt(context, false, bluetoothGattCallback, TRANSPORT_LE);

The first parameter is the application context that Android simply needs to do a connection.

The second parameter is the autoconnect parameter and indicates whether you want to connect immediately or not. So using false means ‘connect immediately’ and Android will try to connect for 30 seconds on most phones and then it times out. When a connection times out, you will receive a connection update with status code 133. This is not the official error code for a connection timeout though . It is defined in the google source code as GATT_ERROR. You’ll get this error on other occasions as well unfortunately. Also keep in mind that you can issue only one connect at a time using false, because Android will cancel any other connect with value false, if there is one.

The next parameter is the BluetoothGattCallback callback you want to use for this device. This is not the same callback as we used for scanning. This callback will be used for all device specific operations like reading and writing. We’ll go into detail about this in the next article.

Autoconnect = true

If you set autoconnect to true, Android will connect whenever it sees the device and this call will never time out. So internally the stack will scan itself and when it sees the device it will connect to it. This is handy if you want to reconnect to a known device whenever it becomes available. In fact, this is the preferred way to reconnect to do so! You simply create a BluetoothDevice object and call connectGattwith the autoconnect set to true.

BluetoothDevice device = bluetoothAdapter.getRemoteDevice("12:34:56:AA:BB:CC");BluetoothGatt gatt = device.connectGatt(context, true, bluetoothGattCallback, TRANSPORT_LE);

Keep in mind that this only works if the device is in the Bluetooth cache or it is has been bonded before! See my previous article for more explanation about caching. However, rebooting your phone or toggling Bluetooth on/off will clear the cache so you must perform necessary checks before using autoconnect! Really annoying…

Autoconnect only works for cached or bonded devices!

In order to know if a device has been cached you can use a small trick. After creating a BluetoothDevice you should do getType() and if it returns TYPE_UNKNOWN the device is obviously not cached. If this is the case, you must first scan for the device with this mac address (using a non-aggressive scan mode) and after that you can use autoconnect again.

To make matters worse, there is a known bug in Android 6 and lower where a race condition could lead to an autoconnect becoming a normal connect. Luckily, some clever guys from Polidea found a workaround for this. I strongly advice you use their workaround if you plan to use autoconnect!

Autoconnecting works well nowadays so you should give it a try. However, the big downside of autoconnect is that is takes a bit longer to connect compared to scanning aggressively first and then connecting with autoconnect set to false. This is because Android internally uses a low-power settings scan. To make matters worse, the time to connect also varies per phone manufacturer! To connect quickly, first scan aggressively and then connect with autoconnect set to false. Another advantage of autoconnect is that you can issue many of them. But with non-autoconnect you can only do one connection attempt at a time!

Connection state changes

After calling connectGatt() the stack will let you know the result on the onConnectionStateChange callback. This callback is called for every connection state change.

Dealing with this callback is not trivial! Don’t be fooled by over simplistic examples you find plenty on the internet. Most of the simplistic examples look something like this:

public void onConnectionStateChange(final BluetoothGatt gatt, final int status, final int newState) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
gatt.discoverServices();
} else {
gatt.close();
}
}

As you can see, the code only looks at the newState parameter and totally ignores the status parameter. However, in many cases this code may get you a long way and is not entirely wrong! When you are connected the next thing to do is indeed to call discoverServices(). And if you are disconnected you indeed need to call close() to release resources in the Android stack. This is actually super important for making BLE work on Android so let’s discuss it immediately!

When you call connectGatt the stack internally registers a new ‘client interface’ (clientIf). You may have noticed a line like this in the logcat:

D/BluetoothGatt: connect() - device: B0:49:5F:01:20:XX, auto: false
D/BluetoothGatt: registerApp()
D/BluetoothGatt: registerApp() — UUID=0e47c0cf-ef13–4afb-9f54–8cf3e9e808d5
D/BluetoothGatt: onClientRegistered() — status=0 clientIf=6

It shows that client ‘6’ got registered after I called connectGatt. Android has a limit of 30 clients (as defined by the GATT_MAX_APPS constant in the stack’s source code) and if you reach it it will not connect to devices anymore and you’ll get an error! Strangely enough, directly after booting your phone will be already on 5 or 6 so I guess Android itself uses the first ones. So if you never call close() you will see this number go up every time you call connectGatt. When you call close(), the stack will unregister your callback, free up the client internally and you will see the number go down again.

D/BluetoothGatt: close()
D/BluetoothGatt: unregisterApp() — mClientIf=6

So remember that whatever you do, you always call close() after you get disconnected! Let’s park disconnection scenarios for now since there is a lot more to say about that.

The connection state

The newState variable contains the new connection state and can have 4 potential values: STATE_CONNECTED, STATE_DISCONNECTED, STATE_CONNECTING, STATE_DISCONNECTING.

I guess these states pretty much speak for themselves. Although STATE_CONNECTING and STATE_DISCONNECTING are possible according to the documentation I have never ever seem them in practice. So if you don’t handle them you’re probably ok. But to make sure I suggest you explicitly handle them and only call close() if you are really disconnected. In my apps I usually handle the state explicitly but I don’t do anything.

The status field

In the example I gave, the status field was totally ignored but it is actually quite important to handle. This field is essentially an error code. If you receive GATT_SUCCESS it means the connection state change was the result of a successful operation like connecting but it could also be because you wanted to disconnect. In most cases you’ll want to handle an unexpected disconnect differently from a desired disconnect you issued yourself!

If you receive any other value than GATT_SUCCESS, something went wrong and the status field will tell you the reason for it. Unfortunately very few error codes are exposed by the BluetoothGatt object. If you want to know all of them you can have a look here. The most common one you’ll encounter in practice is status code 133 which is GATT_ERROR. That just means ‘something went wrong’….not very helpful. More about GATT_ERROR later…

So now that we know what the newStateandstatus fields are we can do a better version of the onConnectionStateChange calllback.

public void onConnectionStateChange(final BluetoothGatt gatt, final int status, final int newState) {if(status == GATT_SUCCESS) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
// We successfully connected, proceed with service discovery
gatt.discoverServices();
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
// We successfully disconnected on our own request
gatt.close();
} else {
// We're CONNECTING or DISCONNECTING, ignore for now
}
} else {
// An error happened...figure out what happened!
...
gatt.close();
}

This is not our final implementation and we’ll expand it further in this article. But at least we have now singled out error cases from success cases.

The missing parameter: bond state

The last parameter also needs to be taken into account in onConnectionStateChange is the bond state. As it is not passed as a parameter we need to get it like this:

int bondstate = device.getBondState();

The bond state can be BOND_NONE, BOND_BONDING or BOND_BONDED. Each of these states have an impact on how you should deal with becoming connected:

  • If BOND_NONE: no problem, you can call discoverServices()
  • If BOND_BONDING: bonding is in progress, don’t call discoverServices() since the stack is busy and this may cause a loss of connection and discoverServices() will fail! After bonding is complete you should call discoverServices(). We’ll show how to do that in our article on bonding.
  • if BOND_BONDED: if you are on Android 8 or higher, you can call discoverServices() immediately but if not you may have to add a delay. On Android 7 or lower, and if your device has the Service Changed Characteristic, the Android stack is still busy handling it and calling discoverServices() without a delay would make it fail. So you have to add a 1000–1500 ms delay. The exact delay time needed depends on the number of characteristics of your device. Since at this point you don’t know yet if the device has the Service Changed Characteristic it is recommendable to simply always to a delay.

So you have to factor in the bond state as well, next to the connection state and status field. Here is how I do it:

if (status == GATT_SUCCESS) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
int bondstate = device.getBondState();
// Take action depending on the bond state
if(bondstate == BOND_NONE || bondstate == BOND_BONDED) {

// Connected to device, now proceed to discover it's services but delay a bit if needed
int delayWhenBonded = 0;
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) {
delayWhenBonded = 1000;
}
final int delay = bondstate == BOND_BONDED ? delayWhenBonded : 0;
discoverServicesRunnable = new Runnable() {
@Override
public void run() {
Log.d(TAG, String.format(Locale.ENGLISH, "discovering services of '%s' with delay of %d ms", getName(), delay));
boolean result = gatt.discoverServices();
if (!result) {
Log.e(TAG, "discoverServices failed to start");
}
discoverServicesRunnable = null;
}
};
bleHandler.postDelayed(discoverServicesRunnable, delay);
} else if (bondstate == BOND_BONDING) {
// Bonding process in progress, let it complete
Log.i(TAG, "waiting for bonding to complete");
}
....

Dealing with errors

Now that we dealt with successful operations we need to look at the errors. There are a bunch of cases that are actually very normal that will present themselves as ‘errors’:

  • The device disconnected itself on purpose. For example, because all data has been transferred and there is nothing else to to. You will receive status 19 (GATT_CONN_TERMINATE_PEER_USER).
  • The connection timed out and the device disconnected itself. In this case you’ll get a status 8 (GATT_CONN_TIMEOUT)
  • There was an low-level error in the communication which led to the loss of the connection. Typically you would receive a status 133 (GATT_ERROR) or a more specific error code if you are lucky!
  • The stack never managed to connect in the first place. In this case you will also receive a status 133 (GATT_ERROR)
  • The connection was lost during service discovery or bonding. In this case you will want to investigate why this happened and perhaps retry the connection.

The first two cases are totally normal and there is nothing else to do than call close() and perhaps do some internal cleanup like disposing of the BluetoothGatt object.

In the other cases, you may want to do something like informing other parts of your app or showing something in the UI. If there was a communication error you might be doing something wrong yourself. Alternatively, the device might be doing something wrong. Either way, something to deal with! It is a bit up to you to what extend you want to deal with all possible cases.

Have a look at my version in my Blessed library how I did it.

Status 133 when connecting

It is very common to see a status 133 when trying to connect to a device, especially while you are developing your code. The status 133 can have many causes and some of them you can control:

  • Make sure you always call close() when there is a disconnection. If you don’t do this you’ll get a 133 for sure next time you try.
  • Make sure you always use TRANSPORT_LE when calling connectGatt()
  • Restart your phone if you see this while developing. You may have corrupted the stack by debugging and it is in a state where it doesn’t behave normal again. Restarting your phone may fix things.
  • Make sure your device is advertising. The connectGatt with autoconnect set to false times out after 30 seconds and you will receive a 133.
  • Change the batteries of your device. Devices typically start behaving erratically when they battery level is very low.

If you have tried all of the above and still see status 133 you need to simply retry the connection! This is one of the Android bugs I never managed to understand or find a workaround for. For some reason, you sometimes get a 133 when connecting to a device but if you call close() and retry it works without a problem! I suspect there is an issue with the Android cache that causes all this and the close() call puts it back in a proper state. But I am really just guessing here…If anybody figures out how to solve this one, let me know!

Disconnecting on your request

If you want to disconnect yourself you need to do the following:

  • Call disconnect()
  • Wait for the callback on onConnectionStateChange to come in
  • Call close()
  • Dispose the gatt object

The disconnect() command will actually do the disconnect and will also update the internal connection state of the Bluetooth stack. It will then trigger a callback to onConnectionStateChange to notify you that the new state is now ‘disconnected’.

The close() call will unregister your BluetoothGattCallback and free up the ‘client interface’ as we already discussed.

Lastly disposing of the BluetoothGatt object will free up other resources related to the connection.

Disconnecting ‘the wrong way’

If you look at examples you can find on the Internet you’ll see that some people disconnect a bit differently.

Sometimes you see this:

  • Call disconnect()
  • Call close() directly after

This will work ‘more or less’. The device will disconnect, but you may never receive the callback with the ‘disconnected’ state. This is because disconnect() is asynchronous and close() unregisters the callback immediately! So by the time Android is ready to trigger the callback, it can’t call the callback because you already unregistered it!

Sometimes people don’t call disconnect() and only call close(). This will ultimately disconnect the device but is not the right way since disconnect() is where the Android stack actually updates the state internally. It not only disconnects an active connection but will also cancel an autoconnect that is pending. So if you only call close(), any autoconnect that is pending may still lead to a new connection!

Cancelling a connection attempt

If you have called connectGatt() and want to cancel the connection attempt, you need to call disconnect(). Since you are not connected yet, you will not get a callback on onConnectionStateChange! So wait a couple of milliseconds for disconnect() to finish and then call close() to clean up.

When you cancel a connection successfully you’ll see this in your log:

D/BluetoothGatt: cancelOpen() — device: CF:A9:BA:D9:62:9E

You will probably never cancel a connection with autoconnect set to false. But it is very common to do this for a connection with autoconnect set to true. For example, you typically want to connect to devices when your app is in the foreground but when your apps goes to the background you may want to stop connecting and hence cancel the pending connections.

Discovering services

Once you are connected to a device you must discover it’s services by calling discoverServices(). This will cause the stack to issues a series of low-level commands to retrieves the services, characteristics and descriptors. This may take a bit, typically a full second or so depending on how many services, characteristics and descriptors your device has. When Android is done it will call onServicesDiscovered.

When service discovery is done, the first thing you must check is to see if there was an error:

// Check if the service discovery succeeded. If not disconnect
if (status == GATT_INTERNAL_ERROR) {
Log.e(TAG, "Service discovery failed");
disconnect();
return;
}

If there was an error (typically GATT_INTERNAL_ERROR which has value 129), you must disconnect since there is no way that you’ll be able to do something meaningful. You can’t turn on notifications nor read/write characteristics. So just disconnect and retry the connection.

If everything went fine you can the proceed to grab the list of services and do your own processing:

final List<BluetoothGattService> services = gatt.getServices();
Log.i(TAG, String.format(Locale.ENGLISH,"discovered %d services for '%s'", services.size(), getName()));
// Do additional processing of the services
...

Caching of services

The Android stack caches the services, characteristics and descriptors it found. So the first connection will trigger a real service discovery and subsequent connections, a cached version will be returned. This is in compliance with the Bluetooth standard. This is normally totally fine and speeds up the time it takes before your can start to receive data from a device.

However, in some edge cases you may want to clear the services cache so that the services are actually discovered again at the next collection. The typical use-case for this would be a scenario where a firmware update would change the services or characteristics that your device has. There is a hidden method to clear the services that you can call using reflection. Here is an example how to do that:

private boolean clearServicesCache()
{
boolean result = false;
try {
Method refreshMethod = bluetoothGatt.getClass().getMethod("refresh");
if(refreshMethod != null) {
result = (boolean) refreshMethod.invoke(bluetoothGatt);
}
} catch (Exception e) {
HBLogger.e(TAG, "ERROR: Could not invoke refresh method");
}
return result;
}

Keep in mind that this method is asynchronous, so give it some time to complete!

Odd stuff regarding connecting and disconnecting

Although connecting and disconnecting sounds straightforward, there are some oddities you should be aware of.

  • You may see an occasional error 133 when trying to connect. I already explained this earlier in this article and to avoid them as much as you can.
  • Sometimes a connect call simply hangs and no timeout happens nor is the onConnectionStateChange callback ever called. This doesn't happen often but I have seen it happen when the batteries of the peripheral are almost empty, or when you are on the edge of the Bluetooth range. My guess is that some communication takes places but then halts and the stack hangs. My workaround is to start your own connection timer and disconnect/close after timing out. It is basically a safeguard to for when the stack hangs.
  • Some phones seem to have an issue with connecting while scanning. The Huawei P8 Lite is one of the reported phones to have this issue. So make sure you stop your scanner before connecting.
  • All calls related to connecting and disconnecting are asynchronous. So the return immediately but it will take some time for the Bluetooth stack to execute them. So avoid making several calls very fast after each other.

Next article: reading and writing characteristics

Now that we covered connecting/disconnecting and discovering services, we will go in-depth on reading and writing characteristics in the next article.

If you want to get started with BLE immediately, try my Blessed library for Android. It uses all the techniques described in this series of articles on BLE on Android and simplifies the use of BLE in your apps!

--

--