The story so far:
Next our discussion turns to Android. Android has had complete support for BLE/GATT for a while. It offers slightly more configurability than iOS in options such as power levels and advertisement data.
There are some gotchas. Support for acting as a central was introduced in Android 4.3 (API level 18). Support for acting as a peripheral didn’t arrive until Android 5.0 (API level 21). I hope that you, dear reader, don’t need to support KitKat. If you do, it won’t be able to broadcast BLE advertisements in any case.
There is also a significant number of bugs and performance problems. Some of this can be attributed to Android; much to poor implementations in specific hardware models. There is some voodoo involved in trying to keep the widest range of hardware happy. It involves things like judicious use of the main thread and attempting to gracefully work around the notorious error 133. These problems afflict centrals more than peripherals so I’ll address them in the next post.
While I can’t speak for the latest versions, matters seem to be improving with time. I’ve had the most trouble with Android 5.0 devices and somewhat less with 6.0. The difficulty with the Android ecosystem is that so many devices in the wild are stuck on old versions. At the time of writing 5.x still has a 22.4% market share and 6.0 has 25.5% (source).
I wasn’t able to find any official walkthroughs of Android peripheral code and I don’t want to get into the Java here. For code examples I’ll refer a couple of times to a nice blog post on nilhcem.com.
Android Permissions for Bluetooth LE
To use the full range of BLE functions an app must have the
BLUETOOTH_ADMIN privileges. See this documentation for a manifest example.
More surprisingly, scanning on Android 6.0+ requires
ACCESS_FINE_LOCATION. The rationale for this is that BLE peripherals are often iBeacons representing particular locations. If the app happens to identify one then it knows where the user is. It… makes sense I suppose? Communicating this requirement effectively to users is left as an exercise for the reader.
Configuring a GATT Server
The process of setting up a tree of services and characteristics is conceptually very similar to on iOS. There is a class representing a single characteristic, another representing a single service, and another representing the overall GATT server (peripheral). You instantiate the “small” ones and add them to the bigger ones until the entire data model is defined.
BluetoothGattCharacteristic is initialised with a UUID, plus some properties and permissions flags. There are many possible flags but the most important ones concern reading and writing. If it’s readable it should have
PERMISSION_READ; if it’s writable it should
PERMISSION_WRITE. If it needs to support both reading and writing the flags can be bitwise-ORed together:
PERMISSION_READ | PERMISSION_WRITE.
The service is represented by a
BluetoothGattService. Initialise one with the service UUID, and you will probably want to use the primary service type unless you already know otherwise. The corresponding characteristics can then be added to it with
BluetoothGattServerCallback and open a
BluetoothGattServer, providing your callback. Services can be added to it with
addService(). It won’t actually broadcast any advertisements – we’ll get to that in a moment. Your callback implementation is what does all the work of handling connected devices and responding to read and write requests.
See the nilhcem.com blog post for a code example of configuring characteristics and services, as well as handling read and write requests. Note that they are also using a feature called characteristic descriptors which I haven’t discussed. It isn’t critical if you’re writing both the central and peripheral app which means you know exactly what the characteristics represent.
Advertising with BluetoothLeAdvertiser
startAdvertising() you need to prepare an
AdvertiseData.Builder, and implement the
AdvertiseCallback interface. This callback class will find out whether
startAdvertising() was successful or not. Like iOS, this is an asynchronous process and you should wait for the callback rather than assume it worked.
The “Start Advertising” section of the nilhcem blog post has a good example of preparing typical parameters.
Extra Advertisement Data with Scan Response
Android provides considerably more control over how advertisement packets are structured than iOS does. As seen above, an
AdvertiseData.Builder is used to construct the contents of the advertisement packet. This can include arbitrary “manufacturer data”, which is really cool. The trouble is that you can only have 31 total bytes of data in the packet. This is really tight and every field you add has overhead. Service UUIDs are really big unless your company can obtain a short UUID allocation.
One solution is an active scan request followed by a scan response. If a central hears a peripheral’s advertisement it can essentially call out “hey can I have a bit more info please?” The peripheral will oblige by broadcasting a second advertising packet. This scan response can contain another 31 bytes, separate from the original. These content of these two packets is merged when you receive it on a central.
On Android you provide the
scanResponse packet as a second instance of
AdvertiseData. There is a corresponding version of
startAdvertising() that takes two
AdvertiseData parameters. Conceptually:
Advertising Rate and Power
Android offers the developer control over the rate at which advertisements are broadcast – and it defaults to a low rate, which could take centrals a relatively long time to detect. This behaviour is controlled by the “mode” in
AdvertiseSettings. There are three constants corresponding to “low power” (the default), “balanced”, and “low latency”.
The exact rates are not specified in the documentation. I have always used the highest “low latency” rate. Anecdotally this produces a stream of advertisements at around 10 Hz. The others are reported to go down to ~4 Hz and 1 Hz. For comparison, iOS apps advertise at a steady 20 Hz.
What rate does your peripheral actually need? If the central user is going to open and interact with your app in order to locate nearby peripherals, their receiving radio will be fully powered. They will probably pick up even a 1 Hz advertisement within a second or two depending on ambient interference. If you’re relying on longer-term background scanning where the OS will be cutting corners to save power, more frequent advertisements means a better chance of being detected promptly.
The power level can also be adjusted using the
AdvertiseSettings. If it only makes sense for your app to communicate at short distances consider using a low power level. This will save energy, help to reduce interference with other Bluetooth devices, and require your centrals to get closer before they detect the peripheral.
This post has been a quick introduction to BLE on Android, the basic steps to act as a peripheral, and a look at some more advanced configuration options.
In the next post I will describe using the central role on Android, along with some guidelines for reducing the number of problems on older devices.