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

Bluetooth Low Energy on Android: Top Tips for the Tricky Bits v4 (CodeMash)

Stuart Kent
January 11, 2018

Bluetooth Low Energy on Android: Top Tips for the Tricky Bits v4 (CodeMash)

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

Stuart Kent

January 11, 2018
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
  2. Content • ! BLE Basics • " Android Landscape •

    # Android Landmarks Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  3. BLE Basics • Small amounts of data • Easy connection

    • Easy communication • 100m range • Low power usage Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  4. 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
  5. Server Profiles • Servers expose characteristics: • one value (int,

    float, string) • one or more descriptors (metadata) • Characteristics grouped into services • Everything addressed by 128-bit UUIDs Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  6. 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
  7. Server Profiles: Custom • For fully-custom peripherals • No UUID

    restrictions Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  8. Android Landscape Abstractions: • SweetBlue: proprietary • RxAndroidBle: tricky state

    management • Nearby APIs: specialized feature set • Others: unexplored! Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  9. Assumptions • Central role • Android 21+ • BLE only

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

    Connecting • Interacting • Disconnecting
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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
  22. Scanning Scanning must be stopped manually: • First result •

    Fixed duration • App backgrounding Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. Command Queuing • All other interactions must happen serially •

    Confusing due to asynchronous APIs Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  30. 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
  31. Command Queuing • Must be serial across all connected servers

    Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  32. 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
  33. 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
  34. Command Queuing public abstract class Command { public Command(String serverId)

    { this.serverId = serverId; } } Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  35. 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
  36. 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
  37. Discovering Services public class DiscoverCommand extends Command { public DiscoverCommand(String

    serverId) { super(serverId); } } Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  38. 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
  39. 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
  40. 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
  41. 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
  42. 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
  43. Notifications • Requires remote and local configuration • Controlled by

    per-characteristic descriptor Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  44. 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
  45. 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
  46. 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
  47. 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
  48. Command Errors Problem • Most commands fail occasionally and obscurely

    Solutions • Code defensively • Tear down and reconnect if status != GATT_SUCCESS (some exceptions, e.g. expected auth errors). • Timebox and limit # of retries Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  49. 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
  50. 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
  51. 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
  52. 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
  53. 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
  54. Service Caching Call refreshDeviceCache after connection succeeds and before discovering

    services. Bluetooth Low Energy on Android · Stuart Kent · @skentphd
  55. 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
  56. 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
  57. 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
  58. 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
  59. 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