Building Android Apps to Control Bluetooth LE Devices

Bluetooth is the wireless protocol designed to be used with devices that are, roughly speaking, in the same room. So it operates at shorter range than Wifi, but longer range than NFC. But unlike a Wifi device, Bluetooth devices are usually battery powered, so that reduced range offers some power savings.

A key trait of classic bluetooth is that it is always transmitting, even when no data is being sent. In the 4.0 version of the Bluetooth spec, Bluetooth Low Energy was added as a mode that changes this dynamic so that either end of the connection only uses power to transmit when there is data to be sent.

These days, all smartphones support BLE, and the most common consumer devices that use it are wearable health devices like FitBits, heart rate monitors, and so on. But due to the simple nature of BLE, it’s straightforward to use an Android phone to connect to and communicate with BLE devices.

Watch Ben Berry's talk from RIoT Developer Day about building BLE apps for Android

Building a BLE App

These are the general steps your app will need to do when establishing a connection with a BLE device.

  1. Scan For Devices (Scanning with a filter is ideal to limit the number or irrelevant devices found.)
  2. Connect to the Device (No pairing or approval process. If the device is a BLE device, anyone can connect to it.)
  3. Get GATT Client (see below for more on GATT)
  4. Discover services available on the device
  5. Get Characteristics (see below for more on characteristics)

GATT And Other Important Profiles

The Bluetooth spec outlines a number of profiles (protocols for communicating) like A2DP for audio streaming, HSP/HFP for phone calls, AVRCP for controlling media, and so on. But BLE relies on GATT, the Generic Attribute Profile, which just has a few simple functions. GATT is all about reading and writing Characteristics, the Bluetooth jargon term for what are essentially key-value stores.

BLE Data Organization

Understanding the hierarchy of accessing data via GATT is useful. Here is how the data accessible via GATT on a device is organized.

  • Service - Logical Grouping of characteristics, referred to by a UUID
  • Characteristic - The piece of data itself, referred to by a separate UUID
  • Value - The bytes actually stored at the Characteristic’s UUID “address”
  • Descriptor - Metadata about the characteristic or value, such as the unit of measure that the value is expressed in

BLE hierarchy

The Code

Below you will find the code necessary to connect your Android app to a BLE device. For greater context, you can see the full video of my recent talk at RIoT Developer Day (including me walking through the code) or review the slides from my presentation. I hope that helps!

Add to AndroidManifest.xml

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

Scanning with Filters

fun scanForDevices(context: Context, serviceUUID: UUID) {
    val adapter = BluetoothAdapter.getDefaultAdapter()

    if (!adapter.isEnabled) {
        //handle error
    }

    val uuid = ParcelUuid(serviceUUID)
    val filter = ScanFilter.Builder().setServiceUuid(uuid).build()
    val filters = listOf(filter)

    val settings = ScanSettings
        .Builder()
        .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
        .build()

    context.runWithPermissions(Manifest.permission.ACCESS_COARSE_LOCATION) {
        adapter.bluetoothLeScanner.startScan(filters, settings, callback)
    }
}

Handling Devices

private val callback = object : ScanCallback() {
    override fun onBatchScanResults(results: MutableList<ScanResult>?) {
        results?.forEach { result ->
            deviceFound(result.device)
        }
    }

    override fun onScanResult(callbackType: Int, result: ScanResult?) {
        result?.let { deviceFound(result.device) }
    }

    override fun onScanFailed(errorCode: Int) {
        handleError(errorCode)
    }
}

private fun deviceFound(device: BluetoothDevice) {
    device.connectGatt(context, true, gattCallback)
}

Interacting with BLE Device

private val gattCallback = object : BluetoothGattCallback() {
    override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
        if (newState == BluetoothGatt.STATE_CONNECTED) {
            gatt?.requestMtu(256)
            gatt?.discoverServices()
        }
    }

    override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
        val characteristic = gatt?.getService(expandUuid(0x180F)) // Battery service
            ?.getCharacteristic(expandUuid(0x2A19)) // Battery life
        gatt?.readCharacteristic(characteristic)
        gatt?.setCharacteristicNotification(characteristic, true)
        characteristic?.value = byteArrayOf(50)
        gatt?.writeCharacteristic(characteristic)
    }

    override fun onCharacteristicRead(
        gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int
    ) { /* ... */ }

    override fun onCharacteristicWrite(
        gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int
    ) { /* ... */}

    override fun onCharacteristicChanged(
        gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?
    ) {
        characteristic?.let {
            val batteryLife = characteristic.value[0].toInt()
            Log.d(TAG, "Battery life is: $batteryLife")
        }
    }
}