package com.adafruit.bluefruit.le.connect.ble; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.os.Handler; import android.os.Looper; import android.util.Log; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.UUID; public class BleDevicesScanner { private static final String TAG = BleDevicesScanner.class.getSimpleName(); private static final long kScanPeriod = 20 * 1000; // scan period in milliseconds // Data private final BluetoothAdapter mBluetoothAdapter; private volatile boolean mIsScanning = false; private Handler mHandler; private List<UUID> mServicesToDiscover; private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); private final LeScansPoster mLeScansPoster; // private BluetoothAdapter.LeScanCallback mLeScanCallback = new BluetoothAdapter.LeScanCallback() { @Override public void onLeScan(final BluetoothDevice device, final int rssi, final byte[] scanRecord) { synchronized (mLeScansPoster) { if (mServicesToDiscover == null || !Collections.disjoint(parseUuids(scanRecord), mServicesToDiscover)) { // only process the devices with uuids in mServicesToDiscover mLeScansPoster.set(device, rssi, scanRecord); mMainThreadHandler.post(mLeScansPoster); } } } }; public BleDevicesScanner(BluetoothAdapter adapter, UUID[] servicesToDiscover, BluetoothAdapter.LeScanCallback callback) { mBluetoothAdapter = adapter; mServicesToDiscover = servicesToDiscover == null ? null : Arrays.asList(servicesToDiscover); mLeScansPoster = new LeScansPoster(callback); mHandler = new Handler(); } public void start() { if (kScanPeriod > 0) { // Stops scanning after a pre-defined scan period. mHandler.postDelayed(new Runnable() { @Override public void run() { if (mIsScanning) { Log.d(TAG, "Scan timer expired. Restart scan"); stop(); start(); } } }, kScanPeriod); } mIsScanning = true; Log.d(TAG, "start scanning"); mBluetoothAdapter.startLeScan(mLeScanCallback); } public void stop() { if (mIsScanning) { mHandler.removeCallbacksAndMessages(null); // cancel pending calls to stop mIsScanning = false; mBluetoothAdapter.stopLeScan(mLeScanCallback); Log.d(TAG, "stop scanning"); } } public boolean isScanning() { return mIsScanning; } private static class LeScansPoster implements Runnable { private final BluetoothAdapter.LeScanCallback leScanCallback; private BluetoothDevice device; private int rssi; private byte[] scanRecord; private LeScansPoster(BluetoothAdapter.LeScanCallback leScanCallback) { this.leScanCallback = leScanCallback; } public void set(BluetoothDevice device, int rssi, byte[] scanRecord) { this.device = device; this.rssi = rssi; this.scanRecord = scanRecord; } @Override public void run() { leScanCallback.onLeScan(device, rssi, scanRecord); } } // Filtering by custom UUID is broken in Android 4.3 and 4.4, see: // http://stackoverflow.com/questions/18019161/startlescan-with-128-bit-uuids-doesnt-work-on-native-android-ble-implementation?noredirect=1#comment27879874_18019161 // This is a workaround function from the SO thread to manually parse advertisement data. private List<UUID> parseUuids(byte[] advertisedData) { List<UUID> uuids = new ArrayList<>(); ByteBuffer buffer = ByteBuffer.wrap(advertisedData).order(ByteOrder.LITTLE_ENDIAN); while (buffer.remaining() > 2) { byte length = buffer.get(); if (length == 0) break; byte type = buffer.get(); switch (type) { case 0x02: // Partial list of 16-bit UUIDs case 0x03: // Complete list of 16-bit UUIDs while (length >= 2) { uuids.add(UUID.fromString(String.format("%08x-0000-1000-8000-00805f9b34fb", buffer.getShort()))); length -= 2; } break; case 0x06: // Partial list of 128-bit UUIDs case 0x07: // Complete list of 128-bit UUIDs while (length >= 16) { long lsb = buffer.getLong(); long msb = buffer.getLong(); uuids.add(new UUID(msb, lsb)); length -= 16; } break; default: buffer.position(buffer.position() + length - 1); break; } } return uuids; } }