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, autoConnect (a boolean
indicating whether to automatically connect to the BLE device as soon as it
becomes available), and a reference to a
BluetoothGattCallback:
var bluetoothGatt: BluetoothGatt? = null // ... bluetoothGatt = device.connectGatt(this, false, 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, as well as any further GATT client operations.
Set up a bound service
In the following example, the BLE app provides an activity
(DeviceControlActivity) to connect to Bluetooth devices, display device data,
and display the 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 using the BLE API. A
bound service, which lets the activity connect to the
BluetoothLeService and call functions to connect to the devices, handles this
communication. The BluetoothLeService needs a Binder
implementation that provides access to the service for the activity.
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 } } }
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.
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) } }
Set up the BluetoothAdapter
After the activity binds to the service, the service needs to access the
BluetoothAdapter. It should check that the adapter is
available on the device. For more information about the BluetoothAdapter, see
Set up Bluetooth. The following example wraps this setup
code in an initialize() function that returns a Boolean value indicating
success.
private const val TAG = "BluetoothLeService" class BluetoothLeService : Service() { private var bluetoothAdapter: BluetoothAdapter? = null fun initialize(): Boolean { bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() if (bluetoothAdapter == null) { Log.e(TAG, "Unable to obtain a BluetoothAdapter.") return false } return true } // ... }
The activity calls this function within its ServiceConnection implementation.
Handling a false return value from the initialize() function depends on your
application. You could show an error message to the user indicating that the
current device does not support the Bluetooth operation or disable any features
that require Bluetooth to work. In the following example, the activity calls
finish() to send the user back to the previous screen.
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() } // perform device connection } } override fun onServiceDisconnected(componentName: ComponentName) { bluetoothService = null } } // ... }
Connect to a device
After the BluetoothLeService instance is initialized, it can connect to the
BLE device. The activity needs to send the device address to the service so it
can initiate the connection. The service first calls
getRemoteDevice() on the BluetoothAdapter to access the device. If
the adapter is unable to find a device with that address, getRemoteDevice()
throws an IllegalArgumentException.
fun connect(address: String): Boolean { bluetoothAdapter?.let { adapter -> try { val device = adapter.getRemoteDevice(address) } catch (exception: IllegalArgumentException) { Log.w(TAG, "Device not found with provided address.") return false } // connect to the GATT server on the device return true } ?: run { Log.w(TAG, "BluetoothAdapter not initialized") return false } }
The DeviceControlActivity calls this connect() function after the service is
initialized. The activity needs to pass in the address of the BLE device. In
the following example, the device address is passed to the activity as an intent
extra.
// 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() } // perform device connection deviceAddress?.let { bluetooth.connect(it) } } } override fun onServiceDisconnected(componentName: ComponentName) { bluetoothService = null } }
Declare GATT callback
After the activity tells the service which device to connect to and the service
connects to the device, the service needs to connect to the GATT server on the
BLE device. This connection requires a BluetoothGattCallback to receive
notifications about the connection state, service discovery, characteristic
reads, and characteristic notifications.
This document focuses on the connection state notifications. To learn how to discover services, read characteristics, and request characteristic notifications, see Transfer BLE data.
The onConnectionStateChange() function is triggered when the
connection to the device's GATT server changes. In the following example, the
callback is defined in the Service class so it can be used with the
BluetoothDevice after the service connects to it.
private val bluetoothGattCallback = object : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) { if (newState == BluetoothProfile.STATE_CONNECTED) { // successfully connected to the GATT Server } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { // disconnected from the GATT Server } } }
Connect to GATT service
After the BluetoothGattCallback is declared, the service can use the
BluetoothDevice object from the connect() function to connect to the GATT
service on the device.
The service calls the connectGatt() function. This requires a
Context object, an autoConnect boolean flag, and the
BluetoothGattCallback. In this example, the app is directly connecting to the
BLE device, so false is passed for autoConnect.
The service also adds a BluetoothGatt property so it can
close the connection when it is no longer needed.
class BluetoothLeService : Service() { // ... private var bluetoothGatt: BluetoothGatt? = null // ... fun connect(address: String): Boolean { bluetoothAdapter?.let { adapter -> try { val device = adapter.getRemoteDevice(address) // connect to the GATT server on the device bluetoothGatt = device.connectGatt(this, false, bluetoothGattCallback) return true } catch (exception: IllegalArgumentException) { Log.w(TAG, "Device not found with provided address. Unable to connect.") return false } } ?: run { Log.w(TAG, "BluetoothAdapter not initialized") return false } } }
Broadcast updates
When the service connects to or disconnects from the GATT server, it needs to notify the activity of the new state. There are several ways to accomplish this. The following example uses broadcasts to send the information from the service to the activity.
The service declares a function to broadcast the new state. This function takes
in an action string which is passed to an Intent object before being broadcast
to the system.
private fun broadcastUpdate(action: String) { val intent = Intent(action) sendBroadcast(intent) }
After the broadcast function is in place, the service uses it within the
BluetoothGattCallback to send information about the connection state with the
GATT server. The service declares constants representing the Intent actions
and the service's current connection state.
class BluetoothLeService : Service() { private var connectionState = STATE_DISCONNECTED private val bluetoothGattCallback = object : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) { if (newState == BluetoothProfile.STATE_CONNECTED) { // successfully connected to the GATT Server connectionState = STATE_CONNECTED broadcastUpdate(ACTION_GATT_CONNECTED) } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { // disconnected from the GATT Server connectionState = STATE_DISCONNECTED broadcastUpdate(ACTION_GATT_DISCONNECTED) } } } // ... companion object { const val ACTION_GATT_CONNECTED = "com.example.bluetooth.le.ACTION_GATT_CONNECTED" const val ACTION_GATT_DISCONNECTED = "com.example.bluetooth.le.ACTION_GATT_DISCONNECTED" private const val STATE_DISCONNECTED = 0 private const val STATE_CONNECTED = 2 } }
Listen for updates in activity
After the service broadcasts the connection updates, the activity needs to
implement a BroadcastReceiver. Register this receiver
when setting up the activity, and unregister it when the activity is leaving the
screen. By listening for the events from the service, the activity is able to
update the user interface based on the current connection state with the BLE
device.
class DeviceControlActivity : AppCompatActivity() { private val gattUpdateReceiver: BroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { when (intent.action) { BluetoothLeService.ACTION_GATT_CONNECTED -> { connected = true updateConnectionState(R.string.connected) } BluetoothLeService.ACTION_GATT_DISCONNECTED -> { connected = false updateConnectionState(R.string.disconnected) } } } } override fun onResume() { super.onResume() registerReceiver(gattUpdateReceiver, makeGattUpdateIntentFilter()) bluetoothService?.let { service -> deviceAddress?.let { address -> val result = service.connect(address) Log.d(TAG, "Connect request result=$result") } } } override fun onPause() { super.onPause() unregisterReceiver(gattUpdateReceiver) } private fun makeGattUpdateIntentFilter(): IntentFilter { return IntentFilter().apply { addAction(BluetoothLeService.ACTION_GATT_CONNECTED) addAction(BluetoothLeService.ACTION_GATT_DISCONNECTED) } } }
In Transfer BLE data, the BroadcastReceiver is also used
to communicate the service discovery as well as the characteristic data from the
device.
Close GATT connection
One important step when dealing with Bluetooth connections is to close the
connection when you are finished with it. To do this, call the close()
function on the BluetoothGatt object. In the following example, the service
holds the reference to the BluetoothGatt. When the activity unbinds from the
service, the connection is closed to avoid draining the device battery.
class BluetoothLeService : Service() { // ... override fun onUnbind(intent: Intent?): Boolean { close() return super.onUnbind(intent) } private fun close() { bluetoothGatt?.let { gatt -> gatt.close() bluetoothGatt = null } } }