fbpx

Blogs from the Ranch

< Back to Our Blog

Bluetooth Low Energy on Android, Part 1

Avatar

Andrew Lunsford

There are many resources available on Bluetooth on Android, but unfortunately many are incomplete snippets, use out-of-date concepts, or only explain half of the puzzle! In this series, we will learn how to set up both a Bluetooth Low Energy (BLE) Client and Server and demystify the Generic Attribute Profile (GATT) communication process.

Part 1 will focus on Server and Client setup and establishing a BLE connection. Let’s jump right in and begin building our app.

Prerequisites

To best understand BLE’s inner workings, you will need two phones. One will act as the Server, and the other as a Client.

Note: The Server will need to support BLE Peripheral Advertising. We will programmatically check for this, but device specifications may not list this functionality. The more recent the device, the better chance it will support advertising.

Since BLE support was added in Jelly Bean 4.3, we’ll use that as our minimum. Now add the necessary permissions to the manifest:

If the device is running Marshmallow or later, the user must also grant permissions at runtime.

Scanning

The first thing we need to be able to do is perform a basic Bluetooth scan. Create a ClientActivity and begin by checking if the device is BLE capable.

protected void onResume() {
    ...
    if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
        finish();
    }
}

Bluetooth is very fickle, so be very strict when checking device capabilities to avoid complications.
For the previous and following requirements, its best to finish the activity or block the action if any are unmet. Now let’s get ahold of the BluetoothAdapter, which will allow us to perform basic BLE operations like our scan.

protected void onCreate(Bundle savedInstanceState) {
    ...
    BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
    mBluetoothAdapter = bluetoothManager.getAdapter();
}

Add a few scan control buttons and attach listeners to our startScan and stopScan.
startScan is a good place to ensure that:

  • we are not already scanning;
  • Bluetooth is enabled; and
  • we have access to fine location.

With a little extraction and organization, that looks something like this:

private void startScan() {
    if (!hasPermissions() || mScanning) {
        return;
    }
    // TODO start the scan
}
private boolean hasPermissions() {
    if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
        requestBluetoothEnable();
        return false;
    } else if (!hasLocationPermissions()) {
        requestLocationPermission();
        return false;
    }
    return true;
}
private void requestBluetoothEnable() {
    Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
    Log.d(TAG, "Requested user enables Bluetooth. Try starting the scan again.");
}
private boolean hasLocationPermissions() {
    return checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED;
}
private void requestLocationPermission() {
    requestPermissions(new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, REQUEST_FINE_LOCATION);
}

We will need to fill out our startScan method by configuring ScanFilters, ScanSettings, and a ScanCallback.
For now, we will not filter anything from our scan.
This will return all broadcasting Bluetooth devices in the area, but will show the scan was successful.
Because this is a BLE application, set the scan mode to low power.

private void startScan() {
    ...
    List<ScanFilter> filters = new ArrayList<>();
    ScanSettings settings = new ScanSettings.Builder()
        .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
        .build();
}

Create a ScanCallback to handle the results, and add a a map to store the results.

private void startScan() {
        ...
        .build();
    mScanResults = new HashMap<>();
    mScanCallback = new BtleScanCallback(mScanResults);
}
private class BtleScanCallback extends ScanCallback {
    @Override
    public void onScanResult(int callbackType, ScanResult result) {
        addScanResult(result);
    }
    @Override
    public void onBatchScanResults(List<ScanResult> results) {
        for (ScanResult result : results) {
            addScanResult(result);
        }
    }
    @Override
    public void onScanFailed(int errorCode) {
        Log.e(TAG, "BLE Scan Failed with code " + errorCode);
    }
    private void addScanResult(ScanResult result) {
        BluetoothDevice device = result.getDevice();
        String deviceAddress = device.getAddress();
        mScanResults.put(deviceAddress, device);
    }
};

Now grab hold of the BluetoothLeScanner to start the scan, and set our scanning boolean to true.

private void startScan() {
    ...
    mScanCallback = new BtleScanCallback(mScanResults);
    mBluetoothLeScanner = mBluetoothAdapter.getBluetoothLeScanner();
    mBluetoothLeScanner.startScan(filters, settings, mScanCallback);
    mScanning = true;
}

At this point, we have a Bluetooth scan that will save all ScanResults into a map.
These hold various useful pieces of information:

  • BluetoothDevice: Name and address
  • RSSI: Received signal strength indication
  • Timestamp
  • ScanRecord
    • Advertisement Flags: Discoverable mode and cababilities of the device
    • Manufacturer Specific Data: Info useful when filtering
    • GATT Service UUIDs

So how long is the scan?
Currently it will run forever, so add a Handler to stop it after a specified time (in milliseconds).

private void startScan() {
    ...
    mScanning = true;
    mHandler = new Handler();
    mHandler.postDelayed(this::stopScan, SCAN_PERIOD);
}

