Descripción general del Bluetooth de bajo consumo

Android 4.3 (API nivel 18) incorpora la compatibilidad de la plataforma con Bluetooth de bajo consumo (BLE) en la función central y ofrece API que las apps pueden usar para descubrir dispositivos, consultar por servicios y transmitir información.

Los siguientes son algunos de los casos de uso típicos:

  • Transferir volúmenes de datos reducidos entre dispositivos cercanos
  • Interactuar con sensores de proximidad como las balizas Google, que permiten que el usuario tenga una experiencia personalizada en función de su ubicación actual

A diferencia del Bluetooth clásico, Bluetooth de bajo consumo (BLE) está diseñado para ofrecer un consumo de energía significativamente menor. Eso permite que las apps de Android se comuniquen con dispositivos BLE con requisitos de consumo más estrictos, como los sensores de proximidad, los monitores de ritmo cardíaco y los dispositivos de estado físico.

Advertencia: Cuando un usuario vincula su dispositivo con otro dispositivo mediante BLE, todas las apps del dispositivo del usuario pueden acceder a los datos que se transmiten entre los dispositivos.

Por esa razón, si tu app captura datos sensibles, debes implementar medidas de seguridad en el nivel de la app para proteger la confidencialidad de esos datos.

Términos y conceptos clave

El siguiente es un resumen de los términos y conceptos clave de BLE:

  • Perfil de atributos genéricos (GATT): El perfil GATT es una especificación para enviar pequeños fragmentos de datos, denominados "atributos", a través de un vínculo BLE. Todos los perfiles de aplicaciones de bajo consumo actuales se basan en GATT.
    • El SIG de Bluetooth define muchos perfiles para dispositivos de bajo consumo. Un perfil es una especificación que define cómo funciona un dispositivo en una aplicación determinada. Cabe señalar que un dispositivo puede implementar más de un perfil. Por ejemplo, un dispositivo puede incluir un monitor de ritmo cardíaco y un detector de nivel de batería.
  • Protocolo de atributos (ATT): GATT está basado en el protocolo de atributos (ATT). Por eso, también se denomina GATT/ATT. ATT está optimizado para funcionar en dispositivos BLE. Por esa razón, usa la menor cantidad de bytes posible. Cada atributo se identifica de forma exclusiva mediante un identificador único universal (UUID), que es un formato estandarizado de 128 bits para un ID de string empleado para identificar información de manera exclusiva. Los atributos transportados por ATT tienen formato de características y servicios.
  • Característica: Una característica contiene un solo valor y 0-n descriptores que describen el valor de la característica. Una característica puede interpretarse como un tipo, que es similar a una clase. 
  • Descriptor: Los descriptores son atributos definidos que describen el valor de una característica. Por ejemplo, un descriptor puede especificar una descripción en lenguaje natural, un rango aceptable para el valor de una característica o una unidad de medida específica del valor de una característica.
  • Servicio: Un servicio es una colección de características. Por ejemplo, podrías tener un servicio denominado "Monitor de ritmo cardíaco" que incluya características como "medición del ritmo cardíaco". Puedes consultar una lista de los perfiles y servicios basados en GATT en bluetooth.org.

Funciones y responsabilidades

Las siguientes son las funciones y responsabilidades que se aplican cuando un dispositivo Android interactúa con un dispositivo BLE:

  • Central y periférica. Esto se aplica a la conexión BLE en sí misma. El dispositivo con la función central escanea en busca de publicidad y el dispositivo con la función periférica hace la publicidad.
  • Servidor GATT y cliente GATT. Esto determina cómo se comunican entre sí dos dispositivos una vez establecida la conexión.

A fin de entender la distinción, supón que tienes un teléfono Android y un dispositivo BLE de seguimiento de actividad. El teléfono es compatible con la función central, y el dispositivo de seguimiento es compatible con la función periférica (para establecer una conexión BLE, se necesita un dispositivo de cada tipo, ya que dos dispositivos que fueran compatibles únicamente con la función periférica no podrían comunicarse, al igual que dos dispositivos compatibles únicamente con la función central).

