How To Use Android BLE to Communicate with Bluetooth Devices - An Overview & Code examples

All you need to know about connecting a BLE device with your Android app in a focused & simple guide to get started. From scanning to pairing, what is GATT, and how to deal with services and characteristics.

I got a new challenge at my job: Connecting a bluetooth sensor to our Android app. I’ve never dealt with bluetooth and during my research I’ve found many resources but unfortunately many were incomplete snippets or tutorials that only explain half of the puzzle. It was exhausting!

After tons of sleepless hours, I’ve conquered the challenge! I actually got data from the sensor! Believe me, it was a very exciting moment to get those bytes of data. So, I decided to sum up my conclusions and the way I handled it to help out others who might be struggling with this. In this post you can find a brief overview of the key components and how to use them to establish a connection and read data from a sensor/monitor/device.


Some basic theory

First, let’s write down all the key components we have on bluetooth in a short and concise definition to get a feel of what we have to do.

BLE: Android built-in platform to discover devices, request and transmit information from our bluetooth device.

GATT: Generic Attribute Profile to define how to exchange data using predefined attributes.

Central: the Computer/Tablet/Mobile device, also referred as GATT client. Scans, requests and uses the data given by the peripheral.

Peripheral: the device broadcasting the data, also referred as GATT server. The data is structured as definitions of how to interact with it’s ‘database’.

Services: set of provided features and associated behaviors to interact with the peripheral. Each service contains a collection of characteristics.

Characteristics: definition of the data divided into declaration and value. Using permission properties (read, write, notify, indicate) to get a value.

Descriptor: an optional attribute nested in a characteristic that describes the specific value and how to access it.

UUID: Universally Unique ID that are transmitted over the air so a peripheral can inform a central what services it provides.

Key points to remember:

  • BLE peripheral can only be connected to one central device at a time
  • The Server does not send data on its own, but only when a Client requests so.
  • Notifications and Indications are operations initiated by the Server whenever a value in the database changes

Tutorial specification

We will connect our Android app to a heart rate sensor to display measurements. Here is the summarized bluetooth specification about the service and characteristic we’re going to use.

Bluetooth specification example — let’s make it more readable!

Heart Rate Service:

  • Assigned Number/UUID: 0x180D
  • Type: org.bluetooth.service.heart_rate
  • Definition: This service exposes heart rate and other data from a Heart Rate Sensor intended for fitness applications.

Heart Rate Measurement Characteristic:

Heart Rate Control Point Characteristic:

After reviewing the specification we can conclude that in order to get measurement, we need to set notification on the Heart Rate Measurement Characteristic and we need to approach Heart Rate Control Point Characteristic by writing to it to get data streaming.


Let’s code!

Start by building a basic Android app with an empty activity.

Declare BLE Permissions:

android.permission.BLUETOOTH
android.permission.BLUETOOTH_ADMIN
android.permission.ACCESS_COARSE_LOCATION

Note: LE Beacons are often associated with location so in order to get scanning results without a filter, we need to request location permission.

Save required UUIDs:

HEART_RATE_SERVICE_UUID = convertFromInteger(0x180D)
HEART_RATE_MEASUREMENT_CHAR_UUID = convertFromInteger(0x2A37)
HEART_RATE_CONTROL_POINT_CHAR_UUID = convertFromInteger(0x2A39)

Use this hack method to convert from an integer to UUID:

public UUID convertFromInteger(int i) {
   final long MSB = 0x0000000000001000L;
final long LSB = 0x800000805f9b34fbL;
long value = i & 0xFFFFFFFF;
return new UUID(MSB | (value << 32), LSB);
}

Set BLE in our Activity:

BluetoothAdapter bluetoothAdapter;

final BluetoothManager bluetoothManager =
(BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
bluetoothAdapter = bluetoothManager.getAdapter();

Ensures Bluetooth is available on the device and it is enabled. If not, display a dialog requesting user permission to enable Bluetooth:

if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
    Intent enableBtIntent = 
new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}

Define a Scan Callback:

For this example, we start scanning when the activity reaches onCreate but don’t forget to use stopLeScan when the activity goes to onPause/onDestroy/onStop.

BluetoothAdapter.LeScanCallback scanCallback = 
new BluetoothAdapter.LeScanCallback() {...}
bluetoothAdapter.startLeScan(scanCallback);

Filter the scan results by checking the specific sensor address (aka MAC). You can find it through the vendor or on this MAC addresses lookup.
 When we find our sensor, save it for future usage.

@Override
public void onLeScan(final BluetoothDevice device, int rssi,
byte[] scanRecord) {
   if(device.address == HR_SENSOR_ADDRESS){
myDevice = device;
   }
}

Define Gatt Client & Callback:

BluetoothGatt gatt;
...
BluetoothGattCallback gattCallback =
new BluetoothGattCallback() {...}

Connect gatt callback, I recommend to set autoConnect to true.

gatt = myDevice.connectGatt(this, true, gattCallback);

When initial pairing is established, start getting the provided services:

@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
if (newState == STATE_CONNECTED){
         gatt.discoverServices());
      }
}

Use this next callback when services are discovered to set notification on Heart Rate Measurement Characteristic. Seems pretty straight forward — just use the obvious gatt method “setCharacteristicNotification”, right?

@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status){
   BluetoothGattCharacteristic characteristic = 
gatt.getService(HEART_RATE_SERVICE_UUID)
.getCharacteristic(HEART_RATE_MEASUREMENT_CHAR_UUID);
   gatt.setCharacteristicNotification(characteristic, enabled);
}

WRONG. Unfortunately, I fell for this one as well!

You probably wonder when does the Descriptor comes in to the picture. Well, to set the notification value, we need to tell the sensor to enables us this notification mode. We will write to the characteristic’s descriptor to set the right value: Notify or Indicate.

According to specification, this characteristic has a Notify property and a Client Characteristic Configuration, so go ahead and save it’s assigned number.

CLIENT_CHARACTERISTIC_CONFIG_UUID = convertFromInteger(0x2902)

Now, let’s add it to our callback:

@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status){
... 
   BluetoothGattDescriptor descriptor = 
characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_UUID);

descriptor.setValue(
BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
   gatt.writeDescriptor(descriptor);
}

After notifications are enabled, we need to write to Heart Rate Control Point Characteristic to tell the sensor to start streaming data. What should we write? a byte[] value that is defined in the vendor documentation. In this case, we write a simple byte array that contains {1,1} that serves as a data streaming command.

@Override
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status){
   BluetoothGattCharacteristic characteristic = 
gatt.getService(HEART_RATE_SERVICE_UUID)
.getCharacteristic(HEART_RATE_CONTROL_POINT_CHAR_UUID);
   characteristic.setValue(new byte[]{1, 1});
gatt.writeCharacteristic(characteristic);
}

Now, all updates from the sensor on characteristic value changes will be posted on this next callback. Use the vendor documentation to parse the value received.

@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {

processData(characteristic.getValue());
}

YES! We got those sweet bytes of data!

That’s it!

Hope this tutorial helped you! Also, Instead of referencing some documentations you probably already looked it, here are some of Stackoverflow questions that saved me time and frustration:


Follow me on Medium to learn more about how to break into the tech industry and how to improve your software development skills.
Connect with me on Linkedin and let me know if this post helped you in any way :)