If you’ve used a modern Android device, you’re likely already familiar with Bluetooth and how it works. But what happens when access to Android’s Bluetooth system settings is limited for security (or other) purposes?
In Esper's case, we decided the best approach was to build our own Bluetooth interface as part of our Settings app. Since many of our customers want to limit end user access to Android’s settings in enterprise devices, scanning for, pairing with, and connecting to Bluetooth devices is a problem that frequently needs to be addressed.
How Bluetooth works on Android
First, let's briefly discuss how Bluetooth works on Android. Most Android devices have built-in Bluetooth chips that transmit wirelessly over frequencies in the 2.4GHz range to share data between devices. The most common use case is for peripheral devices — headphones, keyboards, mice, etc. You can also connect two Android devices over Bluetooth for file sharing and the like.
Let’s say you’re trying to connect a set of Bluetooth headphones to an Android device. You’ll first tell the Android device to scan for nearby Bluetooth connections. At the same time, the headphones should be in pairing mode, which allows them to connect to the scanning device. Selecting the headphones from the Android device’s scanning menu will initiate the pairing process.
At this point, some devices will connect automatically, while others will prompt the user to enter a PIN (though this is unusual). If no PIN is required, the devices should establish a connection almost immediately.
Building a Bluetooth interface inside the Esper Settings app
The Bluetooth module is part of the hardware layer, which Android has APIs to control. An application can send commands to configure Bluetooth via the BluetoothAdapter API, while listening to Bluetooth changes via broadcast receivers. However, we’ll need access to a few permissions to allow scanning, pairing, and sharing data.
- android.permission.BLUETOOTH – This is a basic permission to connect and share data between devices.
- android.permission.BLUETOOTH_ADMIN – This permission is required to initiate discovery, enable discoverability, and modify how long the device remains discoverable.
- android.permission.ACCESS_FINE_LOCATION – Location permission is required to initiate the scan and gather information about nearby devices.*
(*This permission is not required as of Android 12 or higher if your app declares 'NeverForLocation.' See this Android Developer page.)
How to enable Bluetooth
To turn Bluetooth on, we’re going to call on the BluetoothAdapter API, which interfaces with the Bluetooth HAL (Hardware Abstraction Layer) and, in turn, the Bluetooth chip. Essentially, this just means you can interact with the Bluetooth radio at the OS level.
To enable and disable the Bluetooth adapter, for example, we can use the following methoids:
- BluetoothAdapter.enable() turns on the Bluetooth module
- BluetoothAdapter.disable() turns off the Bluetooth module
To receive the change state of the Bluetooth module via the broadcast receiver, you can listen to this action:
android.bluetooth.adapter.action.CONNECTION_STATE_CHANGED
Enable Bluetooth discoverability
Turning the radio on is step one, but for our Bluetooth devices to find each other, we'll have to enable discoverability. This will expose the device sharing name, MAC address, and more. To do this, we’ll need to fire an intent that will enable discoverability on your Android device. Additionally, we can specify the duration of this discoverability state (leaving a device perpetually discoverable could present a security concern):
val discoverableIntent = Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE)
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 60)
startActivity(discoverableIntent)
How to scan for nearby Bluetooth devices
With Bluetooth and Fine Location* enabled, we can scan for nearby devices. We’ll once again call on the BluetoothAdapter API with BluetoothAdapter.startDiscovery(). (*Again, unless on Android 12 or higher and the NeverForLocation value is declared.)
The start and end of discovery can be received via Broadcast by listening to the following action:
android.bluetooth.adapter.action.DISCOVERY_STARTED
android.bluetooth.adapter.action.DISCOVERY_FINISHED
Before initiating a new scan, it’s a good idea to check the current scan status with BluetoothAdapter.isDiscovering().
List discovered Bluetooth devices
Once the scan is finished, you can get info about available devices via the Broadcast receiver by listening to the following:
android.bluetooth.device.action.FOUND
The available device info is shared as an object of BluetoothDevice. This includes the name, MAC address, and device type of discovered devices.
Request Bluetooth pairing
We can now select the device to be paired. It’s a good idea to check the bondState to ensure this device isn’t already paired. If the bondState is BOND_BONDED, the device is already paired and will need to be manually placed into pairing mode to pair to a new Android device.
We can then call BluetoothDevice.createBond() to initiate pairing. Note that if the device isn’t available (or already paired), this will immediately return false, and Bluetooth pairing will fail.
Most accessories like headphones, mice, and keyboards will accept the request and pair without a PIN. Other devices, like phones, tablets, and computers, may require a PIN the user has to enter to complete pairing.
You can receive the above action with this broadcast action:
android.bluetooth.device.action.PAIRING_REQUEST
The action will have data with extras like:
- EXTRA_PAIRING_VARIANT : This indicates the pairing method used (PIN or Key)
- EXTRA_PAIRING_KEY : The PIN or passkey
At this point, the appropriate dialog box will appear asking the user whether they want to pair the device. Once accepted (or the correct PIN is entered), the devices will bond. You should also stop discovery once pairing is complete, as constant scanning consumes a lot of battery.
Creating the bond
The bondState should change from BOND_NONE to BOND_BONDED, which can be verified with this broadcast action:
android.bluetooth.device.action.BOND_STATE_CHANGED
And with that, we successfully scanned, found, and paired a remote device. Now any app can create a Bluetooth socket connection to share data with that device.
If configuring Bluetooth on your Android kiosk, display signage, IoT device, or other dedicated use case has you scratching your head, come talk to Esper. We're the experts when it comes to dedicated Android devices in the enterprise, and we can help. Set up a demo today.