Back in February 2020, I wrote about how location is becoming more complex in terms of privacy. The primary motivation around that post was that Bluetooth scans and pairing require
ACCESS_LOCATION_FINE permission which can be confusing for the user. Why should an app that connects to a Bluetooth device require access to my location? The reason for this is that it is possible to determine the location from known devices (such as beacons). These may have fixed locations. Android 12 introduced new permissions which no longer require
ACCESS_LOCATION_FINE. That is a really good thing. It will make things much less confusing for users. It’s also easier for developers to justify why certain permissions. However, we don’t have to wait for Android 12 to be able to remove the need for
ACCESS_LOCATION_FINE when performing Bluetooth scans and pairing. In this post we’ll explore the options.
Before we begin, I need to set some expectations. This is not going to be an in-depth how-to guide. But more to outline the various options that are available for different versions of Android. Hopefully, it should enable people to determine the correct strategy to use given the
minSdkVersion of their app, and other requirements.
Let’s start by looking at the new permissions added in Android 12. These are
BLUETOOTH_CONNECT. They are direct replacements for the traditional
BLUETOOTH permissions, respectively. The difference is that the legacy permissions also require
ACCESS_LOCATION_FINE which must be requested as a runtime permission.
BLUETOOTH_SCAN permission has an optional qualifier that declares whether the app will use scan data to detect location. Specifying that the results won’t be used to determine location, may result in a smaller set of scan results. Any devices from which the location can be determined (such as beacons) may be omitted. If you specify that it will be used for location, then you must also obtain the
ACCESS_LOCATION_FINE permission otherwise the scan will fail.
Very few apps will have the luxury of having Android 12 as the minSdkVersion. So we need to declare the relevant permissions for different API
Here we declare both
BLUETOOTH_ADMIN permissions, but each has a
android:maxSdkVersion="30" qualifier. Only devices running API 30 or lower require these permissions.
BLUETOOTH_SCAN permission has the
android:usesPermissionFlags="neverForLocation" qualifier. It does not require the
ACCESS_FINE_LOCATION permission as a result. Therefore we can also include the
android:maxSdkVersion="30" qualifier on that permission.
So we don’t need location permissions on devices running Android 12 or higher. But we do on lower API levels.
There is actually an alternative to performing Bluetooth scans at all. That’s to defer things to the Companion Device Manager. Android 8.0 (API 26) introduced Companion Device Manager. Rather than implementing the scanning and pairing ourselves, we can delegate it to Companion Device Manager. A nice analogue here is that when capturing from the camera. We can either do it ourselves using one of the Camera APIs if we need tight control. Otherwise, we can use an
ACTION_IMAGE_CAPTURE Intent to defer this to a system service.
Companion Device Manager works in much the same way. Like the camera example, one real advantage is that our app requires no permissions at all to do this. Neither the Bluetooth permissions, nor location permissions. Of course, if we need to connect to a device after we’ve paired to it using Companion Device Manager, then we’ll need either the
BLUETOOTH_CONNECT permissions. But neither of these is a runtime permission. Nor do they require location permissions.
The other advantage of using Companion Device Manager is it removes both complexity and a chunk of UI work. Performing scans and pairing over Bluetooth can be tricky – particularly when different devices can behave in different ways. Companion Device Manager removes this responsibility from us.
The cost of this is that, similarly to the camera example. Once we pass control over to Companion Device Manager we lose ownership of the UI / UX. This means that we won’t be able to control the device selection UI.
Personally, I think that’s is a worthwhile tradeoff. Although many product owners and designers may not agree! If I have to convince people, then I would take the approach that we should go with Companion Device Manager. This would provide a robust and reliable implementation for minimum effort. We can then add tasks to the backlog to revisit this later on, and do something more bespoke.
Let’s consider a
minSdkVersion="21" app. There is no escaping the fact that we’ll need to have different behaviour on different API levels.
If we were to use Companion Device Manager, then that’s only going to work on API 26 and higher devices. So we’ll need to implement manual scanning and pairing for legacy devices running API levels 21-25. Then on later devices, we can use Companion Device Manager. This means that location and
BLUETOOTH_ADMIN permissions will be required up to API level 25, and we can use a
android:maxSdkVersion="25" qualifier in our manifest declaration.
If we don’t use Companion Device Manager, then we implement our own scanning and pairing for all API levels. However, we could switch the permissions for API 31 and later. That way we only require location permissions up to and including API 30.
Some might argue that there’s less work in the second approach. However, I would counter that with the understanding that Companion Device Manager will be more robust. The majority of our users will tend to be on later versions of Android. We’ll provide a better quality of service to those users on newer devices. Moreover, we’ll only need to explain location permission to those on pre-API 26 devices. Companion Device Manager does not require our app to have that permission.
Bluetooth scanning and pairing is not easy to get right. It is compounded somewhat with having to think across different API levels. Hopefully, this post will help those having to design such components. Ultimately the major decision is whether to use your own implementation for all API levels or to use Companion Device Manager where possible.