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

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

    bugs!) • Race conditions (Even more bugs!) • Testing (Complicated.) • Emulators (No bugs. Doesn't exist!)
  4. 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!
  5. 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))
  6. CDM Example val contract = StartIntentSenderForResult() val resultLauncher = registerForActivityResult(contract)

    { val bluetoothDevice = it.data ?.getParcelableExtra<BluetoothDevice>(EXTRA_DEVICE) bluetoothDevice?.connectGatt(...) }
  7. 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(...)
  8. 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) } }
  9. 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() }
  10. Unbounding - Use hidden API fun BluetoothDevice.releaseBond() { val method:

    Method = this.javaClass.getMethod("removeBond") method.invoke(this) }
  11. 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>
  12. 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) } }
  13. 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(...) }
  14. GATT • BluetoothGattService <- Grouping of characteristics • BluetoothGattCharacteristic <-

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

    notifications 4. Read and Write 5. Disconnect & Close
  16. 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!!! )
  17. 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... }
  18. 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()
  19. BluetoothGattCallback override fun onCharacteristicRead( gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int

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

    val event = CharacteristicChanged( characteristic.service.uuid, characteristic.uuid, characteristic.value ) _events.tryEmit(event) }
  21. 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!!! )
  22. 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); } } }); }
  23. 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; }
  24. 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); } }
  25. 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); } } }); }
  26. Dealing with GATT race conditions • Don't use a Handler

    when connecting • Don't mix characteristics for read and write • Notification sequence numbers
  27. Common mistake fun readAndWrite( gatt: BluetoothGatt, forReading: BluetoothGattCharacteristic, forWriting: BluetoothGattCharacteristic,

    data: ByteArray) { forWriting.value = data gatt.writeCharacteristic(forWriting) gatt.readCharacteristic(forReading) }
  28. 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 } }
  29. 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) } }
  30. 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) }
  31. PHY

  32. 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;
  33. 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)
  34. 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)
  35. Common pitfalls • Not calling BluetoothGatt.discoverServices() • Not queueing operations

    • Retaining BluetoothGatt* objects • Setting autoConnect to false • BluetoothGattCallback instance reused • Not doing disconnect() AND close()
  36. 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