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

Bluetooth Low Energy on Android: Top Tips For T...

Stuart Kent
October 21, 2017

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

Now that 90% of Android consumer devices and 100% of Android Things devices run software that supports Bluetooth Low Energy (BLE), it’s the perfect time for Android developers to dive into the Internet of Things and start building companion apps or custom smart devices. Unfortunately, Android’s Bluetooth stack has a well-deserved reputation for being difficult to work with. Join me for a journey through battle-tested strategies and code that will provide you with a roadmap for navigating the nasty parts. No prior experience with BLE is required; a gentle introduction is included.

Favorite Resources: https://gist.github.com/stkent/a7f0d6b868e805da326b112d60a9f59b

Video: pending

Stuart Kent

October 21, 2017
Tweet

More Decks by Stuart Kent

Other Decks in Technology

Transcript

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

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

    # Android Landmarks Bluetooth Low Energy on Android / Stuart Kent / @skentphd / Mobilization 2017
  3. ! BLE Basics Bluetooth Low Energy on Android / Stuart

    Kent / @skentphd / Mobilization 2017
  4. BLE Basics • Small amounts of data • Easy connection

    • Easy communication • 100m range • Low power usage Bluetooth Low Energy on Android / Stuart Kent / @skentphd / Mobilization 2017
  5. 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 / Mobilization 2017
  6. BLE Device Roles Bluetooth Low Energy on Android / Stuart

    Kent / @skentphd / Mobilization 2017
  7. BLE Device Roles Bluetooth Low Energy on Android / Stuart

    Kent / @skentphd / Mobilization 2017
  8. BLE Device Roles Bluetooth Low Energy on Android / Stuart

    Kent / @skentphd / Mobilization 2017
  9. 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 / Mobilization 2017
  10. 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 / Mobilization 2017
  11. Server Profiles: Custom • For fully-custom peripherals • No UUID

    restrictions Bluetooth Low Energy on Android / Stuart Kent / @skentphd / Mobilization 2017
  12. Abstractions • SweetBlue: proprietary • RxAndroidBle: no persistent connections •

    AndroidBluetoothLibrary: unexplored • Nearby APIs: specialized feature set If none are suitable, build from scratch! Bluetooth Low Energy on Android / Stuart Kent / @skentphd / Mobilization 2017
  13. Assumptions • Central role • Android 21+ • BLE only

    (no Classic fallbacks) • No pairing Bluetooth Low Energy on Android / Stuart Kent / @skentphd / Mobilization 2017
  14. Stages • Planning • Preparing • Scanning • Connecting •

    Interacting • Disconnecting Bluetooth Low Energy on Android / Stuart Kent / @skentphd / Mobilization 2017
  15. 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 / Mobilization 2017
  16. 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" /> Bluetooth Low Energy on Android / Stuart Kent / @skentphd / Mobilization 2017
  17. BLE 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 / Mobilization 2017
  18. BLE State • Treat STATE_ON or STATE_BLE_ON as on •

    Treat every other state as off • Prompt to enable with BluetoothAdapter.ACTION_REQUEST_ENABLE • Use a blocking Activity if required Bluetooth Low Energy on Android / Stuart Kent / @skentphd / Mobilization 2017
  19. 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 / Mobilization 2017
  20. 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 / Mobilization 2017
  21. 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 / Mobilization 2017
  22. Scanning No results! Quiet failure! Bluetooth Low Energy on Android

    / Stuart Kent / @skentphd / Mobilization 2017
  23. 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 / Mobilization 2017
  24. 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 / Mobilization 2017
  25. 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 / Mobilization 2017
  26. Scanning No results! Silent failure! Bluetooth Low Energy on Android

    / Stuart Kent / @skentphd / Mobilization 2017
  27. Scanning Location services must be enabled to receive scan results:

    • Enabled whenever Settings.Secure.getInt( context.getContentResolver(), LOCATION_MODE) does not return LOCATION_MODE_OFF • Prompt to enable with ACTION_LOCATION_SOURCE_SETTINGS or LocationSettingsRequest (Play Services) • Use a blocking Activity if required Bluetooth Low Energy on Android / Stuart Kent / @skentphd / Mobilization 2017
  28. Scanning Scanning must be stopped manually: • First result •

    Fixed duration • App backgrounding Bluetooth Low Energy on Android / Stuart Kent / @skentphd / Mobilization 2017
  29. 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 / Mobilization 2017
  30. 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 / Mobilization 2017
  31. Scanning: Fixed Duration // 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); Bluetooth Low Energy on Android / Stuart Kent / @skentphd / Mobilization 2017
  32. Connecting // Retain; used to initiate future commands. BluetoothGatt server

    = device.connectGatt( context, false, // autoConnect; safest set to false? serverCallback); Bluetooth Low Energy on Android / Stuart Kent / @skentphd / Mobilization 2017
  33. 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 / Mobilization 2017
  34. 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 / Mobilization 2017
  35. Command Queuing • All other interactions must happen serially •

    Confusing due to asynchronous APIs Bluetooth Low Energy on Android / Stuart Kent / @skentphd / Mobilization 2017
  36. 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 / Mobilization 2017
  37. Command Queuing • Must be serial across all connected devices

    Bluetooth Low Energy on Android / Stuart Kent / @skentphd / Mobilization 2017
  38. 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 / Mobilization 2017
  39. Command Queuing public abstract class Command { public Command(String serverId)

    { this.serverId = serverId; } } Bluetooth Low Energy on Android / Stuart Kent / @skentphd / Mobilization 2017
  40. Command Queuing • Don't call server methods immediately • Instead,

    submit Command instances to a CommandManager • Process Queue<Command> one-by-one: • Manager dispatches next Command to correct server • Command completion reported by serverCallback • Repeat while queue is non-empty Bluetooth Low Energy on Android / Stuart Kent / @skentphd / Mobilization 2017
  41. 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 / Mobilization 2017
  42. Discovering Services • Necessary even though you know 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 / Mobilization 2017
  43. Discovering Services public class DiscoverCommand extends Command { public DiscoverCommand(String

    serverId) { super(serverId); } } Bluetooth Low Energy on Android / Stuart Kent / @skentphd / Mobilization 2017
  44. 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 operation can begin. } Bluetooth Low Energy on Android / Stuart Kent / @skentphd / Mobilization 2017
  45. 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 / Mobilization 2017
  46. 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 operation can begin. // Process new value here. } Bluetooth Low Energy on Android / Stuart Kent / @skentphd / Mobilization 2017
  47. 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 / Mobilization 2017
  48. 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 operation can begin. } Bluetooth Low Energy on Android / Stuart Kent / @skentphd / Mobilization 2017
  49. Notifications • Requires remote and local configuration • Controlled by

    per-characteristic descriptor Bluetooth Low Energy on Android / Stuart Kent / @skentphd / Mobilization 2017
  50. 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 / Mobilization 2017
  51. 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 operation can begin. } // (Later...) Characteristic changes: @Override public void onCharacteristicChanged(/* Params */) { // Process new value here. } Bluetooth Low Energy on Android / Stuart Kent / @skentphd / Mobilization 2017
  52. Callback Exception Handling • serverCallback methods catch and log all

    exceptions • Failing to report command completion stalls the queue • Catch all exceptions yourself • Report command completion in finally block Bluetooth Low Energy on Android / Stuart Kent / @skentphd / Mobilization 2017
  53. 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 / Mobilization 2017
  54. Command Errors • Most commands fail occasionally and obscurely •

    Assume the worst; code defensively • Tear down and reconnect if status != GATT_SUCCESS • Timebox and limit number of retries Bluetooth Low Energy on Android / Stuart Kent / @skentphd / Mobilization 2017
  55. 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 / Mobilization 2017
  56. Disconnecting (Unexpectedly) • Will happen (a lot) • Tear down

    (close) and start over • Track boolean userDisconnect; auto-retry only if false • Timebox and limit number of retries Bluetooth Low Energy on Android / Stuart Kent / @skentphd / Mobilization 2017
  57. ! The "End"! Bluetooth Low Energy on Android / Stuart

    Kent / @skentphd / Mobilization 2017
  58. 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 / Mobilization 2017
  59. ! Advanced Tips Bluetooth Low Energy on Android / Stuart

    Kent / @skentphd / Mobilization 2017
  60. 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 / Mobilization 2017
  61. 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 / Mobilization 2017
  62. Service Caching Call refreshDeviceCache after connection succeeds and before discovering

    services. Bluetooth Low Energy on Android / Stuart Kent / @skentphd / Mobilization 2017
  63. 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 / Mobilization 2017
  64. 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 / Mobilization 2017
  65. 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 / Mobilization 2017
  66. 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 / Mobilization 2017
  67. 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 / Mobilization 2017