Visão geral do Bluetooth de baixa energia

O Android 4.3 (API de nível 18) introduz suporte à plataforma integrada para Bluetooth de baixa energia (BLE, na sigla em inglês) no papel central e fornece APIs que os aplicativos podem usar para descobrir dispositivos, fazer consultas de serviços e transmitir informações.

Alguns casos de uso comuns são:

  • Transferência de pequenas quantidades de dados entre dispositivos próximos.
  • Interação com sensores de proximidade como os Sensores do Google para oferecer aos usuários uma experiência personalizada com base na localização deles.

Em comparação com o Bluetooth clássico, o Bluetooth de baixa energia (BLE) foi projetado para fornecer um consumo de energia significativamente menor. Isso permite que os aplicativos Android se comuniquem com dispositivos BLE que tenham requisitos de energia mais rígidos, como sensores de proximidade, monitores de frequência cardíaca e dispositivos de fitness.

Atenção: quando um usuário pareia dispositivos por meio do BLE, os dados comunicados entre os dois aparelhos ficam acessíveis para todos os aplicativos no dispositivo do usuário.

Assim, se o aplicativo capturar dados confidenciais, implemente segurança na camada do aplicativo para proteger a privacidade dos dados.

Principais termos e conceitos

Veja um resumo dos principais termos e conceitos do BLE:

  • Perfil de atributo genérico (GATT, na sigla em inglês): o perfil GATT é uma especificação geral para envio e recebimento de pequenas quantidades de dados conhecidos como “atributos” por um vínculo BLE. Todos os perfis de aplicativos de baixa energia atuais têm como base o GATT.
    • O Bluetooth SIG define muitos perfis para dispositivos de baixa energia. O perfil é uma especificação para como um dispositivo funciona em um aplicativo em particular. O dispositivo pode implementar mais de um perfil. Por exemplo, um dispositivo pode conter um detector de nível da bateria e um monitor de frequência cardíaca.
  • Protocolo de atributo (ATT, na sigla em inglês): o GATT foi criado com base no Protocolo de atributo (ATT). Ele também é chamado de GATT/ATT. O ATT é otimizado para operar em dispositivos BLE. Para isso, ele usa o mínimo possível de bytes. Cada atributo é identificado exclusivamente por um Identificador universalmente único (UUID, na sigla em inglês), um formato padronizado de 128 bits para um código de string usado para identificar esse tipo de informação. Os atributos transportados pelo ATT são formatados como características e serviços.
  • Característica: a característica contém um valor único e descritores 0-n para informar o valor dela. Ela pode ser considerada como um tipo, semelhante a uma classe. 
  • Descritor: os descritores são atributos definidos que descrevem o valor de uma característica. Por exemplo, um descritor pode especificar uma descrição legível, um intervalo aceitável para o valor de uma característica ou uma unidade de medida específica a esse valor.
  • Serviço: um serviço é um conjunto de características. Por exemplo, você pode ter um serviço chamado “Monitor de frequência cardíaca” que inclui características como “medida da frequência cardíaca”. É possível encontrar uma lista de perfis e serviços existentes com base em GATT acessando bluetooth.org.

Papéis e responsabilidades

Estes são os papéis e as responsabilidades que se aplicam quando um dispositivo Android interage com um dispositivo BLE:

  • Central x periférico. Aplica-se à conexão BLE em si. O dispositivo no papel central faz a verificação do anúncio e o dispositivo no papel periférico emite o anúncio.
  • Servidor GATT x cliente GATT. Isso determina como os dois dispositivos se comunicam entre si após o estabelecimento da comunicação.

Para entender a diferença, imagine que você tenha um smartphone Android e um rastreador de atividades que seja um dispositivo BLE. O smartphone é compatível com o papel central, enquanto o rastreador de atividades é compatível com o papel periférico. Para estabelecer uma conexão BLE, você precisará de um de cada. Dois itens que tenham compatibilidade somente periférica ou central não podem se comunicar entre si.

