$30 off During Our Annual Pro Sale. View Details »

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

  2. Bluetooth Classic & Bluetooth Smart

  3. Bluetooth Classic & Bluetooth Smart

  4. Bluetooth Smart

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

  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
  7. Anatomy of BLE • Scanning & Advertising • Discovery •

    Pairing & Bonding • Connecting & Comunicating
  8. BLE Challenges on Android • Bugs! • System behavior (More

    bugs!) • Race conditions (Even more bugs!) • Testing (Complicated.) • Emulators (No bugs. Doesn't exist!)
  9. minSDK 26 Avoid supporting anything below API level 26 (Android

    8.0) for reliable BLE support!
  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!
  11. The Official Documentation Is not good

  12. None
  13. Best practices with BLE in 2022

  14. Use CompanionDeviceManager

  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))
  16. CDM Example val contract = StartIntentSenderForResult() val resultLauncher = registerForActivityResult(contract)

    { val bluetoothDevice = it.data ?.getParcelableExtra<BluetoothDevice>(EXTRA_DEVICE) bluetoothDevice?.connectGatt(...) }
  17. CDM Example

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

  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(...)
  20. Pairing vs Bonding

  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) } }
  22. Bonding suspend fun bondWithDevice(bondStateFlow: Flow<BondState>): Boolean { bondStateFlow.collect { when

    (it.state) { BOND_BONDED -> // Device bonding done BOND_BONDING -> // Device bonding in progress BOND_NONE -> // Device not bonded } } bluetoothDevice.createBond() }
  23. Unbounding - Use hidden API fun BluetoothDevice.releaseBond() { val method:

    Method = this.javaClass.getMethod("removeBond") method.invoke(this) }
  24. Companion Device Service From API Level 31 <service android:name=".MyCompanionService" android:label="@string/service_name"

    android:exported="true" android:permission="android.permission.BIND_COMPANION_DEVICE_SERVICE"> <intent-filter> <action android:name="android.companion.CompanionDeviceService" /> </intent-filter> </service>
  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) } }
  26. Companion Device Service val contract = StartIntentSenderForResult() val resultLauncher =

    registerForActivityResult(contract) { val bluetoothDevice = it.data ?.getParcelableExtra<BluetoothDevice>(EXTRA_DEVICE) val cdm = getSystemService(CompanionDeviceManager::class.java) cdm.startObservingDevicePresence(bluetoothDevice.address) bluetoothDevice?.connectGatt(...) }
  27. Companion Device Manager Always use the CDM for discovery and

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

    available!
  29. Generic ATTribute profile

  30. GATT • BluetoothGattService <- Grouping of characteristics • BluetoothGattCharacteristic <-

    Data "endpoint" • BluetoothGattDescriptor <- Configuration • BluetoothGattDescriptor • BluetoothGattCharacteristic • BluetoothGattDescriptor • BluetoothGattDescriptor
  31. GATT Operations 1. Connect 2. Discover services 3. Register for

    notifications 4. Read and Write 5. Disconnect & Close
  32. Android GATT Architecture

  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!!! )
  34. BluetoothGattCallback class GattCallbackWrapper : BluetoothGattCallback() { private val _events =

    MutableSharedFlow<GattEvent>() val events: SharedFlow<GattEvent> = _events override fun onCharacteristicRead( gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int) { // TODO } override fun onCharacteristicChanged( gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?) { // TODO } // More callbacks here... }
  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()
  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) }
  37. BluetoothGattCallback override fun onCharacteristicChanged( gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic ) {

    val event = CharacteristicChanged( characteristic.service.uuid, characteristic.uuid, characteristic.value ) _events.tryEmit(event) }
  38. Deprecated callbacks

  39. Deprecated callbacks

  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!!! )
  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); } } }); }
  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; }
  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); } }
  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); } } }); }
  45. Dealing with GATT race conditions • Don't use a Handler

    when connecting • Don't mix characteristics for read and write • Notification sequence numbers
  46. Queue GATT operations

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

    gatt.discoverServices() }
  48. Common mistake fun readAndWrite( gatt: BluetoothGatt, forReading: BluetoothGattCharacteristic, forWriting: BluetoothGattCharacteristic,

    data: ByteArray) { forWriting.value = data gatt.writeCharacteristic(forWriting) gatt.readCharacteristic(forReading) }
  49. Queue GATT operations private suspend fun <T> 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 } }
  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) } }
  51. Notifications

  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) }
  53. ClientCharacteristicConfigurationID private val ClientCharacteristicConfiguration = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")

  54. PHY

  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;
  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)
  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)
  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()
  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
  60. Thank You Slides at https://speakerdeck.com/erikhellman @ErikHellman