Slide 1

Slide 1 text

No content

Slide 2

Slide 2 text

Stuart Kent Detroit Labs @skentphd

Slide 3

Slide 3 text

Why This Talk? • ! Smart speaker app • " Fun technology • # Frustrating Android stack • $ Scattered information Bluetooth Low Energy on Android · Stuart Kent · @skentphd

Slide 4

Slide 4 text

Content • ! BLE Basics • " Android Landscape • # Android Landmarks Bluetooth Low Energy on Android · Stuart Kent · @skentphd

Slide 5

Slide 5 text

! BLE Basics Bluetooth Low Energy on Android · Stuart Kent · @skentphd

Slide 6

Slide 6 text

BLE Basics • Small amounts of data • Easy connection • Easy communication • 100m range • Low power usage Bluetooth Low Energy on Android · Stuart Kent · @skentphd

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

Server Profiles Bluetooth Low Energy on Android · Stuart Kent · @skentphd

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

Server Profiles: Custom • For fully-custom peripherals • No UUID restrictions Bluetooth Low Energy on Android · Stuart Kent · @skentphd

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

Android Landscape Abstractions: • SweetBlue: proprietary • RxAndroidBle: no persistent connections • AndroidBluetoothLibrary • Nearby APIs: specialized feature set Bluetooth Low Energy on Android · Stuart Kent · @skentphd

Slide 17

Slide 17 text

Android Landscape Samples: • puck-central-android: complete → complex Bluetooth Low Energy on Android · Stuart Kent · @skentphd

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

Assumptions • Central role • Android 21+ • BLE only (no Classic fallbacks) • No pairing Bluetooth Low Energy on Android · Stuart Kent · @skentphd

Slide 20

Slide 20 text

Stages • Planning • Support • State • Scanning • Connecting • Interacting • Disconnecting

Slide 21

Slide 21 text

Planning Bluetooth Low Energy on Android · Stuart Kent · @skentphd

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

Support Bluetooth Low Energy on Android · Stuart Kent · @skentphd

Slide 24

Slide 24 text

Support AndroidManifest.xml: Bluetooth Low Energy on Android · Stuart Kent · @skentphd

Slide 25

Slide 25 text

Support AndroidManifest.xml: Bluetooth Low Energy on Android · Stuart Kent · @skentphd

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

State Bluetooth Low Energy on Android · Stuart Kent · @skentphd

Slide 28

Slide 28 text

State Query at any time: BluetoothAdapter.getDefaultAdapter().isLeEnabled(); Bluetooth Low Energy on Android · Stuart Kent · @skentphd

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

Scanning Bluetooth Low Energy on Android · Stuart Kent · @skentphd

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

Scanning No results! Quiet failure! Bluetooth Low Energy on Android · Stuart Kent · @skentphd

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

Scanning AndroidManifest.xml: Bluetooth Low Energy on Android · Stuart Kent · @skentphd

Slide 38

Slide 38 text

Scanning ActivityCompat.requestPermissions( this, new String[]{ACCESS_COARSE_LOCATION}, PERMISSIONS_REQUEST_ID); Bluetooth Low Energy on Android · Stuart Kent · @skentphd

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

Scanning No results! Silent failure! Bluetooth Low Energy on Android · Stuart Kent · @skentphd

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

Scanning ! Results! Bluetooth Low Energy on Android · Stuart Kent · @skentphd

Slide 43

Slide 43 text

Scanning Scanning must be stopped manually: • First result • Fixed duration • App backgrounding Bluetooth Low Energy on Android · Stuart Kent · @skentphd

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

Scanning: Fixed Duration // Clear between each scan. SortedSet 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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

Connecting Bluetooth Low Energy on Android · Stuart Kent · @skentphd

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

Interacting Bluetooth Low Energy on Android · Stuart Kent · @skentphd

Slide 52

Slide 52 text

Command Queuing • All other interactions must happen serially • Confusing due to asynchronous APIs Bluetooth Low Energy on Android · Stuart Kent · @skentphd

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

Command Queuing • Must be serial across all connected servers Bluetooth Low Energy on Android · Stuart Kent · @skentphd

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

Command Queuing • Don't call server methods immediately • Instead, submit Command to a CommandManager • Process Queue 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

Slide 60

Slide 60 text

Command Queuing public abstract class Command { public Command(String serverId) { this.serverId = serverId; } } Bluetooth Low Energy on Android · Stuart Kent · @skentphd

Slide 61

Slide 61 text

Command Queuing public class CommandManager { private Queue 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

Slide 62

Slide 62 text

Discovering Services Bluetooth Low Energy on Android · Stuart Kent · @skentphd

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

Discovering Services public class DiscoverCommand extends Command { public DiscoverCommand(String serverId) { super(serverId); } } Bluetooth Low Energy on Android · Stuart Kent · @skentphd

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

Characteristic Commands Bluetooth Low Energy on Android · Stuart Kent · @skentphd

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

Notifications • Requires remote and local configuration • Controlled by per-characteristic descriptor Bluetooth Low Energy on Android · Stuart Kent · @skentphd

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

Disconnecting Bluetooth Low Energy on Android · Stuart Kent · @skentphd

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

! The "End"! Bluetooth Low Energy on Android · Stuart Kent · @skentphd

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

Thanks! @skentphd Bluetooth Low Energy on Android · Stuart Kent · @skentphd

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

Service Caching Call refreshDeviceCache after connection succeeds and before discovering services. Bluetooth Low Energy on Android · Stuart Kent · @skentphd

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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