To stop a scan, we use the same ScanCallback we used earlier.
It’s also good practice to avoid unneccessary calls, so protect the call with a few safety checks.
Now is also a good time to clean up any scan related variables.

private void stopScan() {
    if (mScanning && mBluetoothAdapter != null && mBluetoothAdapter.isEnabled() && mBluetoothLeScanner != null) {
        mBluetoothLeScanner.stopScan(mScanCallback);
        scanComplete();
    }

    mScanCallback = null;
    mScanning = false;
    mHandler = null;
}

scanComplete will perform any actions using the results, for now we can simply log out the devices found during the scan.

private void scanComplete() {
    if (mScanResults.isEmpty()) {
        return;
    }
    for (String deviceAddress : mScanResults.keySet()) {
        Log.d(TAG, "Found device: " + deviceAddress);
    }
}

Advertising

Now that we have a working BLE scanner, let’s create a GATT server for it to find.
The GATT describes how BLE devices communicate.
It has Services, which can have Charactacteristics, which can have Descriptors.

GATT Structure

The GATT Server will broadcast its Services, which can be filtered by UUID.
Characteristics can then be accessed, again by UUID.
This is where we can read and write values, so let’s create a ServerActivity start configuring it.
Much like our Client, the device must have Bluetooth enabled and support low energy functionality.

protected void onCreate(Bundle savedInstanceState) {
    ...
    mBluetoothManager = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
    mBluetoothAdapter = mBluetoothManager.getAdapter();
}
protected void onResume() {
    if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
        Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
        startActivity(enableBtIntent);
        finish();
        return;
    }
    if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
        finish();
        return;
    }
}

We check these in onResume to ensure the user has not disabled BLE while the app was paused.
Last, we need to check that advertising is hardware supported before accessing the BluetoothLeAdvertiser.

protected void onResume() {
        ...
        return;
    }
    if (!mBluetoothAdapter.isMultipleAdvertisementSupported()) {
            finish();
            return;
        }
    mBluetoothLeAdvertiser = mBluetoothAdapter.getBluetoothLeAdvertiser();
}

Now create an empty GattServerCallback to extend BluetoothGattServerCallback and open the server.
We’ll fill it in later when after the server is setup.

protected void onResume() {
    ...
    mBluetoothLeAdvertiser = mBluetoothAdapter.getBluetoothLeAdvertiser();
    GattServerCallback gattServerCallback = new GattServerCallback();
    mGattServer = mBluetoothManager.openGattServer(this, gattServerCallback);
}
private class GattServerCallback extends BluetoothGattServerCallback {}

In the previous diagram, we now have the first piece in place.
Next, we need to add a Service to the server.

protected void onResume() {
    ...
    mGattServer = mBluetoothManager.openGattServer(this, gattServerCallback);
    setupServer();
}
private void setupServer() {
    BluetoothGattService service = new BluetoothGattService(SERVICE_UUID,
                    BluetoothGattService.SERVICE_TYPE_PRIMARY);
    mGattServer.addService(service);
}

The SERVICE_UUID is a unique identifier of your choosing that will ensure we are connecting to the correct GATT Server. We will add a Characteristic later, but we don’t need it at the moment.

Now we need to advertise the GATT server. First we will configure the AdvertiseSettings.

protected void onResume() {
    ...
    setupServer();
    startAdvertising();
}
private void startAdvertising() {
    if (mBluetoothLeAdvertiser == null) {
        return;
    }
    AdvertiseSettings settings = new AdvertiseSettings.Builder()
            .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED)
            .setConnectable(true)
            .setTimeout(0)
            .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_LOW)
            .build();
}

We will use a Balanced advertising mode, so that it will be quickly discoverable but not consume as much energy as Low Latency.
Connectable is true, because we want to pass data back and forth unlike a beacon.
Set Timeout to 0 to advertise forever, and use the Low Power Level setting since we are using BLE.

Note: AdvertiseMode refers to how frequently the server will send out an advertising packet. Higher frequency will consume more energy, and low frequency will use less. TxPowerLevel deals with the broadcast range, so a higher level will have a larger area in which devices will be able to find it.

For the AdvertiseData, we will need to parcel and set the Service UUID.

private void startAdvertising() {
            ...
            .build();
    ParcelUuid parcelUuid = new ParcelUuid(SERVICE_UUID);
    AdvertiseData data = new AdvertiseData.Builder()
            .setIncludeDeviceName(true)
            .addServiceUuid(parcelUuid)
            .build();
}

Including the Device Name is optional, but can be a quick and easy way to identify the server.
This could be the phone name or device number; it varies across devices.
We also need an AdvertiseCallback, but there is not much to do with it.
Its primary responsibility is to report if the server successfully started advertising.
It is also required to stop advertising, much like the ScanCallback.
Finally, let’s begin advertising.

