Tuesday, April 11, 2023

Developer Notes: Navigating Android’s Bluetooth LE Quirks

Introduction

We frequently work with the Android Bluetooth LE API. Earlier this year, we came across some particularly tricky issues. In resolving, we've learned a few things that I'd like to share in the hopes that it will help out fellow programmers who are stuck on some of the API’s quirkier behavior.


One particularly tricky issue I have encountered is this: After running a Bluetooth LE app for many connections, you may encounter one of these two situations: 

  • You keep getting onScanFailed with SCAN_FAILED_APPLICATION_REGISTRATION_FAILED from startScan, or:

  • You get a STATE_DISCONNECTED with status=133 on every connection


This can be especially difficult to debug if it takes days to reproduce, or to diagnose in the field where you do not always have access to the system log.

Common Solutions

One accepted solution that I’ve seen around sites like Stack Overflow is to programmatically reset the Bluetooth adaptor, like so: 

    
BluetoothAdapter.getDefaultAdapter().disable()
	
  

However, this no longer is allowed in later versions of Android, and only worked sporadically to fix the problem when it was allowed. Other articles I’ve seen have advised the user to do things such as reboot their phone, which isn’t a terribly great thing to have to ask users to do.


But if you were to check the log, you might see something like this:


    
bt_stack: [ERROR:gatt_api.cc(975)] can't Register GATT client, MAX client reached: 64
bt_stack: [ERROR:bta_gattc_act.cc(189)] Register with GATT stack failed.
    
  

How could this be? After all, you’re disconnecting from every connection, right?

Quick Refresher on Closing Connections

In order to disconnect from an Android Bluetooth LE connection, it’s not enough to simply call .disconnect() on the BluetoothGatt object. This simply begins the disconnection process. In order to truly disconnect, you must wait for onConnectionStateChange() to come in with a STATE_DISCONNECTED, call .close() on the BluetoothGatt object that it gives you, then null it out so that it can be garbage collected. Simple enough, right? Let’s see how it can go sideways.

Status 133

One of the less intuitive situations occurs when a STATE_DISCONNECTED call occurs with a status=133, which is documented as a non-specific GATT_FAILURE. If you look this up in the official documentation, it states “A GATT operation failed, errors other than the above,” which is fairly broad. In our case, this indicates that the Bluetooth connection has been dropped. To recover from this, close and null out the BluetoothGatt device as usual. But what happens if you are already in STATE_DISCONNECTED?


Here’s an example:

  1. You’re not connected to any Bluetooth device. That is, the current BluetoothGatt state is STATE_DISCONNECTED

  2. You call bluetoothDevice.connectGatt()

  3. Your onConnectionStateChange() is called with the new state STATE_DISCONNECTED and a status of 133.


First, you might be confused why you might get a STATE_DISCONNECTED when STATE_CONNECTED or even STATE_CONNECTING weren’t hit. This is because the states STATE_CONNECTING and STATE_DISCONNECTING are not actually used, despite them being listed in the documentation and them not being marked as deprecated.


The next point of confusion might be why you would get a STATE_DISCONNECTED when you were already in STATE_DISCONNECTED. This is especially strange considering the state parameter is “newState,” implying that it should be different from the old state. 


No, this isn’t a bug. What has happened is this: It never reached STATE_CONNECTED because it never successfully made the connection. This is typically due to a weak signal or other issue, hence the GATT_FAILURE status code.


So, what is the proper thing to do in this case to recover from this error? We know that when we want to close out a GATT connection, we call close() on our connected BluetoothGatt object. But what if it hasn’t connected in the first place? 


The answer is simple: You must close out any BluetoothGatt device passed into onConnectionStateChange when the status is STATE_DISCONNECTED: Each time you call connectGatt, a slot is opened. From this point forward, Android makes it your application’s responsibility to close it, even if it never opens the connection successfully.


If you step over connectGatt(), you will see something like this in the Android system log:


    
bt_stack: [INFO:gatt_api.cc(958)] GATT_Register   
bt_stack: [INFO:gatt_api.cc(995)] allocated gatt_if=13
	
  

This tells you it has registered a GATT device in that slot number. In fact, you can see the logic here: stack/gatt/gatt_api.c - platform/system/bt - Git at Google, starting on line 1206: It iterates through all of the GATT slots and if cannot find one that is free, you will see the error on line 1223: “can't Register GATT client, MAX client reached”. 


How this manifests can depend on your Android configuration: On my Pixel 5 with Android 13, I will get a GATT_FAILURE every time I attempt a connection.  On my Samsung S9 with Android 10, I get a SCAN_FAILED_APPLICATION_REGISTRATION_FAILED every time I even attempt a startScan. Either way, the issue is the same: There are no more GATT slots available for your app.

Slow to Disconnect

There’s one more thing I’d like to mention, which is unrelated to the above issue: There are times when an application would like to do something immediately after a Bluetooth device disconnects. 


But when is that? You might know that even after you get an onConnectionStateChange() with STATE_DISCONNECTED that you are not yet disconnected from the Bluetooth device, because you have yet to call close() on BluetoothGatt. But did you know that even after closing the BluetoothGatt device and nulling it out that it can take a while to disconnect?


According to BluetoothManager’s getConnectedDevices(), your device can still be connected for a few seconds after the BluetoothGatt device is closed out. If you need to do something with a device immediately after disconnection, it might be worthwhile to poll getConnectedDevices() until your device’s address disappears from the list.

The Wrap-Up

I hope these tips have helped you make sense of some counterintuitive behaviors in the Android Bluetooth LE API. If you feel there’s something that should be included here, or something we might have missed, please feel free to contact us.

Further Reading

Aside from the official documentation, our friends over at PunchThrough have written an excellent article about using the Bluetooth LE API. The article goes over everything else: From all the different scanning options, to the mysterious “autoConnect” parameter, to persistent background connections. It is well worth a read here: https://punchthrough.com/android-ble-guide/


No comments:

Post a Comment