Bluetooth LE for Mobile - 4. Android Peripheral
The story so far:
- An Overview
- iOS Peripheral
- iOS Central
- Android Peripheral
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
and BLUETOOTH_ADMIN
privileges. See this documentation for a manifest example.
More surprisingly, scanning on Android 6.0+ requires ACCESS_COARSE_LOCATION
or 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.
A 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 PROPERTY_READ
and PERMISSION_READ
; if it’s writable it should PROPERTY_WRITE
and 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 addCharacteristic()
.
Finally, implement 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
A separate class called BluetoothLeAdvertiser
does the specific job of broadcasting advertisements. You obtain one via BluetoothAdapter
.
To call startAdvertising()
you need to prepare an AdvertiseSettings
using AdvertiseSettings.Builder
, AdvertiseData
using 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.
Summary
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.