Após o smartphone e o rastreador estabelecerem uma conexão, eles começam a transferir metadados GATT um para o outro. Dependendo do tipo de dados transferidos, um deles poderá atuar como servidor. Por exemplo, pode fazer sentido para o rastreador de atividades atuar como servidor caso ele precise enviar dados do sensor para o smartphone. Se o rastreador precisar receber atualizações do smartphone, pode fazer sentido que este último atue como servidor.

No exemplo usado neste documento, o aplicativo Android (operando em um dispositivo Android) é o cliente GATT. O aplicativo recebe dados do servidor GATT, que é um monitor de frequência cardíaca BLE compatível com o Perfil de frequência cardíaca. No entanto, como alternativa, você pode projetar o aplicativo Android para atuar no papel de servidor GATT. Consulte BluetoothGattServer para ver mais informações.

Permissões BLE

Para usar recursos do Bluetooth no aplicativo, é necessário declarar a permissão Bluetooth BLUETOOTH. Você precisa dessa permissão para executar todas as comunicações do Bluetooth, como solicitar ou aceitar uma conexão e transferir dados.

Além disso, como os sensores LE geralmente estão associados com o local, é necessário declarar a permissão ACCESS_FINE_LOCATION. Sem ela, as verificações não retornarão nenhum resultado.

Observação: caso seu aplicativo seja direcionado ao Android 9 (API de nível 28) ou posteriores, você pode declarar a permissão ACCESS_COARSE_LOCATION em vez da permissão ACCESS_FINE_LOCATION.

Se você quiser que o aplicativo inicie a descoberta de dispositivos ou manipule configurações do Bluetooth, será necessário declarar também a permissão .BLUETOOTH_ADMIN Observação: se você usar a permissão BLUETOOTH_ADMIN, deverá também ter a permissão BLUETOOTH.

Declare as permissões no arquivo de manifesto do aplicativo. Por exemplo:

<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" />

Se quiser declarar que o aplicativo está disponível somente para dispositivos compatíveis com BLE, inclua a seguinte informação no manifesto do aplicativo:

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

No entanto, caso queira tornar o aplicativo disponível para dispositivos incompatíveis com BLE, ainda será necessário incluir esse elemento no manifesto do aplicativo, mas defini-lo como required="false". Após isso, no tempo de execução, será possível determinar a disponibilidade do BLE usando 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();
}

Configurar o BLE

Antes que o aplicativo possa se comunicar por meio do BLE, é necessário verificar se o dispositivo permite esse tipo de comunicação e, em caso positivo, verificar se ela está ativada. Essa verificação só será necessária se <uses-feature.../> estiver definido como falso.

Caso não haja suporte ao BLE, desative ordenadamente todos os recursos desse tipo. Se o dispositivo for compatível com BLE, mas esse estiver desativado, será possível solicitar que o usuário ative o Bluetooth sem sair do aplicativo. Essa configuração é efetuada em duas etapas, usando o BluetoothAdapter:

  1. Acesse o BluetoothAdapter

    O BluetoothAdapter é necessário para todas as atividades do Bluetooth. O BluetoothAdapter representa o próprio adaptador Bluetooth do dispositivo (o rádio Bluetooth). Há um adaptador Bluetooth para todo o sistema, e o aplicativo pode usar esse objeto para interagir com ele. O snippet abaixo mostra como acessar o adaptador. Essa abordagem usa getSystemService() para retornar uma instância do BluetoothManager, que é usado em seguida para acessar o adaptador. O Android 4.3 (API de nível 18) introduz o 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. Ative o Bluetooth

    Em seguida, é necessário garantir a ativação do Bluetooth. Chame isEnabled() para verificar se o Bluetooth está ativado. Se método retornar “false”, o Bluetooth está desativado. O snippet a seguir verifica se o Bluetooth está ativado. Caso ele não esteja, o snippet exibirá um erro solicitando ao usuário que acesse as Configurações e ative o 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);
    }
    

    Observação: a constante REQUEST_ENABLE_BT passada a startActivityForResult(android.content.Intent, int) é um inteiro definido localmente (que precisa ser maior que 0) passado pelo sistema para a implementação de onActivityResult(int, int, android.content.Intent) como o parâmetro requestCode.