private void startAdvertising() {
            ...
            .build();
    mBluetoothLeAdvertiser.startAdvertising(settings, data, mAdvertiseCallback);
}
private AdvertiseCallback mAdvertiseCallback = new AdvertiseCallback() {
    @Override
    public void onStartSuccess(AdvertiseSettings settingsInEffect) {
        Log.d(TAG, "Peripheral advertising started.");
    }

    @Override
    public void onStartFailure(int errorCode) {
        Log.d(TAG, "Peripheral advertising failed: " + errorCode);
    }
};

Thats all!
We now have a discoverable GATT server up and running.
But what if we wanted to stop it?
For several reasons (battery life is a primary concern), we should stop the server when our app is backgrounded.

protected void onPause() {
    super.onPause();
    stopAdvertising();
    stopServer();
}
private void stopServer() {
    if (mGattServer != null) {
        mGattServer.close();
    }
}
private void stopAdvertising() {
    if (mBluetoothLeAdvertiser != null) {
        mBluetoothLeAdvertiser.stopAdvertising(mAdvertiseCallback);
    }
}

Connect to GATT Server

Back in the ClientActivity, we can now add a filter for our GATT server’s Service UUID.

private void startScan() {
    ...
    List<ScanFilter> filters = new ArrayList<>();
    ScanFilter scanFilter = new ScanFilter.Builder()
            .setServiceUuid(new ParcelUuid(SERVICE_UUID))
            .build();
    filters.add(scanFilter);
    ScanSettings settings = new ScanSettings.Builder()
    ...
}

Now we should only have a single ScanResult.
We can then pull out the BluetoothDevice and connect to it.

private class BtleScanCallback extends ScanCallback {
    ...
    private void addScanResult(ScanResult result) {
        stopScan();
        BluetoothDevice bluetoothDevice = scanResult.getDevice();
        connectDevice(bluetoothDevice);
    }
}
private void connectDevice(BluetoothDevice device) {
    GattClientCallback gattClientCallback = new GattClientCallback();
    mGatt = device.connectGatt(this, false, gattClientCallback);
}
private class GattClientCallback extends BluetoothGattCallback { }

We pass in a Context, false for autoconnect and a BluetoothGattCallback.
Be careful when using autoconnect, as it could run rampant.
An active BLE interface with uncontrolled connection attempts will drain the battery and choke up the CPU, so its best to handle reconnections manually.

In our bluetoothGattCallback, let’s implement onConnectionStateChange so we can see the results of the connection.
This callback will have a GATT client, the connection status and the new connection state.

private class GattClientCallback extends BluetoothGattCallback {
    @Override
    public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
        super.onConnectionStateChange(gatt, status, newState);
        if (status == BluetoothGatt.GATT_FAILURE) {
            disconnectGattServer();
            return;
        } else if (status != BluetoothGatt.GATT_SUCCESS) {
            disconnectGattServer();
            return;
        }
        if (newState == BluetoothProfile.STATE_CONNECTED) {
            mConnected = true;
        } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
            disconnectGattServer();
        }
    }
}

If the status is Success, great!
We have successfully managed to not run into any issues while connecting, avoiding the various error states.
Update mConnected to match the newState, and disconnect if appropriate.
Any other status should be handled as an error and the client should be disconnected.

public void disconnectGattServer() {
    mConnected = false;
    if (mGatt != null) {
        mGatt.disconnect();
        mGatt.close();
    }
}

On our Server, we need to implement onConnectionStateChange in our BluetoothGattServerCallback to keep track of the connected devices.

private class GattServerCallback extends BluetoothGattServerCallback {
    @Override
    public void onConnectionStateChange(BluetoothDevice device, int status, int newState) {
        super.onConnectionStateChange(device, status, newState);
        if (newState == BluetoothProfile.STATE_CONNECTED) {
            mDevices.add(device));
        } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
            mDevices.remove(device));
        }
    }
}

Here we are simply adding and removing devices from a list based on the newState.

What’s Next

Congratulations!
We now have a BLE Server and Client that can connect.
You can find the full source for this post on my public GitHub repository. And now, a few words of advice if you are having problems getting your Server and Client setup.

Due to the asynchronous nature of callbacks, I recommend adding logs everywhere.
The BLE callbacks have very little tolerance for delay, and stepping through with debugger will cause problems.
This is why it’s important to do as little work in the callback as possible.
Consider using a Handler to send all variables to a listener interface.

Remember to treat almost everything other than Success as a failure.
Always disconnect when:

  • Status is failure
  • Status is not success
  • State is disconnected

And finally, if you run into a problem with either Server or Client not connecting or displaying any other weirdness, you should simply power cycle. Many times the device’s Bluetooth will reach an unrecoverable state and need to be disabled and re-enabled.

In Part 2 of this series, we will add the ability to send data between the Client and Server, so look out for that post soon. Happy coding!

Avatar

Andrew Lunsford

Not Happy with Your Current App, or Digital Product?

Submit your event

Let's Discuss Your Project

Let's Discuss Your Project