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

Bluetooth Low Energy on Android: Top Tips For The Tricky Bits (Video)

3a6060bc7ace07fa75791cd5dac2d46a?s=47 Stuart Kent
September 13, 2017

Bluetooth Low Energy on Android: Top Tips For The Tricky Bits (Video)

Bluetooth Low Energy (BLE) powers the Internet of Things (IoT): smart watches, smart bulbs, and smart cars all use it for short-range communication. Now that 90% of Android consumer devices and 100% of Android Things devices run software that supports BLE, there’s never been a better time for Android developers to jump into the rapidly-growing IoT ecosystem and start building their own companion apps or custom smart devices.

Unfortunately, Android’s Bluetooth stack has a well-deserved reputation for being difficult to work with. The documentation is patchy, the API abstractions are leaky, and the stack itself is unreliable. I worked through all these challenges while building a pro audio app at the start of 2017, and now I'm sharing my story to save you time.

The beginning of this talk will cover BLE basics. The remainder will showcase code samples and strategies for tackling the quirks of the Android Bluetooth stack. No prior experience with BLE is required to enjoy this talk. You’ll leave excited to tinker with BLE on Android and equipped with a roadmap and toolkit to help you navigate the nasty parts.

Favorite Resources: https://gist.github.com/stkent/a7f0d6b868e805da326b112d60a9f59b
Video: https://youtu.be/jDykHjn-4Ng
Venue: GDG Detroit

3a6060bc7ace07fa75791cd5dac2d46a?s=128

Stuart Kent

September 13, 2017
Tweet

