The following sections cover the steps for setting up a Bluetooth low energy (BLE) connection from your app. For information on the concepts you should understand before implementing BLE functionality in your app, see Bluetooth low energy.
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 android:required
is set to false in your app's
manifest. 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. If a device does not have a Bluetooth radio, attempts to access theBluetoothAdapter
will returnnull
. The following code sample shows how to get the adapter. Note that this approach usesgetSystemService()
to return an instance ofBluetoothManager
, which is then used to get the adapter.Kotlin
// Initializes Bluetooth adapter. val bluetoothManager = getSystemService(BluetoothManager::class.java) val bluetoothAdapter: BluetoothAdapter? = bluetoothManager?.adapter
Java
// Initializes Bluetooth adapter. final BluetoothManager bluetoothManager = getSystemService(BluetoothManager.class); BluetoothAdapter bluetoothAdapter = null; if (bluetoothManager != null) { bluetoothAdapter = bluetoothManager.getAdapter(); }
Enable Bluetooth.
First, check that the Bluetooth adapter is not null, meaning that the device has a Bluetooth radio. Then call
isEnabled()
to verify that Bluetooth is currently enabled. If this method returns false, then Bluetooth is disabled. The following code sample checks whether Bluetooth is enabled. If it isn't, the code displays a prompt for the user to enable Bluetooth on the device.Kotlin
// 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) { 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); }
Find BLE devices
To find BLE devices, use the
startScan()
method. This method takes a
ScanCallback
as a parameter.
You must implement this callback because that is how scan results are returned.
Scanning is battery-intensive, so follow these guidelines:
- As soon as you find the desired device, stop scanning.
- Never scan in a loop, and always 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.
In this example, the BLE app provides an activity (DeviceScanActivity
) to scan
for available Bluetooth LE devices and display them in a list to the user. It
also shows how to start and stop a scan.
Kotlin
private val bluetoothLeScanner: BluetoothLeScanner? = bluetoothAdapter?.bluetoothLeScanner private var scanning = false private val handler = Handler() // Stops scanning after 10 seconds. private val SCAN_PERIOD: Long = 10000 private fun scanLeDevice() { bluetoothLeScanner?.let { scanner -> if (!scanning) { // Stops scanning after a pre-defined scan period. handler.postDelayed({ scanning = false scanner.stopScan(leScanCallback) }, SCAN_PERIOD) scanning = true scanner.startScan(leScanCallback) } else { scanning = false scanner.stopScan(leScanCallback) } } }
Java
private BluetoothLeScanner bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner(); private boolean scanning; private Handler handler = new Handler(); // Stops scanning after 10 seconds. private static final long SCAN_PERIOD = 10000; private void scanLeDevice() { if(bluetoothLeScanner != null) { if (!scanning) { // Stops scanning after a pre-defined scan period. handler.postDelayed(new Runnable() { @Override public void run() { scanning = false; bluetoothLeScanner.stopScan(leScanCallback); } }, SCAN_PERIOD); scanning = true; bluetoothLeScanner.startScan(leScanCallback); } else { scanning = false; bluetoothLeScanner.stopScan(leScanCallback); } } }
If you want to scan for only specific types of peripherals, call
startScan(List<ScanFilter>, ScanSettings, ScanCallback)
,
providing a list of ScanFilter
objects to restrict the devices that the scan looks for and a
ScanSettings
object to specify
parameters about the scan.
The following code sample shows an implementation of the ScanCallback
, which
is the interface used to deliver BLE scan results. When results are found,
they're added to a list adapter in the DeviceScanActivity
to display to the
user.
Kotlin
private val leDeviceListAdapter = LeDeviceListAdapter() // Device scan callback. private val leScanCallback: ScanCallback = object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { super.onScanResult(callbackType, result) leDeviceListAdapter.addDevice(result.device) leDeviceListAdapter.notifyDataSetChanged() } }
Java
private LeDeviceListAdapter leDeviceListAdapter = new LeDeviceListAdapter(); // Device scan callback. private ScanCallback leScanCallback = new ScanCallback() { @Override public void onScanResult(int callbackType, ScanResult result) { super.onScanResult(callbackType, result); leDeviceListAdapter.addDevice(result.getDevice()); leDeviceListAdapter.notifyDataSetChanged(); } };
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, use the
connectGatt()
method. This method takes three parameters: a
Context
object, a boolean indicating
whether to automatically connect to the BLE device as soon as it becomes
available (autoConnect
), and a reference to a
BluetoothGattCallback
.
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 and any further
GATT client operations.
Kotlin
var bluetoothGatt: BluetoothGatt? = null ... bluetoothGatt = device.connectGatt(this, false, gattCallback)
Java
bluetoothGatt = device.connectGatt(this, false, gattCallback);
In the following example, the BLE app provides an activity
(DeviceControlActivity
) to connect to Bluetooth devices, display device 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. The communication is
performed using a bound service which allows
the activity to connect to the BluetoothLeService
and call functions to
connect to the devices. The BluetoothService
needs a
Binder
implementation that provides access to
the service for the activity.
Kotlin
class BluetoothLeService : Service() { private val binder = LocalBinder() override fun onBind(intent: Intent): IBinder? { return binder } inner class LocalBinder : Binder() { fun getService() : BluetoothLeService { return this@BluetoothLeService } } }
Java
class BluetoothLeService extends Service { private Binder binder = new LocalBinder(); @Nullable @Override public IBinder onBind(Intent intent) { return binder; } class LocalBinder extends Binder { public BluetoothLeService getService() { return BluetoothLeService.this; } } }
The activity can start the service using
bindService()
,
passing in an Intent
to start the
service, a ServiceConnection
implementation to listen for the connection and disconnection events, and a flag
to specify additional connection options.
Kotlin
class DeviceControlActivity : AppCompatActivity() { private var bluetoothService : BluetoothLeService? = null // Code to manage Service lifecycle. private val serviceConnection: ServiceConnection = object : ServiceConnection { override fun onServiceConnected( componentName: ComponentName, service: IBinder ) { bluetoothService = (service as LocalBinder).getService() bluetoothService?.let { bluetooth -> // call functions on service to check connection and connect to devices } } override fun onServiceDisconnected(componentName: ComponentName) { bluetoothService = null } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.gatt_services_characteristics) val gattServiceIntent = Intent(this, BluetoothLeService::class.java) bindService(gattServiceIntent, serviceConnection, Context.BIND_AUTO_CREATE) } }
Java
class DeviceControlActivity extends AppCompatActivity { private BluetoothLeService bluetoothService; private ServiceConnection serviceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { bluetoothService = ((LocalBinder) service).getService(); if (bluetoothService != null) { // call functions on service to check connection and connect to devices } } @Override public void onServiceDisconnected(ComponentName name) { bluetoothService = null; } }; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.gatt_services_characteristics); Intent gattServiceIntent = new Intent(this, BluetoothLeService.class); bindService(gattServiceIntent, serviceConnection, Context.BIND_AUTO_CREATE); } }
Once the service is bound to, it needs to access the BluetoothAdapter
through
the BluetoothManager
. It should check that both the manager and adapter are
available. The following code sample wraps this setup code in an initialize()
function that returns a Boolean
value indicating success.
Kotlin
private const val TAG = "BluetoothLeService" class BluetoothLeService : Service() { private var bluetoothManager: BluetoothManager? = null private var bluetoothAdapter: BluetoothAdapter? = null fun initialize(): Boolean { // If bluetoothManager is null, try to set it if (bluetoothManager == null) { bluetoothManager = getSystemService(BluetoothManager::class.java) if (bluetoothManager == null) { Log.e(TAG, "Unable to initialize BluetoothManager.") return false } } // For API level 18 and higher, get a reference to BluetoothAdapter through // BluetoothManager. bluetoothManager?.let { manager -> bluetoothAdapter = manager.adapter if (bluetoothAdapter == null) { Log.e(TAG, "Unable to obtain a BluetoothAdapter.") return false } return true } ?: return false } ... }
Java
class BluetoothLeService extends Service { public static final String TAG = "BluetoothLeService"; private BluetoothManager bluetoothManager; private BluetoothAdapter bluetoothAdapter; public boolean initialize() { // If bluetoothManager is null, try to set it if (bluetoothManager == null) { bluetoothManager = getSystemService(BluetoothManager.class); if (bluetoothManager == null) { Log.e(TAG, "Unable to initialize BluetoothManager."); return false; } } // For API level 18 and higher, get a reference to BluetoothAdapter through // BluetoothManager. bluetoothAdapter = bluetoothManager.getAdapter(); if (bluetoothAdapter == null) { Log.e(TAG, "Unable to obtain a BluetoothAdapter."); return false; } return true; } ... }
The activity calls this function within the
ServiceConnection
. Handling a
return value of false from the initialize()
function depends on your
application. For example, you could show an error message to the user indicating
that the current device does not support the Bluetooth operation. In the
following code sample, finish()
is called on the activity to send the user back to the previous screen.
Kotlin
class DeviceControlActivity : AppCompatActivity() { // Code to manage Service lifecycle. private val serviceConnection: ServiceConnection = object : ServiceConnection { override fun onServiceConnected( componentName: ComponentName, service: IBinder ) { bluetoothService = (service as LocalBinder).getService() bluetoothService?.let { bluetooth -> if (!bluetooth.initialize()) { Log.e(TAG, "Unable to initialize Bluetooth") finish() } else { // perform device connection } } } override fun onServiceDisconnected(componentName: ComponentName) { bluetoothService = null } } ... }
Java
class DeviceControlsActivity extends AppCompatActivity { private ServiceConnection serviceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { bluetoothService = ((LocalBinder) service).getService(); if (bluetoothService != null) { if (!bluetoothService.initialize()) { Log.e(TAG, "Unable to initialize Bluetooth"); finish(); } else { // perform device connection } } } @Override public void onServiceDisconnected(ComponentName name) { bluetoothService = null; } }; ... }
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, the following code sample 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. The following code sample 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, call
close()
so that 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; }