Why bespoke Contact Tracing apps don’t work so well on iOS
While Apple and Google work on a new cross-platform contact tracing API to help tackle the COVID-19 pandemic, we’re starting to see some countries release bespoke apps built using just the existing Bluetooth APIs on each platform.
What we’re quickly seeing in the media about these solutions is that the iOS app has some limitations while in the background (i.e. not the visible and active app), and as a result the creators of these apps are often recommending people leave the iOS app in the foreground, or that they work better if Android devices are nearby.
IMPORTANT: This post is not designed to call out any specific implementation by any country, nor should you as a reader be worried about downloading any of them! Although the general consensus is it’s better if countries used the Apple|Google framework, we should still use what is currently available until something potentially better is available.
What we’re not really seeing in the media is a high level technical explanation of exactly why bespoke iOS Contact Tracing apps seem to be so ineffective while running in the background, and why Android doesn’t have the same limitations. I aim to cover exactly that in this post at a high level. You do not need to be a developer to read this post!
TL;DR: It’s because of the way iOS restricts developers running apps in the background to protect privacy and battery life, which is very different from how Android allows apps to run in the background.
I really recommend you read the whole post though, you’ll learn lots about how both iOS and Android work, and more specifics about why this means bespoke Contact Tracing apps have issues on iOS, and a whole bunch more interesting stuff!
What is a Contact Tracing app
Before I go any further, you might be wondering what a Contact Tracing app is actually for. In simple terms, it is designed to keep track of people you have been in close proximity to, and if any of those people are diagnosed with COVID-19, you can be notified so you can self-isolate or get tested, and other people you’ve been close to can do the same. For that to be effective, it needs to be installed on a lot of devices!
Contact Tracing apps on iOS and Android use something called Bluetooth Low Energy (LE), which I’ll just call Bluetooth throughout this post. I’m writing this post as someone who’s developed and released two iOS apps that use Bluetooth while in the background reliably, and I’ve ported one of those apps to Android which has taught me a great deal about the differences between the two platforms when it comes to running the code reliably while the app is not the foreground (visible) app.
COMMENT: I can tell you straight off the bat, lots of people don’t like leaving apps running. Since my users don’t even like leaving my apps running in the iOS App Switcher, they definitely would not use my app if it meant leaving the app running in the foreground (so the app is visible) at all times!
Now we know what a Contact Tracing app is, let’s talk about exactly why the creators of these apps are recommending that people leave their iOS apps running in the foreground to make them more reliable, and why it’s not a problem on Android.
The iOS App Lifecycle
Let’s start with the basics, since understanding the general lifecycle of an iOS app is important before continuing.
When we create an iOS app, by default it cannot run in the background at all, even when it’s showing in the iOS App Switcher. When you leave the app to go to the Home Screen, or a different app, the system “freezes” the app after a few seconds and puts it into what’s called a “suspended” state. At this point, the app is no longer in the foreground, and no longer the active app.
NOTE: It’s important to understand, your app isn’t using any battery while in the suspended state. Even though it’s showing in the App Switcher, it’s not doing anything!
While in the suspended state, if another app (or iOS itself!) needs more memory (RAM) then the suspended app can be terminated. As a user, you’d still see the app in the App Switcher, but it’s just an image of the last state of the UI before it was suspended.
Of course, things have changed since iPhone OS 1.0 and iOS apps can now perform certain tasks while in the background, including continuing to play audio like Spotify, track your location (with permission) like Strava, and also use Bluetooth (again, with permission), like Contact Tracing apps.
For this to happen, when we’re making our iOS app we have to declare to the system (iOS) that our app wants to do certain tasks while in the background. We declare this by enabling one or more “background modes”.
Once we enable a specific background mode, we can then use special APIs that allow these tasks to be carried out. Each background mode has different APIs and rules, and has a different impact on your device’s battery life. For this post, all we care about are the Bluetooth ones.
The Bluetooth LE background modes
To use Bluetooth in the background on iOS, we need to enable one (or both) of two different background modes for our app (more on the difference between them later).
Once we’ve enabled the right background mode(s), our app can continue to use the relevant Bluetooth APIs while not the active app, however there are some really nuanced rules around how this works and what we’re able to do.
But, before I can go into that, I need to explain to you the two different types of Bluetooth APIs that an iOS app can use! In Bluetooth LE, there is a concept of a Central and a Peripheral.
A Central is typically something like a phone or laptop. You can think of this as the client.
A Peripheral is typically something like a heart rate monitor, or some device like an electric skateboard. You can think of this as the server.
The Peripheral (or server) has data, and the Central (or client) wants to read that data. The Peripheral advertises itself, and the Central can scan for it, connect to it once it finds it, and read some data. The concept of advertising and scanning is important to remember for later in this post.
iOS and Android apps can act as both the Central and the Peripheral at the same time, which makes them (almost) perfect for use as a Contact Tracing app.
How Contact Tracing apps use Bluetooth LE
At an extremely high level, the way a Contact Tracing app could work, is every iOS and Android device with the app installed allows the app to act as both a Central and a Peripheral.
This means each device simultaneously advertises itself to other devices to be discovered and connected to, as well as scans for other nearby devices to discover and connect to.
When they discover each other, they can connect, transfer a small amount of data to identify that device, then use that information to be able to say those two devices have been near each other.
NOTE: I’m not going to go into specifics about privacy or cryptography here, that’s a whole different topic!
For it to work well, this needs to happen regardless of whether a user is currently using your app in the foreground, whether they have the phone locked, or whether they’re just using another app.
For that to happen in our iOS app, we’d need to enable the two background modes I mentioned earlier that allow the app to continue to use Bluetooth APIs as both a Central and a Peripheral while no longer the active app.
Once we have those enabled, if we start advertising as a Peripheral, and scanning for other Peripherals as a Central, that will continue to happen when our app is no longer the active app and our app is in the background.
Even though we’ve now enabled the background modes and are advertising and scanning, our app will still enter the suspended state after a few seconds of being in the background. The system itself actually takes over the scanning and advertising, while our app is completely frozen.
NOTE: This is one of the reasons Bluetooth LE apps use such little power on your iPhone!
Only when a Bluetooth event happens (like discovering another Peripheral) will our app get resumed (unsuspended) in the background to handle the event, shortly becoming suspended again after a few seconds.
The system will carry on doing this for us as long as our app is in the App Switcher (and the iOS device is not restarted), even if our app is terminated in the background to free up memory for another app by the system.
If a Bluetooth event arrives after our app is terminated by the system, our app is re-created so we can handle it, and then it will be suspended again.
This all happens in the background, so to a user it’s seamless and seems as though our app is doing all the work. This is the magic of background execution on iOS.
IMPORTANT: Up until iOS 13.4, if we remove our app from the App Switcher, the system will no longer advertise and scan on our behalf, and we’ll have to wait until the app is next launched by the user to start again.
In iOS 13.4 that behaviour seems to have changed and it will continue to stay connected after removing the app from the App Switcher, but that change is undocumented and shouldn’t be relied on.
So what’s the problem?!
That all sounds great, right? Looking at the above, it seems like iOS will continue to do the work we need for us to create our bespoke Contact Tracing app!
So why do the creators of bespoke Contact Tracing apps recommend that people leave the iOS app in the foreground, or do other things to keep the app working reliably?
Well, remember earlier I said that there are some really nuanced rules around how the iOS Bluetooth background modes work? This is where that comes in.
iOS Peripherals in the background
Let’s first talk about the Peripheral background mode, because this is the main issue.
When our app is in the background and we want to let the system take over advertising for our app, it will only allow us to advertise in a way that lets other iOS devices discover it.
This means that other Android devices scanning for nearby devices cannot find an iOS device while the iOS app is in the background (although there is an undocumented way to do it, it’s never a good idea to rely on undocumented behaviour, especially when it comes to Bluetooth).
Additionally, I ran a test where I kept an iOS device locked with a simple Contact Tracing example app running in the background, after 30 minutes I installed the same app onto a different iOS device, and they couldn’t discover each other until I brought the app into the foreground on one of the devices.
NOTE: During my testing, I also found occasions as well where an iOS device couldn’t discover another iOS device that was locked, until I just woke the screen while it was still locked. Sometimes that was less than 30 minutes.
On iOS, Peripheral advertising performance while our app in the background can also be impacted based on whether the iOS device has other apps acting as a Peripheral as well. iOS decides the performance on our behalf, and it’s not something we have any control over.
iOS Centrals in the background
Let’s also talk about the Central background mode. While our iOS app is in the background, the system will likely slow down the rate of scanning for nearby devices to save battery.
Although this isn’t a dealbreaker, it will definitely reduce the usefulness of scanning for nearby Peripherals which may be missed because you walked past someone while it wasn’t actively scanning.
Bluetooth LE on Android
Now that we’ve learnt the restrictions the iOS system has on developers using Bluetooth while in the background, and why they’re a problem for Contact Tracing apps, we can talk about why these aren’t problems on Android.
The Android system has a very different approach when it comes to letting apps continue to operate while they’re not the “active” app. Instead of background modes like on iOS, Android uses something called “services” to allow us to continue to run code reliably while the app is not the active app.
Specifically, these are called “foreground services”, and to use them as developers we must show a user-visible notification in the Notification Drawer.
The user is always able to terminate these services if they wish, although to do so they might need to force-stop the app from within System Settings.
Unlike an iOS app, an Android app does not have to be in the “Recents” app list (the equivalent of the iOS App Switcher) for the service to continue to run.
When we create a service, we declare it to the system in our app’s manifest file (a file the system uses to know what your app can do), and then we are free to run whatever code we want in there, potentially indefinitely.
This makes it perfect for running long-running Bluetooth code, acting both as a Central and a Peripheral, with very few restrictions. Exactly what our Contact Tracing app needs!
This is why Android Contact Tracing apps aren’t recommended to be kept in the foreground by their creators, because there’s no need.
Additionally, the Android Bluetooth APIs give us more options for scanning for Peripherals, which allows for an (undocumented) workaround for scanning for iOS Bluetooth apps that are only meant to be discoverable by other iOS Bluetooth apps.
NOTE: This workaround is quite technical, and it’s exactly what the smart developers who created the NHSX Contact Tracing Android app have done.
It involves scanning for nearby devices with very specific data encoded in an undocumented way on iOS devices. Very clever!
The tradeoff for this freedom on Android, is that we run the risk of using more battery than is necessary, and the system can start to suggest to our users that our app is using more battery than it thinks we should be. Ultimately though, the Bluetooth code is still running the same as if the app was the active app.
Also, unlike iOS, you have significantly more control over whether to advertise and scan more aggressively for increased performance, or conservatively to reduce power consumption. This means our Android Contact Tracing app is far more likely to discover passers by in time if we choose to.
Having worked with Bluetooth on both platforms, I can say there are pros and cons to each.
iOS is clearly more restrictive, but the API and hardware is more stable, because Apple have full control over it all. Android gives us far more freedom and power, but the API is more difficult to get up and running with, and some older hardware just doesn’t work as well.
NOTE: At the time of writing, the official Android documentation for getting started with Bluetooth LE uses deprecated APIs!
Ultimately, using the existing Bluetooth APIs available, Android apps are better at performing the role for a Contact Tracing app.
How is the Apple|Google cross-platform framework different on iOS?
The reason the Exposure Notification framework currently being built by Apple and Google is better on iOS than just using the existing Core Bluetooth APIs is because Apple can avoid the previously mentioned restrictions with apps using Bluetooth in the background.
Because Apple are making the framework, they can automatically give the Exposure Notification framework priority on the system, ensure that it isn’t abused by developers, and balance power consumption correctly specifically for Contact Tracing.
Footnote
I have validated my claims with online documentation and by writing and testing some sample code which you are welcome to download and nose about in if you want!
I’m on Twitter if you want to ask me any questions about this post, or sample code. Alternatively, you can open an issue on GitHub and as a question there.
Some useful documentation if you want to read more on the APIs available:
- iOS Core Bluetooth Programming Guide
- Core Bluetooth API documentation
- Android Bluetooth Low Energy Guide
- Android Bluetooth API documentation
Additionally, the NHSX team have open sourced their iOS and Android Contact Tracing apps and they’re a very good example of how to use Bluetooth on both platforms.
Stay safe! ❤️