Bluetooth Low Energy on Android: Top Tips for the Tricky Bits v3 (droidcon London)

3a6060bc7ace07fa75791cd5dac2d46a?s=47 Stuart Kent
October 27, 2017

Bluetooth Low Energy on Android: Top Tips for the Tricky Bits v3 (droidcon London)

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://skillsmatter.com/skillscasts/10529-bluetooth-low-energy-on-android-top-tips-for-the-tricky-bits

3a6060bc7ace07fa75791cd5dac2d46a?s=128

Stuart Kent

October 27, 2017
Tweet

Transcript

  1. None
  2. Stuart Kent Detroit Labs @skentphd

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

    Fun technology • # Frustrating Android stack • $ Scattered information Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  4. Content • ! BLE Basics • " Android Landscape •

    # Android Landmarks Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  5. ! BLE Basics Bluetooth Low Energy on Android · Stuart

    Kent · @skentphd
  6. BLE Basics • Small amounts of data • Easy connection

    • Easy communication • 100m range • Low power usage Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  7. BLE Device Roles • Central • Usually acts as a

    client • Consumes data; sends commands • Peripheral • Usually acts as a server • Exposes data; responds to commands Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  8. BLE Device Roles Bluetooth Low Energy on Android · Stuart

    Kent · @skentphd
  9. BLE Device Roles Bluetooth Low Energy on Android · Stuart

    Kent · @skentphd
  10. BLE Device Roles Bluetooth Low Energy on Android · Stuart

    Kent · @skentphd
  11. Server Profiles • Servers expose characteristics. Each has: • one

    value (int, float, string) • descriptors (metadata) • Characteristics are grouped into services • Everything addressed using 128-bit UUIDs Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  12. Server Profiles Bluetooth Low Energy on Android · Stuart Kent

    · @skentphd
  13. Server Profiles: Standard • Defined by Bluetooth Special Interest Group

    (SIG) • For interoperability • 16-bit UUIDs inserted into 128-bit template UUID Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  14. Server Profiles: Custom • For fully-custom peripherals • No UUID

    restrictions Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  15. ! Android Landscape Bluetooth Low Energy on Android · Stuart

    Kent · @skentphd
  16. Android Landscape Abstractions: • SweetBlue: proprietary • RxAndroidBle: no persistent

    connections • AndroidBluetoothLibrary • Nearby APIs: specialized feature set Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  17. Android Landscape Samples: • puck-central-android: complete → complex Bluetooth Low

    Energy on Android · Stuart Kent · @skentphd
  18. ! Android Landmarks Bluetooth Low Energy on Android · Stuart

    Kent · @skentphd
  19. Assumptions • Central role • Android 21+ • BLE only

    (no Classic fallbacks) • No pairing Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  20. Stages • Planning • Support • State • Scanning •

    Connecting • Interacting • Disconnecting
  21. Planning Bluetooth Low Energy on Android · Stuart Kent ·

    @skentphd
  22. Planning • at most 7 peripheral connections per central •

    at most 15 subscriptions per peripheral (Android only) • BLE features: required or nice-to-have? Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  23. Support Bluetooth Low Energy on Android · Stuart Kent ·

    @skentphd
  24. Support AndroidManifest.xml: <uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> Bluetooth Low

    Energy on Android · Stuart Kent · @skentphd
  25. 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" /> Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  26. Support Runtime check for emulator & sideloads: boolean isBleSupported(Context context)

    { return BluetoothAdapter.getDefaultAdapter() != null && context.getPackageManager().hasSystemFeature(FEATURE_BLUETOOTH_LE); } Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  27. State Bluetooth Low Energy on Android · Stuart Kent ·

    @skentphd
  28. State Query at any time: BluetoothAdapter.getDefaultAdapter().isLeEnabled(); Bluetooth Low Energy on

    Android · Stuart Kent · @skentphd
  29. State Register for broadcasts: context.registerReceiver( receiver, new IntentFilter( BluetoothAdapter.ACTION_STATE_CHANGED)); Bluetooth

    Low Energy on Android · Stuart Kent · @skentphd
  30. State • Treat STATE_ON or STATE_BLE_ON as on • Treat

    every other state as off • Prompt with BluetoothAdapter.ACTION_REQUEST_ENABLE • Use a blocking Activity if required Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  31. Scanning Bluetooth Low Energy on Android · Stuart Kent ·

    @skentphd
  32. Scanning Get scanner using: BluetoothAdapter.getDefaultAdapter().getBluetoothLeScanner(); Warning: getDefaultAdapter null 㲗 Bluetooth

    not supported (static) getBluetoothLeScanner null 㲗 Bluetooth disabled (dynamic) Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  33. Scanning // After checking that device Bluetooth is on... bleScanner.startScan(

    filters, // Describe relevant peripherals settings, // Specify power profile scanCallback); // Process scan results Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  34. Scanning ScanCallback scanCallback = new ScanCallback() { @Override public void

    onScanResult(int callbackType, ScanResult result) { // Process single scan result here. } }; Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  35. Scanning No results! Quiet failure! Bluetooth Low Energy on Android

    · Stuart Kent · @skentphd
  36. Scanning 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 Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  37. Scanning 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" /> Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  38. Scanning ActivityCompat.requestPermissions( this, new String[]{ACCESS_COARSE_LOCATION}, PERMISSIONS_REQUEST_ID); Bluetooth Low Energy on

    Android · Stuart Kent · @skentphd
  39. Scanning Location permission request will confuse users: • Explain in

    Play Store description • Always present custom explanation dialog (ignore shouldShowRequestPermissionRationale) • Use a blocking Activity if required Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  40. Scanning No results! Silent failure! Bluetooth Low Energy on Android

    · Stuart Kent · @skentphd
  41. Scanning Location services must be enabled to receive scan results:

    • Enabled if Settings.Secure.getInt( context.getContentResolver(), LOCATION_MODE) does not return LOCATION_MODE_OFF • Prompt with ACTION_LOCATION_SOURCE_SETTINGS or LocationSettingsRequest (Play Services) • Use a blocking Activity if required Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  42. Scanning ! Results! Bluetooth Low Energy on Android · Stuart

    Kent · @skentphd
  43. Scanning Scanning must be stopped manually: • First result •

    Fixed duration • App backgrounding Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  44. Scanning: First Result 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(). } }; Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  45. Scanning: Fixed Duration // 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()); } }; Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  46. Scanning: Fixed Duration // Called after bleScanner.startScan: new Handler(Looper.getMainLooper()).postDelayed(new Runnable()

    { @Override public void run() { // Check bleScanner is not null, then: bleScanner.stopScan(scanCallback); // Process scannedDevices (display; connect). } }, scanDurationMs); Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  47. Connecting Bluetooth Low Energy on Android · Stuart Kent ·

    @skentphd
  48. Connecting // Retain; used to initiate future commands. BluetoothGatt server

    = device.connectGatt( context, false, // autoConnect; aggressive serverCallback); // Receives command results Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  49. Connecting // Receives command responses and updates 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. } Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  50. Connecting @Override public void onConnectionStateChange( BluetoothGatt server, int status, int

    newState) { if (BluetoothProfile.STATE_CONNECTED == newState) { // Connection established; begin interacting! } } Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  51. Interacting Bluetooth Low Energy on Android · Stuart Kent ·

    @skentphd
  52. Command Queuing • All other interactions must happen serially •

    Confusing due to asynchronous APIs Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  53. Command Queuing (Single Server) Incorrect server.commandA(); // Overwritten; never executes.

    server.commandB(); Correct server.commandA(); // (Wait for commandA completion callback...) server.commandB(); Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  54. Command Queuing • Must be serial across all connected servers

    Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  55. Command Queuing (Multi Server) Incorrect server1.commandA(); // Overwritten; never executes.

    server2.commandB(); Correct server1.commandA(); // (Wait for server1 commandA completion callback...) server2.commandB(); Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  56. Command Queuing Incorrect Bluetooth Low Energy on Android · Stuart

    Kent · @skentphd
  57. Command Queuing Incorrect Bluetooth Low Energy on Android · Stuart

    Kent · @skentphd
  58. Command Queuing Correct Bluetooth Low Energy on Android · Stuart

    Kent · @skentphd
  59. Command Queuing • Don't call server methods immediately • Instead,

    submit Command to a CommandManager • Process Queue<Command> one-by-one: • Next Command dispatched to correct server • Command completion reported in serverCallback • Repeat until empty Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  60. Command Queuing public abstract class Command { public Command(String serverId)

    { this.serverId = serverId; } } Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  61. Command Queuing public class CommandManager { private Queue<Command> commands =

    new LinkedList<>(); private Command command; // Command in progress; may be null. public synchronized void request(Command newCommand) { commands.add(newCommand); if (command == null) { command = commands.poll(); getServer(command.getServerId()).perform(command); } } public synchronized void reportCompleted() { command = null; if (commands.peek() != null) { command = commands.poll(); getServer(command.getServerId()).perform(command); } } } Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  62. Discovering Services Bluetooth Low Energy on Android · Stuart Kent

    · @skentphd
  63. Discovering Services • Required even for known server profile •

    Takes 1-3 seconds per peripheral • Must occur: • after connection • before any characteristic-level commands Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  64. Discovering Services public class DiscoverCommand extends Command { public DiscoverCommand(String

    serverId) { super(serverId); } } Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  65. Discovering Services // Command created and enqueued: commandManager.request(new DiscoverCommand(/* Params

    */)); // (Later...) Command dispatched and executed: server.discoverServices(); // (Later...) Command completes: @Override public void onServicesDiscovered(/* Params */) { service = server.getService(/* UUID */); // Save reference. commandManager.reportCompleted(); // Next command can begin. } Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  66. Characteristic Commands Bluetooth Low Energy on Android · Stuart Kent

    · @skentphd
  67. Reads public class ReadCommand extends Command { public ReadCommand( String

    serverId, String characteristicId) { super(serverId); this.characteristicId = characteristicId; } } Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  68. Reads // Command created and enqueued: commandManager.request(new ReadCommand(/* Params */));

    // (Later...) Command dispatched and executed: server.readCharacteristic(service.getCharacteristic(characteristicId)); // (Later...) Command completes: @Override public void onCharacteristicRead(/* Params */) { commandManager.reportCompleted(); // Next command can begin. // Process new value here. } Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  69. Writes public class WriteIntCommand extends Command { public WriteIntCommand( String

    serverId, String characteristicId, int value) { // Type should match profile! super(serverId); this.characteristicId = characteristicId; this.value = value; } } Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  70. Writes // Command created and enqueued: commandManager.request(new WriteIntCommand(/* Params */));

    // (Later...) Command dispatched and executed: BluetoothGattCharacteristic characteristic = service.getCharacteristic(characteristicId); characteristic.setValue(value, FORMAT_UINT8, 0); server.writeCharacteristic(characteristic); // (Later...) Command completes: @Override public void onCharacteristicWrite(/* Params */) { commandManager.reportCompleted(); // Next command can begin. } Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  71. Notifications • Requires remote and local configuration • Controlled by

    per-characteristic descriptor Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  72. Notifications public class EnableNotificationsCommand extends Command { public EnableNotificationsCommand( String

    serverId, String characteristicId) { super(serverId); this.characteristicId = characteristicId; } } Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  73. Notifications // Enable notifications locally after service discovery (required): server.setCharacteristicNotification(characteristic,

    true); // Command created and enqueued: commandManager.request(new EnableNotificationsCommand(/* Params */)); // (Later...) Command dispatched 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...) Command completes: @Override public void onDescriptorWrite(/* Params */) { commandManager.reportCompleted(); // Next command can begin. } // (Later...) Characteristic changes: @Override public void onCharacteristicChanged(/* Params */) { // Process new value here. } Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  74. Callback Exception Handling Problem • serverCallback methods catch all exceptions

    • Failing to report command completion stalls the queue Solutions • Catch all exceptions yourself • Report command completion in finally block Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  75. Callback Exception Handling Example @Override public void onCharacteristicRead(/* Params */)

    { try { // Happy path. } catch (Exception e) { // Sad path. } finally { // Always report command completion! commandManager.reportCompleted(); } } Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  76. Command Errors Problem • Most commands fail occasionally and obscurely

    Solutions • Assume the worst; code defensively • Tear down and reconnect if status != GATT_SUCCESS • Timebox and limit # of retries Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  77. Disconnecting Bluetooth Low Energy on Android · Stuart Kent ·

    @skentphd
  78. Disconnecting (Deliberately) • 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 Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  79. Disconnecting (Unexpectedly) Problem • Will happen (a lot) Solutions •

    Tear down (close) and reconnect • Track boolean userDisconnect; auto-retry only if false • Timebox and limit # of retries Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  80. ! The "End"! Bluetooth Low Energy on Android · Stuart

    Kent · @skentphd
  81. Where Next? • Advanced Tips slides (end of this deck)

    • service caching • buffering • long-lived connections • Resources Gist: git.io/v5V2B Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  82. Thanks! @skentphd Bluetooth Low Energy on Android · Stuart Kent

    · @skentphd
  83. ! Advanced Tips Bluetooth Low Energy on Android · Stuart

    Kent · @skentphd
  84. 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 in development builds Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  85. 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."); } } Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  86. Service Caching Call refreshDeviceCache after connection succeeds and before discovering

    services. Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  87. Buffering Problem • Commands take ~100-300ms (round trip) • Some

    UI controls (SeekBar) update rapidly, flooding queue Solutions • Debouncing • Discard redundant commands Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  88. Buffering CommandManager.java: public synchronized void request(Command newCommand) { // Replaces

    a call to commands.add(newCommand): processNewCommand(newCommand); if (command == null) { command = commands.poll(); getServer(command.getServerId()).perform(command); } } Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  89. Buffering processNewCommand(Command newCommand) logic: • If newCommand is a ReadCommand

    • And the commands queue already contains a ReadCommand for the same server and characteristic • Then skip adding the new ReadCommand to the queue Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  90. Buffering processNewCommand(Command newCommand) logic: • If newCommand is a WriteIntCommand

    • And the commands queue contains a pending WriteIntCommand for the same server and characteristic • Then delete the pending WriteIntCommand from the queue • And add the new WriteIntCommand to the queue Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  91. Long-Lived Connections Problem • Monitoring applications need connections that outlive

    app UI Solution • Use a foreground Service Bluetooth Low Energy on Android · Stuart Kent · @skentphd