Una vez que el teléfono y el dispositivo de seguimiento establecen una conexión, comienzan a transferirse metadatos GATT entre sí. Según el tipo de dato que transfieran, cualquiera de los dos puede funcionar como servidor. Por ejemplo, si el dispositivo de seguimiento de actividad quisiera enviar datos del sensor al teléfono, tendría sentido que el primer dispositivo funcionara como servidor. Si el dispositivo de seguimiento de actividad quisiera recibir actualizaciones del teléfono, tendría sentido que el servidor fuera el teléfono.

En el ejemplo que se usa en este documento, la app para Android (que se ejecuta en un dispositivo Android) es el cliente GATT. La app recibe datos del servidor GATT, que es un monitor de ritmo cardíaco BLE compatible con el perfil de ritmo cardíaco. Pero, alternativamente, podrías diseñar la app para Android para que cumpliera la función de servidor GATT. Para obtener más información, consulta BluetoothGattServer.

Permisos BLE

Para usar las características de Bluetooth en tu aplicación, debes declarar el permiso Bluetooth BLUETOOTH. Necesitas este permiso para establecer cualquier comunicación de Bluetooth, como solicitar o aceptar una conexión y transferir datos.

También debes declarar el permiso ACCESS_FINE_LOCATION, dado que las balizas LE suelen estar asociadas a la ubicación. Sin ese permiso, el escaneo no mostrará resultados.

Nota: Si tu app tiene como destino Android 9 (API nivel 28) o una versión inferior, puedes declarar el permiso ACCESS_COARSE_LOCATION en lugar del permiso ACCESS_FINE_LOCATION.

Si deseas que tu app inicie la detección de dispositivos o controle los ajustes de Bluetooth, también debes declarar el permiso BLUETOOTH_ADMIN. Nota: Si usas el permiso BLUETOOTH_ADMIN, también debes contar con el permiso BLUETOOTH.

Declara los permisos en el archivo de manifiesto de tu aplicación. Por ejemplo:

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

<!-- If your app targets Android 9 or lower, you can declare
     ACCESS_COARSE_LOCATION instead. -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

Si quieres declarar que tu app solo está disponible para dispositivos compatibles con BLE, incluye lo siguiente en el manifiesto de la app:

<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>

No obstante, si quieres que tu app esté disponible para dispositivos no compatibles con BLE, debes incluir el elemento en el manifiesto de la app, aunque también debes definir required="false". Luego, durante el tiempo de ejecución, puedes determinar la disponibilidad de BLE con 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();
}

Configuración de BLE

Para que tu aplicación pueda comunicarse a través de BLE, debes verificar que BLE sea compatible con el dispositivo y, si es así, asegurarte de que esté habilitado. Ten en cuenta que esa verificación solo es necesaria si <uses-feature.../> está definido como "false".

Si BLE no es compatible, debes desactivar correctamente todas las funciones relacionadas con BLE. En caso de que BLE sea compatible, pero esté inhabilitado, puedes solicitar que el usuario lo habilite sin abandonar tu aplicación. Esta configuración se logra en dos pasos mediante el BluetoothAdapter.

  1. Obtén el BluetoothAdapter

    El BluetoothAdapter es obligatorio para toda actividad de Bluetooth. El BluetoothAdapter representa el propio adaptador de Bluetooth del dispositivo (la radio Bluetooth). Existe un adaptador de Bluetooth para todo el sistema y tu aplicación puede interactuar con él usando este objeto. El siguiente fragmento ilustra cómo obtener el adaptador. Ten en cuenta que este enfoque usa getSystemService() para devolver una instancia de BluetoothManager, que luego se usa para conseguir el adaptador. Android 4.3 (API nivel 18) incorpora BluetoothManager:

    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();
    
  2. Habilita Bluetooth

    A continuación, debes asegurarte de que Bluetooth esté habilitado. Llama al isEnabled() para verificar si Bluetooth se encuentra actualmente habilitado. Si este método muestra "false", Bluetooth está inhabilitado. El siguiente fragmento verifica si Bluetooth está habilitado. Si no, el fragmento muestra un error que indica al usuario que debe ir a la configuración para activar 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);
    }
    

    Nota: La constante REQUEST_ENABLE_BT pasada a startActivityForResult(android.content.Intent, int) es un valor entero definido localmente (que debe ser superior a 0) que el sistema te devuelve en tu implementación de onActivityResult(int, int, android.content.Intent) como parámetro requestCode.

Buscar dispositivos BLE

