Upgrade to Pro — share decks privately, control downloads, hide ads and more …

State of BLE on Android in 2022

State of BLE on Android in 2022

BLE (Bluetooth Low Energy) has been available on Android since API level 18. This was 9 years ago and a lot has happened since then. The platform APIs have become more stable, new features are supported, and the OS is providing better support for working with BLE while the app is in the background.

In this talk, we will go through the state of BLE on Android in 2022, and also look at what is coming up soon. This session will be valuable for anyone working on BLE or planning to.

Erik Hellman

October 28, 2022
Tweet

More Decks by Erik Hellman

Other Decks in Programming

Transcript

  1. State of BLE on
    Android in 2022
    @ErikHellman

    View Slide

  2. Bluetooth Classic
    &
    Bluetooth Smart

    View Slide

  3. Bluetooth Classic
    &
    Bluetooth Smart

    View Slide

  4. Bluetooth Smart

    View Slide

  5. Bluetooth Low Energy
    Bluetooth Smart1
    1 Marketing name nobody uses...

    View Slide

  6. History of BLE APIs on Android
    • Support for BLE in API level 18 (July 24, 2013)
    • New scanner API in API level 21
    • BLE 5.0 and Companion Device Manager in API level 26
    • Companion Device Service in API level 31
    • API changes for GATT in API level 33

    View Slide

  7. Anatomy of BLE
    • Scanning & Advertising
    • Discovery
    • Pairing & Bonding
    • Connecting & Comunicating

    View Slide

  8. BLE Challenges on Android
    • Bugs!
    • System behavior (More bugs!)
    • Race conditions (Even more bugs!)
    • Testing (Complicated.)
    • Emulators (No bugs. Doesn't exist!)

    View Slide

  9. minSDK 26
    Avoid supporting anything below
    API level 26 (Android 8.0) for
    reliable BLE support!

    View Slide

  10. Android BLE myths
    • Must run on the main thread - false
    • Must run on a background thread - false
    • BluetoothGattCallback callbacks run on a background thread - false2
    • BluetoothGattCharacteristic is thread safe - false
    • Operations are queued safely - false
    • I'm safe if use I RxBLE/RxAndroidBle/Kable - false
    2 Possible to run on dedicated HandlerThread from API level 26 - DO NOT USE!

    View Slide

  11. The Official
    Documentation
    Is not good

    View Slide

  12. View Slide

  13. Best practices with
    BLE in 2022

    View Slide

  14. Use CompanionDeviceManager

    View Slide

  15. CDM Example
    val regexp = Pattern.compile("^My Watch [a-z]{2}[0-9]{2}$")
    val filter = BluetoothLeDeviceFilter.Builder().setNamePattern(regexp).build()
    val request = AssociationRequest.Builder()
    .addDeviceFilter(filter)
    .build()
    val callback = object : CompanionDeviceManager.Callback() {
    override fun onDeviceFound(sender: IntentSender) {
    val senderRequest = IntentSenderRequest.Builder(sender).build()
    resultLauncher.launch(senderRequest)
    }
    override fun onFailure(error: CharSequence?) {
    // TODO Handle error
    }
    }
    val cdm = getSystemService(CompanionDeviceManager::class.java)
    cdm.associate(request, callback, Handler(mainLooper))

    View Slide

  16. CDM Example
    val contract = StartIntentSenderForResult()
    val resultLauncher = registerForActivityResult(contract) {
    val bluetoothDevice = it.data
    ?.getParcelableExtra(EXTRA_DEVICE)
    bluetoothDevice?.connectGatt(...)
    }

    View Slide

  17. CDM Example

    View Slide

  18. CDM Bonus Permissions!
    android:name="android.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND" />
    android:name="android.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND" />

    View Slide

  19. CDM Example - After CDM pairing
    val cdm = getSystemService(CompanionDeviceManager::class.java)
    val deviceAddress = cdm.associations.firstOrNull()
    ?: throw IllegalStateException()
    val bluetoothManager = getSystemService(BluetoothManager::class.java)
    val bluetoothAdapter = bluetoothManager.adapter
    val bluetoothDevice = bluetoothAdapter.getRemoteDevice(deviceAddress)
    bluetoothDevice?.connectGatt(...)

    View Slide

  20. Pairing vs Bonding

    View Slide

  21. Bonding
    data class BondState(val device: BluetoothDevice, val state: Int)
    val bondStateFlow = callbackFlow {
    val receiver = object : BroadcastReceiver() {
    override fun onReceive(context: Context, data: Intent) {
    trySendBlocking(BondState(
    intent.getParcelableExtra(EXTRA_DEVICE)!!,
    intent.getIntExtra(EXTRA_BOND_STATE, BOND_NONE)
    ))
    }
    }
    val filter = IntentFilter(ACTION_BOND_STATE_CHANGED)
    registerReceiver(receiver, filter)
    trySendBlocking(BondState(
    bluetoothDevice,
    bluetoothDevice.getBondState()
    ))
    awaitClose { unregisterReceiver(receiver) }
    }

    View Slide

  22. Bonding
    suspend fun bondWithDevice(bondStateFlow: Flow): Boolean {
    bondStateFlow.collect {
    when (it.state) {
    BOND_BONDED -> // Device bonding done
    BOND_BONDING -> // Device bonding in progress
    BOND_NONE -> // Device not bonded
    }
    }
    bluetoothDevice.createBond()
    }

    View Slide

  23. Unbounding - Use hidden API
    fun BluetoothDevice.releaseBond() {
    val method: Method = this.javaClass.getMethod("removeBond")
    method.invoke(this)
    }

    View Slide

  24. Companion Device Service
    From API Level 31
    android:name=".MyCompanionService"
    android:label="@string/service_name"
    android:exported="true"
    android:permission="android.permission.BIND_COMPANION_DEVICE_SERVICE">




    View Slide

  25. Companion Device Service
    @AndroidEntryPoint
    class MyCompanionService : CompanionDeviceService() {
    private val adapter = getSystemService(BluetoothManager::class.java).adapter
    @Inject lateinit var gattDeviceRepo: GattDeviceRepo
    override fun onDeviceAppeared(address: String) {
    val device = adapter.getRemoteDevice(address)
    gattDeviceRepo.deviceNearby(device)
    }
    override fun onDeviceDisappeared(address: String) {
    val device = adapter.getRemoteDevice(address)
    gattDeviceRepo.deviceGone(device)
    }
    }

    View Slide

  26. Companion Device Service
    val contract = StartIntentSenderForResult()
    val resultLauncher = registerForActivityResult(contract) {
    val bluetoothDevice = it.data
    ?.getParcelableExtra(EXTRA_DEVICE)
    val cdm = getSystemService(CompanionDeviceManager::class.java)
    cdm.startObservingDevicePresence(bluetoothDevice.address)
    bluetoothDevice?.connectGatt(...)
    }

    View Slide

  27. Companion Device
    Manager
    Always use the CDM for discovery and pairing!

    View Slide

  28. Companion Device
    Service
    (API level 31+)
    Use the CDS when available!

    View Slide

  29. Generic ATTribute
    profile

    View Slide

  30. GATT
    • BluetoothGattService <- Grouping of characteristics
    • BluetoothGattCharacteristic <- Data "endpoint"
    • BluetoothGattDescriptor <- Configuration
    • BluetoothGattDescriptor
    • BluetoothGattCharacteristic
    • BluetoothGattDescriptor
    • BluetoothGattDescriptor

    View Slide

  31. GATT Operations
    1. Connect
    2. Discover services
    3. Register for notifications
    4. Read and Write
    5. Disconnect & Close

    View Slide

  32. Android GATT Architecture

    View Slide

  33. Connecting
    val bluetoothGatt = bluetoothDevice.connectGatt(
    context, // Use Application, not Activity
    autoConnect, // Connect when device is available? Always use true!
    callback, // BluetoothGattCallback instance. DO NOT REUSE!
    null // Keep the callbacks on the binder thread! IMPORTANT!!!
    )

    View Slide

  34. BluetoothGattCallback
    class GattCallbackWrapper : BluetoothGattCallback() {
    private val _events = MutableSharedFlow()
    val events: SharedFlow = _events
    override fun onCharacteristicRead(
    gatt: BluetoothGatt?,
    characteristic: BluetoothGattCharacteristic?, status: Int) {
    // TODO
    }
    override fun onCharacteristicChanged(
    gatt: BluetoothGatt?,
    characteristic: BluetoothGattCharacteristic?) {
    // TODO
    }
    // More callbacks here...
    }

    View Slide

  35. BluetoothGattCallback
    sealed class GattEvent
    data class CharacteristicRead(
    val service: UUID,
    val characteristic: UUID,
    val data: ByteArray,
    val status: Int,
    ) : GattEvent()
    data class CharacteristicChanged(
    val service: UUID,
    val characteristic: UUID,
    val data: ByteArray
    ) : GattEvent()

    View Slide

  36. BluetoothGattCallback
    override fun onCharacteristicRead(
    gatt: BluetoothGatt,
    characteristic: BluetoothGattCharacteristic,
    status: Int
    ) {
    val event = CharacteristicRead(
    characteristic.service.uuid,
    characteristic.uuid,
    characteristic.value,
    status
    )
    _events.tryEmit(event)
    }

    View Slide

  37. BluetoothGattCallback
    override fun onCharacteristicChanged(
    gatt: BluetoothGatt,
    characteristic: BluetoothGattCharacteristic
    ) {
    val event = CharacteristicChanged(
    characteristic.service.uuid,
    characteristic.uuid,
    characteristic.value
    )
    _events.tryEmit(event)
    }

    View Slide

  38. Deprecated callbacks

    View Slide

  39. Deprecated callbacks

    View Slide

  40. Deprecated callbacks
    val bluetoothGatt = bluetoothDevice.connectGatt(
    context, // Use Application, not Activity
    autoConnect, // Connect when device is available? Always use true!
    callback, // BluetoothGattCallback instance. DO NOT REUSE!
    null // Keep the callbacks on the binder thread! IMPORTANT!!!
    )

    View Slide

  41. BluetoothGatt.java
    AOSP, API level 26
    public void onNotify(String address, int handle, byte[] value) {
    BluetoothGattCharacteristic characteristic = getCharacteristicById(mDevice, handle);
    characteristic.setValue(value);
    runOrQueueCallback(new Runnable() {
    @Override
    public void run() {
    if (mCallback != null) {
    mCallback.onCharacteristicChanged(
    BluetoothGatt.this,
    characteristic);
    }
    }
    });
    }

    View Slide

  42. BluetoothGatt.java
    AOSP, API level 26
    BluetoothGattCharacteristic getCharacteristicById(
    BluetoothDevice device,
    int instanceId) {
    for(BluetoothGattService svc : mServices) {
    for(BluetoothGattCharacteristic charac : svc.getCharacteristics()) {
    if (charac.getInstanceId() == instanceId)
    return charac;
    }
    }
    return null;
    }

    View Slide

  43. BluetoothGatt.java
    AOSP, API level 26
    private void runOrQueueCallback(final Runnable cb) {
    if (mHandler == null) {
    try {
    cb.run();
    } catch (Exception ex) {
    Log.w(TAG, "Unhandled exception in callback", ex);
    }
    } else {
    mHandler.post(cb);
    }
    }

    View Slide

  44. BluetoothGatt.java
    AOSP, API level 33
    public void onNotify(String address, int handle, byte[] value) {
    BluetoothGattCharacteristic characteristic
    = getCharacteristicById(mDevice, handle);
    if (characteristic == null) return;
    runOrQueueCallback(new Runnable() {
    @Override
    public void run() {
    final BluetoothGattCallback callback = mCallback;
    if (callback != null) {
    characteristic.setValue(value);
    callback.onCharacteristicChanged(BluetoothGatt.this,
    characteristic, value);
    }
    }
    });
    }

    View Slide

  45. Dealing with GATT race
    conditions
    • Don't use a Handler when connecting
    • Don't mix characteristics for read and write
    • Notification sequence numbers

    View Slide

  46. Queue GATT
    operations

    View Slide

  47. Common mistake
    fun connectAndDiscoverServices(device: BluetoothDevice) {
    val gatt = device.connectGatt(...)
    gatt.discoverServices()
    }

    View Slide

  48. Common mistake
    fun readAndWrite(
    gatt: BluetoothGatt,
    forReading: BluetoothGattCharacteristic,
    forWriting: BluetoothGattCharacteristic,
    data: ByteArray) {
    forWriting.value = data
    gatt.writeCharacteristic(forWriting)
    gatt.readCharacteristic(forReading)
    }

    View Slide

  49. Queue GATT operations
    private suspend fun Mutex.queueWithTimeout(
    timeout: Long = DEFAULT_GATT_TIMEOUT,
    block: suspend CoroutineScope.() -> T
    ): T {
    return try {
    withLock { withTimeout(timeMillis = timeout, block = block) }
    } catch (e: Exception) {
    throw e
    }
    }

    View Slide

  50. Queue GATT operations
    suspend fun writeCharacteristic(
    char: BluetoothGattCharacteristic,
    value: ByteArray): CharacteristicWritten {
    mutex.queueWithTimeout {
    events // events Flow from the BluetoothGattCallback
    .onSubscription {
    bluetoothGatt.writeCharacteristic(char, value)
    }
    .firstOrNull {
    it is CharacteristicWritten && it.characteristic.uuid == char.uuid
    } as CharacteristicWritten?
    ?: CharacteristicWritten(char, BluetoothGatt.GATT_FAILURE)
    }
    }

    View Slide

  51. Notifications

    View Slide

  52. Notifications
    // Tell system server to send our app notifications received
    bluetoothGatt.setCharacteristicNotification(characteristic, true)
    characteristic.descriptors
    .find {
    it.uuid == ClientCharacteristicConfiguration
    }
    ?.let { descriptor ->
    descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
    bluetoothGatt.writeDescriptor(descriptor)
    }

    View Slide

  53. ClientCharacteristicConfigurationID
    private val ClientCharacteristicConfiguration =
    UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")

    View Slide

  54. PHY

    View Slide

  55. PHY Options - BluetoothDevice
    public static final int PHY_LE_1M_MASK = 1;
    public static final int PHY_LE_2M_MASK = 2;
    public static final int PHY_LE_CODED_MASK = 4;
    // PHY Options
    public static final int PHY_OPTION_NO_PREFERRED = 0;
    public static final int PHY_OPTION_S2 = 1;
    public static final int PHY_OPTION_S8 = 2;

    View Slide

  56. Setting PHY for long-range, low
    bandwidth
    val phy = BluetoothDevice.PHY_LE_CODED or BluetoothDevice.PHY_LE_1M
    val options = BluetoothDevice.PHY_OPTION_S8
    bluetoothGatt.setPreferredPhy(phy, phy, options)

    View Slide

  57. Setting PHY for 2 MBit/s, short-
    ranged
    val phy = BluetoothDevice.PHY_LE_2M or BluetoothDevice.PHY_LE_1M
    val options = BluetoothDevice.PHY_OPTION_NO_PREFERRED
    bluetoothGatt.setPreferredPhy(phy, phy, options)

    View Slide

  58. Common pitfalls
    • Not calling BluetoothGatt.discoverServices()
    • Not queueing operations
    • Retaining BluetoothGatt* objects
    • Setting autoConnect to false
    • BluetoothGattCallback instance reused
    • Not doing disconnect() AND close()

    View Slide

  59. Summary
    • minSDK 26
    • Use the CDM and CDS
    • Race condition fixed in API level 33
    • Queue all GATT operations
    • Client Characteristic Configuration ID
    • PHY Options

    View Slide

  60. Thank You
    Slides at https://speakerdeck.com/erikhellman
    @ErikHellman

    View Slide