Android 4.3 (API level 18) introduces built-in platform support for Bluetooth Low Energy (BLE) in the central role and provides APIs that apps can use to discover devices, query for services, and transmit information.
Common use cases include the following:
- Transferring small amounts of data between nearby devices.
- Interacting with proximity sensors like Google Beacons to give users a customized experience based on their current location.
In contrast to Classic Bluetooth, Bluetooth Low Energy (BLE) is designed to provide significantly lower power consumption. This allows Android apps to communicate with BLE devices that have stricter power requirements, such as proximity sensors, heart rate monitors, and fitness devices.
Key terms and concepts
Here is a summary of key BLE terms and concepts:
- Generic Attribute Profile (GATT)—The GATT profile
is a general specification for sending and receiving short pieces of data known
as "attributes" over a BLE link. All current Low Energy application profiles are
based on GATT.
- The Bluetooth SIG defines many profiles for Low Energy devices. A profile is a specification for how a device works in a particular application. Note that a device can implement more than one profile. For example, a device could contain a heart rate monitor and a battery level detector.
- Attribute Protocol (ATT)—GATT is built on top of the Attribute Protocol (ATT). This is also referred to as GATT/ATT. ATT is optimized to run on BLE devices. To this end, it uses as few bytes as possible. Each attribute is uniquely identified by a Universally Unique Identifier (UUID), which is a standardized 128-bit format for a string ID used to uniquely identify information. The attributes transported by ATT are formatted as characteristics and services.
- Characteristic—A characteristic contains a single value and 0-n descriptors that describe the characteristic's value. A characteristic can be thought of as a type, analogous to a class.
- Descriptor—Descriptors are defined attributes that describe a characteristic value. For example, a descriptor might specify a human-readable description, an acceptable range for a characteristic's value, or a unit of measure that is specific to a characteristic's value.
- Service—A service is a collection of characteristics. For example, you could have a service called "Heart Rate Monitor" that includes characteristics such as "heart rate measurement." You can find a list of existing GATT-based profiles and services on bluetooth.org.
Roles and responsibilities
Here are the roles and responsibilities that apply when an Android device interacts with a BLE device:
- Central vs. peripheral. This applies to the BLE connection itself. The device in the central role scans, looking for advertisement, and the device in the peripheral role makes the advertisement.
- GATT server vs. GATT client. This determines how two devices talk to each other once they've established the connection.
To understand the distinction, imagine that you have an Android phone and an activity tracker that is a BLE device. The phone supports the central role; the activity tracker supports the peripheral role (to establish a BLE connection you need one of each—two things that only support peripheral couldn't talk to each other, nor could two things that only support central).
Once the phone and the activity tracker have established a connection, they start transferring GATT metadata to one another. Depending on the kind of data they transfer, one or the other might act as the server. For example, if the activity tracker wants to report sensor data to the phone, it might make sense for the activity tracker to act as the server. If the activity tracker wants to receive updates from the phone, then it might make sense for the phone to act as the server.
In the example used in this document, the Android app (running on an Android
device) is the GATT client. The app gets data from the GATT server, which is a
BLE heart rate monitor that supports the
Heart
Rate Profile. But you could alternatively design
your Android app to play the GATT server
role. See BluetoothGattServer
for more
information.
BLE permissions
In order to use Bluetooth features in your application, you must declare
the Bluetooth permission BLUETOOTH
.
You need this permission to perform any Bluetooth communication,
such as requesting a connection, accepting a connection, and transferring data.
If you want your app to initiate device discovery or manipulate Bluetooth
settings, you must also declare the BLUETOOTH_ADMIN
permission. Note: If you use the
BLUETOOTH_ADMIN
permission, then you must
also have the BLUETOOTH
permission.
Declare the Bluetooth permission(s) in your application manifest file. For example:
<uses-permission android:name="android.permission.BLUETOOTH"/> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
If you want to declare that your app is available to BLE-capable devices only, include the following in your app's manifest:
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
However, if you want to make your app available to devices that don't support BLE,
you should still include this element in your app's manifest, but set required="false"
.
Then at run-time you can determine BLE availability by using
PackageManager.hasSystemFeature()
:
Kotlin
private fun PackageManager.missingSystemFeature(name: String): Boolean = !hasSystemFeature(name) ... packageManager.takeIf { it.missingSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE) }?.also { Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show() finish() }
Java
// Use this check to determine whether BLE is supported on the device. Then // you can selectively disable BLE-related features. if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) { Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show(); finish(); }
Note: LE Beacons are often associated with location. In order to use
BluetoothLeScanner
, you must request the user's permission by
declaring either the ACCESS_COARSE_LOCATION
or
ACCESS_FINE_LOCATION
permission in your
app's manifest file. Without these permissions, scans won't return any
results.
Set up BLE
Before your application can communicate over BLE, you need
to verify that BLE is supported on the device, and if so, ensure that it is enabled.
Note that this check is only necessary if <uses-feature.../>
is set to false.
If BLE is not supported, then you should gracefully disable any
BLE features. If BLE is supported, but disabled, then you can request that the
user enable Bluetooth without leaving your application. This setup is
accomplished in two steps, using the BluetoothAdapter
.
- Get the
BluetoothAdapter
The
BluetoothAdapter
is required for any and all Bluetooth activity. TheBluetoothAdapter
represents the device's own Bluetooth adapter (the Bluetooth radio). There's one Bluetooth adapter for the entire system, and your application can interact with it using this object. The snippet below shows how to get the adapter. Note that this approach usesgetSystemService()
to return an instance ofBluetoothManager
, which is then used to get the adapter. Android 4.3 (API Level 18) introducesBluetoothManager
:Kotlin
private val bluetoothAdapter: BluetoothAdapter? by lazy(LazyThreadSafetyMode.NONE) { val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager bluetoothManager.adapter }
Java
private BluetoothAdapter bluetoothAdapter; ... // Initializes Bluetooth adapter. final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); bluetoothAdapter = bluetoothManager.getAdapter();
- Enable Bluetooth
Next, you need to ensure that Bluetooth is enabled. Call
isEnabled()
to check whether Bluetooth is currently enabled. If this method returns false, then Bluetooth is disabled. The following snippet checks whether Bluetooth is enabled. If it isn't, the snippet displays an error prompting the user to go to Settings to enable Bluetooth:Kotlin
private val BluetoothAdapter.isDisabled: Boolean get() = !isEnabled ... // Ensures Bluetooth is available on the device and it is enabled. If not, // displays a dialog requesting user permission to enable Bluetooth. bluetoothAdapter?.takeIf { it.isDisabled }?.apply { val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT) }
Java
// Ensures Bluetooth is available on the device and it is enabled. If not, // displays 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); }
Note: The
REQUEST_ENABLE_BT
constant passed tostartActivityForResult(android.content.Intent, int)
is a locally-defined integer (which must be greater than 0) that the system passes back to you in youronActivityResult(int, int, android.content.Intent)
implementation as therequestCode
parameter.
Find BLE devices
To find BLE devices, you use the
startLeScan()
method.
This method takes a BluetoothAdapter.LeScanCallback
as a parameter. You must implement this callback, because that is how scan
results are returned. Because scanning is battery-intensive, you should observe
the following guidelines:
- As soon as you find the desired device, stop scanning.
- Never scan on a loop, and set a time limit on your scan. A device that was previously available may have moved out of range, and continuing to scan drains the battery.
The following snippet shows how to start and stop a scan:
Kotlin
private const val SCAN_PERIOD: Long = 10000 /** * Activity for scanning and displaying available BLE devices. */ class DeviceScanActivity( private val bluetoothAdapter: BluetoothAdapter, private val handler: Handler ) : ListActivity() { private var mScanning: Boolean = false private fun scanLeDevice(enable: Boolean) { when (enable) { true -> { // Stops scanning after a pre-defined scan period. handler.postDelayed({ mScanning = false bluetoothAdapter.stopLeScan(leScanCallback) }, SCAN_PERIOD) mScanning = true bluetoothAdapter.startLeScan(leScanCallback) } else -> { mScanning = false bluetoothAdapter.stopLeScan(leScanCallback) } } } }
Java
/** * Activity for scanning and displaying available BLE devices. */ public class DeviceScanActivity extends ListActivity { private BluetoothAdapter bluetoothAdapter; private boolean mScanning; private Handler handler; // Stops scanning after 10 seconds. private static final long SCAN_PERIOD = 10000; ... private void scanLeDevice(final boolean enable) { if (enable) { // Stops scanning after a pre-defined scan period. handler.postDelayed(new Runnable() { @Override public void run() { mScanning = false; bluetoothAdapter.stopLeScan(leScanCallback); } }, SCAN_PERIOD); mScanning = true; bluetoothAdapter.startLeScan(leScanCallback); } else { mScanning = false; bluetoothAdapter.stopLeScan(leScanCallback); } ... } ... }
If you want to scan for only specific types of peripherals, you can instead
call startLeScan(UUID[], BluetoothAdapter.LeScanCallback)
,
providing an array of UUID
objects that specify the GATT
services your app supports.
Here is an implementation of the
BluetoothAdapter.LeScanCallback
,
which is the interface used to deliver BLE scan results:
Kotlin
val leDeviceListAdapter: LeDeviceListAdapter = ... private val leScanCallback = BluetoothAdapter.LeScanCallback { device, rssi, scanRecord -> runOnUiThread { leDeviceListAdapter.addDevice(device) leDeviceListAdapter.notifyDataSetChanged() } }
Java
private LeDeviceListAdapter leDeviceListAdapter; ... // Device scan callback. private BluetoothAdapter.LeScanCallback leScanCallback = new BluetoothAdapter.LeScanCallback() { @Override public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) { runOnUiThread(new Runnable() { @Override public void run() { leDeviceListAdapter.addDevice(device); leDeviceListAdapter.notifyDataSetChanged(); } }); } };
Note: You can only scan for Bluetooth LE devices or scan for Classic Bluetooth devices, as described in Bluetooth. You cannot scan for both Bluetooth LE and classic devices at the same time.
Connect to a GATT server
The first step in interacting with a BLE device is connecting to it—
more specifically, connecting to the GATT server on the device. To
connect to a GATT server on a BLE device, you use the
connectGatt()
method.
This method takes three parameters: a Context
object,
autoConnect
(boolean indicating whether to automatically connect to
the BLE device as soon as it becomes available), and a reference to a
BluetoothGattCallback
:
Kotlin
var bluetoothGatt: BluetoothGatt? = null ... bluetoothGatt = device.connectGatt(this, false, gattCallback)
Java
bluetoothGatt = device.connectGatt(this, false, gattCallback);
This connects to the GATT server hosted by the BLE device, and returns a
BluetoothGatt
instance, which you can then use to
conduct GATT client operations. The caller (the Android app) is the GATT client.
The BluetoothGattCallback
is used to deliver results
to the client, such as connection status, as well as any further GATT client
operations.
In this example, the BLE app provides an activity
(DeviceControlActivity
) to connect,
display data, and display GATT services and characteristics
supported by the device. Based on user input, this activity communicates with a
Service
called BluetoothLeService
,
which interacts with the BLE device via the Android BLE API:
Kotlin
private val TAG = BluetoothLeService::class.java.simpleName private const val STATE_DISCONNECTED = 0 private const val STATE_CONNECTING = 1 private const val STATE_CONNECTED = 2 const val ACTION_GATT_CONNECTED = "com.example.bluetooth.le.ACTION_GATT_CONNECTED" const val ACTION_GATT_DISCONNECTED = "com.example.bluetooth.le.ACTION_GATT_DISCONNECTED" const val ACTION_GATT_SERVICES_DISCOVERED = "com.example.bluetooth.le.ACTION_GATT_SERVICES_DISCOVERED" const val ACTION_DATA_AVAILABLE = "com.example.bluetooth.le.ACTION_DATA_AVAILABLE" const val EXTRA_DATA = "com.example.bluetooth.le.EXTRA_DATA" val UUID_HEART_RATE_MEASUREMENT = UUID.fromString(SampleGattAttributes.HEART_RATE_MEASUREMENT) // A service that interacts with the BLE device via the Android BLE API. class BluetoothLeService(private var bluetoothGatt: BluetoothGatt?) : Service() { private var connectionState = STATE_DISCONNECTED // Various callback methods defined by the BLE API. private val gattCallback = object : BluetoothGattCallback() { override fun onConnectionStateChange( gatt: BluetoothGatt, status: Int, newState: Int ) { val intentAction: String when (newState) { BluetoothProfile.STATE_CONNECTED -> { intentAction = ACTION_GATT_CONNECTED connectionState = STATE_CONNECTED broadcastUpdate(intentAction) Log.i(TAG, "Connected to GATT server.") Log.i(TAG, "Attempting to start service discovery: " + bluetoothGatt?.discoverServices()) } BluetoothProfile.STATE_DISCONNECTED -> { intentAction = ACTION_GATT_DISCONNECTED connectionState = STATE_DISCONNECTED Log.i(TAG, "Disconnected from GATT server.") broadcastUpdate(intentAction) } } } // New services discovered override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { when (status) { BluetoothGatt.GATT_SUCCESS -> broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED) else -> Log.w(TAG, "onServicesDiscovered received: $status") } } // Result of a characteristic read operation override fun onCharacteristicRead( gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int ) { when (status) { BluetoothGatt.GATT_SUCCESS -> { broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic) } } } } }
Java
// A service that interacts with the BLE device via the Android BLE API. public class BluetoothLeService extends Service { private final static String TAG = BluetoothLeService.class.getSimpleName(); private BluetoothManager bluetoothManager; private BluetoothAdapter bluetoothAdapter; private String bluetoothDeviceAddress; private BluetoothGatt bluetoothGatt; private int connectionState = STATE_DISCONNECTED; private static final int STATE_DISCONNECTED = 0; private static final int STATE_CONNECTING = 1; private static final int STATE_CONNECTED = 2; public final static String ACTION_GATT_CONNECTED = "com.example.bluetooth.le.ACTION_GATT_CONNECTED"; public final static String ACTION_GATT_DISCONNECTED = "com.example.bluetooth.le.ACTION_GATT_DISCONNECTED"; public final static String ACTION_GATT_SERVICES_DISCOVERED = "com.example.bluetooth.le.ACTION_GATT_SERVICES_DISCOVERED"; public final static String ACTION_DATA_AVAILABLE = "com.example.bluetooth.le.ACTION_DATA_AVAILABLE"; public final static String EXTRA_DATA = "com.example.bluetooth.le.EXTRA_DATA"; public final static UUID UUID_HEART_RATE_MEASUREMENT = UUID.fromString(SampleGattAttributes.HEART_RATE_MEASUREMENT); // Various callback methods defined by the BLE API. private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() { @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { String intentAction; if (newState == BluetoothProfile.STATE_CONNECTED) { intentAction = ACTION_GATT_CONNECTED; connectionState = STATE_CONNECTED; broadcastUpdate(intentAction); Log.i(TAG, "Connected to GATT server."); Log.i(TAG, "Attempting to start service discovery:" + bluetoothGatt.discoverServices()); } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { intentAction = ACTION_GATT_DISCONNECTED; connectionState = STATE_DISCONNECTED; Log.i(TAG, "Disconnected from GATT server."); broadcastUpdate(intentAction); } } @Override // New services discovered public void onServicesDiscovered(BluetoothGatt gatt, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED); } else { Log.w(TAG, "onServicesDiscovered received: " + status); } } @Override // Result of a characteristic read operation public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic); } } ... }; ... }
When a particular callback is triggered, it calls the appropriate
broadcastUpdate()
helper method and passes it an action. Note that the data
parsing in this section is performed in accordance with the Bluetooth Heart Rate
Measurement
profile specifications:
Kotlin
private fun broadcastUpdate(action: String) { val intent = Intent(action) sendBroadcast(intent) } private fun broadcastUpdate(action: String, characteristic: BluetoothGattCharacteristic) { val intent = Intent(action) // This is special handling for the Heart Rate Measurement profile. Data // parsing is carried out as per profile specifications. when (characteristic.uuid) { UUID_HEART_RATE_MEASUREMENT -> { val flag = characteristic.properties val format = when (flag and 0x01) { 0x01 -> { Log.d(TAG, "Heart rate format UINT16.") BluetoothGattCharacteristic.FORMAT_UINT16 } else -> { Log.d(TAG, "Heart rate format UINT8.") BluetoothGattCharacteristic.FORMAT_UINT8 } } val heartRate = characteristic.getIntValue(format, 1) Log.d(TAG, String.format("Received heart rate: %d", heartRate)) intent.putExtra(EXTRA_DATA, (heartRate).toString()) } else -> { // For all other profiles, writes the data formatted in HEX. val data: ByteArray? = characteristic.value if (data?.isNotEmpty() == true) { val hexString: String = data.joinToString(separator = " ") { String.format("%02X", it) } intent.putExtra(EXTRA_DATA, "$data\n$hexString") } } } sendBroadcast(intent) }
Java
private void broadcastUpdate(final String action) { final Intent intent = new Intent(action); sendBroadcast(intent); } private void broadcastUpdate(final String action, final BluetoothGattCharacteristic characteristic) { final Intent intent = new Intent(action); // This is special handling for the Heart Rate Measurement profile. Data // parsing is carried out as per profile specifications. if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) { int flag = characteristic.getProperties(); int format = -1; if ((flag & 0x01) != 0) { format = BluetoothGattCharacteristic.FORMAT_UINT16; Log.d(TAG, "Heart rate format UINT16."); } else { format = BluetoothGattCharacteristic.FORMAT_UINT8; Log.d(TAG, "Heart rate format UINT8."); } final int heartRate = characteristic.getIntValue(format, 1); Log.d(TAG, String.format("Received heart rate: %d", heartRate)); intent.putExtra(EXTRA_DATA, String.valueOf(heartRate)); } else { // For all other profiles, writes the data formatted in HEX. final byte[] data = characteristic.getValue(); if (data != null && data.length > 0) { final StringBuilder stringBuilder = new StringBuilder(data.length); for(byte byteChar : data) stringBuilder.append(String.format("%02X ", byteChar)); intent.putExtra(EXTRA_DATA, new String(data) + "\n" + stringBuilder.toString()); } } sendBroadcast(intent); }
Back in DeviceControlActivity
, these events are handled by a
BroadcastReceiver
:
Kotlin
// Handles various events fired by the Service. // ACTION_GATT_CONNECTED: connected to a GATT server. // ACTION_GATT_DISCONNECTED: disconnected from a GATT server. // ACTION_GATT_SERVICES_DISCOVERED: discovered GATT services. // ACTION_DATA_AVAILABLE: received data from the device. This can be a // result of read or notification operations. private val gattUpdateReceiver = object : BroadcastReceiver() { private lateinit var bluetoothLeService: BluetoothLeService override fun onReceive(context: Context, intent: Intent) { val action = intent.action when (action){ ACTION_GATT_CONNECTED -> { connected = true updateConnectionState(R.string.connected) (context as? Activity)?.invalidateOptionsMenu() } ACTION_GATT_DISCONNECTED -> { connected = false updateConnectionState(R.string.disconnected) (context as? Activity)?.invalidateOptionsMenu() clearUI() } ACTION_GATT_SERVICES_DISCOVERED -> { // Show all the supported services and characteristics on the // user interface. displayGattServices(bluetoothLeService.getSupportedGattServices()) } ACTION_DATA_AVAILABLE -> { displayData(intent.getStringExtra(BluetoothLeService.EXTRA_DATA)) } } } }
Java
// Handles various events fired by the Service. // ACTION_GATT_CONNECTED: connected to a GATT server. // ACTION_GATT_DISCONNECTED: disconnected from a GATT server. // ACTION_GATT_SERVICES_DISCOVERED: discovered GATT services. // ACTION_DATA_AVAILABLE: received data from the device. This can be a // result of read or notification operations. private final BroadcastReceiver gattUpdateReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); if (BluetoothLeService.ACTION_GATT_CONNECTED.equals(action)) { connected = true; updateConnectionState(R.string.connected); invalidateOptionsMenu(); } else if (BluetoothLeService.ACTION_GATT_DISCONNECTED.equals(action)) { connected = false; updateConnectionState(R.string.disconnected); invalidateOptionsMenu(); clearUI(); } else if (BluetoothLeService. ACTION_GATT_SERVICES_DISCOVERED.equals(action)) { // Show all the supported services and characteristics on the // user interface. displayGattServices(bluetoothLeService.getSupportedGattServices()); } else if (BluetoothLeService.ACTION_DATA_AVAILABLE.equals(action)) { displayData(intent.getStringExtra(BluetoothLeService.EXTRA_DATA)); } } };
Read BLE attributes
Once your Android app has connected to a GATT server and discovered services, it can read and write attributes, where supported. For example, this snippet iterates through the server's services and characteristics and displays them in the UI:
Kotlin
class DeviceControlActivity : Activity() { // Demonstrates how to iterate through the supported GATT // Services/Characteristics. // In this sample, we populate the data structure that is bound to the // ExpandableListView on the UI. private fun displayGattServices(gattServices: List<BluetoothGattService>?) { if (gattServices == null) return var uuid: String? val unknownServiceString: String = resources.getString(R.string.unknown_service) val unknownCharaString: String = resources.getString(R.string.unknown_characteristic) val gattServiceData: MutableList<HashMap<String, String>> = mutableListOf() val gattCharacteristicData: MutableList<ArrayList<HashMap<String, String>>> = mutableListOf() mGattCharacteristics = mutableListOf() // Loops through available GATT Services. gattServices.forEach { gattService -> val currentServiceData = HashMap<String, String>() uuid = gattService.uuid.toString() currentServiceData[LIST_NAME] = SampleGattAttributes.lookup(uuid, unknownServiceString) currentServiceData[LIST_UUID] = uuid gattServiceData += currentServiceData val gattCharacteristicGroupData: ArrayList<HashMap<String, String>> = arrayListOf() val gattCharacteristics = gattService.characteristics val charas: MutableList<BluetoothGattCharacteristic> = mutableListOf() // Loops through available Characteristics. gattCharacteristics.forEach { gattCharacteristic -> charas += gattCharacteristic val currentCharaData: HashMap<String, String> = hashMapOf() uuid = gattCharacteristic.uuid.toString() currentCharaData[LIST_NAME] = SampleGattAttributes.lookup(uuid, unknownCharaString) currentCharaData[LIST_UUID] = uuid gattCharacteristicGroupData += currentCharaData } mGattCharacteristics += charas gattCharacteristicData += gattCharacteristicGroupData } } }
Java
public class DeviceControlActivity extends Activity { ... // Demonstrates how to iterate through the supported GATT // Services/Characteristics. // In this sample, we populate the data structure that is bound to the // ExpandableListView on the UI. private void displayGattServices(List<BluetoothGattService> gattServices) { if (gattServices == null) return; String uuid = null; String unknownServiceString = getResources(). getString(R.string.unknown_service); String unknownCharaString = getResources(). getString(R.string.unknown_characteristic); ArrayList<HashMap<String, String>> gattServiceData = new ArrayList<HashMap<String, String>>(); ArrayList<ArrayList<HashMap<String, String>>> gattCharacteristicData = new ArrayList<ArrayList<HashMap<String, String>>>(); mGattCharacteristics = new ArrayList<ArrayList<BluetoothGattCharacteristic>>(); // Loops through available GATT Services. for (BluetoothGattService gattService : gattServices) { HashMap<String, String> currentServiceData = new HashMap<String, String>(); uuid = gattService.getUuid().toString(); currentServiceData.put( LIST_NAME, SampleGattAttributes. lookup(uuid, unknownServiceString)); currentServiceData.put(LIST_UUID, uuid); gattServiceData.add(currentServiceData); ArrayList<HashMap<String, String>> gattCharacteristicGroupData = new ArrayList<HashMap<String, String>>(); List<BluetoothGattCharacteristic> gattCharacteristics = gattService.getCharacteristics(); ArrayList<BluetoothGattCharacteristic> charas = new ArrayList<BluetoothGattCharacteristic>(); // Loops through available Characteristics. for (BluetoothGattCharacteristic gattCharacteristic : gattCharacteristics) { charas.add(gattCharacteristic); HashMap<String, String> currentCharaData = new HashMap<String, String>(); uuid = gattCharacteristic.getUuid().toString(); currentCharaData.put( LIST_NAME, SampleGattAttributes.lookup(uuid, unknownCharaString)); currentCharaData.put(LIST_UUID, uuid); gattCharacteristicGroupData.add(currentCharaData); } mGattCharacteristics.add(charas); gattCharacteristicData.add(gattCharacteristicGroupData); } ... } ... }
Receive GATT notifications
It's common for BLE apps to ask to be notified when a particular
characteristic changes on the device. This snippet shows how to set a notification
for a characteristic, using the
setCharacteristicNotification()
method:
Kotlin
lateinit var bluetoothGatt: BluetoothGatt lateinit var characteristic: BluetoothGattCharacteristic var enabled: Boolean = true ... bluetoothGatt.setCharacteristicNotification(characteristic, enabled) val uuid: UUID = UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG) val descriptor = characteristic.getDescriptor(uuid).apply { value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE } bluetoothGatt.writeDescriptor(descriptor)
Java
private BluetoothGatt bluetoothGatt; BluetoothGattCharacteristic characteristic; boolean enabled; ... bluetoothGatt.setCharacteristicNotification(characteristic, enabled); ... BluetoothGattDescriptor descriptor = characteristic.getDescriptor( UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG)); descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); bluetoothGatt.writeDescriptor(descriptor);
Once notifications are enabled for a characteristic,
an onCharacteristicChanged()
callback is triggered if the characteristic changes on the remote device:
Kotlin
// Characteristic notification override fun onCharacteristicChanged( gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic ) { broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic) }
Java
@Override // Characteristic notification public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic); }
Close the client app
Once your app has finished using a BLE device, it should call
close()
so the system can release resources appropriately:
Kotlin
fun close() { bluetoothGatt?.close() bluetoothGatt = null }
Java
public void close() { if (bluetoothGatt == null) { return; } bluetoothGatt.close(); bluetoothGatt = null; }