Para buscar dispositivos BLE, usas el método startLeScan(). Este método toma un objeto BluetoothAdapter.LeScanCallback como parámetro. Debes implementar esta devolución de llamada, porque así se muestran los resultados de los escaneos. Como el escaneo tiene un consumo alto de batería, debes respetar las siguientes normas:

  • Deja de escanear tan pronto como encuentres el dispositivo que te interesa.
  • Nunca escanees en bucle y define un límite de tiempo para el escaneo. Es posible que un dispositivo disponible anteriormente haya salido del alcance. Seguir el escaneo agota la batería.

En el siguiente fragmento, se muestra cómo iniciar y detener un escaneo:

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);
        }
        ...
    }
...
}

Si quieres buscar tipos de periféricos específicos, puedes hacer una llamada a startLeScan(UUID[], BluetoothAdapter.LeScanCallback) con una matriz de objetos UUID que especifiquen los servicios GATT compatibles con tu app.

Esta es una implementación del objeto BluetoothAdapter.LeScanCallback, que es la interfaz que se usa para entregar los resultados del escaneo BLE:

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();
           }
       });
   }
};

Nota: Solo tienes la opción de buscar dispositivos BLE o dispositivos con Bluetooth clásico, como se describe en Bluetooth. No puedes buscar dispositivos BLE y dispositivos con Bluetooth clásico al mismo tiempo.

Conectarse con un servidor GATT

El primer paso para interactuar con un servidor BLE es conectarse con él: más específicamente, conectarse con el servidor GATT en el dispositivo. Para conectarte con un servidor GATT en un dispositivo BLE, usas el método connectGatt(). El método adopta tres parámetros: un objeto Context, autoConnect (un booleano que define si se intenta una conexión automáticamente con el dispositivo BLE tan pronto como está disponible) y una referencia a BluetoothGattCallback:

Kotlin

var bluetoothGatt: BluetoothGatt? = null
...

bluetoothGatt = device.connectGatt(this, false, gattCallback)

Java

bluetoothGatt = device.connectGatt(this, false, gattCallback);

Esto se conecta con el servidor GATT alojado en el dispositivo BLE y muestra una instancia BluetoothGatt, que puedes usar para realizar operaciones de cliente GATT. El que llama (la app para Android) es el cliente GATT. La BluetoothGattCallback se usa para entregar resultados al cliente, como el estado de conexión y toda operación de cliente GATT adicional.

En este ejemplo, la app BLE ofrece una actividad (DeviceControlActivity) para conectarse, mostrar datos y mostrar servicios y características GATT compatibles con el dispositivo. Sobre la base de los datos ingresados por el usuario, la actividad se comunica con un Service denominado BluetoothLeService, que interactúa con el dispositivo BLE mediante la API de BLE de Android:

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);
            }
        }
     ...
    };
...
}

Cuando se activa una devolución de llamada particular, llamada al método de ayuda broadcastUpdate() correspondiente y le pasa una acción. Cabe señalar que el análisis de datos de esta sección se realiza de acuerdo con las especificaciones del perfil de medición de ritmo cardíaco Bluetooth.

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);
}

En DeviceControlActivity, estos eventos son manejados por un 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));
        }
    }
};

Leer atributos BLE

Una vez que tu app para Android se haya conectado a un servidor GATT y haya detectado servicios, podrá leer y escribir atributos en los casos en los que sea compatible. Por ejemplo, este fragmento hace iteraciones a lo largo de los servicios y características del servidor y los muestra en la IU:

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);
         }
    ...
    }
...
}

Recibir notificaciones GATT

Es habitual que las apps BLE soliciten notificaciones cuando cambia una característica particular en el dispositivo. Este fragmento muestra cómo definir una notificación para una característica usando el método setCharacteristicNotification():

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);

Una vez activadas las notificaciones de una característica, se activa una devolución de llamada onCharacteristicChanged() si la característica cambia en el dispositivo remoto:

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);
}

Cerrar la app cliente

Una vez que tu app haya terminado de usar un dispositivo BLE, debe hacer una llamada a close() para que el sistema pueda liberar recursos de forma adecuada:

Kotlin

fun close() {
    bluetoothGatt?.close()
    bluetoothGatt = null
}

Java

public void close() {
    if (bluetoothGatt == null) {
        return;
    }
    bluetoothGatt.close();
    bluetoothGatt = null;
}