I gave a highlight on the limitations of Google Nearby while implementing Contact Tracing service in Part 1 of this series. In this part, I'll discuss another approach I took.
Trial 2 - Bluetooth Smart - Advertising Mode
While thinking of a solution that would advertise a token in the background on Android, I decided to give Bluetooth Smart aka Bluetooth Low Energy a try. BLE is used widely in fitness and music applications that need to stream a payload between two devices and obviously background subscription and advertisement is supported.
Some BLE stuff
BLE is a low power technology to connect devices. This operates in the 2.4 GHz ISM (Industrial, Scientific, Medical) band alongside your cool devices such as garage door openers, baby monitors and tech like WiFi and NFC. All those years spent studying the Radio Frequency spectrum in my Electrical and Electronics Engineering class paid off here 👨🏾🎓. The Bluetooth Special Interest Group is in charge of standardizing and evolving this technology. These folks introduced BLE in version 4.0 of the Bluetooth Core Specification to target very low power applications such as those powered by a coin cell -- think Beacons etc.
Now, bear with me here. We have to go into some terminology used in BLE in order to understand how to implement our application. I'll reference, at the end, some great material I came across to help me understand this stuff.
- Server: device that has some data to expose - think of your Smart Watch (technically, receives GATT requests/commands and sends responses)
- Client: device that receives data exposed by server e.g. your dearly loved smartphone (technically, initiates GATT requests/commands and accepts responses)
- Attribute: structured data element exposed by a device
- Attribute Protocol (ATT): details how a server exposes data to a client and how the data is structured
- Profile: a set of features or services that a device has
- Generic Access Profile (GAP): defines how two devices discover and establish connections. Also acts as a basis for all other profiles
- Generic Attribute Profile (GATT): facilitates profile discovery and defines how attributes are grouped together
- Characteristic: a single value (exposed by a server) -- contains properties and descriptors
- Characteristic Properties: determine how to use ta characteristic's value
- Descriptor: contains further information about a characteristic's value e.g. providing units of the value such as meters, nits etc
- Service: a collection of attributes -- contains characteristics and other metadata
- Universally unique Identifier: a 128-bit string used to identify BLE attributes
BLE compliant devices can operate in one or more GAP roles at the same time:
- Broadcaster: sends advertisements, doesn't allow connections
- Observer: scans and reads advertisements, doesn't initiate connections
- Peripheral: sends advertisements, allows connections
- Central: scans and reads advertisements, initiates connections
Again, lot's of info but check out the references at the end of the article on some great tutorials and in depth coverage of BLE.
Android provides support for BLE as from API level 18. Its API has evolved a bit from accessing the bluetooth adapter using the static
getDefaultAdapter to having it provided from a system service:
Context.BLUETOOTH_SERVICE -- one of the neat things I find in Android (allows apps to share cacheable system resources via a common api).
The entry point to using BLE is a system service called 'bluetooth' -- the value for Context.BLUETOOTH_SERVICE. It is accessible as the
BluetoothManager class. We then use this to get a
BluetoothAdapter which is the representation of the our device's Bluetooth adapter -- we'll use this adapter to start device discovery and other common Bluetooth tasks. Next, we get the
BluetoothLeAdvertiser from the adapter. This class allows us to advertise the Contact Trace token. In duality, we'll need to perform scans for devices broadcasting their token and for this enters
BluetoothLeScanner class which we also get from the bluetooth adapter. And last but not least, we need the
BluetoothGattServer that will enable us to create, register and serve our services. Here's my initialization listing just described:
lazyNone is my own sane way of writing non-synchronized property initialization since I guarantee this initialization is done on one thread. It's essentially a wrapper around, Kotlin's
lazy generic function with
NONE. Here's the declaration:
One thing to note is that bluetooth operations are asynchronous in nature and thus you have to work with callbacks. I came across an alternative RxAndroidBle which is an Rx wrapper around Android's Bluetooth framework. Nordic Semiconductor has an Android BLE library they claim makes working with Bluetooth LE on Android a pleasure. I didn't have time to explore these options but they all seem promising.
A couple of permissions are required for a BLE app:
- android.Manifest.permission.BLUETOOTH - obviously 😎
- android.Manifest.permission.BLUETOOTH_ADMIN e.g. required when advertising data
- android.permission.ACCESS_FINE_LOCATION for API level 29+ or ACCESS_FINE_LOCATION for <= API level 28 because... LE beacons
Advertising and Scans are possible in the background -- Finally! 🥳 I use a foreground service notifying the user that Contact Tracing is active. The user has explicit control over when to start or stop Contact Tracing.
Apple added BLE a little earlier. They introduced CoreBluetooth framework in iOS 5.0. iOS uses
delegates extensively in Cocoa. In fact, I would call it the Delegate Framework and CoreBluetooth is no different.
To encapsulate Bluetooth implementation, I created a TraceService singleton class -- Apple style. To have the iOS device work in the Central role, you use the
CBCentralManager. Construction is straight forward and takes in a
CBCentralManagerDelegate (in which my TraceService conforms via an extension), pass
nil to use the main queue and
CBCentralManagerOptionRestoreIdentifierKey in the options parameter (check out Central Manager State Restoring Options.)
For a peripheral role,
CBPeripheralManager is your friend. Initialization is a breeze; provide a
CBPeripheralManagerDelegate (my TraceService conforms to this as well),
nil to use main dispatch queue, and
CBPeripheralManagerOptionRestoreIdentifierKey with the restoration id for the options dictionary (check out Peripheral Manager Initialization Options.). This is the manager used to add services to the GATT database and advertise it.
The rest of the work in done in the respective delegates after the managers report
.poweredOn state via the
**ManagerDidUpdateState callback method. One key thing to note is that in iOS, advertisement is managed at the system level and shared by all applications. Remember to add the NSBluetoothAlwaysUsageDescription in your plist. For my Contact Tracing app, I have the Uses Bluetooth LE Accessories and the Acts as a Bluetooth LE accessory capabilities enabled as background modes as well.
One iOS BLE framework wrapper I came across in my research is BlueCap. Again, I didn't get to work with it in this project though looks promising.
From the GAP roles mentioned earlier, I thought the Broadcaster/Observer pair looked promising. Contact Tracing didn't need to establish BLE connections -- just exchange the anonymous tokens. In both apps, I created a custom BLE profile and service,
TraceService respectively. TraceProfile defines services and characteristics and their respective UUIDs. I only needed one service for now of which I defined a custom UUID for it.
In BLE (at least in version I was working with), an advertiser can broadcast up to 31 bytes of advertisement data. (I understand Bluetooth 5.0 increases this to 255 bytes! 🤯). 128-bits for my service UUID is 16 bytes, there are some overhead bytes per field advertised - each UUID (service uuids are grouped into one field, 2 bytes each, for each of the 16-bit, 32-bit and 128-bit uuids) and some flags such as if the advertisement is connectable adds some 3 bytes. Adding some service data to the payload easily blows beyond 31-bytes and in Android results in
ADVERTISE_FAILED_DATA_TOO_LARGE advertising error. With all this in mind, I chose to advertise only the service UUID and my token payload.
You can guess trial 2 wasn't my ideal solution.
- CoreBluetooth advertises data on a "best effort" basis meaning that the framework is in charge of when it does the advertisement.
- CoreBluetooth limits what advertisement data contains -- only the local name, Service UUIDs or a combination of both. No other advertisement payload is allowed thus I can't have iOS working as a broadcaster for my use case 🤕.
- I tried scan response (i.e. a scanning device requests additional advertisement data) but it too has the same limitation above. Also iOS allows only 10 bytes for it and it is usable for local name only -- in foreground mode only. Talk of specifications!
One task that took my time was creating a parser for UUIDs in Android. I needed the convenience of passing around 16-bit or 32-bit shorter UUIDs and have them converted to their 128-bit equivalent. The framework has
ParcelUUID class that's a wrapper around a
UUID. I couldn't find a public native Android way to convert a these shorter UUID strings to their 128-bit equivalent of Bluetooth's base UUID and thus had to write up some simple bit shifting code to parse such.
CBUUID in iOS luckily provides such functionality.
This article has touched on some concepts on BLE is a non-comprehensive way but simple enough to understand my take on BLE as the Contact Tracing solution. Exhaustive explanations and code samples can be found on the official Bluetooth website and respective platform documentation linked below.
With BLE, I'm closer to fulfill my requirements for Contact Tracing app -- sending anonymous tokens to nearby devices on a backgrounded app. However, the two pair roles Broadcaster/Observer don't fit the bill. Trial 3 could be my solution. Let's chat about it Part 3.
Special thanks to Mohammad Afaneh for his crash course on BLE.
- M. Afaneh's NovelBits has a 7 day BLE course via mail
- Ellisys bluetooth video series
- Bluetooth official website
- Apple's Core Bluetooth Framework
- Android's BLE overview and more docs
- Microchip BLE help