Encontrar dispositivos BLE

Para encontrar dispositivos BLE, use o método startLeScan(). Esse método usa um BluetoothAdapter.LeScanCallback como parâmetro. Por essa ser a forma de retornar os resultados da verificação, será necessário implementar esse callback. Como a verificação usa intensivamente a bateria, observe as diretrizes a seguir:

  • Assim que encontrar o dispositivo desejado, interrompa a verificação.
  • Não verifique repetidamente e defina um limite de tempo para a busca. Um dispositivo anteriormente disponível pode ter sido movido para um local fora do alcance. Continuar a verificação causa o consumo da bateria.

O snippet a seguir mostra como iniciar e interromper uma verificação:

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

Se quiser verificar somente tipos específicos de periféricos, você pode chamar startLeScan(UUID[], BluetoothAdapter.LeScanCallback), fornecendo uma matriz de objetos UUID que especificam os serviços GATT compatíveis com o aplicativo.

Esta é a implementação da BluetoothAdapter.LeScanCallback, a interface usada para entregar os resultados da verificação de 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();
           }
       });
   }
};

Observação: é possível verificar somente dispositivos de Bluetooth LE ou somente de Bluetooth clássico, conforme descrito em Bluetooth. Não é possível verificar dispositivos de Bluetooth LE e clássico ao mesmo tempo.

Conectar-se ao servidor GATT

A primeira etapa para interagir com um dispositivo BLE é se conectar a ele: mais especificamente ao servidor GATT do dispositivo. Para se conectar ao servidor GATT em um dispositivo BLE, use o método connectGatt(). Esse método usa três parâmetros: um objeto Context, autoConnect (um valor booleano que indica se a conexão com o dispositivo BLE será feita automaticamente quando ele ficar disponível) e uma referência para um BluetoothGattCallback:

Kotlin

var bluetoothGatt: BluetoothGatt? = null
...

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

Java

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

Com isso, é feita a conexão com o servidor GATT hospedado pelo dispositivo BLE e é retornada uma instância BluetoothGatt, que pode ser usada para conduzir as operações do cliente GATT. O autor da chamada (o aplicativo Android) é o cliente GATT. O BluetoothGattCallback é usado para entregar resultados ao cliente, como o status da conexão e outras operações do cliente GATT.

Neste exemplo, o aplicativo BLE fornece uma atividade (DeviceControlActivity) para conectar, exibir dados e mostrar características e serviços GATT compatíveis com o dispositivo. Com base na entrada do usuário, essa atividade se comunica com um Service chamado BluetoothLeService, que interage com o dispositivo BLE por meio da API Android BLE:

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

Quando um callback em particular é acionado, ele chama o método auxiliar broadcastUpdate() adequado e passa uma ação a ele. Os dados em análise nesta seção são realizados de acordo com as especificações de perfil da Medida de frequência cardíaca 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);
}

Na DeviceControlActivity, esses eventos são gerenciados por um 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));
        }
    }
};

Ler atributos BLE

Após o aplicativo Android se conectar a um servidor GATT e descobrir serviços, ele poderá ler e gravar atributos, quando houver compatibilidade. Por exemplo, este snippet itera nos serviços e nas características do servidor e os exibe na 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);
         }
    ...
    }
...
}

Receber notificações GATT

É comum que aplicativos BLE peçam para ser notificados quando uma característica em particular é alterada no dispositivo. Este snippet mostra como definir a notificação para uma característica, usando o 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);

Após as notificações serem ativadas para uma característica, um callback onCharacteristicChanged() será acionado caso as características mudem no 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);
}

Fechar o aplicativo do cliente

Quando o aplicativo terminar de usar um dispositivo BLE, ele chamará close() para que o sistema possa liberar os recursos de forma adequada:

Kotlin

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

Java

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