Transcript

  1. None
  2. Stuart Kent Detroit Labs @skentphd @skentphd

  3. Why This Talk • ! Smart speaker app • "

    Fun technology • # Frustrating stack • $ Scattered information @skentphd
  4. Content • ✅ BLE Basics • ✅ Android Landscape •

    ✅ Android Landmarks • ⏰ More Tricky Bits @skentphd
  5. Content @skentphd

  6. ! BLE Basics @skentphd

  7. BLE Features • Wireless personal area network • 100m range

    • Small/low power/long lifespan devices • Low-dimensional data • Low-friction communication @skentphd
  8. BLE Device Roles • Peripheral • Central @skentphd

  9. Peripheral • Usually acts as a server: • Characteristic, composed

    of: • a single value (int, float, string) • many descriptors = metadata • Service = group of related characteristics • Profile = all services • Identified using UUIDs @skentphd
  10. Peripheral @skentphd

  11. Heart Rate Monitor (Standard Profile) Profile: "Heart Rate" Services: "Heart

    Rate" Characteristics: • "Body Sensor Location" (read) • "Current Heart Rate" (sub) • "Settings" (write) @skentphd
  12. Smart Speaker (Custom Profile) Profile: Custom Services: Custom Characteristics: •

    Volume (read/write/sub) • Bass/Mid/Treble (read/write/sub) • Clipping (read/sub) • LED (write) @skentphd
  13. Central • Usually acts as a client • Can connect

    to up to 7 BLE servers simultaneously • Reads/writes/subscribes to characteristics • Typically a mobile device! @skentphd
  14. BLE Connections @skentphd

  15. ! Android Landscape @skentphd

  16. Challenges • ! Undocumented/unclear limitations • " Location required for

    scanning • # Connection instability • $ Frequent, obscure errors (GATT_ERROR 133) • ⚖ Unbalanced lifecycle calls @skentphd
  17. Abstractions • SweetBlue: proprietary • RxAndroidBle: not designed for persistent

    connection • AndroidBluetoothLibrary: unexplored • Nearby APIs: specialized feature set • All: necessarily opinionated; build from scratch @skentphd
  18. ! Android Landmarks @skentphd

  19. Assumptions • Android 21+ • BLE only; no Bluetooth Classic

    fallbacks • Central role @skentphd
  20. Steps 1. Planning 2. BLE Support 3. BLE State 4.

    Scanning For Devices 5. Connecting 6. Operation Queuing 7. Discovering Services 8. Performing Operations 9. Disconnecting @skentphd
  21. 1. Planning @skentphd

  22. 1. Planning • Max. 7 concurrent connections • Max. 15

    characteristic subscriptions per peripheral @skentphd
  23. 2. BLE Support @skentphd

  24. 2. BLE Support AndroidManifest.xml: <uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

    @skentphd
  25. 2. BLE Support AndroidManifest.xml: <!-- For Play Store feature-based filtering

    --> <!-- required="true" => mandatory --> <!-- required="false" => preferred --> <uses-feature android:name="android.hardware.bluetooth_le" android:required="true" /> @skentphd
  26. 2. BLE Support Runtime checks are also valuable: • emulator

    • demo mode • sideloads @skentphd
  27. 2. BLE Support Correct boolean isBleSupported(Context context) { return BluetoothAdapter.getDefaultAdapter()

    != null && context.getPackageManager().hasSystemFeature(FEATURE_BLUETOOTH_LE); } Incorrect boolean isBleSupported() { return BluetoothAdapter.getDefaultAdapter() != null && BluetoothAdapter.getDefaultAdapter().getBluetoothLeScanner() != null; } @skentphd
  28. 2. BLE Support getDefaultAdapter() non-null if hardware supports Bluetooth; null

    otherwise (static). getBluetoothLeScanner() non-null if Bluetooth is currently on; null otherwise (dynamic). @skentphd
  29. 3. BLE State @skentphd

  30. 3. BLE State Query at any time: BluetoothAdapter.getDefaultAdapter().isLeEnabled(); @skentphd

  31. 3. BLE State Register for broadcasts: context.registerReceiver( receiver, new IntentFilter(

    BluetoothAdapter.ACTION_STATE_CHANGED)); @skentphd
  32. 3. BLE State • Treat STATE_ON or STATE_BLE_ON as on

    • Treat every other state as off • Check state aggressively • Consider a blocking Activity • Prompt using BluetoothAdapter.ACTION_REQUEST_ENABLE @skentphd
  33. 4. Scanning For Devices @skentphd

  34. 4. Scanning For Devices // After checking that device Bluetooth

    is on... bleScanner.startScan( filters, // Describe relevant peripherals settings, // Specify power profile scanCallback); // Process scan results @skentphd
  35. 4. Scanning For Devices ScanCallback scanCallback = new ScanCallback() {

    @Override public void onScanResult(int callbackType, ScanResult result) { // Process scan result here. } }; @skentphd
  36. 4. Scanning For Devices No results! Quiet failure! @skentphd

  37. 4. Scanning For Devices 09-07 11:20:47.500 W: Caught a RuntimeException

    from the binder stub implementation. java.lang.SecurityException: Need ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION permission to get scan results @skentphd
  38. 4. Scanning For Devices AndroidManifest.xml: <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <!-- For

    Play Store feature-based filtering --> <!-- required="true" => mandatory --> <!-- required="false" => preferred --> <uses-feature android:name="android.hardware.location" android:required="true" /> @skentphd
  39. 4. Scanning For Devices ActivityCompat.requestPermissions( this, new String[]{ACCESS_COARSE_LOCATION}, PERMISSIONS_REQUEST_ID); @skentphd

  40. 4. Scanning For Devices Location permission request will confuse users.

    • Call out in Play Store description • Ignore shouldShowRequestPermissionRationale • Always present custom explanation dialog • Consider a blocking Activity @skentphd
  41. 4. Scanning For Devices No results! Silent failure! @skentphd

  42. 4. Scanning For Devices • Location services must be on

    to receive scan results • Consider a blocking Activity • Query using Settings.Secure.getInt( context.getContentResolver(), LOCATION_MODE) • Prompt to enable using Settings.ACTION_LOCATION_SOURCE_SETTINGS or LocationSettingsRequest (Play Services) @skentphd
  43. 4. Scanning For Devices Results! @skentphd

  44. 4. Scanning For Devices Scanning must be stopped manually: •

    First result • Fixed duration • App backgrounding @skentphd
  45. 4. Scanning For Devices ScanCallback scanCallback = new ScanCallback() {

    @Override public void onScanResult(int callbackType, ScanResult result) { // Check bleScanner is not null, then: bleScanner.stopScan(scanCallback); // Connect to result.getDevice(). } }; @skentphd
  46. 4. Scanning For Devices // Clear between each scan. SortedSet<BluetoothDevice>

    scannedDevices = new TreeSet<>(); ScanCallback scanCallback = new ScanCallback() { @Override public void onScanResult(int callbackType, ScanResult result) { scannedDevices.add(result.getDevice()); } }; @skentphd
  47. 4. Scanning For Devices // Called after bluetoothLeScanner.startScan: new Handler(Looper.getMainLooper()).postDelayed(new

    Runnable() { @Override public void run() { // Check bleScanner is not null, then: bleScanner.stopScan(scanCallback); // Process contents of scannedDevices (display; connect). } }, scanDurationMs); @skentphd
  48. 5. Connecting @skentphd

  49. 5. Connecting // Retain; used for all future device commands.

    BluetoothGatt server = device.connectGatt( context, false, // autoConnect; safest set to false? serverCallback); @skentphd
  50. 5. Connecting // Receives command responses and information from device

    server. BluetoothGattCallback serverCallback = new BluetoothGattCallback() { @Override public void onConnectionStateChange(/* Params */) {} @Override public void onServicesDiscovered(/* Params */) {} @Override public void onCharacteristicRead(/* Params */) {} @Override public void onCharacteristicWrite(/* Params */) {} @Override public void onCharacteristicChanged(/* Params */) {} @Override public void onDescriptorWrite(/* Params */) {} // Other, more esoteric, methods. } @skentphd
  51. 5. Connecting @Override public void onConnectionStateChange( BluetoothGatt server, int status,

    int newState) { if (BluetoothProfile.STATE_CONNECTED == newState) { // Proceed to service discovery. } } @skentphd
  52. 6. Operation Queuing @skentphd

  53. 6. Operation Queuing • All other operations must happen serially

    • Confusing due to asynchronous APIs @skentphd
  54. 6. Operation Queuing (Single Server) Incorrect server.operationA(); // Overwritten; never

    executes. server.operationB(); Correct server.operationA(); // Wait for operationA to complete... server.operationB(); @skentphd
  55. 6. Operation Queuing • Must be serial across all connected

    devices @skentphd
  56. 6. Operation Queuing (Multi Server) Incorrect server1.operationA(); // Overwritten; never

    executes. server2.operationB(); Correct server1.operationA(); // Wait for server1 operationA to complete... server2.operationB(); @skentphd
  57. 6. Operation Queuing Incorrect @skentphd

  58. 6. Operation Queuing • Submit Operation instances to a central

    OperationManager • Populate and process Queue<Operation> one-by-one: • Manager pops operation • Manager dispatches operation to server • Server notifies manager on completion • Repeat until empty @skentphd
  59. 6. Operation Queuing @skentphd

  60. 6. Operation Queuing public abstract class Operation { public Operation(String

    serverId) { this.serverId = serverId; } } @skentphd
  61. 6. Operation Queuing public class OperationManager { private Queue<Operation> operations

    = new LinkedList<>(); private Operation currentOp; public synchronized void request(Operation operation) { operations.add(operation); if (currentOp == null) { currentOp = operations.poll(); getServer(currentOp.getServerId()).perform(currentOp); } } public synchronized void operationCompleted() { currentOp = null; if (operations.peek() != null) { currentOp = operations.poll(); getServer(currentOp.getServerId()).perform(currentOp); } } } @skentphd
  62. 7. Discovering Services @skentphd

  63. 7. Discovering Services • Necessary even if you know server

    profile • Must occur before any server characteristic operations @skentphd
  64. 7. Discovering Services public class DiscoverOperation extends Operation { public

    DiscoverOperation(String serverId) { super(serverId); } } @skentphd
  65. 7. Discovering Services // Operation created and enqueued: operationManager.request(new DiscoverOperation(/*

    Params */)); // (Later...) Operation dequeued and executed: server.discoverServices(); // (Later...) Operation completes: @Override public void onServicesDiscovered(/* Params */) { service = server.getService(/* UUID */); // Save reference. operationManager.operationCompleted(); } @skentphd
  66. 8. Performing Operations @skentphd

  67. 8: Reads public class ReadOperation extends Operation { public ReadOperation(

    String serverId, String characteristicId) { super(serverId); this.characteristicId = characteristicId; } } @skentphd
  68. 8. Reads // Operation created and enqueued: operationManager.request(new ReadOperation(/* Params

    */)); // (Later...) Operation dequeued and executed: server.readCharacteristic(service.getCharacteristic(characteristicId)); // (Later...) Operation completes: @Override public void onCharacteristicRead(/* Params */) { operationManager.operationCompleted(); // Process new value here. } @skentphd
  69. 8. Writes public class WriteIntOperation extends Operation { public WriteIntOperation(

    String serverId, String characteristicId, int value) { // Type should match profile! super(serverId); this.characteristicId = characteristicId; this.value = value; } } @skentphd
  70. 8. Writes // Operation created and enqueued: operationManager.request(new WriteIntOperation(/* Params

    */)); // (Later...) Operation dequeued and executed: BluetoothGattCharacteristic characteristic = service.getCharacteristic(characteristicId); characteristic.setValue(value, FORMAT_UINT8, 0); server.writeCharacteristic(characteristic); // (Later...) Operation completes: @Override public void onCharacteristicWrite(/* Params */) { operationManager.operationCompleted(); } @skentphd
  71. 8. Notifications • A little more complex • Requires remote

    and local configuration • Controlled by per-characteristic descriptor @skentphd
  72. 8. Notifications public class EnableNotificationsOperation extends Operation { public EnableNotificationsOperation(

    String serverId, String characteristicId) { super(serverId); this.characteristicId = characteristicId; } } @skentphd
  73. 8. Notifications // Enable notifications locally after service discovery (REQUIRED):

    server.setCharacteristicNotification(characteristic, true); // Operation created and enqueued: operationManager.request(new EnableNotificationsOperation(/* Params */)); // (Later...) Operation dequeued and executed: characteristic = gattService.getCharacteristic(characteristicId); descriptor = characteristic.getDescriptor("00002902-0000-1000-8000-00805f9b34fb"); // Notifications enabled descriptor UUID. descriptor.setValue({0x01, 0x00}); // Byte array representing enabled state. server.writeDescriptor(descriptor); // (Later...) Operation completes: @Override public void onDescriptorWrite(/* Params */) { operationManager.operationCompleted(); } // (Later...) Characteristic changes: @Override public void onCharacteristicChanged(/* Params */) { // Process new value here. } @skentphd
  74. 8. Other Operations • Similar structures/processes @skentphd

  75. 9. Disconnecting @skentphd

  76. 9. Disconnecting • Call server.disconnect • Call server.close once onConnectionStateChange

    is called with state STATE_DISCONNECTED • This might not happen (disconnect while connecting) • Post delayed Runnable to call server.close() • Cancel Runnable if onConnectionStateChange is called @skentphd
  77. ! More Tricky Bits @skentphd

  78. Operation Errors Problem • Most operations will fail occasionally and

    obscurely Solutions • Assume the worst; code defensively • Tear down and retry connection if status != GATT_SUCCESS • Timebox retries @skentphd
  79. Callback Exceptions Problem • BluetoothGattCallback automatically swallows exceptions • Failing

    to report completion stalls the operation queue Solution • Catch and handle all exceptions yourself • Report operation completion in finally block @skentphd
  80. Callback Exceptions @Override public void onCharacteristicRead(/* Params */) { try

    { // Happy path. } catch (final Exception exception) { // Sad path. } finally { // Always report operation completion! operationManager.operationCompleted(); } } @skentphd
  81. Unexpected Disconnections Problem • Will happen (a lot) Solutions •

    Tear down (close) and start over • Track boolean userDisconnect; auto-retry only if false • Timebox retries @skentphd
  82. Service Caching Problem • First service discovery takes 2-3 seconds

    • Results are cached by the framework • Evolving peripherals lead to a stale cache Solution • Manually clear cache @skentphd
  83. Service Caching private void refreshDeviceCache(BluetoothGatt bluetoothGatt) { try { Method

    hiddenClearCacheMethod = bluetoothGatt.getClass().getMethod("refresh"); if (hiddenClearCacheMethod != null) { Boolean succeeded = (Boolean) hiddenClearCacheMethod.invoke(bluetoothGatt); if (succeeded == null) { Log.e("Ambiguous cache-clearing result. Cache may or may not have been cleared."); } else if (succeeded) { Log.d("Cache was successfully cleared."); } else { Log.e("Hidden cache-clearing method was called but failed. Cache was not cleared."); } } else { Log.e("Could not locate hidden cache-clearing method. Cache was not cleared."); } } catch (Exception ignored) { Log.e("An exception occurred while clearing cache. Cache was not cleared."); } } @skentphd
  84. Service Caching Call refreshDeviceCache after connection succeeds and before discovering

    services. @skentphd
  85. Avoiding Buffering Problem • Operations take ~100-300ms (round trip) •

    Some UI controls (SeekBar) update rapidly, flooding queue Solutions • Debouncing • Discard redundant operations @skentphd
  86. Avoiding Buffering OperationManager.java: public synchronized void request(Operation operation) { //

    Replaces a call to operations.add(operation): processNewOperation(operation); if (currentOp == null) { currentOp = operations.poll(); getServer(currentOp.getServerId()).perform(currentOp); } } @skentphd
  87. Avoiding Buffering processNewOperation(Operation operation): • If operation is a ReadOperation

    • And the operations queue already contains a ReadOperation for the same server and characteristic • Then skip adding the new ReadOperation to the queue @skentphd
  88. Avoiding Buffering processNewOperation(Operation operation): • If operation is a WriteIntOperation

    • And the operations queue contains a pending WriteIntOperation for the same server and characteristic • Then delete the pending WriteIntOperation from the queue • And add the new WriteIntOperation to the queue @skentphd
  89. Long-Lived Connections Problem • Monitoring applications need connections that outlive

    app UI Solution • Use a foreground Service @skentphd
  90. ! The End @skentphd

  91. Resources: git.io/v5V2B @skentphd