Getting started with Bluetooth Low Energy connection in Android
Not so long ago I started familiarizing myself with Android Bluetooth scanner. This service is no longer a novelty (BLE appeared in Android 4.3), but the lack of practical examples forces developers of different levels to spend a lot of time to solve various problems while getting acquainted with this scanner.
In this article, I will try to explain the main points of using BluetoothAdapter as briefly as possible, and will give an example of how to run device searching by Bluetooth, filter when scanning devices by type, connect to a selected device and get information about the device, and I will touch on main problems I had to face. All the code we will go through in this article is available in the Github repository.
BluetoothAdapter is a class that allows you to perform basic tasks with a Bluetooth scanner:
- device searching
- obtaining a list of paired devices
- creating an instance of BluetoothDevice class by its MAC address
- connecting to devices and start scanning data
GATT (Generic Attribute Profile) is an Attribute Protocol (ATT), which sets common operations and data frameworks that are transmitted and stored via Bluetooth.
Service is a set of GATT characteristics defining device functions. Each device has its own set of services.
Characteristics are the basic data element for building GATT services.
Descriptor stores additional information and attributes for GATT characteristics, it can be used to describe the characteristic features or to control specific GATT characteristics.
UUID is a unique identifier that defines the above services, characteristics and descriptors. These identifiers help us determine what kind of data we want to get from or set to the device.
All services, characteristics and descriptors should ideally meet Bluetooth GATT requirements, but our world is not perfect… :)
So let’s get started!
Given the size and purpose of the application, no architecture was used.
RxPermissions library was used for convenience in requesting user permissions.
For Bluetooth ease of use, all logic has been consolidated in the DeviceManager class with the following public methods:
- checkBluetooth() - check if Bluetooth is currently enabled
- startSearchDevices() - start device searching
- stopSearchDevices() - stop device searching
- connectDevice() - connecting the found device
- closeConnection() - disconnect from a previously connected device
- startScanPulse() - start pulse scanning
We will touch on DeviceManager class methods in details a bit later.
First of all, working with Bluetooth requires obtaining 3 permissions
Pay attention that “ACCESS_FINE_LOCATION” is “Dangerous” permission, requires an explicit confirmation from the user.
If the user has granted such permission, we check Bluetooth status:
Going a little bit further, I separated the various tabs for device searching and assigned device types to them:
- DeviceType.ALL - all the devices supporting BLE
- DeviceType.HEART_RATE_MONITOR - heart rate monitors (Polar H10) was taken as an example
- DeviceType.MI_BAND - Mi Band fitness trackers (this task is harder because Xiaomi has launched their proprietary UUID for more secure operation)
So let’s go back to the device searching setup. We requested permission, then we are checking Bluetooth scanner status, and here we step on a first rake. The fact that the official documentation simply called “the ability to collect location data using a Bluetooth scanner” was a direct necessity to have GPS enabled; otherwise, Bluetooth scanner cannot find any device. So after permissions were granted we should check if GPS is enabled:
If GPS is not enabled at the moment, then we conveniently inform the user (SnackBar or dialogue) and grant access to the respective settings in the Android system via intent:
Finally, all permissions are granted, GPS and Bluetooth are enabled; now you can start scanning!
Here we rely on DeviceManager. This class object was initialized at the start of the activity, and the context was given to the constructor to check the BLE status and connect to the specific device. Also, when creating this object, BluetoothAdapter was initialized as well, which grants access to the basic functions of the Bluetooth scanner.
Therefore, the activity is shown to the user on a progress bar, indicating the search process and instructing deviceManager to start scanning the appropriate device type. Please note that the found devices are responded by respective callback one by one in the order of finding.
Let’s find out what DeviceManager performs in “ startSearchDevices()” method:
- First of all, we should disconnect the previously connected device and stop the scanning process if it was started before. Reading data from a device is a very time-consuming process, so Android recommends pairing with only 1 device at a time:
- Next, we set up a device filter (ScanFilter which allows us to specify which devices we are looking for, in our case we will filter the UUID devices that we need for the services) and the scanning process itself (ScanSettings which provides access to scan type settings, limitations on the number of devices found, etc. We use standard settings).
3. Now we initialize callback for the scanner:
4. And the last step of starting device searching will be accessing to the scanner and transferring our settings:
Here’s what we have:
So the first step is done, scanning is started, we start to retrieve the found devices and perform operations by callback (ScanCallback). In our case, we begin to list them according to the ability to select a specific device.
Now let’s connect the selected device.
To do this, we need to call the connectGatt(Context, Boolean, BluetoothGattCallback) method of the selected BluetoothGatt. The first parameter is clear, the second parameter is a Boolean variable that indicates whether to start connecting the device immediately (false) or to wait for the device to be available for connection (true). In our case, we give a false response because this device was just found to be active. Let’s touch on the third parameter (BluetoothGattCallback) in more detail.
To connect the device, we need to redefine the following methods:
onConnectionStateChange() method is accessed when the connection status changes.
We wait for BluetoothProfile.STATE_CONNECTED to confirm device connection, in all other cases, the connection has not been established and must be stopped by requesting bluetoothGatt.close(). Once connected, we can get information about all the services, characteristics and descriptors of this device. To do this, we need to request bluetoothGatt.discoverServices()
onServicesDiscovered() is requested after access to device services was obtained. Now the connection is considered to be fully established.
Scanning device characteristics
Once the device has been connected, you can begin reading or recording services. However, we need to know in advance the Uuid service and its characteristics that we need to work with. I will demonstrate the use of a pulse measurement service with Polar H10 example.
To start scanning, all we have to do is to find device needed characteristic and enable notifications for that characteristic, and let all its descriptors report the change in values. This is done as follows:
in response to recording changes to the descriptor
our previously implemented BluetoothGattCallback notifies us in onDescriptorWrite() method that changes to the descriptor have been recorded successfully. We can then request reading characteristic from the service of the connected device:
After that, any changes to the selected characteristic will be sent to the same callback (BluetoothGattCallback) in the onCharacteristicRead() method. If the reading process is successful (status == BluetoothGatt.GATT_SUCCESS), then the characteristic object will have an array filled with bytes (BluetoothGattCharacteristic#value).
Very important point is that to read data from the device we need to know what the specific characteristic is and what data it must send to us. Someone will say that everything is simple and the structure of all the main characteristics is described on bluetooth.com. Here’s an example of discrepancies: Polar H10 heart rate monitor sends the heart rate per minute as the first byte of values array, while official documentation says that the first byte should be data formatting flags, and then the values themselves:
Now let’s summarize what we need to connect the device:
- Grant permissions to Bluetooth Low Energy;
- Make sure that GPS is enabled and this application is not restricted to access this service;
- Initialize BluetoothAdapter and start scanning, transferring search parameters, if any;
- Select the required device from all the found ones and connect it to GATT Server by transferring BluetoothGattCallback, which will be the link to all the processes that will happen with this device;
- Remember that BLE only allows you to connect 1 device at a time;
- After connecting the device, send a request for all services that this device owns;
To start data scanning and reading from the device:
1. We find the required characteristic among all the characteristics of all services of this device;
2. Enable the ability to send a notification to the characteristic itself and its descriptors;
3. After receiving confirmation that the changes to receive notifications have been applied to the descriptor, we start to listen to value changes of the selected characteristic.
Hope this article will help you quickly understand the operation of the Bluetooth Low Energy scanner and avoid problems with the device searching and connecting.
You can also read the implementation described in this article by the following link.